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