From 22962bbd63ead1dfc64ccaa1416749c98e764091 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 16 Jan 2026 20:56:16 -0600 Subject: [PATCH] misty --- action.cm | 32 ++--- clay.cm | 12 +- color.cm | 24 ++-- compositor.cm | 8 +- controller.cm | 48 +++---- core.cm | 7 + debug_imgui.cm | 28 ++-- ease.cm | 2 +- emacs.cm | 4 +- examples/chess/chess.ce | 6 +- examples/chess/grid.cm | 2 +- examples/chess/pieces.cm | 2 +- examples/chess/rules.cm | 16 +-- examples/pong/main.ce | 4 +- examples/snake/main.ce | 10 +- examples/tetris/main.ce | 12 +- film2d.cm | 223 ++++++++++++++++++++++------- fx_graph.cm | 2 +- gestures.cm | 6 +- graphics.cm | 26 ++-- input.cm | 278 +++++++++++++++++++++++++++++++++++-- line2d.cm | 24 ---- playdate.cm | 2 +- rasterize.cm | 30 ++-- resources.cm | 28 ++-- sdl_gpu.cm | 14 +- shape2d.cm | 26 +--- sprite.cm | 35 +---- tests/animation.ce | 4 +- tests/bunnymark.ce | 14 +- tests/camera_colorspace.ce | 2 +- tween.cm | 14 +- world.cm | 13 +- 33 files changed, 643 insertions(+), 315 deletions(-) diff --git a/action.cm b/action.cm index 1dbe1bff..3a40b7be 100644 --- a/action.cm +++ b/action.cm @@ -29,8 +29,8 @@ action.get_icon_for_action = function(action) var primary_binding = bindings[0] if (this.current_device == 'keyboard') { - if (primary_binding.startsWith('mouse_button_')) { - var button = primary_binding.replace('mouse_button_', '') + if (starts_with(primary_binding, 'mouse_button_')) { + var button = replace(primary_binding, 'mouse_button_', '') return 'ui/mouse/mouse_' + button + '.png' } else { // Handle special keyboard keys @@ -132,10 +132,10 @@ default_action_map.accion = 'y' // Utility to detect device from input id function detect_device(input_id) { - if (input_id.startsWith('gamepad_')) return 'gamepad' - if (input_id.startsWith('swipe_')) return 'touch' - if (input_id.startsWith('mouse_button')) return 'keyboard' // Mouse buttons are part of keyboard/mouse controller - if (input_id.startsWith('touch_')) return 'touch' + if (starts_with(input_id, 'gamepad_')) return 'gamepad' + if (starts_with(input_id, 'swipe_')) return 'touch' + if (starts_with(input_id, 'mouse_button')) return 'keyboard' // Mouse buttons are part of keyboard/mouse controller + if (starts_with(input_id, 'touch_')) return 'touch' return 'keyboard' } @@ -171,7 +171,7 @@ action.current_gamepad_type = null // Copy defaults for (var key in default_action_map) { - action.action_map[key] = default_action_map[key].slice() + action.action_map[key] = array(default_action_map[key]) } // Swipe‐recognizer state & tuning @@ -181,14 +181,14 @@ var SWIPE_MAX_TIME = 500 // ms action.on_input = function(action_id, evt) { // 1) Detect & store which device user is on (only from raw input, ignore passive inputs) - if (action_id != 'mouse_move' && !action_id.startsWith('mouse_pos') && evt.pressed) { + if (action_id != 'mouse_move' && !starts_with(action_id, 'mouse_pos') && evt.pressed) { var new_device = detect_device(action_id) // For keyboard/mouse detection, also check if the event has modifier keys or is a mouse button // This helps distinguish real keyboard/mouse events from mapped actions var is_real_kb_mouse = (new_device == 'keyboard' && (evt.ctrl != null || evt.shift != null || evt.alt != null || - action_id.startsWith('mouse_button'))) + starts_with(action_id, 'mouse_button'))) if (new_device == 'keyboard' && !is_real_kb_mouse) { // This might be a mapped action, not a real keyboard/mouse event @@ -225,7 +225,7 @@ action.on_input = function(action_id, evt) // 3) Otherwise, find all mapped actions for this input var matched_actions = [] for (var mapped_action in this.action_map) { - if (this.action_map[mapped_action].includes(action_id)) { + if (find(this.action_map[mapped_action], action_id) != null) { matched_actions.push(mapped_action) if (evt.pressed) @@ -254,8 +254,8 @@ action.rebind_action = function(action_name, new_key) { // Remove this key from all other actions for (var act in this.action_map) { - var idx = this.action_map[act].indexOf(new_key) - if (idx >= 0) + var idx = find(this.action_map[act], new_key) + if (idx != null) this.action_map[act].splice(idx, 1) } @@ -275,7 +275,7 @@ action.rebind_action = function(action_name, new_key) { action.get_bindings_for_device = function(action_name) { var all = this.action_map[action_name] || [] var self = this - return all.filter(function(id) { return detect_device(id) == self.current_device }) + return filter(all, function(id) { return detect_device(id) == self.current_device }) } // Returns the primary binding for display - prefer keyboard/mouse, then others @@ -284,7 +284,7 @@ action.get_primary_binding = function(action_name) { if (!all.length) return '(unbound)' // Prefer keyboard/mouse bindings for display stability (mouse buttons now detect as keyboard) - var keyboard = all.filter(function(id) { return detect_device(id) == 'keyboard' }) + var keyboard = filter(all, function(id) { return detect_device(id) == 'keyboard' }) if (keyboard.length) return keyboard[0] // Fall back to any binding @@ -328,7 +328,7 @@ action.load_bindings = function() { action.reset_to_defaults = function() { for (var key in default_action_map) { - this.action_map[key] = default_action_map[key].slice() + this.action_map[key] = array(default_action_map[key]) } this.save_bindings() } @@ -344,7 +344,7 @@ return function() obj.current_gamepad_type = null // Copy defaults for (var key in default_action_map) { - obj.action_map[key] = default_action_map[key].slice() + obj.action_map[key] = array(default_action_map[key]) } obj.load_bindings() return obj diff --git a/clay.cm b/clay.cm index 172721db..d978befe 100644 --- a/clay.cm +++ b/clay.cm @@ -116,12 +116,12 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s // Intersect with parent if (parent_scissor) { - sx = number.max(sx, parent_scissor.x) - sy = number.max(sy, parent_scissor.y) - var right = number.min(vis_x + sw, parent_scissor.x + parent_scissor.width) - var bottom = number.min(vis_y + sh, parent_scissor.y + parent_scissor.height) - sw = number.max(0, right - sx) - sh = number.max(0, bottom - sy) + sx = max(sx, parent_scissor.x) + sy = max(sy, parent_scissor.y) + var right = min(vis_x + sw, parent_scissor.x + parent_scissor.width) + var bottom = min(vis_y + sh, parent_scissor.y + parent_scissor.height) + sw = max(0, right - sx) + sh = max(0, bottom - sy) } current_scissor = {x: sx, y: sy, width: sw, height: sh} diff --git a/color.cm b/color.cm index 2b858091..70fb4dfc 100644 --- a/color.cm +++ b/color.cm @@ -1,7 +1,7 @@ function tohex(n) { - var s = number.floor(n).toString(16); + var s = floor(n).toString(16); if (s.length == 1) s = "0" + s; - return s.toUpperCase(); + return upper(s); }; var Color = { @@ -22,19 +22,15 @@ Color.editor = {}; Color.editor.ur = Color.green; Color.tohtml = function (v) { - var html = v.map(function (n) { - return tohex(n * 255); - }); - return "#" + html.join(""); + var html = array(v, n => tohex(n * 255)); + return "#" + text(html); }; var esc = {}; esc.reset = "\x1b[0"; esc.color = function (v) { - var c = v.map(function (n) { - return number.floor(n * 255); - }); - var truecolor = "\x1b[38;2;" + c.join(";") + ";"; + var c = array(v, n => floor(n * 255)); + var truecolor = "\x1b[38;2;" + text(c, ";") + ";"; return truecolor; }; @@ -92,7 +88,7 @@ Color.Editor = { /* Detects the format of all colors and munges them into a floating point format */ Color.normalize = function (c) { var add_a = function (a) { - var n = this.slice(); + var n = array(this); n[3] = a; return n; }; @@ -120,9 +116,7 @@ Color.normalize = function (c) { // Convert from 0-255 to 0-1 if needed if (needs_conversion) { - c[p] = c[p].map(function (x) { - return x / 255; - }); + c[p] = array(c[p], x => x / 255); } c[p].alpha = add_a; @@ -192,7 +186,7 @@ ColorMap.sample = function (t, map = this) { if (t > 1) return map[1]; var lastkey = 0; - for (var key of array(map).sort()) { + for (var key of sorted(array(map))) { if (t < key) { var b = map[key]; var a = map[lastkey]; diff --git a/compositor.cm b/compositor.cm index e8cbcb02..ccd06231 100644 --- a/compositor.cm +++ b/compositor.cm @@ -353,16 +353,16 @@ function _calc_presentation(src, dst, mode) { return {x: 0, y: 0, width: dst.width, height: dst.height} if (mode == 'integer_scale') { - var sx = number.floor(dst.width / src.width) - var sy = number.floor(dst.height / src.height) - var s = number.max(1, number.min(sx, sy)) + var sx = floor(dst.width / src.width) + var sy = floor(dst.height / src.height) + var s = max(1, min(sx, sy)) var w = src.width * s var h = src.height * s return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h} } // letterbox - var scale = number.min(dst.width / src.width, dst.height / src.height) + var scale = min(dst.width / src.width, dst.height / src.height) var w = src.width * scale var h = src.height * scale return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h} diff --git a/controller.cm b/controller.cm index 1f90aa5f..c9bad2ef 100644 --- a/controller.cm +++ b/controller.cm @@ -6,7 +6,7 @@ var downkeys = {}; function keyname(key) { var str = input.keyname(key); - return str.toLowerCase(); + return lower(str); } function modstr(mod = input.keymod()) { @@ -66,7 +66,7 @@ prosperon.on('mouse_button_up', function(e) { input.mouse = {}; input.mouse.screenpos = function mouse_screenpos() { - return mousepos.slice(); + return array(mousepos); }; input.mouse.worldpos = function mouse_worldpos() { return prosperon.camera.screen2world(mousepos); @@ -75,7 +75,7 @@ input.mouse.viewpos = function mouse_viewpos() { var world = input.mouse.worldpos(); - return mousepos.slice(); + return array(world) } input.mouse.disabled = function mouse_disabled() { input.mouse_mode(1); @@ -109,7 +109,7 @@ input.mouse.normal.doc = "Set the mouse to show again after hiding."; input.keyboard = {}; input.keyboard.down = function (code) { if (is_number(code)) return downkeys[code]; - if (is_text(code)) return downkeys[code.toUpperCase().charCodeAt()] || downkeys[code.toLowerCase().charCodeAt()]; + if (is_text(code)) return downkeys[codepoint(upper(code))] || downkeys[codepoint(lower(code))]; return null; }; @@ -175,7 +175,7 @@ input.action = { input.tabcomplete = function tabcomplete(val, list) { if (!val) return val; - list = filter(x => x.startsWith(val)) + list = filter(list, x => starts_with(x, val)) if (list.length == 1) { return list[0]; @@ -186,14 +186,12 @@ input.tabcomplete = function tabcomplete(val, list) { while (!ret && list.length != 0) { var char = list[0][i]; if ( - !list.every(function (x) { - return x[i] == char; - }) + !every(list, x => x[i] == char) ) - ret = list[0].slice(0, i); + ret = text(list[0], 0, i); else { i++; - list = list.filter(x => x.length-1 > i) + list = filter(list, x => x.length-1 > i) } } @@ -209,13 +207,13 @@ input.tabcomplete = function tabcomplete(val, list) { var Player = { players: [], input(fn, ...args) { - this.pawns.forEach(x => x[fn]?.(...args)); + arrfor(this.pawns, x => x[fn]?.(...args)); }, mouse_input(type, ...args) { - for (var pawn of [...this.pawns].reverse()) { + for (var pawn of reverse([...this.pawns])) { if (is_function(pawn.inputs?.mouse?.[type])) { - pawn.inputs.mouse[type].call(pawn, ...args); + call(pawn.inputs.mouse[type], pawn, ...args); pawn.inputs.post?.call(pawn); if (!pawn.inputs.fallthru) return; } @@ -223,9 +221,9 @@ var Player = { }, char_input(c) { - for (var pawn of [...this.pawns].reverse()) { + for (var pawn of reverse([...this.pawns])) { if (is_function(pawn.inputs?.char)) { - pawn.inputs.char.call(pawn, c); + call(pawn.inputs.char, pawn, c); pawn.inputs.post?.call(pawn); if (!pawn.inputs.fallthru) return; } @@ -233,7 +231,7 @@ var Player = { }, joy_input(name, joystick) { - for (var pawn of [...this.pawns].reverse()) { + for (var pawn of reverse([...this.pawns])) { if (!pawn.inputs) return; if (!pawn.inputs.joystick) return; if (!pawn.inputs.joystick[name]) return; @@ -250,7 +248,7 @@ var Player = { }, raw_input(cmd, state, ...args) { - for (var pawn of [...this.pawns].reverse()) { + for (var pawn of reverse([...this.pawns])) { var inputs = pawn.inputs; if (!inputs[cmd]) { @@ -277,10 +275,10 @@ var Player = { var consumed = false; if (is_function(fn)) { - fn.call(pawn, ...args); + call(fn, pawn, ...args); consumed = true; } - if (state == "released") inputs.release_post?.call(pawn); + if (state == "released") call(inputs.release_post, pawn); if (inputs.block) return; if (consumed) return; } @@ -295,12 +293,12 @@ var Player = { }, print_pawns() { - [...this.pawns].reverse().forEach(x => log.console(x)) + arrfor(reverse([...this.pawns]), x => log.console(x)) }, create() { var n = meme(this); - n.pawns = new Set() + n.pawns = {} n.gamepads = []; this.players.push(n); this[this.players.length - 1] = n; @@ -314,18 +312,18 @@ var Player = { if (!pawn.inputs) throw Error("attempted to control a pawn without any input object."); - this.pawns.add(pawn); + this.pawns[pawn] = true }, uncontrol(pawn) { - this.pawns.delete(pawn) + delete this.pawns[pawn] }, }; input.do_uncontrol = function input_do_uncontrol(pawn) { if (!pawn.inputs) return; - Player.players.forEach(function (p) { - p.pawns.delete(pawn) + arrfor(Player.players, function (p) { + delete p.pawns[pawn] }); }; diff --git a/core.cm b/core.cm index 4d3bfc4e..7cfadd53 100644 --- a/core.cm +++ b/core.cm @@ -97,6 +97,9 @@ function _main_loop() { var win_size = _backend.get_window_size() + // Get input module for auto-ingestion + var input_mod = use('input') + for (var ev of evts) { if (_config.imgui || _config.editor) { imgui.process_event(ev) @@ -117,6 +120,10 @@ function _main_loop() { } } + // Auto-ingest through input system + input_mod.ingest(ev_obj) + + // Optional raw hook for debugging if (_config.input) { _config.input(ev_obj) } diff --git a/debug_imgui.cm b/debug_imgui.cm index f89a445e..197d9540 100644 --- a/debug_imgui.cm +++ b/debug_imgui.cm @@ -143,7 +143,7 @@ function _render_node_summary(imgui, node) { var info = [] if (node.pos) { - info.push("pos:(" + text(number.round(node.pos.x)) + "," + text(number.round(node.pos.y)) + ")") + info.push("pos:(" + text(round(node.pos.x)) + "," + text(round(node.pos.y)) + ")") } if (node.width && node.height) { @@ -156,7 +156,7 @@ function _render_node_summary(imgui, node) { if (node.text) { var t = node.text - if (t.length > 20) t = t.substring(0, 17) + "..." + if (t.length > 20) t = text(t, 0, 17) + "..." info.push("\"" + t + "\"") } @@ -165,11 +165,11 @@ function _render_node_summary(imgui, node) { for (var i = 0; i < node.effects.length; i++) { fx.push(node.effects[i].type) } - info.push("fx:[" + fx.join(",") + "]") + info.push("fx:[" + text(fx, ",") + "]") } if (info.length > 0) { - imgui.text(" " + info.join(" ")) + imgui.text(" " + text(info, " ")) } } @@ -221,10 +221,10 @@ function _render_node_inspector(imgui, node) { // World transform (read-only) if (node.world_pos) { imgui.text("---") - imgui.text("World Position: (" + text(number.round(node.world_pos.x * 100) / 100) + ", " + text(number.round(node.world_pos.y * 100) / 100) + ")") + imgui.text("World Position: (" + text(round(node.world_pos.x * 100) / 100) + ", " + text(round(node.world_pos.y * 100) / 100) + ")") } if (node.world_opacity != null) { - imgui.text("World Opacity: " + text(number.round(node.world_opacity * 100) / 100)) + imgui.text("World Opacity: " + text(round(node.world_opacity * 100) / 100)) } // Image @@ -360,8 +360,8 @@ function _render_pass_inspector(imgui, pass) { imgui.text("Uniforms:") for (var k in pass.uniforms) { var v = pass.uniforms[k] - if (Array.isArray(v)) { - imgui.text(" " + k + ": [" + v.join(", ") + "]") + if (is_array(v)) { + imgui.text(" " + k + ": [" + text(v, ", ") + "]") } else { imgui.text(" " + k + ": " + text(v)) } @@ -372,10 +372,10 @@ function _render_pass_inspector(imgui, pass) { if (pass.color) { imgui.text("---") imgui.text("Clear: rgba(" + - text(number.round(pass.color.r * 255)) + "," + - text(number.round(pass.color.g * 255)) + "," + - text(number.round(pass.color.b * 255)) + "," + - text(number.round(pass.color.a * 100) / 100) + ")") + text(round(pass.color.r * 255)) + "," + + text(round(pass.color.g * 255)) + "," + + text(round(pass.color.b * 255)) + "," + + text(round(pass.color.a * 100) / 100) + ")") } // Source size @@ -449,8 +449,8 @@ function _render_stats(imgui, stats) { if (stats.fps) { imgui.text("---") - imgui.text("FPS: " + text(number.round(stats.fps))) - imgui.text("Frame time: " + text(number.round(stats.frame_time_ms * 100) / 100) + " ms") + imgui.text("FPS: " + text(round(stats.fps))) + imgui.text("Frame time: " + text(round(stats.frame_time_ms * 100) / 100) + " ms") } }) } diff --git a/ease.cm b/ease.cm index 473c812e..2f999e48 100644 --- a/ease.cm +++ b/ease.cm @@ -130,7 +130,7 @@ Ease.zoom = { return function(t) { if (t == 0) return 0 if (t == 1) return 1 - if (number.abs(ratio - 1) < 0.001) return t + if (abs(ratio - 1) < 0.001) return t // Position interpolation formula: (r^t - 1) / (r - 1) return (math.power(ratio, t) - 1) / (ratio - 1) } diff --git a/emacs.cm b/emacs.cm index e1f4ab95..a8416b55 100644 --- a/emacs.cm +++ b/emacs.cm @@ -15,7 +15,7 @@ action.on_input = function(action_id, action_data) { if (!action_data.pressed) return - if (!valid_keys.includes(action_id)) + if (find(valid_keys, action_id) == null) return // Only process key events with modifiers or if we're waiting for a prefix continuation @@ -35,7 +35,7 @@ action.on_input = function(action_id, action_data) var key = action_id if (key.length == 1) { // Single character keys - emacs_notation += key.toLowerCase() + emacs_notation += lower(key) } else { // Handle special keys switch (key) { diff --git a/examples/chess/chess.ce b/examples/chess/chess.ce index d27fdc86..33c86613 100644 --- a/examples/chess/chess.ce +++ b/examples/chess/chess.ce @@ -72,7 +72,7 @@ function handleMouseButtonDown(e) { var mx = e.mouse.x; var my = e.mouse.y; - var c = [number.floor(mx / 60), number.floor(my / 60)]; + var c = [floor(mx / 60), floor(my / 60)]; if (!grid.inBounds(c)) return; var cell = grid.at(c); @@ -103,7 +103,7 @@ function handleMouseButtonUp(e) { var mx = e.mouse.x; var my = e.mouse.y; - var c = [number.floor(mx / 60), number.floor(my / 60)]; + var c = [floor(mx / 60), floor(my / 60)]; if (!grid.inBounds(c)) { holdingPiece = false; return; @@ -138,7 +138,7 @@ function handleMouseMotion(e) { var mx = e.pos.x; var my = e.pos.y; - var c = [number.floor(mx / 60), number.floor(my / 60)]; + var c = [floor(mx / 60), floor(my / 60)]; if (!grid.inBounds(c)) { hoverPos = null; return; diff --git a/examples/chess/grid.cm b/examples/chess/grid.cm index e718b676..7e11630d 100644 --- a/examples/chess/grid.cm +++ b/examples/chess/grid.cm @@ -25,7 +25,7 @@ grid.prototype = { // add an entity into a cell add(entity, pos) { this.cell(pos.x, pos.y).push(entity); - entity.coord = pos.slice(); + entity.coord = array(pos); }, // remove an entity from a cell diff --git a/examples/chess/pieces.cm b/examples/chess/pieces.cm index 3d72fb4c..8aa50140 100644 --- a/examples/chess/pieces.cm +++ b/examples/chess/pieces.cm @@ -7,7 +7,7 @@ function Piece(kind, colour) { this.coord = [0,0]; } Piece.prototype.toString = function () { - return this.colour.charAt(0) + this.kind.charAt(0).toUpperCase(); + return character(this.colour) + upper(character(this.kind)); }; function startingPosition(grid) { diff --git a/examples/chess/rules.cm b/examples/chess/rules.cm index b0b2ae54..d14d5fcd 100644 --- a/examples/chess/rules.cm +++ b/examples/chess/rules.cm @@ -11,20 +11,20 @@ var deltas = { var two = (dy == 2 * dir && dx == 0 && cy(pc.coord) == base && grid.at({ x: cx(pc.coord), y: cy(pc.coord)+dir }).length == 0 && grid.at(to).length == 0); - var cap = (dy == dir && number.abs(dx) == 1 && grid.at(to).length); + var cap = (dy == dir && Math.abs(dx) == 1 && grid.at(to).length); return one || two || cap; }, rook : function (pc, dx, dy) { return (dx == 0 || dy == 0); }, - bishop: function (pc, dx, dy) { return number.abs(dx) == number.abs(dy); }, - queen : function (pc, dx, dy) { return (dx == 0 || dy == 0 || number.abs(dx) == number.abs(dy)); }, - knight: function (pc, dx, dy) { return (number.abs(dx) == 1 && number.abs(dy) == 2) || - (number.abs(dx) == 2 && number.abs(dy) == 1); }, - king : function (pc, dx, dy) { return number.max(number.abs(dx), number.abs(dy)) == 1; } + bishop: function (pc, dx, dy) { return Math.abs(dx) == Math.abs(dy); }, + queen : function (pc, dx, dy) { return (dx == 0 || dy == 0 || Math.abs(dx) == Math.abs(dy)); }, + knight: function (pc, dx, dy) { return (Math.abs(dx) == 1 && Math.abs(dy) == 2) || + (Math.abs(dx) == 2 && Math.abs(dy) == 1); }, + king : function (pc, dx, dy) { return Math.max(Math.abs(dx), Math.abs(dy)) == 1; } }; function clearLine(from, to, grid) { - var dx = number.sign(cx(to) - cx(from)); - var dy = number.sign(cy(to) - cy(from)); + var dx = Math.sign(cx(to) - cx(from)); + var dy = Math.sign(cy(to) - cy(from)); var x = cx(from) + dx, y = cy(from) + dy; while (x != cx(to) || y != cy(to)) { if (grid.at({ x: x, y: y }).length) return false; diff --git a/examples/pong/main.ce b/examples/pong/main.ce index a1eb81f1..01d26299 100644 --- a/examples/pong/main.ce +++ b/examples/pong/main.ce @@ -60,10 +60,10 @@ this.update = function(dt) { // Collide with paddle 1? if (r>left1 && lbottom1 && bleft2 && lbottom2 && b [b[0], b[1]]) + blocks: array(SHAPES[key].blocks, b => [b[0], b[1]]) } } @@ -116,7 +116,7 @@ function rotate(blocks) { function clearLines() { var lines = 0 for (var r=ROWS-1; r>=0;) { - if (board[r].every(cell => cell)) { + if (every(board[r], cell => cell)) { lines++ // remove row board.splice(r,1) @@ -134,7 +134,7 @@ function clearLines() { else if (lines==3) score += 500 else if (lines==4) score += 800 linesCleared += lines - level = number.floor(linesCleared/10) + level = floor(linesCleared/10) } function placePiece() { @@ -198,7 +198,7 @@ this.update = function(dt) { if (input.keyboard.down('w')) { if (!rotateHeld) { rotateHeld = true - var test = piece.blocks.map(b => [b[0], b[1]]) + var test = array(piece.blocks, b => [b[0], b[1]]) rotate(test) if (!collides(pieceX, pieceY, test)) piece.blocks = test } @@ -211,7 +211,7 @@ this.update = function(dt) { // Gravity gravityTimer += dt * fallSpeed - var dropInterval = number.max(0.1, baseGravity - level*0.05) + var dropInterval = max(0.1, baseGravity - level*0.05) if (gravityTimer >= dropInterval) { gravityTimer = 0 if (!collides(pieceX, pieceY+1, piece.blocks)) { diff --git a/film2d.cm b/film2d.cm index dcd94434..f8307485 100644 --- a/film2d.cm +++ b/film2d.cm @@ -1,10 +1,115 @@ var film2d = {} +var backend = null + +film2d.set_backend = function(b) { + backend = b +} var next_id = 1 var registry = {} // id -> drawable var group_index = {} // group_name -> [id, id, ...] var plane_index = {} // plane_name -> [id, id, ...] +// Resolve sprite dimensions and UV based on fit mode +// fit: 'fill' | 'contain' | 'cover' | 'none' +// - fill: stretch to exactly match box (distort) +// - contain: fit inside box, preserve aspect (letterbox) +// - cover: fill box, preserve aspect (crop) +// - none: use native pixel size (or pixels_per_tile driven) +function _resolve_sprite_fit(sprite) { + if (!backend || !backend.get_texture_info) return sprite + + var img = sprite.texture || sprite.image + if (!img) return sprite + + var tex_info = backend.get_texture_info(img) + if (!tex_info || !tex_info.width || !tex_info.height) return sprite + + var tex_w = tex_info.width + var tex_h = tex_info.height + var tex_aspect = tex_w / tex_h + + var target_w = sprite.width + var target_h = sprite.height + var fit = sprite.fit || 'none' + + // If one dimension is null, derive from aspect ratio + if (target_w == null && target_h != null) { + target_w = target_h * tex_aspect + sprite.width = target_w + sprite.height = target_h + return sprite + } + if (target_h == null && target_w != null) { + target_h = target_w / tex_aspect + sprite.width = target_w + sprite.height = target_h + return sprite + } + + // Both null - use native size (1 pixel = 1 unit, or could use pixels_per_tile) + if (target_w == null && target_h == null) { + sprite.width = tex_w + sprite.height = tex_h + return sprite + } + + // Both dimensions specified - apply fit mode + var box_aspect = target_w / target_h + + if (fit == 'fill') { + // Stretch to fill - no changes needed, just use target dimensions + sprite.width = target_w + sprite.height = target_h + return sprite + } + + if (fit == 'contain') { + // Fit inside box, preserve aspect (letterbox) + var scale + if (tex_aspect > box_aspect) { + // Image wider than box - constrain by width + scale = target_w / tex_w + } else { + // Image taller than box - constrain by height + scale = target_h / tex_h + } + sprite.width = tex_w * scale + sprite.height = tex_h * scale + return sprite + } + + if (fit == 'cover') { + // Fill box, preserve aspect (crop via UV) + var fit_ax = sprite.fit_anchor_x != null ? sprite.fit_anchor_x : 0.5 + var fit_ay = sprite.fit_anchor_y != null ? sprite.fit_anchor_y : 0.5 + + var scale_w = target_w / tex_w + var scale_h = target_h / tex_h + var scale = max(scale_w, scale_h) + + // Compute visible portion of texture in UV space + var visible_w = target_w / scale + var visible_h = target_h / scale + + // UV rect (0-1 space) + var uv_w = visible_w / tex_w + var uv_h = visible_h / tex_h + var uv_x = (1 - uv_w) * fit_ax + var uv_y = (1 - uv_h) * fit_ay + + sprite.width = target_w + sprite.height = target_h + sprite.uv_rect = {x: uv_x, y: uv_y, width: uv_w, height: uv_h} + return sprite + } + + // fit == 'none' - use native size + sprite.width = tex_w + sprite.height = tex_h + return sprite +} + film2d.register = function(drawable) { var id = text(next_id++) drawable._id = id @@ -34,8 +139,8 @@ film2d.unregister = function(id) { // Remove from plane index var plane = drawable.plane || 'default' if (plane_index[plane]) { - var idx = plane_index[plane].indexOf(id_str) - if (idx >= 0) plane_index[plane].splice(idx, 1) + var idx = find(plane_index[plane], id_str) + if (idx != null) plane_index[plane].splice(idx, 1) } // Remove from group indices @@ -43,8 +148,8 @@ film2d.unregister = function(id) { for (var i = 0; i < groups.length; i++) { var g = groups[i] if (group_index[g]) { - var idx = group_index[g].indexOf(id_str) - if (idx >= 0) group_index[g].splice(idx, 1) + var idx = find(group_index[g], id_str) + if (idx != null) group_index[g].splice(idx, 1) } } @@ -144,73 +249,90 @@ film2d.all_groups = function() { } // Render function - takes drawables directly, no tree traversal -film2d.render = function(params, backend) { +film2d.render = function(params, render_backend) { + backend = render_backend + var drawables = params.drawables || [] var camera = params.camera var target = params.target var target_size = params.target_size var clear_color = params.clear - var layer_sort = params.layer_sort || {} // layer -> 'y' or 'explicit' - - if (drawables.length == 0) return {commands: []} + var layer_sort = params.layer_sort || {} // layer(text) -> "y" or "explicit" - function _y_sort_key(d) { + if (drawables.length == 0) return { commands: [] } + + function _y_sort_key(d) { if (!d || !d.pos) return 0 var y = d.pos.y || 0 var h = d.height || 0 var ay = d.anchor_y if (ay == null) ay = 0.5 - // Convert "pos.y at anchor" -> "feet y" - return y + h * (1 - ay) + return y + h * (1 - ay) // "pos at anchor" -> "feet y" } - - // Sort by layer, then optionally by Y based on layer_sort policy - drawables.sort(function(a, b) { - var al = a.layer || 0 - var bl = b.layer || 0 - var dl = al - bl - if (dl != 0) return dl - var sort_mode = layer_sort[text(al)] || 'explicit' - if (sort_mode == 'y') { - var ay = _y_sort_key(a) - var by = _y_sort_key(b) + // Deterministic "explicit" order anchor (keep if you want _id tie-break behavior) + drawables = sort(drawables, "_id") - // Make this explicit instead of guessing - var y_down = camera && camera.y_down == true + // Bucket drawables by layer + var buckets = Object.create(null) // layer_text -> array + for (var i = 0; i < drawables.length; i++) { + var d = drawables[i] + var layer_key = String(d.layer || 0) + var b = buckets[layer_key] + if (!b) { + b = [] + buckets[layer_key] = b + } + b.push(d) + } - // If y_down: bigger y is lower on screen => should draw later (on top) - // If y_up: smaller y is lower on screen => should draw later (on top) - if (ay != by) return y_down ? (ay - by) : (by - ay) + // Sort layers numerically (keys are text) + var layers = array(buckets) // text keys + var layer_nums = array(layers, k => number(k)) + layers = sort(layers, layer_nums) + + // Merge buckets, y-sorting buckets that request it + var y_down = camera && camera.y_down == true + var sorted_drawables = [] + + for (var li = 0; li < layers.length; li++) { + var layer_key = layers[li] + var b = buckets[layer_key] + + var mode = layer_sort[layer_key] || "explicit" + if (mode == "y") { + var keys = array(b, d => _y_sort_key(d)) + b = sort(b, keys) // ascending feet-y + if (!y_down) b = reverse(b) // y_up => smaller y draws later => reverse } - var aid = a._id || 0 - var bid = b._id || 0 - return aid < bid ? -1 : 1 - }) - + for (var j = 0; j < b.length; j++) sorted_drawables.push(b[j]) + } + + drawables = sorted_drawables + var commands = [] - commands.push({cmd: 'begin_render', target: target, clear: clear_color}) - commands.push({cmd: 'set_camera', camera: camera}) - + commands.push({ cmd: "begin_render", target: target, clear: clear_color, target_size: target_size }) + commands.push({ cmd: "set_camera", camera: camera }) + var batches = _batch_drawables(drawables) - + for (var i = 0; i < batches.length; i++) { var batch = batches[i] - if (batch.type == 'sprite_batch') - commands.push({cmd: 'draw_batch', batch_type: 'sprites', geometry: {sprites: batch.sprites}, texture: batch.texture, material: batch.material}) - else if (batch.type == 'mesh2d_batch') - commands.push({cmd: 'draw_mesh2d', meshes: batch.meshes, texture: batch.texture, material: batch.material}) - else if (batch.type == 'text') - commands.push({cmd: 'draw_text', drawable: batch.drawable}) - else if (batch.type == 'texture_ref') - commands.push({cmd: 'draw_texture_ref', drawable: batch.drawable}) - else if (batch.type == 'shape') - commands.push({cmd: 'draw_shape', drawable: batch.drawable}) + if (batch.type == "sprite_batch") + commands.push({ cmd: "draw_batch", batch_type: "sprites", geometry: { sprites: batch.sprites }, texture: batch.texture, material: batch.material }) + else if (batch.type == "mesh2d_batch") + commands.push({ cmd: "draw_mesh2d", meshes: batch.meshes, texture: batch.texture, material: batch.material }) + else if (batch.type == "text") + commands.push({ cmd: "draw_text", drawable: batch.drawable }) + else if (batch.type == "texture_ref") + commands.push({ cmd: "draw_texture_ref", drawable: batch.drawable }) + else if (batch.type == "shape") + commands.push({ cmd: "draw_shape", drawable: batch.drawable }) } - - commands.push({cmd: 'end_render'}) - return {commands: commands} + + commands.push({ cmd: "end_render" }) + return { commands: commands } } function _batch_drawables(drawables) { @@ -222,6 +344,9 @@ function _batch_drawables(drawables) { var d = drawables[i] if (d.type == 'sprite') { + // Resolve fit mode (computes final width/height/uv_rect) + _resolve_sprite_fit(d) + var tex = d.texture || d.image var mat = d.material || {blend: 'alpha', sampler: d.filter || 'nearest'} diff --git a/fx_graph.cm b/fx_graph.cm index 5d0f5bb9..93348723 100644 --- a/fx_graph.cm +++ b/fx_graph.cm @@ -255,7 +255,7 @@ NODE_EXECUTORS.clip_rect = function(params, backend) { if (!input) return {target: null, commands: []} // Clip doesn't need a new target, just adds scissor to commands - var commands = input.commands ? input.commands.slice() : [] + var commands = input.commands ? array(input.commands) : [] // Insert scissor after begin_render var insert_idx = 0 diff --git a/gestures.cm b/gestures.cm index 94c5ab26..8d0fd20c 100644 --- a/gestures.cm +++ b/gestures.cm @@ -16,7 +16,7 @@ gesture.reset = function() { } gesture.on_input = function(action_id, action) { - if (!action_id.includes('gamepad_touchpad_')) return + if (search(action_id, 'gamepad_touchpad_') == null) return var finger = action.finger || 0 var touchpad = action.touchpad || 0 @@ -59,7 +59,7 @@ gesture.on_input = function(action_id, action) { var currentDist = this.dist(fingers[0], fingers[1]) var d = currentDist - this.startDist - if (number.abs(d) >= this.PINCH_TH) { + if (abs(d) >= this.PINCH_TH) { var gesture_type = d > 0 ? 'pinch_out' : 'pinch_in' // scene.recurse(game.root, 'on_input', gesture_type, { delta: d }) this.startDist = currentDist @@ -79,7 +79,7 @@ gesture.on_input = function(action_id, action) { var dy = action.y - touch.startY if (dt < this.MAX_TIME / 1000) { // Convert to seconds - var absX = number.abs(dx), absY = number.abs(dy) + var absX = abs(dx), absY = abs(dy) if (absX > this.MIN_SWIPE / 100 || absY > this.MIN_SWIPE / 100) { // Normalize for 0-1 range var dir = absX > absY ? (dx > 0 ? 'swipe_right' : 'swipe_left') diff --git a/graphics.cm b/graphics.cm index 5237352d..f96c000e 100644 --- a/graphics.cm +++ b/graphics.cm @@ -55,10 +55,10 @@ function decorate_rect_px(img) { // store pixel-space version: [x, y, w, h] in texels img.rect_px = { - x:number.round(img.rect.x * width), - y:number.round(img.rect.y * height), - width:number.round(img.rect.width * width), - height:number.round(img.rect.height * height) + x:round(img.rect.x * width), + y:round(img.rect.y * height), + width:round(img.rect.width * width), + height:round(img.rect.height * height) } } @@ -75,7 +75,7 @@ function wrapSurface(surf, maybeRect){ return h; } function wrapFrames(arr){ /* [{surface,time,rect}, …] → [{image,time}] */ - return arr.map(f => { + return array(arr, f => { // Handle both surface objects and objects with surface property var surf = f.surface || f; return { @@ -119,7 +119,7 @@ function decode_gif(decoded) { } // Multiple frames - return array with time property - return decoded.frames.map(function(frame) { + return array(decoded.frames, frame => { return { surface: frame, time: (frame.duration || 100) / 1000.0 // convert ms to seconds @@ -138,7 +138,7 @@ function decode_aseprite(decoded) { // Multiple frames without tags - return as single animation return { - frames: decoded.frames.map(function(frame) { + frames: array(decoded.frames, frame => { return { surface: frame, time: (frame.duration || 100) / 1000.0 // convert ms to seconds @@ -152,7 +152,7 @@ function create_image(path){ try{ def bytes = io.slurp(path); - var ext = path.split('.').pop() + var ext = array(path, '.').pop() var raw = decode_image(bytes, ext); /* ── Case A: single surface (from make_texture) ────────────── */ @@ -258,7 +258,7 @@ graphics.texture = function texture(path) { if (!is_text(path)) throw Error('need a string for graphics.texture') - var parts = path.split(':') + var parts = array(path, ':') var id = parts[0] var animName = parts[1] var frameIndex = parts[2] @@ -353,7 +353,7 @@ graphics.texture = function texture(path) { } graphics.tex_hotreload = function tex_hotreload(file) { - var basename = file.split('/').pop().split('.')[0] + var basename = array(array(file, '/').pop(), '.')[0] // Check if this basename exists in our cache if (!(basename in cache)) return @@ -390,7 +390,7 @@ graphics.tex_hotreload = function tex_hotreload(file) { Merges specific properties from nv into ov, using an array of property names. */ function merge_objects(ov, nv, arr) { - arr.forEach(x => ov[x] = nv[x]) + arrfor(arr, x => ov[x] = nv[x]) } /** @@ -411,7 +411,7 @@ graphics.get_font = function get_font(path) { if (!is_text(path)) throw Error(`Can't find font with path: ${path}`) - var parts = path.split('.') + var parts = array(path, '.') var size = 16 // default size parts[1] = number(parts[1]) if (parts[1]) { @@ -434,7 +434,7 @@ graphics.get_font = function get_font(path) { } graphics.queue_sprite_mesh = function(queue) { - var sprites = queue.filter(x => x.type == 'sprite') + var sprites = filter(queue, x => x.type == 'sprite') if (sprites.length == 0) return [] var mesh = graphics.make_sprite_mesh(sprites) for (var i = 0; i < sprites.length; i++) { diff --git a/input.cm b/input.cm index f739c8e5..ac5c8375 100644 --- a/input.cm +++ b/input.cm @@ -1,12 +1,274 @@ -var sdl_input = use('sdl3/input') -var emacs = use('emacs') -var gestures = use('gestures') +// Prosperon Input System +// Engine-driven input with user pairing and possession dispatch -return { - gamepad_id_to_tyle: sdl_input.gamepad_id_to_type, - make: function() { - return { - +var backend = use('input/backends/sdl3') +var devices = use('input/devices') +var bindings_mod = use('input/bindings') +var router_mod = use('input/router') + +// Default UI-focused action map +var default_action_map = { + 'ui_up': ['w', 'up', 'gamepad_dpup'], + 'ui_down': ['s', 'down', 'gamepad_dpdown'], + 'ui_left': ['a', 'left', 'gamepad_dpleft'], + 'ui_right': ['d', 'right', 'gamepad_dpright'], + 'confirm': ['return', 'space', 'mouse_button_left', 'gamepad_a'], + 'cancel': ['escape', 'gamepad_b'], + 'menu': ['escape', 'gamepad_start'] +} + +var default_display_names = { + 'ui_up': 'UI Up', + 'ui_down': 'UI Down', + 'ui_left': 'UI Left', + 'ui_right': 'UI Right', + 'confirm': 'Confirm', + 'cancel': 'Cancel', + 'menu': 'Menu' +} + +// Module state +var _users = [] +var _config = { + max_users: 1, + pairing: 'last_used', + emacs: true, + gestures: true, + action_map: default_action_map, + display_names: default_display_names +} + +var _initialized = false +var _window_callback = null + +// Create an input user +function create_user(index, config) { + var action_map = {} + var display_names = {} + + // Merge defaults with config + for (var k in default_action_map) { + action_map[k] = array(default_action_map[k]) + display_names[k] = default_display_names[k] + } + if (config.action_map) { + for (var k in config.action_map) { + var val = config.action_map[k] + action_map[k] = is_array(val) ? array(val) : [val] } } + if (config.display_names) { + for (var k in config.display_names) { + display_names[k] = config.display_names[k] + } + } + + var user = { + index: index, + paired_devices: [], + active_device: null, + bindings: bindings_mod.make(action_map, display_names), + router: null, + control_stack: [], + + // Get current device kind + get device_kind() { + if (!this.active_device) return 'keyboard' + return devices.kind(this.active_device) + }, + + // Get current gamepad type + get gamepad_type() { + if (!this.active_device) return null + return devices.gamepad_type(this.active_device) + }, + + // Get action down state + get down() { + return this.router ? this.router.down : {} + }, + + // Possess an entity (clears stack, sets as sole target) + possess: function(entity) { + this.control_stack = [entity] + }, + + // Push entity onto control stack + push: function(entity) { + this.control_stack.push(entity) + }, + + // Pop from control stack + pop: function() { + if (this.control_stack.length > 1) { + return this.control_stack.pop() + } + return null + }, + + // Get current control target + get target() { + return this.control_stack.length > 0 + ? this.control_stack[this.control_stack.length - 1] + : null + }, + + // Dispatch action to current target + dispatch: function(action, data) { + var target = this.target + if (target && target.on_input) { + target.on_input(action, data) + } + }, + + // Get icon for action using current device + get_icon_for_action: function(action) { + return this.bindings.get_icon_for_action(action, this.device_kind, this.gamepad_type) + }, + + // Get primary binding for action using current device + get_primary_binding: function(action) { + return this.bindings.get_primary_binding(action, this.device_kind) + } + } + + // Create router + user.router = router_mod.make(user, { + emacs: config.emacs, + gestures: config.gestures, + swipe_min_dist: config.swipe_min_dist, + swipe_max_time: config.swipe_max_time, + pinch_threshold: config.pinch_threshold + }) + + // Load saved bindings + user.bindings.load() + + return user +} + +// Pick user based on pairing policy +function pick_user(canon) { + if (_users.length == 0) return null + + // For last_used: always user 0, just update active device + if (_config.pairing == 'last_used') { + var user = _users[0] + + // Only switch on button press, not axis/motion + if (canon.kind == 'button' && canon.pressed) { + if (user.active_device != canon.device_id) { + // Release all held actions when switching device + var old_down = user.router.down + for (var action in old_down) { + if (old_down[action]) { + user.dispatch(action, { pressed: false, released: true, time: canon.time }) + } + } + + user.active_device = canon.device_id + if (find(user.paired_devices, canon.device_id) == null) { + user.paired_devices.push(canon.device_id) + } + } + } + + return user + } + + // For explicit pairing: find user paired to this device + for (var i = 0; i < _users.length; i++) { + if (find(_users[i].paired_devices, canon.device_id) != null) { + _users[i].active_device = canon.device_id + return _users[i] + } + } + + // Unpaired device - could implement join logic here + return null +} + +// Configure the input system +function configure(opts) { + opts = opts || {} + + _config.max_users = opts.max_users || 1 + _config.pairing = opts.pairing || 'last_used' + _config.emacs = opts.emacs != false + _config.gestures = opts.gestures != false + + if (opts.action_map) _config.action_map = opts.action_map + if (opts.display_names) _config.display_names = opts.display_names + if (opts.on_window) _window_callback = opts.on_window + + // Copy gesture config + _config.swipe_min_dist = opts.swipe_min_dist + _config.swipe_max_time = opts.swipe_max_time + _config.pinch_threshold = opts.pinch_threshold + + // Create users + _users = [] + for (var i = 0; i < _config.max_users; i++) { + _users.push(create_user(i, _config)) + } + + _initialized = true +} + +// Ingest a raw SDL event +function ingest(raw_evt) { + if (!_initialized) { + configure({}) + } + + // Translate to canonical format + var canon = backend.translate(raw_evt) + if (!canon) return + + // Handle window events specially + if (canon.kind == 'window') { + if (_window_callback) { + _window_callback(canon) + } + return + } + + // Handle device events + if (canon.kind == 'device') { + if (canon.control == 'connected') { + devices.register(canon) + } else if (canon.control == 'disconnected') { + devices.unregister(canon.device_id) + } + return + } + + // Register device + devices.register(canon) + + // Pick user and route + var user = pick_user(canon) + if (user) { + user.router.handle(canon) + } +} + +// Get user by index +function user(index) { + return _users[index] +} + +return { + configure: configure, + ingest: ingest, + user: user, + + get player1() { return _users[0] }, + get player2() { return _users[1] }, + get player3() { return _users[2] }, + get player4() { return _users[3] }, + + // Re-export for convenience + devices: devices, + backend: backend } \ No newline at end of file diff --git a/line2d.cm b/line2d.cm index 2008f9b2..eb904592 100644 --- a/line2d.cm +++ b/line2d.cm @@ -43,30 +43,6 @@ var line_proto = { return this }, - set_groups: function(groups) { - var old_groups = this.groups - this.groups = groups - film2d.reindex(this._id, old_groups, groups) - return this - }, - - add_group: function(group) { - if (this.groups.indexOf(group) < 0) { - this.groups.push(group) - film2d.index_group(this._id, group) - } - return this - }, - - remove_group: function(group) { - var idx = this.groups.indexOf(group) - if (idx >= 0) { - this.groups.splice(idx, 1) - film2d.unindex_group(this._id, group) - } - return this - }, - _rebuild: function() { var result = build_polyline_mesh(this) this.verts = result.verts diff --git a/playdate.cm b/playdate.cm index b4ce0f62..e6594ece 100644 --- a/playdate.cm +++ b/playdate.cm @@ -372,7 +372,7 @@ PlaydateBackend.prototype.gray_to_dither_pattern = function(gray) { [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] ] - var index = number.floor(gray * (patterns.length - 1)) + var index = floor(gray * (patterns.length - 1)) return patterns[index] } diff --git a/rasterize.cm b/rasterize.cm index 484fab61..f6e98abe 100644 --- a/rasterize.cm +++ b/rasterize.cm @@ -26,10 +26,10 @@ rasterize.ellipse = function ellipse(pos, radii, opt) { var cx = pos[0], cy = pos[1] var raw_start = opt.start || 0 var raw_end = opt.end || 1 - var full_circle = number.abs(raw_end - raw_start) >= 1 - 1e-9 + var full_circle = abs(raw_end - raw_start) >= 1 - 1e-9 var start = (raw_start % 1 + 1) % 1 var end = (raw_end % 1 + 1) % 1 - var thickness = number.max(1, opt.thickness || 1) + var thickness = max(1, opt.thickness || 1) var rx_i = rx - thickness, ry_i = ry - thickness @@ -46,8 +46,8 @@ rasterize.ellipse = function ellipse(pos, radii, opt) { var pts = [ [cx + x, cy + y], [cx - x, cy + y], [cx + x, cy - y], [cx - x, cy - y] - ].filter(pt => within_wedge(pt[0]-cx, pt[1]-cy, start, end, full_circle)) - points = points.concat(pts) + ] + points = array(points, filter(pts, pt => within_wedge(pt[0]-cx, pt[1]-cy, start, end, full_circle))) } while (px < py) { @@ -72,14 +72,14 @@ rasterize.ellipse = function ellipse(pos, radii, opt) { for (var dy = -ry; dy <= ry; ++dy) { var yy = dy * dy - var x_out = number.floor(rx * math.sqrt(1 - yy / ry_sq)) + var x_out = floor(rx * math.sqrt(1 - yy / ry_sq)) var y_screen = cy + dy - var x_in = hole ? number.floor(rx_i * math.sqrt(1 - yy / ry_i_sq)) : -1 + var x_in = hole ? floor(rx_i * math.sqrt(1 - yy / ry_i_sq)) : -1 var run_start = null for (var dx = -x_out; dx <= x_out; ++dx) { - if (hole && number.abs(dx) <= x_in) { run_start = null; continue } + if (hole && abs(dx) <= x_in) { run_start = null; continue } if (!within_wedge(dx, dy, start, end, full_circle)) { run_start = null; continue } if (run_start == null) run_start = cx + dx @@ -87,7 +87,7 @@ rasterize.ellipse = function ellipse(pos, radii, opt) { var last = (dx == x_out) var next_in_ring = !last && - !(hole && number.abs(dx+1) <= x_in) && + !(hole && abs(dx+1) <= x_in) && within_wedge(dx+1, dy, start, end, full_circle) if (last || !next_in_ring) { @@ -140,7 +140,7 @@ rasterize.round_rect = function round_rect(rect, radius, thickness) { return rasterize.fill_round_rect(rect, radius) } - radius = number.min(radius, rect.width >> 1, rect.height >> 1) + radius = min(radius, rect.width >> 1, rect.height >> 1) if ((thickness << 1) >= rect.width || (thickness << 1) >= rect.height || @@ -169,9 +169,9 @@ rasterize.round_rect = function round_rect(rect, radius, thickness) { for (var dy = 0; dy < radius; ++dy) { var dy_sq = dy * dy - var dx_out = number.floor(math.sqrt(r_out * r_out - dy_sq)) + var dx_out = floor(math.sqrt(r_out * r_out - dy_sq)) var dx_in = (r_in > 0 && dy < r_in) - ? number.floor(math.sqrt(r_in * r_in - dy_sq)) + ? floor(math.sqrt(r_in * r_in - dy_sq)) : -1 var w = dx_out - dx_in if (w <= 0) continue @@ -184,11 +184,11 @@ rasterize.round_rect = function round_rect(rect, radius, thickness) { ) } - return {type: 'rects', data: rects.concat(strips)} + return {type: 'rects', data: array(rects, strips)} } rasterize.fill_round_rect = function fill_round_rect(rect, radius) { - radius = number.min(radius, rect.width >> 1, rect.height >> 1) + radius = min(radius, rect.width >> 1, rect.height >> 1) var x0 = rect.x, y0 = rect.y, @@ -206,7 +206,7 @@ rasterize.fill_round_rect = function fill_round_rect(rect, radius) { var caps = [] for (var dy = 0; dy < radius; ++dy) { - var dx = number.floor(math.sqrt(radius * radius - dy * dy)) + var dx = floor(math.sqrt(radius * radius - dy * dy)) var w = (dx << 1) + 1 caps.push( @@ -217,7 +217,7 @@ rasterize.fill_round_rect = function fill_round_rect(rect, radius) { ) } - return {type: 'rects', data: rects.concat(caps)} + return {type: 'rects', data: array(rects, caps)} } return rasterize \ No newline at end of file diff --git a/resources.cm b/resources.cm index 7a79afb5..c2240cae 100644 --- a/resources.cm +++ b/resources.cm @@ -23,24 +23,24 @@ Resources.lib = [".so", ".dll", ".dylib"] function getExtension(path) { var idx = path.lastIndexOf('.') if (idx < 0) return '' - return path.substring(idx + 1).toLowerCase() + return lower(text(path, idx + 1)) } // Return true if ext is in at least one of the recognized lists function isRecognizedExtension(ext) { if (!ext) return false - if (Resources.scripts.includes(ext)) return true - if (Resources.images.includes(ext)) return true - if (Resources.sounds.includes(ext)) return true - if (Resources.fonts.includes(ext)) return true - if (Resources.lib.includes('.' + ext)) return true // for .so or .dll + if (search(Resources.scripts, ext) != null) return true + if (search(Resources.images, ext) != null) return true + if (search(Resources.sounds, ext) != null) return true + if (search(Resources.fonts, ext) != null) return true + if (search(Resources.lib, '.' + ext) != null) return true // for .so or .dll return false } function find_in_path(filename, exts = []) { if (!is_text(filename)) return null - if (filename.includes('.')) { + if (search(filename, '.') != null) { var candidate = filename // possibly need "/" ? if (io.exists(candidate) && !io.is_directory(candidate)) return candidate return null @@ -87,10 +87,10 @@ function read_ignore(dir) { var path = dir + '/.prosperonignore' var patterns = [] if (io.exists(path)) { - var lines = io.slurp(path).split('\n') + var lines = array(io.slurp(path), '\n') for (var line of lines) { - line = line.trim() - if (!line || line.startsWith('#')) continue + line = trim(line) + if (!line || starts_with(line, '#')) continue patterns.push(line) } } @@ -124,19 +124,19 @@ Resources.gatherStats = function(filePaths) { } for (var path of filePaths) { var ext = getExtension(path) - if (Resources.scripts.includes(ext)) { + if (find(Resources.scripts, ext) != null) { stats.scripts++ continue } - if (Resources.images.includes(ext)) { + if (find(Resources.images, ext) != null) { stats.images++ continue } - if (Resources.sounds.includes(ext)) { + if (find(Resources.sounds, ext) != null) { stats.sounds++ continue } - if (Resources.fonts.includes(ext)) { + if (find(Resources.fonts, ext) != null) { stats.fonts++ continue } diff --git a/sdl_gpu.cm b/sdl_gpu.cm index dc9ea141..d07e2427 100644 --- a/sdl_gpu.cm +++ b/sdl_gpu.cm @@ -700,7 +700,7 @@ function _load_image_file(path) { var decoded if (!bytes) return null - var ext = path.split('.').pop().toLowerCase() + var ext = lower(array(path, '.').pop()) var surface = null switch (ext) { @@ -758,6 +758,13 @@ sdl_gpu.get_texture = function(path) { return tex } +// Get texture info (dimensions) for a path +sdl_gpu.get_texture_info = function(path) { + var tex = sdl_gpu.get_texture(path) + if (!tex) return null + return {width: tex.width, height: tex.height} +} + // ======================================================================== // RENDER TARGET MANAGEMENT // ======================================================================== @@ -844,7 +851,8 @@ function _build_sprite_vertices(sprites, camera) { var white = {r: 1, g: 1, b: 1, a: 1} - array.for(sprites, s => { + for(var i = 0; i < sprites.length; i++) { + var s = sprites[i] var px = s.pos.x var py = s.pos.y var w = s.width || 1 @@ -948,7 +956,7 @@ function _build_sprite_vertices(sprites, camera) { index_data.w16(vertex_count + 3) vertex_count += 4 - }) + } return { vertices: stone(vertex_data), diff --git a/shape2d.cm b/shape2d.cm index 31494b87..72c458ea 100644 --- a/shape2d.cm +++ b/shape2d.cm @@ -8,31 +8,7 @@ var shape_proto = { this.pos.y = y return this }, - - set_groups: function(groups) { - var old_groups = this.groups - this.groups = groups - film2d.reindex(this._id, old_groups, groups) - return this - }, - - add_group: function(group) { - if (this.groups.indexOf(group) < 0) { - this.groups.push(group) - film2d.index_group(this._id, group) - } - return this - }, - - remove_group: function(group) { - var idx = this.groups.indexOf(group) - if (idx >= 0) { - this.groups.splice(idx, 1) - film2d.unindex_group(this._id, group) - } - return this - }, - + destroy: function() { film2d.unregister(this._id) } diff --git a/sprite.cm b/sprite.cm index e5b8ff2b..d66966cb 100644 --- a/sprite.cm +++ b/sprite.cm @@ -3,36 +3,6 @@ var film2d = use('film2d') var sprite_proto = { type: 'sprite', - set_pos: function(x, y) { - this.pos.x = x - this.pos.y = y - return this - }, - - set_groups: function(groups) { - var old_groups = this.groups - this.groups = groups - film2d.reindex(this._id, old_groups, groups) - return this - }, - - add_group: function(group) { - if (this.groups.indexOf(group) < 0) { - this.groups.push(group) - film2d.index_group(this._id, group) - } - return this - }, - - remove_group: function(group) { - var idx = this.groups.indexOf(group) - if (idx >= 0) { - this.groups.splice(idx, 1) - film2d.unindex_group(this._id, group) - } - return this - }, - destroy: function() { film2d.unregister(this._id) } @@ -44,7 +14,10 @@ return function(props) { pos: {x: 0, y: 0}, image: null, width: 1, - height: 1, + height: null, + fit: 'contain', + fit_anchor_x: 0.5, + fit_anchor_y: 0.5, anchor_x: 0, anchor_y: 0, flip: {x: false, y: false}, diff --git a/tests/animation.ce b/tests/animation.ce index a9d737bd..ad54573e 100644 --- a/tests/animation.ce +++ b/tests/animation.ce @@ -15,7 +15,7 @@ var Anim = (() => { a.timer += dt; def frames = a.src.frames; while(true){ - def time = number.max(frames[a.idx].time || 0, Anim.minDelay); + def time = max(frames[a.idx].time || 0, Anim.minDelay); if(a.timer < time) break; /* still on current frame */ a.timer -= time; @@ -98,7 +98,7 @@ function loop(){ } /* schedule next tick: aim for 60 Hz but won’t matter to anim speed */ - $delay(loop, number.max(0, (1/60) - (os.now()-now))); + $delay(loop, max(0, (1/60) - (os.now()-now))); } loop(); diff --git a/tests/bunnymark.ce b/tests/bunnymark.ce index bc97c123..84ce7cab 100644 --- a/tests/bunnymark.ce +++ b/tests/bunnymark.ce @@ -36,8 +36,8 @@ var center = [0.5,0.5] var vel = 50 function hsl_to_rgb(h, s, l) { - var c = (1 - number.abs(2 * l - 1)) * s - var x = c * (1 - number.abs((h / 60) % 2 - 1)) + var c = (1 - abs(2 * l - 1)) * s + var x = c * (1 - abs((h / 60) % 2 - 1)) var m = l - c / 2 var r = 0, g = 0, b = 0 @@ -75,15 +75,15 @@ function loop() render.clear([22/255,120/255,194/255,255/255]) render.camera(camera) - sprite.forEach(x => x.move(x.dir.scale(dt))) + arrfor(sprite, x => x.move(x.dir.scale(dt))) var queue = sprite.queue() //log.console(queue) - for (var q of queue) { - if (!q.image) continue + arrfor(queue, q => { + if (!q.image) return render.geometry(q.image.texture, q.mesh) - } + }) render.present() dt = os.now() - now @@ -93,7 +93,7 @@ function loop() if (now - last_fps_update >= fps_update_period) { var sum = 0 - for (var i = 0; i < fps_samples.length; i++) sum += fps_samples[i] + arrfor(fps_samples, x => sum += x) prosperon.window.title = `Bunnymark [fps: ${(fps_samples.length/sum).toFixed(1)}]`; last_fps_update = now } diff --git a/tests/camera_colorspace.ce b/tests/camera_colorspace.ce index 204231da..a1fe3b4c 100644 --- a/tests/camera_colorspace.ce +++ b/tests/camera_colorspace.ce @@ -65,7 +65,7 @@ for (var cs in colorspaces) { } // Just test first 3 colorspaces - if (array(colorspaces).indexOf(cs) >= 2) break; + if (find(array(colorspaces), cs) >= 2) break; } log.console("\nColorspace test complete!"); diff --git a/tween.cm b/tween.cm index 7df001c5..b5659479 100644 --- a/tween.cm +++ b/tween.cm @@ -11,13 +11,13 @@ function make_engine(default_clock) { this.tweens.push(tween) }, remove(tween) { - this.tweens = this.tweens.filter(t => t != tween) + this.tweens = filter(this.tweens, t => t != tween) }, update(current_time) { if (current_time == null) { current_time = this.default_clock ? this.default_clock() : time.number() } - for (var tween of this.tweens.slice()) { + for (var tween of array(this.tweens)) { tween._update(current_time) } }, @@ -92,7 +92,7 @@ var TweenProto = { seek: function(global_time) { var elapsed = global_time - this.startTime - var t = number.min(number.max(elapsed / this.duration, 0), 1) + var t = min(max(elapsed / this.duration, 0), 1) var eased = this.easing(t) for (var key in this.endVals) { @@ -100,8 +100,8 @@ var TweenProto = { var end = this.endVals[key] var value = start + (end - start) * eased - if (key.includes('.')) { - var parts = key.split('.') + if (search(key, '.') != null) { + var parts = array(key, '.') var objKey = parts[0] var subKey = parts[1] @@ -211,8 +211,8 @@ var TimelineProto = { toJSON: function() { return { current_time: this.current_time, - events: this.events.map(e => ({ time: e.time, fired: e.fired })), - tweens: this.engine.tweens.map(t => t.toJSON()) + events: array(this.events, e => ({ time: e.time, fired: e.fired })), + tweens: array(this.engine.tweens, t => t.toJSON()) } } } diff --git a/world.cm b/world.cm index 6f99d621..4d85a182 100644 --- a/world.cm +++ b/world.cm @@ -1,10 +1,19 @@ var world = {} -world.add_entity = function(entity_proto) { - var entity = meme(entity_proto) +var entities = {} + +world.add_entity = function(entity_proto, data) { + var entity = meme(entity_proto, data) if (is_function(entity.init)) entity.init() + entities[entity] = true return entity } +world.destroy_entity = function(entity) { + if (is_function(entity.on_destroy)) + entity.on_destroy() + delete entities[entity] +} + return world