Skip to main content

ling/runtime/
mod.rs

1// src/runtime/mod.rs — tree-walking interpreter with graphics support
2#[cfg(not(target_arch = "wasm32"))]
3mod ai;
4#[cfg(not(target_arch = "wasm32"))]
5mod gamepad;
6pub(crate) mod jit_abi;
7
8/// Initialize the AOT/JIT runtime. Must be called before any AOT-compiled code.
9/// Creates a new interpreter instance for runtime function dispatch.
10pub fn init_aot_runtime() {
11    let interp = Interpreter::new();
12    jit_abi::init(interp);
13}
14#[cfg(not(target_arch = "wasm32"))]
15mod net;
16use crate::gfx::{GfxState, Light};
17use crate::parser::ast::*;
18use std::cell::RefCell;
19use std::collections::HashMap;
20// `raster` is wasm-safe (pure CPU framebuffer), so `draw_line` is available on
21// web too; `fill_triangle` is only reached from native-gated 3-D fill paths.
22use crate::gfx::raster::draw_line;
23#[cfg(not(target_arch = "wasm32"))]
24use crate::gfx::raster::fill_triangle;
25#[cfg(not(target_arch = "wasm32"))]
26use ling_audio::{AudioEngine, ToneParams, Wave};
27
28#[cfg(not(target_arch = "wasm32"))]
29use ling_audio::FftAnalyzer;
30
31#[cfg(not(target_arch = "wasm32"))]
32use ling_mic;
33
34// ─── Values ──────────────────────────────────────────────────────────────────
35
36#[derive(Debug, Clone)]
37pub enum Value {
38    Str(String),
39    Number(f64),
40    Bool(bool),
41    Unit,
42    List(Vec<Value>),
43    Ok(Box<Value>),
44    Err(Box<Value>),
45    Fn(Vec<String>, Vec<Stmt>, Env),
46    /// `form` record instance — ordered named fields.
47    Struct {
48        name: String,
49        fields: Vec<(String, Value)>,
50    },
51    /// `choose` enum instance — variant tag plus ordered payload.
52    Variant {
53        enum_name: String,
54        variant: String,
55        payload: Vec<Value>,
56    },
57}
58
59type Env = HashMap<String, Value>;
60
61impl std::fmt::Display for Value {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            Value::Str(s) => write!(f, "{s}"),
65            Value::Number(n) => {
66                if n.fract() == 0.0 && n.abs() < 1e15 {
67                    write!(f, "{}", *n as i64)
68                } else {
69                    write!(f, "{n}")
70                }
71            },
72            Value::Bool(b) => write!(f, "{b}"),
73            Value::Unit => write!(f, "()"),
74            Value::List(v) => {
75                write!(f, "[")?;
76                for (i, x) in v.iter().enumerate() {
77                    if i > 0 {
78                        write!(f, ", ")?;
79                    }
80                    write!(f, "{x}")?;
81                }
82                write!(f, "]")
83            },
84            Value::Ok(v) => write!(f, "Ok({v})"),
85            Value::Err(v) => write!(f, "Err({v})"),
86            Value::Fn(_, _, _) => write!(f, "<fn>"),
87            Value::Struct { name, fields } => {
88                write!(f, "{name} {{ ")?;
89                for (i, (k, v)) in fields.iter().enumerate() {
90                    if i > 0 {
91                        write!(f, ", ")?;
92                    }
93                    write!(f, "{k}: {v}")?;
94                }
95                write!(f, " }}")
96            },
97            Value::Variant { variant, payload, .. } => {
98                write!(f, "{variant}")?;
99                if !payload.is_empty() {
100                    write!(f, "(")?;
101                    for (i, v) in payload.iter().enumerate() {
102                        if i > 0 {
103                            write!(f, ", ")?;
104                        }
105                        write!(f, "{v}")?;
106                    }
107                    write!(f, ")")?;
108                }
109                Ok(())
110            },
111        }
112    }
113}
114
115// ─── Control flow ────────────────────────────────────────────────────────────
116
117#[derive(Debug)]
118pub(crate) enum EvalErr {
119    Runtime(String),
120    Return(Value),
121    #[allow(dead_code)] // reserved for future `break` statement support
122    Break,
123}
124
125impl From<String> for EvalErr {
126    fn from(s: String) -> Self {
127        EvalErr::Runtime(s)
128    }
129}
130
131type EvalResult = Result<Value, EvalErr>;
132
133// GfxState is now defined in crate::gfx — see src/gfx/mod.rs.
134
135// ─── SVG writer ───────────────────────────────────────────────────────────────
136
137struct SvgWriter {
138    path: String,
139    width: f64,
140    height: f64,
141    elements: Vec<String>,
142}
143
144impl SvgWriter {
145    fn new(path: String, width: f64, height: f64) -> Self {
146        let bg = format!("<rect width=\"{width}\" height=\"{height}\" fill=\"#0a0a0a\"/>");
147        Self { path, width, height, elements: vec![bg] }
148    }
149
150    fn save(&self) -> std::io::Result<()> {
151        let w = self.width;
152        let h = self.height;
153        let mut out = format!(
154            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
155             <svg xmlns=\"http://www.w3.org/2000/svg\" \
156             width=\"{w}\" height=\"{h}\" viewBox=\"0 0 {w} {h}\">\n"
157        );
158        for elem in &self.elements {
159            out.push_str("  ");
160            out.push_str(elem);
161            out.push('\n');
162        }
163        out.push_str("</svg>\n");
164        // Create parent directory if it doesn't exist.
165        if let Some(parent) = std::path::Path::new(&self.path).parent() {
166            if !parent.as_os_str().is_empty() {
167                let _ = std::fs::create_dir_all(parent);
168            }
169        }
170        std::fs::write(&self.path, out.as_bytes())
171    }
172}
173
174fn hsl_to_hex(h: f64, s: f64, l: f64) -> String {
175    let s = s / 100.0;
176    let l = l / 100.0;
177    let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
178    let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
179    let m = l - c / 2.0;
180    let (r1, g1, b1) = if h < 60.0 {
181        (c, x, 0.0)
182    } else if h < 120.0 {
183        (x, c, 0.0)
184    } else if h < 180.0 {
185        (0.0, c, x)
186    } else if h < 240.0 {
187        (0.0, x, c)
188    } else if h < 300.0 {
189        (x, 0.0, c)
190    } else {
191        (c, 0.0, x)
192    };
193    let r = ((r1 + m) * 255.0).round() as u8;
194    let g = ((g1 + m) * 255.0).round() as u8;
195    let b = ((b1 + m) * 255.0).round() as u8;
196    format!("#{r:02x}{g:02x}{b:02x}")
197}
198
199// ─── Procedural texture helpers ───────────────────────────────────────────────
200
201fn tex_hash(x: i32, y: i32, seed: u32) -> f32 {
202    let mut h = (x as u32)
203        .wrapping_add((y as u32).wrapping_mul(2654435769))
204        .wrapping_add(seed.wrapping_mul(1234567891));
205    h ^= h >> 16;
206    h = h.wrapping_mul(0x45d9f3b);
207    h ^= h >> 16;
208    h as f32 / u32::MAX as f32
209}
210
211fn tex_vnoise(x: f32, y: f32, seed: u32) -> f32 {
212    let xi = x.floor() as i32;
213    let yi = y.floor() as i32;
214    let sm = |t: f32| t * t * (3.0 - 2.0 * t);
215    let xf = sm(x - xi as f32);
216    let yf = sm(y - yi as f32);
217    let a = tex_hash(xi, yi, seed);
218    let b = tex_hash(xi + 1, yi, seed);
219    let c = tex_hash(xi, yi + 1, seed);
220    let d = tex_hash(xi + 1, yi + 1, seed);
221    a + (b - a) * xf + (c - a) * yf + (a - b - c + d) * xf * yf
222}
223
224fn tex_fbm(x: f32, y: f32, octaves: u32, seed: u32) -> f32 {
225    let mut v = 0.0f32;
226    let mut amp = 0.5f32;
227    let mut f = 1.0f32;
228    for i in 0..octaves {
229        v += tex_vnoise(x * f, y * f, seed.wrapping_add(i * 7919)) * amp;
230        amp *= 0.5;
231        f *= 2.0;
232    }
233    v
234}
235
236fn tex_palette(name: &str, t: f32) -> [f32; 3] {
237    let (a, b, c, d): ([f32; 3], [f32; 3], [f32; 3], [f32; 3]) = match name {
238        "fire" => (
239            [0.8, 0.4, 0.1],
240            [0.7, 0.3, 0.1],
241            [1.0, 0.5, 0.3],
242            [0.0, 0.5, 0.8],
243        ),
244        "ocean" => (
245            [0.1, 0.4, 0.7],
246            [0.3, 0.3, 0.4],
247            [0.8, 1.0, 0.5],
248            [0.3, 0.0, 0.6],
249        ),
250        "psychedelic" => (
251            [0.5, 0.5, 0.5],
252            [0.8, 0.8, 0.8],
253            [1.0, 1.3, 0.7],
254            [0.0, 0.15, 0.3],
255        ),
256        "neon" => (
257            [0.5, 0.5, 0.5],
258            [0.5, 0.5, 0.5],
259            [2.0, 1.0, 0.0],
260            [0.5, 0.2, 0.25],
261        ),
262        "forest" => (
263            [0.3, 0.5, 0.2],
264            [0.2, 0.3, 0.1],
265            [1.0, 0.5, 0.8],
266            [0.1, 0.3, 0.6],
267        ),
268        _ => (
269            [0.5, 0.5, 0.5],
270            [0.5, 0.5, 0.5],
271            [1.0, 1.0, 1.0],
272            [0.0, 0.333, 0.667],
273        ),
274    };
275    [0, 1, 2]
276        .map(|i| (a[i] + b[i] * (std::f32::consts::TAU * (c[i] * t + d[i])).cos()).clamp(0.0, 1.0))
277}
278
279/// Map a physical key to a typed character for ling-ui text input (lowercase).
280#[cfg(not(target_arch = "wasm32"))]
281fn key_char(k: minifb::Key) -> Option<char> {
282    use minifb::Key::*;
283    Some(match k {
284        A => 'a',
285        B => 'b',
286        C => 'c',
287        D => 'd',
288        E => 'e',
289        F => 'f',
290        G => 'g',
291        H => 'h',
292        I => 'i',
293        J => 'j',
294        K => 'k',
295        L => 'l',
296        M => 'm',
297        N => 'n',
298        O => 'o',
299        P => 'p',
300        Q => 'q',
301        R => 'r',
302        S => 's',
303        T => 't',
304        U => 'u',
305        V => 'v',
306        W => 'w',
307        X => 'x',
308        Y => 'y',
309        Z => 'z',
310        Key0 => '0',
311        Key1 => '1',
312        Key2 => '2',
313        Key3 => '3',
314        Key4 => '4',
315        Key5 => '5',
316        Key6 => '6',
317        Key7 => '7',
318        Key8 => '8',
319        Key9 => '9',
320        Space => ' ',
321        Minus => '-',
322        Period => '.',
323        _ => return None,
324    })
325}
326
327/// Lowercase-hex encode bytes (the wire format for crypto values in Ling).
328fn hex_encode(bytes: &[u8]) -> String {
329    let mut s = String::with_capacity(bytes.len() * 2);
330    for b in bytes {
331        s.push_str(&format!("{b:02x}"));
332    }
333    s
334}
335
336/// Decode a `ling convert` blob: base64 → zlib-inflate → raw little-endian bytes.
337#[cfg(not(target_arch = "wasm32"))]
338fn decode_blob(s: &str) -> Result<Vec<u8>, String> {
339    use base64::Engine as _;
340    use std::io::Read as _;
341    let comp = base64::engine::general_purpose::STANDARD
342        .decode(s.trim())
343        .map_err(|e| format!("base64: {e}"))?;
344    let mut out = Vec::new();
345    flate2::read::ZlibDecoder::new(&comp[..])
346        .read_to_end(&mut out)
347        .map_err(|e| format!("inflate: {e}"))?;
348    Ok(out)
349}
350
351/// Decode a lowercase/uppercase hex string to bytes (ignores malformed tail).
352fn hex_decode(s: &str) -> Vec<u8> {
353    let s = s.trim();
354    (0..s.len() / 2)
355        .filter_map(|i| u8::from_str_radix(s.get(i * 2..i * 2 + 2)?, 16).ok())
356        .collect()
357}
358
359/// Decode a hex string into a fixed 32-byte key (zero-padded / truncated).
360fn hex_to_32(s: &str) -> [u8; 32] {
361    let v = hex_decode(s);
362    let mut out = [0u8; 32];
363    let n = v.len().min(32);
364    out[..n].copy_from_slice(&v[..n]);
365    out
366}
367
368fn tex_rgb(r: f32, g: f32, b: f32) -> u32 {
369    ((r * 255.0) as u32) << 16 | ((g * 255.0) as u32) << 8 | (b * 255.0) as u32
370}
371
372// ─── 3D Perlin Noise (Improved Perlin 2002) ───────────────────────────────────
373
374const PERM: [u8; 512] = [
375    151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69,
376    142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219,
377    203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 35, 63, 189, 114, 56, 42, 123,
378    165, 38, 72, 93, 69, 139, 138, 78, 149, 159, 56, 89, 152, 78, 61, 140, 63, 26, 142, 76, 124,
379    132, 72, 11, 90, 44, 82, 59, 96, 41, 148, 126, 157, 13, 49, 27, 176, 33, 47, 14, 97, 78, 71,
380    40, 87, 183, 4, 122, 92, 7, 72, 3, 246, 17, 225, 87, 91, 106, 203, 190, 57, 74, 76, 88, 207,
381    208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146,
382    157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196,
383    167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94,
384    11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55,
385    109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180,
386    198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185,
387    134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223,
388    140, 161, 137, 13, 191, 230, 66, 104, 153, 199, 167, 147, 99, 179, 92,
389    // Duplicate for wrap-around indexing
390    151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69,
391    142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219,
392    203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 35, 63, 189, 114, 56, 42, 123,
393    165, 38, 72, 93, 69, 139, 138, 78, 149, 159, 56, 89, 152, 78, 61, 140, 63, 26, 142, 76, 124,
394    132, 72, 11, 90, 44, 82, 59, 96, 41, 148, 126, 157, 13, 49, 27, 176, 33, 47, 14, 97, 78, 71,
395    40, 87, 183, 4, 122, 92, 7, 72, 3, 246, 17, 225, 87, 91, 106, 203, 190, 57, 74, 76, 88, 207,
396    208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146,
397    157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196,
398    167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94,
399    11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55,
400    109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174,
401];
402
403fn fade(t: f32) -> f32 {
404    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
405}
406
407fn grad(hash: u8, x: f32, y: f32, z: f32) -> f32 {
408    let h = hash & 15;
409    let u = if h < 8 { x } else { y };
410    let v = if h < 8 { y } else { z };
411    (if (h & 1) == 0 { u } else { -u }) + (if (h & 2) == 0 { v } else { -v })
412}
413
414fn perlin3(x: f32, y: f32, z: f32) -> f32 {
415    let xi = (x.floor() as i32) & 255;
416    let yi = (y.floor() as i32) & 255;
417    let zi = (z.floor() as i32) & 255;
418
419    let xf = x - x.floor();
420    let yf = y - y.floor();
421    let zf = z - z.floor();
422
423    let u = fade(xf);
424    let v = fade(yf);
425    let w = fade(zf);
426
427    let p0 = PERM[xi as usize] as usize;
428    let p1 = PERM[((xi + 1) & 255) as usize] as usize;
429    let pa = PERM[(p0 + yi as usize) & 255] as usize;
430    let pb = PERM[(p0 + ((yi + 1) & 255) as usize) & 255] as usize;
431    let pc = PERM[(p1 + yi as usize) & 255] as usize;
432    let pd = PERM[(p1 + ((yi + 1) & 255) as usize) & 255] as usize;
433
434    let g000 = grad(PERM[(pa + zi as usize) & 255], xf, yf, zf);
435    let g001 = grad(
436        PERM[(pa + ((zi + 1) & 255) as usize) & 255],
437        xf,
438        yf,
439        zf - 1.0,
440    );
441    let g010 = grad(PERM[(pb + zi as usize) & 255], xf, yf - 1.0, zf);
442    let g011 = grad(
443        PERM[(pb + ((zi + 1) & 255) as usize) & 255],
444        xf,
445        yf - 1.0,
446        zf - 1.0,
447    );
448    let g100 = grad(PERM[(pc + zi as usize) & 255], xf - 1.0, yf, zf);
449    let g101 = grad(
450        PERM[(pc + ((zi + 1) & 255) as usize) & 255],
451        xf - 1.0,
452        yf,
453        zf - 1.0,
454    );
455    let g110 = grad(PERM[(pd + zi as usize) & 255], xf - 1.0, yf - 1.0, zf);
456    let g111 = grad(
457        PERM[(pd + ((zi + 1) & 255) as usize) & 255],
458        xf - 1.0,
459        yf - 1.0,
460        zf - 1.0,
461    );
462
463    let l00 = g000 + u * (g100 - g000);
464    let l01 = g001 + u * (g101 - g001);
465    let l10 = g010 + u * (g110 - g010);
466    let l11 = g011 + u * (g111 - g011);
467
468    let l0 = l00 + v * (l10 - l00);
469    let l1 = l01 + v * (l11 - l01);
470
471    l0 + w * (l1 - l0)
472}
473
474fn fast_rand_f64(state: &mut u64) -> f64 {
475    *state = state
476        .wrapping_mul(6364136223846793005)
477        .wrapping_add(1442695040888963407);
478    ((*state >> 32) as u32) as f64 / 4294967296.0
479}
480
481// ─── Circle Drawing Primitives ────────────────────────────────────────────────
482
483/// Write one pixel into the framebuffer (normal or additive blend).
484#[inline]
485fn put_px(buf: &mut [u32], idx: usize, color: u32, blend: u8) {
486    if idx >= buf.len() {
487        return;
488    }
489    if blend == 0 {
490        buf[idx] = color;
491    } else {
492        let old = buf[idx];
493        let r = (((old >> 16) & 255) + ((color >> 16) & 255)).min(255);
494        let g = (((old >> 8) & 255) + ((color >> 8) & 255)).min(255);
495        let b = ((old & 255) + (color & 255)).min(255);
496        buf[idx] = (r << 16) | (g << 8) | b;
497    }
498}
499
500/// Pack three float colour channels (0..255) into a 0x00RRGGBB word, clamping.
501#[inline]
502fn rgb(r: f64, g: f64, b: f64) -> u32 {
503    let r = (r as i64).clamp(0, 255) as u32;
504    let g = (g as i64).clamp(0, 255) as u32;
505    let b = (b as i64).clamp(0, 255) as u32;
506    (r << 16) | (g << 8) | b
507}
508
509fn draw_circle_outline(
510    buf: &mut [u32],
511    w: i32,
512    h: i32,
513    cx: i32,
514    cy: i32,
515    r: i32,
516    color: u32,
517    blend: u8,
518) {
519    let r = r.clamp(0, 20000); // guard against overflow / runaway from tiny depths
520    if r == 0 {
521        return;
522    }
523    let mut x = 0;
524    let mut y = r;
525    let mut d = 3 - 2 * r;
526    while x <= y {
527        plot_circle_points(buf, w, h, cx, cy, x, y, color, blend);
528        if d < 0 {
529            d += 4 * x + 6;
530        } else {
531            d += 4 * (x - y) + 10;
532            y -= 1;
533        }
534        x += 1;
535    }
536}
537
538fn plot_circle_points(
539    buf: &mut [u32],
540    w: i32,
541    h: i32,
542    cx: i32,
543    cy: i32,
544    x: i32,
545    y: i32,
546    color: u32,
547    blend: u8,
548) {
549    let points = [
550        (cx + x, cy + y),
551        (cx - x, cy + y),
552        (cx + x, cy - y),
553        (cx - x, cy - y),
554        (cx + y, cy + x),
555        (cx - y, cy + x),
556        (cx + y, cy - x),
557        (cx - y, cy - x),
558    ];
559    for &(px, py) in &points {
560        if px >= 0 && px < w && py >= 0 && py < h {
561            put_px(buf, (py * w + px) as usize, color, blend);
562        }
563    }
564}
565
566fn draw_circle_filled(
567    buf: &mut [u32],
568    w: i32,
569    h: i32,
570    cx: i32,
571    cy: i32,
572    r: i32,
573    color: u32,
574    blend: u8,
575) {
576    if r <= 0 {
577        return;
578    }
579    for dy in -r..=r {
580        let dx_max = ((r * r - dy * dy) as f64).sqrt() as i32;
581        let py = cy + dy;
582        if py < 0 || py >= h {
583            continue;
584        }
585        for dx in -dx_max..=dx_max {
586            let px = cx + dx;
587            if px >= 0 && px < w {
588                put_px(buf, (py * w + px) as usize, color, blend);
589            }
590        }
591    }
592}
593
594#[cfg(test)]
595mod draw_tests {
596    use super::*;
597
598    #[test]
599    fn filled_circle_actually_writes_pixels() {
600        let mut buf = vec![0u32; 100 * 100];
601        draw_circle_filled(&mut buf, 100, 100, 50, 50, 10, 0xFF00FF, 0);
602        assert_eq!(buf[50 * 100 + 50], 0xFF00FF, "centre pixel must be filled");
603        assert_eq!(buf[0], 0, "far corner must stay clear");
604        let n = buf.iter().filter(|&&p| p != 0).count();
605        assert!(n > 200 && n < 500, "r=10 disc area ≈ 314, got {n}");
606    }
607
608    #[test]
609    fn circle_outline_writes_a_ring() {
610        let mut buf = vec![0u32; 100 * 100];
611        draw_circle_outline(&mut buf, 100, 100, 50, 50, 20, 0x00FF00, 0);
612        assert_eq!(buf[50 * 100 + 50], 0, "outline must NOT fill the centre");
613        assert!(
614            buf.iter().any(|&p| p == 0x00FF00),
615            "outline must draw a ring"
616        );
617    }
618
619    #[test]
620    fn additive_blend_accumulates_channels() {
621        let mut buf = vec![0x202020u32; 1];
622        put_px(&mut buf, 0, 0x404040, 1);
623        assert_eq!(buf[0], 0x606060);
624    }
625}
626
627// ─── Interpreter ─────────────────────────────────────────────────────────────
628
629/// Customizable colour palette for the vector UI toolkit (packed 0x00RRGGBB).
630/// `ui_theme(...)` sets it; every widget falls back to these and accepts a
631/// trailing `r,g,b` override.
632#[derive(Clone, Copy)]
633pub struct UiTheme {
634    pub primary: u32,
635    pub accent: u32,
636    pub track: u32,
637    pub warn: u32,
638    pub text: u32,
639    pub bg: u32,
640}
641
642impl Default for UiTheme {
643    fn default() -> Self {
644        Self {
645            primary: 0x00D2FF, // holographic cyan
646            accent: 0x28FFB4,  // mint
647            track: 0x2C3E64,   // dim slate
648            warn: 0xFF5A5A,    // alert red
649            text: 0xBEEBFF,    // pale cyan
650            bg: 0x0A1018,      // near-black panel
651        }
652    }
653}
654
655pub struct Interpreter {
656    globals: HashMap<String, Expr>,
657    /// Globals evaluated ONCE at program start (immutable after load).
658    /// call_named clones this instead of re-evaluating every global per call.
659    global_seed: Env,
660    functions: HashMap<String, FnDef>,
661    /// `form` definitions: struct name → ordered field names.
662    pub(crate) structs: HashMap<String, Vec<String>>,
663    /// `choose` variants: variant name (bare and `Enum::Variant`) → (enum name, arity).
664    enum_variants: HashMap<String, (String, usize)>,
665    _modules: HashMap<String, Vec<FnDef>>,
666    gfx: RefCell<GfxState>,
667    svg: RefCell<Option<SvgWriter>>,
668    /// Directory of the primary source file, for relative `use` resolution.
669    pub source_dir: Option<std::path::PathBuf>,
670    /// Files already loaded — prevents circular imports.
671    loaded_files: std::collections::HashSet<String>,
672    /// Optional audio engine — `None` if no audio device is available.
673    #[cfg(not(target_arch = "wasm32"))]
674    audio: Option<AudioEngine>,
675    #[cfg(not(target_arch = "wasm32"))]
676    fft: RefCell<FftAnalyzer>,
677    fft_bands_cache: RefCell<Vec<f32>>,
678    /// Real-time clock — initialized at startup
679    start_time: std::time::Instant,
680    /// Frame counter — incremented at each present()
681    frame_num: u64,
682    /// Random state for rand() builtin (xorshift)
683    rand_state: u64,
684    /// Microphone input (Phase 1 audio reactivity)
685    #[cfg(not(target_arch = "wasm32"))]
686    mic: Option<ling_mic::MicInput>,
687    /// Persistent KEM keypairs (knot / hybrid identities), referenced by handle.
688    #[cfg(not(target_arch = "wasm32"))]
689    crypto_ids: Vec<ling_crypto::KnotIdentity>,
690    /// Editable text-input buffer (ling-ui text fields).
691    text_buffer: String,
692    /// Frame counter for record_frame().
693    record_n: u32,
694    /// Accumulated microphone samples (for turning sound into crypto donuts).
695    #[cfg(not(target_arch = "wasm32"))]
696    mic_buffer: Vec<f32>,
697    /// Loaded vector UI fonts, referenced by handle (index) from `font_load`.
698    #[cfg(not(target_arch = "wasm32"))]
699    fonts: Vec<ling_graphics::VectorFont>,
700    /// Customizable UI colour palette (set via `ui_theme`).
701    ui_theme: UiTheme,
702    /// Left-mouse state on the previous frame — for widget click-edge detection.
703    mouse_was_down: bool,
704    /// Live music engine (decode playback + GM synth) — lazily initialised.
705    #[cfg(not(target_arch = "wasm32"))]
706    music: Option<ling_music::MusicEngine>,
707    #[cfg(not(target_arch = "wasm32"))]
708    music_init: bool,
709    /// Decoded tracks (for analysis + playback), by `music_load` handle.
710    #[cfg(not(target_arch = "wasm32"))]
711    tracks: Vec<ling_music::DecodedAudio>,
712    /// Parsed `.lrc` lyrics, by `music_lrc` handle.
713    #[cfg(not(target_arch = "wasm32"))]
714    lyrics: Vec<ling_music::Lyrics>,
715    /// Parsed MIDI songs, by `music_midi_load` handle.
716    #[cfg(not(target_arch = "wasm32"))]
717    midis: Vec<ling_music::MidiSong>,
718    /// Soft bodies (deformable balls), by `soft_ball` handle.
719    soft_bodies: Vec<ling_physics::soft::SoftBody>,
720    /// Rigid-body world (angular dynamics), shared by `rb_*`.
721    rigid_world: ling_physics::rigid::PhysicsWorld,
722    /// Liquid grids (water/oil), by `liquid_new` handle.
723    liquids: Vec<ling_physics::liquid::LiquidGrid>,
724    meshes: Vec<crate::gfx::shapes::ColorMesh>,
725    /// Active cinematic dialog box (Ocarina/Majora-style), if any.
726    dialog: Option<ling_game::dialog::Dialog>,
727    /// Dialog highlight colours by role: text, name, place, item (0x00RRGGBB).
728    dialog_colors: [u32; 4],
729    /// Active user-function call frames (names), for error tracebacks.
730    frames: Vec<String>,
731    /// Snapshot of `frames` captured the moment a runtime error first arose
732    /// (the deepest call). Consumed by `take_error_trace`.
733    error_trace: Option<Vec<String>>,
734    /// Unified input (gamepads/joysticks/VR/touch via the ling-input
735    /// "Sensorium"). Lazily initialised on the first `pad_*` builtin call;
736    /// `None` if no native input backend is available.
737    #[cfg(not(target_arch = "wasm32"))]
738    input: RefCell<Option<InputState>>,
739}
740
741/// Live gamepad input state: a ling-input hub fed by the native `gilrs` backend.
742#[cfg(not(target_arch = "wasm32"))]
743struct InputState {
744    sensorium: ling_input::Sensorium,
745    backend: ling_input::backend::GilrsBackend,
746}
747
748impl Interpreter {
749    pub fn new() -> Self {
750        #[cfg(not(target_arch = "wasm32"))]
751        let audio = AudioEngine::new()
752            .map_err(|e| eprintln!("audio init failed (no sound): {e}"))
753            .ok();
754        Self {
755            globals: HashMap::new(),
756            global_seed: HashMap::new(),
757            functions: HashMap::new(),
758            structs: HashMap::new(),
759            enum_variants: HashMap::new(),
760            _modules: HashMap::new(),
761            gfx: RefCell::new(GfxState::new()),
762            svg: RefCell::new(None),
763            source_dir: None,
764            loaded_files: std::collections::HashSet::new(),
765            #[cfg(not(target_arch = "wasm32"))]
766            audio,
767            #[cfg(not(target_arch = "wasm32"))]
768            fft: RefCell::new(FftAnalyzer::new(2048, 44100)),
769            fft_bands_cache: RefCell::new(vec![]),
770            start_time: std::time::Instant::now(),
771            frame_num: 0,
772            rand_state: 0x123456789ABCDEF,
773            #[cfg(not(target_arch = "wasm32"))]
774            mic: None,
775            #[cfg(not(target_arch = "wasm32"))]
776            crypto_ids: Vec::new(),
777            text_buffer: String::new(),
778            record_n: 0,
779            #[cfg(not(target_arch = "wasm32"))]
780            mic_buffer: Vec::new(),
781            #[cfg(not(target_arch = "wasm32"))]
782            fonts: Vec::new(),
783            ui_theme: UiTheme::default(),
784            mouse_was_down: false,
785            #[cfg(not(target_arch = "wasm32"))]
786            music: None,
787            #[cfg(not(target_arch = "wasm32"))]
788            music_init: false,
789            #[cfg(not(target_arch = "wasm32"))]
790            tracks: Vec::new(),
791            #[cfg(not(target_arch = "wasm32"))]
792            lyrics: Vec::new(),
793            #[cfg(not(target_arch = "wasm32"))]
794            midis: Vec::new(),
795            soft_bodies: Vec::new(),
796            rigid_world: ling_physics::rigid::PhysicsWorld::new(),
797            liquids: Vec::new(),
798            meshes: Vec::new(),
799            dialog: None,
800            dialog_colors: [0xE6F2FF, 0xFFD24A, 0x4AD2FF, 0x6CFF8C], // text · name · place · item
801            frames: Vec::new(),
802            error_trace: None,
803            #[cfg(not(target_arch = "wasm32"))]
804            input: RefCell::new(None),
805        }
806    }
807
808    /// Lazily initialise the input system and advance it one frame; returns the
809    /// number of connected gamepads. Call once per game-loop iteration (like a
810    /// window update) before reading `pad_*` state.
811    #[cfg(not(target_arch = "wasm32"))]
812    fn pad_poll(&self) -> usize {
813        let mut slot = self.input.borrow_mut();
814        if slot.is_none() {
815            match ling_input::backend::GilrsBackend::new() {
816                Ok(backend) => {
817                    *slot = Some(InputState { sensorium: ling_input::Sensorium::new(4), backend });
818                },
819                Err(_) => return 0,
820            }
821        }
822        let st = slot.as_mut().unwrap();
823        st.sensorium.begin_frame();
824        st.sensorium.pump(&mut st.backend);
825        st.sensorium.update(1.0 / 60.0);
826        st.sensorium.devices.count()
827    }
828
829    /// Read player `slot`'s gamepad with `f`, or return `default` if there is no
830    /// input system / no such pad.
831    #[cfg(not(target_arch = "wasm32"))]
832    fn with_pad<T>(&self, slot: usize, default: T, f: impl FnOnce(&ling_input::Gamepad) -> T) -> T {
833        let inp = self.input.borrow();
834        match inp.as_ref().and_then(|s| s.sensorium.player(slot)) {
835            Some(p) => f(p),
836            None => default,
837        }
838    }
839
840    /// Take the call-stack snapshot captured at the deepest runtime error, if any.
841    /// Frames are ordered outermost-first (entry point first, failing call last).
842    pub fn take_error_trace(&mut self) -> Vec<String> {
843        self.error_trace.take().unwrap_or_default()
844    }
845
846    /// Run `body`, recording `name` as a call frame and snapshotting the stack on
847    /// the first runtime error so a traceback can be reported.
848    fn framed<T, F>(&mut self, name: &str, body: F) -> Result<T, EvalErr>
849    where
850        F: FnOnce(&mut Self) -> Result<T, EvalErr>,
851    {
852        self.frames.push(name.to_string());
853        let result = body(self);
854        if matches!(result, Err(EvalErr::Runtime(_))) && self.error_trace.is_none() {
855            self.error_trace = Some(self.frames.clone());
856        }
857        self.frames.pop();
858        result
859    }
860
861    /// Render the active dialog box: beveled frame + dark fill, then the visible
862    /// (typewriter-revealed) text word-wrapped with colour-coded runs, plus a
863    /// blinking advance arrow once the page is fully typed.
864    #[cfg(not(target_arch = "wasm32"))]
865    fn render_dialog(&mut self, x: f32, y: f32, w: f32, h: f32, font: i64, t: f32) {
866        let (runs, typing) = match &self.dialog {
867            Some(d) if !d.is_closed() => {
868                let runs: Vec<(String, usize, bool)> = d
869                    .visible_runs()
870                    .into_iter()
871                    .map(|r| (r.text, r.role.index(), r.newline_before))
872                    .collect();
873                (runs, d.is_typing())
874            },
875            _ => return,
876        };
877        let colors = self.dialog_colors;
878        // ── frame + fill ──
879        let b = 12.0;
880        let corners: Vec<[f32; 2]> = vec![
881            [x + b, y],
882            [x + w - b, y],
883            [x + w, y + b],
884            [x + w, y + h - b],
885            [x + w - b, y + h],
886            [x + b, y + h],
887            [x, y + h - b],
888            [x, y + b],
889            [x + b, y],
890        ];
891        {
892            let mut gfx = self.gfx.borrow_mut();
893            let (bw, bh) = (gfx.width, gfx.height);
894            crate::gfx::raster::fill_contours_aa(
895                &mut gfx.buffer,
896                bw,
897                bh,
898                0x0A1018,
899                false,
900                std::slice::from_ref(&corners),
901            );
902            for seg in corners.windows(2) {
903                crate::gfx::raster::draw_line_aa(
904                    &mut gfx.buffer,
905                    bw,
906                    bh,
907                    0x00D2FF,
908                    false,
909                    seg[0][0],
910                    seg[0][1],
911                    seg[1][0],
912                    seg[1][1],
913                );
914            }
915        }
916        // ── word-wrapped, colour-coded text ──
917        let px = 22.0f32;
918        let pad = 20.0f32;
919        let line_h = px * 1.45;
920        let mut cx = x + pad;
921        let mut cy = y + pad;
922        let use_font = font >= 0 && (font as usize) < self.fonts.len();
923        for (text, role, nl) in &runs {
924            if *nl {
925                cx = x + pad;
926                cy += line_h;
927            }
928            for word in text.split_inclusive(' ') {
929                let wpx = if use_font {
930                    self.fonts[font as usize].measure(word, px)
931                } else {
932                    ling_ui::holo::text_width(word, px * 0.6, px * 0.24)
933                };
934                if cx + wpx > x + w - pad && cx > x + pad + 1.0 {
935                    cx = x + pad;
936                    cy += line_h;
937                }
938                if cy + line_h > y + h {
939                    break;
940                }
941                let col = colors[(*role).min(3)];
942                if use_font {
943                    let glyphs = self.font_layout_2d_glyphs(font as usize, cx, cy, px, word);
944                    let mut gfx = self.gfx.borrow_mut();
945                    let (bw, bh, add) = (gfx.width, gfx.height, gfx.blend == 1);
946                    for contours in &glyphs {
947                        crate::gfx::raster::fill_contours_aa(
948                            &mut gfx.buffer,
949                            bw,
950                            bh,
951                            col,
952                            add,
953                            contours,
954                        );
955                    }
956                } else {
957                    let segs = ling_ui::holo::text_lines(word, cx, cy, px * 0.6, px, px * 0.24);
958                    let mut gfx = self.gfx.borrow_mut();
959                    let (bw, bh) = (gfx.width, gfx.height);
960                    for s in segs {
961                        draw_line(&mut gfx.buffer, bw, bh, col, s[0], s[1], s[2], s[3]);
962                    }
963                }
964                cx += wpx;
965            }
966        }
967        // ── blinking advance arrow when fully typed ──
968        if !typing && (t * 3.0).sin() > 0.0 {
969            let ax = x + w - 26.0;
970            let ay = y + h - 22.0;
971            let mut gfx = self.gfx.borrow_mut();
972            let (bw, bh) = (gfx.width, gfx.height);
973            crate::gfx::raster::fill_contours_aa(
974                &mut gfx.buffer,
975                bw,
976                bh,
977                0x00D2FF,
978                false,
979                std::slice::from_ref(&vec![
980                    [ax - 7.0, ay],
981                    [ax + 7.0, ay],
982                    [ax, ay + 9.0],
983                    [ax - 7.0, ay],
984                ]),
985            );
986        }
987    }
988
989    /// Lazily start the music engine on first use (playback/synth need a device;
990    /// analysis/decoding do not). Returns `false` if no audio device is available.
991    #[cfg(not(target_arch = "wasm32"))]
992    fn ensure_music(&mut self) -> bool {
993        if self.music.is_some() {
994            return true;
995        }
996        if self.music_init {
997            return false;
998        }
999        self.music_init = true;
1000        match ling_music::MusicEngine::new() {
1001            Ok(m) => {
1002                self.music = Some(m);
1003                true
1004            },
1005            Err(e) => {
1006                eprintln!("music engine init failed (no music playback): {e}");
1007                false
1008            },
1009        }
1010    }
1011
1012    /// Lay out `text` for font `id` at size `px`, returning every glyph contour as
1013    /// a screen-space polyline (x→right, y→down). `(x, y)` is the text box top-left;
1014    /// the baseline is placed `ascent*px` below it. Curves are flattened to 0.3 px.
1015    #[cfg(not(target_arch = "wasm32"))]
1016    fn font_layout_2d(
1017        &mut self,
1018        id: usize,
1019        x: f32,
1020        y: f32,
1021        px: f32,
1022        text: &str,
1023    ) -> Vec<Vec<[f32; 2]>> {
1024        let mut out = Vec::new();
1025        for g in self.font_layout_2d_glyphs(id, x, y, px, text) {
1026            out.extend(g);
1027        }
1028        out
1029    }
1030
1031    /// Same as [`font_layout_2d`] but grouped per glyph (so a fill can apply the
1032    /// non-zero winding rule within each glyph, preserving interior holes).
1033    #[cfg(not(target_arch = "wasm32"))]
1034    fn font_layout_2d_glyphs(
1035        &mut self,
1036        id: usize,
1037        x: f32,
1038        y: f32,
1039        px: f32,
1040        text: &str,
1041    ) -> Vec<Vec<Vec<[f32; 2]>>> {
1042        let font = &mut self.fonts[id];
1043        let asc = font.ascent();
1044        let tol = 0.3 / px;
1045        let mut pen = 0.0f32;
1046        let mut glyphs = Vec::new();
1047        for ch in text.chars() {
1048            let go = font.glyph_outline(ch, tol);
1049            let mut contours = Vec::with_capacity(go.polylines.len());
1050            for pl in &go.polylines {
1051                let mapped: Vec<[f32; 2]> = pl
1052                    .iter()
1053                    .map(|p| [x + (pen + p[0]) * px, y + (asc - p[1]) * px])
1054                    .collect();
1055                contours.push(mapped);
1056            }
1057            glyphs.push(contours);
1058            pen += go.advance;
1059        }
1060        glyphs
1061    }
1062
1063    pub fn run_program(&mut self, program: &Program) -> Result<(), String> {
1064        for item in &program.items {
1065            self.register_item("", item)?;
1066        }
1067        let entry = self
1068            .find_entry()
1069            .ok_or("no entry point — need `bind start = do {...}` or `ผูก เริ่ม = ทำ {...}`")?;
1070        // Seed the entry env with non-Do globals so top-level `令` bindings
1071        // are visible in the main Do block (same two-pass logic as call_named).
1072        let mut env = Env::new();
1073        let non_do: Vec<_> = self
1074            .globals
1075            .iter()
1076            .filter(|(_, e)| !matches!(e, Expr::Do(_)))
1077            .map(|(k, e)| (k.clone(), e.clone()))
1078            .collect();
1079        let mut pending: Vec<(String, Expr)> = Vec::new();
1080        for (k, expr) in &non_do {
1081            let mut tmp = Env::new();
1082            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
1083                env.insert(k.clone(), v);
1084            } else {
1085                pending.push((k.clone(), expr.clone()));
1086            }
1087        }
1088        for (k, expr) in &pending {
1089            let mut tmp = env.clone();
1090            if let Ok(v) = self.eval_expr(expr, &mut tmp) {
1091                env.insert(k.clone(), v);
1092            }
1093        }
1094        // Cache the evaluated globals so every user-function call can clone this
1095        // seed instead of re-evaluating all globals (see call_named).
1096        self.global_seed = env.clone();
1097        self.framed("start", |me| me.eval_expr(&entry, &mut env))
1098            .map(|_| ())
1099            .map_err(|e| match e {
1100                EvalErr::Runtime(s) => s,
1101                EvalErr::Return(_) => "unexpected top-level return".to_string(),
1102                EvalErr::Break => "unexpected break at top level".to_string(),
1103            })
1104    }
1105
1106    fn register_item(&mut self, ns: &str, item: &Item) -> Result<(), String> {
1107        match item {
1108            Item::Bind(name, expr) => {
1109                let key = if ns.is_empty() {
1110                    name.clone()
1111                } else {
1112                    format!("{ns}::{name}")
1113                };
1114                self.globals.insert(key, expr.clone());
1115            },
1116            Item::Fn(def) => {
1117                let key = if ns.is_empty() {
1118                    def.name.clone()
1119                } else {
1120                    format!("{ns}::{}", def.name)
1121                };
1122                self.functions.insert(key, def.clone());
1123            },
1124            Item::Mod(name, body) => {
1125                let child_ns = if ns.is_empty() {
1126                    name.clone()
1127                } else {
1128                    format!("{ns}::{name}")
1129                };
1130                for child in body {
1131                    self.register_item(&child_ns, child)?;
1132                }
1133            },
1134            Item::TypeAlias(_, _) => {},
1135            Item::Struct(name, fields) => {
1136                self.structs.insert(name.clone(), fields.clone());
1137                if !ns.is_empty() {
1138                    self.structs.insert(format!("{ns}::{name}"), fields.clone());
1139                }
1140            },
1141            Item::Enum(name, variants) => {
1142                for v in variants {
1143                    self.enum_variants
1144                        .insert(v.name.clone(), (name.clone(), v.arity));
1145                    self.enum_variants
1146                        .insert(format!("{name}::{}", v.name), (name.clone(), v.arity));
1147                    if !ns.is_empty() {
1148                        self.enum_variants
1149                            .insert(format!("{ns}::{name}::{}", v.name), (name.clone(), v.arity));
1150                    }
1151                }
1152            },
1153            Item::Use { path, alias } => {
1154                self.load_module(path, alias.as_deref(), ns)?;
1155            },
1156        }
1157        Ok(())
1158    }
1159
1160    /// Resolve `path` relative to `source_dir`, load and parse it, then
1161    /// register all its definitions.  If `alias` is given, every name is
1162    /// prefixed with `<parent_ns>::<alias>`.  Circular imports are silently
1163    /// skipped.
1164    fn load_module(
1165        &mut self,
1166        path: &str,
1167        alias: Option<&str>,
1168        parent_ns: &str,
1169    ) -> Result<(), String> {
1170        // Build candidate file paths (.ling extension variants)
1171        let base_dir = self
1172            .source_dir
1173            .clone()
1174            .unwrap_or_else(|| std::path::PathBuf::from("."));
1175        let raw = std::path::Path::new(path);
1176        let candidates: Vec<std::path::PathBuf> = vec![
1177            base_dir.join(format!("{}.ling", path)),
1178            base_dir.join(format!("{}.灵", path)),
1179            base_dir.join(format!("{}.령", path)),
1180            base_dir.join(format!("{}.霊", path)),
1181            base_dir.join(format!("{}.ลิง", path)),
1182            // exact path if already has extension
1183            base_dir.join(raw),
1184            std::path::PathBuf::from(format!("{}.ling", path)),
1185            std::path::PathBuf::from(path),
1186        ];
1187
1188        let resolved = candidates
1189            .into_iter()
1190            .find(|p| p.exists())
1191            .ok_or_else(|| format!("use: cannot find module '{path}'"))?;
1192
1193        let canonical = resolved
1194            .canonicalize()
1195            .unwrap_or_else(|_| resolved.clone())
1196            .to_string_lossy()
1197            .to_string();
1198
1199        // Skip if already loaded (circular import guard)
1200        if self.loaded_files.contains(&canonical) {
1201            return Ok(());
1202        }
1203        self.loaded_files.insert(canonical.clone());
1204
1205        let source = std::fs::read_to_string(&resolved)
1206            .map_err(|e| format!("use: failed to read '{path}': {e}"))?;
1207
1208        // Save/restore source_dir for nested relative imports
1209        let prev_dir = self.source_dir.clone();
1210        self.source_dir = resolved.parent().map(|p| p.to_path_buf());
1211
1212        let program = crate::parser::parse(&source)
1213            .map_err(|e| format!("use: parse error in '{path}': {e}"))?;
1214
1215        // Compute target namespace: parent_ns :: alias (or just alias, or just parent_ns)
1216        let target_ns = match (parent_ns.is_empty(), alias) {
1217            (_, Some(a)) if !parent_ns.is_empty() => format!("{parent_ns}::{a}"),
1218            (_, Some(a)) => a.to_string(),
1219            (false, None) => parent_ns.to_string(),
1220            (true, None) => String::new(),
1221        };
1222
1223        for item in &program.items {
1224            self.register_item(&target_ns, item)?;
1225        }
1226
1227        self.source_dir = prev_dir;
1228        Ok(())
1229    }
1230
1231    fn find_entry(&self) -> Option<Expr> {
1232        // Known entry-point names across supported human languages.
1233        for key in crate::entry::ENTRY_NAMES {
1234            if let Some(e) = self.globals.get(*key) {
1235                return Some(e.clone());
1236            }
1237        }
1238        self.globals
1239            .values()
1240            .find(|e| matches!(e, Expr::Do(_)))
1241            .cloned()
1242    }
1243
1244    // ─── Expression evaluation ────────────────────────────────────────────────
1245
1246    fn eval_expr(&mut self, expr: &Expr, env: &mut Env) -> EvalResult {
1247        match expr {
1248            Expr::Str(s) => Ok(Value::Str(s.clone())),
1249            Expr::Number(n) => Ok(Value::Number(*n)),
1250            Expr::Bool(b) => Ok(Value::Bool(*b)),
1251            Expr::Unit => Ok(Value::Unit),
1252            Expr::Array(elems) => {
1253                let vs: Vec<_> = elems
1254                    .iter()
1255                    .map(|e| self.eval_expr(e, env))
1256                    .collect::<Result<_, _>>()?;
1257                Ok(Value::List(vs))
1258            },
1259
1260            Expr::Ident(name) => self.lookup(name, env),
1261
1262            Expr::Path(segs) => {
1263                if segs.len() == 1 {
1264                    return self.lookup(&segs[0], env);
1265                }
1266                Ok(Value::Str(segs.join("::")))
1267            },
1268
1269            Expr::Ref(inner) => self.eval_expr(inner, env),
1270            Expr::Await(inner) => self.eval_expr(inner, env),
1271
1272            Expr::Do(stmts) => {
1273                let mut local = env.clone();
1274                Ok(self.exec_block(stmts, &mut local)?.unwrap_or(Value::Unit))
1275            },
1276
1277            Expr::BinOp(op, lhs, rhs) => {
1278                let l = self.eval_expr(lhs, env)?;
1279                let r = self.eval_expr(rhs, env)?;
1280                self.apply_binop(op, l, r)
1281            },
1282
1283            Expr::If { cond, then, elseifs, else_body } => {
1284                let cond_val = self.eval_expr(cond, env)?;
1285                if self.is_truthy(&cond_val) {
1286                    return Ok(self.exec_block(then, env)?.unwrap_or(Value::Unit));
1287                }
1288                for (ei_cond, ei_body) in elseifs {
1289                    let ei_cond_val = self.eval_expr(ei_cond, env)?;
1290                    if self.is_truthy(&ei_cond_val) {
1291                        return Ok(self.exec_block(ei_body, env)?.unwrap_or(Value::Unit));
1292                    }
1293                }
1294                if let Some(eb) = else_body {
1295                    return Ok(self.exec_block(eb, env)?.unwrap_or(Value::Unit));
1296                }
1297                Ok(Value::Unit)
1298            },
1299
1300            Expr::While { cond, body } => {
1301                // Run the body directly in the *outer* env so that
1302                // `bind counter = counter + 1` persists across iterations,
1303                // which is the expected behaviour in a scripting language.
1304                loop {
1305                    let cv = self.eval_expr(cond, env)?;
1306                    if !self.is_truthy(&cv) {
1307                        break;
1308                    }
1309                    match self.exec_block(body, env) {
1310                        Ok(_) => {},
1311                        Err(EvalErr::Break) => break,
1312                        Err(e) => return Err(e),
1313                    }
1314                }
1315                Ok(Value::Unit)
1316            },
1317
1318            Expr::For { var, iter, body } => {
1319                let iter_val = self.eval_expr(iter, env)?;
1320                let items = self.value_to_iter(iter_val)?;
1321                for item in items {
1322                    let mut local = env.clone();
1323                    local.insert(var.clone(), item);
1324                    match self.exec_block(body, &mut local) {
1325                        Ok(_) => {},
1326                        Err(EvalErr::Break) => break,
1327                        Err(e) => return Err(e),
1328                    }
1329                }
1330                Ok(Value::Unit)
1331            },
1332
1333            Expr::Match(subject, arms) => {
1334                let subj = self.eval_expr(subject, env)?;
1335                for arm in arms {
1336                    if let Some(bindings) = self.match_pattern(&arm.pattern, &subj) {
1337                        let mut local = env.clone();
1338                        local.extend(bindings);
1339                        return self.eval_expr(&arm.body, &mut local);
1340                    }
1341                }
1342                Ok(Value::Unit)
1343            },
1344
1345            Expr::Range(lo, hi) => {
1346                let lo_v = self.eval_expr(lo, env)?;
1347                let hi_v = self.eval_expr(hi, env)?;
1348                let lo_n = self.to_number(&lo_v)? as i64;
1349                let hi_n = self.to_number(&hi_v)? as i64;
1350                Ok(Value::List(
1351                    (lo_n..hi_n).map(|i| Value::Number(i as f64)).collect(),
1352                ))
1353            },
1354
1355            Expr::Index(base, idx) => {
1356                let b = self.eval_expr(base, env)?;
1357                let i = self.eval_expr(idx, env)?;
1358                let n = self.to_number(&i)? as usize;
1359                match b {
1360                    Value::List(v) => v
1361                        .get(n)
1362                        .cloned()
1363                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
1364                    Value::Str(s) => s
1365                        .chars()
1366                        .nth(n)
1367                        .map(|c| Value::Str(c.to_string()))
1368                        .ok_or_else(|| EvalErr::from(format!("index {n} out of bounds"))),
1369                    other => Err(EvalErr::from(format!("cannot index {:?}", other))),
1370                }
1371            },
1372
1373            Expr::Call(callee, args) => {
1374                let arg_vals: Vec<Value> = args
1375                    .iter()
1376                    .map(|a| self.eval_expr(a, env))
1377                    .collect::<Result<_, _>>()?;
1378                match callee.as_ref() {
1379                    Expr::Ident(name) => self.call_named(name, arg_vals, env),
1380                    Expr::Path(segs) => self.call_named(&segs.join("::"), arg_vals, env),
1381                    _ => {
1382                        let v = self.eval_expr(callee, env)?;
1383                        self.call_value(v, arg_vals)
1384                    },
1385                }
1386            },
1387
1388            Expr::MethodCall { receiver, method, args } => {
1389                let recv = self.eval_expr(receiver, env)?;
1390                let arg_vals: Vec<Value> = args
1391                    .iter()
1392                    .map(|a| self.eval_expr(a, env))
1393                    .collect::<Result<_, _>>()?;
1394                self.call_method(recv, method, arg_vals)
1395            },
1396
1397            Expr::Closure(params, body) => Ok(Value::Fn(
1398                params.clone(),
1399                vec![Stmt::Expr(*body.clone())],
1400                env.clone(),
1401            )),
1402        }
1403    }
1404
1405    // ─── Block execution ─────────────────────────────────────────────────────
1406
1407    fn exec_block(&mut self, stmts: &[Stmt], env: &mut Env) -> Result<Option<Value>, EvalErr> {
1408        let mut last: Option<Value> = None;
1409        for stmt in stmts {
1410            match stmt {
1411                Stmt::Bind(name, expr) => {
1412                    let v = self.eval_expr(expr, env)?;
1413                    env.insert(name.clone(), v);
1414                    last = None;
1415                },
1416                Stmt::Return(expr) => {
1417                    let v = self.eval_expr(expr, env)?;
1418                    return Err(EvalErr::Return(v));
1419                },
1420                Stmt::Expr(expr) => {
1421                    last = Some(self.eval_expr(expr, env)?);
1422                },
1423            }
1424        }
1425        Ok(last)
1426    }
1427
1428    // ─── Dispatch helpers ─────────────────────────────────────────────────────
1429
1430    fn lookup(&self, name: &str, env: &Env) -> EvalResult {
1431        if let Some(v) = env.get(name) {
1432            return Ok(v.clone());
1433        }
1434        if self.functions.contains_key(name) {
1435            let def = &self.functions[name];
1436            return Ok(Value::Fn(def.params.clone(), def.body.clone(), Env::new()));
1437        }
1438        // Bare nullary enum variant used as a value (e.g. `bind p = Origin`).
1439        if let Some((enum_name, 0)) = self.enum_variants.get(name).cloned() {
1440            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
1441            return Ok(Value::Variant { enum_name, variant, payload: Vec::new() });
1442        }
1443        // Math constants usable as plain identifiers (e.g. `sin(pi)`)
1444        match name {
1445            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" => {
1446                return Ok(Value::Number(std::f64::consts::PI))
1447            },
1448            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว" => {
1449                return Ok(Value::Number(std::f64::consts::TAU))
1450            },
1451            _ => {},
1452        }
1453        Err(EvalErr::from(format!("undefined: '{name}'")))
1454    }
1455
1456    pub(crate) fn call_named(&mut self, name: &str, args: Vec<Value>, env: &Env) -> EvalResult {
1457        // A user-defined function shadows any builtin of the same name, matching
1458        // the JIT/AOT backends (which always resolve a defined function first).
1459        if let Some(def) = self.functions.get(name).cloned() {
1460            let mut call_env = self.global_seed.clone();
1461            let _ = env; // call-site locals are intentionally NOT visible to fns
1462            for (param, arg) in def.params.iter().zip(args) {
1463                call_env.insert(param.clone(), arg);
1464            }
1465            return match self.framed(name, |me| me.exec_block(&def.body, &mut call_env)) {
1466                Ok(v) => Ok(v.unwrap_or(Value::Unit)),
1467                Err(EvalErr::Return(v)) => Ok(v),
1468                Err(e) => Err(e),
1469            };
1470        }
1471        match name {
1472            // ── Print ──
1473            "print" | "println" | "印" | "打印" | "印刷" | "พิมพ์" | "출력" | "вывести"
1474            | "imprimir" | "afficher" => {
1475                let s = args
1476                    .iter()
1477                    .map(|v| v.to_string())
1478                    .collect::<Vec<_>>()
1479                    .join("");
1480                println!("{s}");
1481                return Ok(Value::Unit);
1482            },
1483            // print_color(colorIdx, text...) — ANSI-coloured console line.
1484            //   colorIdx 0..7 → bright fg (90+idx): 1=red 2=green 3=yellow 4=blue 6=cyan 7=white.
1485            "print_color" | "พิมพ์สี" => {
1486                #[cfg(windows)]
1487                {
1488                    use std::sync::Once;
1489                    static VT: Once = Once::new();
1490                    VT.call_once(|| {
1491                        extern "system" {
1492                            fn GetStdHandle(n: u32) -> *mut std::ffi::c_void;
1493                            fn GetConsoleMode(h: *mut std::ffi::c_void, m: *mut u32) -> i32;
1494                            fn SetConsoleMode(h: *mut std::ffi::c_void, m: u32) -> i32;
1495                        }
1496                        unsafe {
1497                            let h = GetStdHandle(0xFFFF_FFF5u32); // STD_OUTPUT_HANDLE (-11)
1498                            let mut mode = 0u32;
1499                            if GetConsoleMode(h, &mut mode) != 0 {
1500                                SetConsoleMode(h, mode | 0x0004); // ENABLE_VIRTUAL_TERMINAL_PROCESSING
1501                            }
1502                        }
1503                    });
1504                }
1505                let col = self.arg_num(&args, 0, 7.0)? as i64;
1506                let s = args
1507                    .iter()
1508                    .skip(1)
1509                    .map(|v| v.to_string())
1510                    .collect::<Vec<_>>()
1511                    .join("");
1512                let code = 90 + col.clamp(0, 7);
1513                println!("\x1b[1;{code}m{s}\x1b[0m");
1514                return Ok(Value::Unit);
1515            },
1516            // ── Format ──
1517            "format"
1518            | "格式"
1519            | "フォーマット"
1520            | "서식"
1521            | "รูปแบบ"
1522            | "форматировать"
1523            | "formatear"
1524            | "formater" => {
1525                return Ok(Value::Str(self.builtin_format(&args)?));
1526            },
1527            // ── String join / concatenation ──
1528            "格式::拼接" | "format::join" => match args.first() {
1529                Some(Value::List(items)) => {
1530                    return Ok(Value::Str(items.iter().map(|v| v.to_string()).collect()));
1531                },
1532                _ => return Ok(Value::Str(self.builtin_format(&args)?)),
1533            },
1534            // ── Result constructors ──
1535            "ok" | "好" | "良し" | "좋아" | "โอเค" => {
1536                let val = args.into_iter().next().unwrap_or(Value::Unit);
1537                return Ok(Value::Ok(Box::new(val)));
1538            },
1539            "bad" | "坏" | "err" | "悪い" | "나쁨" | "ผิด" => {
1540                let val = args.into_iter().next().unwrap_or(Value::Unit);
1541                return Ok(Value::Err(Box::new(val)));
1542            },
1543            // ── Vec constructors ──
1544            "向量::从" | "Vec::from" => {
1545                if let Some(Value::List(v)) = args.first() {
1546                    return Ok(Value::List(v.clone()));
1547                }
1548                return Ok(Value::List(args));
1549            },
1550            "向量::有容量" | "Vec::with_capacity" => return Ok(Value::List(Vec::new())),
1551            // ── Timer stubs ──
1552            "计时::获取当前小时" | "Timer::hour" => return Ok(Value::Number(14.0)),
1553            "计时::现在" | "Timer::now" => return Ok(Value::Number(1000.0)),
1554            // ── Sleep ──
1555            "sleep" | "หยุด" | "นอน" | "sleep_ms" | "睡眠" | "眠る" | "スリープ" | "잠자기"
1556            | "잠" | "流水::睡眠" | "Flow::sleep" => {
1557                if let Some(ms_val) = args.first() {
1558                    if let Ok(ms) = self.to_number(ms_val) {
1559                        std::thread::sleep(std::time::Duration::from_millis(ms as u64));
1560                    }
1561                }
1562                return Ok(Value::Unit);
1563            },
1564            // ── Flow::parallel stub ──
1565            "流水::并行" | "Flow::parallel" => {
1566                if let Some(Value::Fn(params, body, mut cap)) = args.first().cloned() {
1567                    let _ = params;
1568                    match self.exec_block(&body, &mut cap) {
1569                        Ok(Some(v)) => return Ok(v),
1570                        Ok(None) => return Ok(Value::Unit),
1571                        Err(EvalErr::Return(v)) => return Ok(v),
1572                        Err(e) => return Err(e),
1573                    }
1574                }
1575                return Ok(Value::Unit);
1576            },
1577
1578            // ══════════════════════════════════════════════════════════════════
1579            // MATH BUILTINS  (all args and results are f64)
1580            // Thai aliases: ไซน์ โคไซน์ แทนเจนต์ รากที่สอง ค่าสัมบูรณ์
1581            //               ปัดลง ปัดขึ้น ปัดเศษ ตัดทศนิยม ต่ำสุด สูงสุด
1582            //               จำกัด ยกกำลัง ลอการิทึม พาย
1583            // ══════════════════════════════════════════════════════════════════
1584
1585            // ── Trigonometry (input in radians) ──
1586            "sin" | "ไซน์" | "正弦" | "サイン" | "사인" => {
1587                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sin()));
1588            },
1589            "cos" | "โคไซน์" | "余弦" | "コサイン" | "코사인" => {
1590                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cos()));
1591            },
1592
1593            // ── Hyperbolic functions ──
1594            // Hyperbolic tangent
1595            "tanh" | "tanhf" | "双曲正切" | "双曲線正接" | "쌍곡탄젠트" => {
1596                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tanh()));
1597            },
1598
1599            "tan" | "แทนเจนต์" | "正切" | "タンジェント" | "탄젠트" => {
1600                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.tan()));
1601            },
1602            "asin" | "arcsin" | "反正弦" | "アークサイン" | "아크사인" | "อาร์กไซน์" =>
1603            {
1604                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.asin()));
1605            },
1606            "acos" | "arccos" | "反余弦" | "アークコサイン" | "아크코사인" | "อาร์กโคไซน์" =>
1607            {
1608                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.acos()));
1609            },
1610            "atan" | "arctan" | "反正切" | "アークタンジェント" | "아크탄젠트" | "อาร์กแทนเจนต์" =>
1611            {
1612                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.atan()));
1613            },
1614            "atan2" | "arctan2" | "反正切2" | "アークタンジェント2" | "아크탄젠트2" =>
1615            {
1616                let y = self.arg_num(&args, 0, 0.0)?;
1617                let x = self.arg_num(&args, 1, 1.0)?;
1618                return Ok(Value::Number(y.atan2(x)));
1619            },
1620
1621            // ── Roots / powers ──
1622            "sqrt" | "รากที่สอง" | "平方根" | "根" | "제곱근" => {
1623                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.sqrt()));
1624            },
1625            "cbrt" | "立方根" | "세제곱근" | "รากที่สาม" => {
1626                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.cbrt()));
1627            },
1628            "pow" | "ยกกำลัง" | "幂" | "べき乗" | "거듭제곱" => {
1629                let base = self.arg_num(&args, 0, 0.0)?;
1630                let exp = self.arg_num(&args, 1, 1.0)?;
1631                return Ok(Value::Number(base.powf(exp)));
1632            },
1633            "exp" | "指数" | "指数関数" | "지수" => {
1634                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.exp()));
1635            },
1636            "hypot" | "斜边" | "斜辺" | "빗변" => {
1637                let x = self.arg_num(&args, 0, 0.0)?;
1638                let y = self.arg_num(&args, 1, 0.0)?;
1639                return Ok(Value::Number(x.hypot(y)));
1640            },
1641
1642            // ── Logarithms ──
1643            "ln" | "log" | "ลอการิทึม" | "对数" | "対数" | "로그" => {
1644                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.ln()));
1645            },
1646            "log2" | "对数2" | "対数2" | "로그2" => {
1647                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log2()));
1648            },
1649            "log10" | "对数10" | "対数10" | "로그10" => {
1650                return Ok(Value::Number(self.arg_num(&args, 0, 1.0)?.log10()));
1651            },
1652
1653            // ── Rounding / truncation ──
1654            "abs" | "ค่าสัมบูรณ์" | "绝对值" | "绝对" | "絶対値" | "절댓값" | "절대값" =>
1655            {
1656                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.abs()));
1657            },
1658            "floor" | "ปัดลง" | "向下取整" | "下整" | "床関数" | "내림" => {
1659                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.floor()));
1660            },
1661            "ceil" | "ปัดขึ้น" | "向上取整" | "上整" | "天井関数" | "올림" =>
1662            {
1663                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.ceil()));
1664            },
1665            "round" | "ปัดเศษ" | "四舍五入" | "四舍" | "四捨五入" | "반올림" =>
1666            {
1667                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.round()));
1668            },
1669            "trunc"
1670            | "int"
1671            | "ตัดทศนิยม"
1672            | "取整"
1673            | "整数化"
1674            | "整数"
1675            | "截整"
1676            | "정수화"
1677            | "정수"
1678            | "切り捨て"
1679            | "버림" => {
1680                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.trunc()));
1681            },
1682            "fract" | "小数部分" | "小数部" | "소수부" => {
1683                return Ok(Value::Number(self.arg_num(&args, 0, 0.0)?.fract()));
1684            },
1685
1686            // ── min / max / clamp ──
1687            "min" | "ต่ำสุด" | "最小" | "최솟값" => {
1688                let a = self.arg_num(&args, 0, 0.0)?;
1689                let b = self.arg_num(&args, 1, 0.0)?;
1690                return Ok(Value::Number(a.min(b)));
1691            },
1692            "max" | "สูงสุด" | "最大" | "최댓값" => {
1693                let a = self.arg_num(&args, 0, 0.0)?;
1694                let b = self.arg_num(&args, 1, 0.0)?;
1695                return Ok(Value::Number(a.max(b)));
1696            },
1697            "clamp" | "จำกัด" | "截取" | "範囲制限" | "범위제한" => {
1698                let x = self.arg_num(&args, 0, 0.0)?;
1699                let lo = self.arg_num(&args, 1, 0.0)?;
1700                let hi = self.arg_num(&args, 2, 1.0)?;
1701                return Ok(Value::Number(x.clamp(lo, hi)));
1702            },
1703
1704            // ── Constants (also accessible as plain identifiers via lookup) ──
1705            "pi" | "π" | "พาย" | "圆周率" | "円周率" | "파이" => {
1706                return Ok(Value::Number(std::f64::consts::PI))
1707            },
1708            "tau" | "τ" | "双周率" | "タウ" | "타우" | "ทาว" => {
1709                return Ok(Value::Number(std::f64::consts::TAU))
1710            },
1711
1712            // ══════════════════════════════════════════════════════════════════
1713            // PHASE 1: DMT TRIP CODER FEATURES
1714            // ══════════════════════════════════════════════════════════════════
1715
1716            // ── Step 1: Noise Functions ──
1717            "vnoise" | "noise2" | "นอยส์2ดี" | "柏林噪声2D" | "バリューノイズ2D" | "값노이즈2D" =>
1718            {
1719                let x = self.arg_num(&args, 0, 0.0)? as f32;
1720                let y = self.arg_num(&args, 1, 0.0)? as f32;
1721                let seed = self.arg_num(&args, 2, 0.0)? as u32;
1722                return Ok(Value::Number(tex_vnoise(x, y, seed) as f64));
1723            },
1724
1725            "fbm" | "นอยส์ออร์แกนิก" | "分形噪声" | "フラクタルノイズ" | "프랙탈노이즈" =>
1726            {
1727                let x = self.arg_num(&args, 0, 0.0)? as f32;
1728                let y = self.arg_num(&args, 1, 0.0)? as f32;
1729                let octaves = self.arg_num(&args, 2, 4.0)? as u32;
1730                let seed = self.arg_num(&args, 3, 0.0)? as u32;
1731                return Ok(Value::Number(tex_fbm(x, y, octaves, seed) as f64));
1732            },
1733
1734            "perlin"
1735            | "perlin3"
1736            | "เพอร์ลิน3ดี"
1737            | "柏林噪声3D"
1738            | "パーリンノイズ3D"
1739            | "펄린노이즈3D" => {
1740                let x = self.arg_num(&args, 0, 0.0)? as f32;
1741                let y = self.arg_num(&args, 1, 0.0)? as f32;
1742                let z = self.arg_num(&args, 2, 0.0)? as f32;
1743                return Ok(Value::Number(perlin3(x, y, z) as f64));
1744            },
1745
1746            // ── Step 2: Math Ergonomics ──
1747            "lerp" | "ค่าระหว่าง" | "线性插值" | "線形補間" | "선형보간" =>
1748            {
1749                let a = self.arg_num(&args, 0, 0.0)?;
1750                let b = self.arg_num(&args, 1, 1.0)?;
1751                let t = self.arg_num(&args, 2, 0.0)?;
1752                return Ok(Value::Number(a + (b - a) * t));
1753            },
1754
1755            "smoothstep" | "เปลี่ยนแบบนุ่ม" | "平滑步进" | "スムーズステップ" | "스무스스텝" =>
1756            {
1757                let lo = self.arg_num(&args, 0, 0.0)?;
1758                let hi = self.arg_num(&args, 1, 1.0)?;
1759                let x = self.arg_num(&args, 2, 0.5)?;
1760                let t = ((x - lo) / (hi - lo)).clamp(0.0, 1.0);
1761                return Ok(Value::Number(t * t * (3.0 - 2.0 * t)));
1762            },
1763
1764            "rand" | "สุ่ม" | "随机" | "乱数" | "난수" => {
1765                let val = fast_rand_f64(&mut self.rand_state);
1766                return Ok(Value::Number(val));
1767            },
1768
1769            "sign" | "เครื่องหมาย" | "符号" | "符号関数" | "부호" => {
1770                let x = self.arg_num(&args, 0, 0.0)?;
1771                return Ok(Value::Number(x.signum()));
1772            },
1773
1774            "hsv_to_rgb" | "เอชเอสวีเป็นRGB" | "HSV转RGB" | "HSV変換RGB" | "HSV변환RGB" =>
1775            {
1776                let h = self.arg_num(&args, 0, 0.0)?; // 0-360
1777                let s = self.arg_num(&args, 1, 1.0)?; // 0-1
1778                let v = self.arg_num(&args, 2, 1.0)?; // 0-1
1779                let c = v * s;
1780                let x = c * (1.0 - (((h / 60.0) % 2.0) - 1.0).abs());
1781                let m = v - c;
1782                let (r1, g1, b1) = if h < 60.0 {
1783                    (c, x, 0.0)
1784                } else if h < 120.0 {
1785                    (x, c, 0.0)
1786                } else if h < 180.0 {
1787                    (0.0, c, x)
1788                } else if h < 240.0 {
1789                    (0.0, x, c)
1790                } else if h < 300.0 {
1791                    (x, 0.0, c)
1792                } else {
1793                    (c, 0.0, x)
1794                };
1795                let r = ((r1 + m) * 255.0).round();
1796                let g = ((g1 + m) * 255.0).round();
1797                let b = ((b1 + m) * 255.0).round();
1798                return Ok(Value::List(vec![
1799                    Value::Number(r),
1800                    Value::Number(g),
1801                    Value::Number(b),
1802                ]));
1803            },
1804
1805            "lerp_color" | "ไล่สี" | "颜色插值" | "色補間" | "색보간" => {
1806                let r1 = self.arg_num(&args, 0, 0.0)?;
1807                let g1 = self.arg_num(&args, 1, 0.0)?;
1808                let b1 = self.arg_num(&args, 2, 0.0)?;
1809                let r2 = self.arg_num(&args, 3, 255.0)?;
1810                let g2 = self.arg_num(&args, 4, 255.0)?;
1811                let b2 = self.arg_num(&args, 5, 255.0)?;
1812                let t = self.arg_num(&args, 6, 0.0)?;
1813                let r = r1 + (r2 - r1) * t;
1814                let g = g1 + (g2 - g1) * t;
1815                let b = b1 + (b2 - b1) * t;
1816                let c = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
1817                self.gfx.borrow_mut().color = c;
1818                return Ok(Value::Unit);
1819            },
1820
1821            // ── Step 3: Real-Time Clock ──
1822            "time_now" | "เวลาปัจจุบัน" | "当前时间" | "経過時間" | "현재시간" =>
1823            {
1824                return Ok(Value::Number(self.start_time.elapsed().as_secs_f64()));
1825            },
1826
1827            "frame_count" | "เฟรม" | "帧数" | "フレーム数" | "프레임수" => {
1828                return Ok(Value::Number(self.frame_num as f64));
1829            },
1830
1831            // ── Step 4: Microphone Input ──
1832            "mic_open" | "เปิดไมค์" | "开麦克风" | "マイク開く" | "마이크열기" =>
1833            {
1834                #[cfg(not(target_arch = "wasm32"))]
1835                {
1836                    match ling_mic::MicInput::open(Default::default()) {
1837                        Ok(mic) => {
1838                            let _ = mic.start(|_samples: &[f32]| {}); // No-op callback
1839                            self.mic = Some(mic);
1840                            return Ok(Value::Number(1.0)); // opened
1841                        },
1842                        // No device / permission denied → graceful: don't crash the game loop.
1843                        // Returns 0.0; mic_rms/mic_peak return 0.0 while self.mic is None.
1844                        Err(_e) => {
1845                            self.mic = None;
1846                            return Ok(Value::Number(0.0));
1847                        },
1848                    }
1849                }
1850                #[cfg(target_arch = "wasm32")]
1851                return Ok(Value::Unit);
1852            },
1853
1854            "mic_rms" | "เสียงRMS" | "麦克风音量" | "マイクRMS" | "마이크RMS" =>
1855            {
1856                #[cfg(not(target_arch = "wasm32"))]
1857                {
1858                    let rms = self
1859                        .mic
1860                        .as_ref()
1861                        .map(|m: &ling_mic::MicInput| m.rms())
1862                        .unwrap_or(0.0);
1863                    return Ok(Value::Number(rms as f64));
1864                }
1865                #[cfg(target_arch = "wasm32")]
1866                return Ok(Value::Number(0.0));
1867            },
1868
1869            "mic_peak" | "เสียงพีค" | "麦克风峰值" | "マイクピーク" | "마이크피크" =>
1870            {
1871                #[cfg(not(target_arch = "wasm32"))]
1872                {
1873                    let peak = self
1874                        .mic
1875                        .as_ref()
1876                        .map(|m: &ling_mic::MicInput| m.peak())
1877                        .unwrap_or(0.0);
1878                    return Ok(Value::Number(peak as f64));
1879                }
1880                #[cfg(target_arch = "wasm32")]
1881                return Ok(Value::Number(0.0));
1882            },
1883
1884            "mic_fft" | "วิเคราะห์เสียงสด" | "实时频谱" | "リアルタイムFFT" | "실시간FFT" =>
1885            {
1886                #[cfg(not(target_arch = "wasm32"))]
1887                {
1888                    let n = self.arg_num(&args, 0, 8.0)? as usize;
1889                    if let Some(mic) = self.mic.as_ref() {
1890                        let samples = mic.latest_samples();
1891                        self.fft.borrow_mut().push_samples(&samples);
1892                    }
1893                    let bands = self.fft.borrow().freq_bands(n);
1894                    let result = bands.iter().map(|&v| Value::Number(v as f64)).collect();
1895                    return Ok(Value::List(result));
1896                }
1897                #[cfg(target_arch = "wasm32")]
1898                return Ok(Value::List(vec![]));
1899            },
1900
1901            // ── Step 5: Additive Blend Mode ──
1902            "set_blend" | "โหมดผสม" | "混合模式" | "ブレンドモード" | "블렌드모드" =>
1903            {
1904                let mode = self.arg_num(&args, 0, 0.0)? as u8;
1905                let mut gfx = self.gfx.borrow_mut();
1906                gfx.blend = mode;
1907                let a = gfx.alpha;
1908                gfx.depth_queue.set_state(mode, a); // 3-D queue captures blend for subsequent pushes
1909                return Ok(Value::Unit);
1910            },
1911
1912            // ── Step 6: Circle Primitives ──
1913            "draw_circle" | "วาดวงกลม" | "画圆" | "円描画" | "원그리기" =>
1914            {
1915                let cx = self.arg_num(&args, 0, 0.0)? as i32;
1916                let cy = self.arg_num(&args, 1, 0.0)? as i32;
1917                let r = self.arg_num(&args, 2, 10.0)? as i32;
1918                let mut gfx = self.gfx.borrow_mut();
1919                let (w, h, color, blend) =
1920                    (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
1921                draw_circle_outline(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
1922                return Ok(Value::Unit);
1923            },
1924
1925            "draw_filled_circle"
1926            | "draw_disc"
1927            | "วาดวงกลมทึบ"
1928            | "画实心圆"
1929            | "塗りつぶし円"
1930            | "원채우기" => {
1931                let cx = self.arg_num(&args, 0, 0.0)? as i32;
1932                let cy = self.arg_num(&args, 1, 0.0)? as i32;
1933                let r = self.arg_num(&args, 2, 10.0)? as i32;
1934                let mut gfx = self.gfx.borrow_mut();
1935                let (w, h, color, blend) =
1936                    (gfx.width as i32, gfx.height as i32, gfx.color, gfx.blend);
1937                draw_circle_filled(&mut gfx.buffer, w, h, cx, cy, r, color, blend);
1938                return Ok(Value::Unit);
1939            },
1940
1941            // ── Step 7: Transparent fills, gradient surfaces & colored shadows ──
1942            // These all write straight into the software framebuffer (gfx.buffer)
1943            // on both native and web, so no target gating is needed.
1944
1945            // set_alpha(a) — pen opacity 0..1 for the alpha-blended fills below.
1946            "set_alpha" | "ตั้งความโปร่งใส" | "设透明" | "アルファ設定" | "투명도설정" =>
1947            {
1948                let a = self.arg_num(&args, 0, 1.0)? as f32;
1949                let mut gfx = self.gfx.borrow_mut();
1950                gfx.alpha = a.clamp(0.0, 1.0);
1951                let (m, al) = (gfx.blend, gfx.alpha);
1952                gfx.depth_queue.set_state(m, al); // 3-D queue captures alpha for subsequent pushes
1953                return Ok(Value::Unit);
1954            },
1955
1956            // set_color_space(mode) — 0 = legacy sRGB compositing (default),
1957            // 1 = gamma-correct linear-light compositing (blend in linear, store
1958            // sRGB) so alpha and gradients don't darken/shift hue.
1959            "set_color_space" | "ปริภูมิสี" | "色彩空间" | "色空間" | "색공간" =>
1960            {
1961                let m = self.arg_num(&args, 0, 0.0)? as i64;
1962                self.gfx.borrow_mut().linear_blend = m != 0;
1963                return Ok(Value::Unit);
1964            },
1965
1966            // set_gradient_space(mode) — 1 = perceptual OkLab gradient interp
1967            // (default), 0 = legacy sRGB. Affects grad_triangle / grad_rect.
1968            "set_gradient_space" | "ปริภูมิไล่สี" | "渐变空间" | "グラデ空間" | "그라데이션공간" =>
1969            {
1970                let m = self.arg_num(&args, 0, 1.0)? as i64;
1971                self.gfx.borrow_mut().grad_oklab = m != 0;
1972                return Ok(Value::Unit);
1973            },
1974
1975            // mix_color(r0,g0,b0, r1,g1,b1, t) — set the pen colour to the
1976            // perceptual OkLab blend of two colours (t in 0..1). Far nicer
1977            // mid-tones than a raw RGB lerp.
1978            "mix_color" | "ผสมสี" | "混合颜色" | "色混合" | "색혼합" => {
1979                let c0 = rgb(
1980                    self.arg_num(&args, 0, 0.0)?,
1981                    self.arg_num(&args, 1, 0.0)?,
1982                    self.arg_num(&args, 2, 0.0)?,
1983                );
1984                let c1 = rgb(
1985                    self.arg_num(&args, 3, 255.0)?,
1986                    self.arg_num(&args, 4, 255.0)?,
1987                    self.arg_num(&args, 5, 255.0)?,
1988                );
1989                let t = self.arg_num(&args, 6, 0.5)? as f32;
1990                self.gfx.borrow_mut().color = crate::gfx::color::mix_oklab(c0, c1, t);
1991                return Ok(Value::Unit);
1992            },
1993
1994            // set_depth_test(on) — enable the per-pixel z-buffer for the deferred
1995            // 3-D/queued draws (correct interpenetration) instead of painter's-
1996            // only sort. 0 = off (default), non-zero = on.
1997            "set_depth_test" | "ทดสอบความลึก" | "深度测试" | "深度テスト" | "깊이테스트" =>
1998            {
1999                let on = self.arg_num(&args, 0, 1.0)? as i64 != 0;
2000                self.gfx.borrow_mut().depth_test = on;
2001                return Ok(Value::Unit);
2002            },
2003
2004            // grad_triangle(x0,y0,r0,g0,b0, x1,y1,r1,g1,b1, x2,y2,r2,g2,b2)
2005            // Smooth per-vertex gradient triangle — a cheap lit surface: put the
2006            // bright colour on the vertex facing the light. Honours set_alpha.
2007            "grad_triangle" | "สามเหลี่ยมไล่สี" | "渐变三角" | "グラデ三角" | "그라데삼각" =>
2008            {
2009                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
2010                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
2011                let c0 = rgb(
2012                    self.arg_num(&args, 2, 255.0)?,
2013                    self.arg_num(&args, 3, 255.0)?,
2014                    self.arg_num(&args, 4, 255.0)?,
2015                );
2016                let x1 = self.arg_num(&args, 5, 0.0)? as f32;
2017                let y1 = self.arg_num(&args, 6, 0.0)? as f32;
2018                let c1 = rgb(
2019                    self.arg_num(&args, 7, 255.0)?,
2020                    self.arg_num(&args, 8, 255.0)?,
2021                    self.arg_num(&args, 9, 255.0)?,
2022                );
2023                let x2 = self.arg_num(&args, 10, 0.0)? as f32;
2024                let y2 = self.arg_num(&args, 11, 0.0)? as f32;
2025                let c2 = rgb(
2026                    self.arg_num(&args, 12, 255.0)?,
2027                    self.arg_num(&args, 13, 255.0)?,
2028                    self.arg_num(&args, 14, 255.0)?,
2029                );
2030                let mut gfx = self.gfx.borrow_mut();
2031                let (w, h, alpha, mode, lin, ok) = (
2032                    gfx.width,
2033                    gfx.height,
2034                    gfx.alpha,
2035                    gfx.blend,
2036                    gfx.linear_blend,
2037                    gfx.grad_oklab,
2038                );
2039                crate::gfx::raster::fill_triangle_grad(
2040                    &mut gfx.buffer,
2041                    w,
2042                    h,
2043                    alpha,
2044                    mode,
2045                    lin,
2046                    ok,
2047                    x0,
2048                    y0,
2049                    c0,
2050                    x1,
2051                    y1,
2052                    c1,
2053                    x2,
2054                    y2,
2055                    c2,
2056                );
2057                return Ok(Value::Unit);
2058            },
2059
2060            // grad_rect(x,y,w,h, r0,g0,b0, r1,g1,b1, dir) — linear-gradient rect.
2061            // dir 0 = horizontal (left→right), else vertical (top→bottom).
2062            "grad_rect" | "สี่เหลี่ยมไล่สี" | "渐变矩形" | "グラデ矩形" | "그라데사각" =>
2063            {
2064                let x = self.arg_num(&args, 0, 0.0)? as f32;
2065                let y = self.arg_num(&args, 1, 0.0)? as f32;
2066                let rw = self.arg_num(&args, 2, 0.0)? as f32;
2067                let rh = self.arg_num(&args, 3, 0.0)? as f32;
2068                let c0 = rgb(
2069                    self.arg_num(&args, 4, 255.0)?,
2070                    self.arg_num(&args, 5, 255.0)?,
2071                    self.arg_num(&args, 6, 255.0)?,
2072                );
2073                let c1 = rgb(
2074                    self.arg_num(&args, 7, 0.0)?,
2075                    self.arg_num(&args, 8, 0.0)?,
2076                    self.arg_num(&args, 9, 0.0)?,
2077                );
2078                let dir = self.arg_num(&args, 10, 1.0)? as u8;
2079                let mut gfx = self.gfx.borrow_mut();
2080                let (w, h, alpha, mode, lin, ok) = (
2081                    gfx.width,
2082                    gfx.height,
2083                    gfx.alpha,
2084                    gfx.blend,
2085                    gfx.linear_blend,
2086                    gfx.grad_oklab,
2087                );
2088                crate::gfx::raster::fill_rect_grad(
2089                    &mut gfx.buffer,
2090                    w,
2091                    h,
2092                    alpha,
2093                    mode,
2094                    lin,
2095                    ok,
2096                    x,
2097                    y,
2098                    rw,
2099                    rh,
2100                    c0,
2101                    c1,
2102                    dir,
2103                );
2104                return Ok(Value::Unit);
2105            },
2106
2107            // shadow_blob(cx,cy, rx,ry, alpha) — soft colored shadow ellipse in
2108            // the current pen colour. Dark colour = normal shadow; any hue = a
2109            // tinted/coloured shadow. Edge softness comes from shadow_params.
2110            "shadow_blob" | "เงาวงรี" | "阴影斑" | "影ブロブ" | "그림자블롭" =>
2111            {
2112                let cx = self.arg_num(&args, 0, 0.0)? as f32;
2113                let cy = self.arg_num(&args, 1, 0.0)? as f32;
2114                let rx = self.arg_num(&args, 2, 16.0)? as f32;
2115                let ry = self.arg_num(&args, 3, 8.0)? as f32;
2116                let a = self.arg_num(&args, 4, 0.5)? as f32;
2117                let mut gfx = self.gfx.borrow_mut();
2118                let (w, h, color, soft, mode, lin) = (
2119                    gfx.width,
2120                    gfx.height,
2121                    gfx.color,
2122                    gfx.shadow.soft,
2123                    gfx.blend,
2124                    gfx.linear_blend,
2125                );
2126                crate::gfx::raster::fill_disc_soft(
2127                    &mut gfx.buffer,
2128                    w,
2129                    h,
2130                    cx,
2131                    cy,
2132                    rx,
2133                    ry,
2134                    color,
2135                    a,
2136                    soft,
2137                    mode,
2138                    lin,
2139                );
2140                return Ok(Value::Unit);
2141            },
2142
2143            // cast_shadow(cx,cy, height) — height-driven contact shadow in the
2144            // current pen colour. Closer to the surface (small height) = smaller,
2145            // darker, sharper; farther (large height) = bigger, fainter, softer.
2146            // Tune the ramp with shadow_params.
2147            "cast_shadow" | "ทอดเงา" | "投射阴影" | "影を落とす" | "그림자드리우기" =>
2148            {
2149                let cx = self.arg_num(&args, 0, 0.0)? as f32;
2150                let cy = self.arg_num(&args, 1, 0.0)? as f32;
2151                let height = (self.arg_num(&args, 2, 0.0)? as f32).max(0.0);
2152                let mut gfx = self.gfx.borrow_mut();
2153                let sp = gfx.shadow;
2154                let radius = (sp.base + sp.grow * height).max(0.5);
2155                let alpha = (sp.alpha - sp.fade * height).clamp(0.04, 1.0);
2156                let soft = (sp.soft + height * 0.004).clamp(0.0, 0.95);
2157                let (w, h, color, mode, lin) = (
2158                    gfx.width,
2159                    gfx.height,
2160                    gfx.color,
2161                    gfx.blend,
2162                    gfx.linear_blend,
2163                );
2164                crate::gfx::raster::fill_disc_soft(
2165                    &mut gfx.buffer,
2166                    w,
2167                    h,
2168                    cx,
2169                    cy,
2170                    radius,
2171                    radius * 0.62,
2172                    color,
2173                    alpha,
2174                    soft,
2175                    mode,
2176                    lin,
2177                );
2178                return Ok(Value::Unit);
2179            },
2180
2181            // shadow_params(base, grow, alpha, fade, soft) — tune cast_shadow.
2182            // Each arg defaults to the current value, so you can set just one.
2183            "shadow_params" | "ตั้งค่าเงา" | "阴影参数" | "影設定" | "그림자설정" =>
2184            {
2185                let cur = self.gfx.borrow().shadow;
2186                let base = self.arg_num(&args, 0, cur.base as f64)? as f32;
2187                let grow = self.arg_num(&args, 1, cur.grow as f64)? as f32;
2188                let alpha = self.arg_num(&args, 2, cur.alpha as f64)? as f32;
2189                let fade = self.arg_num(&args, 3, cur.fade as f64)? as f32;
2190                let soft = self.arg_num(&args, 4, cur.soft as f64)? as f32;
2191                self.gfx.borrow_mut().shadow =
2192                    crate::gfx::ShadowParams { base, grow, alpha, fade, soft };
2193                return Ok(Value::Unit);
2194            },
2195
2196            // depth_triangle(x0,y0, x1,y1, x2,y2, z) — queue a depth-sorted tri in
2197            // the current colour. Drawn back-to-front (painter's algorithm) at
2198            // present(); larger z = farther away. Lets 2-D sprites/quads sort by
2199            // depth the same way 3-D faces do.
2200            "depth_triangle" | "สามเหลี่ยมเรียงลึก" | "深度三角" | "深度三角形" | "깊이삼각" =>
2201            {
2202                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
2203                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
2204                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
2205                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
2206                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
2207                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
2208                let z = self.arg_num(&args, 6, 0.0)? as f32;
2209                let mut gfx = self.gfx.borrow_mut();
2210                let color = gfx.color;
2211                gfx.depth_queue
2212                    .push_triangle(z, color, x0, y0, x1, y1, x2, y2);
2213                return Ok(Value::Unit);
2214            },
2215
2216            // depth_line(x0,y0, x1,y1, z) — queue a depth-sorted line in the
2217            // current colour (same painter's queue as depth_triangle).
2218            "depth_line" | "เส้นเรียงลึก" | "深度线" | "深度線" | "깊이선" =>
2219            {
2220                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
2221                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
2222                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
2223                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
2224                let z = self.arg_num(&args, 4, 0.0)? as f32;
2225                let mut gfx = self.gfx.borrow_mut();
2226                let color = gfx.color;
2227                gfx.depth_queue.push_line(z, color, x0, y0, x1, y1);
2228                return Ok(Value::Unit);
2229            },
2230
2231            // ══════════════════════════════════════════════════════════════════
2232            // GRAPHICS BUILTINS
2233            // Thai names first, then English aliases.
2234            // ══════════════════════════════════════════════════════════════════
2235
2236            // ── เปิดหน้าต่าง(width, height, title) — open_window ──
2237            "เปิดหน้าต่าง" | "open_window" | "gfx_window" | "开窗" | "ウィンドウ開く" | "창열기" =>
2238            {
2239                let w = self.arg_num(&args, 0, 800.0)? as usize;
2240                let h = self.arg_num(&args, 1, 600.0)? as usize;
2241                #[cfg(not(target_arch = "wasm32"))]
2242                {
2243                    let title = args
2244                        .get(2)
2245                        .map(|v| v.to_string())
2246                        .unwrap_or_else(|| "Ling".into());
2247                    let mut gfx = self.gfx.borrow_mut();
2248                    let mut win = minifb::Window::new(
2249                        &title,
2250                        w,
2251                        h,
2252                        minifb::WindowOptions {
2253                            resize: false,
2254                            scale: minifb::Scale::X1,
2255                            ..Default::default()
2256                        },
2257                    )
2258                    .map_err(|e| EvalErr::from(format!("cannot open window: {e}")))?;
2259                    #[allow(deprecated)]
2260                    win.limit_update_rate(Some(std::time::Duration::from_millis(8)));
2261                    gfx.buffer = vec![0u32; w * h];
2262                    gfx.width = w;
2263                    gfx.height = h;
2264                    gfx.window = Some(win);
2265                    gfx.sync_projection();
2266                    hide_console_window();
2267                }
2268                #[cfg(target_arch = "wasm32")]
2269                {
2270                    let mut gfx = self.gfx.borrow_mut();
2271                    gfx.width = w;
2272                    gfx.height = h;
2273                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
2274                    gfx.sync_projection();
2275                    crate::gfx::webgl::resize(w as u32, h as u32);
2276                }
2277                return Ok(Value::Unit);
2278            },
2279
2280            // ── เติม(r, g, b) — fill / clear screen with colour ──
2281            "เติม" | "fill" | "gfx_fill" | "clear" | "填" | "塗り潰し" | "채우기" | "清"
2282            | "消去" | "지우기" => {
2283                let r = self.arg_num(&args, 0, 0.0)? as u32;
2284                let g = self.arg_num(&args, 1, 0.0)? as u32;
2285                let b = self.arg_num(&args, 2, 0.0)? as u32;
2286                #[cfg(not(target_arch = "wasm32"))]
2287                {
2288                    let c = (r << 16) | (g << 8) | b;
2289                    self.gfx.borrow_mut().buffer.fill(c);
2290                }
2291                #[cfg(target_arch = "wasm32")]
2292                {
2293                    let mut gfx = self.gfx.borrow_mut();
2294                    gfx.fill_r = r as f32 / 255.0;
2295                    gfx.fill_g = g as f32 / 255.0;
2296                    gfx.fill_b = b as f32 / 255.0;
2297                    // The web path renders into the software framebuffer and blits
2298                    // it at present(), so clear the buffer too (mirrors native).
2299                    let c = (r << 16) | (g << 8) | b;
2300                    gfx.buffer.fill(c);
2301                }
2302                return Ok(Value::Unit);
2303            },
2304
2305            // ── set_color_hsl(h, s, l) — set drawing colour from HSL ──
2306            // h: 0–360 degrees, s: 0–100 saturation, l: 0–100 lightness
2307            "set_color_hsl" | "颜色HSL" | "色相" | "HSL色" | "HSL색설정" | "สีHSLวาด" =>
2308            {
2309                let h = self.arg_num(&args, 0, 0.0)?;
2310                let s = self.arg_num(&args, 1, 70.0)?;
2311                let l = self.arg_num(&args, 2, 50.0)?;
2312                let hex = hsl_to_hex(h, s, l);
2313                let r = u32::from_str_radix(&hex[1..3], 16).unwrap_or(255);
2314                let g = u32::from_str_radix(&hex[3..5], 16).unwrap_or(255);
2315                let b = u32::from_str_radix(&hex[5..7], 16).unwrap_or(255);
2316                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
2317                return Ok(Value::Unit);
2318            },
2319
2320            // ── สีดินสอ(r, g, b) — set drawing colour ──
2321            "สีดินสอ" | "set_color" | "gfx_color" | "color" | "设色" | "色設定" | "색설정" =>
2322            {
2323                let r = self.arg_num(&args, 0, 255.0)? as u32;
2324                let g = self.arg_num(&args, 1, 255.0)? as u32;
2325                let b = self.arg_num(&args, 2, 255.0)? as u32;
2326                self.gfx.borrow_mut().color = (r << 16) | (g << 8) | b;
2327                return Ok(Value::Unit);
2328            },
2329
2330            // ── วาดสามเหลี่ยม(x1,y1, x2,y2, x3,y3) — draw filled triangle ──
2331            "วาดสามเหลี่ยม"
2332            | "draw_triangle"
2333            | "gfx_triangle"
2334            | "triangle"
2335            | "画三角"
2336            | "三角形描画"
2337            | "삼각형그리기" => {
2338                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
2339                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
2340                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
2341                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
2342                let x2 = self.arg_num(&args, 4, 0.0)? as f32;
2343                let y2 = self.arg_num(&args, 5, 0.0)? as f32;
2344                let mut gfx = self.gfx.borrow_mut();
2345                let color = gfx.color;
2346                #[cfg(not(target_arch = "wasm32"))]
2347                {
2348                    let w = gfx.width;
2349                    let h = gfx.height;
2350                    fill_triangle(&mut gfx.buffer, w, h, color, x0, y0, x1, y1, x2, y2);
2351                }
2352                #[cfg(target_arch = "wasm32")]
2353                gfx.depth_queue
2354                    .push_triangle(0.0, color, x0, y0, x1, y1, x2, y2);
2355                return Ok(Value::Unit);
2356            },
2357
2358            // ── วาดเส้น(x1,y1, x2,y2) — draw line ──
2359            "วาดเส้น" | "draw_line" | "gfx_line" | "line" | "画线" | "線描く" | "선그리기" =>
2360            {
2361                let x0 = self.arg_num(&args, 0, 0.0)? as f32;
2362                let y0 = self.arg_num(&args, 1, 0.0)? as f32;
2363                let x1 = self.arg_num(&args, 2, 0.0)? as f32;
2364                let y1 = self.arg_num(&args, 3, 0.0)? as f32;
2365                let mut gfx = self.gfx.borrow_mut();
2366                let color = gfx.color;
2367                #[cfg(not(target_arch = "wasm32"))]
2368                {
2369                    let w = gfx.width;
2370                    let h = gfx.height;
2371                    draw_line(&mut gfx.buffer, w, h, color, x0, y0, x1, y1);
2372                }
2373                #[cfg(target_arch = "wasm32")]
2374                gfx.depth_queue.push_line(0.0, color, x0, y0, x1, y1);
2375                return Ok(Value::Unit);
2376            },
2377
2378            // ── วาดจุด(x, y) — plot a single pixel ──
2379            "วาดจุด" | "draw_pixel" | "gfx_pixel" | "pixel" | "画点" | "点描く" | "점그리기" =>
2380            {
2381                let px = self.arg_num(&args, 0, 0.0)? as i32;
2382                let py = self.arg_num(&args, 1, 0.0)? as i32;
2383                #[cfg(not(target_arch = "wasm32"))]
2384                {
2385                    let mut gfx = self.gfx.borrow_mut();
2386                    let color = gfx.color;
2387                    let w = gfx.width;
2388                    let h = gfx.height;
2389                    if px >= 0 && py >= 0 && (px as usize) < w && (py as usize) < h {
2390                        gfx.buffer[py as usize * w + px as usize] = color;
2391                    }
2392                }
2393                #[cfg(target_arch = "wasm32")]
2394                {
2395                    // Render pixel as a 1×1 square via two triangles.
2396                    let mut gfx = self.gfx.borrow_mut();
2397                    let color = gfx.color;
2398                    let x = px as f32;
2399                    let y = py as f32;
2400                    gfx.depth_queue
2401                        .push_triangle(0.0, color, x, y, x + 1.0, y, x + 1.0, y + 1.0);
2402                    gfx.depth_queue
2403                        .push_triangle(0.0, color, x, y, x + 1.0, y + 1.0, x, y + 1.0);
2404                }
2405                return Ok(Value::Unit);
2406            },
2407
2408            // ── แสดงผล() — flush depth queue, then present frame to screen ──
2409            "แสดงผล" | "present" | "gfx_present" | "show" | "显" | "呈现" | "表示" | "표시" =>
2410            {
2411                #[cfg(not(target_arch = "wasm32"))]
2412                {
2413                    // Flush depth queue and present — release borrow before reading mouse.
2414                    {
2415                        let mut gfx = self.gfx.borrow_mut();
2416                        if !gfx.depth_queue.is_empty() {
2417                            let w = gfx.width;
2418                            let h = gfx.height;
2419                            let dt = gfx.depth_test;
2420                            let (bm, ba) = (gfx.blend, gfx.alpha);
2421                            let queue = std::mem::take(&mut gfx.depth_queue);
2422                            {
2423                                let g = &mut *gfx;
2424                                let z = if dt { Some(&mut g.depth_buf) } else { None };
2425                                queue.flush(&mut g.buffer, z, w, h);
2426                            }
2427                            gfx.depth_queue.set_state(bm, ba);
2428                        }
2429                        let buf = gfx.buffer.clone();
2430                        let w = gfx.width;
2431                        let h = gfx.height;
2432                        if let Some(win) = gfx.window.as_mut() {
2433                            win.update_with_buffer(&buf, w, h)
2434                                .map_err(|e| EvalErr::from(format!("present error: {e}")))?;
2435                        }
2436                    }
2437                    // Read mouse AFTER update_with_buffer so events are processed.
2438                    let mouse_pos = {
2439                        let gfx = self.gfx.borrow();
2440                        gfx.window
2441                            .as_ref()
2442                            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
2443                    };
2444                    let mut gfx = self.gfx.borrow_mut();
2445                    if gfx.mouse_captured {
2446                        let w = gfx.width as f32;
2447                        let h = gfx.height as f32;
2448                        if let Some((mx, my)) = mouse_pos {
2449                            if gfx.last_mx.is_nan() {
2450                                gfx.mouse_dx = 0.0;
2451                                gfx.mouse_dy = 0.0;
2452                                gfx.last_mx = mx;
2453                                gfx.last_my = my;
2454                            } else {
2455                                gfx.mouse_dx = mx - gfx.last_mx;
2456                                gfx.mouse_dy = my - gfx.last_my;
2457                                // Wrap the cursor at every edge (L/R/U/D) → infinite look
2458                                // on both axes, and the cursor is NOT trapped (alt-tab works).
2459                                let margin = 6.0;
2460                                let (mut nx, mut ny, mut warp) = (mx, my, false);
2461                                if mx < margin {
2462                                    nx = w - margin - 2.0;
2463                                    warp = true;
2464                                } else if mx > w - margin {
2465                                    nx = margin + 2.0;
2466                                    warp = true;
2467                                }
2468                                if my < margin {
2469                                    ny = h - margin - 2.0;
2470                                    warp = true;
2471                                } else if my > h - margin {
2472                                    ny = margin + 2.0;
2473                                    warp = true;
2474                                }
2475                                if warp {
2476                                    #[cfg(windows)]
2477                                    unsafe {
2478                                        #[repr(C)]
2479                                        struct RECT {
2480                                            left: i32,
2481                                            top: i32,
2482                                            right: i32,
2483                                            bottom: i32,
2484                                        }
2485                                        extern "system" {
2486                                            fn GetForegroundWindow() -> isize;
2487                                            fn GetWindowRect(hwnd: isize, lpRect: *mut RECT)
2488                                                -> i32;
2489                                            fn SetCursorPos(x: i32, y: i32) -> i32;
2490                                        }
2491                                        let hwnd = GetForegroundWindow();
2492                                        let mut rect =
2493                                            RECT { left: 0, top: 0, right: 0, bottom: 0 };
2494                                        if GetWindowRect(hwnd, &mut rect) != 0 {
2495                                            SetCursorPos(
2496                                                rect.left + nx as i32,
2497                                                rect.top + ny as i32,
2498                                            );
2499                                        }
2500                                    }
2501                                    gfx.last_mx = nx;
2502                                    gfx.last_my = ny;
2503                                } else {
2504                                    gfx.last_mx = mx;
2505                                    gfx.last_my = my;
2506                                }
2507                            }
2508                        } else {
2509                            gfx.mouse_dx = 0.0;
2510                            gfx.mouse_dy = 0.0;
2511                        }
2512                    } else if let Some((mx, my)) = mouse_pos {
2513                        if gfx.last_mx.is_nan() {
2514                            gfx.mouse_dx = 0.0;
2515                            gfx.mouse_dy = 0.0;
2516                        } else {
2517                            gfx.mouse_dx = mx - gfx.last_mx;
2518                            gfx.mouse_dy = my - gfx.last_my;
2519                        }
2520                        gfx.last_mx = mx;
2521                        gfx.last_my = my;
2522                    } else {
2523                        gfx.mouse_dx = 0.0;
2524                        gfx.mouse_dy = 0.0;
2525                    }
2526                }
2527                #[cfg(target_arch = "wasm32")]
2528                {
2529                    // Software-render everything (3-D depth queue + 2-D vtex/ui that
2530                    // already wrote into the buffer) into the framebuffer, exactly
2531                    // like native, then upload that buffer to the canvas in one blit.
2532                    let mut gfx = self.gfx.borrow_mut();
2533                    let w = gfx.width;
2534                    let h = gfx.height;
2535                    if gfx.buffer.len() != w * h {
2536                        gfx.buffer.resize(w * h, 0);
2537                    }
2538                    if !gfx.depth_queue.is_empty() {
2539                        let dt = gfx.depth_test;
2540                        let queue = std::mem::take(&mut gfx.depth_queue);
2541                        let g = &mut *gfx;
2542                        let z = if dt { Some(&mut g.depth_buf) } else { None };
2543                        queue.flush(&mut g.buffer, z, w, h);
2544                    }
2545                    crate::gfx::webgl::blit_rgb(&gfx.buffer, w, h);
2546                }
2547                // Update the click-edge latch for interactive UI widgets.
2548                #[cfg(not(target_arch = "wasm32"))]
2549                {
2550                    let (_, _, down) = self.mouse_now();
2551                    self.mouse_was_down = down;
2552                }
2553                // Increment frame counter
2554                self.frame_num += 1;
2555                return Ok(Value::Unit);
2556            },
2557
2558            // ── เปิดหน้าต่างเต็มจอ(title) — true native-res fullscreen window ──
2559            "เปิดหน้าต่างเต็มจอ"
2560            | "open_fullscreen"
2561            | "fullscreen"
2562            | "全屏"
2563            | "全画面"
2564            | "전체화면" => {
2565                // In WASM the canvas defines the viewport; use its current size
2566                // as the default so the projection matches what's actually visible.
2567                #[cfg(target_arch = "wasm32")]
2568                let (default_w, default_h) = {
2569                    let (cw, ch) = crate::gfx::webgl::canvas_size();
2570                    (cw as f64, ch as f64)
2571                };
2572                // On native: query the actual primary monitor resolution.
2573                #[cfg(all(not(target_arch = "wasm32"), windows))]
2574                let (default_w, default_h) = unsafe {
2575                    extern "system" {
2576                        fn GetSystemMetrics(nIndex: i32) -> i32;
2577                    }
2578                    (GetSystemMetrics(0) as f64, GetSystemMetrics(1) as f64)
2579                };
2580                #[cfg(all(not(target_arch = "wasm32"), not(windows)))]
2581                let (default_w, default_h) = native_screen_size();
2582
2583                let w = args
2584                    .get(1)
2585                    .map(|v| self.to_number(v).unwrap_or(default_w) as usize)
2586                    .unwrap_or(default_w as usize);
2587                let h = args
2588                    .get(2)
2589                    .map(|v| self.to_number(v).unwrap_or(default_h) as usize)
2590                    .unwrap_or(default_h as usize);
2591                #[cfg(not(target_arch = "wasm32"))]
2592                {
2593                    let title = args
2594                        .get(0)
2595                        .map(|v| v.to_string())
2596                        .unwrap_or_else(|| "Ling".into());
2597                    let mut gfx = self.gfx.borrow_mut();
2598                    let mut win = minifb::Window::new(
2599                        &title,
2600                        w,
2601                        h,
2602                        minifb::WindowOptions {
2603                            borderless: true,
2604                            title: false,
2605                            resize: false,
2606                            topmost: true,
2607                            scale: minifb::Scale::X1,
2608                            ..Default::default()
2609                        },
2610                    )
2611                    .map_err(|e| EvalErr::from(format!("cannot open fullscreen: {e}")))?;
2612                    // Drive the loop at the monitor's real refresh rate instead of
2613                    // a hard-coded cap, so a 144 Hz panel runs at 144 fps.
2614                    win.set_target_fps(monitor_info().2.max(30) as usize);
2615                    // Grab the native handle *before* moving the window into gfx.
2616                    #[cfg(windows)]
2617                    let hwnd = win.get_window_handle() as isize;
2618                    gfx.buffer = vec![0u32; w * h];
2619                    gfx.width = w;
2620                    gfx.height = h;
2621                    gfx.window = Some(win);
2622                    gfx.sync_projection();
2623                    // Strip all chrome and cover the full screen, above the taskbar.
2624                    #[cfg(windows)]
2625                    make_borderless_fullscreen(hwnd, w as i32, h as i32);
2626                    hide_console_window();
2627                }
2628                #[cfg(target_arch = "wasm32")]
2629                {
2630                    let mut gfx = self.gfx.borrow_mut();
2631                    gfx.width = w;
2632                    gfx.height = h;
2633                    gfx.buffer.resize(w * h, 0); // keep the CPU framebuffer in sync
2634                    gfx.sync_projection();
2635                    crate::gfx::webgl::resize(w as u32, h as u32);
2636                }
2637                return Ok(Value::Unit);
2638            },
2639
2640            // ── ความกว้าง() / ความสูง() — current framebuffer size ──
2641            "get_width" | "ความกว้าง" | "宽" | "幅取得" | "너비" => {
2642                return Ok(Value::Number(self.gfx.borrow().width as f64));
2643            },
2644            "get_height" | "ความสูง" | "高" | "高取得" | "높이" => {
2645                return Ok(Value::Number(self.gfx.borrow().height as f64));
2646            },
2647
2648            // ── monitor detection: physical display, not the framebuffer ──────
2649            // monitor_width() → primary-monitor pixel width
2650            "monitor_width" | "screen_width" | "屏宽" | "画面幅" | "화면너비" | "ความกว้างจอ" =>
2651            {
2652                return Ok(Value::Number(monitor_info().0 as f64));
2653            },
2654            // monitor_height() → primary-monitor pixel height
2655            "monitor_height" | "screen_height" | "屏高" | "画面高" | "화면높이" | "ความสูงจอ" =>
2656            {
2657                return Ok(Value::Number(monitor_info().1 as f64));
2658            },
2659            // monitor_refresh() → refresh rate in Hz (a.k.a. the monitor framerate)
2660            "monitor_refresh"
2661            | "monitor_hz"
2662            | "monitor_fps"
2663            | "refresh_rate"
2664            | "刷新率"
2665            | "リフレッシュレート"
2666            | "주사율"
2667            | "อัตรารีเฟรช" => {
2668                return Ok(Value::Number(monitor_info().2 as f64));
2669            },
2670            // monitor_info() → [width, height, refresh_hz]
2671            "monitor_info" | "screen_info" | "屏幕信息" | "画面情報" | "화면정보" | "ข้อมูลจอ" =>
2672            {
2673                let (w, h, hz) = monitor_info();
2674                return Ok(Value::List(vec![
2675                    Value::Number(w as f64),
2676                    Value::Number(h as f64),
2677                    Value::Number(hz as f64),
2678                ]));
2679            },
2680            // set_fps(n) → cap the render loop at n frames per second
2681            "set_fps"
2682            | "set_target_fps"
2683            | "target_fps"
2684            | "设帧率"
2685            | "フレームレート設定"
2686            | "프레임설정"
2687            | "ตั้งเฟรมเรต" => {
2688                let fps = self.arg_num(&args, 0, 60.0)?.max(1.0) as usize;
2689                #[cfg(not(target_arch = "wasm32"))]
2690                {
2691                    let mut gfx = self.gfx.borrow_mut();
2692                    if let Some(win) = gfx.window.as_mut() {
2693                        win.set_target_fps(fps);
2694                    }
2695                }
2696                return Ok(Value::Unit);
2697            },
2698
2699            // ── หน้าต่างเปิดอยู่() → bool — is the window still open? ──
2700            "หน้าต่างเปิดอยู่"
2701            | "window_is_open"
2702            | "gfx_is_open"
2703            | "is_open"
2704            | "窗开"
2705            | "開いている"
2706            | "창열림" => {
2707                #[cfg(not(target_arch = "wasm32"))]
2708                {
2709                    let gfx = self.gfx.borrow();
2710                    let open = gfx
2711                        .window
2712                        .as_ref()
2713                        .map(|w| w.is_open() && !w.is_key_down(minifb::Key::Escape))
2714                        .unwrap_or(false);
2715                    return Ok(Value::Bool(open));
2716                }
2717                #[cfg(target_arch = "wasm32")]
2718                return Ok(Value::Bool(true));
2719            },
2720
2721            // ── key_down(name) → bool — is a key held? ──
2722            "key_down" | "กดค้าง" | "按键" | "キー押す" | "키누름" => {
2723                #[cfg(not(target_arch = "wasm32"))]
2724                {
2725                    let name = self.arg_str(&args, 0, "");
2726                    let gfx = self.gfx.borrow();
2727                    let down = gfx
2728                        .window
2729                        .as_ref()
2730                        .and_then(|w| str_to_minifb_key(&name).map(|k| w.is_key_down(k)))
2731                        .unwrap_or(false);
2732                    return Ok(Value::Bool(down));
2733                }
2734                #[cfg(target_arch = "wasm32")]
2735                return Ok(Value::Bool(false));
2736            },
2737
2738            // ── key_pressed(name) → bool — was a key pressed this frame? ──
2739            "key_pressed" | "กดปุ่ม" | "键按" | "キー押した" | "키눌림" => {
2740                #[cfg(not(target_arch = "wasm32"))]
2741                {
2742                    let name = self.arg_str(&args, 0, "");
2743                    let pressed = {
2744                        let gfx = self.gfx.borrow();
2745                        gfx.window
2746                            .as_ref()
2747                            .and_then(|w| {
2748                                str_to_minifb_key(&name)
2749                                    .map(|k| w.is_key_pressed(k, minifb::KeyRepeat::No))
2750                            })
2751                            .unwrap_or(false)
2752                    };
2753                    // gamepad Start behaves like Enter everywhere
2754                    let pressed =
2755                        pressed || ((name == "enter" || name == "return") && gamepad::start_edge());
2756                    return Ok(Value::Bool(pressed));
2757                }
2758                #[cfg(target_arch = "wasm32")]
2759                return Ok(Value::Bool(false));
2760            },
2761
2762            // ── mouse_dx() / mouse_dy() → f64 — delta since last frame ──
2763            "mouse_dx" | "เมาส์X" | "鼠ΔX" | "マウスΔX" | "마우스ΔX" => {
2764                #[cfg(not(target_arch = "wasm32"))]
2765                return Ok(Value::Number(self.gfx.borrow().mouse_dx as f64));
2766                #[cfg(target_arch = "wasm32")]
2767                return Ok(Value::Number(0.0));
2768            },
2769            // ── mouse_scroll() → f64 — vertical scroll-wheel delta this frame ──
2770            #[cfg(not(target_arch = "wasm32"))]
2771            "mouse_scroll" | "ล้อเมาส์" | "滚轮" | "ホイール" | "스크롤" =>
2772            {
2773                let gfx = self.gfx.borrow();
2774                let s = gfx
2775                    .window
2776                    .as_ref()
2777                    .and_then(|w| w.get_scroll_wheel())
2778                    .map(|(_, y)| y as f64)
2779                    .unwrap_or(0.0);
2780                return Ok(Value::Number(s));
2781            },
2782            "mouse_dy" | "เมาส์Y" | "鼠ΔY" | "マウスΔY" | "마우스ΔY" => {
2783                #[cfg(not(target_arch = "wasm32"))]
2784                return Ok(Value::Number(self.gfx.borrow().mouse_dy as f64));
2785                #[cfg(target_arch = "wasm32")]
2786                return Ok(Value::Number(0.0));
2787            },
2788
2789            // ── Gamepad / joystick input (ling-input "Sensorium" + gilrs) ──
2790            // pad_poll() → number — advance input one frame; returns # connected pads.
2791            "pad_poll" | "手柄轮询" | "パッド更新" | "패드폴링" | "อัปเดตแพด" =>
2792            {
2793                #[cfg(not(target_arch = "wasm32"))]
2794                return Ok(Value::Number(self.pad_poll() as f64));
2795                #[cfg(target_arch = "wasm32")]
2796                return Ok(Value::Number(0.0));
2797            },
2798            // pad_count() → number — connected gamepads.
2799            "pad_count" | "手柄数" | "パッド数" | "패드수" | "จำนวนแพด" =>
2800            {
2801                #[cfg(not(target_arch = "wasm32"))]
2802                {
2803                    let inp = self.input.borrow();
2804                    let n = inp.as_ref().map_or(0, |s| s.sensorium.devices.count());
2805                    return Ok(Value::Number(n as f64));
2806                }
2807                #[cfg(target_arch = "wasm32")]
2808                return Ok(Value::Number(0.0));
2809            },
2810            // pad_connected(i) → bool.
2811            "pad_connected" | "手柄连接" | "パッド接続" | "패드연결" | "แพดเชื่อม" =>
2812            {
2813                #[cfg(not(target_arch = "wasm32"))]
2814                {
2815                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2816                    let inp = self.input.borrow();
2817                    let c = inp
2818                        .as_ref()
2819                        .is_some_and(|s| s.sensorium.devices.for_player(i as u8).is_some());
2820                    return Ok(Value::Bool(c));
2821                }
2822                #[cfg(target_arch = "wasm32")]
2823                return Ok(Value::Bool(false));
2824            },
2825            // pad_button(i, name) → bool — is the button held?
2826            "pad_button" | "手柄按键" | "パッドボタン" | "패드버튼" | "ปุ่มแพด" =>
2827            {
2828                #[cfg(not(target_arch = "wasm32"))]
2829                {
2830                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2831                    let name = self.arg_str(&args, 1, "");
2832                    let down = parse_pad_button(&name)
2833                        .is_some_and(|b| self.with_pad(i, false, |p| p.is_down(b)));
2834                    return Ok(Value::Bool(down));
2835                }
2836                #[cfg(target_arch = "wasm32")]
2837                return Ok(Value::Bool(false));
2838            },
2839            // pad_pressed(i, name) → bool — pressed this frame?
2840            "pad_pressed" | "手柄按下" | "パッド押下" | "패드눌림" | "แพดกด" =>
2841            {
2842                #[cfg(not(target_arch = "wasm32"))]
2843                {
2844                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2845                    let name = self.arg_str(&args, 1, "");
2846                    let p = parse_pad_button(&name)
2847                        .is_some_and(|b| self.with_pad(i, false, |g| g.just_pressed(b)));
2848                    return Ok(Value::Bool(p));
2849                }
2850                #[cfg(target_arch = "wasm32")]
2851                return Ok(Value::Bool(false));
2852            },
2853            // pad_lx(i)/pad_ly(i)/pad_rx(i)/pad_ry(i) → number — stick axes (−1..=1).
2854            "pad_lx" | "手柄左X" | "パッド左X" | "패드왼X" | "แพดซ้ายX" => {
2855                #[cfg(not(target_arch = "wasm32"))]
2856                {
2857                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2858                    return Ok(Value::Number(
2859                        self.with_pad(i, 0.0, |p| p.left_stick.x as f64),
2860                    ));
2861                }
2862                #[cfg(target_arch = "wasm32")]
2863                return Ok(Value::Number(0.0));
2864            },
2865            "pad_ly" | "手柄左Y" | "パッド左Y" | "패드왼Y" | "แพดซ้ายY" => {
2866                #[cfg(not(target_arch = "wasm32"))]
2867                {
2868                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2869                    return Ok(Value::Number(
2870                        self.with_pad(i, 0.0, |p| p.left_stick.y as f64),
2871                    ));
2872                }
2873                #[cfg(target_arch = "wasm32")]
2874                return Ok(Value::Number(0.0));
2875            },
2876            "pad_rx" | "手柄右X" | "パッド右X" | "패드오X" | "แพดขวาX" => {
2877                #[cfg(not(target_arch = "wasm32"))]
2878                {
2879                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2880                    return Ok(Value::Number(
2881                        self.with_pad(i, 0.0, |p| p.right_stick.x as f64),
2882                    ));
2883                }
2884                #[cfg(target_arch = "wasm32")]
2885                return Ok(Value::Number(0.0));
2886            },
2887            "pad_ry" | "手柄右Y" | "パッド右Y" | "패드오Y" | "แพดขวาY" => {
2888                #[cfg(not(target_arch = "wasm32"))]
2889                {
2890                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2891                    return Ok(Value::Number(
2892                        self.with_pad(i, 0.0, |p| p.right_stick.y as f64),
2893                    ));
2894                }
2895                #[cfg(target_arch = "wasm32")]
2896                return Ok(Value::Number(0.0));
2897            },
2898            // pad_lt(i)/pad_rt(i) → number — analog triggers (0..=1).
2899            "pad_lt" | "手柄左扳机" | "パッド左トリガー" | "패드왼트리거" | "ไกแพดซ้าย" =>
2900            {
2901                #[cfg(not(target_arch = "wasm32"))]
2902                {
2903                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2904                    return Ok(Value::Number(
2905                        self.with_pad(i, 0.0, |p| p.left_trigger as f64),
2906                    ));
2907                }
2908                #[cfg(target_arch = "wasm32")]
2909                return Ok(Value::Number(0.0));
2910            },
2911            "pad_rt" | "手柄右扳机" | "パッド右トリガー" | "패드오트리거" | "ไกแพดขวา" =>
2912            {
2913                #[cfg(not(target_arch = "wasm32"))]
2914                {
2915                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2916                    return Ok(Value::Number(
2917                        self.with_pad(i, 0.0, |p| p.right_trigger as f64),
2918                    ));
2919                }
2920                #[cfg(target_arch = "wasm32")]
2921                return Ok(Value::Number(0.0));
2922            },
2923            // pad_rumble(i, lo, hi) → unit — set rumble motor amplitudes (0..=1).
2924            "pad_rumble" | "手柄震动" | "パッド振動" | "패드진동" | "แพดสั่น" =>
2925            {
2926                #[cfg(not(target_arch = "wasm32"))]
2927                {
2928                    use ling_input::backend::InputBackend;
2929                    let i = self.arg_num(&args, 0, 0.0)? as usize;
2930                    let lo = self.arg_num(&args, 1, 0.0)? as f32;
2931                    let hi = self.arg_num(&args, 2, lo as f64)? as f32;
2932                    let mut inp = self.input.borrow_mut();
2933                    if let Some(s) = inp.as_mut() {
2934                        if let Some(dev) = s.sensorium.devices.for_player(i as u8).map(|d| d.id) {
2935                            s.backend.set_rumble(
2936                                dev,
2937                                ling_input::Rumble { low: lo, high: hi, ..Default::default() },
2938                            );
2939                        }
2940                    }
2941                    return Ok(Value::Unit);
2942                }
2943                #[cfg(target_arch = "wasm32")]
2944                return Ok(Value::Unit);
2945            },
2946
2947            // ── set_camera_pos(x, y, z) — move camera to world position ──
2948            "set_camera_pos" | "ตั้งตำแหน่งกล้อง" | "镜坐标" | "カメラ座標" | "카메라좌표" =>
2949            {
2950                let x = self.arg_num(&args, 0, 0.0)? as f32;
2951                let y = self.arg_num(&args, 1, 0.0)? as f32;
2952                let z = self.arg_num(&args, 2, 0.0)? as f32;
2953                {
2954                    let mut gfx = self.gfx.borrow_mut();
2955                    gfx.camera.tx = x;
2956                    gfx.camera.ty = y;
2957                    gfx.camera.tz = z;
2958                }
2959                #[cfg(not(target_arch = "wasm32"))]
2960                if let Some(audio) = &self.audio {
2961                    audio.set_listener_pos(x, y, z);
2962                }
2963                return Ok(Value::Unit);
2964            },
2965
2966            // ── move_camera(dx, dy, dz) — translate camera by delta ──
2967            "move_camera" => {
2968                let dx = self.arg_num(&args, 0, 0.0)? as f32;
2969                let dy = self.arg_num(&args, 1, 0.0)? as f32;
2970                let dz = self.arg_num(&args, 2, 0.0)? as f32;
2971                let mut gfx = self.gfx.borrow_mut();
2972                gfx.camera.tx += dx;
2973                gfx.camera.ty += dy;
2974                gfx.camera.tz += dz;
2975                return Ok(Value::Unit);
2976            },
2977
2978            // ── set_zdist(d) — set perspective z-offset (field-of-view taper) ──
2979            "set_zdist" | "ตั้งระยะห่าง" | "镜距" | "Z距離設定" | "Z거리설정" =>
2980            {
2981                let d = self.arg_num(&args, 0, 5.0)? as f32;
2982                self.gfx.borrow_mut().camera.zdist = d;
2983                return Ok(Value::Unit);
2984            },
2985
2986            // ── capture_mouse() — hide cursor and warp to centre each frame ──
2987            "capture_mouse" | "จับเมาส์" | "捕鼠" | "マウス捕捉" | "마우스잡기" =>
2988            {
2989                #[cfg(not(target_arch = "wasm32"))]
2990                {
2991                    let mut gfx = self.gfx.borrow_mut();
2992                    gfx.mouse_captured = true;
2993                    gfx.last_mx = f32::NAN;
2994                    if let Some(win) = gfx.window.as_mut() {
2995                        win.set_cursor_visibility(false);
2996                    }
2997                }
2998                return Ok(Value::Unit);
2999            },
3000
3001            // ── release_mouse() — restore cursor and remove clip region ──
3002            "release_mouse" => {
3003                #[cfg(not(target_arch = "wasm32"))]
3004                {
3005                    let mut gfx = self.gfx.borrow_mut();
3006                    gfx.mouse_captured = false;
3007                    gfx.last_mx = f32::NAN;
3008                    if let Some(win) = gfx.window.as_mut() {
3009                        win.set_cursor_visibility(true);
3010                    }
3011                    #[cfg(windows)]
3012                    unsafe {
3013                        // Null releases the clip; reuse the RECT-typed declaration above.
3014                        extern "system" {
3015                            fn ClipCursor(lpRect: *const std::ffi::c_void) -> i32;
3016                        }
3017                        ClipCursor(std::ptr::null());
3018                    }
3019                }
3020                return Ok(Value::Unit);
3021            },
3022
3023            // ══════════════════════════════════════════════════════════════════
3024            // 3-D / 4-D DRAWING — camera, lights, depth-sorted geometry
3025            // ══════════════════════════════════════════════════════════════════
3026
3027            // ── set_camera(cry, sry, crx, srx) — store precomputed camera trig ──
3028            // Call once per frame after computing cos/sin of your rotation angles.
3029            "set_camera" | "ตั้งกล้อง" | "设镜" | "设置摄像机" | "カメラ設定" | "카메라설정" =>
3030            {
3031                let cry = self.arg_num(&args, 0, 1.0)? as f32;
3032                let sry = self.arg_num(&args, 1, 0.0)? as f32;
3033                let crx = self.arg_num(&args, 2, 1.0)? as f32;
3034                let srx = self.arg_num(&args, 3, 0.0)? as f32;
3035                let mut gfx = self.gfx.borrow_mut();
3036                gfx.camera.cry = cry;
3037                gfx.camera.sry = sry;
3038                gfx.camera.crx = crx;
3039                gfx.camera.srx = srx;
3040                return Ok(Value::Unit);
3041            },
3042
3043            // ── set_projection(cx, cy, focal, zdist) — override projection params ──
3044            // Automatically set when the window opens; override only if needed.
3045            "set_projection" | "ตั้งโปรเจกชัน" | "投影" | "投影設定" | "투영설정" =>
3046            {
3047                let cx = self.arg_num(&args, 0, 960.0)? as f32;
3048                let cy = self.arg_num(&args, 1, 540.0)? as f32;
3049                let focal = self.arg_num(&args, 2, 1080.0)? as f32;
3050                let zdist = self.arg_num(&args, 3, 5.0)? as f32;
3051                let mut gfx = self.gfx.borrow_mut();
3052                gfx.camera.cx = cx;
3053                gfx.camera.cy = cy;
3054                gfx.camera.focal = focal;
3055                gfx.camera.zdist = zdist;
3056                return Ok(Value::Unit);
3057            },
3058
3059            // ── draw_mesh(pos, idx, ox, oy, oz, scale, mode) ──
3060            //   Native batched triangle mesh. pos = flat [x,y,z,…], idx = flat tri indices.
3061            //   mode 0 = lit with current pen colour; 1 = per-face hue cycle.
3062            //   Vertices are batch-projected via ling-gpu (CPU fallback, or CUDA when the
3063            //   `cuda` feature is on); the per-triangle loop runs natively (not in the
3064            //   interpreter) so dense meshes (imported glTF, grids) stay fast.
3065            "draw_mesh" | "วาดเมช" => {
3066                let pos = match args.first() {
3067                    Some(Value::List(v)) => v,
3068                    _ => return Ok(Value::Unit),
3069                };
3070                let idx = match args.get(1) {
3071                    Some(Value::List(v)) => v,
3072                    _ => return Ok(Value::Unit),
3073                };
3074                let ox = self.arg_num(&args, 2, 0.0)? as f32;
3075                let oy = self.arg_num(&args, 3, 0.0)? as f32;
3076                let oz = self.arg_num(&args, 4, 0.0)? as f32;
3077                let scale = self.arg_num(&args, 5, 1.0)? as f32;
3078                let mode = self.arg_num(&args, 6, 0.0)? as i64;
3079                let nv = pos.len() / 3;
3080                if nv == 0 {
3081                    return Ok(Value::Unit);
3082                }
3083                let mut world = vec![0.0f32; nv * 3];
3084                for i in 0..nv {
3085                    world[i * 3] = ox + self.to_number(&pos[i * 3]).unwrap_or(0.0) as f32 * scale;
3086                    world[i * 3 + 1] =
3087                        oy + self.to_number(&pos[i * 3 + 1]).unwrap_or(0.0) as f32 * scale;
3088                    world[i * 3 + 2] =
3089                        oz + self.to_number(&pos[i * 3 + 2]).unwrap_or(0.0) as f32 * scale;
3090                }
3091                let mut gfx = self.gfx.borrow_mut();
3092                let cp = {
3093                    let c = &gfx.camera;
3094                    ling_gpu::CameraParams {
3095                        cry: c.cry,
3096                        sry: c.sry,
3097                        crx: c.crx,
3098                        srx: c.srx,
3099                        cx: c.cx,
3100                        cy: c.cy,
3101                        focal: c.focal,
3102                        zdist: c.zdist,
3103                        tx: c.tx,
3104                        ty: c.ty,
3105                        tz: c.tz,
3106                    }
3107                };
3108                let near = -gfx.camera.zdist + 0.02;
3109                let base = gfx.color;
3110                let ambient = gfx.ambient;
3111                let mut proj = vec![0.0f32; nv * 3]; // (sx, sy, depth) per vertex
3112                ling_gpu::backend().project_points(&world, &cp, &mut proj);
3113                let nt = idx.len() / 3;
3114                for t in 0..nt {
3115                    let ia = self.to_number(&idx[t * 3]).unwrap_or(0.0) as usize;
3116                    let ib = self.to_number(&idx[t * 3 + 1]).unwrap_or(0.0) as usize;
3117                    let ic = self.to_number(&idx[t * 3 + 2]).unwrap_or(0.0) as usize;
3118                    if ia >= nv || ib >= nv || ic >= nv {
3119                        continue;
3120                    }
3121                    let (da, db, dc) = (proj[ia * 3 + 2], proj[ib * 3 + 2], proj[ic * 3 + 2]);
3122                    if (da + db + dc) / 3.0 <= near {
3123                        continue;
3124                    } // near-plane cull (centroid)
3125                    let col = if mode == 1 {
3126                        let h = t as f32 * 0.6;
3127                        let r = ((h.sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
3128                        let g = (((h + 2.094).sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
3129                        let b = (((h + 4.189).sin() * 0.5 + 0.5) * 150.0 + 55.0) as u32;
3130                        (r << 16) | (g << 8) | b
3131                    } else {
3132                        let (ax, ay, az) = (world[ia * 3], world[ia * 3 + 1], world[ia * 3 + 2]);
3133                        let (bx, by, bz) = (world[ib * 3], world[ib * 3 + 1], world[ib * 3 + 2]);
3134                        let (px, py, pz) = (world[ic * 3], world[ic * 3 + 1], world[ic * 3 + 2]);
3135                        let (ux, uy, uz) = (bx - ax, by - ay, bz - az);
3136                        let (vx, vy, vz) = (px - ax, py - ay, pz - az);
3137                        let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
3138                        let centroid = [
3139                            (ax + bx + px) / 3.0,
3140                            (ay + by + py) / 3.0,
3141                            (az + bz + pz) / 3.0,
3142                        ];
3143                        crate::gfx::light::compute_lit_color(
3144                            base,
3145                            normal,
3146                            centroid,
3147                            &gfx.lights,
3148                            ambient,
3149                        )
3150                    };
3151                    let depth = (da + db + dc) / 3.0;
3152                    let col = gfx.fog_apply(col, depth);
3153                    gfx.depth_queue.push_triangle(
3154                        depth,
3155                        col,
3156                        proj[ia * 3],
3157                        proj[ia * 3 + 1],
3158                        proj[ib * 3],
3159                        proj[ib * 3 + 1],
3160                        proj[ic * 3],
3161                        proj[ic * 3 + 1],
3162                    );
3163                }
3164                return Ok(Value::Unit);
3165            },
3166
3167            // ── add_light(x, y, z, r, g, b, intensity, radius) ──
3168            // Adds a point light in world space.  r/g/b in [0..1].
3169            // radius == 0 → no distance falloff.
3170            "add_light" | "เพิ่มแสง" | "加灯" | "ライト追加" | "조명추가" =>
3171            {
3172                let x = self.arg_num(&args, 0, 0.0)? as f32;
3173                let y = self.arg_num(&args, 1, -3.0)? as f32;
3174                let z = self.arg_num(&args, 2, 3.0)? as f32;
3175                let mut r = self.arg_num(&args, 3, 1.0)? as f32;
3176                let mut g = self.arg_num(&args, 4, 1.0)? as f32;
3177                let mut b = self.arg_num(&args, 5, 1.0)? as f32;
3178                // Forgive 0-255 colour values: if any channel is clearly > 1,
3179                // treat the triple as 0-255 and normalise. Keeps 0-1 callers exact.
3180                if r > 1.5 || g > 1.5 || b > 1.5 {
3181                    r /= 255.0;
3182                    g /= 255.0;
3183                    b /= 255.0;
3184                }
3185                let intensity = self.arg_num(&args, 6, 1.0)? as f32;
3186                let radius = self.arg_num(&args, 7, 0.0)? as f32;
3187                self.gfx
3188                    .borrow_mut()
3189                    .lights
3190                    .push(Light { x, y, z, r, g, b, intensity, radius });
3191                return Ok(Value::Unit);
3192            },
3193
3194            // ── clear_lights() — remove all lights ──
3195            "clear_lights" | "ล้างแสง" | "清灯" | "ライト消去" | "조명초기화" =>
3196            {
3197                self.gfx.borrow_mut().lights.clear();
3198                return Ok(Value::Unit);
3199            },
3200
3201            // ── set_ambient(v) — ambient light level [0..1] ──
3202            "set_ambient" | "ตั้งแสงรอบข้าง" | "环境光" | "環境光設定" | "환경광설정" =>
3203            {
3204                let v = self.arg_num(&args, 0, 0.15)? as f32;
3205                self.gfx.borrow_mut().ambient = v;
3206                return Ok(Value::Unit);
3207            },
3208
3209            // ── set_fog(r,g,b, start, end) — distance fog toward (r,g,b).
3210            //    triangles/lines fade from `start`..`end` camera depth. end<=0 = off.
3211            "set_fog" | "ตั้งหมอก" | "雾" | "霧設定" | "안개설정" => {
3212                let r = self.arg_num(&args, 0, 0.0)?.clamp(0.0, 255.0) as u32;
3213                let g = self.arg_num(&args, 1, 0.0)?.clamp(0.0, 255.0) as u32;
3214                let b = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 255.0) as u32;
3215                let start = self.arg_num(&args, 3, 0.0)? as f32;
3216                let end = self.arg_num(&args, 4, 0.0)? as f32;
3217                let mut gfx = self.gfx.borrow_mut();
3218                gfx.fog_color = (r << 16) | (g << 8) | b;
3219                gfx.fog_start = start;
3220                gfx.fog_end = end;
3221                return Ok(Value::Unit);
3222            },
3223
3224            // ── วาดสามเหลี่ยม3มิติ(ax,ay,az, bx,by,bz, cx,cy,cz) ──
3225            // Computes lighting from world-space normal + active lights (cel shading),
3226            // projects via the stored camera, and pushes to the depth queue.
3227            "วาดสามเหลี่ยม3มิติ" | "draw_triangle_3d" | "triangle3d" =>
3228            {
3229                let ax = self.arg_num(&args, 0, 0.0)? as f32;
3230                let ay = self.arg_num(&args, 1, 0.0)? as f32;
3231                let az = self.arg_num(&args, 2, 0.0)? as f32;
3232                let bx = self.arg_num(&args, 3, 0.0)? as f32;
3233                let by = self.arg_num(&args, 4, 0.0)? as f32;
3234                let bz = self.arg_num(&args, 5, 0.0)? as f32;
3235                let cx = self.arg_num(&args, 6, 0.0)? as f32;
3236                let cy = self.arg_num(&args, 7, 0.0)? as f32;
3237                let cz = self.arg_num(&args, 8, 0.0)? as f32;
3238
3239                let mut gfx = self.gfx.borrow_mut();
3240
3241                // World-space face normal  N = (B−A) × (C−A)
3242                let ux = bx - ax;
3243                let uy = by - ay;
3244                let uz = bz - az;
3245                let vx = cx - ax;
3246                let vy = cy - ay;
3247                let vz = cz - az;
3248                let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
3249                // World-space centroid
3250                let centroid = [
3251                    (ax + bx + cx) / 3.0,
3252                    (ay + by + cy) / 3.0,
3253                    (az + bz + cz) / 3.0,
3254                ];
3255
3256                // Cel-shaded colour
3257                let lit_color = crate::gfx::light::compute_lit_color(
3258                    gfx.color,
3259                    normal,
3260                    centroid,
3261                    &gfx.lights,
3262                    gfx.ambient,
3263                );
3264
3265                // ── Near-plane CLIP (Sutherland–Hodgman) ──
3266                // A vertex just in front of the eye divides by ~0 in the perspective
3267                // projection and smears the triangle into a screen-filling fan. The old
3268                // "cull if any vertex past near" still let a vertex sitting just inside
3269                // near blow up. Clipping the triangle to the near plane keeps only the
3270                // in-front portion (finite projection — no fan, no over-cull).
3271                let near = -gfx.camera.zdist + 0.05;
3272                let vw = [
3273                    (ax, ay, az, gfx.camera.depth(ax, ay, az)),
3274                    (bx, by, bz, gfx.camera.depth(bx, by, bz)),
3275                    (cx, cy, cz, gfx.camera.depth(cx, cy, cz)),
3276                ];
3277                let mut poly: Vec<(f32, f32, f32)> = Vec::with_capacity(4);
3278                let mut ei = 0;
3279                while ei < 3 {
3280                    let a = vw[ei];
3281                    let b = vw[(ei + 1) % 3];
3282                    let ain = a.3 > near;
3283                    let bin = b.3 > near;
3284                    if ain {
3285                        poly.push((a.0, a.1, a.2));
3286                    }
3287                    if ain != bin {
3288                        let tt = (near - a.3) / (b.3 - a.3);
3289                        poly.push((
3290                            a.0 + (b.0 - a.0) * tt,
3291                            a.1 + (b.1 - a.1) * tt,
3292                            a.2 + (b.2 - a.2) * tt,
3293                        ));
3294                    }
3295                    ei += 1;
3296                }
3297                if poly.len() < 3 {
3298                    return Ok(Value::Unit);
3299                }
3300                // Project the clipped polygon, painter-depth = mean, fan-triangulate.
3301                let proj: Vec<(f32, f32, f32)> = poly
3302                    .iter()
3303                    .map(|p| gfx.camera.project(p.0, p.1, p.2))
3304                    .collect();
3305                let mut dsum = 0.0f32;
3306                for p in &proj {
3307                    dsum += p.2;
3308                }
3309                let depth = dsum / proj.len() as f32;
3310                let lit_color = gfx.fog_apply(lit_color, depth);
3311                let mut fk = 1;
3312                while fk + 1 < proj.len() {
3313                    gfx.depth_queue.push_triangle_zv(
3314                        lit_color,
3315                        proj[0].0,
3316                        proj[0].1,
3317                        proj[0].2,
3318                        proj[fk].0,
3319                        proj[fk].1,
3320                        proj[fk].2,
3321                        proj[fk + 1].0,
3322                        proj[fk + 1].1,
3323                        proj[fk + 1].2,
3324                    );
3325                    fk += 1;
3326                }
3327                return Ok(Value::Unit);
3328            },
3329
3330            // ── วาดเส้น3มิติ(ax,ay,az, bx,by,bz) ──
3331            // Projects two world-space points via the stored camera and pushes
3332            // a line to the depth queue.
3333            "วาดเส้น3มิติ" | "draw_line_3d" | "line3d" | "画3D线" | "3D線描く" | "3D선그리기" =>
3334            {
3335                let ax = self.arg_num(&args, 0, 0.0)? as f32;
3336                let ay = self.arg_num(&args, 1, 0.0)? as f32;
3337                let az = self.arg_num(&args, 2, 0.0)? as f32;
3338                let bx = self.arg_num(&args, 3, 0.0)? as f32;
3339                let by = self.arg_num(&args, 4, 0.0)? as f32;
3340                let bz = self.arg_num(&args, 5, 0.0)? as f32;
3341
3342                let mut gfx = self.gfx.borrow_mut();
3343                let color = gfx.color;
3344                // Near-plane clip in 3-D before perspective divide
3345                let near = -gfx.camera.zdist + 0.05;
3346                let mut lax = ax;
3347                let mut lay = ay;
3348                let mut laz = az;
3349                let mut lbx = bx;
3350                let mut lby = by;
3351                let mut lbz = bz;
3352                let da_raw = gfx.camera.depth(lax, lay, laz);
3353                let db_raw = gfx.camera.depth(lbx, lby, lbz);
3354                if da_raw <= near && db_raw <= near {
3355                    return Ok(Value::Unit);
3356                }
3357                if da_raw <= near {
3358                    let t = (near - da_raw) / (db_raw - da_raw);
3359                    lax += t * (lbx - lax);
3360                    lay += t * (lby - lay);
3361                    laz += t * (lbz - laz);
3362                } else if db_raw <= near {
3363                    let t = (near - da_raw) / (db_raw - da_raw);
3364                    lbx = lax + t * (lbx - lax);
3365                    lby = lay + t * (lby - lay);
3366                    lbz = laz + t * (lbz - laz);
3367                }
3368                let (sax, say, da) = gfx.camera.project(lax, lay, laz);
3369                let (sbx, sby, db) = gfx.camera.project(lbx, lby, lbz);
3370                let depth = (da + db) / 2.0;
3371                let color = gfx.fog_apply(color, depth);
3372                gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
3373                return Ok(Value::Unit);
3374            },
3375
3376            // orb_shell(cx,cy,cz, radius, rot_y, rot_x, density, r,g,b)
3377            //   A single trippy, grayscale, depth-faded vector pattern wound around
3378            //   a sphere — two families of interleaved spherical spirals (a guilloché
3379            //   weave), NOT a lat/long cage. Each segment's brightness follows its
3380            //   facing (front bright, back dim), so it reads as a translucent
3381            //   grayscale "texture" with alpha rather than a hard wireframe; the
3382            //   inner marble shows through. `rot_y`/`rot_x` roll the texture around
3383            //   the orb; `density` = spirals per winding direction. r,g,b tint it
3384            //   (pass a gray like 230,230,230 for pure grayscale).
3385            #[cfg(not(target_arch = "wasm32"))]
3386            "orb_shell" | "球壳" | "オーブ殻" | "오브껍질" | "เปลือกทรงกลม" =>
3387            {
3388                let cx = self.arg_num(&args, 0, 0.)? as f32;
3389                let cy = self.arg_num(&args, 1, 0.)? as f32;
3390                let cz = self.arg_num(&args, 2, 0.)? as f32;
3391                let radius = self.arg_num(&args, 3, 1.0)? as f32;
3392                let ry = self.arg_num(&args, 4, 0.)? as f32;
3393                let rx = self.arg_num(&args, 5, 0.)? as f32;
3394                let density = (self.arg_num(&args, 6, 10.)? as i32).clamp(1, 48);
3395                let tr = (self.arg_num(&args, 7, 230.)? as f32).clamp(0., 255.);
3396                let tg = (self.arg_num(&args, 8, 230.)? as f32).clamp(0., 255.);
3397                let tb = (self.arg_num(&args, 9, 235.)? as f32).clamp(0., 255.);
3398                let (cyr, syr) = (ry.cos(), ry.sin());
3399                let (cxr, sxr) = (rx.cos(), rx.sin());
3400                let tau = std::f32::consts::TAU;
3401                let pi = std::f32::consts::PI;
3402                let turns = 6.0_f32; // how many times each spiral wraps pole→pole
3403                let nseg = 96; // segments per spiral (smoothness)
3404                let inv_r = if radius.abs() > 1e-5 {
3405                    1.0 / radius
3406                } else {
3407                    0.0
3408                };
3409                // a point along a spiral (param u 0..1, start angle theta0, winding dir),
3410                // spun by ry/rx — returns (world point, facing 0..1 where 1 = toward camera)
3411                let pt = |u: f32, theta0: f32, dir: f32| -> ([f32; 3], f32) {
3412                    let phi = pi * u; // 0..pi  (north → south)
3413                    let th = dir * turns * tau * u + theta0;
3414                    let (mut x, y, mut z) = (
3415                        phi.sin() * th.cos() * radius,
3416                        phi.cos() * radius,
3417                        phi.sin() * th.sin() * radius,
3418                    );
3419                    let x1 = x * cyr + z * syr; // yaw about Y
3420                    let z1 = -x * syr + z * cyr;
3421                    x = x1;
3422                    z = z1;
3423                    let y2 = y * cxr - z * sxr; // pitch about X
3424                    let z2 = y * sxr + z * cxr;
3425                    // facing: camera sits at -zdist looking +z, so smaller z2 = nearer = brighter
3426                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.0, 1.0);
3427                    ([cx + x, cy + y2, cz + z2], facing)
3428                };
3429                let mut gfx = self.gfx.borrow_mut();
3430                let near = -gfx.camera.zdist + 0.05;
3431                // draw one segment (near-clipped) in a grayscale tint scaled by `lum`
3432                let seg = |gfx: &mut crate::gfx::GfxState, a: [f32; 3], b: [f32; 3], lum: f32| {
3433                    let (mut lax, mut lay, mut laz) = (a[0], a[1], a[2]);
3434                    let (mut lbx, mut lby, mut lbz) = (b[0], b[1], b[2]);
3435                    let da = gfx.camera.depth(lax, lay, laz);
3436                    let db = gfx.camera.depth(lbx, lby, lbz);
3437                    if da <= near && db <= near {
3438                        return;
3439                    }
3440                    if da <= near {
3441                        let t = (near - da) / (db - da);
3442                        lax += t * (lbx - lax);
3443                        lay += t * (lby - lay);
3444                        laz += t * (lbz - laz);
3445                    } else if db <= near {
3446                        let t = (near - da) / (db - da);
3447                        lbx = lax + t * (lbx - lax);
3448                        lby = lay + t * (lby - lay);
3449                        lbz = laz + t * (lbz - laz);
3450                    }
3451                    let (sax, say, da2) = gfx.camera.project(lax, lay, laz);
3452                    let (sbx, sby, db2) = gfx.camera.project(lbx, lby, lbz);
3453                    // grayscale-alpha: front-facing bright, back faded toward black
3454                    let l = (0.12 + 0.88 * lum).clamp(0.0, 1.0);
3455                    let cr = (tr * l) as u32;
3456                    let cg = (tg * l) as u32;
3457                    let cb = (tb * l) as u32;
3458                    let color = (cr << 16) | (cg << 8) | cb;
3459                    gfx.depth_queue
3460                        .push_line((da2 + db2) * 0.5, color, sax, say, sbx, sby);
3461                };
3462                // two opposite winding directions → a soft guilloché weave (not a cage)
3463                for &dir in &[1.0_f32, -1.0_f32] {
3464                    for s in 0..density {
3465                        let theta0 = s as f32 * tau / density as f32;
3466                        let mut prev = pt(0.0, theta0, dir);
3467                        for k in 1..=nseg {
3468                            let cur = pt(k as f32 / nseg as f32, theta0, dir);
3469                            seg(&mut gfx, prev.0, cur.0, (prev.1 + cur.1) * 0.5);
3470                            prev = cur;
3471                        }
3472                    }
3473                }
3474                return Ok(Value::Unit);
3475            },
3476
3477            // orb_particles(cx,cy,cz, radius, count, t, r,g,b)
3478            //   Fills the VOLUME of a sphere with `count` swirling vector points —
3479            //   like motes suspended inside a snow-globe orb. Points are distributed
3480            //   uniformly through the ball, slowly tumble as a cloud + wobble
3481            //   individually over time `t`, and are depth-shaded (near = bright,
3482            //   far = dim) so the cloud has real volume. Additive, so it layers under
3483            //   a shell / over a liquid marble.
3484            #[cfg(not(target_arch = "wasm32"))]
3485            "orb_particles" | "球内粒子" | "オーブ粒子" | "오브입자" | "อนุภาคทรงกลม" =>
3486            {
3487                let cx = self.arg_num(&args, 0, 0.)? as f32;
3488                let cy = self.arg_num(&args, 1, 0.)? as f32;
3489                let cz = self.arg_num(&args, 2, 0.)? as f32;
3490                let radius = self.arg_num(&args, 3, 1.0)? as f32;
3491                let count = (self.arg_num(&args, 4, 160.)? as i32).clamp(1, 4000);
3492                let t = self.arg_num(&args, 5, 0.)? as f32;
3493                let tr = (self.arg_num(&args, 6, 255.)? as f32).clamp(0., 255.);
3494                let tg = (self.arg_num(&args, 7, 255.)? as f32).clamp(0., 255.);
3495                let tb = (self.arg_num(&args, 8, 255.)? as f32).clamp(0., 255.);
3496                let inv_r = if radius.abs() > 1e-5 {
3497                    1.0 / radius
3498                } else {
3499                    0.0
3500                };
3501                // cheap deterministic hash → [0,1)
3502                let h = |mut x: u32| -> f32 {
3503                    x = x.wrapping_mul(747796405).wrapping_add(2891336453);
3504                    x = ((x >> ((x >> 28).wrapping_add(4))) ^ x).wrapping_mul(277803737);
3505                    (((x >> 22) ^ x) & 0xFFFFFF) as f32 / 16_777_216.0
3506                };
3507                let tau = std::f32::consts::TAU;
3508                // slow tumble of the whole cloud
3509                let (cyr, syr) = ((t * 0.5).cos(), (t * 0.5).sin());
3510                let (cxr, sxr) = ((t * 0.23).cos(), (t * 0.23).sin());
3511                let mut gfx = self.gfx.borrow_mut();
3512                let near = -gfx.camera.zdist + 0.05;
3513                let (sw, sh) = (gfx.width as i32, gfx.height as i32);
3514                for i in 0..count {
3515                    let i = i as u32;
3516                    // uniform-in-volume: r = cbrt(u) * radius; direction from two hashes
3517                    let u = h(i.wrapping_mul(3) + 1);
3518                    let rr = u.cbrt() * radius * (0.85 + 0.15 * (t * 1.3 + i as f32).sin()); // gentle pulse
3519                    let th = h(i.wrapping_mul(3) + 2) * tau + t * (0.3 + 0.5 * h(i * 7 + 5)); // per-mote orbit
3520                    let ph = (h(i.wrapping_mul(3) + 3) * 2.0 - 1.0).acos(); // uniform cos(phi)
3521                    let (mut x, y, mut z) = (
3522                        rr * ph.sin() * th.cos(),
3523                        rr * ph.cos(),
3524                        rr * ph.sin() * th.sin(),
3525                    );
3526                    // tumble the cloud (yaw then pitch)
3527                    let x1 = x * cyr + z * syr;
3528                    let z1 = -x * syr + z * cyr;
3529                    x = x1;
3530                    z = z1;
3531                    let y2 = y * cxr - z * sxr;
3532                    let z2 = y * sxr + z * cxr;
3533                    let (wx, wy, wz) = (cx + x, cy + y2, cz + z2);
3534                    if gfx.camera.depth(wx, wy, wz) <= near {
3535                        continue;
3536                    }
3537                    let (sx, sy, dep) = gfx.camera.project(wx, wy, wz);
3538                    let sxi = sx as i32;
3539                    let syi = sy as i32;
3540                    if sxi < 0 || syi < 0 || sxi >= sw || syi >= sh {
3541                        continue;
3542                    }
3543                    // depth-shade: nearer (smaller z2) = brighter
3544                    let facing = (0.5 - 0.5 * z2 * inv_r).clamp(0.15, 1.0);
3545                    let l = facing;
3546                    let cr = (tr * l) as u32;
3547                    let cg = (tg * l) as u32;
3548                    let cb = (tb * l) as u32;
3549                    let color = (cr << 16) | (cg << 8) | cb;
3550                    // a 1–2px dot (bigger when near) as a short segment in the depth queue
3551                    let len = if facing > 0.7 { 1.0 } else { 0.0 };
3552                    gfx.depth_queue.push_line(dep, color, sx, sy, sx + len, sy);
3553                }
3554                return Ok(Value::Unit);
3555            },
3556
3557            // project_3d(x,y,z) -> [screen_x, screen_y, depth]; behind the camera
3558            // returns a sentinel ([-99999,-99999, depth]) so scripts can skip it.
3559            // Lets scripts place 2-D overlays (e.g. filled teardrop flames) onto 3-D points.
3560            "project_3d" | "投影3D" | "3D投影" | "3D투영" | "ฉาย3มิติ" => {
3561                let x = self.arg_num(&args, 0, 0.0)? as f32;
3562                let y = self.arg_num(&args, 1, 0.0)? as f32;
3563                let z = self.arg_num(&args, 2, 0.0)? as f32;
3564                let gfx = self.gfx.borrow();
3565                let near = -gfx.camera.zdist + 0.05;
3566                let d = gfx.camera.depth(x, y, z);
3567                if d <= near {
3568                    return Ok(Value::List(vec![
3569                        Value::Number(-99999.0),
3570                        Value::Number(-99999.0),
3571                        Value::Number(d as f64),
3572                    ]));
3573                }
3574                let (sx, sy, depth) = gfx.camera.project(x, y, z);
3575                return Ok(Value::List(vec![
3576                    Value::Number(sx as f64),
3577                    Value::Number(sy as f64),
3578                    Value::Number(depth as f64),
3579                ]));
3580            },
3581            // draw_poly([x0,y0,x1,y1,…]) — filled 2-D polygon in the current colour,
3582            // honouring the blend mode (additive → translucent glow). Auto-closes.
3583            #[cfg(not(target_arch = "wasm32"))]
3584            "draw_poly" | "填充多边形" | "ポリゴン塗り" | "다각형채우기" | "เติมรูปหลายเหลี่ยม" =>
3585            {
3586                let mut pts: Vec<[f32; 2]> = Vec::new();
3587                if let Some(Value::List(v)) = args.first() {
3588                    let mut i = 0;
3589                    while i + 1 < v.len() {
3590                        let x = self.to_number(&v[i]).unwrap_or(0.0) as f32;
3591                        let y = self.to_number(&v[i + 1]).unwrap_or(0.0) as f32;
3592                        pts.push([x, y]);
3593                        i += 2;
3594                    }
3595                }
3596                if pts.len() >= 3 {
3597                    if pts[0] != pts[pts.len() - 1] {
3598                        let p0 = pts[0];
3599                        pts.push(p0);
3600                    } // close
3601                    let mut gfx = self.gfx.borrow_mut();
3602                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
3603                    crate::gfx::raster::fill_contours_aa(
3604                        &mut gfx.buffer,
3605                        w,
3606                        h,
3607                        color,
3608                        add,
3609                        std::slice::from_ref(&pts),
3610                    );
3611                }
3612                return Ok(Value::Unit);
3613            },
3614
3615            // ══════════════════════════════════════════════════════════════════
3616            // VECTOR TEXTURE BUILTINS  (src/gfx/vtex.rs)
3617            // All patterns are depth-biased so they appear on top of surfaces.
3618            // Plane defined by: centre (cx,cy,cz) + U tangent + V tangent.
3619            // Last two args always: fr (frame f32), hue (phase offset f32).
3620            // ══════════════════════════════════════════════════════════════════
3621
3622            // vtex_grid(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cw,ch, fr,hue)
3623            "vtex_grid" | "ลายตาราง" | "纹格" | "格子模様" | "격자무늬" =>
3624            {
3625                let cx = self.arg_num(&args, 0, 0.)? as f32;
3626                let cy = self.arg_num(&args, 1, 0.)? as f32;
3627                let cz = self.arg_num(&args, 2, 0.)? as f32;
3628                let ux = self.arg_num(&args, 3, 1.)? as f32;
3629                let uy = self.arg_num(&args, 4, 0.)? as f32;
3630                let uz = self.arg_num(&args, 5, 0.)? as f32;
3631                let vx = self.arg_num(&args, 6, 0.)? as f32;
3632                let vy = self.arg_num(&args, 7, 0.)? as f32;
3633                let vz = self.arg_num(&args, 8, 1.)? as f32;
3634                let cols = self.arg_num(&args, 9, 10.)? as usize;
3635                let rows = self.arg_num(&args, 10, 10.)? as usize;
3636                let cw = self.arg_num(&args, 11, 1.)? as f32;
3637                let ch = self.arg_num(&args, 12, 1.)? as f32;
3638                let fr = self.arg_num(&args, 13, 0.)? as f32;
3639                let hue = self.arg_num(&args, 14, 0.)? as f32;
3640                let mut gfx = self.gfx.borrow_mut();
3641                let cam = gfx.camera.clone();
3642                crate::gfx::vtex::draw_grid(
3643                    &mut gfx.depth_queue,
3644                    &cam,
3645                    cx,
3646                    cy,
3647                    cz,
3648                    ux,
3649                    uy,
3650                    uz,
3651                    vx,
3652                    vy,
3653                    vz,
3654                    cols,
3655                    rows,
3656                    cw,
3657                    ch,
3658                    fr,
3659                    hue,
3660                );
3661                return Ok(Value::Unit);
3662            },
3663
3664            // vtex_rings(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_rings,n_sides, max_r,twist, fr,hue)
3665            "vtex_rings" | "ลายวงซ้อน" | "纹环" | "同心円" | "동심원" => {
3666                let cx = self.arg_num(&args, 0, 0.)? as f32;
3667                let cy = self.arg_num(&args, 1, 0.)? as f32;
3668                let cz = self.arg_num(&args, 2, 0.)? as f32;
3669                let ux = self.arg_num(&args, 3, 1.)? as f32;
3670                let uy = self.arg_num(&args, 4, 0.)? as f32;
3671                let uz = self.arg_num(&args, 5, 0.)? as f32;
3672                let vx = self.arg_num(&args, 6, 0.)? as f32;
3673                let vy = self.arg_num(&args, 7, 0.)? as f32;
3674                let vz = self.arg_num(&args, 8, 1.)? as f32;
3675                let nr = self.arg_num(&args, 9, 6.)? as usize;
3676                let ns = self.arg_num(&args, 10, 6.)? as usize;
3677                let mr = self.arg_num(&args, 11, 3.)? as f32;
3678                let tw = self.arg_num(&args, 12, 0.)? as f32;
3679                let fr = self.arg_num(&args, 13, 0.)? as f32;
3680                let hue = self.arg_num(&args, 14, 0.)? as f32;
3681                let mut gfx = self.gfx.borrow_mut();
3682                let cam = gfx.camera.clone();
3683                crate::gfx::vtex::draw_rings(
3684                    &mut gfx.depth_queue,
3685                    &cam,
3686                    cx,
3687                    cy,
3688                    cz,
3689                    ux,
3690                    uy,
3691                    uz,
3692                    vx,
3693                    vy,
3694                    vz,
3695                    nr,
3696                    ns,
3697                    mr,
3698                    tw,
3699                    fr,
3700                    hue,
3701                );
3702                return Ok(Value::Unit);
3703            },
3704
3705            // vtex_star(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_pts,r_out,r_in, rot_speed, fr,hue)
3706            "vtex_star" | "ลายดาว" | "纹星" | "星模様" | "별무늬" => {
3707                let cx = self.arg_num(&args, 0, 0.)? as f32;
3708                let cy = self.arg_num(&args, 1, 0.)? as f32;
3709                let cz = self.arg_num(&args, 2, 0.)? as f32;
3710                let ux = self.arg_num(&args, 3, 1.)? as f32;
3711                let uy = self.arg_num(&args, 4, 0.)? as f32;
3712                let uz = self.arg_num(&args, 5, 0.)? as f32;
3713                let vx = self.arg_num(&args, 6, 0.)? as f32;
3714                let vy = self.arg_num(&args, 7, 0.)? as f32;
3715                let vz = self.arg_num(&args, 8, 1.)? as f32;
3716                let np = self.arg_num(&args, 9, 6.)? as usize;
3717                let ro = self.arg_num(&args, 10, 2.)? as f32;
3718                let ri = self.arg_num(&args, 11, 1.)? as f32;
3719                let rs = self.arg_num(&args, 12, 0.01)? as f32;
3720                let fr = self.arg_num(&args, 13, 0.)? as f32;
3721                let hue = self.arg_num(&args, 14, 0.)? as f32;
3722                let mut gfx = self.gfx.borrow_mut();
3723                let cam = gfx.camera.clone();
3724                crate::gfx::vtex::draw_star(
3725                    &mut gfx.depth_queue,
3726                    &cam,
3727                    cx,
3728                    cy,
3729                    cz,
3730                    ux,
3731                    uy,
3732                    uz,
3733                    vx,
3734                    vy,
3735                    vz,
3736                    np,
3737                    ro,
3738                    ri,
3739                    rs,
3740                    fr,
3741                    hue,
3742                );
3743                return Ok(Value::Unit);
3744            },
3745
3746            // vtex_spiral(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_turns,max_r,steps, fr,hue)
3747            "vtex_spiral" | "ลายเกลียว" | "纹螺" | "螺旋" | "나선" => {
3748                let cx = self.arg_num(&args, 0, 0.)? as f32;
3749                let cy = self.arg_num(&args, 1, 0.)? as f32;
3750                let cz = self.arg_num(&args, 2, 0.)? as f32;
3751                let ux = self.arg_num(&args, 3, 1.)? as f32;
3752                let uy = self.arg_num(&args, 4, 0.)? as f32;
3753                let uz = self.arg_num(&args, 5, 0.)? as f32;
3754                let vx = self.arg_num(&args, 6, 0.)? as f32;
3755                let vy = self.arg_num(&args, 7, 0.)? as f32;
3756                let vz = self.arg_num(&args, 8, 1.)? as f32;
3757                let nt = self.arg_num(&args, 9, 3.)? as f32;
3758                let mr = self.arg_num(&args, 10, 3.)? as f32;
3759                let st = self.arg_num(&args, 11, 120.)? as usize;
3760                let fr = self.arg_num(&args, 12, 0.)? as f32;
3761                let hue = self.arg_num(&args, 13, 0.)? as f32;
3762                let mut gfx = self.gfx.borrow_mut();
3763                let cam = gfx.camera.clone();
3764                crate::gfx::vtex::draw_spiral(
3765                    &mut gfx.depth_queue,
3766                    &cam,
3767                    cx,
3768                    cy,
3769                    cz,
3770                    ux,
3771                    uy,
3772                    uz,
3773                    vx,
3774                    vy,
3775                    vz,
3776                    nt,
3777                    mr,
3778                    st,
3779                    fr,
3780                    hue,
3781                );
3782                return Ok(Value::Unit);
3783            },
3784
3785            // vtex_flower(cx,cy,cz, ux,uy,uz, vx,vy,vz, radius,n_sides, fr,hue)
3786            "vtex_flower" | "ลายดอก" | "纹花" | "花模様" | "꽃무늬" => {
3787                let cx = self.arg_num(&args, 0, 0.)? as f32;
3788                let cy = self.arg_num(&args, 1, 0.)? as f32;
3789                let cz = self.arg_num(&args, 2, 0.)? as f32;
3790                let ux = self.arg_num(&args, 3, 1.)? as f32;
3791                let uy = self.arg_num(&args, 4, 0.)? as f32;
3792                let uz = self.arg_num(&args, 5, 0.)? as f32;
3793                let vx = self.arg_num(&args, 6, 0.)? as f32;
3794                let vy = self.arg_num(&args, 7, 0.)? as f32;
3795                let vz = self.arg_num(&args, 8, 1.)? as f32;
3796                let r = self.arg_num(&args, 9, 1.)? as f32;
3797                let ns = self.arg_num(&args, 10, 24.)? as usize;
3798                let fr = self.arg_num(&args, 11, 0.)? as f32;
3799                let hue = self.arg_num(&args, 12, 0.)? as f32;
3800                let mut gfx = self.gfx.borrow_mut();
3801                let cam = gfx.camera.clone();
3802                crate::gfx::vtex::draw_flower(
3803                    &mut gfx.depth_queue,
3804                    &cam,
3805                    cx,
3806                    cy,
3807                    cz,
3808                    ux,
3809                    uy,
3810                    uz,
3811                    vx,
3812                    vy,
3813                    vz,
3814                    r,
3815                    ns,
3816                    fr,
3817                    hue,
3818                );
3819                return Ok(Value::Unit);
3820            },
3821
3822            // vtex_letter_rain(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_cols,n_vis, col_w,row_h, speed, fr,hue)
3823            "vtex_letter_rain" | "ลายอักษรไหล" | "纹字雨" | "文字雨" | "글자비" =>
3824            {
3825                let cx = self.arg_num(&args, 0, 0.)? as f32;
3826                let cy = self.arg_num(&args, 1, 0.)? as f32;
3827                let cz = self.arg_num(&args, 2, 0.)? as f32;
3828                let ux = self.arg_num(&args, 3, 1.)? as f32;
3829                let uy = self.arg_num(&args, 4, 0.)? as f32;
3830                let uz = self.arg_num(&args, 5, 0.)? as f32;
3831                let vx = self.arg_num(&args, 6, 0.)? as f32;
3832                let vy = self.arg_num(&args, 7, 0.)? as f32;
3833                let vz = self.arg_num(&args, 8, 1.)? as f32;
3834                let nc = self.arg_num(&args, 9, 16.)? as usize;
3835                let nv = self.arg_num(&args, 10, 14.)? as usize;
3836                let cw = self.arg_num(&args, 11, 0.65)? as f32;
3837                let rh = self.arg_num(&args, 12, 0.60)? as f32;
3838                let sp = self.arg_num(&args, 13, 0.025)? as f32;
3839                let fr = self.arg_num(&args, 14, 0.)? as f32;
3840                let hue = self.arg_num(&args, 15, 0.)? as f32;
3841                let mut gfx = self.gfx.borrow_mut();
3842                let cam = gfx.camera.clone();
3843                crate::gfx::vtex::draw_letter_rain(
3844                    &mut gfx.depth_queue,
3845                    &cam,
3846                    cx,
3847                    cy,
3848                    cz,
3849                    ux,
3850                    uy,
3851                    uz,
3852                    vx,
3853                    vy,
3854                    vz,
3855                    nc,
3856                    nv,
3857                    cw,
3858                    rh,
3859                    sp,
3860                    fr,
3861                    hue,
3862                );
3863                return Ok(Value::Unit);
3864            },
3865
3866            // vtex_hyperbolic_uv(cx,cy,cz, ux,uy,uz, vx,vy,vz, max_r,n_circles,n_rays, fr,hue)
3867            "vtex_hyperbolic_uv" | "ลายไฮเพอร์โบลิก" | "纹曲面" | "双曲線" | "쌍곡선" =>
3868            {
3869                let cx = self.arg_num(&args, 0, 0.)? as f32;
3870                let cy = self.arg_num(&args, 1, 0.)? as f32;
3871                let cz = self.arg_num(&args, 2, 0.)? as f32;
3872                let ux = self.arg_num(&args, 3, 1.)? as f32;
3873                let uy = self.arg_num(&args, 4, 0.)? as f32;
3874                let uz = self.arg_num(&args, 5, 0.)? as f32;
3875                let vx = self.arg_num(&args, 6, 0.)? as f32;
3876                let vy = self.arg_num(&args, 7, 0.)? as f32;
3877                let vz = self.arg_num(&args, 8, 1.)? as f32;
3878                let mr = self.arg_num(&args, 9, 5.)? as f32;
3879                let nc = self.arg_num(&args, 10, 12.)? as usize;
3880                let nr = self.arg_num(&args, 11, 18.)? as usize;
3881                let fr = self.arg_num(&args, 12, 0.)? as f32;
3882                let hue = self.arg_num(&args, 13, 0.)? as f32;
3883                let mut gfx = self.gfx.borrow_mut();
3884                let cam = gfx.camera.clone();
3885                crate::gfx::vtex::draw_hyperbolic_uv(
3886                    &mut gfx.depth_queue,
3887                    &cam,
3888                    cx,
3889                    cy,
3890                    cz,
3891                    ux,
3892                    uy,
3893                    uz,
3894                    vx,
3895                    vy,
3896                    vz,
3897                    mr,
3898                    nc,
3899                    nr,
3900                    fr,
3901                    hue,
3902                );
3903                return Ok(Value::Unit);
3904            },
3905
3906            // vtex_halftone(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell_w,cell_h, density, fr,hue)
3907            "vtex_halftone" | "ลายจุด" | "纹半调" | "網点模様" | "망점" => {
3908                let cx = self.arg_num(&args, 0, 0.)? as f32;
3909                let cy = self.arg_num(&args, 1, 0.)? as f32;
3910                let cz = self.arg_num(&args, 2, 0.)? as f32;
3911                let ux = self.arg_num(&args, 3, 1.)? as f32;
3912                let uy = self.arg_num(&args, 4, 0.)? as f32;
3913                let uz = self.arg_num(&args, 5, 0.)? as f32;
3914                let vx = self.arg_num(&args, 6, 0.)? as f32;
3915                let vy = self.arg_num(&args, 7, 0.)? as f32;
3916                let vz = self.arg_num(&args, 8, 1.)? as f32;
3917                let cols = self.arg_num(&args, 9, 16.)? as usize;
3918                let rows = self.arg_num(&args, 10, 12.)? as usize;
3919                let cw = self.arg_num(&args, 11, 0.5)? as f32;
3920                let ch = self.arg_num(&args, 12, 0.5)? as f32;
3921                let dens = self.arg_num(&args, 13, 0.4)? as f32;
3922                let fr = self.arg_num(&args, 14, 0.)? as f32;
3923                let hue = self.arg_num(&args, 15, 0.)? as f32;
3924                let mut gfx = self.gfx.borrow_mut();
3925                let cam = gfx.camera.clone();
3926                crate::gfx::vtex::draw_halftone(
3927                    &mut gfx.depth_queue,
3928                    &cam,
3929                    cx,
3930                    cy,
3931                    cz,
3932                    ux,
3933                    uy,
3934                    uz,
3935                    vx,
3936                    vy,
3937                    vz,
3938                    cols,
3939                    rows,
3940                    cw,
3941                    ch,
3942                    dens,
3943                    fr,
3944                    hue,
3945                );
3946                return Ok(Value::Unit);
3947            },
3948
3949            // vtex_tessellated(cx,cy,cz, ux,uy,uz, vx,vy,vz, cols,rows, cell, amplitude,freq, fr,hue)
3950            "vtex_tessellated" | "ลายตาข่าย" | "纹镶嵌" | "網目模様" | "격자망" =>
3951            {
3952                let cx = self.arg_num(&args, 0, 0.)? as f32;
3953                let cy = self.arg_num(&args, 1, 0.)? as f32;
3954                let cz = self.arg_num(&args, 2, 0.)? as f32;
3955                let ux = self.arg_num(&args, 3, 1.)? as f32;
3956                let uy = self.arg_num(&args, 4, 0.)? as f32;
3957                let uz = self.arg_num(&args, 5, 0.)? as f32;
3958                let vx = self.arg_num(&args, 6, 0.)? as f32;
3959                let vy = self.arg_num(&args, 7, 0.)? as f32;
3960                let vz = self.arg_num(&args, 8, 1.)? as f32;
3961                let cols = self.arg_num(&args, 9, 14.)? as usize;
3962                let rows = self.arg_num(&args, 10, 10.)? as usize;
3963                let cell = self.arg_num(&args, 11, 0.6)? as f32;
3964                let amp = self.arg_num(&args, 12, 0.25)? as f32;
3965                let freq = self.arg_num(&args, 13, 4.)? as f32;
3966                let fr = self.arg_num(&args, 14, 0.)? as f32;
3967                let hue = self.arg_num(&args, 15, 0.)? as f32;
3968                let mut gfx = self.gfx.borrow_mut();
3969                let cam = gfx.camera.clone();
3970                crate::gfx::vtex::draw_tessellated(
3971                    &mut gfx.depth_queue,
3972                    &cam,
3973                    cx,
3974                    cy,
3975                    cz,
3976                    ux,
3977                    uy,
3978                    uz,
3979                    vx,
3980                    vy,
3981                    vz,
3982                    cols,
3983                    rows,
3984                    cell,
3985                    amp,
3986                    freq,
3987                    fr,
3988                    hue,
3989                );
3990                return Ok(Value::Unit);
3991            },
3992
3993            // vtex_lotus(cx,cy,cz, ux,uy,uz, vx,vy,vz, r_inner,r_outer,n_petals, fr,hue)
3994            "vtex_lotus" | "ลายดอกบัว" | "纹莲" | "蓮模様" | "연꽃무늬" =>
3995            {
3996                let cx = self.arg_num(&args, 0, 0.)? as f32;
3997                let cy = self.arg_num(&args, 1, 0.)? as f32;
3998                let cz = self.arg_num(&args, 2, 0.)? as f32;
3999                let ux = self.arg_num(&args, 3, 1.)? as f32;
4000                let uy = self.arg_num(&args, 4, 0.)? as f32;
4001                let uz = self.arg_num(&args, 5, 0.)? as f32;
4002                let vx = self.arg_num(&args, 6, 0.)? as f32;
4003                let vy = self.arg_num(&args, 7, 0.)? as f32;
4004                let vz = self.arg_num(&args, 8, 1.)? as f32;
4005                let ri = self.arg_num(&args, 9, 1.)? as f32;
4006                let ro = self.arg_num(&args, 10, 2.)? as f32;
4007                let np = self.arg_num(&args, 11, 12.)? as usize;
4008                let fr = self.arg_num(&args, 12, 0.)? as f32;
4009                let hue = self.arg_num(&args, 13, 0.)? as f32;
4010                let mut gfx = self.gfx.borrow_mut();
4011                let cam = gfx.camera.clone();
4012                crate::gfx::vtex::draw_lotus(
4013                    &mut gfx.depth_queue,
4014                    &cam,
4015                    cx,
4016                    cy,
4017                    cz,
4018                    ux,
4019                    uy,
4020                    uz,
4021                    vx,
4022                    vy,
4023                    vz,
4024                    ri,
4025                    ro,
4026                    np,
4027                    fr,
4028                    hue,
4029                );
4030                return Ok(Value::Unit);
4031            },
4032
4033            // vtex_chakra(cx,cy,cz, ux,uy,uz, vx,vy,vz, r,n_spokes, fr,hue)
4034            "vtex_chakra" | "ลายจักร" | "纹轮" | "輪模様" | "바퀴무늬" => {
4035                let cx = self.arg_num(&args, 0, 0.)? as f32;
4036                let cy = self.arg_num(&args, 1, 0.)? as f32;
4037                let cz = self.arg_num(&args, 2, 0.)? as f32;
4038                let ux = self.arg_num(&args, 3, 1.)? as f32;
4039                let uy = self.arg_num(&args, 4, 0.)? as f32;
4040                let uz = self.arg_num(&args, 5, 0.)? as f32;
4041                let vx = self.arg_num(&args, 6, 0.)? as f32;
4042                let vy = self.arg_num(&args, 7, 0.)? as f32;
4043                let vz = self.arg_num(&args, 8, 1.)? as f32;
4044                let r = self.arg_num(&args, 9, 2.)? as f32;
4045                let ns = self.arg_num(&args, 10, 8.)? as usize;
4046                let fr = self.arg_num(&args, 11, 0.)? as f32;
4047                let hue = self.arg_num(&args, 12, 0.)? as f32;
4048                let mut gfx = self.gfx.borrow_mut();
4049                let cam = gfx.camera.clone();
4050                crate::gfx::vtex::draw_chakra(
4051                    &mut gfx.depth_queue,
4052                    &cam,
4053                    cx,
4054                    cy,
4055                    cz,
4056                    ux,
4057                    uy,
4058                    uz,
4059                    vx,
4060                    vy,
4061                    vz,
4062                    r,
4063                    ns,
4064                    fr,
4065                    hue,
4066                );
4067                return Ok(Value::Unit);
4068            },
4069
4070            // vtex_yantra(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_layers,max_r, fr,hue)
4071            "vtex_yantra" | "ลายยันต์" | "纹咒" | "護符模様" | "부적무늬" =>
4072            {
4073                let cx = self.arg_num(&args, 0, 0.)? as f32;
4074                let cy = self.arg_num(&args, 1, 0.)? as f32;
4075                let cz = self.arg_num(&args, 2, 0.)? as f32;
4076                let ux = self.arg_num(&args, 3, 1.)? as f32;
4077                let uy = self.arg_num(&args, 4, 0.)? as f32;
4078                let uz = self.arg_num(&args, 5, 0.)? as f32;
4079                let vx = self.arg_num(&args, 6, 0.)? as f32;
4080                let vy = self.arg_num(&args, 7, 0.)? as f32;
4081                let vz = self.arg_num(&args, 8, 1.)? as f32;
4082                let nl = self.arg_num(&args, 9, 4.)? as usize;
4083                let mr = self.arg_num(&args, 10, 3.)? as f32;
4084                let fr = self.arg_num(&args, 11, 0.)? as f32;
4085                let hue = self.arg_num(&args, 12, 0.)? as f32;
4086                let mut gfx = self.gfx.borrow_mut();
4087                let cam = gfx.camera.clone();
4088                crate::gfx::vtex::draw_yantra(
4089                    &mut gfx.depth_queue,
4090                    &cam,
4091                    cx,
4092                    cy,
4093                    cz,
4094                    ux,
4095                    uy,
4096                    uz,
4097                    vx,
4098                    vy,
4099                    vz,
4100                    nl,
4101                    mr,
4102                    fr,
4103                    hue,
4104                );
4105                return Ok(Value::Unit);
4106            },
4107
4108            // vtex_spiked_cog(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_teeth,r_body,r_spike,r_hub,n_spokes, fr,hue)
4109            "vtex_spiked_cog" | "ฟันเฟืองหนาม" | "纹棘轮" | "歯車模様" | "톱니바퀴" =>
4110            {
4111                let cx = self.arg_num(&args, 0, 0.)? as f32;
4112                let cy = self.arg_num(&args, 1, 0.)? as f32;
4113                let cz = self.arg_num(&args, 2, 0.)? as f32;
4114                let ux = self.arg_num(&args, 3, 1.)? as f32;
4115                let uy = self.arg_num(&args, 4, 0.)? as f32;
4116                let uz = self.arg_num(&args, 5, 0.)? as f32;
4117                let vx = self.arg_num(&args, 6, 0.)? as f32;
4118                let vy = self.arg_num(&args, 7, 0.)? as f32;
4119                let vz = self.arg_num(&args, 8, 1.)? as f32;
4120                let nt = self.arg_num(&args, 9, 12.)? as usize;
4121                let rb = self.arg_num(&args, 10, 1.)? as f32;
4122                let rs = self.arg_num(&args, 11, 1.3)? as f32;
4123                let rh = self.arg_num(&args, 12, 0.2)? as f32;
4124                let ns = self.arg_num(&args, 13, 6.)? as usize;
4125                let fr = self.arg_num(&args, 14, 0.)? as f32;
4126                let hue = self.arg_num(&args, 15, 0.)? as f32;
4127                let mut gfx = self.gfx.borrow_mut();
4128                let cam = gfx.camera.clone();
4129                crate::gfx::vtex::draw_spiked_cog(
4130                    &mut gfx.depth_queue,
4131                    &cam,
4132                    cx,
4133                    cy,
4134                    cz,
4135                    ux,
4136                    uy,
4137                    uz,
4138                    vx,
4139                    vy,
4140                    vz,
4141                    nt,
4142                    rb,
4143                    rs,
4144                    rh,
4145                    ns,
4146                    fr,
4147                    hue,
4148                );
4149                return Ok(Value::Unit);
4150            },
4151
4152            // vtex_torii(cx,cy,cz, ux,uy,uz, vx,vy,vz, width,height, fr,hue)
4153            "vtex_torii" | "ประตูโทริอิ" | "纹鸟居" | "鳥居" | "도리이" =>
4154            {
4155                let cx = self.arg_num(&args, 0, 0.)? as f32;
4156                let cy = self.arg_num(&args, 1, 0.)? as f32;
4157                let cz = self.arg_num(&args, 2, 0.)? as f32;
4158                let ux = self.arg_num(&args, 3, 1.)? as f32;
4159                let uy = self.arg_num(&args, 4, 0.)? as f32;
4160                let uz = self.arg_num(&args, 5, 0.)? as f32;
4161                let vx = self.arg_num(&args, 6, 0.)? as f32;
4162                let vy = self.arg_num(&args, 7, 0.)? as f32;
4163                let vz = self.arg_num(&args, 8, 1.)? as f32;
4164                let w = self.arg_num(&args, 9, 4.)? as f32;
4165                let h = self.arg_num(&args, 10, 5.)? as f32;
4166                let fr = self.arg_num(&args, 11, 0.)? as f32;
4167                let hue = self.arg_num(&args, 12, 0.)? as f32;
4168                let mut gfx = self.gfx.borrow_mut();
4169                let cam = gfx.camera.clone();
4170                crate::gfx::vtex::draw_torii(
4171                    &mut gfx.depth_queue,
4172                    &cam,
4173                    cx,
4174                    cy,
4175                    cz,
4176                    ux,
4177                    uy,
4178                    uz,
4179                    vx,
4180                    vy,
4181                    vz,
4182                    w,
4183                    h,
4184                    fr,
4185                    hue,
4186                );
4187                return Ok(Value::Unit);
4188            },
4189
4190            // vtex_pagoda(cx,cy,cz, ux,uy,uz, vx,vy,vz, n_tiers,base_w,tier_h,taper,eave_out, fr,hue)
4191            "vtex_pagoda" | "เจดีย์" | "纹塔" | "塔" | "탑" => {
4192                let cx = self.arg_num(&args, 0, 0.)? as f32;
4193                let cy = self.arg_num(&args, 1, 0.)? as f32;
4194                let cz = self.arg_num(&args, 2, 0.)? as f32;
4195                let ux = self.arg_num(&args, 3, 1.)? as f32;
4196                let uy = self.arg_num(&args, 4, 0.)? as f32;
4197                let uz = self.arg_num(&args, 5, 0.)? as f32;
4198                let vx = self.arg_num(&args, 6, 0.)? as f32;
4199                let vy = self.arg_num(&args, 7, 0.)? as f32;
4200                let vz = self.arg_num(&args, 8, 1.)? as f32;
4201                let nt = self.arg_num(&args, 9, 5.)? as usize;
4202                let bw = self.arg_num(&args, 10, 2.)? as f32;
4203                let th = self.arg_num(&args, 11, 1.)? as f32;
4204                let tp = self.arg_num(&args, 12, 0.72)? as f32;
4205                let eo = self.arg_num(&args, 13, 0.28)? as f32;
4206                let fr = self.arg_num(&args, 14, 0.)? as f32;
4207                let hue = self.arg_num(&args, 15, 0.)? as f32;
4208                let mut gfx = self.gfx.borrow_mut();
4209                let cam = gfx.camera.clone();
4210                crate::gfx::vtex::draw_pagoda(
4211                    &mut gfx.depth_queue,
4212                    &cam,
4213                    cx,
4214                    cy,
4215                    cz,
4216                    ux,
4217                    uy,
4218                    uz,
4219                    vx,
4220                    vy,
4221                    vz,
4222                    nt,
4223                    bw,
4224                    th,
4225                    tp,
4226                    eo,
4227                    fr,
4228                    hue,
4229                );
4230                return Ok(Value::Unit);
4231            },
4232
4233            // ══════════════════════════════════════════════════════════════════
4234            // AUDIO BUILTINS
4235            // ══════════════════════════════════════════════════════════════════
4236
4237            // audio_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth)
4238            #[cfg(not(target_arch = "wasm32"))]
4239            "audio_tone"
4240            | "เสียงโทน"
4241            | "音调"
4242            | "音調"
4243            | "음조"
4244            | "空间音"
4245            | "空間音"
4246            | "공간음" => {
4247                let idx = self.arg_num(&args, 0, 0.0)? as usize;
4248                let x = self.arg_num(&args, 1, 0.0)? as f32;
4249                let y = self.arg_num(&args, 2, 0.0)? as f32;
4250                let z = self.arg_num(&args, 3, 0.0)? as f32;
4251                let w = self.arg_num(&args, 4, 1.0)? as f32;
4252                let freq = self.arg_num(&args, 5, 220.0)? as f32;
4253                let amp = self.arg_num(&args, 6, 0.15)? as f32;
4254                let lfo_rate = self.arg_num(&args, 7, 0.5)? as f32;
4255                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
4256                if let Some(audio) = &self.audio {
4257                    audio.set_tone(
4258                        idx,
4259                        ToneParams { x, y, z, w, freq, amp, lfo_rate, lfo_depth },
4260                    );
4261                }
4262                return Ok(Value::Unit);
4263            },
4264
4265            #[cfg(not(target_arch = "wasm32"))]
4266            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" =>
4267            {
4268                let cry = self.arg_num(&args, 0, 1.0)? as f32;
4269                let sry = self.arg_num(&args, 1, 0.0)? as f32;
4270                let crx = self.arg_num(&args, 2, 1.0)? as f32;
4271                let srx = self.arg_num(&args, 3, 0.0)? as f32;
4272                if let Some(audio) = &self.audio {
4273                    audio.set_listener(cry, sry, crx, srx);
4274                }
4275                return Ok(Value::Unit);
4276            },
4277
4278            #[cfg(not(target_arch = "wasm32"))]
4279            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" =>
4280            {
4281                let path = match args.first() {
4282                    Some(Value::Str(s)) => s.clone(),
4283                    _ => return Ok(Value::Unit),
4284                };
4285                let vol = self.arg_num(&args, 1, 0.5)? as f32;
4286                if let Some(audio) = &self.audio {
4287                    audio.load_bgm(&path, vol);
4288                }
4289                return Ok(Value::Unit);
4290            },
4291
4292            #[cfg(not(target_arch = "wasm32"))]
4293            "audio_bgm_volume"
4294            | "ระดับเสียงพื้นหลัง"
4295            | "ระดับเพลงประกอบ"
4296            | "背景乐音量"
4297            | "BGM音量"
4298            | "배경음악음량" => {
4299                let vol = self.arg_num(&args, 0, 0.5)? as f32;
4300                if let Some(audio) = &self.audio {
4301                    audio.set_bgm_volume(vol);
4302                }
4303                return Ok(Value::Unit);
4304            },
4305
4306            #[cfg(not(target_arch = "wasm32"))]
4307            "audio_volume" | "ระดับเสียง" | "音量" | "음량" => {
4308                let vol = self.arg_num(&args, 0, 0.7)? as f32;
4309                if let Some(audio) = &self.audio {
4310                    audio.set_master_volume(vol);
4311                }
4312                return Ok(Value::Unit);
4313            },
4314
4315            // WASM audio builtins — delegate to Web Audio API
4316            #[cfg(target_arch = "wasm32")]
4317            "audio_tone"
4318            | "เสียงโทน"
4319            | "音调"
4320            | "音調"
4321            | "음조"
4322            | "空间音"
4323            | "空間音"
4324            | "공간음" => {
4325                let idx = self.arg_num(&args, 0, 0.0)? as usize;
4326                let x = self.arg_num(&args, 1, 0.0)? as f32;
4327                let y = self.arg_num(&args, 2, 0.0)? as f32;
4328                let z = self.arg_num(&args, 3, 0.0)? as f32;
4329                let w = self.arg_num(&args, 4, 1.0)? as f32;
4330                let freq = self.arg_num(&args, 5, 220.0)? as f32;
4331                let amp = self.arg_num(&args, 6, 0.15)? as f32;
4332                let lfo_rate = self.arg_num(&args, 7, 0.5)? as f32;
4333                let lfo_depth = self.arg_num(&args, 8, 0.02)? as f32;
4334                crate::gfx::audio_web::set_tone(idx, x, y, z, w, freq, amp, lfo_rate, lfo_depth);
4335                return Ok(Value::Unit);
4336            },
4337
4338            #[cfg(target_arch = "wasm32")]
4339            "audio_listener" | "ผู้ฟัง" | "音频监听" | "音声リスナー" | "오디오리스너" =>
4340            {
4341                let cry = self.arg_num(&args, 0, 1.0)? as f32;
4342                let sry = self.arg_num(&args, 1, 0.0)? as f32;
4343                let crx = self.arg_num(&args, 2, 1.0)? as f32;
4344                let srx = self.arg_num(&args, 3, 0.0)? as f32;
4345                crate::gfx::audio_web::set_listener(cry, sry, crx, srx);
4346                return Ok(Value::Unit);
4347            },
4348
4349            #[cfg(target_arch = "wasm32")]
4350            "audio_bgm" | "เพลงพื้นหลัง" | "เพลงประกอบ" | "背景乐" | "BGM" | "배경음악" =>
4351            {
4352                let path = self.arg_str(&args, 0, "");
4353                let vol = self.arg_num(&args, 1, 0.5)? as f32;
4354                crate::gfx::audio_web::load_bgm(&path, vol);
4355                return Ok(Value::Unit);
4356            },
4357
4358            #[cfg(target_arch = "wasm32")]
4359            "audio_bgm_volume"
4360            | "ระดับเสียงพื้นหลัง"
4361            | "ระดับเพลงประกอบ"
4362            | "背景乐音量"
4363            | "BGM音量"
4364            | "배경음악음량" => {
4365                let vol = self.arg_num(&args, 0, 0.5)? as f32;
4366                crate::gfx::audio_web::set_bgm_volume(vol);
4367                return Ok(Value::Unit);
4368            },
4369
4370            #[cfg(target_arch = "wasm32")]
4371            "audio_volume" | "ระดับเสียง" | "音量" | "음량" => {
4372                let vol = self.arg_num(&args, 0, 0.7)? as f32;
4373                crate::gfx::audio_web::set_master_volume(vol);
4374                return Ok(Value::Unit);
4375            },
4376
4377            // ── รอหน้าต่าง() — block until window closed / Escape ──
4378            "รอหน้าต่าง" | "wait_window" | "gfx_wait" => {
4379                #[cfg(not(target_arch = "wasm32"))]
4380                loop {
4381                    let still_open = {
4382                        let gfx = self.gfx.borrow();
4383                        gfx.window
4384                            .as_ref()
4385                            .map(|w| w.is_open() && !w.is_key_down(minifb::Key::Escape))
4386                            .unwrap_or(false)
4387                    };
4388                    if !still_open {
4389                        break;
4390                    }
4391                    let (buf, w, h) = {
4392                        let gfx = self.gfx.borrow();
4393                        (gfx.buffer.clone(), gfx.width, gfx.height)
4394                    };
4395                    let mut gfx = self.gfx.borrow_mut();
4396                    if let Some(win) = gfx.window.as_mut() {
4397                        if win.update_with_buffer(&buf, w, h).is_err() {
4398                            break;
4399                        }
4400                    }
4401                }
4402                return Ok(Value::Unit);
4403            },
4404
4405            // ── File I/O ──────────────────────────────────────────────────────
4406            "read_file" | "อ่านไฟล์" => {
4407                let path = self.arg_str(&args, 0, "");
4408                return std::fs::read_to_string(&path)
4409                    .map(Value::Str)
4410                    .map_err(|e| EvalErr::from(format!("read_file '{path}': {e}")));
4411            },
4412            // ── networking (TCP, 2-peer co-op) ───────────────────────────────
4413            #[cfg(not(target_arch = "wasm32"))]
4414            "net_host" | "เน็ตโฮสต์" => {
4415                let port = self.arg_num(&args, 0, 7777.0)? as u16;
4416                net::host(port);
4417                return Ok(Value::Unit);
4418            },
4419            #[cfg(not(target_arch = "wasm32"))]
4420            "net_join" | "เน็ตจอย" => {
4421                let ip = self.arg_str(&args, 0, "127.0.0.1");
4422                let port = self.arg_num(&args, 1, 7777.0)? as u16;
4423                net::join(&ip, port);
4424                return Ok(Value::Unit);
4425            },
4426            #[cfg(not(target_arch = "wasm32"))]
4427            "net_send" | "เน็ตส่ง" => {
4428                let s = self.arg_str(&args, 0, "");
4429                net::send(&s);
4430                return Ok(Value::Unit);
4431            },
4432            #[cfg(not(target_arch = "wasm32"))]
4433            "net_recv" | "เน็ตรับ" => {
4434                return Ok(Value::Str(net::recv()));
4435            },
4436            #[cfg(not(target_arch = "wasm32"))]
4437            "net_status" | "เน็ตสถานะ" => {
4438                return Ok(Value::Number(net::status() as f64));
4439            },
4440            // ── LAN lobby discovery (UDP broadcast) ──
4441            #[cfg(not(target_arch = "wasm32"))]
4442            "net_announce" | "เน็ตประกาศ" => {
4443                let port = self.arg_num(&args, 0, 7778.0)? as u16;
4444                let info = self.arg_str(&args, 1, "");
4445                net::announce(port, &info);
4446                return Ok(Value::Unit);
4447            },
4448            #[cfg(not(target_arch = "wasm32"))]
4449            "net_announce_stop" | "เน็ตหยุดประกาศ" => {
4450                net::announce_stop();
4451                return Ok(Value::Unit);
4452            },
4453            #[cfg(not(target_arch = "wasm32"))]
4454            "net_discover" | "เน็ตค้นหา" => {
4455                let port = self.arg_num(&args, 0, 7778.0)? as u16;
4456                return Ok(Value::Str(net::discover(port)));
4457            },
4458            #[cfg(not(target_arch = "wasm32"))]
4459            "net_test" | "เน็ตทดสอบ" => {
4460                let port = self.arg_num(&args, 0, 7777.0)? as u16;
4461                return Ok(Value::Str(net::test_bind(port)));
4462            },
4463            // ── gamepad (gilrs) ──
4464            #[cfg(not(target_arch = "wasm32"))]
4465            "gamepad_poll" | "จอยโพล" => {
4466                gamepad::poll();
4467                return Ok(Value::Unit);
4468            },
4469            #[cfg(not(target_arch = "wasm32"))]
4470            "gamepad_button" | "จอยปุ่ม" => {
4471                let name = self.arg_str(&args, 0, "");
4472                return Ok(Value::Number(if gamepad::button(&name) {
4473                    1.0
4474                } else {
4475                    0.0
4476                }));
4477            },
4478            #[cfg(not(target_arch = "wasm32"))]
4479            "gamepad_axis" | "จอยแกน" => {
4480                let name = self.arg_str(&args, 0, "");
4481                return Ok(Value::Number(gamepad::axis(&name) as f64));
4482            },
4483            #[cfg(not(target_arch = "wasm32"))]
4484            "gamepad_rumble" | "จอยสั่น" => {
4485                let low = self.arg_num(&args, 0, 0.0)? as f32;
4486                let high = self.arg_num(&args, 1, 0.0)? as f32;
4487                let ms = self.arg_num(&args, 2, 200.0)? as u32;
4488                gamepad::rumble(low, high, ms);
4489                return Ok(Value::Unit);
4490            },
4491            #[cfg(not(target_arch = "wasm32"))]
4492            "gamepad_list" | "จอยรายการ" => {
4493                return Ok(Value::Str(gamepad::list()));
4494            },
4495            #[cfg(not(target_arch = "wasm32"))]
4496            "gamepad_any" | "จอยใดๆ" => {
4497                return Ok(Value::Number(if gamepad::any_button() { 1.0 } else { 0.0 }));
4498            },
4499
4500            // ── game AI: neural networks ─────────────────────────────────────
4501            // nn_new(inputs[, seed]) → handle
4502            #[cfg(not(target_arch = "wasm32"))]
4503            "nn_new" | "建神经网" | "ニューラル作成" | "신경망생성" | "สร้างโครงข่าย" =>
4504            {
4505                let n_in = self.arg_num(&args, 0, 1.0)?.max(0.0) as usize;
4506                let seed = self.arg_num(&args, 1, 1.0)? as u64;
4507                return Ok(Value::Number(ai::nn_new(n_in, seed) as f64));
4508            },
4509            // nn_dense(handle, units[, activation]) — append a layer
4510            #[cfg(not(target_arch = "wasm32"))]
4511            "nn_dense" | "密集层" | "密層追加" | "밀집층" | "ชั้นหนาแน่น" =>
4512            {
4513                let id = self.arg_num(&args, 0, -1.0)? as i64;
4514                let units = self.arg_num(&args, 1, 1.0)?.max(1.0) as usize;
4515                let act = self.arg_str(&args, 2, "relu");
4516                ai::nn_dense(id, units, &act);
4517                return Ok(Value::Unit);
4518            },
4519            // nn_forward(handle, [inputs]) → [outputs]
4520            #[cfg(not(target_arch = "wasm32"))]
4521            "nn_forward" | "神经前向" | "順伝播" | "순전파" | "ส่งต่อโครงข่าย" =>
4522            {
4523                let id = self.arg_num(&args, 0, -1.0)? as i64;
4524                let input = self.arg_list_f32(&args, 1);
4525                let out = ai::nn_forward(id, &input);
4526                return Ok(Value::List(
4527                    out.into_iter().map(|v| Value::Number(v as f64)).collect(),
4528                ));
4529            },
4530            // nn_train(handle, [inputs], [targets][, lr]) → loss
4531            #[cfg(not(target_arch = "wasm32"))]
4532            "nn_train" | "训练网" | "ニューラル学習" | "신경망학습" | "ฝึกโครงข่าย" =>
4533            {
4534                let id = self.arg_num(&args, 0, -1.0)? as i64;
4535                let input = self.arg_list_f32(&args, 1);
4536                let target = self.arg_list_f32(&args, 2);
4537                let lr = self.arg_num(&args, 3, 0.01)? as f32;
4538                return Ok(Value::Number(ai::nn_train(id, &input, &target, lr) as f64));
4539            },
4540            // nn_save(handle, path) → bool
4541            #[cfg(not(target_arch = "wasm32"))]
4542            "nn_save" | "保存网" | "網保存" | "신경망저장" | "บันทึกโครงข่าย" =>
4543            {
4544                let id = self.arg_num(&args, 0, -1.0)? as i64;
4545                let path = self.arg_str(&args, 1, "model.lnn");
4546                return Ok(Value::Bool(ai::nn_save(id, &path)));
4547            },
4548            // nn_load(path) → handle (-1 on failure)
4549            #[cfg(not(target_arch = "wasm32"))]
4550            "nn_load" | "载入网" | "網読込" | "신경망불러오기" | "โหลดโครงข่าย" =>
4551            {
4552                let path = self.arg_str(&args, 0, "model.lnn");
4553                return Ok(Value::Number(ai::nn_load(&path) as f64));
4554            },
4555
4556            // ── game AI: behavior trees ──────────────────────────────────────
4557            // bt_build(dsl_string) → handle
4558            #[cfg(not(target_arch = "wasm32"))]
4559            "bt_build" | "建行为树" | "行動木構築" | "행동트리구성" | "สร้างต้นไม้พฤติกรรม" =>
4560            {
4561                let spec = self.arg_str(&args, 0, "");
4562                return Ok(Value::Number(ai::bt_build(&spec) as f64));
4563            },
4564            // bt_set(handle, key, value) — set a blackboard fact
4565            #[cfg(not(target_arch = "wasm32"))]
4566            "bt_set" | "设事实" | "事実設定" | "사실설정" | "ตั้งข้อเท็จจริง" =>
4567            {
4568                let id = self.arg_num(&args, 0, -1.0)? as i64;
4569                let key = self.arg_str(&args, 1, "");
4570                let val = self.arg_num(&args, 2, 0.0)? as f32;
4571                ai::bt_set(id, &key, val);
4572                return Ok(Value::Unit);
4573            },
4574            // bt_tick(handle) → chosen action name ("" if none)
4575            #[cfg(not(target_arch = "wasm32"))]
4576            "bt_tick" | "行为树滴答" | "行動木更新" | "행동트리틱" | "เดินต้นไม้พฤติกรรม" =>
4577            {
4578                let id = self.arg_num(&args, 0, -1.0)? as i64;
4579                return Ok(Value::Str(ai::bt_tick(id)));
4580            },
4581            // bt_status(handle) → 0 fail / 1 success / 2 running
4582            #[cfg(not(target_arch = "wasm32"))]
4583            "bt_status" | "行为树状态" | "行動木状態" | "행동트리상태" | "สถานะต้นไม้พฤติกรรม" =>
4584            {
4585                let id = self.arg_num(&args, 0, -1.0)? as i64;
4586                return Ok(Value::Number(ai::bt_status(id) as f64));
4587            },
4588
4589            // ── game AI: miniature dialog LLM ────────────────────────────────
4590            // dialog_new([ctx, embed, hidden, seed]) → handle
4591            #[cfg(not(target_arch = "wasm32"))]
4592            "dialog_new" | "建对话模型" | "対話モデル作成" | "대화모델생성" | "สร้างโมเดลสนทนา" =>
4593            {
4594                let ctx = self.arg_num(&args, 0, 3.0)?.max(1.0) as usize;
4595                let embed = self.arg_num(&args, 1, 32.0)?.max(1.0) as usize;
4596                let hidden = self.arg_num(&args, 2, 64.0)?.max(1.0) as usize;
4597                let seed = self.arg_num(&args, 3, 1.0)? as u64;
4598                return Ok(Value::Number(
4599                    ai::dialog_new(ctx, embed, hidden, seed) as f64
4600                ));
4601            },
4602            // dialog_learn(handle, text) — add one utterance to the corpus
4603            #[cfg(not(target_arch = "wasm32"))]
4604            "dialog_learn" | "对话学习" | "対話学習" | "대화학습" | "เรียนรู้สนทนา" =>
4605            {
4606                let id = self.arg_num(&args, 0, -1.0)? as i64;
4607                let text = self.arg_str(&args, 1, "");
4608                ai::dialog_learn(id, &text);
4609                return Ok(Value::Unit);
4610            },
4611            // dialog_load(handle, path) → lines added (-1 on error)
4612            #[cfg(not(target_arch = "wasm32"))]
4613            "dialog_load" | "对话载入" | "対話読込" | "대화불러오기" | "โหลดชุดสนทนา" =>
4614            {
4615                let id = self.arg_num(&args, 0, -1.0)? as i64;
4616                let path = self.arg_str(&args, 1, "");
4617                return Ok(Value::Number(ai::dialog_load(id, &path) as f64));
4618            },
4619            // dialog_train(handle[, epochs, lr]) → loss
4620            #[cfg(not(target_arch = "wasm32"))]
4621            "dialog_train" | "对话训练" | "対話訓練" | "대화훈련" | "ฝึกสนทนา" =>
4622            {
4623                let id = self.arg_num(&args, 0, -1.0)? as i64;
4624                let epochs = self.arg_num(&args, 1, 20.0)?.max(1.0) as usize;
4625                let lr = self.arg_num(&args, 2, 0.1)? as f32;
4626                return Ok(Value::Number(ai::dialog_train(id, epochs, lr) as f64));
4627            },
4628            // dialog_say(handle, prompt[, max_tokens, temperature]) → reply text
4629            #[cfg(not(target_arch = "wasm32"))]
4630            "dialog_say" | "对话生成" | "対話生成" | "대화생성" | "พูดสนทนา" =>
4631            {
4632                let id = self.arg_num(&args, 0, -1.0)? as i64;
4633                let prompt = self.arg_str(&args, 1, "");
4634                let max = self.arg_num(&args, 2, 24.0)?.max(1.0) as usize;
4635                let temp = self.arg_num(&args, 3, 0.8)? as f32;
4636                return Ok(Value::Str(ai::dialog_say(id, &prompt, max, temp)));
4637            },
4638            // dialog_save(handle, path) → bool
4639            #[cfg(not(target_arch = "wasm32"))]
4640            "dialog_save" | "对话存模" | "対話モデル保存" | "대화모델저장" | "บันทึกโมเดลสนทนา" =>
4641            {
4642                let id = self.arg_num(&args, 0, -1.0)? as i64;
4643                let path = self.arg_str(&args, 1, "model.llm");
4644                return Ok(Value::Bool(ai::dialog_save(id, &path)));
4645            },
4646            // dialog_load_model(path) → handle (-1 on failure)
4647            #[cfg(not(target_arch = "wasm32"))]
4648            "dialog_load_model"
4649            | "对话载模"
4650            | "対話モデル読込"
4651            | "대화모델불러오기"
4652            | "โหลดโมเดลสนทนา" => {
4653                let path = self.arg_str(&args, 0, "model.llm");
4654                return Ok(Value::Number(ai::dialog_load_model(&path) as f64));
4655            },
4656
4657            "write_file" | "เขียนไฟล์" => {
4658                let path = self.arg_str(&args, 0, "");
4659                let content = self.arg_str(&args, 1, "");
4660                std::fs::write(&path, content.as_bytes())
4661                    .map_err(|e| EvalErr::from(format!("write_file '{path}': {e}")))?;
4662                return Ok(Value::Unit);
4663            },
4664            "print_file" | "พิมพ์ไฟล์" => {
4665                let content = self.arg_str(&args, 0, "");
4666                print!("{content}");
4667                return Ok(Value::Unit);
4668            },
4669
4670            // ── CLI arguments ─────────────────────────────────────────────────
4671            "get_args" | "รับอาร์กิวเมนต์" => {
4672                let v: Vec<Value> = std::env::args().map(Value::Str).collect();
4673                return Ok(Value::List(v));
4674            },
4675
4676            // ── String utilities ──────────────────────────────────────────────
4677            "split" | "str_split" | "แยก" => {
4678                let s = self.arg_str(&args, 0, "");
4679                let sep = self.arg_str(&args, 1, "\n");
4680                let sep = if sep.is_empty() { "\n".into() } else { sep };
4681                let parts: Vec<Value> = s
4682                    .split(sep.as_str())
4683                    .map(|p| Value::Str(p.to_string()))
4684                    .collect();
4685                return Ok(Value::List(parts));
4686            },
4687            "trim" | "str_trim" | "ตัดช่องว่าง" => {
4688                let s = self.arg_str(&args, 0, "");
4689                return Ok(Value::Str(s.trim().to_string()));
4690            },
4691            "starts_with" | "str_starts_with" | "เริ่มด้วย" => {
4692                let s = self.arg_str(&args, 0, "");
4693                let prefix = self.arg_str(&args, 1, "");
4694                return Ok(Value::Bool(s.starts_with(prefix.as_str())));
4695            },
4696            "ends_with" | "str_ends_with" | "ลงท้ายด้วย" => {
4697                let s = self.arg_str(&args, 0, "");
4698                let suffix = self.arg_str(&args, 1, "");
4699                return Ok(Value::Bool(s.ends_with(suffix.as_str())));
4700            },
4701            "str_replace" | "แทนสตริง" => {
4702                let s = self.arg_str(&args, 0, "");
4703                let from = self.arg_str(&args, 1, "");
4704                let to = self.arg_str(&args, 2, "");
4705                return Ok(Value::Str(s.replace(from.as_str(), to.as_str())));
4706            },
4707            "str_find" | "หาในสตริง" => {
4708                let s = self.arg_str(&args, 0, "");
4709                let needle = self.arg_str(&args, 1, "");
4710                // Return char index (not byte index) for consistency with substr
4711                let pos = s
4712                    .find(needle.as_str())
4713                    .map(|byte_i| s[..byte_i].chars().count() as f64)
4714                    .unwrap_or(-1.0);
4715                return Ok(Value::Number(pos));
4716            },
4717            "substr" | "str_slice" | "ส่วนสตริง" => {
4718                let s = self.arg_str(&args, 0, "");
4719                let start = self.arg_num(&args, 1, 0.0)? as usize;
4720                let len = args
4721                    .get(2)
4722                    .map(|v| self.to_number(v).unwrap_or(999999.0) as usize)
4723                    .unwrap_or_else(|| s.chars().count().saturating_sub(start));
4724                let chars: Vec<char> = s.chars().collect();
4725                let end = (start + len).min(chars.len());
4726                let slice: String = chars.get(start..end).unwrap_or(&[]).iter().collect();
4727                return Ok(Value::Str(slice));
4728            },
4729            "to_str" | "str" | "num_str" | "แปลงสตริง" => {
4730                let v = args.into_iter().next().unwrap_or(Value::Unit);
4731                return Ok(Value::Str(v.to_string()));
4732            },
4733            "str_repeat" | "ทำซ้ำสตริง" => {
4734                let s = self.arg_str(&args, 0, "");
4735                let n = self.arg_num(&args, 1, 1.0)? as usize;
4736                return Ok(Value::Str(s.repeat(n)));
4737            },
4738            "str_upper" => {
4739                let s = self.arg_str(&args, 0, "");
4740                return Ok(Value::Str(s.to_uppercase()));
4741            },
4742            "str_lower" => {
4743                let s = self.arg_str(&args, 0, "");
4744                return Ok(Value::Str(s.to_lowercase()));
4745            },
4746            "str_len" | "len" | "ความยาว" | "长度" | "長さ" | "길이" => {
4747                match args.first() {
4748                    Some(Value::Str(s)) => return Ok(Value::Number(s.chars().count() as f64)),
4749                    Some(Value::List(v)) => return Ok(Value::Number(v.len() as f64)),
4750                    _ => return Ok(Value::Number(0.0)),
4751                }
4752            },
4753
4754            // ── FNV-1a hash (deterministic, normalized 0.0–1.0) ──────────────
4755            "hash_str" | "แฮช" => {
4756                let s = self.arg_str(&args, 0, "");
4757                let mut h: u64 = 14695981039346656037_u64;
4758                for b in s.bytes() {
4759                    h ^= b as u64;
4760                    h = h.wrapping_mul(1099511628211);
4761                }
4762                return Ok(Value::Number((h & 0xFFFFFF) as f64 / 16777215.0));
4763            },
4764            "hash_int" | "แฮชจำนวน" => {
4765                let s = self.arg_str(&args, 0, "");
4766                let n = self.arg_num(&args, 1, 100.0)? as u64;
4767                let mut h: u64 = 14695981039346656037_u64;
4768                for b in s.bytes() {
4769                    h ^= b as u64;
4770                    h = h.wrapping_mul(1099511628211);
4771                }
4772                return Ok(Value::Number((h % n.max(1)) as f64));
4773            },
4774
4775            // ── List utilities ────────────────────────────────────────────────
4776            "list_new" | "รายการใหม่" | "新建列表" | "新規リスト" | "새목록" =>
4777            {
4778                return Ok(Value::List(Vec::new()));
4779            },
4780            "list_push" | "เพิ่มรายการ" | "列表添加" | "リスト追加" | "목록추가" =>
4781            {
4782                let lst = args.first().cloned().unwrap_or(Value::List(vec![]));
4783                let val = args.get(1).cloned().unwrap_or(Value::Unit);
4784                if let Value::List(mut v) = lst {
4785                    v.push(val);
4786                    return Ok(Value::List(v));
4787                }
4788                return Ok(Value::List(vec![val]));
4789            },
4790            "list_get" | "รับรายการ" | "取元素" | "要素取得" | "요소가져오기" =>
4791            {
4792                let lst = args.first().cloned().unwrap_or(Value::List(vec![]));
4793                let i = self.arg_num(&args, 1, 0.0)? as usize;
4794                if let Value::List(v) = lst {
4795                    return Ok(v.get(i).cloned().unwrap_or(Value::Str(String::new())));
4796                }
4797                return Ok(Value::Str(String::new()));
4798            },
4799            "list_join" | "join" | "รวมรายการ" | "连接" | "連結" | "연결" =>
4800            {
4801                let lst = args.first().cloned().unwrap_or(Value::List(vec![]));
4802                let sep = args.get(1).map(|v| v.to_string()).unwrap_or_default();
4803                if let Value::List(v) = lst {
4804                    return Ok(Value::Str(
4805                        v.iter()
4806                            .map(|x| x.to_string())
4807                            .collect::<Vec<_>>()
4808                            .join(&sep),
4809                    ));
4810                }
4811                return Ok(Value::Str(String::new()));
4812            },
4813            // blob_f32("<deflate+base64>") / blob_i32(...) — decode an embedded,
4814            // losslessly-compressed numeric blob into a list. Produced by
4815            // `ling convert`; lets converted assets carry geometry/PCM/etc. compactly.
4816            #[cfg(not(target_arch = "wasm32"))]
4817            "blob_f32" | "blob_i32" => {
4818                let s = self.arg_str(&args, 0, "");
4819                let is_i32 = name == "blob_i32";
4820                match decode_blob(&s) {
4821                    Ok(bytes) => {
4822                        let mut out = Vec::with_capacity(bytes.len() / 4);
4823                        for ch in bytes.chunks_exact(4) {
4824                            let arr = [ch[0], ch[1], ch[2], ch[3]];
4825                            let n = if is_i32 {
4826                                i32::from_le_bytes(arr) as f64
4827                            } else {
4828                                f32::from_le_bytes(arr) as f64
4829                            };
4830                            out.push(Value::Number(n));
4831                        }
4832                        return Ok(Value::List(out));
4833                    },
4834                    Err(e) => {
4835                        eprintln!("blob decode failed: {e}");
4836                        return Ok(Value::List(vec![]));
4837                    },
4838                }
4839            },
4840
4841            // ══════════════════════════════════════════════════════════════════
4842            // SVG EXPORT  (svg_begin / svg_rect / svg_circle / svg_line /
4843            //              svg_polyline / svg_text / svg_end / hsl_color)
4844            // Chinese aliases: 开始SVG 结束SVG SVG矩形 SVG圆形 SVG线段 SVG折线 SVG文本 HSL颜色
4845            // Thai aliases:    เริ่มSVG จบSVG SVGสี่เหลี่ยม SVGวงกลม SVGเส้น SVGเส้นหัก SVGข้อความ สีHSL
4846            // ══════════════════════════════════════════════════════════════════
4847            "svg_begin" | "开始SVG" | "เริ่มSVG" => {
4848                let path = self.arg_str(&args, 0, "output.svg");
4849                let width = self.arg_num(&args, 1, 800.0)?;
4850                let height = self.arg_num(&args, 2, 600.0)?;
4851                *self.svg.borrow_mut() = Some(SvgWriter::new(path, width, height));
4852                return Ok(Value::Unit);
4853            },
4854
4855            "svg_rect" | "SVG矩形" | "SVGสี่เหลี่ยม" => {
4856                let x = self.arg_num(&args, 0, 0.0)?;
4857                let y = self.arg_num(&args, 1, 0.0)?;
4858                let w = self.arg_num(&args, 2, 10.0)?;
4859                let h = self.arg_num(&args, 3, 10.0)?;
4860                let fill = self.arg_str(&args, 4, "#ffffff");
4861                if let Some(svg) = self.svg.borrow_mut().as_mut() {
4862                    svg.elements.push(format!(
4863                        "<rect x=\"{x:.1}\" y=\"{y:.1}\" width=\"{w:.1}\" \
4864                         height=\"{h:.1}\" fill=\"{fill}\"/>"
4865                    ));
4866                }
4867                return Ok(Value::Unit);
4868            },
4869
4870            "svg_circle" | "SVG圆形" | "SVGวงกลม" => {
4871                let cx = self.arg_num(&args, 0, 0.0)?;
4872                let cy = self.arg_num(&args, 1, 0.0)?;
4873                let r = self.arg_num(&args, 2, 5.0)?;
4874                let fill = self.arg_str(&args, 3, "#ffffff");
4875                if let Some(svg) = self.svg.borrow_mut().as_mut() {
4876                    svg.elements.push(format!(
4877                        "<circle cx=\"{cx:.1}\" cy=\"{cy:.1}\" r=\"{r:.1}\" fill=\"{fill}\"/>"
4878                    ));
4879                }
4880                return Ok(Value::Unit);
4881            },
4882
4883            "svg_line" | "SVG线段" | "SVGเส้น" => {
4884                let x1 = self.arg_num(&args, 0, 0.0)?;
4885                let y1 = self.arg_num(&args, 1, 0.0)?;
4886                let x2 = self.arg_num(&args, 2, 0.0)?;
4887                let y2 = self.arg_num(&args, 3, 0.0)?;
4888                let stroke = self.arg_str(&args, 4, "#ffffff");
4889                let sw = self.arg_num(&args, 5, 1.0)?;
4890                if let Some(svg) = self.svg.borrow_mut().as_mut() {
4891                    svg.elements.push(format!(
4892                        "<line x1=\"{x1:.1}\" y1=\"{y1:.1}\" x2=\"{x2:.1}\" y2=\"{y2:.1}\" \
4893                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"
4894                    ));
4895                }
4896                return Ok(Value::Unit);
4897            },
4898
4899            "svg_polyline" | "SVG折线" | "SVGเส้นหัก" => {
4900                let pts = self.arg_str(&args, 0, "");
4901                let stroke = self.arg_str(&args, 1, "#ffffff");
4902                let sw = self.arg_num(&args, 2, 1.0)?;
4903                if let Some(svg) = self.svg.borrow_mut().as_mut() {
4904                    svg.elements.push(format!(
4905                        "<polyline points=\"{pts}\" fill=\"none\" \
4906                         stroke=\"{stroke}\" stroke-width=\"{sw:.1}\"/>"
4907                    ));
4908                }
4909                return Ok(Value::Unit);
4910            },
4911
4912            "svg_text" | "SVG文本" | "SVGข้อความ" => {
4913                let x = self.arg_num(&args, 0, 0.0)?;
4914                let y = self.arg_num(&args, 1, 0.0)?;
4915                let text = self.arg_str(&args, 2, "");
4916                let fill = self.arg_str(&args, 3, "#ffffff");
4917                let size = self.arg_num(&args, 4, 12.0)?;
4918                if let Some(svg) = self.svg.borrow_mut().as_mut() {
4919                    let safe = text
4920                        .replace('&', "&amp;")
4921                        .replace('<', "&lt;")
4922                        .replace('>', "&gt;");
4923                    svg.elements.push(format!(
4924                        "<text x=\"{x:.1}\" y=\"{y:.1}\" fill=\"{fill}\" \
4925                         font-family=\"monospace\" font-size=\"{size:.0}\">{safe}</text>"
4926                    ));
4927                }
4928                return Ok(Value::Unit);
4929            },
4930
4931            "svg_end" | "结束SVG" | "จบSVG" => {
4932                {
4933                    let borrow = self.svg.borrow();
4934                    if let Some(svg) = borrow.as_ref() {
4935                        svg.save()
4936                            .map_err(|e| EvalErr::from(format!("svg_end: {e}")))?;
4937                    }
4938                }
4939                *self.svg.borrow_mut() = None;
4940                return Ok(Value::Unit);
4941            },
4942
4943            "hsl_color" | "HSL颜色" | "สีHSL" => {
4944                let h = self.arg_num(&args, 0, 0.0)?;
4945                let s = self.arg_num(&args, 1, 70.0)?;
4946                let l = self.arg_num(&args, 2, 50.0)?;
4947                return Ok(Value::Str(hsl_to_hex(h, s, l)));
4948            },
4949
4950            // ══════════════════════════════════════════════════════════════════
4951            // FFT / AUDIO ANALYSIS BUILTINS  (native only)
4952            // ══════════════════════════════════════════════════════════════════
4953
4954            // fft_push(samples_list) — feed raw audio samples and run FFT
4955            #[cfg(not(target_arch = "wasm32"))]
4956            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" =>
4957            {
4958                if let Some(Value::List(v)) = args.first() {
4959                    let samples: Vec<f32> = v
4960                        .iter()
4961                        .filter_map(|x| {
4962                            if let Value::Number(n) = x {
4963                                Some(*n as f32)
4964                            } else {
4965                                None
4966                            }
4967                        })
4968                        .collect();
4969                    self.fft.borrow_mut().push_samples(&samples);
4970                }
4971                return Ok(Value::Unit);
4972            },
4973
4974            // fft_bands(n) → list of n log-spaced magnitude bands (0..1)
4975            #[cfg(not(target_arch = "wasm32"))]
4976            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" =>
4977            {
4978                let n = self.arg_num(&args, 0, 32.0)? as usize;
4979                let bands = self.fft.borrow().freq_bands(n);
4980                *self.fft_bands_cache.borrow_mut() = bands.clone();
4981                return Ok(Value::List(
4982                    bands.into_iter().map(|v| Value::Number(v as f64)).collect(),
4983                ));
4984            },
4985
4986            // fft_beat() → bool
4987            #[cfg(not(target_arch = "wasm32"))]
4988            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" =>
4989            {
4990                return Ok(Value::Bool(self.fft.borrow().is_beat()));
4991            },
4992
4993            // fft_beat_ratio() → f64  (1.0 = at threshold, >1 = strong beat)
4994            #[cfg(not(target_arch = "wasm32"))]
4995            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" =>
4996            {
4997                return Ok(Value::Number(self.fft.borrow().beat_ratio() as f64));
4998            },
4999
5000            // fft_rms() → f64
5001            #[cfg(not(target_arch = "wasm32"))]
5002            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" => {
5003                return Ok(Value::Number(self.fft.borrow().rms() as f64));
5004            },
5005
5006            // fft_dominant_freq() → f64  in Hz
5007            #[cfg(not(target_arch = "wasm32"))]
5008            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" =>
5009            {
5010                return Ok(Value::Number(self.fft.borrow().dominant_freq() as f64));
5011            },
5012
5013            // ── wasm32 stubs: fft builtins are no-ops on web ───────────────
5014            #[cfg(target_arch = "wasm32")]
5015            "fft_push" | "วิเคราะห์เสียง" | "频谱输入" | "FFT入力" | "FFT입력" =>
5016            {
5017                return Ok(Value::Unit);
5018            },
5019            #[cfg(target_arch = "wasm32")]
5020            "fft_bands" | "แถบความถี่" | "频段" | "周波数帯" | "주파수대" =>
5021            {
5022                let n = self.arg_num(&args, 0, 32.0)? as usize;
5023                return Ok(Value::List(vec![Value::Number(0.0); n]));
5024            },
5025            #[cfg(target_arch = "wasm32")]
5026            "fft_beat" | "จังหวะเสียง" | "节拍检测" | "ビート検出" | "비트" =>
5027            {
5028                return Ok(Value::Bool(false));
5029            },
5030            #[cfg(target_arch = "wasm32")]
5031            "fft_beat_ratio" | "อัตราจังหวะ" | "节拍比" | "ビート比" | "비트비율" =>
5032            {
5033                return Ok(Value::Number(1.0));
5034            },
5035            #[cfg(target_arch = "wasm32")]
5036            "fft_rms" | "ระดับRMS" | "均方根" | "二乗平均" | "RMS레벨" => {
5037                return Ok(Value::Number(0.0));
5038            },
5039            #[cfg(target_arch = "wasm32")]
5040            "fft_dominant_freq" | "ความถี่หลัก" | "主频" | "主要周波数" | "주파수" =>
5041            {
5042                return Ok(Value::Number(0.0));
5043            },
5044
5045            // ══════════════════════════════════════════════════════════════════
5046            // PROCEDURAL TEXTURE BLIT BUILTINS  (screen-space)
5047            // All: name(dst_x, dst_y, width, height, ...params, palette)
5048            // palette: "rainbow" | "fire" | "ocean" | "psychedelic" | "neon" | "forest"
5049            // ══════════════════════════════════════════════════════════════════
5050
5051            // tex_checkerboard(x, y, w, h, tiles, r1,g1,b1, r2,g2,b2)
5052            "tex_checkerboard" | "ลายตารางหมากรุก" => {
5053                let (tx, ty, tw, th) = self.tex_rect(&args)?;
5054                let tiles = self.arg_num(&args, 4, 8.0)? as u32;
5055                let (r1, g1, b1) = (
5056                    self.arg_num(&args, 5, 255.)? as u32,
5057                    self.arg_num(&args, 6, 255.)? as u32,
5058                    self.arg_num(&args, 7, 255.)? as u32,
5059                );
5060                let (r2, g2, b2) = (
5061                    self.arg_num(&args, 8, 0.)? as u32,
5062                    self.arg_num(&args, 9, 0.)? as u32,
5063                    self.arg_num(&args, 10, 0.)? as u32,
5064                );
5065                let c1 = (r1 << 16) | (g1 << 8) | b1;
5066                let c2 = (r2 << 16) | (g2 << 8) | b2;
5067                let mut gfx = self.gfx.borrow_mut();
5068                let (bw, bh) = (gfx.width, gfx.height);
5069                for row in 0..th {
5070                    for col in 0..tw {
5071                        let cx = col as u32 * tiles / tw as u32;
5072                        let cy = row as u32 * tiles / th as u32;
5073                        let (dx, dy) = (tx + col, ty + row);
5074                        if dx < bw && dy < bh {
5075                            gfx.buffer[dy * bw + dx] = if (cx + cy) % 2 == 0 { c1 } else { c2 };
5076                        }
5077                    }
5078                }
5079                return Ok(Value::Unit);
5080            },
5081
5082            // tex_gradient(x, y, w, h, angle_deg, r1,g1,b1, r2,g2,b2)
5083            "tex_gradient" | "ลายไล่สี" => {
5084                let (tx, ty, tw, th) = self.tex_rect(&args)?;
5085                let angle = self.arg_num(&args, 4, 0.0)? as f32;
5086                let (r1, g1, b1) = (
5087                    self.arg_num(&args, 5, 0.)? as f32 / 255.,
5088                    self.arg_num(&args, 6, 0.)? as f32 / 255.,
5089                    self.arg_num(&args, 7, 0.)? as f32 / 255.,
5090                );
5091                let (r2, g2, b2) = (
5092                    self.arg_num(&args, 8, 255.)? as f32 / 255.,
5093                    self.arg_num(&args, 9, 255.)? as f32 / 255.,
5094                    self.arg_num(&args, 10, 255.)? as f32 / 255.,
5095                );
5096                let (ca, sa) = (angle.to_radians().cos(), angle.to_radians().sin());
5097                let mut gfx = self.gfx.borrow_mut();
5098                let (bw, bh) = (gfx.width, gfx.height);
5099                for row in 0..th {
5100                    for col in 0..tw {
5101                        let nx = col as f32 / tw as f32 - 0.5;
5102                        let ny = row as f32 / th as f32 - 0.5;
5103                        let t = ((nx * ca + ny * sa + 0.707) / 1.414).clamp(0., 1.);
5104                        let (dx, dy) = (tx + col, ty + row);
5105                        if dx < bw && dy < bh {
5106                            gfx.buffer[dy * bw + dx] =
5107                                tex_rgb(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t);
5108                        }
5109                    }
5110                }
5111                return Ok(Value::Unit);
5112            },
5113
5114            // tex_noise(x, y, w, h, scale, octaves, seed, palette)
5115            "tex_noise" | "ลายนอยส์" => {
5116                let (tx, ty, tw, th) = self.tex_rect(&args)?;
5117                let scale = self.arg_num(&args, 4, 4.0)? as f32;
5118                let octaves = self.arg_num(&args, 5, 4.0)? as u32;
5119                let seed = self.arg_num(&args, 6, 0.0)? as u32;
5120                let palette = self.arg_str(&args, 7, "rainbow");
5121                let mut gfx = self.gfx.borrow_mut();
5122                let (bw, bh) = (gfx.width, gfx.height);
5123                for row in 0..th {
5124                    for col in 0..tw {
5125                        let v = tex_fbm(
5126                            col as f32 * scale / tw as f32,
5127                            row as f32 * scale / th as f32,
5128                            octaves,
5129                            seed,
5130                        );
5131                        let [r, g, b] = tex_palette(&palette, v);
5132                        let (dx, dy) = (tx + col, ty + row);
5133                        if dx < bw && dy < bh {
5134                            gfx.buffer[dy * bw + dx] = tex_rgb(r, g, b);
5135                        }
5136                    }
5137                }
5138                return Ok(Value::Unit);
5139            },
5140
5141            // tex_freq_map(x, y, w, h, time, speed, palette)
5142            // Uses bands written by the last fft_bands() call.
5143            "tex_freq_map" | "ลายความถี่" => {
5144                let (tx, ty, tw, th) = self.tex_rect(&args)?;
5145                let time = self.arg_num(&args, 4, 0.0)? as f32;
5146                let speed = self.arg_num(&args, 5, 0.3)? as f32;
5147                let palette = self.arg_str(&args, 6, "rainbow");
5148                let bands: Vec<f32> = {
5149                    let c = self.fft_bands_cache.borrow();
5150                    if c.is_empty() {
5151                        vec![0.0; 32]
5152                    } else {
5153                        c.clone()
5154                    }
5155                };
5156                let n = bands.len().max(1);
5157                let mut gfx = self.gfx.borrow_mut();
5158                let (bw, bh) = (gfx.width, gfx.height);
5159                for row in 0..th {
5160                    for col in 0..tw {
5161                        let band_idx = (col * n / tw.max(1)).min(n - 1);
5162                        let mag = bands[band_idx].clamp(0., 1.);
5163                        let fill_y = (mag * th as f32) as usize;
5164                        if row >= th.saturating_sub(fill_y) {
5165                            let t = (col as f32 / tw as f32 + time * speed) % 1.0;
5166                            let [r, g, b] = tex_palette(&palette, t);
5167                            let bright = mag * (1.0 - row as f32 / th as f32 * 0.5);
5168                            let (dx, dy) = (tx + col, ty + row);
5169                            if dx < bw && dy < bh {
5170                                gfx.buffer[dy * bw + dx] =
5171                                    tex_rgb(r * bright, g * bright, b * bright);
5172                            }
5173                        }
5174                    }
5175                }
5176                return Ok(Value::Unit);
5177            },
5178
5179            // tex_spiral(x, y, w, h, freq, bands, time, palette)
5180            "tex_spiral" | "ลายเกลียวหมุน" => {
5181                let (tx, ty, tw, th) = self.tex_rect(&args)?;
5182                let freq = self.arg_num(&args, 4, 5.0)? as f32;
5183                let n_bands = self.arg_num(&args, 5, 8.0)? as f32;
5184                let time = self.arg_num(&args, 6, 0.0)? as f32;
5185                let palette = self.arg_str(&args, 7, "rainbow");
5186                let mut gfx = self.gfx.borrow_mut();
5187                let (bw, bh) = (gfx.width, gfx.height);
5188                for row in 0..th {
5189                    for col in 0..tw {
5190                        let nx = col as f32 / tw as f32 - 0.5;
5191                        let ny = row as f32 / th as f32 - 0.5;
5192                        let r = (nx * nx + ny * ny).sqrt();
5193                        let theta = ny.atan2(nx);
5194                        let t = ((r * freq - theta / std::f32::consts::TAU + time * 0.5) * n_bands
5195                            % 1.0)
5196                            .abs();
5197                        let [cr, cg, cb] = tex_palette(&palette, t);
5198                        let (dx, dy) = (tx + col, ty + row);
5199                        if dx < bw && dy < bh {
5200                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
5201                        }
5202                    }
5203                }
5204                return Ok(Value::Unit);
5205            },
5206
5207            // tex_ripple(x, y, w, h, freq, cx, cy, time, palette)
5208            "tex_ripple" | "ลายระลอก" => {
5209                let (tx, ty, tw, th) = self.tex_rect(&args)?;
5210                let freq = self.arg_num(&args, 4, 10.0)? as f32;
5211                let rcx = self.arg_num(&args, 5, 0.5)? as f32;
5212                let rcy = self.arg_num(&args, 6, 0.5)? as f32;
5213                let time = self.arg_num(&args, 7, 0.0)? as f32;
5214                let palette = self.arg_str(&args, 8, "ocean");
5215                let mut gfx = self.gfx.borrow_mut();
5216                let (bw, bh) = (gfx.width, gfx.height);
5217                for row in 0..th {
5218                    for col in 0..tw {
5219                        let nx = col as f32 / tw as f32 - rcx;
5220                        let ny = row as f32 / th as f32 - rcy;
5221                        let r = (nx * nx + ny * ny).sqrt();
5222                        let t = ((r * freq - time) % 1.0).abs();
5223                        let [cr, cg, cb] = tex_palette(&palette, t);
5224                        let (dx, dy) = (tx + col, ty + row);
5225                        if dx < bw && dy < bh {
5226                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
5227                        }
5228                    }
5229                }
5230                return Ok(Value::Unit);
5231            },
5232
5233            // tex_mandelbrot(x, y, w, h, zoom, cx, cy, max_iter, palette)
5234            "tex_mandelbrot" | "ลายแมนเดลบรอต" => {
5235                let (tx, ty, tw, th) = self.tex_rect(&args)?;
5236                let zoom = self.arg_num(&args, 4, 1.0)?;
5237                let mcx = self.arg_num(&args, 5, -0.5)?;
5238                let mcy = self.arg_num(&args, 6, 0.0)?;
5239                let max_iter = self.arg_num(&args, 7, 64.0)? as u32;
5240                let palette = self.arg_str(&args, 8, "psychedelic");
5241                let mut gfx = self.gfx.borrow_mut();
5242                let (bw, bh) = (gfx.width, gfx.height);
5243                for row in 0..th {
5244                    for col in 0..tw {
5245                        let zx0 = (col as f64 / tw as f64 - 0.5) / zoom + mcx;
5246                        let zy0 = (row as f64 / th as f64 - 0.5) / zoom + mcy;
5247                        let mut x = 0.0f64;
5248                        let mut y = 0.0f64;
5249                        let mut i = 0u32;
5250                        while i < max_iter && x * x + y * y < 4.0 {
5251                            let t = x * x - y * y + zx0;
5252                            y = 2.0 * x * y + zy0;
5253                            x = t;
5254                            i += 1;
5255                        }
5256                        let t = if i == max_iter {
5257                            0.0f32
5258                        } else {
5259                            (i as f32
5260                                - (x as f32 * x as f32 + y as f32 * y as f32).ln().ln()
5261                                    / 2.0f32.ln())
5262                                / max_iter as f32
5263                        };
5264                        let [cr, cg, cb] = tex_palette(&palette, t.clamp(0., 1.));
5265                        let (dx, dy) = (tx + col, ty + row);
5266                        if dx < bw && dy < bh {
5267                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
5268                        }
5269                    }
5270                }
5271                return Ok(Value::Unit);
5272            },
5273
5274            // tex_julia(x, y, w, h, c_re, c_im, max_iter, palette)
5275            "tex_julia" | "ลายจูเลีย" => {
5276                let (tx, ty, tw, th) = self.tex_rect(&args)?;
5277                let c_re = self.arg_num(&args, 4, -0.7)?;
5278                let c_im = self.arg_num(&args, 5, 0.27)?;
5279                let max_iter = self.arg_num(&args, 6, 64.0)? as u32;
5280                let palette = self.arg_str(&args, 7, "neon");
5281                let mut gfx = self.gfx.borrow_mut();
5282                let (bw, bh) = (gfx.width, gfx.height);
5283                for row in 0..th {
5284                    for col in 0..tw {
5285                        let mut zx = (col as f64 / tw as f64 - 0.5) * 3.5;
5286                        let mut zy = (row as f64 / th as f64 - 0.5) * 3.5;
5287                        let mut i = 0u32;
5288                        while i < max_iter && zx * zx + zy * zy < 4.0 {
5289                            let t = zx * zx - zy * zy + c_re;
5290                            zy = 2.0 * zx * zy + c_im;
5291                            zx = t;
5292                            i += 1;
5293                        }
5294                        let t = i as f32 / max_iter as f32;
5295                        let [cr, cg, cb] = tex_palette(&palette, t);
5296                        let (dx, dy) = (tx + col, ty + row);
5297                        if dx < bw && dy < bh {
5298                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
5299                        }
5300                    }
5301                }
5302                return Ok(Value::Unit);
5303            },
5304
5305            // tex_voronoi(x, y, w, h, cells, seed, palette)
5306            "tex_voronoi" | "ลายโวโรนอย" => {
5307                let (tx, ty, tw, th) = self.tex_rect(&args)?;
5308                let cells = self.arg_num(&args, 4, 16.0)? as u32;
5309                let seed = self.arg_num(&args, 5, 42.0)? as u32;
5310                let palette = self.arg_str(&args, 6, "rainbow");
5311                let pts: Vec<[f32; 2]> = (0..cells)
5312                    .map(|i| {
5313                        [
5314                            tex_hash(i as i32, 0, seed),
5315                            tex_hash(i as i32, 1, seed + 999),
5316                        ]
5317                    })
5318                    .collect();
5319                let mut gfx = self.gfx.borrow_mut();
5320                let (bw, bh) = (gfx.width, gfx.height);
5321                for row in 0..th {
5322                    for col in 0..tw {
5323                        let (fx, fy) = (col as f32 / tw as f32, row as f32 / th as f32);
5324                        let (min_d, nearest) = pts.iter().enumerate().fold(
5325                            (f32::MAX, 0usize),
5326                            |(d, idx), (i, &[cx, cy])| {
5327                                let dd = (fx - cx).powi(2) + (fy - cy).powi(2);
5328                                if dd < d {
5329                                    (dd, i)
5330                                } else {
5331                                    (d, idx)
5332                                }
5333                            },
5334                        );
5335                        let t = (nearest as f32 / cells as f32 + min_d * 4.0) % 1.0;
5336                        let [cr, cg, cb] = tex_palette(&palette, t);
5337                        let (dx, dy) = (tx + col, ty + row);
5338                        if dx < bw && dy < bh {
5339                            gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
5340                        }
5341                    }
5342                }
5343                return Ok(Value::Unit);
5344            },
5345
5346            // tex_halftone(x, y, w, h, dot_size, time, palette)
5347            "tex_halftone" | "ลายฮาล์ฟโทน" => {
5348                let (tx, ty, tw, th) = self.tex_rect(&args)?;
5349                let dot_size = self.arg_num(&args, 4, 0.05)? as f32;
5350                let time = self.arg_num(&args, 5, 0.0)? as f32;
5351                let palette = self.arg_str(&args, 6, "rainbow");
5352                let mut gfx = self.gfx.borrow_mut();
5353                let (bw, bh) = (gfx.width, gfx.height);
5354                for row in 0..th {
5355                    for col in 0..tw {
5356                        let (fx, fy) = (col as f32 / tw as f32, row as f32 / th as f32);
5357                        let gx = (fx / dot_size).floor();
5358                        let gy = (fy / dot_size).floor();
5359                        let lx = (fx / dot_size - gx - 0.5) * 2.0;
5360                        let ly = (fy / dot_size - gy - 0.5) * 2.0;
5361                        let r = (lx * lx + ly * ly).sqrt();
5362                        let t = (gx / (1.0 / dot_size) + time * 0.1) % 1.0;
5363                        let a = if r < 0.7 {
5364                            ((0.7 - r) / 0.7).clamp(0., 1.)
5365                        } else {
5366                            0.0
5367                        };
5368                        if a > 0.0 {
5369                            let [cr, cg, cb] = tex_palette(&palette, t);
5370                            let (dx, dy) = (tx + col, ty + row);
5371                            if dx < bw && dy < bh {
5372                                gfx.buffer[dy * bw + dx] = tex_rgb(cr, cg, cb);
5373                            }
5374                        }
5375                    }
5376                }
5377                return Ok(Value::Unit);
5378            },
5379
5380            // ══════════════════════════════════════════════════════════════════
5381            // RENDER / LIGHTING MODES  (holographic cel shading)
5382            // ══════════════════════════════════════════════════════════════════
5383            // set_shade_mode(m) — 0 flat · 1 cel · 2 holo (default)
5384            "set_shade_mode" | "设置着色" | "シェード設定" | "셰이드모드" | "ตั้งการแรเงา" =>
5385            {
5386                let m = self.arg_num(&args, 0, 2.0)? as u8;
5387                self.gfx.borrow_mut().shade_mode = m;
5388                return Ok(Value::Unit);
5389            },
5390            // set_cel_bands(n) — number of posterisation bands (>=2)
5391            "set_cel_bands" | "设置色阶" | "セル段数" | "셀밴드" | "ตั้งระดับสี" =>
5392            {
5393                let n = (self.arg_num(&args, 0, 4.0)? as u32).max(2);
5394                self.gfx.borrow_mut().shade.bands = n;
5395                return Ok(Value::Unit);
5396            },
5397            // set_shadow_color(r,g,b) — coloured-shadow tint, 0-255
5398            "set_shadow_color" | "设置阴影色" | "影の色" | "그림자색" | "ตั้งสีเงา" =>
5399            {
5400                let r = self.arg_num(&args, 0, 26.)? as f32 / 255.0;
5401                let g = self.arg_num(&args, 1, 33.)? as f32 / 255.0;
5402                let b = self.arg_num(&args, 2, 77.)? as f32 / 255.0;
5403                self.gfx.borrow_mut().shade.shadow = [r, g, b];
5404                return Ok(Value::Unit);
5405            },
5406            // set_rim(strength, r,g,b) — holographic fresnel edge glow
5407            // ══════════════════════════════════════════════════════════════════
5408            // CRYPTOGRAPHY (ling-crypto) — geo suite, hybrid PQ KEM, holographic
5409            // Bytes cross the language boundary as lowercase hex strings.
5410            // ══════════════════════════════════════════════════════════════════
5411            #[cfg(not(target_arch = "wasm32"))]
5412            "crypto_hash" | "แฮชเข้ารหัส" | "几何哈希" | "幾何ハッシュ" | "기하해시" =>
5413            {
5414                let s = self.arg_str(&args, 0, "");
5415                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(
5416                    s.as_bytes(),
5417                ))));
5418            },
5419            // 3-D torus-knot fingerprint of any text/key → flat [x,y,z, x,y,z, …]
5420            #[cfg(not(target_arch = "wasm32"))]
5421            "knot_points" | "จุดปม" | "结点坐标" | "結び目点" | "매듭점" => {
5422                let s = self.arg_str(&args, 0, "");
5423                let shape = ling_crypto::geo::KnotShape::from_bytes(s.as_bytes());
5424                let mut out = Vec::with_capacity(shape.points.len() * 3);
5425                for p in &shape.points {
5426                    out.push(Value::Number(p[0] as f64));
5427                    out.push(Value::Number(p[1] as f64));
5428                    out.push(Value::Number(p[2] as f64));
5429                }
5430                return Ok(Value::List(out));
5431            },
5432            #[cfg(not(target_arch = "wasm32"))]
5433            "knot_label" | "ป้ายปม" | "结点标签" | "結び目ラベル" | "매듭라벨" =>
5434            {
5435                let s = self.arg_str(&args, 0, "");
5436                return Ok(Value::Str(
5437                    ling_crypto::geo::KnotShape::from_bytes(s.as_bytes()).label(),
5438                ));
5439            },
5440            // KEM keypair (hybrid X25519+ML-KEM-768) → integer handle
5441            #[cfg(not(target_arch = "wasm32"))]
5442            "knot_keygen" | "hybrid_keygen" | "สร้างกุญแจปม" | "生成密钥" | "鍵生成" | "키생성" =>
5443            {
5444                self.crypto_ids.push(ling_crypto::KnotIdentity::generate());
5445                return Ok(Value::Number((self.crypto_ids.len() - 1) as f64));
5446            },
5447            #[cfg(not(target_arch = "wasm32"))]
5448            "knot_public" | "hybrid_public" | "กุญแจสาธารณะปม" | "公钥" | "公開鍵" | "공개키" =>
5449            {
5450                let h = self.arg_num(&args, 0, 0.0)? as usize;
5451                let pk = self
5452                    .crypto_ids
5453                    .get(h)
5454                    .map(|id| hex_encode(id.public_key()))
5455                    .unwrap_or_default();
5456                return Ok(Value::Str(pk));
5457            },
5458            // encapsulate(pubkey_hex) → [ciphertext_hex, shared_secret_hex]
5459            #[cfg(not(target_arch = "wasm32"))]
5460            "knot_encapsulate"
5461            | "hybrid_encapsulate"
5462            | "ห่อกุญแจปม"
5463            | "封装密钥"
5464            | "カプセル化"
5465            | "캡슐화" => {
5466                let pk = hex_decode(&self.arg_str(&args, 0, ""));
5467                match ling_crypto::geo::knot_encapsulate(&pk) {
5468                    Ok((ct, ss)) => {
5469                        return Ok(Value::List(vec![
5470                            Value::Str(hex_encode(&ct)),
5471                            Value::Str(hex_encode(&ss)),
5472                        ]))
5473                    },
5474                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
5475                }
5476            },
5477            // decapsulate(handle, ciphertext_hex) → shared_secret_hex
5478            #[cfg(not(target_arch = "wasm32"))]
5479            "knot_decapsulate"
5480            | "hybrid_decapsulate"
5481            | "แกะกุญแจปม"
5482            | "解封装密钥"
5483            | "カプセル解除"
5484            | "캡슐해제" => {
5485                let h = self.arg_num(&args, 0, 0.0)? as usize;
5486                let ct = hex_decode(&self.arg_str(&args, 1, ""));
5487                let ss = self
5488                    .crypto_ids
5489                    .get(h)
5490                    .and_then(|id| id.decapsulate(&ct).ok())
5491                    .map(|s| hex_encode(&s))
5492                    .unwrap_or_default();
5493                return Ok(Value::Str(ss));
5494            },
5495            // Authenticated encryption (XChaCha20-Poly1305) — seal(key_hex, text) → ct_hex
5496            #[cfg(not(target_arch = "wasm32"))]
5497            "crypto_seal" | "ผนึก" | "封印" | "封印する" | "봉인" => {
5498                let key = hex_to_32(&self.arg_str(&args, 0, ""));
5499                let pt = self.arg_str(&args, 1, "");
5500                match ling_crypto::geo::holo_seal(key, pt.as_bytes()) {
5501                    Ok(ct) => return Ok(Value::Str(hex_encode(&ct))),
5502                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
5503                }
5504            },
5505            #[cfg(not(target_arch = "wasm32"))]
5506            "crypto_open" | "เปิดผนึก" | "解封" | "封印解除" | "봉인해제" =>
5507            {
5508                let key = hex_to_32(&self.arg_str(&args, 0, ""));
5509                let ct = hex_decode(&self.arg_str(&args, 1, ""));
5510                match ling_crypto::geo::holo_open(key, &ct) {
5511                    Ok(pt) => return Ok(Value::Str(String::from_utf8_lossy(&pt).into_owned())),
5512                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
5513                }
5514            },
5515            // Holographic all-or-nothing transform — 4-D fragment coords [a,b,c,d, …]
5516            #[cfg(not(target_arch = "wasm32"))]
5517            "holo_points" | "จุดโฮโลแกรม" | "全息点" | "ホログラム点" | "홀로그램점" =>
5518            {
5519                let s = self.arg_str(&args, 0, "");
5520                let frags = ling_crypto::geo::scatter(s.as_bytes());
5521                let mut out = Vec::with_capacity(frags.len() * 4);
5522                for f in &frags {
5523                    for c in f.coord {
5524                        out.push(Value::Number(c as f64));
5525                    }
5526                }
5527                return Ok(Value::List(out));
5528            },
5529            #[cfg(not(target_arch = "wasm32"))]
5530            "holo_fragment_count"
5531            | "จำนวนชิ้นโฮโลแกรม"
5532            | "全息碎片数"
5533            | "ホログラム断片数"
5534            | "홀로그램조각수" => {
5535                let s = self.arg_str(&args, 0, "");
5536                return Ok(Value::Number(
5537                    ling_crypto::geo::scatter(s.as_bytes()).len() as f64
5538                ));
5539            },
5540
5541            // ══════════════════════════════════════════════════════════════════
5542            // ling-ui — animation easings + holographic vector widgets + text I/O
5543            // ══════════════════════════════════════════════════════════════════
5544            "ease" => {
5545                let name = self.arg_str(&args, 0, "ease");
5546                let t = self.arg_num(&args, 1, 0.0)? as f32;
5547                return Ok(Value::Number(
5548                    ling_ui::Easing::from_name(&name).apply(t) as f64
5549                ));
5550            },
5551
5552            // ══════════════════════════════════════════════════════════════════
5553            // Anima — unified animation drivers (ling-animation). Organic 灵 +
5554            // mechanical 机 scalar drivers, callable per frame from a script.
5555            // ══════════════════════════════════════════════════════════════════
5556            "tween" | "补间" | "補間" | "트윈" | "แทรกค่า" => {
5557                let a = self.arg_num(&args, 0, 0.0)?;
5558                let b = self.arg_num(&args, 1, 0.0)?;
5559                let t = self.arg_num(&args, 2, 0.0)?.clamp(0.0, 1.0);
5560                return Ok(Value::Number(a + (b - a) * t));
5561            },
5562            "tween_ease" | "缓动补间" | "緩和補間" | "이징트윈" | "แทรกนุ่ม" =>
5563            {
5564                let a = self.arg_num(&args, 0, 0.0)? as f32;
5565                let b = self.arg_num(&args, 1, 0.0)? as f32;
5566                let t = self.arg_num(&args, 2, 0.0)? as f32;
5567                let kind = self.arg_str(&args, 3, "linear");
5568                let e = ling_animation::EaseFunction::from_name(&kind);
5569                return Ok(Value::Number(
5570                    ling_animation::ease::tween_ease(&a, &b, t, e) as f64,
5571                ));
5572            },
5573            // ── Organic 灵 ──
5574            "breathe" | "呼吸" | "호흡" | "หายใจ" => {
5575                let t = self.arg_num(&args, 0, 0.0)? as f32;
5576                let rate = self.arg_num(&args, 1, 1.0)? as f32;
5577                let depth = self.arg_num(&args, 2, 0.1)? as f32;
5578                return Ok(Value::Number(
5579                    ling_animation::scalar::breathe(t, rate, depth) as f64,
5580                ));
5581            },
5582            "wobble" | "摆动" | "揺れ" | "흔들림" | "โยก" => {
5583                let t = self.arg_num(&args, 0, 0.0)? as f32;
5584                let freq = self.arg_num(&args, 1, 1.0)? as f32;
5585                let amp = self.arg_num(&args, 2, 1.0)? as f32;
5586                let phase = self.arg_num(&args, 3, 0.0)? as f32;
5587                return Ok(Value::Number(
5588                    ling_animation::scalar::wobble(t, freq, amp, phase) as f64,
5589                ));
5590            },
5591            "gait_phase" | "步相" | "歩相" | "걸음위상" | "เฟสก้าว" => {
5592                let t = self.arg_num(&args, 0, 0.0)? as f32;
5593                let speed = self.arg_num(&args, 1, 1.0)? as f32;
5594                return Ok(Value::Number(
5595                    ling_animation::scalar::gait_phase(t, speed) as f64
5596                ));
5597            },
5598            "gait_swing" | "步摆" | "歩振り" | "걸음흔들" | "ก้าวแกว่ง" =>
5599            {
5600                let t = self.arg_num(&args, 0, 0.0)? as f32;
5601                let speed = self.arg_num(&args, 1, 1.0)? as f32;
5602                let stride = self.arg_num(&args, 2, 1.0)? as f32;
5603                return Ok(Value::Number(
5604                    ling_animation::scalar::gait_swing(t, speed, stride) as f64,
5605                ));
5606            },
5607            "gait_lift" | "抬脚" | "足上げ" | "발들기" | "ยกเท้า" => {
5608                let t = self.arg_num(&args, 0, 0.0)? as f32;
5609                let speed = self.arg_num(&args, 1, 1.0)? as f32;
5610                let height = self.arg_num(&args, 2, 1.0)? as f32;
5611                return Ok(Value::Number(
5612                    ling_animation::scalar::gait_lift(t, speed, height) as f64,
5613                ));
5614            },
5615            "spring_to" | "弹向" | "バネ寄せ" | "스프링이동" | "สปริงไป" =>
5616            {
5617                let pos = self.arg_num(&args, 0, 0.0)? as f32;
5618                let vel = self.arg_num(&args, 1, 0.0)? as f32;
5619                let target = self.arg_num(&args, 2, 0.0)? as f32;
5620                let stiffness = self.arg_num(&args, 3, 120.0)? as f32;
5621                let damping = self.arg_num(&args, 4, 14.0)? as f32;
5622                let dt = self.arg_num(&args, 5, 1.0 / 60.0)? as f32;
5623                let (np, nv) =
5624                    ling_animation::scalar::spring_step(pos, vel, target, stiffness, damping, dt);
5625                return Ok(Value::List(vec![
5626                    Value::Number(np as f64),
5627                    Value::Number(nv as f64),
5628                ]));
5629            },
5630            "ik2" | "反解" | "逆運動" | "역운동" | "ไอเค2" => {
5631                let l1 = self.arg_num(&args, 0, 1.0)? as f32;
5632                let l2 = self.arg_num(&args, 1, 1.0)? as f32;
5633                let tx = self.arg_num(&args, 2, 0.0)? as f32;
5634                let ty = self.arg_num(&args, 3, 0.0)? as f32;
5635                let (sh, el) = ling_animation::scalar::two_bone_ik(l1, l2, tx, ty);
5636                return Ok(Value::List(vec![
5637                    Value::Number(sh as f64),
5638                    Value::Number(el as f64),
5639                ]));
5640            },
5641            // ── Mechanical 机 ──
5642            "gear_couple" | "齿轮联动" | "歯車連動" | "기어연동" | "เฟืองทด" =>
5643            {
5644                let angle = self.arg_num(&args, 0, 0.0)? as f32;
5645                let ti = self.arg_num(&args, 1, 1.0)? as f32;
5646                let to = self.arg_num(&args, 2, 1.0)? as f32;
5647                return Ok(Value::Number(
5648                    ling_animation::scalar::gear(angle, ti, to) as f64
5649                ));
5650            },
5651            "gear_train" | "齿轮组" | "歯車列" | "기어열" | "ชุดเฟือง" => {
5652                let angle = self.arg_num(&args, 0, 0.0)? as f32;
5653                let teeth: Vec<f32> = match args.get(1) {
5654                    Some(Value::List(items)) => items
5655                        .iter()
5656                        .filter_map(|v| {
5657                            if let Value::Number(n) = v {
5658                                Some(*n as f32)
5659                            } else {
5660                                None
5661                            }
5662                        })
5663                        .collect(),
5664                    _ => Vec::new(),
5665                };
5666                let out = ling_animation::mechanism::gear_train(angle, &teeth);
5667                return Ok(Value::List(
5668                    out.into_iter().map(|a| Value::Number(a as f64)).collect(),
5669                ));
5670            },
5671            "cam_lift" | "凸轮升程" | "カム揚程" | "캠리프트" | "ยกลูกเบี้ยว" =>
5672            {
5673                let angle = self.arg_num(&args, 0, 0.0)? as f32;
5674                let lift = self.arg_num(&args, 1, 1.0)? as f32;
5675                return Ok(Value::Number(
5676                    ling_animation::scalar::cam_lift(angle, lift) as f64
5677                ));
5678            },
5679            "piston" | "活塞" | "ピストン" | "피스톤" | "ลูกสูบ" => {
5680                let angle = self.arg_num(&args, 0, 0.0)? as f32;
5681                let crank = self.arg_num(&args, 1, 1.0)? as f32;
5682                let rod = self.arg_num(&args, 2, 2.0)? as f32;
5683                return Ok(Value::Number(
5684                    ling_animation::scalar::piston(angle, crank, rod) as f64,
5685                ));
5686            },
5687            "rack" | "齿条" | "ラック" | "랙" | "แร็ค" => {
5688                let angle = self.arg_num(&args, 0, 0.0)? as f32;
5689                let radius = self.arg_num(&args, 1, 1.0)? as f32;
5690                return Ok(Value::Number(
5691                    ling_animation::scalar::rack(angle, radius) as f64
5692                ));
5693            },
5694            #[cfg(not(target_arch = "wasm32"))]
5695            "mouse_x" => {
5696                let gfx = self.gfx.borrow();
5697                let v = gfx
5698                    .window
5699                    .as_ref()
5700                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
5701                    .map(|p| p.0 as f64)
5702                    .unwrap_or(0.0);
5703                return Ok(Value::Number(v));
5704            },
5705            #[cfg(not(target_arch = "wasm32"))]
5706            "mouse_y" => {
5707                let gfx = self.gfx.borrow();
5708                let v = gfx
5709                    .window
5710                    .as_ref()
5711                    .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
5712                    .map(|p| p.1 as f64)
5713                    .unwrap_or(0.0);
5714                return Ok(Value::Number(v));
5715            },
5716            #[cfg(not(target_arch = "wasm32"))]
5717            "mouse_down" => {
5718                let gfx = self.gfx.borrow();
5719                let d = gfx
5720                    .window
5721                    .as_ref()
5722                    .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
5723                    .unwrap_or(false);
5724                return Ok(Value::Bool(d));
5725            },
5726            #[cfg(not(target_arch = "wasm32"))]
5727            "mouse_down_right" | "เมาส์ขวา" => {
5728                let gfx = self.gfx.borrow();
5729                let d = gfx
5730                    .window
5731                    .as_ref()
5732                    .map(|w| w.get_mouse_down(minifb::MouseButton::Right))
5733                    .unwrap_or(false);
5734                return Ok(Value::Bool(d));
5735            },
5736            #[cfg(not(target_arch = "wasm32"))]
5737            "mouse_down_middle" | "เมาส์กลาง" => {
5738                let gfx = self.gfx.borrow();
5739                let d = gfx
5740                    .window
5741                    .as_ref()
5742                    .map(|w| w.get_mouse_down(minifb::MouseButton::Middle))
5743                    .unwrap_or(false);
5744                return Ok(Value::Bool(d));
5745            },
5746            #[cfg(not(target_arch = "wasm32"))]
5747            "ui_hot" | "热区" | "ホットエリア" | "핫존" | "พื้นที่สัมผัส" =>
5748            {
5749                let x = self.arg_num(&args, 0, 0.0)? as f32;
5750                let y = self.arg_num(&args, 1, 0.0)? as f32;
5751                let w = self.arg_num(&args, 2, 0.0)? as f32;
5752                let h = self.arg_num(&args, 3, 0.0)? as f32;
5753                let gfx = self.gfx.borrow();
5754                let (mx, my) = gfx
5755                    .window
5756                    .as_ref()
5757                    .and_then(|win| win.get_mouse_pos(minifb::MouseMode::Clamp))
5758                    .unwrap_or((0.0, 0.0));
5759                return Ok(Value::Bool(ling_ui::holo::hit_rect(mx, my, x, y, w, h)));
5760            },
5761            // ui_text(x, y, scale, "string") — holographic vector text
5762            "ui_text" | "界面文字" | "UI文字" | "UI텍스트" | "ข้อความหน้าจอ" =>
5763            {
5764                let x = self.arg_num(&args, 0, 0.0)? as f32;
5765                let y = self.arg_num(&args, 1, 0.0)? as f32;
5766                let scale = self.arg_num(&args, 2, 16.0)? as f32;
5767                let s = self.arg_str(&args, 3, "");
5768                let segs = ling_ui::holo::text_lines(&s, x, y, scale * 0.62, scale, scale * 0.24);
5769                let mut gfx = self.gfx.borrow_mut();
5770                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
5771                for sg in segs {
5772                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
5773                }
5774                return Ok(Value::Unit);
5775            },
5776            // font_load("path.ttf") — load a vector font (outlines cached lazily as
5777            // cache/fonts/<stem>/<codepoint>.ling). Returns a handle, or -1 on failure.
5778            #[cfg(not(target_arch = "wasm32"))]
5779            "font_load" | "โหลดฟอนต์" | "加载字体" | "フォント読込" | "글꼴로드" =>
5780            {
5781                let path = self.arg_str(&args, 0, "");
5782                // Optional 2nd arg: variable-font weight (e.g. 600 for a solid, bold UI).
5783                let weight = match self.arg_num(&args, 1, 0.0)? {
5784                    w if w > 0.0 => Some(w as f32),
5785                    _ => None,
5786                };
5787                // Try the path as given, then relative to the script's directory.
5788                let mut loaded = ling_graphics::VectorFont::from_path_weight(&path, weight);
5789                if loaded.is_err() {
5790                    if let Some(dir) = &self.source_dir {
5791                        let joined = dir.join(&path);
5792                        loaded = ling_graphics::VectorFont::from_path_weight(
5793                            &joined.to_string_lossy(),
5794                            weight,
5795                        );
5796                    }
5797                }
5798                match loaded {
5799                    Ok(f) => {
5800                        let id = self.fonts.len();
5801                        self.fonts.push(f);
5802                        return Ok(Value::Number(id as f64));
5803                    },
5804                    Err(e) => {
5805                        eprintln!("font_load failed ({path}): {e}");
5806                        return Ok(Value::Number(-1.0));
5807                    },
5808                }
5809            },
5810            // font_text(handle, x, y, px, "string") — anti-aliased *stroked* vector outline
5811            // in the current set_color / set_blend. (x,y) is the text box top-left.
5812            #[cfg(not(target_arch = "wasm32"))]
5813            "font_text" | "ข้อความฟอนต์" | "字体文本" | "フォント文字" | "글꼴텍스트" =>
5814            {
5815                let id = self.arg_num(&args, 0, 0.0)? as i64;
5816                let x = self.arg_num(&args, 1, 0.0)? as f32;
5817                let y = self.arg_num(&args, 2, 0.0)? as f32;
5818                let px = self.arg_num(&args, 3, 16.0)? as f32;
5819                let s = self.arg_str(&args, 4, "");
5820                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
5821                    let strokes = self.font_layout_2d(id as usize, x, y, px, &s);
5822                    let mut gfx = self.gfx.borrow_mut();
5823                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
5824                    for pl in &strokes {
5825                        for seg in pl.windows(2) {
5826                            crate::gfx::raster::draw_line_aa(
5827                                &mut gfx.buffer,
5828                                w,
5829                                h,
5830                                color,
5831                                add,
5832                                seg[0][0],
5833                                seg[0][1],
5834                                seg[1][0],
5835                                seg[1][1],
5836                            );
5837                        }
5838                    }
5839                }
5840                return Ok(Value::Unit);
5841            },
5842            // font_text_fill(handle, x, y, px, "string") — anti-aliased *filled* vector glyphs.
5843            #[cfg(not(target_arch = "wasm32"))]
5844            "font_text_fill" | "เติมฟอนต์" | "填充字体" | "フォント塗り" | "글꼴채움" =>
5845            {
5846                let id = self.arg_num(&args, 0, 0.0)? as i64;
5847                let x = self.arg_num(&args, 1, 0.0)? as f32;
5848                let y = self.arg_num(&args, 2, 0.0)? as f32;
5849                let px = self.arg_num(&args, 3, 16.0)? as f32;
5850                let s = self.arg_str(&args, 4, "");
5851                if id >= 0 && (id as usize) < self.fonts.len() && px > 0.0 {
5852                    // fill each glyph independently so interior holes (winding) stay correct
5853                    let glyphs = self.font_layout_2d_glyphs(id as usize, x, y, px, &s);
5854                    let mut gfx = self.gfx.borrow_mut();
5855                    let (w, h, color, add) = (gfx.width, gfx.height, gfx.color, gfx.blend == 1);
5856                    for contours in &glyphs {
5857                        crate::gfx::raster::fill_contours_aa(
5858                            &mut gfx.buffer,
5859                            w,
5860                            h,
5861                            color,
5862                            add,
5863                            contours,
5864                        );
5865                    }
5866                }
5867                return Ok(Value::Unit);
5868            },
5869            // font_text_3d(handle, cx,cy,cz, ux,uy,uz, vx,vy,vz, size, "string")
5870            // — stroked vector text on a 3D plane: u = advance dir, v = up dir, size = world/em.
5871            //   Flows through the depth-sorted line pipeline, so it rotates with the camera (and 4D).
5872            #[cfg(not(target_arch = "wasm32"))]
5873            "font_text_3d" | "ข้อความฟอนต์3มิติ" | "字体3D" | "フォント3D" | "글꼴3D" =>
5874            {
5875                let id = self.arg_num(&args, 0, 0.0)? as i64;
5876                let cx = self.arg_num(&args, 1, 0.0)? as f32;
5877                let cy = self.arg_num(&args, 2, 0.0)? as f32;
5878                let cz = self.arg_num(&args, 3, 0.0)? as f32;
5879                let ux = self.arg_num(&args, 4, 1.0)? as f32;
5880                let uy = self.arg_num(&args, 5, 0.0)? as f32;
5881                let uz = self.arg_num(&args, 6, 0.0)? as f32;
5882                let vx = self.arg_num(&args, 7, 0.0)? as f32;
5883                let vy = self.arg_num(&args, 8, 1.0)? as f32;
5884                let vz = self.arg_num(&args, 9, 0.0)? as f32;
5885                let size = self.arg_num(&args, 10, 1.0)? as f32;
5886                let s = self.arg_str(&args, 11, "");
5887                if id >= 0 && (id as usize) < self.fonts.len() && size > 0.0 {
5888                    // Build world-space polylines: world = C + (pen+ex)*size*U + ey*size*V
5889                    let font = &mut self.fonts[id as usize];
5890                    let asc = font.ascent();
5891                    let mut pen = 0.0f32;
5892                    let mut lines: Vec<[f32; 6]> = Vec::new();
5893                    for ch in s.chars() {
5894                        let go = font.glyph_outline(ch, 0.01);
5895                        for pl in &go.polylines {
5896                            for seg in pl.windows(2) {
5897                                let map = |p: [f32; 2]| {
5898                                    let a = pen + p[0];
5899                                    let b = p[1] - asc; // shift so the top of the cap sits near C
5900                                    [
5901                                        cx + a * size * ux + b * size * vx,
5902                                        cy + a * size * uy + b * size * vy,
5903                                        cz + a * size * uz + b * size * vz,
5904                                    ]
5905                                };
5906                                let p0 = map(seg[0]);
5907                                let p1 = map(seg[1]);
5908                                lines.push([p0[0], p0[1], p0[2], p1[0], p1[1], p1[2]]);
5909                            }
5910                        }
5911                        pen += go.advance;
5912                    }
5913                    let mut gfx = self.gfx.borrow_mut();
5914                    let color = gfx.color;
5915                    let near = -gfx.camera.zdist + 0.05;
5916                    for l in &lines {
5917                        let (mut ax, mut ay, mut az) = (l[0], l[1], l[2]);
5918                        let (mut bx, mut by, mut bz) = (l[3], l[4], l[5]);
5919                        let da = gfx.camera.depth(ax, ay, az);
5920                        let db = gfx.camera.depth(bx, by, bz);
5921                        if da <= near && db <= near {
5922                            continue;
5923                        }
5924                        if da <= near {
5925                            let t = (near - da) / (db - da);
5926                            ax += t * (bx - ax);
5927                            ay += t * (by - ay);
5928                            az += t * (bz - az);
5929                        } else if db <= near {
5930                            let t = (near - da) / (db - da);
5931                            bx = ax + t * (bx - ax);
5932                            by = ay + t * (by - ay);
5933                            bz = az + t * (bz - az);
5934                        }
5935                        let (sax, say, da2) = gfx.camera.project(ax, ay, az);
5936                        let (sbx, sby, db2) = gfx.camera.project(bx, by, bz);
5937                        let depth = (da2 + db2) / 2.0;
5938                        gfx.depth_queue.push_line(depth, color, sax, say, sbx, sby);
5939                    }
5940                }
5941                return Ok(Value::Unit);
5942            },
5943            // font_width(handle, px, "string") — pixel width of a string in a loaded font.
5944            #[cfg(not(target_arch = "wasm32"))]
5945            "font_width" | "ความกว้างฟอนต์" | "字体宽度" | "フォント幅" | "글꼴너비" =>
5946            {
5947                let id = self.arg_num(&args, 0, 0.0)? as i64;
5948                let px = self.arg_num(&args, 1, 16.0)? as f32;
5949                let s = self.arg_str(&args, 2, "");
5950                if id >= 0 && (id as usize) < self.fonts.len() {
5951                    return Ok(Value::Number(self.fonts[id as usize].measure(&s, px) as f64));
5952                }
5953                return Ok(Value::Number(0.0));
5954            },
5955            // ui_frame(x,y,w,h, bracketLen) — sci-fi corner brackets
5956            "ui_frame" | "边框" | "フレーム枠" | "프레임틀" | "กรอบ" => {
5957                let x = self.arg_num(&args, 0, 0.0)? as f32;
5958                let y = self.arg_num(&args, 1, 0.0)? as f32;
5959                let w0 = self.arg_num(&args, 2, 0.0)? as f32;
5960                let h0 = self.arg_num(&args, 3, 0.0)? as f32;
5961                let l = self.arg_num(&args, 4, 14.0)? as f32;
5962                let segs = ling_ui::holo::corner_brackets(x, y, w0, h0, l);
5963                let mut gfx = self.gfx.borrow_mut();
5964                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
5965                for sg in segs {
5966                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
5967                }
5968                return Ok(Value::Unit);
5969            },
5970            // ui_bevel(x,y,w,h, bevel) — beveled holographic panel outline
5971            "ui_bevel" | "斜角框" | "ベベル枠" | "베벨틀" | "กรอบเฉียง" =>
5972            {
5973                let x = self.arg_num(&args, 0, 0.0)? as f32;
5974                let y = self.arg_num(&args, 1, 0.0)? as f32;
5975                let w0 = self.arg_num(&args, 2, 0.0)? as f32;
5976                let h0 = self.arg_num(&args, 3, 0.0)? as f32;
5977                let bv = self.arg_num(&args, 4, 10.0)? as f32;
5978                let segs = ling_ui::holo::beveled_rect(x, y, w0, h0, bv);
5979                let mut gfx = self.gfx.borrow_mut();
5980                let (w, h, color) = (gfx.width, gfx.height, gfx.color);
5981                for sg in segs {
5982                    draw_line(&mut gfx.buffer, w, h, color, sg[0], sg[1], sg[2], sg[3]);
5983                }
5984                return Ok(Value::Unit);
5985            },
5986
5987            // ══════════════════════════════════════════════════════════════════
5988            // VECTOR UI TOOLKIT  (crates/ling-ui/src/widgets.rs)
5989            // All widgets are vector + theme-coloured with an optional trailing
5990            // r,g,b override; interactive ones read the mouse and return state.
5991            // ══════════════════════════════════════════════════════════════════
5992            #[cfg(not(target_arch = "wasm32"))]
5993            "ui_theme" | "界面主题" | "UIテーマ" | "인터페이스테마" | "ธีมส่วนติดต่อ" =>
5994            {
5995                let cur = self.ui_theme;
5996                let primary = self.color_at(&args, 0, cur.primary);
5997                let accent = self.color_at(&args, 3, cur.accent);
5998                let track = self.color_at(&args, 6, cur.track);
5999                let warn = self.color_at(&args, 9, cur.warn);
6000                let text = self.color_at(&args, 12, cur.text);
6001                let bg = self.color_at(&args, 15, cur.bg);
6002                self.ui_theme = UiTheme { primary, accent, track, warn, text, bg };
6003                return Ok(Value::Unit);
6004            },
6005
6006            // ── HUD ──────────────────────────────────────────────────────────
6007            #[cfg(not(target_arch = "wasm32"))]
6008            "ui_radar" | "雷达" | "レーダー" | "레이더" | "เรดาร์" => {
6009                let cx = self.arg_num(&args, 0, 0.)? as f32;
6010                let cy = self.arg_num(&args, 1, 0.)? as f32;
6011                let r = self.arg_num(&args, 2, 60.)? as f32;
6012                let sweep = self.arg_num(&args, 3, 0.)? as f32;
6013                let th = self.ui_theme;
6014                let prim = self.color_at(&args, 4, th.primary);
6015                self.draw_ui(&ling_ui::widgets::radar(
6016                    cx, cy, r, sweep, prim, th.accent, th.track,
6017                ));
6018                return Ok(Value::Unit);
6019            },
6020            #[cfg(not(target_arch = "wasm32"))]
6021            "ui_compass" | "罗盘" | "コンパス" | "나침반" | "เข็มทิศ" => {
6022                let x = self.arg_num(&args, 0, 0.)? as f32;
6023                let y = self.arg_num(&args, 1, 0.)? as f32;
6024                let w0 = self.arg_num(&args, 2, 300.)? as f32;
6025                let h0 = self.arg_num(&args, 3, 24.)? as f32;
6026                let head = self.arg_num(&args, 4, 0.)? as f32;
6027                let th = self.ui_theme;
6028                let prim = self.color_at(&args, 5, th.primary);
6029                self.draw_ui(&ling_ui::widgets::compass(
6030                    x, y, w0, h0, head, prim, th.track,
6031                ));
6032                return Ok(Value::Unit);
6033            },
6034            #[cfg(not(target_arch = "wasm32"))]
6035            "ui_reticle" | "准星" | "照準" | "조준선" | "เป้าเล็ง" => {
6036                let cx = self.arg_num(&args, 0, 0.)? as f32;
6037                let cy = self.arg_num(&args, 1, 0.)? as f32;
6038                let r = self.arg_num(&args, 2, 30.)? as f32;
6039                let spread = self.arg_num(&args, 3, 0.)? as f32;
6040                let th = self.ui_theme;
6041                let prim = self.color_at(&args, 4, th.primary);
6042                self.draw_ui(&ling_ui::widgets::reticle(cx, cy, r, spread, prim));
6043                return Ok(Value::Unit);
6044            },
6045            #[cfg(not(target_arch = "wasm32"))]
6046            "ui_target" | "锁定框" | "ターゲット" | "표적" | "กรอบเป้า" =>
6047            {
6048                let x = self.arg_num(&args, 0, 0.)? as f32;
6049                let y = self.arg_num(&args, 1, 0.)? as f32;
6050                let w0 = self.arg_num(&args, 2, 80.)? as f32;
6051                let h0 = self.arg_num(&args, 3, 80.)? as f32;
6052                let lock = self.arg_num(&args, 4, 0.)? as f32;
6053                let th = self.ui_theme;
6054                let prim = self.color_at(&args, 5, th.primary);
6055                self.draw_ui(&ling_ui::widgets::target(
6056                    x, y, w0, h0, lock, prim, th.accent,
6057                ));
6058                return Ok(Value::Unit);
6059            },
6060            #[cfg(not(target_arch = "wasm32"))]
6061            "ui_panel" | "面板" | "パネル" | "패널" | "แผง" => {
6062                let x = self.arg_num(&args, 0, 0.)? as f32;
6063                let y = self.arg_num(&args, 1, 0.)? as f32;
6064                let w0 = self.arg_num(&args, 2, 200.)? as f32;
6065                let h0 = self.arg_num(&args, 3, 120.)? as f32;
6066                let bv = self.arg_num(&args, 4, 12.)? as f32;
6067                let th = self.ui_theme;
6068                let prim = self.color_at(&args, 5, th.primary);
6069                self.draw_ui(&ling_ui::widgets::panel(x, y, w0, h0, bv, prim, th.bg));
6070                return Ok(Value::Unit);
6071            },
6072            #[cfg(not(target_arch = "wasm32"))]
6073            "ui_scanlines" | "扫描线" | "走査線" | "스캔라인" | "เส้นสแกน" =>
6074            {
6075                let x = self.arg_num(&args, 0, 0.)? as f32;
6076                let y = self.arg_num(&args, 1, 0.)? as f32;
6077                let w0 = self.arg_num(&args, 2, 200.)? as f32;
6078                let h0 = self.arg_num(&args, 3, 120.)? as f32;
6079                let dens = self.arg_num(&args, 4, 24.)? as usize;
6080                let th = self.ui_theme;
6081                let line = self.color_at(&args, 5, th.track);
6082                self.draw_ui(&ling_ui::widgets::scanlines(x, y, w0, h0, dens, line));
6083                return Ok(Value::Unit);
6084            },
6085
6086            // ── Meters ───────────────────────────────────────────────────────
6087            #[cfg(not(target_arch = "wasm32"))]
6088            "ui_bar" | "进度条" | "バー" | "막대" | "แถบ" => {
6089                let x = self.arg_num(&args, 0, 0.)? as f32;
6090                let y = self.arg_num(&args, 1, 0.)? as f32;
6091                let w0 = self.arg_num(&args, 2, 160.)? as f32;
6092                let h0 = self.arg_num(&args, 3, 16.)? as f32;
6093                let val = self.arg_num(&args, 4, 0.)? as f32;
6094                let max = self.arg_num(&args, 5, 1.)? as f32;
6095                let th = self.ui_theme;
6096                let fill = self.color_at(&args, 6, th.primary);
6097                self.draw_ui(&ling_ui::widgets::bar(
6098                    x,
6099                    y,
6100                    w0,
6101                    h0,
6102                    val / max.max(1e-6),
6103                    fill,
6104                    th.track,
6105                ));
6106                return Ok(Value::Unit);
6107            },
6108            #[cfg(not(target_arch = "wasm32"))]
6109            "ui_segbar" | "分段条" | "分割バー" | "분할막대" | "แถบแบ่ง" =>
6110            {
6111                let x = self.arg_num(&args, 0, 0.)? as f32;
6112                let y = self.arg_num(&args, 1, 0.)? as f32;
6113                let w0 = self.arg_num(&args, 2, 160.)? as f32;
6114                let h0 = self.arg_num(&args, 3, 16.)? as f32;
6115                let val = self.arg_num(&args, 4, 0.)? as f32;
6116                let max = self.arg_num(&args, 5, 1.)? as f32;
6117                let segs = self.arg_num(&args, 6, 10.)? as usize;
6118                let th = self.ui_theme;
6119                let fill = self.color_at(&args, 7, th.primary);
6120                self.draw_ui(&ling_ui::widgets::segbar(
6121                    x,
6122                    y,
6123                    w0,
6124                    h0,
6125                    val / max.max(1e-6),
6126                    segs,
6127                    fill,
6128                    th.track,
6129                ));
6130                return Ok(Value::Unit);
6131            },
6132            #[cfg(not(target_arch = "wasm32"))]
6133            "ui_gauge" | "仪表" | "ゲージ" | "게이지" | "มาตรวัด" => {
6134                let cx = self.arg_num(&args, 0, 0.)? as f32;
6135                let cy = self.arg_num(&args, 1, 0.)? as f32;
6136                let r = self.arg_num(&args, 2, 50.)? as f32;
6137                let val = self.arg_num(&args, 3, 0.)? as f32;
6138                let max = self.arg_num(&args, 4, 1.)? as f32;
6139                let th = self.ui_theme;
6140                let needle = self.color_at(&args, 5, th.warn);
6141                self.draw_ui(&ling_ui::widgets::gauge(
6142                    cx,
6143                    cy,
6144                    r,
6145                    val / max.max(1e-6),
6146                    needle,
6147                    th.accent,
6148                    th.track,
6149                ));
6150                return Ok(Value::Unit);
6151            },
6152            #[cfg(not(target_arch = "wasm32"))]
6153            "ui_ring" | "环表" | "リングメーター" | "링미터" | "วงแหวนวัด" =>
6154            {
6155                let cx = self.arg_num(&args, 0, 0.)? as f32;
6156                let cy = self.arg_num(&args, 1, 0.)? as f32;
6157                let r = self.arg_num(&args, 2, 40.)? as f32;
6158                let val = self.arg_num(&args, 3, 0.)? as f32;
6159                let max = self.arg_num(&args, 4, 1.)? as f32;
6160                let th = self.ui_theme;
6161                let fill = self.color_at(&args, 5, th.primary);
6162                self.draw_ui(&ling_ui::widgets::ring(
6163                    cx,
6164                    cy,
6165                    r,
6166                    val / max.max(1e-6),
6167                    fill,
6168                    th.track,
6169                ));
6170                return Ok(Value::Unit);
6171            },
6172            #[cfg(not(target_arch = "wasm32"))]
6173            "ui_vu" | "音量条" | "VUメーター" | "음량막대" | "มาตรเสียง" =>
6174            {
6175                let x = self.arg_num(&args, 0, 0.)? as f32;
6176                let y = self.arg_num(&args, 1, 0.)? as f32;
6177                let w0 = self.arg_num(&args, 2, 160.)? as f32;
6178                let h0 = self.arg_num(&args, 3, 60.)? as f32;
6179                let levels = self.arg_list_f32(&args, 4);
6180                let th = self.ui_theme;
6181                let fill = self.color_at(&args, 5, th.primary);
6182                self.draw_ui(&ling_ui::widgets::vu(x, y, w0, h0, &levels, fill, th.warn));
6183                return Ok(Value::Unit);
6184            },
6185            #[cfg(not(target_arch = "wasm32"))]
6186            "ui_spark" | "迷你图" | "スパークライン" | "스파크라인" | "กราฟจิ๋ว" =>
6187            {
6188                let x = self.arg_num(&args, 0, 0.)? as f32;
6189                let y = self.arg_num(&args, 1, 0.)? as f32;
6190                let w0 = self.arg_num(&args, 2, 160.)? as f32;
6191                let h0 = self.arg_num(&args, 3, 40.)? as f32;
6192                let vals = self.arg_list_f32(&args, 4);
6193                let th = self.ui_theme;
6194                let line = self.color_at(&args, 5, th.accent);
6195                self.draw_ui(&ling_ui::widgets::spark(x, y, w0, h0, &vals, line));
6196                return Ok(Value::Unit);
6197            },
6198            #[cfg(not(target_arch = "wasm32"))]
6199            "ui_battery" | "电池" | "バッテリー" | "배터리" | "แบตเตอรี่" =>
6200            {
6201                let x = self.arg_num(&args, 0, 0.)? as f32;
6202                let y = self.arg_num(&args, 1, 0.)? as f32;
6203                let w0 = self.arg_num(&args, 2, 50.)? as f32;
6204                let h0 = self.arg_num(&args, 3, 22.)? as f32;
6205                let val = self.arg_num(&args, 4, 1.)? as f32;
6206                let max = self.arg_num(&args, 5, 1.)? as f32;
6207                let th = self.ui_theme;
6208                let fill = self.color_at(&args, 6, th.accent);
6209                self.draw_ui(&ling_ui::widgets::battery(
6210                    x,
6211                    y,
6212                    w0,
6213                    h0,
6214                    val / max.max(1e-6),
6215                    fill,
6216                    th.track,
6217                    th.warn,
6218                ));
6219                return Ok(Value::Unit);
6220            },
6221
6222            // ── Interface controls (interactive → return state) ──────────────
6223            #[cfg(not(target_arch = "wasm32"))]
6224            "ui_button" | "按钮" | "ボタン" | "버튼" | "ปุ่ม" => {
6225                let x = self.arg_num(&args, 0, 0.)? as f32;
6226                let y = self.arg_num(&args, 1, 0.)? as f32;
6227                let w0 = self.arg_num(&args, 2, 120.)? as f32;
6228                let h0 = self.arg_num(&args, 3, 40.)? as f32;
6229                let (mx, my, down) = self.mouse_now();
6230                let hover = ling_ui::holo::hit_rect(mx, my, x, y, w0, h0);
6231                let clicked = hover && down && !self.mouse_was_down;
6232                let th = self.ui_theme;
6233                let prim = self.color_at(&args, 4, th.primary);
6234                self.draw_ui(&ling_ui::widgets::button(
6235                    x,
6236                    y,
6237                    w0,
6238                    h0,
6239                    hover,
6240                    down && hover,
6241                    prim,
6242                    th.bg,
6243                ));
6244                return Ok(Value::Number(if clicked { 1.0 } else { 0.0 }));
6245            },
6246            #[cfg(not(target_arch = "wasm32"))]
6247            "ui_toggle" | "开关" | "トグル" | "토글" | "สวิตช์" => {
6248                let x = self.arg_num(&args, 0, 0.)? as f32;
6249                let y = self.arg_num(&args, 1, 0.)? as f32;
6250                let w0 = self.arg_num(&args, 2, 52.)? as f32;
6251                let h0 = self.arg_num(&args, 3, 24.)? as f32;
6252                let mut state = self.arg_num(&args, 4, 0.)? > 0.5;
6253                let (mx, my, down) = self.mouse_now();
6254                let hover = ling_ui::holo::hit_rect(mx, my, x, y, w0, h0);
6255                if hover && down && !self.mouse_was_down {
6256                    state = !state;
6257                }
6258                let th = self.ui_theme;
6259                let on = self.color_at(&args, 5, th.accent);
6260                self.draw_ui(&ling_ui::widgets::toggle(x, y, w0, h0, state, on, th.track));
6261                return Ok(Value::Number(if state { 1.0 } else { 0.0 }));
6262            },
6263            #[cfg(not(target_arch = "wasm32"))]
6264            "ui_slider" | "滑块" | "スライダー" | "슬라이더" | "แถบเลื่อน" =>
6265            {
6266                let x = self.arg_num(&args, 0, 0.)? as f32;
6267                let y = self.arg_num(&args, 1, 0.)? as f32;
6268                let w0 = self.arg_num(&args, 2, 160.)? as f32;
6269                let mut val = self.arg_num(&args, 3, 0.)? as f32;
6270                let mn = self.arg_num(&args, 4, 0.)? as f32;
6271                let mx_ = self.arg_num(&args, 5, 1.)? as f32;
6272                let (mx, my, down) = self.mouse_now();
6273                let hover = ling_ui::holo::hit_rect(mx, my, x - 8.0, y - 10.0, w0 + 16.0, 20.0);
6274                if hover && down {
6275                    let frac = ((mx - x) / w0).max(0.0).min(1.0);
6276                    val = mn + (mx_ - mn) * frac;
6277                }
6278                let frac = ((val - mn) / (mx_ - mn).abs().max(1e-6)).max(0.0).min(1.0);
6279                let th = self.ui_theme;
6280                let fill = self.color_at(&args, 6, th.primary);
6281                self.draw_ui(&ling_ui::widgets::slider(
6282                    x, y, w0, frac, hover, fill, th.track,
6283                ));
6284                return Ok(Value::Number(val as f64));
6285            },
6286            #[cfg(not(target_arch = "wasm32"))]
6287            "ui_checkbox" | "复选框" | "チェックボックス" | "체크박스" | "ช่องเลือก" =>
6288            {
6289                let x = self.arg_num(&args, 0, 0.)? as f32;
6290                let y = self.arg_num(&args, 1, 0.)? as f32;
6291                let s = self.arg_num(&args, 2, 20.)? as f32;
6292                let mut checked = self.arg_num(&args, 3, 0.)? > 0.5;
6293                let (mx, my, down) = self.mouse_now();
6294                let hover = ling_ui::holo::hit_rect(mx, my, x, y, s, s);
6295                if hover && down && !self.mouse_was_down {
6296                    checked = !checked;
6297                }
6298                let th = self.ui_theme;
6299                let prim = self.color_at(&args, 4, th.primary);
6300                self.draw_ui(&ling_ui::widgets::checkbox(
6301                    x, y, s, checked, hover, prim, th.track,
6302                ));
6303                return Ok(Value::Number(if checked { 1.0 } else { 0.0 }));
6304            },
6305            #[cfg(not(target_arch = "wasm32"))]
6306            "ui_tabs" | "标签页" | "タブ" | "탭" | "แท็บ" => {
6307                let x = self.arg_num(&args, 0, 0.)? as f32;
6308                let y = self.arg_num(&args, 1, 0.)? as f32;
6309                let w0 = self.arg_num(&args, 2, 240.)? as f32;
6310                let h0 = self.arg_num(&args, 3, 28.)? as f32;
6311                let count = self.arg_num(&args, 4, 3.)? as usize;
6312                let mut active = self.arg_num(&args, 5, 0.)? as i32;
6313                let (mx, my, down) = self.mouse_now();
6314                let mut hover = -1;
6315                if my >= y && my <= y + h0 && mx >= x && mx <= x + w0 && count > 0 {
6316                    hover = (((mx - x) / (w0 / count as f32)) as i32)
6317                        .max(0)
6318                        .min(count as i32 - 1);
6319                    if down && !self.mouse_was_down {
6320                        active = hover;
6321                    }
6322                }
6323                let th = self.ui_theme;
6324                let prim = self.color_at(&args, 6, th.primary);
6325                self.draw_ui(&ling_ui::widgets::tabs(
6326                    x,
6327                    y,
6328                    w0,
6329                    h0,
6330                    count,
6331                    active as usize,
6332                    hover,
6333                    prim,
6334                    th.track,
6335                ));
6336                return Ok(Value::Number(active as f64));
6337            },
6338            #[cfg(not(target_arch = "wasm32"))]
6339            "ui_progress" | "进度" | "プログレス" | "진행바" | "ความคืบหน้า" =>
6340            {
6341                let x = self.arg_num(&args, 0, 0.)? as f32;
6342                let y = self.arg_num(&args, 1, 0.)? as f32;
6343                let w0 = self.arg_num(&args, 2, 200.)? as f32;
6344                let h0 = self.arg_num(&args, 3, 12.)? as f32;
6345                let frac = self.arg_num(&args, 4, 0.)? as f32;
6346                let th = self.ui_theme;
6347                let fill = self.color_at(&args, 5, th.accent);
6348                self.draw_ui(&ling_ui::widgets::progress(
6349                    x, y, w0, h0, frac, fill, th.track,
6350                ));
6351                return Ok(Value::Unit);
6352            },
6353            #[cfg(not(target_arch = "wasm32"))]
6354            "ui_tooltip" | "提示框" | "ツールチップ" | "툴팁" | "คำแนะนำ" =>
6355            {
6356                let x = self.arg_num(&args, 0, 0.)? as f32;
6357                let y = self.arg_num(&args, 1, 0.)? as f32;
6358                let w0 = self.arg_num(&args, 2, 120.)? as f32;
6359                let h0 = self.arg_num(&args, 3, 28.)? as f32;
6360                let th = self.ui_theme;
6361                let prim = self.color_at(&args, 4, th.primary);
6362                self.draw_ui(&ling_ui::widgets::tooltip(x, y, w0, h0, prim, th.bg));
6363                return Ok(Value::Unit);
6364            },
6365            #[cfg(not(target_arch = "wasm32"))]
6366            "ui_stepper" | "步进器" | "ステッパー" | "스테퍼" | "ตัวปรับค่า" =>
6367            {
6368                let x = self.arg_num(&args, 0, 0.)? as f32;
6369                let y = self.arg_num(&args, 1, 0.)? as f32;
6370                let w0 = self.arg_num(&args, 2, 120.)? as f32;
6371                let h0 = self.arg_num(&args, 3, 28.)? as f32;
6372                let mut val = self.arg_num(&args, 4, 0.)? as f32;
6373                let step = self.arg_num(&args, 5, 1.)? as f32;
6374                let (mx, my, down) = self.mouse_now();
6375                let hm = ling_ui::holo::hit_rect(mx, my, x, y, h0, h0);
6376                let hp = ling_ui::holo::hit_rect(mx, my, x + w0 - h0, y, h0, h0);
6377                if down && !self.mouse_was_down {
6378                    if hm {
6379                        val -= step;
6380                    }
6381                    if hp {
6382                        val += step;
6383                    }
6384                }
6385                let th = self.ui_theme;
6386                let prim = self.color_at(&args, 6, th.primary);
6387                self.draw_ui(&ling_ui::widgets::stepper(
6388                    x, y, w0, h0, hm, hp, prim, th.track,
6389                ));
6390                return Ok(Value::Number(val as f64));
6391            },
6392
6393            // ── Game UI ──────────────────────────────────────────────────────
6394            #[cfg(not(target_arch = "wasm32"))]
6395            "ui_healthbar" | "血条" | "体力バー" | "체력바" | "แถบพลังชีวิต" =>
6396            {
6397                let x = self.arg_num(&args, 0, 0.)? as f32;
6398                let y = self.arg_num(&args, 1, 0.)? as f32;
6399                let w0 = self.arg_num(&args, 2, 180.)? as f32;
6400                let h0 = self.arg_num(&args, 3, 16.)? as f32;
6401                let val = self.arg_num(&args, 4, 1.)? as f32;
6402                let max = self.arg_num(&args, 5, 1.)? as f32;
6403                let pulse = self.arg_num(&args, 6, 0.)? as f32;
6404                let th = self.ui_theme;
6405                let full = self.color_at(&args, 7, th.accent);
6406                self.draw_ui(&ling_ui::widgets::healthbar(
6407                    x,
6408                    y,
6409                    w0,
6410                    h0,
6411                    val / max.max(1e-6),
6412                    pulse,
6413                    full,
6414                    th.warn,
6415                    th.track,
6416                ));
6417                return Ok(Value::Unit);
6418            },
6419            #[cfg(not(target_arch = "wasm32"))]
6420            "ui_cooldown" | "冷却" | "クールダウン" | "쿨다운" | "คูลดาวน์" =>
6421            {
6422                let cx = self.arg_num(&args, 0, 0.)? as f32;
6423                let cy = self.arg_num(&args, 1, 0.)? as f32;
6424                let r = self.arg_num(&args, 2, 28.)? as f32;
6425                let frac = self.arg_num(&args, 3, 0.)? as f32;
6426                let th = self.ui_theme;
6427                let fill = self.color_at(&args, 4, th.primary);
6428                self.draw_ui(&ling_ui::widgets::cooldown(cx, cy, r, frac, fill, th.track));
6429                return Ok(Value::Unit);
6430            },
6431            #[cfg(not(target_arch = "wasm32"))]
6432            "ui_counter" | "计数器" | "カウンター" | "카운터" | "ตัวนับ" => {
6433                let x = self.arg_num(&args, 0, 0.)? as f32;
6434                let y = self.arg_num(&args, 1, 0.)? as f32;
6435                let dw = self.arg_num(&args, 2, 14.)? as f32;
6436                let dh = self.arg_num(&args, 3, 24.)? as f32;
6437                let val = self.arg_num(&args, 4, 0.)? as i64;
6438                let digits = self.arg_num(&args, 5, 4.)? as usize;
6439                let th = self.ui_theme;
6440                let on = self.color_at(&args, 6, th.primary);
6441                let off = ling_ui::widgets::shade(th.track, 0.5);
6442                self.draw_ui(&ling_ui::widgets::counter(
6443                    x, y, dw, dh, val, digits, on, off,
6444                ));
6445                return Ok(Value::Unit);
6446            },
6447            #[cfg(not(target_arch = "wasm32"))]
6448            "ui_minimap" | "小地图" | "ミニマップ" | "미니맵" | "แผนที่ย่อ" =>
6449            {
6450                let x = self.arg_num(&args, 0, 0.)? as f32;
6451                let y = self.arg_num(&args, 1, 0.)? as f32;
6452                let w0 = self.arg_num(&args, 2, 140.)? as f32;
6453                let h0 = self.arg_num(&args, 3, 140.)? as f32;
6454                let th = self.ui_theme;
6455                let prim = self.color_at(&args, 4, th.primary);
6456                self.draw_ui(&ling_ui::widgets::minimap(x, y, w0, h0, prim, th.bg));
6457                return Ok(Value::Unit);
6458            },
6459            #[cfg(not(target_arch = "wasm32"))]
6460            "ui_dpad" | "方向键" | "方向パッド" | "방향패드" | "ปุ่มทิศทาง" =>
6461            {
6462                let cx = self.arg_num(&args, 0, 0.)? as f32;
6463                let cy = self.arg_num(&args, 1, 0.)? as f32;
6464                let r = self.arg_num(&args, 2, 50.)? as f32;
6465                let (mx, my, down) = self.mouse_now();
6466                let mut dir = 0;
6467                if down {
6468                    let (dx, dy) = (mx - cx, my - cy);
6469                    if dx * dx + dy * dy <= r * r {
6470                        if dx.abs() > dy.abs() {
6471                            dir = if dx > 0.0 { 2 } else { 4 };
6472                        } else {
6473                            dir = if dy > 0.0 { 3 } else { 1 };
6474                        }
6475                    }
6476                }
6477                let th = self.ui_theme;
6478                let prim = self.color_at(&args, 3, th.primary);
6479                self.draw_ui(&ling_ui::widgets::dpad(cx, cy, r, dir, prim, th.track));
6480                return Ok(Value::Number(dir as f64));
6481            },
6482            #[cfg(not(target_arch = "wasm32"))]
6483            "ui_slotgrid" | "物品格" | "スロットグリッド" | "슬롯격자" | "ช่องไอเทม" =>
6484            {
6485                let x = self.arg_num(&args, 0, 0.)? as f32;
6486                let y = self.arg_num(&args, 1, 0.)? as f32;
6487                let cols = self.arg_num(&args, 2, 4.)? as usize;
6488                let rows = self.arg_num(&args, 3, 1.)? as usize;
6489                let cell = self.arg_num(&args, 4, 36.)? as f32;
6490                let sel = self.arg_num(&args, 5, -1.)? as i32;
6491                let th = self.ui_theme;
6492                let prim = self.color_at(&args, 6, th.primary);
6493                self.draw_ui(&ling_ui::widgets::slotgrid(
6494                    x, y, cols, rows, cell, sel, prim, th.track,
6495                ));
6496                return Ok(Value::Unit);
6497            },
6498            #[cfg(not(target_arch = "wasm32"))]
6499            "ui_vignette" | "暗角" | "ビネット" | "비네트" | "ขอบมืด" => {
6500                let intensity = self.arg_num(&args, 0, 0.5)? as f32;
6501                let (w, h) = {
6502                    let g = self.gfx.borrow();
6503                    (g.width as f32, g.height as f32)
6504                };
6505                let th = self.ui_theme;
6506                let col = self.color_at(&args, 1, th.warn);
6507                self.draw_ui(&ling_ui::widgets::vignette(w, h, intensity, col));
6508                return Ok(Value::Unit);
6509            },
6510
6511            // ── Faux-3D in 2D space ──────────────────────────────────────────
6512            #[cfg(not(target_arch = "wasm32"))]
6513            "ui_gauge3d" | "立体仪表" | "立体ゲージ" | "입체게이지" | "มาตรวัด3มิติ" =>
6514            {
6515                let cx = self.arg_num(&args, 0, 0.)? as f32;
6516                let cy = self.arg_num(&args, 1, 0.)? as f32;
6517                let r = self.arg_num(&args, 2, 50.)? as f32;
6518                let val = self.arg_num(&args, 3, 0.)? as f32;
6519                let max = self.arg_num(&args, 4, 1.)? as f32;
6520                let spin = self.arg_num(&args, 5, 0.)? as f32;
6521                let th = self.ui_theme;
6522                let fill = self.color_at(&args, 6, th.primary);
6523                self.draw_ui(&ling_ui::widgets::gauge3d(
6524                    cx,
6525                    cy,
6526                    r,
6527                    val / max.max(1e-6),
6528                    spin,
6529                    fill,
6530                    th.track,
6531                ));
6532                return Ok(Value::Unit);
6533            },
6534            #[cfg(not(target_arch = "wasm32"))]
6535            "ui_panel3d" | "立体面板" | "立体パネル" | "입체패널" | "แผง3มิติ" =>
6536            {
6537                let x = self.arg_num(&args, 0, 0.)? as f32;
6538                let y = self.arg_num(&args, 1, 0.)? as f32;
6539                let w0 = self.arg_num(&args, 2, 200.)? as f32;
6540                let h0 = self.arg_num(&args, 3, 120.)? as f32;
6541                let depth = self.arg_num(&args, 4, 14.)? as f32;
6542                let th = self.ui_theme;
6543                let prim = self.color_at(&args, 5, th.primary);
6544                self.draw_ui(&ling_ui::widgets::panel3d(x, y, w0, h0, depth, prim, th.bg));
6545                return Ok(Value::Unit);
6546            },
6547            #[cfg(not(target_arch = "wasm32"))]
6548            "ui_radar3d" | "立体雷达" | "立体レーダー" | "입체레이더" | "เรดาร์3มิติ" =>
6549            {
6550                let cx = self.arg_num(&args, 0, 0.)? as f32;
6551                let cy = self.arg_num(&args, 1, 0.)? as f32;
6552                let r = self.arg_num(&args, 2, 60.)? as f32;
6553                let tilt = self.arg_num(&args, 3, 0.9)? as f32;
6554                let sweep = self.arg_num(&args, 4, 0.)? as f32;
6555                let th = self.ui_theme;
6556                let prim = self.color_at(&args, 5, th.primary);
6557                self.draw_ui(&ling_ui::widgets::radar3d(
6558                    cx, cy, r, tilt, sweep, prim, th.track,
6559                ));
6560                return Ok(Value::Unit);
6561            },
6562
6563            // ── Interface sounds ─────────────────────────────────────────────
6564            #[cfg(not(target_arch = "wasm32"))]
6565            "audio_blip" | "提示音" | "ビープ音" | "효과음" | "เสียงบี๊บ" =>
6566            {
6567                let freq = self.arg_num(&args, 0, 660.)? as f32;
6568                let dur = self.arg_num(&args, 1, 0.08)? as f32;
6569                let wave = Wave::from_name(&self.arg_str(&args, 2, "sine"));
6570                let amp = self.arg_num(&args, 3, 0.25)? as f32;
6571                if let Some(audio) = &self.audio {
6572                    audio.blip(freq, amp, dur, wave);
6573                }
6574                return Ok(Value::Unit);
6575            },
6576            #[cfg(not(target_arch = "wasm32"))]
6577            "ui_sound" | "界面音" | "UI音" | "인터페이스음" | "เสียงปุ่ม" =>
6578            {
6579                let name = self.arg_str(&args, 0, "click");
6580                if let Some(audio) = &self.audio {
6581                    match name.as_str() {
6582                        "hover" => audio.blip(880.0, 0.10, 0.04, Wave::Sine),
6583                        "confirm" => {
6584                            audio.blip(660.0, 0.22, 0.07, Wave::Square);
6585                            audio.blip(990.0, 0.18, 0.10, Wave::Square);
6586                        },
6587                        "error" => {
6588                            audio.blip(180.0, 0.30, 0.16, Wave::Saw);
6589                            audio.blip(140.0, 0.30, 0.18, Wave::Saw);
6590                        },
6591                        "toggle" => audio.blip(520.0, 0.22, 0.05, Wave::Triangle),
6592                        "tick" => audio.blip(1500.0, 0.12, 0.02, Wave::Square),
6593                        _ => audio.blip(720.0, 0.26, 0.05, Wave::Square), // "click"
6594                    }
6595                }
6596                return Ok(Value::Unit);
6597            },
6598
6599            // ══════════════════════════════════════════════════════════════════
6600            // MUSIC TOOLKIT  (crates/ling-music) — decode · analysis · GM synth ·
6601            // rhythm · karaoke. Analysis/decoding need no audio device; playback
6602            // and synthesis lazily start a dedicated music engine.
6603            // ══════════════════════════════════════════════════════════════════
6604
6605            // music_load(path) -> track handle (decodes WAV/FLAC/OGG/MP3/AAC)
6606            #[cfg(not(target_arch = "wasm32"))]
6607            "music_load" | "载入音乐" | "音楽読込" | "음악로드" | "โหลดเพลง" =>
6608            {
6609                let path = self.arg_str(&args, 0, "");
6610                let resolved = if std::path::Path::new(&path).exists() {
6611                    path.clone()
6612                } else if let Some(d) = &self.source_dir {
6613                    d.join(&path).to_string_lossy().into_owned()
6614                } else {
6615                    path.clone()
6616                };
6617                match ling_music::load(&resolved) {
6618                    Ok(t) => {
6619                        let id = self.tracks.len();
6620                        self.tracks.push(t);
6621                        return Ok(Value::Number(id as f64));
6622                    },
6623                    Err(e) => {
6624                        eprintln!("music_load failed ({path}): {e}");
6625                        return Ok(Value::Number(-1.0));
6626                    },
6627                }
6628            },
6629            #[cfg(not(target_arch = "wasm32"))]
6630            "music_duration" | "音乐时长" | "音楽長さ" | "음악길이" | "ความยาวเพลง" =>
6631            {
6632                let id = self.arg_num(&args, 0, 0.0)? as i64;
6633                let d = self
6634                    .tracks
6635                    .get(id as usize)
6636                    .map(|t| t.duration)
6637                    .unwrap_or(0.0);
6638                return Ok(Value::Number(d as f64));
6639            },
6640            #[cfg(not(target_arch = "wasm32"))]
6641            "music_bpm" | "节拍速度" | "テンポ" | "템포" | "จังหวะต่อนาที" =>
6642            {
6643                let id = self.arg_num(&args, 0, 0.0)? as i64;
6644                let b = self
6645                    .tracks
6646                    .get(id as usize)
6647                    .map(|t| ling_music::analysis::bpm(&t.mono, t.rate))
6648                    .unwrap_or(0.0);
6649                return Ok(Value::Number(b as f64));
6650            },
6651            #[cfg(not(target_arch = "wasm32"))]
6652            "music_key" | "调性" | "調性" | "조성" | "คีย์เพลง" => {
6653                let id = self.arg_num(&args, 0, 0.0)? as i64;
6654                let k = self
6655                    .tracks
6656                    .get(id as usize)
6657                    .map(|t| ling_music::analysis::key_name(&t.mono, t.rate))
6658                    .unwrap_or_default();
6659                return Ok(Value::Str(k));
6660            },
6661            #[cfg(not(target_arch = "wasm32"))]
6662            "music_onsets" | "音符起点" | "オンセット" | "온셋" | "จุดเริ่มเสียง" =>
6663            {
6664                let id = self.arg_num(&args, 0, 0.0)? as i64;
6665                let v = self
6666                    .tracks
6667                    .get(id as usize)
6668                    .map(|t| ling_music::analysis::onsets(&t.mono, t.rate))
6669                    .unwrap_or_default();
6670                return Ok(Value::List(
6671                    v.into_iter().map(|x| Value::Number(x as f64)).collect(),
6672                ));
6673            },
6674            #[cfg(not(target_arch = "wasm32"))]
6675            "music_beat_grid" | "节拍网格" | "ビートグリッド" | "비트그리드" | "กริดจังหวะ" =>
6676            {
6677                let id = self.arg_num(&args, 0, 0.0)? as i64;
6678                let beats = self
6679                    .tracks
6680                    .get(id as usize)
6681                    .map(|t| {
6682                        let b = ling_music::analysis::bpm(&t.mono, t.rate);
6683                        ling_music::analysis::beat_grid(&t.mono, t.rate, b)
6684                    })
6685                    .unwrap_or_default();
6686                return Ok(Value::List(
6687                    beats.into_iter().map(|x| Value::Number(x as f64)).collect(),
6688                ));
6689            },
6690
6691            // ── playback ──
6692            #[cfg(not(target_arch = "wasm32"))]
6693            "music_play" | "播放音乐" | "音楽再生" | "음악재생" | "เล่นเพลง" =>
6694            {
6695                let id = self.arg_num(&args, 0, 0.0)? as i64;
6696                if self.ensure_music() {
6697                    let track = self
6698                        .tracks
6699                        .get(id as usize)
6700                        .map(|t| (t.stereo.clone(), t.rate));
6701                    if let (Some((st, rate)), Some(m)) = (track, &self.music) {
6702                        m.set_track(st, rate);
6703                        m.play();
6704                    } else if let Some(m) = &self.music {
6705                        m.play();
6706                    }
6707                }
6708                return Ok(Value::Unit);
6709            },
6710            #[cfg(not(target_arch = "wasm32"))]
6711            "music_pause" | "暂停音乐" | "音楽一時停止" | "음악일시정지" | "หยุดเพลงชั่วคราว" =>
6712            {
6713                if let Some(m) = &self.music {
6714                    m.pause();
6715                }
6716                return Ok(Value::Unit);
6717            },
6718            #[cfg(not(target_arch = "wasm32"))]
6719            "music_stop" | "停止音乐" | "音楽停止" | "음악정지" | "หยุดเพลง" =>
6720            {
6721                if let Some(m) = &self.music {
6722                    m.stop();
6723                }
6724                return Ok(Value::Unit);
6725            },
6726            #[cfg(not(target_arch = "wasm32"))]
6727            "music_seek" | "定位音乐" | "音楽シーク" | "음악탐색" | "ค้นหาเพลง" =>
6728            {
6729                let sec = self.arg_num(&args, 0, 0.0)? as f32;
6730                if let Some(m) = &self.music {
6731                    m.seek(sec);
6732                }
6733                return Ok(Value::Unit);
6734            },
6735            #[cfg(not(target_arch = "wasm32"))]
6736            "music_pos" | "音乐位置" | "音楽位置" | "음악위치" | "ตำแหน่งเพลง" =>
6737            {
6738                let p = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
6739                return Ok(Value::Number(p as f64));
6740            },
6741            #[cfg(not(target_arch = "wasm32"))]
6742            "music_volume" | "音乐音量" | "音楽音量" | "음악음량" | "ระดับเพลง" =>
6743            {
6744                let v = self.arg_num(&args, 0, 0.8)? as f32;
6745                if self.ensure_music() {
6746                    if let Some(m) = &self.music {
6747                        m.set_volume(v);
6748                    }
6749                }
6750                return Ok(Value::Unit);
6751            },
6752
6753            // ── synthesis (GM-capable, patches from .ling files) ──
6754            #[cfg(not(target_arch = "wasm32"))]
6755            "music_patch" | "乐器音色" | "音色読込" | "악기패치" | "แพตช์เครื่องดนตรี" =>
6756            {
6757                let path = self.arg_str(&args, 0, "");
6758                let resolved = if std::path::Path::new(&path).exists() {
6759                    path.clone()
6760                } else if let Some(d) = &self.source_dir {
6761                    d.join(&path).to_string_lossy().into_owned()
6762                } else {
6763                    path.clone()
6764                };
6765                if !self.ensure_music() {
6766                    return Ok(Value::Number(-1.0));
6767                }
6768                match ling_music::patch::from_path(&resolved) {
6769                    Ok(p) => {
6770                        let id = self.music.as_ref().unwrap().add_patch(p);
6771                        return Ok(Value::Number(id as f64));
6772                    },
6773                    Err(e) => {
6774                        eprintln!("music_patch failed ({path}): {e}");
6775                        return Ok(Value::Number(-1.0));
6776                    },
6777                }
6778            },
6779            #[cfg(not(target_arch = "wasm32"))]
6780            "music_note" | "弹音符" | "音符演奏" | "음표연주" | "เล่นโน้ต" =>
6781            {
6782                let inst = self.arg_num(&args, 0, 0.0)? as usize;
6783                let midi = self.pitch_arg(&args, 1, 60);
6784                let dur = self.arg_num(&args, 2, 0.5)? as f32;
6785                let vel = self.arg_num(&args, 3, 0.9)? as f32;
6786                if self.ensure_music() {
6787                    if let Some(m) = &self.music {
6788                        m.note(inst, midi, vel, dur);
6789                    }
6790                }
6791                return Ok(Value::Unit);
6792            },
6793            #[cfg(not(target_arch = "wasm32"))]
6794            "music_note_on" | "音符开始" | "音符オン" | "음표켜기" | "โน้ตเริ่ม" =>
6795            {
6796                let inst = self.arg_num(&args, 0, 0.0)? as usize;
6797                let midi = self.pitch_arg(&args, 1, 60);
6798                let vel = self.arg_num(&args, 2, 0.9)? as f32;
6799                if self.ensure_music() {
6800                    if let Some(m) = &self.music {
6801                        m.note_on(inst, midi, vel);
6802                    }
6803                }
6804                return Ok(Value::Unit);
6805            },
6806            #[cfg(not(target_arch = "wasm32"))]
6807            "music_note_off" | "音符结束" | "音符オフ" | "음표끄기" | "โน้ตจบ" =>
6808            {
6809                let inst = self.arg_num(&args, 0, 0.0)? as usize;
6810                let midi = self.pitch_arg(&args, 1, 60);
6811                if let Some(m) = &self.music {
6812                    m.note_off(inst, midi);
6813                }
6814                return Ok(Value::Unit);
6815            },
6816
6817            // ── rhythm-game judging ──
6818            #[cfg(not(target_arch = "wasm32"))]
6819            "music_judge" | "判定" | "判定する" | "판정" | "ตัดสินจังหวะ" =>
6820            {
6821                let delta_ms = self.arg_num(&args, 0, 9999.0)? as f32;
6822                return Ok(Value::Number(
6823                    ling_music::Grade::judge(delta_ms).index() as f64
6824                ));
6825            },
6826            #[cfg(not(target_arch = "wasm32"))]
6827            "music_grade_name" | "判定名" | "判定名称" | "판정이름" | "ชื่อการตัดสิน" =>
6828            {
6829                let idx = self.arg_num(&args, 0, 4.0)? as i32;
6830                return Ok(Value::Str(
6831                    ling_music::Grade::from_index(idx).name().to_string(),
6832                ));
6833            },
6834
6835            // ── karaoke ──
6836            #[cfg(not(target_arch = "wasm32"))]
6837            "music_lrc" | "载入歌词" | "歌詞読込" | "가사로드" | "โหลดเนื้อเพลง" =>
6838            {
6839                let path = self.arg_str(&args, 0, "");
6840                let resolved = if std::path::Path::new(&path).exists() {
6841                    path.clone()
6842                } else if let Some(d) = &self.source_dir {
6843                    d.join(&path).to_string_lossy().into_owned()
6844                } else {
6845                    path.clone()
6846                };
6847                match std::fs::read_to_string(&resolved) {
6848                    Ok(text) => {
6849                        let id = self.lyrics.len();
6850                        self.lyrics.push(ling_music::Lyrics::parse(&text));
6851                        return Ok(Value::Number(id as f64));
6852                    },
6853                    Err(e) => {
6854                        eprintln!("music_lrc failed ({path}): {e}");
6855                        return Ok(Value::Number(-1.0));
6856                    },
6857                }
6858            },
6859            #[cfg(not(target_arch = "wasm32"))]
6860            "music_lyric" | "当前歌词" | "現在歌詞" | "현재가사" | "เนื้อเพลงปัจจุบัน" =>
6861            {
6862                let id = self.arg_num(&args, 0, 0.0)? as i64;
6863                let t = self.arg_num(&args, 1, 0.0)? as f32;
6864                let line = self
6865                    .lyrics
6866                    .get(id as usize)
6867                    .map(|l| l.line_at(t).to_string())
6868                    .unwrap_or_default();
6869                return Ok(Value::Str(line));
6870            },
6871            #[cfg(not(target_arch = "wasm32"))]
6872            "music_mic_pitch" | "麦克风音高" | "マイク音程" | "마이크음정" | "ระดับเสียงไมค์" =>
6873            {
6874                let hz = if let Some(mic) = self.mic.as_ref() {
6875                    let s = mic.latest_samples();
6876                    let rate = mic.sample_rate();
6877                    ling_music::pitch::detect(&s, rate).unwrap_or(0.0)
6878                } else {
6879                    0.0
6880                };
6881                return Ok(Value::Number(hz as f64));
6882            },
6883            #[cfg(not(target_arch = "wasm32"))]
6884            "music_note_name" | "音名" | "音名称" | "음이름" | "ชื่อโน้ต" =>
6885            {
6886                let hz = self.arg_num(&args, 0, 0.0)? as f32;
6887                return Ok(Value::Str(ling_music::note::hz_to_name(hz)));
6888            },
6889            #[cfg(not(target_arch = "wasm32"))]
6890            "music_hz" | "音符频率" | "音符周波数" | "음표주파수" | "ความถี่โน้ต" =>
6891            {
6892                let midi = self.pitch_arg(&args, 0, 69);
6893                return Ok(Value::Number(
6894                    ling_music::note::midi_to_hz(midi as f32) as f64
6895                ));
6896            },
6897            #[cfg(not(target_arch = "wasm32"))]
6898            "music_pitch_score" | "音准评分" | "音程スコア" | "음정점수" | "คะแนนเสียง" =>
6899            {
6900                let hz = self.arg_num(&args, 0, 0.0)? as f32;
6901                let target = self.arg_num(&args, 1, 0.0)? as f32;
6902                return Ok(Value::Number(
6903                    ling_music::karaoke::pitch_score(hz, target) as f64
6904                ));
6905            },
6906
6907            // ── MIDI (inaudible note source: drive coins, cues, etc.) ──
6908            #[cfg(not(target_arch = "wasm32"))]
6909            "music_midi_load" | "载入MIDI" | "MIDI読込" | "미디로드" | "โหลดมิดี" =>
6910            {
6911                let path = self.arg_str(&args, 0, "");
6912                let resolved = if std::path::Path::new(&path).exists() {
6913                    path.clone()
6914                } else if let Some(d) = &self.source_dir {
6915                    d.join(&path).to_string_lossy().into_owned()
6916                } else {
6917                    path.clone()
6918                };
6919                match ling_music::midi::load(&resolved) {
6920                    Ok(m) => {
6921                        let id = self.midis.len();
6922                        self.midis.push(m);
6923                        return Ok(Value::Number(id as f64));
6924                    },
6925                    Err(e) => {
6926                        eprintln!("music_midi_load failed ({path}): {e}");
6927                        return Ok(Value::Number(-1.0));
6928                    },
6929                }
6930            },
6931            #[cfg(not(target_arch = "wasm32"))]
6932            "music_midi_count" | "MIDI数量" | "MIDI数" | "미디수" | "จำนวนมิดี" =>
6933            {
6934                let id = self.arg_num(&args, 0, 0.0)? as i64;
6935                let n = self
6936                    .midis
6937                    .get(id as usize)
6938                    .map(|m| m.notes.len())
6939                    .unwrap_or(0);
6940                return Ok(Value::Number(n as f64));
6941            },
6942            // music_midi_notes(id) -> flat [time, midi, time, midi, …]
6943            #[cfg(not(target_arch = "wasm32"))]
6944            "music_midi_notes" | "MIDI音符" | "MIDIノート" | "미디음표" | "โน้ตมิดี" =>
6945            {
6946                let id = self.arg_num(&args, 0, 0.0)? as i64;
6947                let mut out = Vec::new();
6948                if let Some(m) = self.midis.get(id as usize) {
6949                    for n in &m.notes {
6950                        out.push(Value::Number(n.time as f64));
6951                        out.push(Value::Number(n.midi as f64));
6952                    }
6953                }
6954                return Ok(Value::List(out));
6955            },
6956            // music_midi_bars(id) -> flat [time, midi, dur, …] (for karaoke note bars)
6957            #[cfg(not(target_arch = "wasm32"))]
6958            "music_midi_bars" | "MIDI音条" | "MIDIバー" | "미디바" | "แท่งมิดี" =>
6959            {
6960                let id = self.arg_num(&args, 0, 0.0)? as i64;
6961                let mut out = Vec::new();
6962                if let Some(m) = self.midis.get(id as usize) {
6963                    for n in &m.notes {
6964                        out.push(Value::Number(n.time as f64));
6965                        out.push(Value::Number(n.midi as f64));
6966                        out.push(Value::Number(n.dur as f64));
6967                    }
6968                }
6969                return Ok(Value::List(out));
6970            },
6971
6972            // music_fft(track_id, nbands) -> spectrum at the current playback position
6973            #[cfg(not(target_arch = "wasm32"))]
6974            "music_fft" | "音乐频谱" | "音楽スペクトル" | "음악스펙트럼" | "สเปกตรัมเพลง" =>
6975            {
6976                let id = self.arg_num(&args, 0, 0.0)? as i64;
6977                let nbands = self.arg_num(&args, 1, 16.0)? as usize;
6978                let pos = self.music.as_ref().map(|m| m.position()).unwrap_or(0.0);
6979                if let Some(t) = self.tracks.get(id as usize) {
6980                    let idx = (pos * t.rate as f32) as usize;
6981                    let end = (idx + 2048).min(t.mono.len());
6982                    if end > idx + 64 {
6983                        self.fft.borrow_mut().push_samples(&t.mono[idx..end]);
6984                    }
6985                }
6986                let bands = self.fft.borrow().freq_bands(nbands);
6987                return Ok(Value::List(
6988                    bands.into_iter().map(|x| Value::Number(x as f64)).collect(),
6989                ));
6990            },
6991
6992            // ── spatial (2D/3D/4D) one-shot SFX ──
6993            #[cfg(not(target_arch = "wasm32"))]
6994            "audio_sfx" | "音效" | "空間効果音" | "공간효과음" | "เสียงเอฟเฟกต์" =>
6995            {
6996                let x = self.arg_num(&args, 0, 0.0)? as f32;
6997                let y = self.arg_num(&args, 1, 0.0)? as f32;
6998                let z = self.arg_num(&args, 2, 0.0)? as f32;
6999                let w = self.arg_num(&args, 3, 1.0)? as f32;
7000                let freq = self.arg_num(&args, 4, 440.0)? as f32;
7001                let amp = self.arg_num(&args, 5, 0.3)? as f32;
7002                let dur = self.arg_num(&args, 6, 0.15)? as f32;
7003                let wave = Wave::from_name(&self.arg_str(&args, 7, "sine"));
7004                if let Some(a) = &self.audio {
7005                    a.sfx(x, y, z, w, freq, amp, dur, wave);
7006                }
7007                return Ok(Value::Unit);
7008            },
7009            // ── sample load / positional play / loop / stop ──
7010            #[cfg(not(target_arch = "wasm32"))]
7011            "audio_sample_load" | "载入采样" | "サンプル読込" | "샘플로드" | "โหลดตัวอย่างเสียง" =>
7012            {
7013                let path = self.arg_str(&args, 0, "");
7014                let resolved = if std::path::Path::new(&path).exists() {
7015                    path.clone()
7016                } else if let Some(d) = &self.source_dir {
7017                    d.join(&path).to_string_lossy().into_owned()
7018                } else {
7019                    path.clone()
7020                };
7021                match ling_music::load(&resolved) {
7022                    Ok(t) => {
7023                        if let Some(a) = &self.audio {
7024                            return Ok(Value::Number(a.add_sample(t.mono, t.rate) as f64));
7025                        }
7026                        return Ok(Value::Number(-1.0));
7027                    },
7028                    Err(e) => {
7029                        eprintln!("audio_sample_load failed ({path}): {e}");
7030                        return Ok(Value::Number(-1.0));
7031                    },
7032                }
7033            },
7034            #[cfg(not(target_arch = "wasm32"))]
7035            "audio_sample_play" | "播放采样" | "サンプル再生" | "샘플재생" | "เล่นตัวอย่างเสียง" =>
7036            {
7037                let id = self.arg_num(&args, 0, 0.0)? as usize;
7038                let x = self.arg_num(&args, 1, 0.0)? as f32;
7039                let y = self.arg_num(&args, 2, 0.0)? as f32;
7040                let z = self.arg_num(&args, 3, 0.0)? as f32;
7041                let w = self.arg_num(&args, 4, 1.0)? as f32;
7042                let vol = self.arg_num(&args, 5, 1.0)? as f32;
7043                let looping = self.arg_num(&args, 6, 0.0)? > 0.5;
7044                let v = self
7045                    .audio
7046                    .as_ref()
7047                    .map(|a| a.play_sample(id, x, y, z, w, vol, looping))
7048                    .unwrap_or(0);
7049                return Ok(Value::Number(v as f64));
7050            },
7051            #[cfg(not(target_arch = "wasm32"))]
7052            "audio_sample_stop" | "停止采样" | "サンプル停止" | "샘플정지" | "หยุดตัวอย่างเสียง" =>
7053            {
7054                let v = self.arg_num(&args, 0, 0.0)? as u32;
7055                if let Some(a) = &self.audio {
7056                    a.stop_sample(v);
7057                }
7058                return Ok(Value::Unit);
7059            },
7060            // ── master FX: delay / reverb / low-pass (underwater) ──
7061            #[cfg(not(target_arch = "wasm32"))]
7062            "audio_fx_delay" | "回声" | "ディレイ効果" | "딜레이" | "เสียงสะท้อน" =>
7063            {
7064                let time = self.arg_num(&args, 0, 0.3)? as f32;
7065                let fb = self.arg_num(&args, 1, 0.3)? as f32;
7066                let mix = self.arg_num(&args, 2, 0.3)? as f32;
7067                if let Some(a) = &self.audio {
7068                    a.fx_delay(time, fb, mix);
7069                }
7070                return Ok(Value::Unit);
7071            },
7072            #[cfg(not(target_arch = "wasm32"))]
7073            "audio_fx_reverb" | "混响" | "リバーブ" | "리버브" | "เสียงก้อง" =>
7074            {
7075                let mix = self.arg_num(&args, 0, 0.3)? as f32;
7076                if let Some(a) = &self.audio {
7077                    a.fx_reverb(mix);
7078                }
7079                return Ok(Value::Unit);
7080            },
7081            #[cfg(not(target_arch = "wasm32"))]
7082            "audio_fx_lowpass" | "低通滤波" | "ローパス" | "저역통과" | "กรองความถี่ต่ำ" =>
7083            {
7084                let cutoff = self.arg_num(&args, 0, 1.0)? as f32;
7085                if let Some(a) = &self.audio {
7086                    a.fx_lowpass(cutoff);
7087                }
7088                return Ok(Value::Unit);
7089            },
7090
7091            // ══════════════════════════════════════════════════════════════════
7092            // PHYSICS BUILTINS  (crates/ling-physics) — soft bodies, rigid+angular,
7093            // and a fast 2-D water/oil liquid sim mappable onto 3-D surfaces.
7094            // ══════════════════════════════════════════════════════════════════
7095
7096            // ── soft bodies (deformable bouncy balls) ──
7097            #[cfg(not(target_arch = "wasm32"))]
7098            "soft_ball" | "软球" | "ソフトボール" | "소프트볼" | "ลูกบอลนุ่ม" =>
7099            {
7100                let x = self.arg_num(&args, 0, 0.)? as f32;
7101                let y = self.arg_num(&args, 1, 0.)? as f32;
7102                let z = self.arg_num(&args, 2, 0.)? as f32;
7103                let r = self.arg_num(&args, 3, 1.0)? as f32;
7104                let b = ling_physics::soft::SoftBody::sphere(
7105                    ling_physics::Vec3::new(x, y, z),
7106                    r,
7107                    8,
7108                    12,
7109                    1.0,
7110                );
7111                let id = self.soft_bodies.len();
7112                self.soft_bodies.push(b);
7113                return Ok(Value::Number(id as f64));
7114            },
7115            #[cfg(not(target_arch = "wasm32"))]
7116            "soft_step" | "软体步进" | "ソフト更新" | "소프트스텝" | "ก้าวนุ่ม" =>
7117            {
7118                let id = self.arg_num(&args, 0, 0.)? as usize;
7119                let dt = self.arg_num(&args, 1, 0.016)? as f32;
7120                let gy = self.arg_num(&args, 2, 15.0)? as f32;
7121                if let Some(b) = self.soft_bodies.get_mut(id) {
7122                    b.integrate(dt, ling_physics::Vec3::new(0.0, gy, 0.0), 4);
7123                }
7124                return Ok(Value::Unit);
7125            },
7126            #[cfg(not(target_arch = "wasm32"))]
7127            "soft_bounce" | "软体落地" | "ソフト着地" | "소프트바운스" | "เด้งนุ่ม" =>
7128            {
7129                let id = self.arg_num(&args, 0, 0.)? as usize;
7130                let fy = self.arg_num(&args, 1, 0.)? as f32;
7131                let rest = self.arg_num(&args, 2, 0.5)? as f32;
7132                if let Some(b) = self.soft_bodies.get_mut(id) {
7133                    b.floor_collision(fy, rest);
7134                }
7135                return Ok(Value::Unit);
7136            },
7137            #[cfg(not(target_arch = "wasm32"))]
7138            "soft_contain" | "软体边界" | "ソフト箱" | "소프트경계" | "กล่องนุ่ม" =>
7139            {
7140                let id = self.arg_num(&args, 0, 0.)? as usize;
7141                let nx = self.arg_num(&args, 1, -5.)? as f32;
7142                let ny = self.arg_num(&args, 2, -5.)? as f32;
7143                let nz = self.arg_num(&args, 3, -5.)? as f32;
7144                let mx = self.arg_num(&args, 4, 5.)? as f32;
7145                let my = self.arg_num(&args, 5, 5.)? as f32;
7146                let mz = self.arg_num(&args, 6, 5.)? as f32;
7147                let rest = self.arg_num(&args, 7, 0.6)? as f32;
7148                if let Some(b) = self.soft_bodies.get_mut(id) {
7149                    b.contain(
7150                        ling_physics::Vec3::new(nx, ny, nz),
7151                        ling_physics::Vec3::new(mx, my, mz),
7152                        rest,
7153                    );
7154                }
7155                return Ok(Value::Unit);
7156            },
7157            #[cfg(not(target_arch = "wasm32"))]
7158            "soft_kick" | "软体踢" | "ソフト衝撃" | "소프트킥" | "เตะนุ่ม" =>
7159            {
7160                let id = self.arg_num(&args, 0, 0.)? as usize;
7161                let dx = self.arg_num(&args, 1, 0.)? as f32;
7162                let dy = self.arg_num(&args, 2, 0.)? as f32;
7163                let dz = self.arg_num(&args, 3, 0.)? as f32;
7164                let s = self.arg_num(&args, 4, 0.1)? as f32;
7165                if let Some(b) = self.soft_bodies.get_mut(id) {
7166                    b.kick(ling_physics::Vec3::new(dx, dy, dz), s);
7167                }
7168                return Ok(Value::Unit);
7169            },
7170            // soft_spin(id, ax, ay, az, rate) — add angular velocity about the axis
7171            // through the centroid (rate = rad/step; ≈ surface_speed / radius to roll)
7172            #[cfg(not(target_arch = "wasm32"))]
7173            "soft_spin" | "软体自旋" | "ソフト回転" | "소프트회전" | "หมุนนุ่ม" =>
7174            {
7175                let id = self.arg_num(&args, 0, 0.)? as usize;
7176                let ax = self.arg_num(&args, 1, 0.)? as f32;
7177                let ay = self.arg_num(&args, 2, 0.)? as f32;
7178                let az = self.arg_num(&args, 3, 0.)? as f32;
7179                let rate = self.arg_num(&args, 4, 0.1)? as f32;
7180                if let Some(b) = self.soft_bodies.get_mut(id) {
7181                    b.spin(ling_physics::Vec3::new(ax, ay, az), rate);
7182                }
7183                return Ok(Value::Unit);
7184            },
7185            #[cfg(not(target_arch = "wasm32"))]
7186            "soft_deform" | "形变量" | "変形量" | "변형량" | "ความบิดเบี้ยว" =>
7187            {
7188                let id = self.arg_num(&args, 0, 0.)? as usize;
7189                let d = self
7190                    .soft_bodies
7191                    .get(id)
7192                    .map(|b| b.deformation())
7193                    .unwrap_or(0.0);
7194                return Ok(Value::Number(d as f64));
7195            },
7196            // soft_angular_speed(id) -> magnitude of the body's angular velocity
7197            // (how fast it is tumbling/rolling), derived from its node velocities.
7198            #[cfg(not(target_arch = "wasm32"))]
7199            "soft_angular_speed"
7200            | "软体角速"
7201            | "ソフト角速度"
7202            | "소프트각속도"
7203            | "ความเร็วเชิงมุมนุ่ม" => {
7204                let id = self.arg_num(&args, 0, 0.)? as usize;
7205                let w = self
7206                    .soft_bodies
7207                    .get(id)
7208                    .map(|b| b.angular_speed())
7209                    .unwrap_or(0.0);
7210                return Ok(Value::Number(w as f64));
7211            },
7212            #[cfg(not(target_arch = "wasm32"))]
7213            "soft_centroid" | "软体质心" | "ソフト重心" | "소프트중심" | "จุดศูนย์กลางนุ่ม" =>
7214            {
7215                let id = self.arg_num(&args, 0, 0.)? as usize;
7216                let c = self
7217                    .soft_bodies
7218                    .get(id)
7219                    .map(|b| b.centroid())
7220                    .unwrap_or(ling_physics::Vec3::ZERO);
7221                return Ok(Value::List(vec![
7222                    Value::Number(c.x as f64),
7223                    Value::Number(c.y as f64),
7224                    Value::Number(c.z as f64),
7225                ]));
7226            },
7227            // soft_nodes(id) -> flat [x,y,z, x,y,z, …] for rendering the deformed mesh
7228            #[cfg(not(target_arch = "wasm32"))]
7229            "soft_nodes" | "软体节点" | "ソフト節点" | "소프트노드" | "จุดนุ่ม" =>
7230            {
7231                let id = self.arg_num(&args, 0, 0.)? as usize;
7232                let mut out = Vec::new();
7233                if let Some(b) = self.soft_bodies.get(id) {
7234                    for n in &b.nodes {
7235                        out.push(Value::Number(n.pos.x as f64));
7236                        out.push(Value::Number(n.pos.y as f64));
7237                        out.push(Value::Number(n.pos.z as f64));
7238                    }
7239                }
7240                return Ok(Value::List(out));
7241            },
7242
7243            // ── rigid bodies with angular dynamics ──
7244            #[cfg(not(target_arch = "wasm32"))]
7245            "rb_add" | "刚体添加" | "剛体追加" | "강체추가" | "เพิ่มวัตถุแข็ง" =>
7246            {
7247                let x = self.arg_num(&args, 0, 0.)? as f32;
7248                let y = self.arg_num(&args, 1, 0.)? as f32;
7249                let z = self.arg_num(&args, 2, 0.)? as f32;
7250                let mass = self.arg_num(&args, 3, 1.0)? as f32;
7251                let mut b =
7252                    ling_physics::rigid::RigidBody::new(ling_physics::Vec3::new(x, y, z), mass);
7253                b.restitution = 0.6;
7254                return Ok(Value::Number(self.rigid_world.add(b) as f64));
7255            },
7256            #[cfg(not(target_arch = "wasm32"))]
7257            "rb_torque" | "扭矩" | "トルク" | "토크" | "แรงบิด" => {
7258                let i = self.arg_num(&args, 0, 0.)? as usize;
7259                let tx = self.arg_num(&args, 1, 0.)? as f32;
7260                let ty = self.arg_num(&args, 2, 0.)? as f32;
7261                let tz = self.arg_num(&args, 3, 0.)? as f32;
7262                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
7263                    b.apply_torque(ling_physics::Vec3::new(tx, ty, tz));
7264                }
7265                return Ok(Value::Unit);
7266            },
7267            #[cfg(not(target_arch = "wasm32"))]
7268            "rb_spin" | "自旋" | "スピン" | "스핀" | "หมุน" => {
7269                let i = self.arg_num(&args, 0, 0.)? as usize;
7270                let wx = self.arg_num(&args, 1, 0.)? as f32;
7271                let wy = self.arg_num(&args, 2, 0.)? as f32;
7272                let wz = self.arg_num(&args, 3, 0.)? as f32;
7273                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
7274                    b.apply_spin(ling_physics::Vec3::new(wx, wy, wz));
7275                }
7276                return Ok(Value::Unit);
7277            },
7278            #[cfg(not(target_arch = "wasm32"))]
7279            "rb_impulse" | "刚体冲量" | "剛体インパルス" | "강체충격" | "แรงดลแข็ง" =>
7280            {
7281                let i = self.arg_num(&args, 0, 0.)? as usize;
7282                let ix = self.arg_num(&args, 1, 0.)? as f32;
7283                let iy = self.arg_num(&args, 2, 0.)? as f32;
7284                let iz = self.arg_num(&args, 3, 0.)? as f32;
7285                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
7286                    b.apply_impulse(ling_physics::Vec3::new(ix, iy, iz));
7287                }
7288                return Ok(Value::Unit);
7289            },
7290            #[cfg(not(target_arch = "wasm32"))]
7291            "rb_floor" | "刚体落地" | "剛体着地" | "강체바닥" | "พื้นแข็ง" =>
7292            {
7293                let i = self.arg_num(&args, 0, 0.)? as usize;
7294                let fy = self.arg_num(&args, 1, 0.)? as f32;
7295                let rest = self.arg_num(&args, 2, 0.6)? as f32;
7296                let fric = self.arg_num(&args, 3, 0.6)? as f32;
7297                if let Some(b) = self.rigid_world.bodies.get_mut(i) {
7298                    b.bounce_floor(fy, rest, fric);
7299                }
7300                return Ok(Value::Unit);
7301            },
7302            #[cfg(not(target_arch = "wasm32"))]
7303            "rb_gravity" | "刚体重力" | "剛体重力" | "강체중력" | "แรงโน้มถ่วงแข็ง" =>
7304            {
7305                let gx = self.arg_num(&args, 0, 0.)? as f32;
7306                let gy = self.arg_num(&args, 1, 9.81)? as f32;
7307                let gz = self.arg_num(&args, 2, 0.)? as f32;
7308                self.rigid_world.gravity = ling_physics::Vec3::new(gx, gy, gz);
7309                return Ok(Value::Unit);
7310            },
7311            #[cfg(not(target_arch = "wasm32"))]
7312            "rb_step" | "刚体步进" | "剛体更新" | "강체스텝" | "ก้าวแข็ง" =>
7313            {
7314                let dt = self.arg_num(&args, 0, 0.016)? as f32;
7315                self.rigid_world.step(dt);
7316                return Ok(Value::Unit);
7317            },
7318            #[cfg(not(target_arch = "wasm32"))]
7319            "rb_pos" | "刚体位置" | "剛体位置" | "강체위치" | "ตำแหน่งแข็ง" =>
7320            {
7321                let i = self.arg_num(&args, 0, 0.)? as usize;
7322                let p = self
7323                    .rigid_world
7324                    .bodies
7325                    .get(i)
7326                    .map(|b| b.pos)
7327                    .unwrap_or(ling_physics::Vec3::ZERO);
7328                return Ok(Value::List(vec![
7329                    Value::Number(p.x as f64),
7330                    Value::Number(p.y as f64),
7331                    Value::Number(p.z as f64),
7332                ]));
7333            },
7334            #[cfg(not(target_arch = "wasm32"))]
7335            "rb_rot" | "刚体旋转" | "剛体回転" | "강체회전" | "การหมุนแข็ง" =>
7336            {
7337                let i = self.arg_num(&args, 0, 0.)? as usize;
7338                let q = self
7339                    .rigid_world
7340                    .bodies
7341                    .get(i)
7342                    .map(|b| b.orientation)
7343                    .unwrap_or(ling_physics::Quat::IDENTITY);
7344                return Ok(Value::List(vec![
7345                    Value::Number(q.x as f64),
7346                    Value::Number(q.y as f64),
7347                    Value::Number(q.z as f64),
7348                    Value::Number(q.w as f64),
7349                ]));
7350            },
7351
7352            // ── native-res mesh (.lmesh): load once, draw fast (unlit, per-tri colour) ──
7353            #[cfg(not(target_arch = "wasm32"))]
7354            "mesh_load" | "โหลดเมช" | "载入网格" | "メッシュ読込" | "메시로드" =>
7355            {
7356                let path = self.arg_str(&args, 0, "");
7357                let resolved = if std::path::Path::new(&path).exists() {
7358                    path.clone()
7359                } else if let Some(d) = &self.source_dir {
7360                    d.join(&path).to_string_lossy().into_owned()
7361                } else {
7362                    path.clone()
7363                };
7364                let bytes = match std::fs::read(&resolved) {
7365                    Ok(b) => b,
7366                    Err(e) => {
7367                        eprintln!("mesh_load failed ({path}): {e}");
7368                        return Ok(Value::Number(-1.0));
7369                    },
7370                };
7371                if bytes.len() < 16 || &bytes[0..4] != b"LMSH" {
7372                    eprintln!("mesh_load: bad header ({path})");
7373                    return Ok(Value::Number(-1.0));
7374                }
7375                let rd4 =
7376                    |o: usize| -> [u8; 4] { [bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]] };
7377                let height = f32::from_le_bytes(rd4(8));
7378                let ntri = u32::from_le_bytes(rd4(12)) as usize;
7379                let need = 16usize.saturating_add(ntri.saturating_mul(9 * 4 + 3));
7380                if bytes.len() < need {
7381                    eprintln!("mesh_load: truncated ({path})");
7382                    return Ok(Value::Number(-1.0));
7383                }
7384                let mut pos = Vec::with_capacity(ntri * 3);
7385                let mut col = Vec::with_capacity(ntri);
7386                let mut off = 16usize;
7387                for _ in 0..ntri {
7388                    for _k in 0..3 {
7389                        let x = f32::from_le_bytes(rd4(off));
7390                        let y = f32::from_le_bytes(rd4(off + 4));
7391                        let z = f32::from_le_bytes(rd4(off + 8));
7392                        off += 12;
7393                        pos.push([x, y, z]);
7394                    }
7395                    col.push([bytes[off], bytes[off + 1], bytes[off + 2]]);
7396                    off += 3;
7397                }
7398                eprintln!("mesh_load: {} ({} tris, h={:.2})", path, ntri, height);
7399                let id = self.meshes.len();
7400                self.meshes
7401                    .push(crate::gfx::shapes::ColorMesh { pos, col, height });
7402                return Ok(Value::Number(id as f64));
7403            },
7404            #[cfg(not(target_arch = "wasm32"))]
7405            "mesh_draw" | "วาดเมชสี" | "绘制网格" | "メッシュ描画" | "메시그리기" =>
7406            {
7407                // ('วาดเมช' is taken by draw_mesh — use a distinct Thai alias)
7408                let id = self.arg_num(&args, 0, 0.)? as usize;
7409                let cx = self.arg_num(&args, 1, 0.)? as f32;
7410                let cy = self.arg_num(&args, 2, 0.)? as f32;
7411                let cz = self.arg_num(&args, 3, 0.)? as f32;
7412                let sc = self.arg_num(&args, 4, 1.)? as f32;
7413                let yaw = self.arg_num(&args, 5, 0.)? as f32;
7414                let sway = self.arg_num(&args, 6, 0.)? as f32;
7415                let arm = self.arg_num(&args, 7, 0.)? as f32;
7416                let lean = self.arg_num(&args, 8, 0.)? as f32;
7417                let leg = self.arg_num(&args, 9, 0.)? as f32;
7418                let tuck = self.arg_num(&args, 10, 0.)? as f32;
7419                if id < self.meshes.len() {
7420                    let m = &self.meshes[id];
7421                    let mut gfx = self.gfx.borrow_mut();
7422                    gfx.draw_color_mesh(m, cx, cy, cz, sc, yaw, sway, arm, lean, leg, tuck);
7423                }
7424                return Ok(Value::Unit);
7425            },
7426
7427            // ── liquid sim (water + oil, immiscible) ──
7428            #[cfg(not(target_arch = "wasm32"))]
7429            "liquid_new" | "新建液体" | "液体新規" | "액체생성" | "สร้างของเหลว" =>
7430            {
7431                let w = self.arg_num(&args, 0, 64.)? as usize;
7432                let h = self.arg_num(&args, 1, 64.)? as usize;
7433                let id = self.liquids.len();
7434                self.liquids
7435                    .push(ling_physics::liquid::LiquidGrid::new(w, h));
7436                return Ok(Value::Number(id as f64));
7437            },
7438            #[cfg(not(target_arch = "wasm32"))]
7439            "liquid_set_colors" | "液体颜色" | "液体配色" | "액체색상" | "สีของเหลว" =>
7440            {
7441                let id = self.arg_num(&args, 0, 0.)? as usize;
7442                let wr = self.arg_num(&args, 1, 40.)? as f32;
7443                let wg = self.arg_num(&args, 2, 110.)? as f32;
7444                let wb = self.arg_num(&args, 3, 235.)? as f32;
7445                let or_ = self.arg_num(&args, 4, 240.)? as f32;
7446                let og = self.arg_num(&args, 5, 175.)? as f32;
7447                let ob = self.arg_num(&args, 6, 45.)? as f32;
7448                if let Some(g) = self.liquids.get_mut(id) {
7449                    g.set_colors(wr, wg, wb, or_, og, ob);
7450                }
7451                return Ok(Value::Unit);
7452            },
7453            #[cfg(not(target_arch = "wasm32"))]
7454            "liquid_splat" | "液体注入" | "液体追加" | "액체분사" | "หยดของเหลว" =>
7455            {
7456                let id = self.arg_num(&args, 0, 0.)? as usize;
7457                let x = self.arg_num(&args, 1, 0.)? as f32;
7458                let y = self.arg_num(&args, 2, 0.)? as f32;
7459                let kind = self.arg_num(&args, 3, 0.)? as i32;
7460                let amt = self.arg_num(&args, 4, 1.0)? as f32;
7461                let rad = self.arg_num(&args, 5, 4.0)? as f32;
7462                if let Some(g) = self.liquids.get_mut(id) {
7463                    g.splat(x, y, kind, amt, rad);
7464                }
7465                return Ok(Value::Unit);
7466            },
7467            #[cfg(not(target_arch = "wasm32"))]
7468            "liquid_gravity" | "液体重力" | "液体重力ベクトル" | "액체중력" | "แรงโน้มถ่วงเหลว" =>
7469            {
7470                let id = self.arg_num(&args, 0, 0.)? as usize;
7471                let gx = self.arg_num(&args, 1, 0.)? as f32;
7472                let gy = self.arg_num(&args, 2, 60.)? as f32;
7473                if let Some(g) = self.liquids.get_mut(id) {
7474                    g.set_gravity(gx, gy);
7475                }
7476                return Ok(Value::Unit);
7477            },
7478            #[cfg(not(target_arch = "wasm32"))]
7479            "liquid_step" | "液体步进" | "液体更新" | "액체스텝" | "ก้าวของเหลว" =>
7480            {
7481                let id = self.arg_num(&args, 0, 0.)? as usize;
7482                let dt = self.arg_num(&args, 1, 0.016)? as f32;
7483                if let Some(g) = self.liquids.get_mut(id) {
7484                    g.step(dt);
7485                }
7486                return Ok(Value::Unit);
7487            },
7488            // liquid_rainbow(id, on) — colour the fluid as a flowing ROYGBIV marble
7489            #[cfg(not(target_arch = "wasm32"))]
7490            "liquid_rainbow" | "液体彩虹" | "液体虹" | "액체무지개" | "ของเหลวสายรุ้ง" =>
7491            {
7492                let id = self.arg_num(&args, 0, 0.)? as usize;
7493                let on = self.arg_num(&args, 1, 1.0)? > 0.5;
7494                if let Some(g) = self.liquids.get_mut(id) {
7495                    g.rainbow = on;
7496                }
7497                return Ok(Value::Unit);
7498            },
7499            // liquid_mix(id) -> 0 (oil/water separated) .. 1 (fully intermixed)
7500            #[cfg(not(target_arch = "wasm32"))]
7501            "liquid_mix" | "液体混合" | "液体混合度" | "액체혼합" | "การผสมของเหลว" =>
7502            {
7503                let id = self.arg_num(&args, 0, 0.)? as usize;
7504                let m = self.liquids.get(id).map(|g| g.mix_amount()).unwrap_or(0.0);
7505                return Ok(Value::Number(m as f64));
7506            },
7507            // liquid_draw(id, sx, sy, scale) — fast flat 2-D blit of the colour field
7508            #[cfg(not(target_arch = "wasm32"))]
7509            "liquid_draw" | "绘制液体" | "液体描画" | "액체그리기" | "วาดของเหลว" =>
7510            {
7511                let id = self.arg_num(&args, 0, 0.)? as usize;
7512                let sx = self.arg_num(&args, 1, 0.)? as i32;
7513                let sy = self.arg_num(&args, 2, 0.)? as i32;
7514                let scale = (self.arg_num(&args, 3, 4.)? as i32).max(1);
7515                if id < self.liquids.len() {
7516                    let (gw, gh) = {
7517                        let g = &self.liquids[id];
7518                        (g.w, g.h)
7519                    };
7520                    let mut gfx = self.gfx.borrow_mut();
7521                    let (w, h) = (gfx.width as i32, gfx.height as i32);
7522                    let g = &self.liquids[id];
7523                    for cy in 0..gh {
7524                        for cx in 0..gw {
7525                            let col = g.sample_rgb(cx, cy);
7526                            let bx = sx + cx as i32 * scale;
7527                            let by = sy + cy as i32 * scale;
7528                            for dy in 0..scale {
7529                                for dx in 0..scale {
7530                                    let px = bx + dx;
7531                                    let py = by + dy;
7532                                    if px >= 0 && py >= 0 && px < w && py < h {
7533                                        gfx.buffer[(py * w + px) as usize] = col;
7534                                    }
7535                                }
7536                            }
7537                        }
7538                    }
7539                }
7540                return Ok(Value::Unit);
7541            },
7542            // liquid_draw_surface(id, kind, cx,cy,cz, radius, height)
7543            //   kind: 0 plane · 1 sphere · 2 cylinder · 3 cone · 4 dome
7544            #[cfg(not(target_arch = "wasm32"))]
7545            "liquid_draw_surface" | "液体贴面" | "液体曲面" | "액체곡면" | "ของเหลวบนพื้นผิว" =>
7546            {
7547                let id = self.arg_num(&args, 0, 0.)? as usize;
7548                let kind = self.arg_num(&args, 1, 1.)? as i32;
7549                let cx = self.arg_num(&args, 2, 0.)? as f32;
7550                let cy = self.arg_num(&args, 3, 0.)? as f32;
7551                let cz = self.arg_num(&args, 4, 0.)? as f32;
7552                let radius = self.arg_num(&args, 5, 2.0)? as f32;
7553                let height = self.arg_num(&args, 6, 3.0)? as f32;
7554                if id < self.liquids.len() {
7555                    let (gw, gh) = {
7556                        let g = &self.liquids[id];
7557                        (g.w, g.h)
7558                    };
7559                    let mut gfx = self.gfx.borrow_mut();
7560                    let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
7561                    let cam = gfx.camera.clone();
7562                    let near = -cam.zdist + 0.05;
7563                    let g = &self.liquids[id];
7564                    let tau = std::f32::consts::TAU;
7565                    let pi = std::f32::consts::PI;
7566                    // surface point for a (u,v) in [0,1] on the chosen primitive
7567                    let sp = |u: f32, v: f32| -> [f32; 3] {
7568                        if kind == 0 {
7569                            [
7570                                cx + (u - 0.5) * 2.0 * radius,
7571                                cy,
7572                                cz + (v - 0.5) * 2.0 * radius,
7573                            ]
7574                        } else if kind == 2 {
7575                            let th = u * tau;
7576                            [
7577                                cx + th.cos() * radius,
7578                                cy + (v - 0.5) * height,
7579                                cz + th.sin() * radius,
7580                            ]
7581                        } else if kind == 3 {
7582                            let th = u * tau;
7583                            let rr = radius * (1.0 - v);
7584                            [
7585                                cx + th.cos() * rr,
7586                                cy + (v - 0.5) * height,
7587                                cz + th.sin() * rr,
7588                            ]
7589                        } else if kind == 4 {
7590                            let th = u * tau;
7591                            let ph = v * pi * 0.5;
7592                            [
7593                                cx + ph.sin() * th.cos() * radius,
7594                                cy - ph.cos() * radius,
7595                                cz + ph.sin() * th.sin() * radius,
7596                            ]
7597                        } else {
7598                            let th = u * tau;
7599                            let ph = v * pi;
7600                            [
7601                                cx + ph.sin() * th.cos() * radius,
7602                                cy + ph.cos() * radius,
7603                                cz + ph.sin() * th.sin() * radius,
7604                            ]
7605                        }
7606                    };
7607                    let nrm = |u: f32, v: f32| -> [f32; 3] {
7608                        if kind == 0 {
7609                            [0.0, -1.0, 0.0]
7610                        } else if kind == 2 {
7611                            let th = u * tau;
7612                            [th.cos(), 0.0, th.sin()]
7613                        } else if kind == 3 {
7614                            let th = u * tau;
7615                            let s = (radius / height.max(0.01)).atan();
7616                            [th.cos() * s.cos(), s.sin(), th.sin() * s.cos()]
7617                        } else if kind == 4 {
7618                            let th = u * tau;
7619                            let ph = v * pi * 0.5;
7620                            [ph.sin() * th.cos(), -ph.cos(), ph.sin() * th.sin()]
7621                        } else {
7622                            let th = u * tau;
7623                            let ph = v * pi;
7624                            [ph.sin() * th.cos(), ph.cos(), ph.sin() * th.sin()]
7625                        }
7626                    };
7627                    let gwf = gw as f32;
7628                    let ghf = gh as f32;
7629                    let mut cyc = 0usize;
7630                    while cyc < gh {
7631                        let mut cxc = 0usize;
7632                        while cxc < gw {
7633                            // cull by the cell centre's outward normal
7634                            let uc = (cxc as f32 + 0.5) / gwf;
7635                            let vc = (cyc as f32 + 0.5) / ghf;
7636                            let c = sp(uc, vc);
7637                            let n = nrm(uc, vc);
7638                            let dc = cam.depth(c[0], c[1], c[2]);
7639                            if dc > near {
7640                                let cull = kind != 0
7641                                    && cam.depth(
7642                                        c[0] + n[0] * 0.06,
7643                                        c[1] + n[1] * 0.06,
7644                                        c[2] + n[2] * 0.06,
7645                                    ) > dc;
7646                                if !cull {
7647                                    // project the 4 cell corners → a filled AA vector quad
7648                                    let u0 = cxc as f32 / gwf;
7649                                    let u1 = (cxc + 1) as f32 / gwf;
7650                                    let v0 = cyc as f32 / ghf;
7651                                    let v1 = (cyc + 1) as f32 / ghf;
7652                                    let q = [sp(u0, v0), sp(u1, v0), sp(u1, v1), sp(u0, v1)];
7653                                    let mut poly: Vec<[f32; 2]> = Vec::with_capacity(5);
7654                                    let mut ok = true;
7655                                    for p in &q {
7656                                        if cam.depth(p[0], p[1], p[2]) <= near {
7657                                            ok = false;
7658                                            break;
7659                                        }
7660                                        let (sx, sy, _) = cam.project(p[0], p[1], p[2]);
7661                                        poly.push([sx, sy]);
7662                                    }
7663                                    if ok {
7664                                        let p0 = poly[0];
7665                                        poly.push(p0);
7666                                        let col = g.sample_rgb(cxc, cyc);
7667                                        crate::gfx::raster::fill_contours_aa(
7668                                            &mut gfx.buffer,
7669                                            w,
7670                                            h,
7671                                            col,
7672                                            add,
7673                                            std::slice::from_ref(&poly),
7674                                        );
7675                                    }
7676                                }
7677                            }
7678                            cxc += 1;
7679                        }
7680                        cyc += 1;
7681                    }
7682                }
7683                return Ok(Value::Unit);
7684            },
7685            // sparkle(x, y, w, h, count [, t]) — scatter twinkling vector star-sparkles
7686            // in a rect (snowglobe effect) in the current colour + blend mode.
7687            #[cfg(not(target_arch = "wasm32"))]
7688            "sparkle" | "闪光" | "きらめき" | "반짝임" | "ประกาย" => {
7689                let x = self.arg_num(&args, 0, 0.)? as f32;
7690                let y = self.arg_num(&args, 1, 0.)? as f32;
7691                let ww = self.arg_num(&args, 2, 200.)? as f32;
7692                let hh = self.arg_num(&args, 3, 200.)? as f32;
7693                let count = self.arg_num(&args, 4, 40.)? as i32;
7694                let t = self.arg_num(&args, 5, 0.)? as f32;
7695                let mut gfx = self.gfx.borrow_mut();
7696                let (w, h, add, color) = (gfx.width, gfx.height, gfx.blend == 1, gfx.color);
7697                let (cr, cg, cb) = (
7698                    (color >> 16 & 0xFF) as f32,
7699                    (color >> 8 & 0xFF) as f32,
7700                    (color & 0xFF) as f32,
7701                );
7702                let mut n = 0i32;
7703                while n < count {
7704                    let hsh = (n as u32).wrapping_mul(2654435761).wrapping_add(0x9E3779B9);
7705                    let u = ((hsh >> 8) & 1023) as f32 / 1023.0;
7706                    let v = ((hsh >> 18) & 1023) as f32 / 1023.0;
7707                    let phase = (hsh & 255) as f32 / 255.0;
7708                    let tw = (t * 3.0 + phase * 6.2831 + n as f32).sin() * 0.5 + 0.5;
7709                    let sz = 1.5 + tw * 5.0;
7710                    let px = x + u * ww;
7711                    let py = y + v * hh;
7712                    let b = tw * tw; // sharp twinkle
7713                    let col =
7714                        (((cr * b) as u32) << 16) | (((cg * b) as u32) << 8) | ((cb * b) as u32);
7715                    crate::gfx::raster::draw_line_aa(
7716                        &mut gfx.buffer,
7717                        w,
7718                        h,
7719                        col,
7720                        add,
7721                        px - sz,
7722                        py,
7723                        px + sz,
7724                        py,
7725                    );
7726                    crate::gfx::raster::draw_line_aa(
7727                        &mut gfx.buffer,
7728                        w,
7729                        h,
7730                        col,
7731                        add,
7732                        px,
7733                        py - sz,
7734                        px,
7735                        py + sz,
7736                    );
7737                    let d = sz * 0.55;
7738                    crate::gfx::raster::draw_line_aa(
7739                        &mut gfx.buffer,
7740                        w,
7741                        h,
7742                        col,
7743                        add,
7744                        px - d,
7745                        py - d,
7746                        px + d,
7747                        py + d,
7748                    );
7749                    crate::gfx::raster::draw_line_aa(
7750                        &mut gfx.buffer,
7751                        w,
7752                        h,
7753                        col,
7754                        add,
7755                        px - d,
7756                        py + d,
7757                        px + d,
7758                        py - d,
7759                    );
7760                    n += 1;
7761                }
7762                return Ok(Value::Unit);
7763            },
7764
7765            // ══════════════════════════════════════════════════════════════════
7766            // DIALOG BUILTINS  (crates/ling-game/src/dialog.rs) — cinematic,
7767            // typed-out, colour-coded text boxes. Markup: {n}name{/} {p}place{/}
7768            // {i}item{/}, \n newline, || page break.
7769            // ══════════════════════════════════════════════════════════════════
7770            #[cfg(not(target_arch = "wasm32"))]
7771            "dialog_show" | "对话显示" | "会話表示" | "대화표시" | "แสดงบทสนทนา" =>
7772            {
7773                let text = self.arg_str(&args, 0, "");
7774                let cps = self.arg_num(&args, 1, 32.0)? as f32;
7775                self.dialog = Some(ling_game::dialog::Dialog::new(&text, cps));
7776                return Ok(Value::Unit);
7777            },
7778            #[cfg(not(target_arch = "wasm32"))]
7779            "dialog_step" | "对话步进" | "会話更新" | "대화스텝" | "ก้าวบทสนทนา" =>
7780            {
7781                let dt = self.arg_num(&args, 0, 0.016)? as f32;
7782                if let Some(d) = self.dialog.as_mut() {
7783                    d.update(dt);
7784                }
7785                return Ok(Value::Unit);
7786            },
7787            #[cfg(not(target_arch = "wasm32"))]
7788            "dialog_advance" | "对话推进" | "会話送り" | "대화진행" | "เลื่อนบทสนทนา" =>
7789            {
7790                if let Some(d) = self.dialog.as_mut() {
7791                    d.advance();
7792                }
7793                return Ok(Value::Unit);
7794            },
7795            #[cfg(not(target_arch = "wasm32"))]
7796            "dialog_active" | "对话激活" | "会話中" | "대화중" | "บทสนทนาทำงาน" =>
7797            {
7798                let a = self
7799                    .dialog
7800                    .as_ref()
7801                    .map(|d| !d.is_closed())
7802                    .unwrap_or(false);
7803                return Ok(Value::Bool(a));
7804            },
7805            #[cfg(not(target_arch = "wasm32"))]
7806            "dialog_typing" | "对话打字" | "会話タイプ中" | "대화타이핑" | "กำลังพิมพ์บทสนทนา" =>
7807            {
7808                use ling_game::dialog::Dialog;
7809
7810                let a = self
7811                    .dialog
7812                    .as_ref()
7813                    .map(|d: &Dialog| !d.is_closed() && d.is_typing())
7814                    .unwrap_or(false);
7815                return Ok(Value::Bool(a));
7816            },
7817            #[cfg(not(target_arch = "wasm32"))]
7818            "dialog_close" | "对话关闭" | "会話閉じる" | "대화닫기" | "ปิดบทสนทนา" =>
7819            {
7820                self.dialog = None;
7821                return Ok(Value::Unit);
7822            },
7823            // dialog_color(role, r, g, b) — role: 0 text · 1 name · 2 place · 3 item
7824            #[cfg(not(target_arch = "wasm32"))]
7825            "dialog_color" | "对话颜色" | "会話色" | "대화색" | "สีบทสนทนา" =>
7826            {
7827                let role = (self.arg_num(&args, 0, 0.0)? as usize).min(3);
7828                let r = self.arg_num(&args, 1, 255.0)? as u32 & 0xFF;
7829                let g = self.arg_num(&args, 2, 255.0)? as u32 & 0xFF;
7830                let b = self.arg_num(&args, 3, 255.0)? as u32 & 0xFF;
7831                self.dialog_colors[role] = (r << 16) | (g << 8) | b;
7832                return Ok(Value::Unit);
7833            },
7834            // dialog_draw(x, y, w, h [, font_handle]) — draw the box + typed text
7835            #[cfg(not(target_arch = "wasm32"))]
7836            "dialog_draw" | "对话绘制" | "会話描画" | "대화그리기" | "วาดบทสนทนา" =>
7837            {
7838                let x = self.arg_num(&args, 0, 40.0)? as f32;
7839                let y = self.arg_num(&args, 1, 0.0)? as f32;
7840                let ww = self.arg_num(&args, 2, 720.0)? as f32;
7841                let hh = self.arg_num(&args, 3, 150.0)? as f32;
7842                let font = self.arg_num(&args, 4, -1.0)? as i64;
7843                let t = self.start_time.elapsed().as_secs_f32();
7844                self.render_dialog(x, y, ww, hh, font, t);
7845                return Ok(Value::Unit);
7846            },
7847
7848            // text_poll() — fold newly-typed keys into the input buffer, return it
7849            #[cfg(not(target_arch = "wasm32"))]
7850            "text_poll" => {
7851                let keys = {
7852                    let gfx = self.gfx.borrow();
7853                    gfx.window
7854                        .as_ref()
7855                        .map(|w| w.get_keys_pressed(minifb::KeyRepeat::No))
7856                        .unwrap_or_default()
7857                };
7858                for k in keys {
7859                    if k == minifb::Key::Backspace {
7860                        self.text_buffer.pop();
7861                    } else if let Some(c) = key_char(k) {
7862                        self.text_buffer.push(c);
7863                    }
7864                }
7865                return Ok(Value::Str(self.text_buffer.clone()));
7866            },
7867            "text_get" => return Ok(Value::Str(self.text_buffer.clone())),
7868            "text_set" => {
7869                self.text_buffer = self.arg_str(&args, 0, "");
7870                return Ok(Value::Unit);
7871            },
7872            "text_clear" => {
7873                self.text_buffer.clear();
7874                return Ok(Value::Unit);
7875            },
7876            // record_frame() — append the current framebuffer as a PPM, return frame #
7877            #[cfg(not(target_arch = "wasm32"))]
7878            "record_frame" => {
7879                let n = self.record_n;
7880                let (buf, w, h) = {
7881                    let gfx = self.gfx.borrow();
7882                    (gfx.buffer.clone(), gfx.width, gfx.height)
7883                };
7884                let _ = std::fs::create_dir_all("recordings");
7885                let mut out = Vec::with_capacity(w * h * 3 + 32);
7886                out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes());
7887                for px in &buf {
7888                    let p = *px;
7889                    out.push((p >> 16) as u8);
7890                    out.push((p >> 8) as u8);
7891                    out.push(p as u8);
7892                }
7893                let _ = std::fs::write(format!("recordings/frame_{n:05}.ppm"), out);
7894                self.record_n += 1;
7895                return Ok(Value::Number(n as f64));
7896            },
7897            "record_count" => return Ok(Value::Number(self.record_n as f64)),
7898            // ── screenshot(mode) → PNG in ./screenshots/ with timestamp + mode + size ──
7899            #[cfg(not(target_arch = "wasm32"))]
7900            "screenshot" | "บันทึกภาพ" => {
7901                let mode = self.arg_str(&args, 0, "game");
7902                let (buf, w, h) = {
7903                    let gfx = self.gfx.borrow();
7904                    (gfx.buffer.clone(), gfx.width, gfx.height)
7905                };
7906                let _ = std::fs::create_dir_all("screenshots");
7907                let ts = std::time::SystemTime::now()
7908                    .duration_since(std::time::UNIX_EPOCH)
7909                    .map(|d| d.as_secs())
7910                    .unwrap_or(0);
7911                let safe: String = mode
7912                    .chars()
7913                    .map(|c| if c.is_alphanumeric() { c } else { '_' })
7914                    .collect();
7915                let path = format!("screenshots/ss_{ts}_{safe}_{w}x{h}.png");
7916                let mut rgb = Vec::with_capacity(w * h * 3);
7917                for px in &buf {
7918                    let p = *px;
7919                    rgb.push((p >> 16) as u8);
7920                    rgb.push((p >> 8) as u8);
7921                    rgb.push(p as u8);
7922                }
7923                if let Some(img) = image::RgbImage::from_raw(w as u32, h as u32, rgb) {
7924                    let _ = img.save(&path);
7925                }
7926                return Ok(Value::Str(path));
7927            },
7928            // ── microphone → crypto donut ──
7929            // mic_capture() — append the latest mic samples to the record buffer
7930            // (call each frame while recording). Returns the buffer length.
7931            #[cfg(not(target_arch = "wasm32"))]
7932            "mic_capture" => {
7933                if let Some(mic) = self.mic.as_ref() {
7934                    let s = mic.latest_samples();
7935                    self.mic_buffer.extend_from_slice(&s);
7936                    let cap = 96_000usize; // ~2 s @ 48 kHz
7937                    if self.mic_buffer.len() > cap {
7938                        let drop = self.mic_buffer.len() - cap;
7939                        self.mic_buffer.drain(0..drop);
7940                    }
7941                }
7942                return Ok(Value::Number(self.mic_buffer.len() as f64));
7943            },
7944            // mic_seed() — SHA3-256 hex of the recorded audio, usable as a donut seed
7945            #[cfg(not(target_arch = "wasm32"))]
7946            "mic_seed" => {
7947                let mut bytes = Vec::with_capacity(self.mic_buffer.len() * 4);
7948                for f in &self.mic_buffer {
7949                    bytes.extend_from_slice(&f.to_le_bytes());
7950                }
7951                return Ok(Value::Str(hex_encode(&ling_crypto::geo::holo_hash(&bytes))));
7952            },
7953            #[cfg(not(target_arch = "wasm32"))]
7954            "mic_clear" => {
7955                self.mic_buffer.clear();
7956                return Ok(Value::Number(0.0));
7957            },
7958            // flush the 3-D depth queue onto the framebuffer WITHOUT presenting,
7959            // so 2-D UI drawn afterwards overlays the 3-D scene.
7960            #[cfg(not(target_arch = "wasm32"))]
7961            "flush_3d" | "render_3d" => {
7962                let mut gfx = self.gfx.borrow_mut();
7963                if !gfx.depth_queue.is_empty() {
7964                    let w = gfx.width;
7965                    let h = gfx.height;
7966                    let dt = gfx.depth_test;
7967                    let (bm, ba) = (gfx.blend, gfx.alpha);
7968                    let queue = std::mem::take(&mut gfx.depth_queue);
7969                    {
7970                        let g = &mut *gfx;
7971                        let z = if dt { Some(&mut g.depth_buf) } else { None };
7972                        queue.flush(&mut g.buffer, z, w, h);
7973                    }
7974                    gfx.depth_queue.set_state(bm, ba); // keep active blend/alpha across the mid-frame flush
7975                }
7976                return Ok(Value::Unit);
7977            },
7978
7979            // Viscous full-screen distortion (warp/pucker/bloat, edge-wrapped). Call
7980            // after the 3-D flush and before the UI so only the world layer warps.
7981            #[cfg(not(target_arch = "wasm32"))]
7982            "screen_distort" | "บิดจอ" | "屏幕扭曲" | "画面歪み" | "화면왜곡" =>
7983            {
7984                let amount = self.arg_num(&args, 0, 8.0)? as f32;
7985                let t = self.arg_num(&args, 1, 0.0)? as f32;
7986                self.gfx.borrow_mut().distort(amount, t);
7987                return Ok(Value::Unit);
7988            },
7989
7990            "set_rim" | "设置边缘光" | "リム設定" | "림라이트" | "ตั้งขอบเรือง" =>
7991            {
7992                let s = self.arg_num(&args, 0, 0.6)? as f32;
7993                let r = self.arg_num(&args, 1, 115.)? as f32 / 255.0;
7994                let g = self.arg_num(&args, 2, 217.)? as f32 / 255.0;
7995                let b = self.arg_num(&args, 3, 255.)? as f32 / 255.0;
7996                let mut gfx = self.gfx.borrow_mut();
7997                gfx.shade.rim = s;
7998                gfx.shade.rim_color = [r, g, b];
7999                return Ok(Value::Unit);
8000            },
8001
8002            // ══════════════════════════════════════════════════════════════════
8003            // 3-D PRIMITIVES  (src/gfx/shapes.rs)  — "Inkscape for 3-D"
8004            //   shape(cx,cy,cz,  sx,sy,sz,  rx,ry,rz,  mode,  e0,e1,e2)
8005            //     centre (cx,cy,cz), per-axis scale, Euler rotation (radians),
8006            //     mode: 0 filled · 1 wireframe · 2 both,
8007            //     e0..e2: shape-specific (segments / sides / ratio …).
8008            //   Pen colour (set_color) drives fill lighting and wireframe colour.
8009            // ══════════════════════════════════════════════════════════════════
8010            n if crate::gfx::shapes::canon(n).is_some() => {
8011                let kind = crate::gfx::shapes::canon(n).unwrap();
8012                let cx = self.arg_num(&args, 0, 0.)? as f32;
8013                let cy = self.arg_num(&args, 1, 0.)? as f32;
8014                let cz = self.arg_num(&args, 2, 0.)? as f32;
8015                let sx = self.arg_num(&args, 3, 1.)? as f32;
8016                let sy = self.arg_num(&args, 4, 1.)? as f32;
8017                let sz = self.arg_num(&args, 5, 1.)? as f32;
8018                let rx = self.arg_num(&args, 6, 0.)? as f32;
8019                let ry = self.arg_num(&args, 7, 0.)? as f32;
8020                let rz = self.arg_num(&args, 8, 0.)? as f32;
8021                let mode = self.arg_num(&args, 9, 0.)? as i32;
8022                let e0 = self.arg_num(&args, 10, 0.)? as f32;
8023                let e1 = self.arg_num(&args, 11, 0.)? as f32;
8024                let e2 = self.arg_num(&args, 12, 0.)? as f32;
8025                if let Some(mesh) = crate::gfx::shapes::build(
8026                    kind,
8027                    [cx, cy, cz, sx, sy, sz, rx, ry, rz],
8028                    e0,
8029                    e1,
8030                    e2,
8031                ) {
8032                    let mut gfx = self.gfx.borrow_mut();
8033                    gfx.emit_mesh(&mesh, mode);
8034                }
8035                return Ok(Value::Unit);
8036            },
8037
8038            _ => {},
8039        }
8040
8041        // `form` struct constructor: positional `Name(v0, v1, ...)`.
8042        if let Some(field_names) = self.structs.get(name).cloned() {
8043            if args.len() != field_names.len() {
8044                return Err(EvalErr::from(format!(
8045                    "{name} expects {} field(s), got {}",
8046                    field_names.len(),
8047                    args.len()
8048                )));
8049            }
8050            let fields = field_names.into_iter().zip(args).collect();
8051            return Ok(Value::Struct { name: name.to_string(), fields });
8052        }
8053
8054        // `choose` enum variant constructor: `Variant(...)` or `Enum::Variant(...)`.
8055        if let Some((enum_name, arity)) = self.enum_variants.get(name).cloned() {
8056            if args.len() != arity {
8057                return Err(EvalErr::from(format!(
8058                    "{name} expects {arity} value(s), got {}",
8059                    args.len()
8060                )));
8061            }
8062            let variant = name.rsplit("::").next().unwrap_or(name).to_string();
8063            return Ok(Value::Variant { enum_name, variant, payload: args });
8064        }
8065
8066        Err(EvalErr::from(format!("unknown function '{name}'")))
8067    }
8068
8069    fn call_value(&mut self, v: Value, args: Vec<Value>) -> EvalResult {
8070        match v {
8071            Value::Fn(params, body, mut captured) => {
8072                for (p, a) in params.iter().zip(args) {
8073                    captured.insert(p.clone(), a);
8074                }
8075                match self.framed("<closure>", |me| me.exec_block(&body, &mut captured)) {
8076                    Ok(v) => Ok(v.unwrap_or(Value::Unit)),
8077                    Err(EvalErr::Return(v)) => Ok(v),
8078                    Err(e) => Err(e),
8079                }
8080            },
8081            other => Err(EvalErr::from(format!("cannot call {:?}", other))),
8082        }
8083    }
8084
8085    fn call_method(&self, recv: Value, method: &str, args: Vec<Value>) -> EvalResult {
8086        match (&recv, method) {
8087            (Value::Str(s), "is_empty" | "是空") => Ok(Value::Bool(s.is_empty())),
8088            (Value::Str(s), "len" | "长") => Ok(Value::Number(s.len() as f64)),
8089            (Value::Str(s), "to_string" | "转文") => Ok(Value::Str(s.clone())),
8090            (Value::Str(s), "contains" | "包含") => {
8091                if let Some(Value::Str(sub)) = args.first() {
8092                    Ok(Value::Bool(s.contains(sub.as_str())))
8093                } else {
8094                    Ok(Value::Bool(false))
8095                }
8096            },
8097            (Value::Str(s), "push_str" | "推_文") => {
8098                let mut s2 = s.clone();
8099                if let Some(Value::Str(a)) = args.first() {
8100                    s2.push_str(a);
8101                }
8102                Ok(Value::Str(s2))
8103            },
8104            (Value::List(v), "len" | "长") => Ok(Value::Number(v.len() as f64)),
8105            (Value::List(v), "push" | "推") => {
8106                let mut v2 = v.clone();
8107                if let Some(a) = args.first() {
8108                    v2.push(a.clone());
8109                }
8110                Ok(Value::List(v2))
8111            },
8112            // `form` field access: `point.x` (no-arg method == field read).
8113            (Value::Struct { fields, .. }, _) if args.is_empty() => fields
8114                .iter()
8115                .find(|(k, _)| k == method)
8116                .map(|(_, v)| v.clone())
8117                .ok_or_else(|| EvalErr::from(format!("no field '{method}' on {recv}"))),
8118            // Enum introspection: `.tag` → variant name, `.is(Name)` not needed for now.
8119            (Value::Variant { variant, .. }, "tag" | "标签" | "タグ" | "태그" | "ป้าย")
8120                if args.is_empty() =>
8121            {
8122                Ok(Value::Str(variant.clone()))
8123            },
8124            (Value::Ok(inner), _) | (Value::Err(inner), _) => Ok(*inner.clone()),
8125            _ => Err(EvalErr::from(format!("no method '{method}' on {recv}"))),
8126        }
8127    }
8128
8129    // ─── Pattern matching ─────────────────────────────────────────────────────
8130
8131    fn match_pattern(&self, pat: &Pattern, val: &Value) -> Option<Env> {
8132        match (pat, val) {
8133            (Pattern::Wildcard, _) => Some(Env::new()),
8134            (Pattern::Str(s), Value::Str(v)) if s == v => Some(Env::new()),
8135            (Pattern::Number(n), Value::Number(v)) if (n - v).abs() < 1e-12 => Some(Env::new()),
8136            (Pattern::Bool(b), Value::Bool(v)) if b == v => Some(Env::new()),
8137            (Pattern::Ident(name), _) => {
8138                let mut e = Env::new();
8139                e.insert(name.clone(), val.clone());
8140                Some(e)
8141            },
8142            (Pattern::Constructor(ctor, inner_pat), _) => {
8143                let (matches, inner_val) = match (ctor.as_str(), val) {
8144                    ("ok" | "好", Value::Ok(v)) => (true, Some(v.as_ref().clone())),
8145                    ("bad" | "坏", Value::Err(v)) => (true, Some(v.as_ref().clone())),
8146                    ("ok" | "好", v) if !matches!(v, Value::Err(_)) => (true, Some(v.clone())),
8147                    _ => (false, None),
8148                };
8149                if !matches {
8150                    return None;
8151                }
8152                match (inner_pat, inner_val) {
8153                    (Some(p), Some(v)) => self.match_pattern(p, &v),
8154                    (None, _) => Some(Env::new()),
8155                    (Some(p), None) => self.match_pattern(p, &Value::Unit),
8156                }
8157            },
8158            // User enum variant pattern: `Circle(r)`, `Pair(a, b)`, nullary `Origin()`.
8159            (Pattern::Variant(vname, sub_pats), Value::Variant { variant, payload, .. }) => {
8160                if vname != variant || sub_pats.len() != payload.len() {
8161                    return None;
8162                }
8163                let mut bindings = Env::new();
8164                for (p, v) in sub_pats.iter().zip(payload.iter()) {
8165                    bindings.extend(self.match_pattern(p, v)?);
8166                }
8167                Some(bindings)
8168            },
8169            // A zero-payload variant pattern also matches the bare result-style `ok`/`bad`
8170            // values so `Ok()`-style patterns keep working uniformly.
8171            (Pattern::Variant(vname, sub), Value::Ok(v)) if (vname == "ok" || vname == "好") => {
8172                match sub.as_slice() {
8173                    [] => Some(Env::new()),
8174                    [p] => self.match_pattern(p, v),
8175                    _ => None,
8176                }
8177            },
8178            (Pattern::Variant(vname, sub), Value::Err(v))
8179                if (vname == "bad" || vname == "坏" || vname == "err") =>
8180            {
8181                match sub.as_slice() {
8182                    [] => Some(Env::new()),
8183                    [p] => self.match_pattern(p, v),
8184                    _ => None,
8185                }
8186            },
8187            _ => None,
8188        }
8189    }
8190
8191    // ─── Utilities ───────────────────────────────────────────────────────────
8192
8193    fn value_to_iter(&self, val: Value) -> Result<Vec<Value>, EvalErr> {
8194        match val {
8195            Value::List(v) => Ok(v),
8196            Value::Str(s) => Ok(s.chars().map(|c| Value::Str(c.to_string())).collect()),
8197            Value::Number(n) => Ok((0..n as i64).map(|i| Value::Number(i as f64)).collect()),
8198            other => Err(EvalErr::from(format!("cannot iterate over {:?}", other))),
8199        }
8200    }
8201
8202    pub(crate) fn is_truthy(&self, val: &Value) -> bool {
8203        match val {
8204            Value::Bool(b) => *b,
8205            Value::Unit => false,
8206            Value::Number(n) => *n != 0.0,
8207            Value::Str(s) => !s.is_empty(),
8208            Value::List(v) => !v.is_empty(),
8209            Value::Ok(_) => true,
8210            Value::Err(_) => false,
8211            Value::Fn(_, _, _) => true,
8212            Value::Struct { .. } => true,
8213            Value::Variant { .. } => true,
8214        }
8215    }
8216
8217    fn to_number(&self, val: &Value) -> Result<f64, EvalErr> {
8218        match val {
8219            Value::Number(n) => Ok(*n),
8220            Value::Str(s) => s
8221                .parse()
8222                .map_err(|_| EvalErr::from(format!("cannot convert '{s}' to number"))),
8223            other => Err(EvalErr::from(format!("expected number, got {:?}", other))),
8224        }
8225    }
8226
8227    /// Get the n-th argument as f64, falling back to `default` if missing.
8228    fn arg_num(&self, args: &[Value], n: usize, default: f64) -> Result<f64, EvalErr> {
8229        match args.get(n) {
8230            Some(v) => self.to_number(v),
8231            None => Ok(default),
8232        }
8233    }
8234
8235    fn arg_str(&self, args: &[Value], n: usize, default: &str) -> String {
8236        args.get(n)
8237            .map(|v| v.to_string())
8238            .unwrap_or_else(|| default.to_string())
8239    }
8240
8241    /// Read a list-of-numbers argument as `Vec<f32>` (empty if absent/not a list).
8242    #[allow(dead_code)]
8243    fn arg_list_f32(&self, args: &[Value], n: usize) -> Vec<f32> {
8244        match args.get(n) {
8245            Some(Value::List(v)) => v
8246                .iter()
8247                .filter_map(|x| match x {
8248                    Value::Number(n) => Some(*n as f32),
8249                    _ => None,
8250                })
8251                .collect(),
8252            _ => Vec::new(),
8253        }
8254    }
8255
8256    /// Optional `r,g,b` colour override starting at arg `i` → packed 0x00RRGGBB,
8257    /// or `default` if those three numeric args aren't present.
8258    #[cfg(not(target_arch = "wasm32"))]
8259    fn color_at(&self, args: &[Value], i: usize, default: u32) -> u32 {
8260        match (args.get(i), args.get(i + 1), args.get(i + 2)) {
8261            (Some(a), Some(b), Some(c)) => {
8262                match (self.to_number(a), self.to_number(b), self.to_number(c)) {
8263                    (Ok(r), Ok(g), Ok(bl)) => {
8264                        ((r as u32 & 0xFF) << 16) | ((g as u32 & 0xFF) << 8) | (bl as u32 & 0xFF)
8265                    },
8266                    _ => default,
8267                }
8268            },
8269            _ => default,
8270        }
8271    }
8272
8273    /// A pitch argument: a note-name string (`"C4"`, `"A#3"`) or a numeric MIDI value.
8274    #[cfg(not(target_arch = "wasm32"))]
8275    fn pitch_arg(&self, args: &[Value], i: usize, default: i32) -> i32 {
8276        match args.get(i) {
8277            Some(Value::Str(s)) => ling_music::note::parse_pitch(s).unwrap_or(default),
8278            Some(Value::Number(n)) => *n as i32,
8279            _ => default,
8280        }
8281    }
8282
8283    /// Current mouse position + left-button-down (native window only).
8284    #[cfg(not(target_arch = "wasm32"))]
8285    fn mouse_now(&self) -> (f32, f32, bool) {
8286        let gfx = self.gfx.borrow();
8287        let (mx, my) = gfx
8288            .window
8289            .as_ref()
8290            .and_then(|w| w.get_mouse_pos(minifb::MouseMode::Clamp))
8291            .unwrap_or((0.0, 0.0));
8292        let down = gfx
8293            .window
8294            .as_ref()
8295            .map(|w| w.get_mouse_down(minifb::MouseButton::Left))
8296            .unwrap_or(false);
8297        (mx, my, down)
8298    }
8299
8300    /// Rasterize a UI [`ling_ui::widgets::Draw`] into the framebuffer: filled
8301    /// polygons via the AA scanline fill, polylines via AA lines, honouring the
8302    /// current blend mode.
8303    #[cfg(not(target_arch = "wasm32"))]
8304    fn draw_ui(&self, d: &ling_ui::widgets::Draw) {
8305        let mut gfx = self.gfx.borrow_mut();
8306        let (w, h, add) = (gfx.width, gfx.height, gfx.blend == 1);
8307        for (c, poly) in &d.fills {
8308            crate::gfx::raster::fill_contours_aa(
8309                &mut gfx.buffer,
8310                w,
8311                h,
8312                *c,
8313                add,
8314                std::slice::from_ref(poly),
8315            );
8316        }
8317        for (c, pl) in &d.strokes {
8318            for s in pl.windows(2) {
8319                crate::gfx::raster::draw_line_aa(
8320                    &mut gfx.buffer,
8321                    w,
8322                    h,
8323                    *c,
8324                    add,
8325                    s[0][0],
8326                    s[0][1],
8327                    s[1][0],
8328                    s[1][1],
8329                );
8330            }
8331        }
8332    }
8333
8334    /// Parse (dst_x, dst_y, width, height) from the first four args of a tex_* builtin.
8335    fn tex_rect(&self, args: &[Value]) -> Result<(usize, usize, usize, usize), EvalErr> {
8336        let tx = self.arg_num(args, 0, 0.0)? as usize;
8337        let ty = self.arg_num(args, 1, 0.0)? as usize;
8338        let tw = self.arg_num(args, 2, 256.0)? as usize;
8339        let th = self.arg_num(args, 3, 256.0)? as usize;
8340        Ok((tx, ty, tw.max(1), th.max(1)))
8341    }
8342
8343    pub(crate) fn apply_binop(&self, op: &BinOp, l: Value, r: Value) -> EvalResult {
8344        match op {
8345            BinOp::Add => match (l, r) {
8346                (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
8347                (Value::Str(a), Value::Str(b)) => Ok(Value::Str(a + &b)),
8348                (Value::Str(a), b) => Ok(Value::Str(a + &b.to_string())),
8349                (a, Value::Str(b)) => Ok(Value::Str(a.to_string() + &b)),
8350                (a, b) => Err(EvalErr::from(format!("cannot add {:?} and {:?}", a, b))),
8351            },
8352            BinOp::Sub => Ok(Value::Number(self.to_number(&l)? - self.to_number(&r)?)),
8353            BinOp::Mul => Ok(Value::Number(self.to_number(&l)? * self.to_number(&r)?)),
8354            BinOp::Div => Ok(Value::Number(self.to_number(&l)? / self.to_number(&r)?)),
8355            BinOp::Rem => Ok(Value::Number(self.to_number(&l)? % self.to_number(&r)?)),
8356            BinOp::Eq => Ok(Value::Bool(values_equal(&l, &r))),
8357            BinOp::Ne => Ok(Value::Bool(!values_equal(&l, &r))),
8358            BinOp::Lt => Ok(Value::Bool(self.to_number(&l)? < self.to_number(&r)?)),
8359            BinOp::Gt => Ok(Value::Bool(self.to_number(&l)? > self.to_number(&r)?)),
8360            BinOp::Le => Ok(Value::Bool(self.to_number(&l)? <= self.to_number(&r)?)),
8361            BinOp::Ge => Ok(Value::Bool(self.to_number(&l)? >= self.to_number(&r)?)),
8362            BinOp::And => Ok(Value::Bool(self.is_truthy(&l) && self.is_truthy(&r))),
8363            BinOp::Or => Ok(Value::Bool(self.is_truthy(&l) || self.is_truthy(&r))),
8364        }
8365    }
8366
8367    fn builtin_format(&self, args: &[Value]) -> Result<String, EvalErr> {
8368        if args.is_empty() {
8369            return Ok(String::new());
8370        }
8371        let fmt = match &args[0] {
8372            Value::Str(s) => s.clone(),
8373            other => return Ok(other.to_string()),
8374        };
8375
8376        let mut result = String::new();
8377        let mut arg_idx = 1usize;
8378        let mut chars = fmt.chars().peekable();
8379        while let Some(c) = chars.next() {
8380            if c == '{' {
8381                if chars.peek() == Some(&'}') {
8382                    chars.next();
8383                    if arg_idx < args.len() {
8384                        result.push_str(&args[arg_idx].to_string());
8385                        arg_idx += 1;
8386                    }
8387                } else {
8388                    let mut spec = String::new();
8389                    for ch in chars.by_ref() {
8390                        if ch == '}' {
8391                            break;
8392                        }
8393                        spec.push(ch);
8394                    }
8395                    if arg_idx < args.len() {
8396                        if spec.starts_with(":.") {
8397                            if let Value::Number(n) = &args[arg_idx] {
8398                                let prec: usize =
8399                                    spec[2..].trim_end_matches('f').parse().unwrap_or(2);
8400                                result.push_str(&format!("{:.prec$}", n));
8401                                arg_idx += 1;
8402                                continue;
8403                            }
8404                        }
8405                        result.push_str(&args[arg_idx].to_string());
8406                        arg_idx += 1;
8407                    }
8408                }
8409            } else {
8410                result.push(c);
8411            }
8412        }
8413        Ok(result)
8414    }
8415}
8416
8417#[cfg(not(target_arch = "wasm32"))]
8418/// Map a friendly button name (any vendor / d-pad alias) to a gamepad button.
8419#[cfg(not(target_arch = "wasm32"))]
8420fn parse_pad_button(name: &str) -> Option<ling_input::GamepadButton> {
8421    use ling_input::GamepadButton as B;
8422    Some(match name.to_ascii_lowercase().as_str() {
8423        "a" | "south" | "cross" => B::South,
8424        "b" | "east" | "circle" => B::East,
8425        "x" | "west" | "square" => B::West,
8426        "y" | "north" | "triangle" => B::North,
8427        "lb" | "l1" | "left_shoulder" => B::LeftShoulder,
8428        "rb" | "r1" | "right_shoulder" => B::RightShoulder,
8429        "lt" | "l2" | "left_trigger" => B::LeftTrigger,
8430        "rt" | "r2" | "right_trigger" => B::RightTrigger,
8431        "start" | "menu" | "options" => B::Start,
8432        "select" | "back" | "share" | "view" => B::Select,
8433        "guide" | "home" => B::Guide,
8434        "l3" | "left_stick" => B::LeftStick,
8435        "r3" | "right_stick" => B::RightStick,
8436        "up" | "dpad_up" => B::DpadUp,
8437        "down" | "dpad_down" => B::DpadDown,
8438        "left" | "dpad_left" => B::DpadLeft,
8439        "right" | "dpad_right" => B::DpadRight,
8440        _ => return None,
8441    })
8442}
8443
8444#[cfg(not(target_arch = "wasm32"))]
8445fn str_to_minifb_key(name: &str) -> Option<minifb::Key> {
8446    use minifb::Key;
8447    Some(match name {
8448        "numpad0" | "kp0" => Key::NumPad0,
8449        "numpad1" | "kp1" => Key::NumPad1,
8450        "numpad2" | "kp2" => Key::NumPad2,
8451        "numpad3" | "kp3" => Key::NumPad3,
8452        "numpad4" | "kp4" => Key::NumPad4,
8453        "numpad5" | "kp5" => Key::NumPad5,
8454        "numpad6" | "kp6" => Key::NumPad6,
8455        "numpad7" | "kp7" => Key::NumPad7,
8456        "numpad8" | "kp8" => Key::NumPad8,
8457        "numpad9" | "kp9" => Key::NumPad9,
8458        "numpad+" | "kp+" => Key::NumPadPlus,
8459        "numpad-" | "kp-" => Key::NumPadMinus,
8460        "numpad*" | "kp*" => Key::NumPadAsterisk,
8461        "numpad/" | "kp/" => Key::NumPadSlash,
8462        "left" => Key::Left,
8463        "right" => Key::Right,
8464        "up" => Key::Up,
8465        "down" => Key::Down,
8466        "space" => Key::Space,
8467        "enter" => Key::Enter,
8468        "escape" => Key::Escape,
8469        "pageup" => Key::PageUp,
8470        "pagedown" => Key::PageDown,
8471        "lshift" | "leftshift" => Key::LeftShift,
8472        "rshift" | "rightshift" => Key::RightShift,
8473        "lctrl" | "leftctrl" => Key::LeftCtrl,
8474        "rctrl" | "rightctrl" => Key::RightCtrl,
8475        "lalt" | "leftalt" => Key::LeftAlt,
8476        "ralt" | "rightalt" => Key::RightAlt,
8477        "tab" => Key::Tab,
8478        "backspace" => Key::Backspace,
8479        "delete" => Key::Delete,
8480        "insert" => Key::Insert,
8481        "home" => Key::Home,
8482        "end" => Key::End,
8483        "a" => Key::A,
8484        "b" => Key::B,
8485        "c" => Key::C,
8486        "d" => Key::D,
8487        "e" => Key::E,
8488        "f" => Key::F,
8489        "g" => Key::G,
8490        "h" => Key::H,
8491        "i" => Key::I,
8492        "j" => Key::J,
8493        "k" => Key::K,
8494        "l" => Key::L,
8495        "m" => Key::M,
8496        "n" => Key::N,
8497        "o" => Key::O,
8498        "p" => Key::P,
8499        "q" => Key::Q,
8500        "r" => Key::R,
8501        "s" => Key::S,
8502        "t" => Key::T,
8503        "u" => Key::U,
8504        "v" => Key::V,
8505        "w" => Key::W,
8506        "x" => Key::X,
8507        "y" => Key::Y,
8508        "z" => Key::Z,
8509        "0" => Key::Key0,
8510        "1" => Key::Key1,
8511        "2" => Key::Key2,
8512        "3" => Key::Key3,
8513        "4" => Key::Key4,
8514        "5" => Key::Key5,
8515        "6" => Key::Key6,
8516        "7" => Key::Key7,
8517        "8" => Key::Key8,
8518        "9" => Key::Key9,
8519        _ => return None,
8520    })
8521}
8522
8523pub(crate) fn values_equal(a: &Value, b: &Value) -> bool {
8524    match (a, b) {
8525        (Value::Number(x), Value::Number(y)) => (x - y).abs() < 1e-12,
8526        (Value::Str(x), Value::Str(y)) => x == y,
8527        (Value::Bool(x), Value::Bool(y)) => x == y,
8528        (Value::Unit, Value::Unit) => true,
8529        _ => false,
8530    }
8531}
8532
8533// Rasteriser functions live in crate::gfx::raster — imported at top of file.
8534
8535// ── Window platform helpers ────────────────────────────────────────────────────
8536
8537/// Hide the console window that the OS auto-attaches to console-subsystem
8538/// processes. No-op on non-Windows and when no console is present.
8539#[cfg(not(target_arch = "wasm32"))]
8540fn hide_console_window() {
8541    #[cfg(windows)]
8542    unsafe {
8543        extern "system" {
8544            fn GetConsoleWindow() -> isize;
8545            fn ShowWindow(hwnd: isize, nCmdShow: i32) -> i32;
8546        }
8547        let hwnd = GetConsoleWindow();
8548        if hwnd != 0 {
8549            ShowWindow(hwnd, 0); // SW_HIDE = 0
8550        }
8551    }
8552}
8553
8554/// Strip *all* window chrome from `hwnd` and make it cover the whole primary
8555/// monitor (0,0 → screen_w × screen_h), above the taskbar. This turns the
8556/// minifb window into a true borderless-fullscreen surface: no title bar, no
8557/// frame, no resize grips — there is no visible window "handle" left.
8558#[cfg(all(not(target_arch = "wasm32"), windows))]
8559fn make_borderless_fullscreen(hwnd: isize, screen_w: i32, screen_h: i32) {
8560    if hwnd == 0 {
8561        return;
8562    }
8563    unsafe {
8564        extern "system" {
8565            fn SetWindowLongPtrW(hwnd: isize, index: i32, new: isize) -> isize;
8566            fn SetWindowPos(
8567                hwnd: isize,
8568                insert_after: isize,
8569                x: i32,
8570                y: i32,
8571                cx: i32,
8572                cy: i32,
8573                flags: u32,
8574            ) -> i32;
8575            fn ShowWindow(hwnd: isize, cmd: i32) -> i32;
8576        }
8577        const GWL_STYLE: i32 = -16;
8578        const GWL_EXSTYLE: i32 = -20;
8579        // WS_POPUP (0x80000000) | WS_VISIBLE (0x10000000) — a bare top-level
8580        // window with no caption, border, or system menu.
8581        SetWindowLongPtrW(hwnd, GWL_STYLE, 0x9000_0000isize);
8582        // Clear extended edges (WS_EX_WINDOWEDGE / CLIENTEDGE / DLGMODALFRAME).
8583        SetWindowLongPtrW(hwnd, GWL_EXSTYLE, 0);
8584        // HWND_TOPMOST = -1; SWP_FRAMECHANGED (0x0020) | SWP_SHOWWINDOW (0x0040).
8585        SetWindowPos(hwnd, -1isize, 0, 0, screen_w, screen_h, 0x0020 | 0x0040);
8586        ShowWindow(hwnd, 3); // SW_MAXIMIZE-equivalent paint; 3 = SW_SHOWMAXIMIZED
8587    }
8588}
8589
8590/// Primary-monitor resolution and refresh rate as `(width, height, hz)`.
8591/// `hz` falls back to 60 when the driver reports an unknown/`default` rate.
8592#[cfg(all(not(target_arch = "wasm32"), windows))]
8593fn monitor_info() -> (i32, i32, i32) {
8594    unsafe {
8595        extern "system" {
8596            fn GetSystemMetrics(index: i32) -> i32;
8597            fn GetDC(hwnd: isize) -> isize;
8598            fn ReleaseDC(hwnd: isize, hdc: isize) -> i32;
8599            fn GetDeviceCaps(hdc: isize, index: i32) -> i32;
8600        }
8601        let w = GetSystemMetrics(0).max(1); // SM_CXSCREEN
8602        let h = GetSystemMetrics(1).max(1); // SM_CYSCREEN
8603        let hdc = GetDC(0);
8604        let mut hz = if hdc != 0 { GetDeviceCaps(hdc, 116) } else { 0 }; // VREFRESH
8605        if hdc != 0 {
8606            ReleaseDC(0, hdc);
8607        }
8608        if hz <= 1 {
8609            hz = 60; // 0 or 1 means "device default" → assume 60 Hz
8610        }
8611        (w, h, hz)
8612    }
8613}
8614
8615/// Non-Windows native fallback: resolution from [`native_screen_size`], 60 Hz.
8616#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
8617fn monitor_info() -> (i32, i32, i32) {
8618    let (w, h) = native_screen_size();
8619    (w as i32, h as i32, 60)
8620}
8621
8622/// WASM fallback: the canvas is the display surface; assume 60 Hz.
8623#[cfg(target_arch = "wasm32")]
8624fn monitor_info() -> (i32, i32, i32) {
8625    let (w, h) = crate::gfx::webgl::canvas_size();
8626    (w as i32, h as i32, 60)
8627}
8628
8629/// Query the primary display resolution on non-Windows platforms.
8630/// Falls back to 1920×1080 if the size cannot be determined.
8631#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
8632fn native_screen_size() -> (f64, f64) {
8633    // On Linux/macOS we don't have an easy dependency-free call; return a
8634    // sensible default. Callers can always pass explicit dimensions.
8635    (1920.0, 1080.0)
8636}