From 3d87fdeb5fef521813701615b864e63855e47d63 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 24 Feb 2026 21:08:46 -0600 Subject: [PATCH] probe --- anim2d.cm | 95 ++++++ camera.cm | 5 +- collision2d.cm | 108 ++++++ compositor.cm | 35 ++ core.cm | 103 +++++- docs/api/modules/draw2d.md | 268 ++++----------- docs/entities.md | 51 ++- docs/prosperon.md | 7 +- draw2d.cm | 6 +- examples/bunnymark/config.cm | 6 +- examples/bunnymark/main.ce | 153 ++++++--- examples/chess/chess.ce | 623 +++++++++++++---------------------- examples/chess/config.cm | 10 +- examples/pong/config.cm | 4 +- examples/pong/main.ce | 193 ++++++----- examples/snake/main.ce | 275 ++++++++++------ examples/tetris/main.ce | 491 ++++++++++++++------------- film2d.cm | 28 ++ graphics.cm | 21 ++ input.cm | 31 +- snapshot.cm | 45 +++ tween.cm | 17 +- world.cm | 62 ++++ 23 files changed, 1551 insertions(+), 1086 deletions(-) create mode 100644 anim2d.cm create mode 100644 collision2d.cm create mode 100644 snapshot.cm diff --git a/anim2d.cm b/anim2d.cm new file mode 100644 index 00000000..8455fc3c --- /dev/null +++ b/anim2d.cm @@ -0,0 +1,95 @@ +var film2d = use('film2d') +var graphics = use('graphics') + +var anim_proto = { + type: 'sprite', + + play: function(name) { + var anim = name ? this._anims[name] : this._anims + if (!anim || !anim.frames) return this + this._anim = anim + this._frame = 0 + this._elapsed = 0 + this._playing = true + this.image = anim.frames[0].image + return this + }, + + stop: function() { + this._playing = false + return this + }, + + resume: function() { + this._playing = true + return this + }, + + update: function(dt) { + if (!this._playing || !this._anim) return + var frames = this._anim.frames + this._elapsed += dt + while (this._elapsed >= frames[this._frame].time) { + this._elapsed -= frames[this._frame].time + this._frame++ + if (this._frame >= length(frames)) { + if (this._anim.loop) { + this._frame = 0 + } else { + this._frame = length(frames) - 1 + this._playing = false + break + } + } + } + this.image = frames[this._frame].image + }, + + destroy: function() { + film2d.unregister(this._id) + } +} + +return function(props) { + var img = props.image || props.anim + var anims = graphics.texture(img) + + var defaults = { + type: 'sprite', + pos: {x: 0, y: 0}, + width: null, + height: null, + anchor_x: 0.5, + anchor_y: 0.5, + rotation: 0, + color: {r: 1, g: 1, b: 1, a: 1}, + opacity: 1, + tint: {r: 1, g: 1, b: 1, a: 1}, + filter: 'nearest', + plane: 'default', + layer: 0, + groups: [], + visible: true + } + + var data = object(defaults, props) + data._anims = anims + data._anim = null + data._frame = 0 + data._elapsed = 0 + data._playing = false + data.image = null + + var s = meme(anim_proto, data) + + // Auto-play: if anims is a single animation, start it + if (anims && anims.frames) { + s._anim = anims + s._frame = 0 + s._playing = true + s.image = anims.frames[0].image + } + + film2d.register(s) + return s +} diff --git a/camera.cm b/camera.cm index 633ce583..2eab6490 100644 --- a/camera.cm +++ b/camera.cm @@ -128,9 +128,8 @@ var basecam = { }, } -cam.make = function() -{ - return meme(basecam) +cam.make = function(config) { + return meme(basecam, config || {}) } return cam diff --git a/collision2d.cm b/collision2d.cm new file mode 100644 index 00000000..970a8065 --- /dev/null +++ b/collision2d.cm @@ -0,0 +1,108 @@ +var collision2d = {} + +collision2d.body = function(config) { + var c = config || {} + return { + type: c.type || 'aabb', + width: c.width || 0, + height: c.height || 0, + radius: c.radius || 0, + offset: c.offset || {x: 0, y: 0}, + layer: c.layer || 0, + mask: c.mask || 0xFFFF + } +} + +function body_center(body, pos) { + return { + x: pos.x + body.offset.x, + y: pos.y + body.offset.y + } +} + +function test_aabb_aabb(a, ap, b, bp) { + var ac = body_center(a, ap) + var bc = body_center(b, bp) + var ahw = a.width * 0.5 + var ahh = a.height * 0.5 + var bhw = b.width * 0.5 + var bhh = b.height * 0.5 + return abs(ac.x - bc.x) < ahw + bhw && abs(ac.y - bc.y) < ahh + bhh +} + +function test_circle_circle(a, ap, b, bp) { + var ac = body_center(a, ap) + var bc = body_center(b, bp) + var dx = ac.x - bc.x + var dy = ac.y - bc.y + var r = a.radius + b.radius + return dx * dx + dy * dy < r * r +} + +function test_aabb_circle(aabb, ap, circ, cp) { + var ac = body_center(aabb, ap) + var cc = body_center(circ, cp) + var hw = aabb.width * 0.5 + var hh = aabb.height * 0.5 + var cx = max(ac.x - hw, min(cc.x, ac.x + hw)) + var cy = max(ac.y - hh, min(cc.y, ac.y + hh)) + var dx = cc.x - cx + var dy = cc.y - cy + return dx * dx + dy * dy < circ.radius * circ.radius +} + +collision2d.test = function(a, a_pos, b, b_pos) { + if (a.layer & b.mask == 0 || b.layer & a.mask == 0) + return false + + if (a.type == 'aabb' && b.type == 'aabb') + return test_aabb_aabb(a, a_pos, b, b_pos) + if (a.type == 'circle' && b.type == 'circle') + return test_circle_circle(a, a_pos, b, b_pos) + if (a.type == 'aabb' && b.type == 'circle') + return test_aabb_circle(a, a_pos, b, b_pos) + if (a.type == 'circle' && b.type == 'aabb') + return test_aabb_circle(b, b_pos, a, a_pos) + return false +} + +collision2d.overlap = function(body, pos, others) { + var results = [] + var i = 0 + var other = null + for (i = 0; i < length(others); i++) { + other = others[i] + if (collision2d.test(body, pos, other.body, other.pos)) + push(results, other) + } + return results +} + +collision2d.overlap_point = function(point, bodies) { + var results = [] + var i = 0 + var b = null + var c = null + var hw = 0 + var hh = 0 + var dx = 0 + var dy = 0 + for (i = 0; i < length(bodies); i++) { + b = bodies[i] + c = body_center(b.body, b.pos) + if (b.body.type == 'aabb') { + hw = b.body.width * 0.5 + hh = b.body.height * 0.5 + if (abs(point.x - c.x) < hw && abs(point.y - c.y) < hh) + push(results, b) + } else if (b.body.type == 'circle') { + dx = point.x - c.x + dy = point.y - c.y + if (dx * dx + dy * dy < b.body.radius * b.body.radius) + push(results, b) + } + } + return results +} + +return collision2d diff --git a/compositor.cm b/compositor.cm index 64e7a824..541c26f1 100644 --- a/compositor.cm +++ b/compositor.cm @@ -410,4 +410,39 @@ function _calc_presentation(src, dst, mode) { return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h} } +var _last_plan = null + +var _orig_compile = compositor.compile +compositor.compile = function(config) { + _last_plan = _orig_compile(config) + return _last_plan +} + +compositor.snapshot = function() { + if (!_last_plan) return null + var planes = [] + var i = 0 + var pass = null + for (i = 0; i < length(_last_plan.passes); i++) { + pass = _last_plan.passes[i] + if (pass.type == 'render') { + push(planes, { + drawable_count: length(pass.drawables), + camera: pass.camera ? { + pos: pass.camera.pos, + width: pass.camera.width, + height: pass.camera.height + } : null, + target_size: pass.target_size + }) + } + } + return { + pass_count: length(_last_plan.passes), + target_count: length(array(_last_plan.targets)), + screen_size: _last_plan.screen_size, + planes: planes + } +} + return compositor \ No newline at end of file diff --git a/core.cm b/core.cm index f8eaf3c7..621118a0 100644 --- a/core.cm +++ b/core.cm @@ -9,11 +9,11 @@ // update: function(dt) { ... }, // render: function() { return graph } // }) +// +// Headless mode (no window, no input, no render, no audio): +// core.start({ headless: true, update: function(dt) { ... } }) -var video = use('sdl3/video') -var events = use('sdl3/input') var time_mod = use('time') -var debug_imgui = use('debug_imgui') var core = {} @@ -25,40 +25,56 @@ var _window = null var _last_time = 0 var _framerate = 60 -var imgui = use('imgui') +// Lazy-loaded modules (only in non-headless mode) +var video = null +var events = null +var imgui = null +var debug_imgui = null // Start the application core.start = function(config) { _config = config _framerate = config.framerate || 60 - + _running = true + _last_time = time_mod.number() + + if (config.probe) _register_probes() + + if (config.headless) { + _headless_loop() + return true + } + + // Load SDL modules + video = use('sdl3/video') + events = use('sdl3/input') + imgui = use('imgui') + debug_imgui = use('debug_imgui') + // Initialize SDL GPU backend var sdl_gpu = use('sdl_gpu') _backend = sdl_gpu - + var init_result = _backend.init({ width: config.width || 1280, height: config.height || 720, title: config.title || "Prosperon" }) - + if (!init_result) { log.console("core: Failed to initialize backend") return false } - + _window = _backend.get_window() - + if ((config.imgui || config.editor) && imgui.init) { imgui.init(_window, _backend.get_device()) } - - _running = true - _last_time = time_mod.number() - + // Start main loop _main_loop() - + return true } @@ -105,6 +121,24 @@ function fps_get_avg() { var _current_fps = 0 var _frame_time_ms = 0 +// Headless loop — update only, no window/input/render/audio +function _headless_loop() { + if (!_running) return + + var now = time_mod.number() + var dt = now - _last_time + _last_time = now + + if (_config.update) _config.update(dt) + + var frame_time = 1 / _framerate + var elapsed = time_mod.number() - now + var delay = frame_time - elapsed + if (delay < 0) delay = 0 + + $delay(_headless_loop, delay) +} + // Main loop function _main_loop() { var frame_start = time_mod.number() @@ -232,4 +266,45 @@ function _main_loop() { $delay(_main_loop, delay) } +function _register_probes() { + var probe = use('probe') + var film2d = use('film2d') + var world = use('world') + var comp = use('compositor') + var input_mod = use('input') + var tween_mod = use('tween') + var graphics_mod = use('graphics') + + probe.register("drawables", { + all: function(args) { return film2d.snapshot() } + }) + + probe.register("world", { + all: function(args) { return world.snapshot() }, + count: function(args) { return world.count() } + }) + + probe.register("compositor", { + all: function(args) { return comp.snapshot() } + }) + + probe.register("input", { + all: function(args) { return input_mod.snapshot() } + }) + + probe.register("tweens", { + all: function(args) { return tween_mod.snapshot() } + }) + + probe.register("assets", { + all: function(args) { return graphics_mod.snapshot() } + }) + + probe.register("core", { + fps: function(args) { + return {fps: _current_fps, frame_time_ms: _frame_time_ms} + } + }) +} + return core diff --git a/docs/api/modules/draw2d.md b/docs/api/modules/draw2d.md index 6b94b661..c971ec9b 100644 --- a/docs/api/modules/draw2d.md +++ b/docs/api/modules/draw2d.md @@ -5,229 +5,91 @@ type: docs # draw2d +A collection of retained-mode 2D drawing factories. Each factory creates an object that auto-registers with `film2d` and renders via the compositor. Destroy objects when no longer needed. -A collection of 2D drawing functions that operate in screen space. Provides primitives -for lines, rectangles, text, sprite drawing, etc. +```javascript +var draw = use('draw2d') +``` +## Factories -### point(pos, size, color) function +### draw.sprite(props) +Create a sprite from an image. Auto-registers with `film2d`. +```javascript +var s = draw.sprite({ + image: "hero.png", + pos: {x: 100, y: 200}, + width: 32, height: 32, + plane: 'game', layer: 0 +}) +s.destroy() // remove from renderer +``` +See `sprite.cm` for full property list: `pos`, `image`, `width`, `height`, `anchor_x`, `anchor_y`, `rotation`, `color`, `opacity`, `tint`, `filter`, `plane`, `layer`, `groups`, `visible`, `flip`, `fit`, `uv`. -**pos**: A 2D position ([x, y]) where the point should be drawn. +### draw.shape.rect(props) / circle(props) / ellipse(props) / pill(props) -**size**: The size of the point (not currently affecting rendering). +Create SDF shapes. Supports fill, stroke, rounded corners, dashing, feathering, and texture fill. -**color**: The color of the point, defaults to Color.blue. +```javascript +var box = draw.shape.rect({ + pos: {x: 50, y: 50}, width: 100, height: 60, + fill: {r: 1, g: 0, b: 0, a: 1}, + stroke: {r: 1, g: 1, b: 1, a: 1}, stroke_thickness: 2, + radius: 8, + plane: 'game' +}) +var ball = draw.shape.circle({ + pos: {x: 200, y: 200}, radius: 16, + fill: {r: 0, g: 1, b: 0, a: 1}, + plane: 'game', groups: ['glow'] +}) +``` -**Returns**: None +Properties: `shape_type`, `pos`, `width`, `height`, `radius`, `corner_style`, `feather`, `stroke_thickness`, `stroke_align`, `dash_len`, `gap_len`, `dash_offset`, `cap`, `join`, `fill`, `stroke`, `blend`, `opacity`, `fill_tex`, `uv`, `plane`, `layer`, `groups`, `visible`. +### draw.text(props) -### line(points, color, thickness, pipeline) function +Create a text label. Updates live when you change `.text`. +```javascript +var label = draw.text({ + text: "Score: 0", + pos: {x: 10, y: 500}, + font: "fonts/dos", size: 16, + color: {r: 1, g: 1, b: 1, a: 1}, + plane: 'hud' +}) +label.text = "Score: 42" // updates on next frame +``` +### draw.tilemap(props) +Create a tile-based map. See `tilemap2d.cm`. -**points**: An array of 2D positions representing the line vertices. +### draw.anim(props) -**color**: The color of the line, default Color.white. +Create an animated sprite from an aseprite/gif file. Auto-plays if the image has frames. -**thickness**: The line thickness, default 1. +```javascript +var anim = draw.anim({ + image: "hero.aseprite", + pos: {x: 100, y: 100}, + plane: 'game' +}) +anim.play("walk") // play named animation +anim.stop() // pause +anim.resume() // resume +anim.update(dt) // advance frame (call from update loop) +``` -**pipeline**: (Optional) A pipeline or rendering state object. +## Lifecycle +All draw2d objects register with `film2d` on creation. Call `.destroy()` to unregister and remove from rendering. Set `.visible = false` to hide without destroying. -**Returns**: None - - -### cross(pos, size, color, thickness, pipe) function - - - - -**pos**: The center of the cross as a 2D position ([x, y]). - -**size**: Half the size of each cross arm. - -**color**: The color of the cross, default Color.red. - -**thickness**: The thickness of each line, default 1. - -**pipe**: (Optional) A pipeline or rendering state object. - - -**Returns**: None - - -### arrow(start, end, color, wingspan, wingangle, pipe) function - - - - -**start**: The start position of the arrow ([x, y]). - -**end**: The end (tip) position of the arrow ([x, y]). - -**color**: The color, default Color.red. - -**wingspan**: The length of each arrowhead 'wing', default 4. - -**wingangle**: Wing rotation in degrees, default 10. - -**pipe**: (Optional) A pipeline or rendering state object. - - -**Returns**: None - - -### rectangle(rect, color, pipeline) function - - - - -**rect**: A rectangle object with {x, y, width, height}. - -**color**: The fill color, default Color.white. - -**pipeline**: (Optional) A pipeline or rendering state object. - - -**Returns**: None - - -### tile(image, rect, color, tile, pipeline) function - - -:raises Error: If no image is provided. - - -**image**: An image object or string path to a texture. - -**rect**: A rectangle specifying draw location/size ({x, y, width, height}). - -**color**: The color tint, default Color.white. - -**tile**: A tiling definition ({repeat_x, repeat_y}), default tile_def. - -**pipeline**: (Optional) A pipeline or rendering state object. - - -**Returns**: None - - -### slice9(image, rect, slice, color, info, pipeline) function - - -:raises Error: If no image is provided. - - -**image**: An image object or string path to a texture. - -**rect**: A rectangle specifying draw location/size, default [0, 0]. - -**slice**: The pixel inset or spacing for the 9-slice (number or object). - -**color**: The color tint, default Color.white. - -**info**: A slice9 info object controlling tiling of edges/corners. - -**pipeline**: (Optional) A pipeline or rendering state object. - - -**Returns**: None - - -### image(image, rect, rotation, color, pipeline) function - - -:raises Error: If no image is provided. - - -**image**: An image object or string path to a texture. - -**rect**: A rectangle specifying draw location/size, default [0,0]; width/height default to image size. - -**rotation**: Rotation in degrees (not currently used). - -**color**: The color tint, default none. - -**pipeline**: (Optional) A pipeline or rendering state object. - - -**Returns**: A sprite object that was created for this draw call. - - -### images(image, rects, config) function - - -:raises Error: If no image is provided. - - -**image**: An image object or string path to a texture. - -**rects**: An array of rectangle objects ({x, y, width, height}) to draw. - -**config**: (Unused) Additional config data if needed. - - -**Returns**: An array of sprite objects created and queued for rendering. - - -### sprites(sprites, sort, pipeline) function - - - - -**sprites**: An array of sprite objects to draw. - -**sort**: Sorting mode or order, default 0. - -**pipeline**: (Optional) A pipeline or rendering state object. - - -**Returns**: None - - -### circle(pos, radius, color, inner_radius, pipeline) function - - - - -**pos**: Center of the circle ([x, y]). - -**radius**: The circle radius. - -**color**: The fill color of the circle, default none. - -**inner_radius**: (Unused) Possibly ring thickness, default 1. - -**pipeline**: (Optional) A pipeline or rendering state object. - - -**Returns**: None - - -### text(text, rect, font, size, color, wrap, pipeline) function - - - - -**text**: The string to draw. - -**rect**: A rectangle specifying draw position (and possibly wrapping area). - -**font**: A font object or string path, default sysfont. - -**size**: (Unused) Possibly intended for scaling the font size. - -**color**: The text color, default Color.white. - -**wrap**: Pixel width for text wrapping, default 0 (no wrap). - -**pipeline**: (Optional) A pipeline or rendering state object. - - -**Returns**: None +## Planes and Groups +Every drawable has a `plane` (string) and optional `groups` (array of strings). The compositor renders drawables per-plane. Groups route drawables through effects (bloom, mask, etc.). diff --git a/docs/entities.md b/docs/entities.md index bc037daa..f539189f 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -105,13 +105,58 @@ Each system module provides behavior that operates on the entity's data. No clas ## Querying Entities -The world module lets you find entities: +The world module provides several ways to find and iterate entities: ```javascript var world = use('world') -// All entities are tracked internally -// Query patterns are still being developed +// Iterate all entities +world.each(function(entity) { + log.console(entity) +}) + +// Filter entities by predicate — returns array +var enemies = world.query(function(e) { return e.team == 'enemy' }) + +// Find first matching entity +var player = world.find(function(e) { return e.is_player }) + +// Get entity count +var n = world.count() +``` + +## Updating Entities + +Call `world.update(dt)` each frame to tick all entities that have an `update(dt)` method: + +```javascript +// In your core.start() update callback: +core.start({ + update: function(dt) { + world.update(dt) + } +}) +``` + +## Loading Levels + +Load a level from a JSON array of entity definitions: + +```javascript +world.load_level([ + {"script": "entities/goblin", "pos": {"x": 100, "y": 200}}, + {"script": "entities/tree", "pos": {"x": 300, "y": 100}} +]) +``` + +Each entry's `script` field is loaded via `use()` as the prototype. All other fields are applied as overrides. + +## Clearing the World + +Remove and destroy all entities: + +```javascript +world.clear() ``` ## Override Rules diff --git a/docs/prosperon.md b/docs/prosperon.md index ce53832b..0f9fa4b9 100644 --- a/docs/prosperon.md +++ b/docs/prosperon.md @@ -28,10 +28,13 @@ Prosperon is not a monolithic engine with a global state object. It is a collect | `compositor` | Build render plans from scene configs | | `input` | Action mapping, device routing | | `sound` | Audio playback | -| `world` | Entity management | +| `world` | Entity management (add, query, update, levels) | | `camera` | Viewport into the world | +| `draw2d` | Re-exports sprite, shape, text, tilemap, anim | | `text2d` | Text rendering | -| `shape2d` | SDF shapes | +| `shape2d` | SDF shapes (rect, circle, ellipse, pill) | +| `anim2d` | Sprite animation playback (aseprite/gif) | +| `collision2d` | AABB + circle overlap queries | | `tilemap2d` | Grid-based tile rendering | | `tween` | Value interpolation | | `resources` | Asset path resolution | diff --git a/draw2d.cm b/draw2d.cm index 51bb7b5e..6ccaa966 100644 --- a/draw2d.cm +++ b/draw2d.cm @@ -2,10 +2,12 @@ var sprite = use('sprite') var tilemap = use('tilemap2d') var text = use('text2d') var shape = use('shape2d') +var anim = use('anim2d') return { sprite, tilemap, text, - shape -} \ No newline at end of file + shape, + anim +} diff --git a/examples/bunnymark/config.cm b/examples/bunnymark/config.cm index 58000d55..46d0afc3 100644 --- a/examples/bunnymark/config.cm +++ b/examples/bunnymark/config.cm @@ -1,5 +1,5 @@ return { - title:"Bunnymark", - width:1200, - height:600, + title: "Bunnymark", + width: 1200, + height: 600 } diff --git a/examples/bunnymark/main.ce b/examples/bunnymark/main.ce index f498ee84..bbaae0e1 100644 --- a/examples/bunnymark/main.ce +++ b/examples/bunnymark/main.ce @@ -1,59 +1,124 @@ -var draw = use('draw2d') -var render = use('render') -var graphics = use('graphics') -var sprite = use('sprite') -var geom = use('geometry') -var config = use('config') -var color = use('color') +var core = use('core') +var camera = use('camera') +var compositor = use('compositor') +var input = use('input') +var sprite_factory = use('sprite') +var text2d = use('text2d') var random = use('random') -var bunnyTex = graphics.texture("bunny") +var GW = 1200, GH = 600 -// We'll store our bunnies in an array of objects: { x, y, vx, vy } +var game_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) +var hud_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) + +input.configure({ + action_map: { + spawn: ['mouse_button_left'] + } +}) + +// Bunny tracking var bunnies = [] +var bunny_sprites = [] +var BUNNY_W = 26, BUNNY_H = 37 -// Start with some initial bunnies: -var i = 0; -for (i = 0; i < 100; i++) { - push(bunnies, { - x: random.random() * config.width, - y: random.random() * config.height, +// HUD +var count_label = text2d({ + text: "Bunnies: 0", pos: {x: 10, y: GH - 25}, + plane: 'hud', size: 16, color: {r: 1, g: 1, b: 1, a: 1} +}) +var fps_label = text2d({ + text: "FPS: 0", pos: {x: 10, y: GH - 45}, + plane: 'hud', size: 16, color: {r: 1, g: 1, b: 1, a: 1} +}) + +var fps_timer = 0 +var frame_count = 0 + +function add_bunny(x, y) { + var bunny = { vx: (random.random() * 300) - 150, vy: (random.random() * 300) - 150 + } + var s = sprite_factory({ + image: "bunny", + pos: {x: x, y: y}, + width: BUNNY_W, height: BUNNY_H, + anchor_x: 0.5, anchor_y: 0.5, + plane: 'game' }) + push(bunnies, bunny) + push(bunny_sprites, s) } -var update = function(dt) { - // If left mouse is down, spawn some more bunnies: - var mouse = input.mousestate() - var i = 0; - var b = null; - if (mouse.left) - for (i = 0; i < 50; i++) { - push(bunnies, { - x: mouse.x, - y: mouse.y, - vx: (random.random() * 300) - 150, - vy: (random.random() * 300) - 150 - }) +// Initial bunnies +var i = 0 +for (i = 0; i < 100; i++) { + add_bunny( + random.random() * GW, + random.random() * GH + ) +} +count_label.text = "Bunnies: " + length(bunnies) + +// Mouse position tracking +var mouse_x = GW / 2, mouse_y = GH / 2 + +var comp_config = { + clear: {r: 0.2, g: 0.2, b: 0.3, a: 1}, + planes: [ + {name: 'game', camera: game_cam, resolution: {width: GW, height: GH}, presentation: 'letterbox'}, + {name: 'hud', camera: hud_cam, resolution: {width: GW, height: GH}, presentation: 'stretch'} + ] +} + +core.start({ + width: 1200, height: 600, title: "Bunnymark", + + input: function(ev) { + if (ev.type == 'mouse_motion') { + mouse_x = ev.pos.x + mouse_y = ev.pos.y + } + }, + + update: function(dt) { + // Spawn bunnies while clicking + var down = input.player1().down() + if (down.spawn) { + var si = 0 + for (si = 0; si < 50; si++) { + add_bunny(mouse_x, mouse_y) + } + count_label.text = "Bunnies: " + length(bunnies) } - // Update bunny positions and bounce them inside the screen: - for (i = 0; i < length(bunnies); i++) { - b = bunnies[i] - b.x += b.vx * dt - b.y += b.vy * dt + // Update all bunnies + var b = null, s = null + for (i = 0; i < length(bunnies); i++) { + b = bunnies[i] + s = bunny_sprites[i] - // Bounce off left/right edges - if (b.x < 0) { b.x = 0; b.vx = -b.vx } - else if (b.x > config.width) { b.x = config.width; b.vx = -b.vx } + s.pos.x += b.vx * dt + s.pos.y += b.vy * dt - // Bounce off bottom/top edges - if (b.y < 0) { b.y = 0; b.vy = -b.vy } - else if (b.y > config.height) { b.y = config.height; b.vy = -b.vy } + if (s.pos.x < 0) { s.pos.x = 0; b.vx = -b.vx } + else if (s.pos.x > GW) { s.pos.x = GW; b.vx = -b.vx } + if (s.pos.y < 0) { s.pos.y = 0; b.vy = -b.vy } + else if (s.pos.y > GH) { s.pos.y = GH; b.vy = -b.vy } + } + + // FPS counter + frame_count++ + fps_timer += dt + if (fps_timer >= 1) { + fps_label.text = "FPS: " + frame_count + frame_count = 0 + fps_timer -= 1 + } + }, + + render: function() { + return compositor.execute(compositor.compile(comp_config)) } -} - -var hud = function() { - draw.images(bunnyTex, bunnies) -} +}) diff --git a/examples/chess/chess.ce b/examples/chess/chess.ce index 76db1e2a..2c3ad68c 100644 --- a/examples/chess/chess.ce +++ b/examples/chess/chess.ce @@ -1,403 +1,232 @@ -/* main.js – runs the demo with your prototype-based grid */ +var core = use('core') +var camera = use('camera') +var compositor = use('compositor') +var input = use('input') +var shape = use('shape2d') +var sprite_factory = use('sprite') +var text2d = use('text2d') -var json = use('json') -var draw2d = use('prosperon/draw2d') +var Grid = use('grid') +var MovementSystem = use('movement').MovementSystem +var startingPos = use('pieces').startingPosition +var rules = use('rules') -var blob = use('blob') +var S = 60 +var GW = S * 8, GH = S * 8 -/*──── import our pieces + systems ───────────────────────────────────*/ -var Grid = use('grid'); // your new ctor -var MovementSystem = use('movement').MovementSystem; -var startingPos = use('pieces').startingPosition; -var rules = use('rules'); +var game_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) +var hud_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) -/*──── build board ───────────────────────────────────────────────────*/ -var grid = Grid(8, 8); -grid.width = 8; // (the ctor didn't store them) -grid.height = 8; - -var mover = MovementSystem(grid, rules); -startingPos(grid); - -/*──── networking and game state ─────────────────────────────────────*/ -var gameState = 'waiting'; // 'waiting', 'searching', 'server_waiting', 'connected' -var isServer = false; -var opponent = null; -var myColor = null; // 'white' or 'black' -var isMyTurn = false; - -function updateTitle() { - var title = "Misty Chess - "; - - if (gameState == 'waiting') { - title += "Press S to start server or J to join"; - } else if (gameState == 'searching') { - title += "Searching for server..."; - } else if (gameState == 'server_waiting') { - title += "Waiting for player to join..."; - } else if (gameState == 'connected') { - if (myColor) { - title += (mover.turn == myColor ? "Your turn (" + myColor + ")" : "Opponent's turn (" + mover.turn + ")"); - } else { - title += mover.turn + " turn"; - } - } - - log.console(title) -} - -// Initialize title -updateTitle(); - -/*──── mouse → click-to-move ─────────────────────────────────────────*/ -var selectPos = null; -var hoverPos = null; -var holdingPiece = false; - -var opponentMousePos = null; -var opponentHoldingPiece = false; -var opponentSelectPos = null; - -function handleMouseButtonDown(e) { - if (e.which != 0) return; - - // Don't allow piece selection unless we have an opponent - if (gameState != 'connected' || !opponent) return; - - var mx = e.mouse.x; - var my = e.mouse.y; - - var c = [floor(mx / 60), floor(my / 60)]; - if (!grid.inBounds(c)) return; - - var cell = grid.at(c); - if (length(cell) && cell[0].colour == mover.turn) { - selectPos = c; - holdingPiece = true; - // Send pickup notification to opponent - if (opponent) { - send(opponent, { - type: 'piece_pickup', - pos: c - }); - } - } else { - selectPos = null; - } -} - -function handleMouseButtonUp(e) { - if (e.which != 0 || !holdingPiece || !selectPos) return; - - // Don't allow moves unless we have an opponent and it's our turn - if (gameState != 'connected' || !opponent || !isMyTurn) { - holdingPiece = false; - return; - } - - var mx = e.mouse.x; - var my = e.mouse.y; - - var c = [floor(mx / 60), floor(my / 60)]; - if (!grid.inBounds(c)) { - holdingPiece = false; - return; - } - - if (mover.tryMove(grid.at(selectPos)[0], c)) { - log.console("Made move from", selectPos, "to", c); - // Send move to opponent - log.console("Sending move to opponent:", opponent); - send(opponent, { - type: 'move', - from: selectPos, - to: c - }); - isMyTurn = false; // It's now opponent's turn - log.console("Move sent, now opponent's turn"); - selectPos = null; - updateTitle(); - } - - holdingPiece = false; - - // Send piece drop notification to opponent - if (opponent) { - send(opponent, { - type: 'piece_drop' - }); - } -} - -function handleMouseMotion(e) { - var mx = e.pos.x; - var my = e.pos.y; - - var c = [floor(mx / 60), floor(my / 60)]; - if (!grid.inBounds(c)) { - hoverPos = null; - return; - } - - hoverPos = c; - - // Send mouse position to opponent in real-time - if (opponent && gameState == 'connected') { - send(opponent, { - type: 'mouse_move', - pos: c, - holding: holdingPiece, - selectPos: selectPos - }); - } -} - -function handleKeyDown(e) { - // S key - start server - if (e.scancode == 22 && gameState == 'waiting') { // S key - startServer(); - } - // J key - join server - else if (e.scancode == 13 && gameState == 'waiting') { // J key - joinServer(); - } -} - -/*──── drawing helpers ───────────────────────────────────────────────*/ -/* ── constants ─────────────────────────────────────────────────── */ -var S = 60; // square size in px -var light = [0.93,0.93,0.93,1]; -var dark = [0.25,0.25,0.25,1]; -var allowedColor = [1.0, 0.84, 0.0, 1.0]; // Gold for allowed moves -var myMouseColor = [0.0, 1.0, 0.0, 1.0]; // Green for my mouse -var opponentMouseColor = [1.0, 0.0, 0.0, 1.0]; // Red for opponent mouse - -/* ── draw one 8×8 chess board ──────────────────────────────────── */ -function drawBoard() { - var y = 0; - var x = 0; - var isMyHover = null; - var isOpponentHover = null; - var isValidMove = null; - var color = null; - for (y = 0; y < 8; ++y) - for (x = 0; x < 8; ++x) { - isMyHover = hoverPos && hoverPos[0] == x && hoverPos[1] == y; - isOpponentHover = opponentMousePos && opponentMousePos[0] == x && opponentMousePos[1] == y; - isValidMove = selectPos && holdingPiece && isValidMoveForTurn(selectPos, [x, y]); - - color = ((x+y)&1) ? dark : light; - - if (isValidMove) { - color = allowedColor; // Gold for allowed moves - } else if (isMyHover && !isOpponentHover) { - color = myMouseColor; // Green for my mouse - } else if (isOpponentHover) { - color = opponentMouseColor; // Red for opponent mouse - } - - draw2d.rectangle( - { x: x*S, y: y*S, width: S, height: S }, - { thickness: 0 }, - { color: color } - ); - } -} - -function isValidMoveForTurn(from, to) { - if (!grid.inBounds(to)) return false; - - var piece = grid.at(from)[0]; - if (!piece) return false; - - // Check if the destination has a piece of the same color - var destCell = grid.at(to); - if (length(destCell) && destCell[0].colour == piece.colour) { - return false; - } - - return rules.canMove(piece, from, to, grid); -} - -/* ── draw every live piece ─────────────────────────────────────── */ -function drawPieces() { - var piece = null; - var r = null; - var opponentPiece = null; - - grid.each(function (p) { - if (p.captured) return; - - // Skip drawing the piece being held (by me or opponent) - if (holdingPiece && selectPos && - p.coord[0] == selectPos[0] && - p.coord[1] == selectPos[1]) { - return; - } - - // Skip drawing the piece being held by opponent - if (opponentHoldingPiece && opponentSelectPos && - p.coord[0] == opponentSelectPos[0] && - p.coord[1] == opponentSelectPos[1]) { - return; - } - - var pr = { x: p.coord[0]*S, y: p.coord[1]*S, - width:S, height:S }; - - draw2d.image(p.sprite, pr); - }); - - // Draw the held piece at the mouse position if we're holding one - if (holdingPiece && selectPos && hoverPos) { - piece = grid.at(selectPos)[0]; - if (piece) { - r = { x: hoverPos[0]*S, y: hoverPos[1]*S, - width:S, height:S }; - - draw2d.image(piece.sprite, r); - } - } - - // Draw opponent's held piece if they're dragging one - if (opponentHoldingPiece && opponentSelectPos && opponentMousePos) { - opponentPiece = grid.at(opponentSelectPos)[0]; - if (opponentPiece) { - r = { x: opponentMousePos[0]*S, y: opponentMousePos[1]*S, - width:S, height:S }; - - // Draw with slight transparency to show it's the opponent's piece - draw2d.image(opponentPiece.sprite, r); - } - } -} - -function update(dt) -{ - return {} -} - -function draw() -{ - draw2d.clear() - drawBoard() - drawPieces() - return draw2d.get_commands() -} - -function startServer() { - gameState = 'server_waiting'; - isServer = true; - myColor = 'white'; - isMyTurn = true; - updateTitle(); - - $portal(e => { - log.console("Portal received contact message"); - // Reply with this actor to establish connection - log.console ($self) - send(e, $self); - log.console("Portal replied with server actor"); - }, 5678); -} - -function joinServer() { - gameState = 'searching'; - updateTitle(); - - function contact_fn(actor, reason) { - log.console("CONTACTED!", actor ? "SUCCESS" : "FAILED", reason); - if (actor) { - opponent = actor; - log.console("Connection established with server, sending join request"); - - // Send a greet message with our actor object - send(opponent, { - type: 'greet', - client_actor: $self - }); - } else { - log.console(`Failed to connect: ${reason}`); - gameState = 'waiting'; - updateTitle(); - } - } - - $contact(contact_fn, { - address: "192.168.0.149", - port: 5678 - }); -} - -$receiver(e => { - var fromCell = null; - var piece = null; - - if (e.kind == 'update') - send(e, update(e.dt)) - else if (e.kind == 'draw') - send(e, draw()) - else if (e.type == 'game_start' || e.type == 'move' || e.type == 'greet') - log.console("Receiver got message:", e.type, e); - - if (e.type == 'greet') { - log.console("Server received greet from client"); - // Store the client's actor object for ongoing communication - opponent = e.client_actor; - log.console("Stored client actor:", opponent); - gameState = 'connected'; - updateTitle(); - - // Send game_start to the client - log.console("Sending game_start to client"); - send(opponent, { - type: 'game_start', - your_color: 'black' - }); - log.console("game_start message sent to client"); - } - else if (e.type == 'game_start') { - log.console("Game starting, I am:", e.your_color); - myColor = e.your_color; - isMyTurn = (myColor == 'white'); - gameState = 'connected'; - updateTitle(); - } else if (e.type == 'move') { - log.console("Received move from opponent:", e.from, "to", e.to); - // Apply opponent's move - fromCell = grid.at(e.from); - if (length(fromCell)) { - piece = fromCell[0]; - if (mover.tryMove(piece, e.to)) { - isMyTurn = true; // It's now our turn - updateTitle(); - log.console("Applied opponent move, now my turn"); - } else { - log.console("Failed to apply opponent move"); - } - } else { - log.console("No piece found at from position"); - } - } else if (e.type == 'mouse_move') { - // Update opponent's mouse position - opponentMousePos = e.pos; - opponentHoldingPiece = e.holding; - opponentSelectPos = e.selectPos; - } else if (e.type == 'piece_pickup') { - // Opponent picked up a piece - opponentSelectPos = e.pos; - opponentHoldingPiece = true; - } else if (e.type == 'piece_drop') { - // Opponent dropped their piece - opponentHoldingPiece = false; - opponentSelectPos = null; - } else if (e.type == 'mouse_button_down') { - handleMouseButtonDown(e) - } else if (e.type == 'mouse_button_up') { - handleMouseButtonUp(e) - } else if (e.type == 'mouse_motion') { - handleMouseMotion(e) - } else if (e.type == 'key_down') { - handleKeyDown(e) +input.configure({ + action_map: { + select: ['mouse_button_left'], + cancel: ['escape', 'mouse_button_right'] + } +}) + +// Build board +var grid = Grid(8, 8) +grid.width = 8 +grid.height = 8 +var mover = MovementSystem(grid, rules) +startingPos(grid) + +// Board squares (shape2d) +var light_color = {r: 0.93, g: 0.93, b: 0.85, a: 1} +var dark_color = {r: 0.45, g: 0.55, b: 0.35, a: 1} +var select_color = {r: 1, g: 0.84, b: 0, a: 1} +var valid_color = {r: 0.6, g: 0.8, b: 0.4, a: 1} + +var board_shapes = [] +var bx = 0, by = 0 +for (by = 0; by < 8; by++) { + var row = [] + for (bx = 0; bx < 8; bx++) { + var col = ((bx + by) & 1) ? dark_color : light_color + push(row, shape.rect({ + pos: {x: bx * S + S / 2, y: by * S + S / 2}, + width: S, height: S, + fill: {r: col.r, g: col.g, b: col.b, a: col.a}, + plane: 'game', layer: 0 + })) + } + push(board_shapes, row) +} + +// Piece sprites — one per piece, keyed by piece object +var piece_sprites = {} +var piece_id = 0 +grid.each(function(p) { + piece_id++ + p._id = piece_id + piece_sprites[piece_id] = sprite_factory({ + image: p.sprite, + pos: {x: p.coord[0] * S + S / 2, y: p.coord[1] * S + S / 2}, + width: S, height: S, + anchor_x: 0.5, anchor_y: 0.5, + plane: 'game', layer: 1 + }) +}) + +// Selection state +var selectPos = null +var validMoves = [] + +// Mouse position in grid coords +var hover_gx = -1, hover_gy = -1 + +// Status text +var status_label = text2d({ + text: "White's turn", pos: {x: 10, y: GH - 20}, + plane: 'hud', size: 14, color: {r: 1, g: 1, b: 1, a: 1} +}) + +function update_status() { + status_label.text = mover.turn + "'s turn" +} + +function reset_board_colors() { + var x = 0, y = 0 + for (y = 0; y < 8; y++) { + for (x = 0; x < 8; x++) { + var col = ((x + y) & 1) ? dark_color : light_color + board_shapes[y][x].fill = {r: col.r, g: col.g, b: col.b, a: col.a} + } + } +} + +function highlight_selection() { + reset_board_colors() + if (!selectPos) return + + // Highlight selected square + board_shapes[selectPos[1]][selectPos[0]].fill = { + r: select_color.r, g: select_color.g, b: select_color.b, a: select_color.a + } + + // Highlight valid moves + var i = 0 + for (i = 0; i < length(validMoves); i++) { + var m = validMoves[i] + board_shapes[m[1]][m[0]].fill = { + r: valid_color.r, g: valid_color.g, b: valid_color.b, a: valid_color.a + } + } +} + +function compute_valid_moves(from) { + validMoves = [] + var piece = grid.at(from)[0] + if (!piece) return + + var x = 0, y = 0, to = null, dest = null + for (y = 0; y < 8; y++) { + for (x = 0; x < 8; x++) { + to = [x, y] + dest = grid.at(to) + if (length(dest) && dest[0].colour == piece.colour) continue + if (rules.canMove(piece, from, to, grid)) + push(validMoves, to) + } + } +} + +function sync_piece_sprites() { + grid.each(function(p) { + var spr = piece_sprites[p._id] + if (!spr) return + if (p.captured) { + spr.visible = false + } else { + spr.pos.x = p.coord[0] * S + S / 2 + spr.pos.y = p.coord[1] * S + S / 2 + spr.visible = true + } + }) +} + +// Input handler +var game_input = { + on_input: function(action, data) { + if (!data.pressed) return + + if (action == 'cancel') { + selectPos = null + validMoves = [] + highlight_selection() + return + } + + if (action == 'select' && hover_gx >= 0 && hover_gx < 8 && hover_gy >= 0 && hover_gy < 8) { + var clicked = [hover_gx, hover_gy] + var cell = grid.at(clicked) + + if (selectPos) { + // Try to move + var is_valid = false + var i = 0 + for (i = 0; i < length(validMoves); i++) { + if (validMoves[i][0] == clicked[0] && validMoves[i][1] == clicked[1]) { + is_valid = true + break + } + } + + if (is_valid) { + var src_piece = grid.at(selectPos)[0] + if (src_piece && mover.tryMove(src_piece, clicked)) { + sync_piece_sprites() + update_status() + } + selectPos = null + validMoves = [] + } else if (length(cell) && cell[0].colour == mover.turn) { + // Select different piece + selectPos = clicked + compute_valid_moves(selectPos) + } else { + selectPos = null + validMoves = [] + } + } else { + // Select piece + if (length(cell) && cell[0].colour == mover.turn) { + selectPos = clicked + compute_valid_moves(selectPos) + } + } + highlight_selection() + } + } +} +input.player1().possess(game_input) + +var comp_config = { + clear: {r: 0.15, g: 0.15, b: 0.2, a: 1}, + planes: [ + {name: 'game', camera: game_cam, resolution: {width: GW, height: GH}, presentation: 'letterbox'}, + {name: 'hud', camera: hud_cam, resolution: {width: GW, height: GH}, presentation: 'stretch'} + ] +} + +core.start({ + width: 640, height: 640, title: "Chess", + + input: function(ev) { + if (ev.type == 'mouse_motion') { + // Convert pixel coords to grid coords via camera + var wp = game_cam.window_to_world(ev.pos.x, ev.pos.y) + if (wp) { + hover_gx = floor(wp.x / S) + hover_gy = floor(wp.y / S) + } + } + }, + + update: function(dt) { + }, + + render: function() { + return compositor.execute(compositor.compile(comp_config)) } }) diff --git a/examples/chess/config.cm b/examples/chess/config.cm index d50f71be..1f9b64f1 100644 --- a/examples/chess/config.cm +++ b/examples/chess/config.cm @@ -1,9 +1,5 @@ -// Chess game configuration for Moth framework return { title: "Chess", - resolution: { width: 480, height: 480 }, - internal_resolution: { width: 480, height: 480 }, - fps: 60, - clearColor: [22/255, 120/255, 194/255, 1], - mode: 'stretch' // No letterboxing for chess -}; \ No newline at end of file + width: 640, + height: 640 +} diff --git a/examples/pong/config.cm b/examples/pong/config.cm index 7c612689..a325217e 100644 --- a/examples/pong/config.cm +++ b/examples/pong/config.cm @@ -1,5 +1,5 @@ return { title: "Pong", - width: 858, - height: 525 + width: 960, + height: 540 } diff --git a/examples/pong/main.ce b/examples/pong/main.ce index 30110c61..c280a16b 100644 --- a/examples/pong/main.ce +++ b/examples/pong/main.ce @@ -1,86 +1,127 @@ -// main.js -var draw = use('draw2d') -var config = use('config') -var color = use('color') -var random = use('random') +var core = use('core') +var camera = use('camera') +var compositor = use('compositor') +var input = use('input') +var shape = use('shape2d') +var text2d = use('text2d') -prosperon.camera.transform.pos = [0,0] - -var paddleW = 10, paddleH = 80 -var p1 = {x: 30, y: config.height*0.5, speed: 300} -var p2 = {x: config.width-30, y: config.height*0.5, speed: 300} -var ball = {x: 0, y: 0, vx: 220, vy: 150, size: 10} +var GW = 480, GH = 270 +var paddleW = 8, paddleH = 50, speed = 200 +var ballSize = 8 +var bvx = 220, bvy = 150 var score1 = 0, score2 = 0 -function resetBall() { - ball.x = config.width*0.5 - ball.y = config.height*0.5 - // give it a random vertical bounce - ball.vy = (random.random()<0.5 ? -1:1)*150 - // keep horizontal speed to the same magnitude - ball.vx = ball.vx>0 ? 220 : -220 +// Cameras: game at pixel-art res, HUD at native +var game_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) +var hud_cam = camera.make({width: 960, height: 540, pos: {x: 480, y: 270}}) + +// Action mapping +input.configure({ + action_map: { + p1_up: ['w'], + p1_down: ['s'], + p2_up: ['up'], + p2_down: ['down'] + } +}) + +// Retained shapes — game plane +var midline = shape.rect({ + pos: {x: GW / 2, y: GH / 2}, width: 2, height: GH, + fill: {r: 0.3, g: 0.3, b: 0.3, a: 1}, plane: 'game' +}) + +var p1 = shape.rect({ + pos: {x: 20, y: GH / 2}, width: paddleW, height: paddleH, + fill: {r: 1, g: 1, b: 1, a: 1}, plane: 'game' +}) + +var p2 = shape.rect({ + pos: {x: GW - 20, y: GH / 2}, width: paddleW, height: paddleH, + fill: {r: 1, g: 1, b: 1, a: 1}, plane: 'game' +}) + +var ball = shape.rect({ + pos: {x: GW / 2, y: GH / 2}, width: ballSize, height: ballSize, + fill: {r: 1, g: 1, b: 1, a: 1}, plane: 'game', groups: ['glow'] +}) + +// HUD plane — score text +var score_label = text2d({ + text: "0 0", pos: {x: 420, y: 490}, + plane: 'hud', size: 32, color: {r: 1, g: 1, b: 1, a: 1} +}) + +function reset_ball() { + ball.pos.x = GW / 2 + ball.pos.y = GH / 2 + bvy = (bvy > 0 ? -1 : 1) * 150 + bvx = bvx > 0 ? -220 : 220 } -resetBall() - -var update = function(dt) { - // Move paddles: positive Y is up, so W/↑ means p.y += speed - if (input.keyboard.down('w')) p1.y += p1.speed*dt - if (input.keyboard.down('s')) p1.y -= p1.speed*dt - - // Paddle 2 movement (ArrowUp = up, ArrowDown = down) - if (input.keyboard.down('i')) p2.y += p2.speed*dt - if (input.keyboard.down('k')) p2.y -= p2.speed*dt - - // Clamp paddles to screen - if (p1.y < paddleH*0.5) p1.y = paddleH*0.5 - if (p1.y > config.height - paddleH*0.5) p1.y = config.height - paddleH*0.5 - if (p2.y < paddleH*0.5) p2.y = paddleH*0.5 - if (p2.y > config.height - paddleH*0.5) p2.y = config.height - paddleH*0.5 - - // Move ball - ball.x += ball.vx*dt - ball.y += ball.vy*dt - - // Bounce top/bottom - if (ball.y+ball.size*0.5>config.height || ball.y-ball.size*0.5<0) ball.vy = -ball.vy - - // Check paddle collisions - // p1 bounding box - var left1 = p1.x - paddleW*0.5, right1 = p1.x + paddleW*0.5 - var top1 = p1.y + paddleH*0.5, bottom1 = p1.y - paddleH*0.5 - // p2 bounding box - var left2 = p2.x - paddleW*0.5, right2 = p2.x + paddleW*0.5 - var top2 = p2.y + paddleH*0.5, bottom2 = p2.y - paddleH*0.5 - - // ball half-edges - var l = ball.x - ball.size*0.5, r = ball.x + ball.size*0.5 - var b = ball.y - ball.size*0.5, t = ball.y + ball.size*0.5 - - // Collide with paddle 1? - if (r>left1 && lbottom1 && bleft2 && lbottom2 && bconfig.width) { score1++; resetBall() } +// Compositor: game plane with bloom on ball, HUD overlay +var comp_config = { + clear: {r: 0, g: 0, b: 0, a: 1}, + planes: [ + {name: 'game', camera: game_cam, resolution: {width: GW, height: GH}, presentation: 'letterbox'}, + {name: 'hud', camera: hud_cam, resolution: {width: 960, height: 540}, presentation: 'stretch'} + ], + group_effects: { + glow: {effects: [{type: 'bloom', threshold: 0.3, intensity: 2}]} + } } -var hud = function() { - // Clear screen black - draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1]) +core.start({ + width: 960, height: 540, title: "Pong", - // Draw paddles - draw.rectangle({x:p1.x - paddleW*0.5, y:p1.y - paddleH*0.5, width:paddleW, height:paddleH}, color.white) - draw.rectangle({x:p2.x - paddleW*0.5, y:p2.y - paddleH*0.5, width:paddleW, height:paddleH}, color.white) + update: function(dt) { + var down = input.player1().down() - // Draw ball - draw.rectangle({x:ball.x - ball.size*0.5, y:ball.y - ball.size*0.5, width:ball.size, height:ball.size}, color.white) + // Move paddles + if (down.p1_up) p1.pos.y += speed * dt + if (down.p1_down) p1.pos.y -= speed * dt + if (down.p2_up) p2.pos.y += speed * dt + if (down.p2_down) p2.pos.y -= speed * dt - // Simple score display - var msg = score1 + " " + score2 - draw.text(msg, {x:0, y:10, width:config.width, height:40}, null, 0, color.white, 0) -} + // Clamp paddles + var hh = paddleH / 2 + if (p1.pos.y < hh) p1.pos.y = hh + if (p1.pos.y > GH - hh) p1.pos.y = GH - hh + if (p2.pos.y < hh) p2.pos.y = hh + if (p2.pos.y > GH - hh) p2.pos.y = GH - hh + + // Move ball + ball.pos.x += bvx * dt + ball.pos.y += bvy * dt + + // Bounce top/bottom + var bs = ballSize / 2 + if (ball.pos.y - bs < 0 || ball.pos.y + bs > GH) bvy = -bvy + + // Paddle collisions + var bx = ball.pos.x, by = ball.pos.y + var pw = paddleW / 2, ph = paddleH / 2 + if (bx - bs < p1.pos.x + pw && bx + bs > p1.pos.x - pw && + by + bs > p1.pos.y - ph && by - bs < p1.pos.y + ph) + bvx = abs(bvx) + if (bx + bs > p2.pos.x - pw && bx - bs < p2.pos.x + pw && + by + bs > p2.pos.y - ph && by - bs < p2.pos.y + ph) + bvx = -abs(bvx) + + // Scoring + if (bx < 0) { + score2++ + score_label.text = score1 + " " + score2 + reset_ball() + } + if (bx > GW) { + score1++ + score_label.text = score1 + " " + score2 + reset_ball() + } + }, + + render: function() { + return compositor.execute(compositor.compile(comp_config)) + } +}) diff --git a/examples/snake/main.ce b/examples/snake/main.ce index 5205cc02..b84e201e 100644 --- a/examples/snake/main.ce +++ b/examples/snake/main.ce @@ -1,125 +1,192 @@ -// main.js -var draw = use('draw2d') -var render = use('render') -var graphics = use('graphics') +var core = use('core') +var camera = use('camera') +var compositor = use('compositor') var input = use('input') -var config = use('config') -var color = use('color') +var shape = use('shape2d') +var text2d = use('text2d') +var tw = use('tween') +var ease = use('ease') var random = use('random') -prosperon.camera.transform.pos = [0,0] - +var GW = 600, GH = 600 var cellSize = 20 -var gridW = floor(config.width / cellSize) -var gridH = floor(config.height / cellSize) +var gridW = GW / cellSize +var gridH = GH / cellSize -var snake = null, direction = null, nextDirection = null, apple = null -var moveInterval = 0.1 -var moveTimer = 0 -var gameState = "playing" +var game_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) +var hud_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) -function resetGame() { - var cx = floor(gridW / 2) - var cy = floor(gridH / 2) - snake = [ - {x: cx, y: cy}, - {x: cx-1, y: cy}, - {x: cx-2, y: cy} - ] - direction = {x:1, y:0} - nextDirection = {x:1, y:0} - spawnApple() - gameState = "playing" - moveTimer = 0 +// Action mapping with possession +input.configure({ + action_map: { + up: ['w', 'up'], + down: ['s', 'down'], + left: ['a', 'left'], + right: ['d', 'right'], + restart: ['space'] + } +}) + +// Game state +var snake_shapes = [] +var snake_pos = [] +var dir = {x: 1, y: 0} +var next_dir = {x: 1, y: 0} +var move_timer = 0 +var move_interval = 0.12 +var state = 'playing' +var gameover_label = null +var score = 0 +var score_label = null + +// Input handler entity — possessed by player1 +var game_input = { + on_input: function(action, data) { + if (!data.pressed) return + if (state == 'playing') { + if (action == 'up' && dir.y != -1) next_dir = {x: 0, y: 1} + if (action == 'down' && dir.y != 1) next_dir = {x: 0, y: -1} + if (action == 'left' && dir.x != 1) next_dir = {x: -1, y: 0} + if (action == 'right' && dir.x != -1) next_dir = {x: 1, y: 0} + } + if (action == 'restart' && state == 'gameover') reset_game() + } +} +input.player1().possess(game_input) + +function grid_to_world(gx, gy) { + return {x: gx * cellSize + cellSize / 2, y: gy * cellSize + cellSize / 2} } -function spawnApple() { - apple = {x:floor(random.random()*gridW), y:floor(random.random()*gridH)} - // Re-spawn if apple lands on snake - var i = 0; - for (i=0; i= gridW) pos.x = 0 - if (pos.y < 0) pos.y = gridH - 1 - if (pos.y >= gridH) pos.y = 0 -} - -resetGame() - -var update = function(dt) { - if (gameState != "playing") return - moveTimer += dt - if (moveTimer < moveInterval) return - moveTimer -= moveInterval - - // Update direction - direction = {x: nextDirection.x, y: nextDirection.y} - - // New head - var head = {x: snake[0].x + direction.x, y: snake[0].y + direction.y} - wrap(head) - - // Check collision with body - var i = 0; - for (i=0; i= gridW) hx = 0 + if (hy < 0) hy = gridH - 1 + if (hy >= gridH) hy = 0 + + // Self collision + var i = 0 + for (i = 0; i < length(snake_pos); i++) { + if (snake_pos[i].x == hx && snake_pos[i].y == hy) { + state = 'gameover' + gameover_label = text2d({ + text: "GAME OVER — SPACE to restart", + pos: {x: GW / 2 - 170, y: GH / 2}, + plane: 'hud', size: 20, color: {r: 1, g: 0.3, b: 0.3, a: 1} + }) + return + } + } + + // Add head with tween from old head position + var old_wp = grid_to_world(snake_pos[0].x, snake_pos[0].y) + var new_wp = grid_to_world(hx, hy) + snake_pos.unshift({x: hx, y: hy}) + var head = shape.rect({ + pos: {x: old_wp.x, y: old_wp.y}, width: cellSize - 2, height: cellSize - 2, + fill: {r: 0, g: 1, b: 0.3, a: 1}, plane: 'game' + }) + // Smooth tween from old position to new position + tw.tween(head.pos).to({x: new_wp.x, y: new_wp.y}, move_interval).ease(ease.linear) + snake_shapes.unshift(head) + + // Eat apple? + if (hx == apple_gx && hy == apple_gy) { + score++ + score_label.text = "Score: " + score + spawn_apple() + } else { + // Remove tail + var tail = pop(snake_shapes) + tail.destroy() + pop(snake_pos) + } + }, + + render: function() { + return compositor.execute(compositor.compile(comp_config)) + } +}) diff --git a/examples/tetris/main.ce b/examples/tetris/main.ce index e77acd63..e3c39ac0 100644 --- a/examples/tetris/main.ce +++ b/examples/tetris/main.ce @@ -1,123 +1,151 @@ -var draw = use('draw2d') +var core = use('core') +var camera = use('camera') +var compositor = use('compositor') var input = use('input') -var config = use('config') -var color = use('color') +var shape = use('shape2d') +var text2d = use('text2d') var random = use('random') -prosperon.camera.transform.pos = [0,0] - -// Board constants var COLS = 10, ROWS = 20 -var TILE = 6 // each cell is 6x6 +var TILE = 8 +var GW = COLS * TILE + 80 // extra space for next piece + score +var GH = ROWS * TILE -// Board storage (2D), each cell is either 0 or a [r,g,b,a] color -var board = [] +var game_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) +var hud_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) -// Gravity timing -var baseGravity = 0.8 // seconds between drops at level 0 -var gravityTimer = 0 - -// Current piece & position -var piece = null -var pieceX = 0 -var pieceY = 0 - -// Next piece -var nextPiece = null - -// Score/lines/level -var score = 0 -var linesCleared = 0 -var level = 0 - -// Rotation lock to prevent spinning with W -var rotateHeld = false -var gameOver = false - -// Horizontal movement gating -var hMoveTimer = 0 -var hDelay = 0.2 // delay before repeated moves begin -var hRepeat = 0.05 // time between repeated moves -var prevLeft = false -var prevRight = false +// Action mapping with possession for discrete inputs +input.configure({ + action_map: { + left: ['a', 'left'], + right: ['d', 'right'], + rotate: ['w', 'up'], + soft_drop: ['s', 'down'], + hard_drop: ['space'] + } +}) // Tetrimino definitions var SHAPES = { - I: { color:[0,1,1,1], blocks:[[0,0],[1,0],[2,0],[3,0]] }, - O: { color:[1,1,0,1], blocks:[[0,0],[1,0],[0,1],[1,1]] }, - T: { color:[1,0,1,1], blocks:[[0,0],[1,0],[2,0],[1,1]] }, - S: { color:[0,1,0,1], blocks:[[1,0],[2,0],[0,1],[1,1]] }, - Z: { color:[1,0,0,1], blocks:[[0,0],[1,0],[1,1],[2,1]] }, - J: { color:[0,0,1,1], blocks:[[0,0],[0,1],[1,1],[2,1]] }, - L: { color:[1,0.5,0,1], blocks:[[2,0],[0,1],[1,1],[2,1]] } + I: {color: {r: 0, g: 1, b: 1, a: 1}, blocks: [[0, 0], [1, 0], [2, 0], [3, 0]]}, + O: {color: {r: 1, g: 1, b: 0, a: 1}, blocks: [[0, 0], [1, 0], [0, 1], [1, 1]]}, + T: {color: {r: 1, g: 0, b: 1, a: 1}, blocks: [[0, 0], [1, 0], [2, 0], [1, 1]]}, + S: {color: {r: 0, g: 1, b: 0, a: 1}, blocks: [[1, 0], [2, 0], [0, 1], [1, 1]]}, + Z: {color: {r: 1, g: 0, b: 0, a: 1}, blocks: [[0, 0], [1, 0], [1, 1], [2, 1]]}, + J: {color: {r: 0, g: 0, b: 1, a: 1}, blocks: [[0, 0], [0, 1], [1, 1], [2, 1]]}, + L: {color: {r: 1, g: 0.5, b: 0, a: 1}, blocks: [[2, 0], [0, 1], [1, 1], [2, 1]]} } var shapeKeys = array(SHAPES) -// Initialize board with empty (0) -function initBoard() { +// Board: 2D array, null or color object +var board = [] +// Board shapes: one shape per cell, toggled visible +var board_shapes = [] + +function init_board() { board = [] - var r = 0; - var row = null; - var c = 0; - for (r=0; r [b[0], b[1]]) + blocks: array(SHAPES[key].blocks, function(b) { return [b[0], b[1]] }) } } -function spawnPiece() { - piece = nextPiece || randomShape() - nextPiece = randomShape() - pieceX = 3 - pieceY = 0 - // Collision on spawn => game over - if (collides(pieceX, pieceY, piece.blocks)) gameOver = true -} - function collides(px, py, blocks) { - var i = 0; - var x = 0; - var y = 0; - for (i=0; i=COLS || y<0 || y>=ROWS) return true - if (y>=0 && board[y][x]) return true + if (x < 0 || x >= COLS || y < 0 || y >= ROWS) return true + if (y >= 0 && board[y][x]) return true } return false } -// Lock piece into board -function lockPiece() { - var i = 0; - var x = 0; - var y = 0; - for (i=0; i=0) board[y][x] = piece.color + if (y >= 0 && y < ROWS && x >= 0 && x < COLS) { + board[y][x] = piece.color + board_shapes[y][x].fill = piece.color + board_shapes[y][x].visible = true + } } } -// Rotate 90° clockwise -function rotate(blocks) { - // (x,y) => (y,-x) - var i = 0; - var x = 0; - var y = 0; - for (i=0; i=0;) { - if (every(board[r], cell => cell)) { + var r = ROWS - 1, c = 0, full = false, newRow = null + while (r >= 0) { + full = true + for (c = 0; c < COLS; c++) { + if (!board[r][c]) { full = false; break } + } + if (full) { lines++ - // remove row - board = array(array(board, 0, r), array(board, r+1)) - // add empty row on top - newRow = [] - for (c=0; c 0) { + for (c = 0; c < COLS; c++) { + board[sr][c] = board[sr - 1][c] + if (board[sr][c]) { + board_shapes[sr][c].fill = board[sr][c] + board_shapes[sr][c].visible = true + } else { + board_shapes[sr][c].visible = false + } + } + sr-- + } + for (c = 0; c < COLS; c++) { + board[0][c] = null + board_shapes[0][c].visible = false + } } else { r-- } } - // Score - if (lines==1) score += 100 - else if (lines==2) score += 300 - else if (lines==3) score += 500 - else if (lines==4) score += 800 + if (lines == 1) score += 100 + else if (lines == 2) score += 300 + else if (lines == 3) score += 500 + else if (lines == 4) score += 800 linesCleared += lines - level = floor(linesCleared/10) + level = floor(linesCleared / 10) + score_label.text = "Score: " + score + level_label.text = "Level: " + level } -function placePiece() { - lockPiece() - clearLines() - spawnPiece() +function update_piece_shapes() { + var i = 0 + for (i = 0; i < 4; i++) { + var bx = pieceX + piece.blocks[i][0] + var by = pieceY + piece.blocks[i][1] + piece_shapes[i].pos.x = bx * TILE + TILE / 2 + piece_shapes[i].pos.y = by * TILE + TILE / 2 + piece_shapes[i].fill = piece.color + piece_shapes[i].visible = true + } } -// Hard drop -function hardDrop() { - while(!collides(pieceX, pieceY+1, piece.blocks)) pieceY++ - placePiece() +function update_next_display() { + var i = 0 + for (i = 0; i < 4; i++) { + var bx = nextPiece.blocks[i][0] + var by = nextPiece.blocks[i][1] + next_shapes[i].pos.x = (COLS + 1) * TILE + bx * TILE + TILE / 2 + next_shapes[i].pos.y = 20 + by * TILE + TILE / 2 + next_shapes[i].fill = nextPiece.color + next_shapes[i].visible = true + } } -spawnPiece() - -var update = function(dt) { - if (gameOver) return - - // ======= Horizontal Movement Gate ======= - var leftPressed = input.keyboard.down('a') - var rightPressed = input.keyboard.down('d') - var horizontalMove = 0 - - // If user just pressed A, move once & start gating - if (leftPressed && !prevLeft) { - horizontalMove = -1 - hMoveTimer = hDelay - } - // If user is holding A & the timer is up, move again, then reset timer to repeat - else if (leftPressed && hMoveTimer <= 0) { - horizontalMove = -1 - hMoveTimer = hRepeat +function spawn_piece() { + piece = nextPiece || random_shape() + nextPiece = random_shape() + pieceX = 3 + pieceY = 0 + if (collides(pieceX, pieceY, piece.blocks)) { + gameOver = true + var i = 0 + for (i = 0; i < 4; i++) piece_shapes[i].visible = false + return } + update_piece_shapes() + update_next_display() +} - // Same logic for D - if (rightPressed && !prevRight) { - horizontalMove = 1 - hMoveTimer = hDelay - } else if (rightPressed && hMoveTimer <= 0) { - horizontalMove = 1 - hMoveTimer = hRepeat - } +function place_piece() { + lock_piece() + clear_lines() + spawn_piece() +} - // Move horizontally if it doesn't collide - if (horizontalMove < 0 && !collides(pieceX-1, pieceY, piece.blocks)) pieceX-- - else if (horizontalMove > 0 && !collides(pieceX+1, pieceY, piece.blocks)) pieceX++ +// Create 4 shapes for active piece (layer 1 = on top of board) +var pi = 0 +for (pi = 0; pi < 4; pi++) { + push(piece_shapes, shape.rect({ + pos: {x: 0, y: 0}, width: TILE - 1, height: TILE - 1, + fill: {r: 1, g: 1, b: 1, a: 1}, plane: 'game', layer: 1, visible: false + })) +} - // If neither A nor D is pressed, reset the timer so next press is immediate - if (!leftPressed && !rightPressed) { - hMoveTimer = 0 - } +init_board() +spawn_piece() - // Decrement horizontal timer - hMoveTimer -= dt - prevLeft = leftPressed - prevRight = rightPressed - // ======= End Horizontal Movement Gate ======= +// Compositor +var comp_config = { + clear: {r: 0, g: 0, b: 0, a: 1}, + planes: [ + {name: 'game', camera: game_cam, resolution: {width: GW, height: GH}, presentation: 'letterbox'}, + {name: 'hud', camera: hud_cam, resolution: {width: GW, height: GH}, presentation: 'stretch'} + ] +} - // Rotate with W (once per press, no spinning) - var test = null; - if (input.keyboard.down('w')) { - if (!rotateHeld) { - rotateHeld = true - test = array(piece.blocks, b => [b[0], b[1]]) - rotate(test) - if (!collides(pieceX, pieceY, test)) piece.blocks = test - } - } else { - rotateHeld = false - } +core.start({ + width: 640, height: 480, title: "Tetris", - // Soft drop if S is held (accelerates gravity) - var fallSpeed = input.keyboard.down('s') ? 10 : 1 + update: function(dt) { + if (gameOver) return - // Gravity - gravityTimer += dt * fallSpeed - var dropInterval = max(0.1, baseGravity - level*0.05) - if (gravityTimer >= dropInterval) { - gravityTimer = 0 - if (!collides(pieceX, pieceY+1, piece.blocks)) { - pieceY++ + var down = input.player1().down() + var test = null + + // Horizontal movement with DAS + var wantLeft = down.left + var wantRight = down.right + var newDir = 0 + if (wantLeft && !wantRight) newDir = -1 + else if (wantRight && !wantLeft) newDir = 1 + + if (newDir != 0) { + if (newDir != hDir || !hHeld) { + // First press + if (!collides(pieceX + newDir, pieceY, piece.blocks)) pieceX += newDir + hMoveTimer = hDelay + hDir = newDir + hHeld = true + } else { + hMoveTimer -= dt + if (hMoveTimer <= 0) { + if (!collides(pieceX + newDir, pieceY, piece.blocks)) pieceX += newDir + hMoveTimer = hRepeat + } + } } else { - placePiece() + hDir = 0 + hHeld = false + hMoveTimer = 0 } - } - // Hard drop if space is held - if (input.keyboard.down('space')) { -// hardDrop() - } -} - -var hud = function() { - // Clear screen - draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1]) - - // Draw board - var r = 0; - var c = 0; - var cell = null; - var i = 0; - var x = 0; - var y = 0; - var nx = 0; - var ny = 0; - var dx = 0; - var dy = 0; - for (r=0; r= dropInterval) { + gravityTimer = 0 + if (!collides(pieceX, pieceY + 1, piece.blocks)) { + pieceY++ + } else { + place_piece() + } } - } - // Score & Level - var info = "Score: " + score + "\nLines: " + linesCleared + "\nLevel: " + level - draw.text(info, {x:70, y:30, width:90, height:50}, null, 0, color.white) + update_piece_shapes() + }, - if (gameOver) { - draw.text("GAME OVER", {x:10, y:config.height*0.5-5, width:config.width-20, height:20}, null, 0, color.red) + render: function() { + return compositor.execute(compositor.compile(comp_config)) } -} +}) diff --git a/film2d.cm b/film2d.cm index 7e039add..3e060e6a 100644 --- a/film2d.cm +++ b/film2d.cm @@ -523,4 +523,32 @@ function _mat_eq(a, b) { return a.blend == b.blend && a.sampler == b.sampler } +film2d.snapshot = function() { + var result = [] + var ids = array(registry) + var i = 0 + var d = null + for (i = 0; i < length(ids); i++) { + d = registry[ids[i]] + if (!d) continue + push(result, { + id: ids[i], + type: d.type, + pos: d.pos ? {x: d.pos.x, y: d.pos.y} : null, + width: d.width, + height: d.height, + plane: d.plane || 'default', + layer: d.layer || 0, + groups: d.groups || [], + visible: d.visible != false, + fill: d.fill, + color: d.color, + opacity: d.opacity, + image: d.image ? (is_text(d.image) ? d.image : '(texture)') : null, + text: d.text + }) + } + return result +} + return film2d \ No newline at end of file diff --git a/graphics.cm b/graphics.cm index 02d6661a..8a2a2a76 100644 --- a/graphics.cm +++ b/graphics.cm @@ -466,4 +466,25 @@ graphics.queue_sprite_mesh = function(queue) { return [mesh.pos, mesh.uv, mesh.color, mesh.indices] } +graphics.snapshot = function() { + var images = [] + var fonts = [] + arrfor(array(cache), function(k) { + var entry = cache[k] + var info = {name: k} + if (entry && entry.width) { + info.width = entry.width + info.height = entry.height + } else if (entry && entry.frames) { + info.frame_count = length(entry.frames) + info.type = 'animation' + } + push(images, info) + }) + arrfor(array(fontcache), function(k) { + push(fonts, k) + }) + return {images: images, fonts: fonts} +} + return graphics diff --git a/input.cm b/input.cm index 1a2ae0fb..fdaec932 100644 --- a/input.cm +++ b/input.cm @@ -260,16 +260,43 @@ function user(index) { return _users[index] } +function snapshot() { + var users = [] + var i = 0 + var u = null + var target = null + for (i = 0; i < length(_users); i++) { + u = _users[i] + target = u.target() + push(users, { + index: u.index, + device_kind: u.device_kind(), + active_device: u.active_device, + paired_devices: array(u.paired_devices), + down: u.down(), + control_stack_depth: length(u.control_stack), + target: target ? (target.name || '(entity)') : null + }) + } + return { + max_users: _config.max_users, + pairing: _config.pairing, + action_map: _config.action_map, + users: users + } +} + return { configure: configure, ingest: ingest, user: user, - + snapshot: snapshot, + player1() { return _users[0] }, player2() { return _users[1] }, player3() { return _users[2] }, player4() { return _users[3] }, - + // Re-export for convenience devices: devices, backend: backend diff --git a/snapshot.cm b/snapshot.cm new file mode 100644 index 00000000..5046f3db --- /dev/null +++ b/snapshot.cm @@ -0,0 +1,45 @@ +var film2d = use('film2d') +var world = use('world') +var compositor = use('compositor') +var input = use('input') +var tween = use('tween') +var graphics = use('graphics') + +var snapshot = {} + +snapshot.drawables = function() { + return film2d.snapshot() +} + +snapshot.entities = function() { + return world.snapshot() +} + +snapshot.compositor = function() { + return compositor.snapshot() +} + +snapshot.input = function() { + return input.snapshot() +} + +snapshot.tweens = function() { + return tween.snapshot() +} + +snapshot.assets = function() { + return graphics.snapshot() +} + +snapshot.all = function() { + return { + drawables: film2d.snapshot(), + entities: world.snapshot(), + compositor: compositor.snapshot(), + input: input.snapshot(), + tweens: tween.snapshot(), + assets: graphics.snapshot() + } +} + +return snapshot diff --git a/tween.cm b/tween.cm index ba0eb696..8d995a40 100644 --- a/tween.cm +++ b/tween.cm @@ -254,11 +254,26 @@ $delay(() => { } }, 0) +function snapshot() { + var result = [] + arrfor(TweenEngine.tweens, function(tw) { + push(result, { + startVals: tw.startVals, + endVals: tw.endVals, + duration: tw.duration, + startTime: tw.startTime, + easing: tw.easing ? (tw.easing.name || 'unknown') : 'none' + }) + }) + return {active_count: length(TweenEngine.tweens), tweens: result} +} + var tween_ret = { init, Timeline, TweenEngine, - tween + tween, + snapshot } return tween_ret diff --git a/world.cm b/world.cm index 4d85a182..22999be4 100644 --- a/world.cm +++ b/world.cm @@ -16,4 +16,66 @@ world.destroy_entity = function(entity) { delete entities[entity] } +world.update = function(dt) { + arrfor(array(entities), function(e) { + if (is_function(e.update)) + e.update(dt) + }) +} + +world.each = function(fn) { + arrfor(array(entities), fn) +} + +world.query = function(filter_fn) { + return filter(array(entities), filter_fn) +} + +world.find = function(predicate) { + var keys = array(entities) + var i = 0 + for (i = 0; i < length(keys); i++) { + if (predicate(keys[i])) + return keys[i] + } + return null +} + +world.clear = function() { + arrfor(array(entities), function(e) { + if (is_function(e.on_destroy)) + e.on_destroy() + }) + entities = {} +} + +world.load_level = function(json_array) { + var i = 0 + var entry = null + var proto = null + var entity = null + for (i = 0; i < length(json_array); i++) { + entry = json_array[i] + proto = entry.script ? use(entry.script) : {} + entity = world.add_entity(proto, entry) + } +} + +world.count = function() { + return length(array(entities)) +} + +world.snapshot = function() { + var result = [] + arrfor(array(entities), function(e) { + var snap = {} + arrfor(array(e), function(k) { + if (!is_function(e[k])) + snap[k] = e[k] + }) + push(result, snap) + }) + return result +} + return world