initial add
121
camera.cm
Normal file
@@ -0,0 +1,121 @@
|
||||
var cam = {}
|
||||
|
||||
/*
|
||||
presentation can be one of
|
||||
letterbox
|
||||
overscan
|
||||
stretch
|
||||
... or simply 'null' for no presentation
|
||||
*/
|
||||
|
||||
var basecam = {
|
||||
pos: [0,0], // where it is
|
||||
ortho:true,
|
||||
width: 100,
|
||||
aspect_ratio: 16/9,
|
||||
fov:50,
|
||||
near_z:0,
|
||||
far_z:1000,
|
||||
anchor:[0.5,0.5],
|
||||
rotation:[0,0,0,1],
|
||||
presentation: "letterbox",
|
||||
background: {r:1,g:1,b:1,a:0},
|
||||
viewport: {x:0,y:0,width:1,height:1},
|
||||
}
|
||||
|
||||
basecam.draw_rect = function(size)
|
||||
{
|
||||
var mode = this.presentation || "letterbox"
|
||||
var vp = {
|
||||
x:this.viewport.x,
|
||||
y:1-this.viewport.y-this.viewport.height,
|
||||
width:this.viewport.width,
|
||||
height:this.viewport.height
|
||||
}
|
||||
var src_rect = {x:0,y:0,width:this.size.x,height:this.size.y}
|
||||
var dst_rect = {x:vp.x*size.x,y:vp.y*size.y,width:vp.width*size.x,height:vp.height*size.y};
|
||||
return mode_rect(src_rect,dst_rect,mode);
|
||||
}
|
||||
|
||||
basecam.screen2camera = function(pos)
|
||||
{
|
||||
var draw_rect = this.draw_rect(prosperon.window.size);
|
||||
var ret = [pos.x-draw_rect.x, pos.y - draw_rect.y];
|
||||
ret.x /= draw_rect.width;
|
||||
ret.y /= draw_rect.height;
|
||||
ret.y = 1 - ret.y;
|
||||
return ret;
|
||||
}
|
||||
|
||||
basecam.screen2hud = function(pos)
|
||||
{
|
||||
var cam = this.screen2camera(pos);
|
||||
cam.x *= this.size.x;
|
||||
cam.y *= this.size.y;
|
||||
return cam;
|
||||
}
|
||||
|
||||
basecam.screen2world = function(pos)
|
||||
{
|
||||
var hud = this.screen2hud(pos);
|
||||
hud.x += this.transform.pos.x - this.size.x/2;
|
||||
hud.y += this.transform.pos.y - this.size.y/2;
|
||||
return hud;
|
||||
}
|
||||
|
||||
function mode_rect(src,dst,mode = "stretch")
|
||||
{
|
||||
var aspect_src = src.width/src.height;
|
||||
var aspect_dst = dst.width/dst.height;
|
||||
var out = {
|
||||
x:dst.x,
|
||||
y:dst.y,
|
||||
width:dst.width,
|
||||
height:dst.height
|
||||
};
|
||||
if (mode == "stretch") return out;
|
||||
|
||||
if (mode == "letterbox") {
|
||||
if (aspect_src > aspect_dst) {
|
||||
var scaled_h = out.width/aspect_src;
|
||||
var off = (out.height - scaled_h) * 0.5;
|
||||
out.y += off;
|
||||
out.height = scaled_h;
|
||||
} else {
|
||||
var scaled_w =out.height * aspect_src;
|
||||
var off = (out.width - scaled_w) * 0.5;
|
||||
out.x += off;
|
||||
out.width = scaled_w;
|
||||
}
|
||||
} else if (mode == "overscan"){
|
||||
if (aspect_src > aspect_dst) {
|
||||
var scaled_w = out.height * aspect_src;
|
||||
var off = (out.width - scaled_w) * 0.5;
|
||||
out.x += off;
|
||||
out.width = scaled_w;
|
||||
} else {
|
||||
var scaled_h = out.width / aspect_src;
|
||||
var off = (out.height - scaled_h) * 0.5;
|
||||
out.y += off;
|
||||
out.height = scaled_h;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
cam.make = function()
|
||||
{
|
||||
var c = Object.create(basecam)
|
||||
c.transform = new transform
|
||||
c.transform.unit()
|
||||
c.size = [640,360]
|
||||
c.mode = 'keep'
|
||||
c.viewport = {x:0,y:0,width:1,height:1}
|
||||
c.fov = 45
|
||||
c.type = 'ortho'
|
||||
c.ortho = true
|
||||
c.aspect = 16/9
|
||||
return c
|
||||
}
|
||||
|
||||
return cam
|
||||
410
clay.cm
Normal file
@@ -0,0 +1,410 @@
|
||||
// Layout code
|
||||
// Contain is for how it will treat its children. If they should be laid out as a row, or column, or in a flex style, etc.
|
||||
|
||||
var layout = use('layout')
|
||||
var geometry = use('geometry')
|
||||
var draw = use('prosperon/draw2d')
|
||||
var graphics = use('graphics')
|
||||
var util = use('util')
|
||||
var input = use('input')
|
||||
var prosperon = use('prosperon')
|
||||
|
||||
var CHILDREN = Symbol('children')
|
||||
var PARENT = Symbol('parent')
|
||||
|
||||
function normalizeSpacing(spacing) {
|
||||
if (typeof spacing == 'number') {
|
||||
return {l: spacing, r: spacing, t: spacing, b: spacing}
|
||||
} else if (Array.isArray(spacing)) {
|
||||
if (spacing.length == 2) {
|
||||
return {l: spacing[0], r: spacing[0], t: spacing[1], b: spacing[1]}
|
||||
} else if (spacing.length == 4) {
|
||||
return {l: spacing[0], r: spacing[1], t: spacing[2], b: spacing[3]}
|
||||
}
|
||||
} else if (typeof spacing == 'object') {
|
||||
return {l: spacing.l || 0, r: spacing.r || 0, t: spacing.t || 0, b: spacing.b || 0}
|
||||
} else {
|
||||
return {l:0, r:0, t:0, b:0}
|
||||
}
|
||||
}
|
||||
|
||||
var lay_ctx = layout.make_context();
|
||||
|
||||
var clay_base = {
|
||||
font: null,
|
||||
background_image: null,
|
||||
slice: 0,
|
||||
font: 'smalle.16',
|
||||
font_size: null,
|
||||
color: {r:1,g:1,b:1,a:1},
|
||||
spacing:0,
|
||||
padding:0,
|
||||
margin:0,
|
||||
offset:{x:0, y:0},
|
||||
size:null,
|
||||
background_color: null,
|
||||
clipped: false,
|
||||
text_break: 'word',
|
||||
text_align: 'left',
|
||||
max_size: null, // {width: null, height: null}
|
||||
};
|
||||
|
||||
var root_item;
|
||||
var root_config;
|
||||
var tree_root;
|
||||
var clay = {}
|
||||
clay.CHILDREN = CHILDREN
|
||||
clay.PARENT = PARENT
|
||||
|
||||
var focused_textbox = null
|
||||
|
||||
clay.behave = layout.behave;
|
||||
clay.contain = layout.contain;
|
||||
|
||||
clay.draw = function draw(fn, size = [prosperon.camera.width, prosperon.camera.height])
|
||||
{
|
||||
lay_ctx.reset();
|
||||
var root = lay_ctx.item();
|
||||
// Accept both array and object formats
|
||||
if (Array.isArray(size)) {
|
||||
size = {width: size[0], height: size[1]};
|
||||
}
|
||||
lay_ctx.set_size(root,size);
|
||||
lay_ctx.set_contain(root,layout.contain.row);
|
||||
root_item = root;
|
||||
root_config = Object.assign({}, clay_base);
|
||||
tree_root = {
|
||||
id: root,
|
||||
config: root_config,
|
||||
};
|
||||
tree_root[CHILDREN] = [];
|
||||
tree_root[PARENT] = null;
|
||||
fn()
|
||||
lay_ctx.run();
|
||||
|
||||
// Adjust bounding boxes for padding - traverse tree instead of array
|
||||
function adjust_bounding_boxes(node) {
|
||||
node.content = lay_ctx.get_rect(node.id);
|
||||
node.boundingbox = Object.assign({}, node.content);
|
||||
|
||||
var padding = normalizeSpacing(node.config.padding || 0);
|
||||
|
||||
node.boundingbox.x -= padding.l;
|
||||
node.boundingbox.y -= padding.t;
|
||||
node.boundingbox.width += padding.l + padding.r;
|
||||
node.boundingbox.height += padding.t + padding.b;
|
||||
|
||||
node.marginbox = Object.assign({}, node.content);
|
||||
var margin = normalizeSpacing(node.config.margin || 0);
|
||||
node.marginbox.x -= margin.l;
|
||||
node.marginbox.y -= margin.t;
|
||||
node.marginbox.width += margin.l+margin.r;
|
||||
node.marginbox.height += margin.t+margin.b;
|
||||
|
||||
// Apply max_size clamping post-layout
|
||||
if (node.config.max_size) {
|
||||
if (node.config.max_size.width != null) {
|
||||
// Clamp the layout rect size
|
||||
var rect = lay_ctx.get_rect(node.id);
|
||||
rect.width = Math.min(rect.width, node.config.max_size.width);
|
||||
// Also clamp bounding box
|
||||
node.content.width = Math.min(node.content.width, node.config.max_size.width);
|
||||
node.boundingbox.width = Math.min(node.boundingbox.width, node.config.max_size.width + padding.l + padding.r);
|
||||
node.marginbox.width = Math.min(node.marginbox.width, node.config.max_size.width + padding.l + padding.r + margin.l + margin.r);
|
||||
}
|
||||
if (node.config.max_size.height != null) {
|
||||
// Clamp the layout rect size
|
||||
var rect = lay_ctx.get_rect(node.id);
|
||||
rect.height = Math.min(rect.height, node.config.max_size.height);
|
||||
// Also clamp bounding box
|
||||
node.content.height = Math.min(node.content.height, node.config.max_size.height);
|
||||
node.boundingbox.height = Math.min(node.boundingbox.height, node.config.max_size.height + padding.t + padding.b);
|
||||
node.marginbox.height = Math.min(node.marginbox.height, node.config.max_size.height + padding.t + padding.b + margin.t + margin.b);
|
||||
}
|
||||
}
|
||||
|
||||
node.content.y *= -1;
|
||||
node.content.y += size.height;
|
||||
node.boundingbox.y *= -1;
|
||||
node.boundingbox.y += size.height;
|
||||
node.content.anchor_y = 1;
|
||||
node.boundingbox.anchor_y = 1;
|
||||
|
||||
// Recursively adjust children
|
||||
if (node[CHILDREN]) {
|
||||
for (var child of node[CHILDREN]) {
|
||||
adjust_bounding_boxes(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adjust_bounding_boxes(tree_root);
|
||||
|
||||
return tree_root;
|
||||
}
|
||||
|
||||
function create_view_fn(base_config)
|
||||
{
|
||||
var base = Object.assign(Object.create(clay_base), base_config);
|
||||
return function view(config = {}, fn) {
|
||||
config.__proto__ = base;
|
||||
var item = add_item(config);
|
||||
|
||||
var prev_item = root_item;
|
||||
var prev_config = root_config;
|
||||
var prev_tree_root = tree_root;
|
||||
root_item = item;
|
||||
root_config = config;
|
||||
root_config._childIndex = 0; // Initialize child index
|
||||
// Find the tree node for this item and set it as current tree_root
|
||||
if (prev_tree_root[CHILDREN] && prev_tree_root[CHILDREN].length > 0) {
|
||||
tree_root = prev_tree_root[CHILDREN][prev_tree_root[CHILDREN].length - 1];
|
||||
} else {
|
||||
// If no children yet, this shouldn't happen, but handle gracefully
|
||||
tree_root = prev_tree_root;
|
||||
}
|
||||
fn?.();
|
||||
root_item = prev_item;
|
||||
root_config = prev_config;
|
||||
tree_root = prev_tree_root;
|
||||
}
|
||||
}
|
||||
|
||||
clay.vstack = create_view_fn({
|
||||
contain: layout.contain.column | layout.contain.start,
|
||||
});
|
||||
|
||||
clay.hstack = create_view_fn({
|
||||
contain: layout.contain.row | layout.contain.start,
|
||||
});
|
||||
|
||||
clay.spacer = create_view_fn({
|
||||
behave: layout.behave.hfill | layout.behave.vfill
|
||||
});
|
||||
|
||||
clay.frame = create_view_fn({});
|
||||
|
||||
function image_size(img)
|
||||
{
|
||||
return [img.width * (img.rect?.width || 1), img.height * (img.rect?.height || 1)];
|
||||
}
|
||||
|
||||
function add_item(config)
|
||||
{
|
||||
// Normalize the child's margin
|
||||
var margin = normalizeSpacing(config.margin || 0);
|
||||
var padding = normalizeSpacing(config.padding || 0);
|
||||
var childGap = root_config.child_gap || 0;
|
||||
|
||||
// Adjust for child_gap
|
||||
root_config._childIndex ??= 0
|
||||
if (root_config._childIndex > 0) {
|
||||
var parentContain = root_config.contain || 0;
|
||||
var directionMask = layout.contain.row | layout.contain.column;
|
||||
var direction = parentContain & directionMask;
|
||||
var isVStack = direction == layout.contain.column;
|
||||
var isHStack = direction == layout.contain.row;
|
||||
|
||||
if (isVStack) {
|
||||
margin.t += childGap;
|
||||
} else if (isHStack) {
|
||||
margin.l += childGap;
|
||||
}
|
||||
}
|
||||
|
||||
var use_config = Object.create(config);
|
||||
|
||||
use_config.margin = {
|
||||
t: margin.t+padding.t,
|
||||
b: margin.b+padding.b,
|
||||
r:margin.r+padding.r,
|
||||
l:margin.l+padding.l
|
||||
};
|
||||
|
||||
var item = lay_ctx.item();
|
||||
lay_ctx.set_margins(item, use_config.margin);
|
||||
use_config.size ??= {width:0, height:0}
|
||||
// Convert array to object if needed
|
||||
if (Array.isArray(use_config.size)) {
|
||||
use_config.size = {width: use_config.size[0], height: use_config.size[1]};
|
||||
}
|
||||
// Apply max_size constraint - only clamp computed size, don't set explicit size pre-layout
|
||||
if (use_config.max_size) {
|
||||
// max_size should not force explicit sizing pre-layout - let children compute natural size,
|
||||
// then clamp the container after layout. For now, just ensure we don't set size to max_size.
|
||||
}
|
||||
lay_ctx.set_size(item,use_config.size);
|
||||
lay_ctx.set_contain(item,use_config.contain);
|
||||
lay_ctx.set_behave(item,use_config.behave);
|
||||
var tree_node = {
|
||||
id: item,
|
||||
config: use_config,
|
||||
};
|
||||
tree_node[CHILDREN] = [];
|
||||
tree_node[PARENT] = tree_root;
|
||||
tree_root[CHILDREN].push(tree_node);
|
||||
lay_ctx.insert(root_item,item);
|
||||
|
||||
// Increment the parent's child index
|
||||
root_config._childIndex++;
|
||||
return item;
|
||||
}
|
||||
|
||||
function rectify_configs(config_array)
|
||||
{
|
||||
if (config_array.length == 0)
|
||||
config_array = [{}];
|
||||
|
||||
for (var i = config_array.length-1; i > 0; i--)
|
||||
config_array[i].__proto__ = config_array[i-1];
|
||||
|
||||
config_array[0].__proto__ = clay_base;
|
||||
var cleanobj = Object.create(config_array[config_array.length-1]);
|
||||
|
||||
return cleanobj;
|
||||
}
|
||||
|
||||
clay.image = function image(path, ...configs)
|
||||
{
|
||||
var config = rectify_configs(configs);
|
||||
var image = graphics.texture(path);
|
||||
config.image = path; // Store the path string, not the texture object
|
||||
config.size ??= {width: image.width, height: image.height};
|
||||
add_item(config);
|
||||
}
|
||||
|
||||
clay.text = function text(str, ...configs)
|
||||
{
|
||||
var config = rectify_configs(configs);
|
||||
config.size ??= [0,0]
|
||||
config.font = graphics.get_font(config.font)
|
||||
config.text = str
|
||||
var tsize = config.font.text_size(str, 0, config.size[0], config.text_break, config.text_align);
|
||||
tsize.x = Math.ceil(tsize.x)
|
||||
tsize.y = Math.ceil(tsize.y)
|
||||
config.size = config.size.map((x,i) => Math.max(x, tsize[i]));
|
||||
add_item(config);
|
||||
}
|
||||
|
||||
/*
|
||||
For a given size,
|
||||
the layout engine should "see" size + margin
|
||||
but its interior content should "see" size - padding
|
||||
hence, the layout box should be size-padding, with margin of margin+padding
|
||||
*/
|
||||
|
||||
var button_base = Object.assign(Object.create(clay_base), {
|
||||
padding:0,
|
||||
hovered:{
|
||||
}
|
||||
});
|
||||
clay.button = function button(str, action, config = {})
|
||||
{
|
||||
config.__proto__ = button_base;
|
||||
config.font = graphics.get_font(config.font)
|
||||
config.size = config.font.text_size(str, 0, 0, config.text_break, config.text_align)
|
||||
add_item(config);
|
||||
config.text = str;
|
||||
config.action = action;
|
||||
}
|
||||
|
||||
var point = use('point')
|
||||
|
||||
clay.draw_commands = function draw_commands(tree_root, pos = {x:0,y:0})
|
||||
{
|
||||
function draw_node(node) {
|
||||
var config = node.config
|
||||
var boundingbox = geometry.rect_move(node.boundingbox,point.add(pos,config.offset))
|
||||
var content = geometry.rect_move(node.content,point.add(pos, config.offset))
|
||||
|
||||
if (config.background_image)
|
||||
if (config.slice)
|
||||
draw.slice9(config.background_image, boundingbox, config.slice, config.background_color)
|
||||
else
|
||||
draw.image(config.background_image, boundingbox, 0, config.color)
|
||||
else if (config.background_color)
|
||||
draw.rectangle(boundingbox, null, {color:config.background_color})
|
||||
|
||||
if (config.text) {
|
||||
var baseline_y = content.y + content.height - config.font.ascent
|
||||
draw.text(config.text, {x: content.x, y: baseline_y}, config.font, config.color, content.width)
|
||||
}
|
||||
|
||||
if (config.image)
|
||||
draw.image(config.image, content, 0, config.color)
|
||||
|
||||
if (config.clipped) {
|
||||
draw.scissor(content)
|
||||
}
|
||||
|
||||
// Recursively draw children
|
||||
if (node[CHILDREN]) {
|
||||
for (var child of node[CHILDREN]) {
|
||||
draw_node(child);
|
||||
}
|
||||
}
|
||||
if (config.clipped)
|
||||
draw.scissor(null)
|
||||
}
|
||||
|
||||
draw_node(tree_root);
|
||||
}
|
||||
|
||||
var dbg_colors = {};
|
||||
clay.debug_colors = dbg_colors;
|
||||
dbg_colors.content = [1,0,0,0.1];
|
||||
dbg_colors.boundingbox = [0,1,0,0,0.1];
|
||||
dbg_colors.margin = [0,0,1,0.1];
|
||||
clay.draw_debug = function draw_debug(tree_root, pos = {x:0, y:0})
|
||||
{
|
||||
function draw_debug_node(node) {
|
||||
var boundingbox = geometry.rect_move(node.boundingbox,pos);
|
||||
var content = geometry.rect_move(node.content,pos);
|
||||
draw.rectangle(content, null, {color:dbg_colors.content});
|
||||
draw.rectangle(boundingbox, null, {color:dbg_colors.boundingbox});
|
||||
// draw.rectangle(geometry.rect_move(node.marginbox,pos), dbg_colors.margin);
|
||||
|
||||
// Recursively draw debug for children
|
||||
if (node[CHILDREN]) {
|
||||
for (var child of node[CHILDREN]) {
|
||||
draw_debug_node(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw_debug_node(tree_root);
|
||||
}
|
||||
|
||||
clay.print_tree = function print_tree(tree_root, indent = 0) {
|
||||
var indent_str = ' '.repeat(indent)
|
||||
var node_type = 'unknown'
|
||||
|
||||
if (tree_root.config.text) {
|
||||
node_type = 'text'
|
||||
} else if (tree_root.config.image) {
|
||||
node_type = 'image'
|
||||
} else if (tree_root.config.contain) {
|
||||
if (tree_root.config.contain & layout.contain.column) {
|
||||
node_type = 'vstack'
|
||||
} else if (tree_root.config.contain & layout.contain.row) {
|
||||
node_type = 'hstack'
|
||||
} else {
|
||||
node_type = 'container'
|
||||
}
|
||||
} else {
|
||||
node_type = 'node'
|
||||
}
|
||||
|
||||
log.console(`${indent_str}${node_type} (id: ${tree_root.id})`)
|
||||
|
||||
if (tree_root[CHILDREN] && tree_root[CHILDREN].length > 0) {
|
||||
log.console(`${indent_str} children: ${tree_root[CHILDREN].length}`)
|
||||
for (var child of tree_root[CHILDREN]) {
|
||||
print_tree(child, indent + 1)
|
||||
}
|
||||
} else {
|
||||
log.console(`${indent_str} (no children)`)
|
||||
}
|
||||
}
|
||||
|
||||
return clay
|
||||
110
clay_input.cm
Normal file
@@ -0,0 +1,110 @@
|
||||
// clay_input.cm - Input handling for clay UI
|
||||
// Separates input concerns from layout/rendering
|
||||
|
||||
var geometry = use('geometry')
|
||||
var point = use('point')
|
||||
var clay = use('prosperon/clay')
|
||||
|
||||
var clay_input = {}
|
||||
|
||||
function rect_contains(node, pos) {
|
||||
var bb = geometry.rect_move(node.boundingbox, node.config.offset || {x: 0, y: 0})
|
||||
return geometry.rect_point_inside(bb, pos)
|
||||
}
|
||||
|
||||
function pointer_enabled(node) {
|
||||
var p = node.config.pointer_events
|
||||
if (!p || p == 'auto') return true
|
||||
if (p == 'none') return false
|
||||
return true
|
||||
}
|
||||
|
||||
function should_skip_children(node) {
|
||||
var p = node.config.pointer_events
|
||||
if (p == 'box-only') return true
|
||||
return false
|
||||
}
|
||||
|
||||
function should_skip_self(node) {
|
||||
var p = node.config.pointer_events
|
||||
if (p == 'box-none') return true
|
||||
return false
|
||||
}
|
||||
|
||||
function find_path(node, path, pos) {
|
||||
if (!pointer_enabled(node)) return null
|
||||
if (!rect_contains(node, pos)) return null
|
||||
|
||||
var next_path = path.concat(node)
|
||||
|
||||
if (node[clay.CHILDREN] && !should_skip_children(node)) {
|
||||
// Children drawn later should be tested first; reverse if your render order differs
|
||||
for (var i = node[clay.CHILDREN].length - 1; i >= 0; i--) {
|
||||
var child = node[clay.CHILDREN][i]
|
||||
var child_path = find_path(child, next_path, pos)
|
||||
if (child_path) return child_path
|
||||
}
|
||||
}
|
||||
|
||||
if (should_skip_self(node)) return null
|
||||
return next_path
|
||||
}
|
||||
|
||||
clay_input.deepest = function deepest(tree_root, pos) {
|
||||
var path = find_path(tree_root, [], pos) || []
|
||||
var deepest = path.length ? path[path.length - 1] : null
|
||||
return deepest
|
||||
}
|
||||
|
||||
clay_input.bubble = function bubble(deepest, prop) {
|
||||
var current = deepest
|
||||
while (current) {
|
||||
if (current.config && current.config[prop])
|
||||
return current
|
||||
current = current[clay.PARENT]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
clay_input.click = function click(tree_root, mousepos, button = 'left') {
|
||||
var deepest = clay_input.deepest(tree_root, mousepos)
|
||||
var action_target = clay_input.bubble(deepest, 'action')
|
||||
if (action_target && action_target.config.action) action_target.config.action()
|
||||
}
|
||||
|
||||
clay_input.get_actionable = function get_actionable(tree_root) {
|
||||
var actionable = []
|
||||
function walk(node) {
|
||||
if (node.config.action) actionable.push(node)
|
||||
if (node[clay.CHILDREN])
|
||||
for (var child of node[clay.CHILDREN]) walk(child)
|
||||
}
|
||||
walk(tree_root)
|
||||
return actionable
|
||||
}
|
||||
|
||||
clay_input.filter = function filter(tree_root, predicate) {
|
||||
var results = []
|
||||
function rec(node) {
|
||||
if (predicate(node)) results.push(node)
|
||||
if (node[clay.CHILDREN])
|
||||
for (var child of node[clay.CHILDREN]) rec(child)
|
||||
}
|
||||
rec(tree_root)
|
||||
return results
|
||||
}
|
||||
|
||||
clay_input.find_by_id = function find_by_id(tree_root, id) {
|
||||
function rec(node) {
|
||||
if (node.id == id) return node
|
||||
if (node[clay.CHILDREN])
|
||||
for (var child of node[clay.CHILDREN]) {
|
||||
var f = rec(child)
|
||||
if (f) return f
|
||||
}
|
||||
return null
|
||||
}
|
||||
return rec(tree_root)
|
||||
}
|
||||
|
||||
return clay_input
|
||||
216
color.cm
Normal file
@@ -0,0 +1,216 @@
|
||||
function tohex(n) {
|
||||
var s = Math.floor(n).toString(16);
|
||||
if (s.length == 1) s = "0" + s;
|
||||
return s.toUpperCase();
|
||||
};
|
||||
|
||||
var Color = {
|
||||
white: [1, 1, 1],
|
||||
black: [0, 0, 0],
|
||||
blue: [0, 0, 1],
|
||||
green: [0, 1, 0],
|
||||
yellow: [1, 1, 0],
|
||||
red: [1, 0, 0],
|
||||
gray: [0.71, 0.71, 0.71],
|
||||
cyan: [0, 1, 1],
|
||||
purple: [0.635, 0.365, 0.89],
|
||||
orange: [1, 0.565, 0.251],
|
||||
magenta: [1, 0, 1],
|
||||
};
|
||||
|
||||
Color.editor = {};
|
||||
Color.editor.ur = Color.green;
|
||||
|
||||
Color.tohtml = function (v) {
|
||||
var html = v.map(function (n) {
|
||||
return tohex(n * 255);
|
||||
});
|
||||
return "#" + html.join("");
|
||||
};
|
||||
|
||||
var esc = {};
|
||||
esc.reset = "\x1b[0";
|
||||
esc.color = function (v) {
|
||||
var c = v.map(function (n) {
|
||||
return Math.floor(n * 255);
|
||||
});
|
||||
var truecolor = "\x1b[38;2;" + c.join(";") + ";";
|
||||
return truecolor;
|
||||
};
|
||||
|
||||
esc.doc = "Functions and constants for ANSI escape sequences.";
|
||||
|
||||
Color.Arkanoid = {
|
||||
orange: [1, 0.561, 0],
|
||||
teal: [0, 1, 1],
|
||||
green: [0, 1, 0],
|
||||
red: [1, 0, 0],
|
||||
blue: [0, 0.439, 1],
|
||||
purple: [1, 0, 1],
|
||||
yellow: [1, 1, 0],
|
||||
silver: [0.616, 0.616, 0.616],
|
||||
gold: [0.737, 0.682, 0],
|
||||
};
|
||||
|
||||
Color.Arkanoid.Powerups = {
|
||||
red: [0.682, 0, 0] /* laser */,
|
||||
blue: [0, 0, 0.682] /* enlarge */,
|
||||
green: [0, 0.682, 0] /* catch */,
|
||||
orange: [0.878, 0.561, 0] /* slow */,
|
||||
purple: [0.824, 0, 0.824] /* break */,
|
||||
cyan: [0, 0.682, 1] /* disruption */,
|
||||
gray: [0.561, 0.561, 0.561] /* 1up */,
|
||||
};
|
||||
|
||||
Color.Gameboy = {
|
||||
darkest: [0.898, 0.42, 0.102],
|
||||
dark: [0.898, 0.741, 0.102],
|
||||
light: [0.741, 0.898, 0.102],
|
||||
lightest: [0.42, 0.898, 0.102],
|
||||
};
|
||||
|
||||
Color.Apple = {
|
||||
green: [0.369, 0.741, 0.243],
|
||||
yellow: [1, 0.725, 0],
|
||||
orange: [0.969, 0.51, 0],
|
||||
red: [0.886, 0.22, 0.22],
|
||||
purple: [0.592, 0.224, 0.6],
|
||||
blue: [0, 0.612, 0.875],
|
||||
};
|
||||
|
||||
Color.Debug = {
|
||||
boundingbox: Color.white,
|
||||
names: [0.329, 0.431, 1],
|
||||
};
|
||||
|
||||
Color.Editor = {
|
||||
grid: [0.388, 1, 0.502],
|
||||
select: [1, 1, 0.216],
|
||||
newgroup: [0.471, 1, 0.039],
|
||||
};
|
||||
|
||||
/* Detects the format of all colors and munges them into a floating point format */
|
||||
Color.normalize = function (c) {
|
||||
var add_a = function (a) {
|
||||
var n = this.slice();
|
||||
n[3] = a;
|
||||
return n;
|
||||
};
|
||||
|
||||
for (var p of Object.keys(c)) {
|
||||
if (typeof c[p] != "object") continue;
|
||||
if (!Array.isArray(c[p])) {
|
||||
Color.normalize(c[p]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add alpha channel if not present
|
||||
if (c[p].length == 3) {
|
||||
c[p][3] = 1;
|
||||
}
|
||||
|
||||
// Check if any values are > 1 (meaning they're in 0-255 format)
|
||||
var needs_conversion = false;
|
||||
for (var color of c[p]) {
|
||||
if (color > 1) {
|
||||
needs_conversion = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert from 0-255 to 0-1 if needed
|
||||
if (needs_conversion) {
|
||||
c[p] = c[p].map(function (x) {
|
||||
return x / 255;
|
||||
});
|
||||
}
|
||||
|
||||
c[p].alpha = add_a;
|
||||
}
|
||||
};
|
||||
|
||||
Color.normalize(Color);
|
||||
|
||||
var ColorMap = {};
|
||||
ColorMap.makemap = function (map) {
|
||||
var newmap = Object.create(ColorMap);
|
||||
Object.assign(newmap, map);
|
||||
return newmap;
|
||||
};
|
||||
ColorMap.Jet = ColorMap.makemap({
|
||||
0: [0, 0, 0.514],
|
||||
0.125: [0, 0.235, 0.667],
|
||||
0.375: [0.02, 1, 1],
|
||||
0.625: [1, 1, 0],
|
||||
0.875: [0.98, 0, 0],
|
||||
1: [0.502, 0, 0],
|
||||
});
|
||||
|
||||
ColorMap.BlueRed = ColorMap.makemap({
|
||||
0: [0, 0, 1],
|
||||
1: [1, 0, 0],
|
||||
});
|
||||
|
||||
ColorMap.Inferno = ColorMap.makemap({
|
||||
0: [0, 0, 0.016],
|
||||
0.13: [0.122, 0.047, 0.282],
|
||||
0.25: [0.333, 0.059, 0.427],
|
||||
0.38: [0.533, 0.133, 0.416],
|
||||
0.5: [0.729, 0.212, 0.333],
|
||||
0.63: [0.89, 0.349, 0.2],
|
||||
0.75: [0.976, 0.549, 0.039],
|
||||
0.88: [0.976, 0.788, 0.196],
|
||||
1: [0.988, 1, 0.643],
|
||||
});
|
||||
|
||||
ColorMap.Bathymetry = ColorMap.makemap({
|
||||
0: [0.157, 0.102, 0.173],
|
||||
0.13: [0.233, 0.192, 0.353],
|
||||
0.25: [0.251, 0.298, 0.545],
|
||||
0.38: [0.247, 0.431, 0.592],
|
||||
0.5: [0.282, 0.557, 0.62],
|
||||
0.63: [0.333, 0.682, 0.639],
|
||||
0.75: [0.471, 0.808, 0.639],
|
||||
0.88: [0.733, 0.902, 0.675],
|
||||
1: [0.992, 0.996, 0.8],
|
||||
});
|
||||
|
||||
ColorMap.Viridis = ColorMap.makemap({
|
||||
0: [0.267, 0.004, 0.329],
|
||||
0.13: [0.278, 0.173, 0.478],
|
||||
0.25: [0.231, 0.318, 0.545],
|
||||
0.38: [0.173, 0.443, 0.557],
|
||||
0.5: [0.129, 0.565, 0.553],
|
||||
0.63: [0.153, 0.678, 0.506],
|
||||
0.75: [0.361, 0.784, 0.388],
|
||||
0.88: [0.667, 0.863, 0.196],
|
||||
1: [0.992, 0.906, 0.145],
|
||||
});
|
||||
|
||||
Color.normalize(ColorMap);
|
||||
|
||||
ColorMap.sample = function (t, map = this) {
|
||||
if (t < 0) return map[0];
|
||||
if (t > 1) return map[1];
|
||||
|
||||
var lastkey = 0;
|
||||
for (var key of Object.keys(map).sort()) {
|
||||
if (t < key) {
|
||||
var b = map[key];
|
||||
var a = map[lastkey];
|
||||
var tt = (key - lastkey) * t;
|
||||
return a.lerp(b, tt);
|
||||
}
|
||||
lastkey = key;
|
||||
}
|
||||
return map[1];
|
||||
};
|
||||
|
||||
ColorMap.doc = {
|
||||
sample: "Sample a given colormap at the given percentage (0 to 1).",
|
||||
};
|
||||
|
||||
Color.maps = ColorMap
|
||||
Color.utils = esc
|
||||
|
||||
return Color
|
||||
345
controller.cm
Normal file
@@ -0,0 +1,345 @@
|
||||
var input = use('input')
|
||||
return {}
|
||||
|
||||
var downkeys = {};
|
||||
|
||||
function keyname(key)
|
||||
{
|
||||
var str = input.keyname(key);
|
||||
return str.toLowerCase();
|
||||
}
|
||||
|
||||
function modstr(mod = input.keymod()) {
|
||||
var s = "";
|
||||
if (mod.ctrl) s += "C-";
|
||||
if (mod.alt) s += "M-";
|
||||
if (mod.super) s += "S-";
|
||||
return s;
|
||||
}
|
||||
|
||||
prosperon.on('key_down', function key_down(e) {
|
||||
downkeys[e.key] = true;
|
||||
var emacs = modstr(e.mod) + keyname(e.key);
|
||||
if (e.repeat) player[0].raw_input(emacs, "rep");
|
||||
else player[0].raw_input(emacs, "pressed");
|
||||
})
|
||||
|
||||
prosperon.on('quit', function() {
|
||||
os.exit(0);
|
||||
})
|
||||
|
||||
prosperon.on('key_up', function key_up(e) {
|
||||
delete downkeys[e.key];
|
||||
var emacs = modstr(e.mod) + keyname(e.key);
|
||||
player[0].raw_input(emacs, "released");
|
||||
})
|
||||
|
||||
prosperon.on('drop_file', function (path) {
|
||||
player[0].raw_input("drop", "pressed", path);
|
||||
})
|
||||
|
||||
var mousepos = [0, 0];
|
||||
|
||||
prosperon.on('text_input', function (e) {
|
||||
player[0].raw_input("char", "pressed", e.text);
|
||||
})
|
||||
|
||||
prosperon.on('mouse_motion', function (e)
|
||||
{
|
||||
mousepos = e.pos;
|
||||
player[0].mouse_input("move", e.pos, e.d_pos);
|
||||
})
|
||||
|
||||
prosperon.on('mouse_wheel', function mousescroll(e) {
|
||||
player[0].mouse_input(modstr() + "scroll", e.scroll);
|
||||
})
|
||||
|
||||
prosperon.on('mouse_button_down', function(e) {
|
||||
player[0].mouse_input(modstr() + e.button, "pressed");
|
||||
input.mouse.buttons[e.button] = true
|
||||
})
|
||||
|
||||
prosperon.on('mouse_button_up', function(e) {
|
||||
player[0].mouse_input(modstr() + e.button, "released");
|
||||
input.mouse.buttons[e.button] = false
|
||||
})
|
||||
|
||||
input.mouse = {};
|
||||
input.mouse.screenpos = function mouse_screenpos() {
|
||||
return mousepos.slice();
|
||||
};
|
||||
input.mouse.worldpos = function mouse_worldpos() {
|
||||
return prosperon.camera.screen2world(mousepos);
|
||||
};
|
||||
input.mouse.viewpos = function mouse_viewpos()
|
||||
{
|
||||
var world = input.mouse.worldpos();
|
||||
|
||||
return mousepos.slice();
|
||||
}
|
||||
input.mouse.disabled = function mouse_disabled() {
|
||||
input.mouse_mode(1);
|
||||
};
|
||||
input.mouse.normal = function mouse_normal() {
|
||||
input.mouse_mode(0);
|
||||
};
|
||||
input.mouse.mode = function mouse_mode(m) {
|
||||
if (input.mouse.custom[m]) input.cursor_img(input.mouse.custom[m]);
|
||||
else input.mouse_cursor(m);
|
||||
};
|
||||
input.mouse.buttons = {
|
||||
0:false,
|
||||
1:false,
|
||||
2:false
|
||||
}
|
||||
|
||||
input.mouse.set_custom_cursor = function mouse_cursor(img, mode = input.mouse.cursor.default) {
|
||||
if (!img) delete input.mouse.custom[mode];
|
||||
else {
|
||||
input.cursor_img(img);
|
||||
input.mouse.custom[mode] = img;
|
||||
}
|
||||
};
|
||||
input.mouse.doc = {};
|
||||
input.mouse.doc.pos = "The screen position of the mouse.";
|
||||
input.mouse.doc.worldpos = "The position in the game world of the mouse.";
|
||||
input.mouse.disabled.doc = "Set the mouse to hidden. This locks it to the game and hides it, but still provides movement and click events.";
|
||||
input.mouse.normal.doc = "Set the mouse to show again after hiding.";
|
||||
|
||||
input.keyboard = {};
|
||||
input.keyboard.down = function (code) {
|
||||
if (typeof code == "number") return downkeys[code];
|
||||
if (typeof code == "string") return downkeys[code.toUpperCase().charCodeAt()] || downkeys[code.toLowerCase().charCodeAt()];
|
||||
return null;
|
||||
};
|
||||
|
||||
input.print_pawn_kbm = function (pawn) {
|
||||
if (!("inputs" in pawn)) return;
|
||||
var str = "";
|
||||
for (var key in pawn.inputs) {
|
||||
if (!pawn.inputs[key].doc) continue;
|
||||
str += `${key} | ${pawn.inputs[key].doc}\n`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
var joysticks = {};
|
||||
|
||||
joysticks["wasd"] = {
|
||||
uy: "w",
|
||||
dy: "s",
|
||||
ux: "d",
|
||||
dx: "a",
|
||||
};
|
||||
|
||||
input.procdown = function procdown() {
|
||||
for (var k in downkeys) player[0].raw_input(keyname(k), "down");
|
||||
|
||||
for (var i in joysticks) {
|
||||
var joy = joysticks[i];
|
||||
var x = joy.ux - joy.dx;
|
||||
var y = joy.uy - joy.dy;
|
||||
player[0].joy_input(i, joysticks[i]);
|
||||
}
|
||||
};
|
||||
|
||||
input.print_md_kbm = function print_md_kbm(pawn) {
|
||||
if (!("inputs" in pawn)) return;
|
||||
|
||||
var str = "";
|
||||
str += "|control|description|\n|---|---|\n";
|
||||
|
||||
for (var key in pawn.inputs) {
|
||||
str += `|${key}|${pawn.inputs[key].doc}|`;
|
||||
str += "\n";
|
||||
}
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
input.has_bind = function (pawn, bind) {
|
||||
return typeof pawn.inputs?.[bind] == "function";
|
||||
};
|
||||
|
||||
input.action = {
|
||||
add_new(name) {
|
||||
var action = Object.create(input.action);
|
||||
action.name = name;
|
||||
action.inputs = [];
|
||||
this.actions.push(action);
|
||||
|
||||
return action;
|
||||
},
|
||||
actions: [],
|
||||
};
|
||||
|
||||
input.tabcomplete = function tabcomplete(val, list) {
|
||||
if (!val) return val;
|
||||
list = filter(x => x.startsWith(val))
|
||||
|
||||
if (list.length == 1) {
|
||||
return list[0];
|
||||
}
|
||||
|
||||
var ret = null;
|
||||
var i = val.length;
|
||||
while (!ret && list.length != 0) {
|
||||
var char = list[0][i];
|
||||
if (
|
||||
!list.every(function (x) {
|
||||
return x[i] == char;
|
||||
})
|
||||
)
|
||||
ret = list[0].slice(0, i);
|
||||
else {
|
||||
i++;
|
||||
list = list.filter(x => x.length-1 > i)
|
||||
}
|
||||
}
|
||||
|
||||
return ret ? ret : val;
|
||||
};
|
||||
|
||||
/* May be a human player; may be an AI player */
|
||||
|
||||
/*
|
||||
'block' on a pawn's input blocks any input from reaching below for the
|
||||
*/
|
||||
|
||||
var Player = {
|
||||
players: [],
|
||||
input(fn, ...args) {
|
||||
this.pawns.forEach(x => x[fn]?.(...args));
|
||||
},
|
||||
|
||||
mouse_input(type, ...args) {
|
||||
for (var pawn of [...this.pawns].reverse()) {
|
||||
if (typeof pawn.inputs?.mouse?.[type] == "function") {
|
||||
pawn.inputs.mouse[type].call(pawn, ...args);
|
||||
pawn.inputs.post?.call(pawn);
|
||||
if (!pawn.inputs.fallthru) return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
char_input(c) {
|
||||
for (var pawn of [...this.pawns].reverse()) {
|
||||
if (typeof pawn.inputs?.char == "function") {
|
||||
pawn.inputs.char.call(pawn, c);
|
||||
pawn.inputs.post?.call(pawn);
|
||||
if (!pawn.inputs.fallthru) return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
joy_input(name, joystick) {
|
||||
for (var pawn of [...this.pawns].reverse()) {
|
||||
if (!pawn.inputs) return;
|
||||
if (!pawn.inputs.joystick) return;
|
||||
if (!pawn.inputs.joystick[name]) return;
|
||||
|
||||
var x = 0;
|
||||
if (input.keyboard.down(joystick.ux)) x++;
|
||||
if (input.keyboard.down(joystick.dx)) x--;
|
||||
var y = 0;
|
||||
if (input.keyboard.down(joystick.uy)) y++;
|
||||
if (input.keyboard.down(joystick.dy)) y--;
|
||||
|
||||
pawn.inputs.joystick[name](x, y);
|
||||
}
|
||||
},
|
||||
|
||||
raw_input(cmd, state, ...args) {
|
||||
for (var pawn of [...this.pawns].reverse()) {
|
||||
var inputs = pawn.inputs;
|
||||
|
||||
if (!inputs[cmd]) {
|
||||
if (inputs.block) return;
|
||||
continue;
|
||||
}
|
||||
|
||||
var fn = null;
|
||||
|
||||
switch (state) {
|
||||
case "pressed":
|
||||
fn = inputs[cmd];
|
||||
break;
|
||||
case "rep":
|
||||
fn = inputs[cmd].rep ? inputs[cmd] : null;
|
||||
break;
|
||||
case "released":
|
||||
fn = inputs[cmd].released;
|
||||
break;
|
||||
case "down":
|
||||
if (typeof inputs[cmd].down == "function") fn = inputs[cmd].down;
|
||||
else if (inputs[cmd].down) fn = inputs[cmd];
|
||||
}
|
||||
|
||||
var consumed = false;
|
||||
if (typeof fn == "function") {
|
||||
fn.call(pawn, ...args);
|
||||
consumed = true;
|
||||
}
|
||||
if (state == "released") inputs.release_post?.call(pawn);
|
||||
if (inputs.block) return;
|
||||
if (consumed) return;
|
||||
}
|
||||
},
|
||||
|
||||
obj_controlled(obj) {
|
||||
for (var p in Player.players) {
|
||||
if (p.pawns.has(obj)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
print_pawns() {
|
||||
[...this.pawns].reverse().forEach(x => log.console(x))
|
||||
},
|
||||
|
||||
create() {
|
||||
var n = Object.create(this);
|
||||
n.pawns = new Set()
|
||||
n.gamepads = [];
|
||||
this.players.push(n);
|
||||
this[this.players.length - 1] = n;
|
||||
return n;
|
||||
},
|
||||
|
||||
control(pawn) {
|
||||
if (!pawn)
|
||||
return
|
||||
|
||||
if (!pawn.inputs)
|
||||
throw new Error("attempted to control a pawn without any input object.");
|
||||
|
||||
this.pawns.add(pawn);
|
||||
},
|
||||
|
||||
uncontrol(pawn) {
|
||||
this.pawns.delete(pawn)
|
||||
},
|
||||
};
|
||||
|
||||
input.do_uncontrol = function input_do_uncontrol(pawn) {
|
||||
if (!pawn.inputs) return;
|
||||
Player.players.forEach(function (p) {
|
||||
p.pawns.delete(pawn)
|
||||
});
|
||||
};
|
||||
|
||||
//for (var i = 0; i < 4; i++)
|
||||
Player.create();
|
||||
|
||||
Player.control.doc = "Control a provided object, if the object has an 'inputs' object.";
|
||||
Player.uncontrol.doc = "Uncontrol a previously controlled object.";
|
||||
Player.print_pawns.doc = "Print out a list of the current pawn control stack.";
|
||||
Player.doc = {};
|
||||
Player.doc.players = "A list of current players.";
|
||||
|
||||
var player = Player;
|
||||
|
||||
input.player = Player
|
||||
|
||||
return input
|
||||
35
device.cm
Normal file
@@ -0,0 +1,35 @@
|
||||
// helpful render devices. width and height in pixels; diagonal in inches.
|
||||
return {
|
||||
pc: { width: 1920, height: 1080 },
|
||||
macbook_m2: { width: 2560, height: 1664, diagonal: 13.6 },
|
||||
ds_top: { width: 400, height: 240, diagonal: 3.53 },
|
||||
ds_bottom: { width: 320, height: 240, diagonal: 3.02 },
|
||||
playdate: { width: 400, height: 240, diagonal: 2.7 },
|
||||
switch: { width: 1280, height: 720, diagonal: 6.2 },
|
||||
switch_lite: { width: 1280, height: 720, diagonal: 5.5 },
|
||||
switch_oled: { width: 1280, height: 720, diagonal: 7 },
|
||||
dsi: { width: 256, height: 192, diagonal: 3.268 },
|
||||
ds: { width: 256, height: 192, diagonal: 3 },
|
||||
dsixl: { width: 256, height: 192, diagonal: 4.2 },
|
||||
ipad_air_m2: { width: 2360, height: 1640, diagonal: 11.97 },
|
||||
iphone_se: { width: 1334, height: 750, diagonal: 4.7 },
|
||||
iphone_12_pro: { width: 2532, height: 1170, diagonal: 6.06 },
|
||||
iphone_15: { width: 2556, height: 1179, diagonal: 6.1 },
|
||||
gba: { width: 240, height: 160, diagonal: 2.9 },
|
||||
gameboy: { width: 160, height: 144, diagonal: 2.48 },
|
||||
gbc: { width: 160, height: 144, diagonal: 2.28 },
|
||||
steamdeck: { width: 1280, height: 800, diagonal: 7 },
|
||||
vita: { width: 960, height: 544, diagonal: 5 },
|
||||
psp: { width: 480, height: 272, diagonal: 4.3 },
|
||||
imac_m3: { width: 4480, height: 2520, diagonal: 23.5 },
|
||||
macbook_pro_m3: { width: 3024, height: 1964, diagonal: 14.2 },
|
||||
ps1: { width: 320, height: 240, diagonal: 5 },
|
||||
ps2: { width: 640, height: 480 },
|
||||
snes: { width: 256, height: 224 },
|
||||
gamecube: { width: 640, height: 480 },
|
||||
n64: { width: 320, height: 240 },
|
||||
c64: { width: 320, height: 200 },
|
||||
macintosh: { width: 512, height: 342 },
|
||||
gamegear: { width: 160, height: 144, diagonal: 3.2 }
|
||||
};
|
||||
|
||||
251
draw2d.cm
Normal file
@@ -0,0 +1,251 @@
|
||||
var math = use('math')
|
||||
var color = use('color')
|
||||
var gamestate = use('gamestate')
|
||||
|
||||
var draw = {}
|
||||
|
||||
var current_list = []
|
||||
|
||||
// Clear current list
|
||||
draw.clear = function() {
|
||||
current_list = []
|
||||
}
|
||||
|
||||
// Get commands from current list
|
||||
draw.get_commands = function() {
|
||||
return current_list
|
||||
}
|
||||
|
||||
// Helper to add a command
|
||||
function add_command(type, data) {
|
||||
data.cmd = type
|
||||
current_list.push(data)
|
||||
}
|
||||
|
||||
// Default geometry definitions
|
||||
var ellipse_def = {
|
||||
start: 0,
|
||||
end: 1,
|
||||
mode: 'fill',
|
||||
thickness: 1,
|
||||
}
|
||||
|
||||
var line_def = {
|
||||
thickness: 1,
|
||||
cap:"butt",
|
||||
}
|
||||
|
||||
var rect_def = {
|
||||
thickness:1,
|
||||
radius: 0
|
||||
}
|
||||
|
||||
var image_info = {
|
||||
tile_x: false,
|
||||
tile_y: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
mode: 'linear'
|
||||
}
|
||||
|
||||
var circle_def = {
|
||||
inner_radius:1, // percentage: 1 means filled circle
|
||||
start:0,
|
||||
end: 1,
|
||||
}
|
||||
|
||||
// Drawing functions
|
||||
draw.point = function(pos, size, opt = {}, material) {
|
||||
add_command("draw_point", {
|
||||
pos: pos,
|
||||
size: size,
|
||||
opt: opt,
|
||||
material: material
|
||||
})
|
||||
}
|
||||
|
||||
draw.ellipse = function(pos, radii, defl, material) {
|
||||
var opt = defl ? {...ellipse_def, ...defl} : ellipse_def
|
||||
if (opt.thickness <= 0) opt.thickness = Math.max(radii[0], radii[1])
|
||||
|
||||
add_command("draw_ellipse", {
|
||||
pos: pos,
|
||||
radii: radii,
|
||||
opt: opt,
|
||||
material: material
|
||||
})
|
||||
}
|
||||
|
||||
draw.line = function(points, defl, material)
|
||||
{
|
||||
var opt = defl ? {...line_def, ...defl} : line_def
|
||||
|
||||
add_command("draw_line", {
|
||||
points: points,
|
||||
opt: opt,
|
||||
material: material
|
||||
})
|
||||
}
|
||||
|
||||
draw.cross = function render_cross(pos, size, defl, material) {
|
||||
var a = [pos.add([0, size]), pos.add([0, -size])]
|
||||
var b = [pos.add([size, 0]), pos.add([-size, 0])]
|
||||
draw.line(a, defl, material)
|
||||
draw.line(b, defl, material)
|
||||
}
|
||||
|
||||
draw.arrow = function render_arrow(start, end, wingspan = 4, wingangle = 10, defl, material) {
|
||||
var dir = math.norm(end.sub(start))
|
||||
var wing1 = [math.rotate(dir, wingangle).scale(wingspan).add(end), end]
|
||||
var wing2 = [math.rotate(dir, -wingangle).scale(wingspan).add(end), end]
|
||||
draw.line([start, end], defl, material)
|
||||
draw.line(wing1, defl, material)
|
||||
draw.line(wing2, defl, material)
|
||||
}
|
||||
|
||||
draw.rectangle = function render_rectangle(rect, defl, material = {color:{r:1,g:1,b:1,a:1}}) {
|
||||
var opt = defl ? {...rect_def, ...defl} : rect_def
|
||||
|
||||
add_command("draw_rect", {
|
||||
rect,
|
||||
opt,
|
||||
material
|
||||
})
|
||||
}
|
||||
|
||||
var slice9_info = {
|
||||
tile_top:true,
|
||||
tile_bottom:true,
|
||||
tile_left:true,
|
||||
tile_right:true,
|
||||
tile_center_x:true,
|
||||
tile_center_right:true,
|
||||
}
|
||||
|
||||
draw.slice9 = function slice9(image, rect = [0,0], slice = 0, info = slice9_info, material) {
|
||||
if (!image) throw Error('Need an image to render.')
|
||||
|
||||
add_command("draw_slice9", {
|
||||
image,
|
||||
rect,
|
||||
slice,
|
||||
info,
|
||||
material
|
||||
})
|
||||
}
|
||||
|
||||
draw.image = function image(image, rect, scale = {x:1,y:1}, anchor, shear, info, material) {
|
||||
if (!rect) throw Error('Need rectangle to render image.')
|
||||
if (!image) throw Error('Need an image to render.')
|
||||
|
||||
if (!('x' in rect && 'y' in rect)) throw Error('Must provide X and Y for image.')
|
||||
|
||||
add_command("draw_image", {
|
||||
image,
|
||||
rect,
|
||||
scale,
|
||||
anchor,
|
||||
shear,
|
||||
info,
|
||||
material
|
||||
})
|
||||
}
|
||||
|
||||
draw.circle = function render_circle(pos, radius, defl, material) {
|
||||
draw.ellipse(pos, [radius,radius], defl, material)
|
||||
}
|
||||
|
||||
// wrap is the width before wrapping
|
||||
// config is any additional config to pass to the text renderer
|
||||
var text_base_config = {
|
||||
align: 'left', // left, right, center, justify
|
||||
break: 'word', // word, character
|
||||
}
|
||||
draw.text = function text(text, pos, font = 'fonts/c64.8', color = {r:1,g:1,b:1,a:1}, wrap = 0, config = {}) {
|
||||
config.align ??= text_base_config.align
|
||||
config.break ??= text_base_config.break
|
||||
|
||||
add_command("draw_text", {
|
||||
text,
|
||||
pos,
|
||||
font,
|
||||
wrap,
|
||||
material: {color},
|
||||
config
|
||||
})
|
||||
}
|
||||
|
||||
draw.grid = function grid(rect, spacing, thickness = 1, offset = {x: 0, y: 0}, material) {
|
||||
if (!rect || rect.x == null || rect.y == null ||
|
||||
rect.width == null || rect.height == null) {
|
||||
throw Error('Grid requires rect with x, y, width, height')
|
||||
}
|
||||
if (!spacing || typeof spacing.x == 'undefined' || typeof spacing.y == 'undefined') {
|
||||
throw Error('Grid requires spacing with x and y')
|
||||
}
|
||||
|
||||
var left = rect.x
|
||||
var right = rect.x + rect.width
|
||||
var top = rect.y
|
||||
var bottom = rect.y + rect.height
|
||||
|
||||
// Apply offset and align to grid
|
||||
var start_x = Math.floor((left - offset.x) / spacing.x) * spacing.x + offset.x
|
||||
var end_x = Math.ceil((right - offset.x) / spacing.x) * spacing.x + offset.x
|
||||
var start_y = Math.floor((top - offset.y) / spacing.y) * spacing.y + offset.y
|
||||
var end_y = Math.ceil((bottom - offset.y) / spacing.y) * spacing.y + offset.y
|
||||
|
||||
// Draw vertical lines
|
||||
for (var x = start_x; x <= end_x; x += spacing.x) {
|
||||
if (x >= left && x <= right) {
|
||||
var line_top = Math.max(top, start_y)
|
||||
var line_bottom = Math.min(bottom, end_y)
|
||||
draw.line([[x, line_top], [x, line_bottom]], {thickness: thickness}, material)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw horizontal lines
|
||||
for (var y = start_y; y <= end_y; y += spacing.y) {
|
||||
if (y >= top && y <= bottom) {
|
||||
var line_left = Math.max(left, start_x)
|
||||
var line_right = Math.min(right, end_x)
|
||||
draw.line([[line_left, y], [line_right, y]], {thickness: thickness}, material)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw.scissor = function(rect)
|
||||
{
|
||||
var screen_rect = null
|
||||
if (rect && gamestate.camera) {
|
||||
var bottom_left = gamestate.camera.world_to_window(rect.x, rect.y)
|
||||
var top_right = gamestate.camera.world_to_window(rect.x + rect.width, rect.y + rect.height)
|
||||
var screen_left = bottom_left.x
|
||||
var screen_top = bottom_left.y
|
||||
var screen_right = top_right.x
|
||||
var screen_bottom = top_right.y
|
||||
|
||||
screen_rect = {
|
||||
x: Math.round(screen_left),
|
||||
y: Math.round(screen_top),
|
||||
width: Math.round(screen_right - screen_left),
|
||||
height: Math.round(screen_bottom - screen_top)
|
||||
}
|
||||
|
||||
// TODO: must be a better way than manually inverting here. Some camera specific function.
|
||||
var sensor = gamestate.camera.sensor()
|
||||
screen_rect.y = sensor.height - screen_rect.y - screen_rect.height
|
||||
}
|
||||
|
||||
current_list.push({
|
||||
cmd: "scissor",
|
||||
rect: screen_rect
|
||||
})
|
||||
}
|
||||
|
||||
draw.add_command = function(cmd)
|
||||
{
|
||||
current_list.push(cmd)
|
||||
}
|
||||
|
||||
return draw
|
||||
148
ease.cm
Normal file
@@ -0,0 +1,148 @@
|
||||
var Ease = {
|
||||
linear(t) {
|
||||
return t
|
||||
},
|
||||
in(t) {
|
||||
return t * t
|
||||
},
|
||||
out(t) {
|
||||
var d = 1 - t
|
||||
return 1 - d * d
|
||||
},
|
||||
inout(t) {
|
||||
var d = -2 * t + 2
|
||||
return t < 0.5 ? 2 * t * t : 1 - (d * d) / 2
|
||||
},
|
||||
}
|
||||
|
||||
function make_easing_fns(num) {
|
||||
var obj = {}
|
||||
|
||||
obj.in = function (t) {
|
||||
return Math.pow(t, num)
|
||||
}
|
||||
|
||||
obj.out = function (t) {
|
||||
return 1 - Math.pow(1 - t, num)
|
||||
}
|
||||
|
||||
var mult = Math.pow(2, num - 1)
|
||||
obj.inout = function (t) {
|
||||
return t < 0.5 ? mult * Math.pow(t, num) : 1 - Math.pow(-2 * t + 2, num) / 2
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
Ease.quad = make_easing_fns(2)
|
||||
Ease.cubic = make_easing_fns(3)
|
||||
Ease.quart = make_easing_fns(4)
|
||||
Ease.quint = make_easing_fns(5)
|
||||
|
||||
Ease.expo = {
|
||||
in(t) {
|
||||
return t == 0 ? 0 : Math.pow(2, 10 * t - 10)
|
||||
},
|
||||
out(t) {
|
||||
return t == 1 ? 1 : 1 - Math.pow(2, -10 * t)
|
||||
},
|
||||
inout(t) {
|
||||
return t == 0
|
||||
? 0
|
||||
: t == 1
|
||||
? 1
|
||||
: t < 0.5
|
||||
? Math.pow(2, 20 * t - 10) / 2
|
||||
: (2 - Math.pow(2, -20 * t + 10)) / 2
|
||||
},
|
||||
}
|
||||
|
||||
Ease.bounce = {
|
||||
in(t) {
|
||||
return 1 - this.out(1 - t)
|
||||
},
|
||||
out(t) {
|
||||
var n1 = 7.5625
|
||||
var d1 = 2.75
|
||||
if (t < 1 / d1) {
|
||||
return n1 * t * t
|
||||
} else if (t < 2 / d1) {
|
||||
return n1 * (t -= 1.5 / d1) * t + 0.75
|
||||
} else if (t < 2.5 / d1) {
|
||||
return n1 * (t -= 2.25 / d1) * t + 0.9375
|
||||
} else return n1 * (t -= 2.625 / d1) * t + 0.984375
|
||||
},
|
||||
inout(t) {
|
||||
return t < 0.5 ? (1 - this.out(1 - 2 * t)) / 2 : (1 + this.out(2 * t - 1)) / 2
|
||||
},
|
||||
}
|
||||
|
||||
Ease.sine = {
|
||||
in(t) {
|
||||
return 1 - Math.cos((t * Math.PI) / 2)
|
||||
},
|
||||
out(t) {
|
||||
return Math.sin((t * Math.PI) / 2)
|
||||
},
|
||||
inout(t) {
|
||||
return -(Math.cos(Math.PI * t) - 1) / 2
|
||||
},
|
||||
}
|
||||
|
||||
Ease.elastic = {
|
||||
in(t) {
|
||||
return t == 0
|
||||
? 0
|
||||
: t == 1
|
||||
? 1
|
||||
: -Math.pow(2, 10 * t - 10) *
|
||||
Math.sin((t * 10 - 10.75) * this.c4)
|
||||
},
|
||||
out(t) {
|
||||
return t == 0
|
||||
? 0
|
||||
: t == 1
|
||||
? 1
|
||||
: Math.pow(2, -10 * t) *
|
||||
Math.sin((t * 10 - 0.75) * this.c4) +
|
||||
1
|
||||
},
|
||||
inout(t) {
|
||||
return t == 0
|
||||
? 0
|
||||
: t == 1
|
||||
? 1
|
||||
: t < 0.5
|
||||
? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * this.c5)) / 2
|
||||
: (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * this.c5)) / 2 + 1
|
||||
},
|
||||
}
|
||||
|
||||
Ease.elastic.c4 = (2 * Math.PI) / 3
|
||||
Ease.elastic.c5 = (2 * Math.PI) / 4.5
|
||||
|
||||
Ease.zoom = {
|
||||
// Creates a smooth zoom that maintains constant perceptual speed
|
||||
// ratio is the zoom factor (e.g., 10 for 10x zoom)
|
||||
smooth(ratio) {
|
||||
return function(t) {
|
||||
if (t == 0) return 0
|
||||
if (t == 1) return 1
|
||||
if (Math.abs(ratio - 1) < 0.001) return t
|
||||
// Position interpolation formula: (r^t - 1) / (r - 1)
|
||||
return (Math.pow(ratio, t) - 1) / (ratio - 1)
|
||||
}
|
||||
},
|
||||
// Exponential interpolation for zoom values
|
||||
// Interpolates in logarithmic space for smooth visual zoom
|
||||
exp(startZoom, endZoom) {
|
||||
return function(t) {
|
||||
if (t == 0) return startZoom
|
||||
if (t == 1) return endZoom
|
||||
// Scale := Exp(LinearInterpolate(Ln(Scale1), Ln(Scale2), t))
|
||||
return Math.exp(Math.log(startZoom) + t * (Math.log(endZoom) - Math.log(startZoom)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ease
|
||||
BIN
examples/bunnymark/bunny.png
Normal file
|
After Width: | Height: | Size: 449 B |
5
examples/bunnymark/config.cm
Normal file
@@ -0,0 +1,5 @@
|
||||
return {
|
||||
title:"Bunnymark",
|
||||
width:1200,
|
||||
height:600,
|
||||
}
|
||||
70
examples/bunnymark/main.ce
Normal file
@@ -0,0 +1,70 @@
|
||||
var draw = use('draw2d')
|
||||
var render = use('render')
|
||||
var graphics = use('graphics')
|
||||
var sprite = use('sprite')
|
||||
var geom = use('geometry')
|
||||
var input = use('controller')
|
||||
var config = use('config')
|
||||
var color = use('color')
|
||||
|
||||
var bunnyTex = graphics.texture("bunny")
|
||||
|
||||
// We'll store our bunnies in an array of objects: { x, y, vx, vy }
|
||||
var bunnies = []
|
||||
|
||||
// Start with some initial bunnies:
|
||||
for (var i = 0; i < 100; i++) {
|
||||
bunnies.push({
|
||||
x: Math.random() * config.width,
|
||||
y: Math.random() * config.height,
|
||||
vx: (Math.random() * 300) - 150,
|
||||
vy: (Math.random() * 300) - 150
|
||||
})
|
||||
}
|
||||
|
||||
var fpsSamples = []
|
||||
var fpsAvg = 0
|
||||
|
||||
this.update = function(dt) {
|
||||
// Compute FPS average over the last 60 frames:
|
||||
var currentFPS = 1 / dt
|
||||
fpsSamples.push(currentFPS)
|
||||
if (fpsSamples.length > 60) fpsSamples.shift()
|
||||
var sum = 0
|
||||
for (var f of fpsSamples) sum += f
|
||||
fpsAvg = sum / fpsSamples.length
|
||||
|
||||
// If left mouse is down, spawn some more bunnies:
|
||||
var mouse = input.mousestate()
|
||||
if (mouse.left)
|
||||
for (var i = 0; i < 50; i++) {
|
||||
bunnies.push({
|
||||
x: mouse.x,
|
||||
y: mouse.y,
|
||||
vx: (Math.random() * 300) - 150,
|
||||
vy: (Math.random() * 300) - 150
|
||||
})
|
||||
}
|
||||
|
||||
// Update bunny positions and bounce them inside the screen:
|
||||
for (var i = 0; i < bunnies.length; i++) {
|
||||
var b = bunnies[i]
|
||||
b.x += b.vx * dt
|
||||
b.y += b.vy * dt
|
||||
|
||||
// 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 }
|
||||
|
||||
// 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 }
|
||||
}
|
||||
}
|
||||
|
||||
this.hud = function() {
|
||||
draw.images(bunnyTex, bunnies)
|
||||
|
||||
var msg = 'FPS: ' + fpsAvg.toFixed(2) + ' Bunnies: ' + bunnies.length
|
||||
draw.text(msg, {x:0, y:0, width:config.width, height:40}, null, 0, color.white, 0)
|
||||
}
|
||||
BIN
examples/chess/black_bishop.png
Normal file
|
After Width: | Height: | Size: 390 B |
BIN
examples/chess/black_king.png
Normal file
|
After Width: | Height: | Size: 438 B |
BIN
examples/chess/black_knight.png
Normal file
|
After Width: | Height: | Size: 398 B |
BIN
examples/chess/black_pawn.png
Normal file
|
After Width: | Height: | Size: 337 B |
BIN
examples/chess/black_queen.png
Normal file
|
After Width: | Height: | Size: 390 B |
BIN
examples/chess/black_rook.png
Normal file
|
After Width: | Height: | Size: 379 B |
395
examples/chess/chess.ce
Normal file
@@ -0,0 +1,395 @@
|
||||
/* main.js – runs the demo with your prototype-based grid */
|
||||
|
||||
var json = use('json')
|
||||
var draw2d = use('prosperon/draw2d')
|
||||
|
||||
var blob = use('blob')
|
||||
|
||||
/*──── 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');
|
||||
|
||||
/*──── build board ───────────────────────────────────────────────────*/
|
||||
var grid = new Grid(8, 8);
|
||||
grid.width = 8; // (the ctor didn't store them)
|
||||
grid.height = 8;
|
||||
|
||||
var mover = new 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 - ";
|
||||
|
||||
switch(gameState) {
|
||||
case 'waiting':
|
||||
title += "Press S to start server or J to join";
|
||||
break;
|
||||
case 'searching':
|
||||
title += "Searching for server...";
|
||||
break;
|
||||
case 'server_waiting':
|
||||
title += "Waiting for player to join...";
|
||||
break;
|
||||
case 'connected':
|
||||
if (myColor) {
|
||||
title += (mover.turn == myColor ? "Your turn (" + myColor + ")" : "Opponent's turn (" + mover.turn + ")");
|
||||
} else {
|
||||
title += mover.turn + " turn";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
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 = [Math.floor(mx / 60), Math.floor(my / 60)];
|
||||
if (!grid.inBounds(c)) return;
|
||||
|
||||
var cell = grid.at(c);
|
||||
if (cell.length && 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 = [Math.floor(mx / 60), Math.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 = [Math.floor(mx / 60), Math.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() {
|
||||
for (var y = 0; y < 8; ++y)
|
||||
for (var x = 0; x < 8; ++x) {
|
||||
var isMyHover = hoverPos && hoverPos[0] == x && hoverPos[1] == y;
|
||||
var isOpponentHover = opponentMousePos && opponentMousePos[0] == x && opponentMousePos[1] == y;
|
||||
var isValidMove = selectPos && holdingPiece && isValidMoveForTurn(selectPos, [x, y]);
|
||||
|
||||
var 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 (destCell.length && destCell[0].colour == piece.colour) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return rules.canMove(piece, from, to, grid);
|
||||
}
|
||||
|
||||
/* ── draw every live piece ─────────────────────────────────────── */
|
||||
function drawPieces() {
|
||||
grid.each(function (piece) {
|
||||
if (piece.captured) return;
|
||||
|
||||
// Skip drawing the piece being held (by me or opponent)
|
||||
if (holdingPiece && selectPos &&
|
||||
piece.coord[0] == selectPos[0] &&
|
||||
piece.coord[1] == selectPos[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip drawing the piece being held by opponent
|
||||
if (opponentHoldingPiece && opponentSelectPos &&
|
||||
piece.coord[0] == opponentSelectPos[0] &&
|
||||
piece.coord[1] == opponentSelectPos[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var r = { x: piece.coord[0]*S, y: piece.coord[1]*S,
|
||||
width:S, height:S };
|
||||
|
||||
draw2d.image(piece.sprite, r);
|
||||
});
|
||||
|
||||
// Draw the held piece at the mouse position if we're holding one
|
||||
if (holdingPiece && selectPos && hoverPos) {
|
||||
var piece = grid.at(selectPos)[0];
|
||||
if (piece) {
|
||||
var 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) {
|
||||
var opponentPiece = grid.at(opponentSelectPos)[0];
|
||||
if (opponentPiece) {
|
||||
var 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 (json.encode($_))
|
||||
send(e, $_);
|
||||
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: $_
|
||||
});
|
||||
} else {
|
||||
log.console(`Failed to connect: ${json.encode(reason)}`);
|
||||
gameState = 'waiting';
|
||||
updateTitle();
|
||||
}
|
||||
}
|
||||
|
||||
$_.contact(contact_fn, {
|
||||
address: "192.168.0.149",
|
||||
port: 5678
|
||||
});
|
||||
}
|
||||
|
||||
$_.receiver(e => {
|
||||
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:", json.encode(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
|
||||
var fromCell = grid.at(e.from);
|
||||
if (fromCell.length) {
|
||||
var 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)
|
||||
}
|
||||
})
|
||||
9
examples/chess/config.cm
Normal file
@@ -0,0 +1,9 @@
|
||||
// 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
|
||||
};
|
||||
69
examples/chess/grid.cm
Normal file
@@ -0,0 +1,69 @@
|
||||
function grid(w, h) {
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
// create a height×width array of empty lists
|
||||
this.cells = new Array(h);
|
||||
for (let y = 0; y < h; y++) {
|
||||
this.cells[y] = new Array(w);
|
||||
for (let x = 0; x < w; x++) {
|
||||
this.cells[y][x] = []; // each cell holds its own list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grid.prototype = {
|
||||
// return the array at (x,y)
|
||||
cell(x, y) {
|
||||
return this.cells[y][x];
|
||||
},
|
||||
|
||||
// alias for cell
|
||||
at(pos) {
|
||||
return this.cell(pos.x, pos.y);
|
||||
},
|
||||
|
||||
// add an entity into a cell
|
||||
add(entity, pos) {
|
||||
this.cell(pos.x, pos.y).push(entity);
|
||||
entity.coord = pos.slice();
|
||||
},
|
||||
|
||||
// remove an entity from a cell
|
||||
remove(entity, pos) {
|
||||
const c = this.cell(pos.x, pos.y);
|
||||
const i = c.indexOf(entity);
|
||||
if (i !== -1) c.splice(i, 1);
|
||||
},
|
||||
|
||||
// bounds check
|
||||
inBounds(pos) {
|
||||
return (
|
||||
pos.x >= 0 && pos.x < this.width &&
|
||||
pos.y >= 0 && pos.y < this.height
|
||||
);
|
||||
},
|
||||
|
||||
// call fn(entity, coord) for every entity in every cell
|
||||
each(fn) {
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
const list = this.cells[y][x];
|
||||
for (let entity of list) {
|
||||
fn(entity, entity.coord);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// printable representation
|
||||
toString() {
|
||||
let out = `grid [${this.width}×${this.height}]\n`;
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
out += this.cells[y][x].length;
|
||||
}
|
||||
if (y !== this.height - 1) out += "\n";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
32
examples/chess/movement.cm
Normal file
@@ -0,0 +1,32 @@
|
||||
var MovementSystem = function(grid, rules) {
|
||||
this.grid = grid;
|
||||
this.rules = rules || {}; // expects { canMove: fn }
|
||||
this.turn = 'white';
|
||||
}
|
||||
|
||||
MovementSystem.prototype.tryMove = function (piece, to) {
|
||||
if (piece.colour != this.turn) return false;
|
||||
|
||||
// normalise ‘to’ into our hybrid coord
|
||||
var dest = [to.x ?? t[0],
|
||||
to.y ?? to[1]];
|
||||
|
||||
if (!this.grid.inBounds(dest)) return false;
|
||||
if (!this.rules.canMove(piece, piece.coord, dest, this.grid)) return false;
|
||||
|
||||
var victims = this.grid.at(dest);
|
||||
if (victims.length && victims[0].colour == piece.colour) return false;
|
||||
if (victims.length) victims[0].captured = true;
|
||||
|
||||
this.grid.remove(piece, piece.coord);
|
||||
this.grid.add (piece, dest);
|
||||
|
||||
// grid.add() re-creates coord; re-add .x/.y fields:
|
||||
piece.coord.x = dest.x;
|
||||
piece.coord.y = dest.y;
|
||||
|
||||
this.turn = (this.turn == 'white') ? 'black' : 'white';
|
||||
return true;
|
||||
};
|
||||
|
||||
return { MovementSystem: MovementSystem };
|
||||
29
examples/chess/pieces.cm
Normal file
@@ -0,0 +1,29 @@
|
||||
/* pieces.js – simple data holders + starting layout */
|
||||
function Piece(kind, colour) {
|
||||
this.kind = kind; // "pawn" etc.
|
||||
this.colour = colour; // "white"/"black"
|
||||
this.sprite = colour + '_' + kind; // for draw2d.image
|
||||
this.captured = false;
|
||||
this.coord = [0,0];
|
||||
}
|
||||
Piece.prototype.toString = function () {
|
||||
return this.colour.charAt(0) + this.kind.charAt(0).toUpperCase();
|
||||
};
|
||||
|
||||
function startingPosition(grid) {
|
||||
var W = 'white', B = 'black', x;
|
||||
|
||||
// pawns
|
||||
for (x = 0; x < 8; x++) {
|
||||
grid.add(new Piece('pawn', W), [x, 6]);
|
||||
grid.add(new Piece('pawn', B), [x, 1]);
|
||||
}
|
||||
// major pieces
|
||||
var back = ['rook','knight','bishop','queen','king','bishop','knight','rook'];
|
||||
for (x = 0; x < 8; x++) {
|
||||
grid.add(new Piece(back[x], W), [x, 7]);
|
||||
grid.add(new Piece(back[x], B), [x, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
return { Piece, startingPosition };
|
||||
BIN
examples/chess/prosperon
Executable file
45
examples/chess/rules.cm
Normal file
@@ -0,0 +1,45 @@
|
||||
/* helper – robust coord access */
|
||||
function cx(c) { return c.x ?? c[0] }
|
||||
function cy(c) { return c.y ?? c[1] }
|
||||
|
||||
/* simple move-shape checks */
|
||||
var deltas = {
|
||||
pawn: function (pc, dx, dy, grid, to) {
|
||||
var dir = (pc.colour == 'white') ? -1 : 1;
|
||||
var base = (pc.colour == 'white') ? 6 : 1;
|
||||
var one = (dy == dir && dx == 0 && grid.at(to).length == 0);
|
||||
var two = (dy == 2 * dir && dx == 0 && cy(pc.coord) == base &&
|
||||
grid.at({ x: cx(pc.coord), y: cy(pc.coord)+dir }).length == 0 &&
|
||||
grid.at(to).length == 0);
|
||||
var cap = (dy == dir && Math.abs(dx) == 1 && grid.at(to).length);
|
||||
return one || two || cap;
|
||||
},
|
||||
rook : function (pc, dx, dy) { return (dx == 0 || dy == 0); },
|
||||
bishop: function (pc, dx, dy) { return Math.abs(dx) == Math.abs(dy); },
|
||||
queen : function (pc, dx, dy) { return (dx == 0 || dy == 0 || Math.abs(dx) == Math.abs(dy)); },
|
||||
knight: function (pc, dx, dy) { return (Math.abs(dx) == 1 && Math.abs(dy) == 2) ||
|
||||
(Math.abs(dx) == 2 && Math.abs(dy) == 1); },
|
||||
king : function (pc, dx, dy) { return Math.max(Math.abs(dx), Math.abs(dy)) == 1; }
|
||||
};
|
||||
|
||||
function clearLine(from, to, grid) {
|
||||
var dx = Math.sign(cx(to) - cx(from));
|
||||
var dy = Math.sign(cy(to) - cy(from));
|
||||
var x = cx(from) + dx, y = cy(from) + dy;
|
||||
while (x != cx(to) || y != cy(to)) {
|
||||
if (grid.at({ x: x, y: y }).length) return false;
|
||||
x += dx; y += dy;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function canMove(piece, from, to, grid) {
|
||||
var dx = cx(to) - cx(from);
|
||||
var dy = cy(to) - cy(from);
|
||||
var f = deltas[piece.kind];
|
||||
if (!f || !f(piece, dx, dy, grid, to)) return false;
|
||||
if (piece.kind == 'knight') return true;
|
||||
return clearLine(from, to, grid);
|
||||
}
|
||||
|
||||
return { canMove };
|
||||
BIN
examples/chess/white_bishop.png
Normal file
|
After Width: | Height: | Size: 376 B |
BIN
examples/chess/white_king.png
Normal file
|
After Width: | Height: | Size: 403 B |
BIN
examples/chess/white_knight.png
Normal file
|
After Width: | Height: | Size: 381 B |
BIN
examples/chess/white_pawn.png
Normal file
|
After Width: | Height: | Size: 313 B |
BIN
examples/chess/white_queen.png
Normal file
|
After Width: | Height: | Size: 378 B |
BIN
examples/chess/white_rook.png
Normal file
|
After Width: | Height: | Size: 378 B |
5
examples/pong/config.cm
Normal file
@@ -0,0 +1,5 @@
|
||||
return {
|
||||
title: "Pong",
|
||||
width: 858,
|
||||
height: 525
|
||||
}
|
||||
86
examples/pong/main.ce
Normal file
@@ -0,0 +1,86 @@
|
||||
// main.js
|
||||
var draw = use('draw2d')
|
||||
var input = use('controller')
|
||||
var config = use('config')
|
||||
var color = use('color')
|
||||
|
||||
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 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 = (Math.random()<0.5 ? -1:1)*150
|
||||
// keep horizontal speed to the same magnitude
|
||||
ball.vx = ball.vx>0 ? 220 : -220
|
||||
}
|
||||
|
||||
resetBall()
|
||||
|
||||
this.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 && l<right1 && t>bottom1 && b<top1)
|
||||
ball.vx = Math.abs(ball.vx)
|
||||
// Collide with paddle 2?
|
||||
if (r>left2 && l<right2 && t>bottom2 && b<top2)
|
||||
ball.vx = -Math.abs(ball.vx)
|
||||
|
||||
// Check left/right out-of-bounds
|
||||
if (r<0) { score2++; resetBall() }
|
||||
if (l>config.width) { score1++; resetBall() }
|
||||
}
|
||||
|
||||
this.hud = function() {
|
||||
// Clear screen black
|
||||
draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1])
|
||||
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// Simple score display
|
||||
var msg = score1 + " " + score2
|
||||
draw.text(msg, {x:0, y:10, width:config.width, height:40}, null, 0, color.white, 0)
|
||||
}
|
||||
5
examples/snake/config.cm
Normal file
@@ -0,0 +1,5 @@
|
||||
return {
|
||||
title: "Snake",
|
||||
width: 600,
|
||||
height: 600
|
||||
}
|
||||
119
examples/snake/main.ce
Normal file
@@ -0,0 +1,119 @@
|
||||
// main.js
|
||||
var draw = use('draw2d')
|
||||
var render = use('render')
|
||||
var graphics = use('graphics')
|
||||
var input = use('input')
|
||||
var config = use('config')
|
||||
var color = use('color')
|
||||
|
||||
prosperon.camera.transform.pos = [0,0]
|
||||
|
||||
var cellSize = 20
|
||||
var gridW = Math.floor(config.width / cellSize)
|
||||
var gridH = Math.floor(config.height / cellSize)
|
||||
|
||||
var snake, direction, nextDirection, apple
|
||||
var moveInterval = 0.1
|
||||
var moveTimer = 0
|
||||
var gameState = "playing"
|
||||
|
||||
function resetGame() {
|
||||
var cx = Math.floor(gridW / 2)
|
||||
var cy = Math.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
|
||||
}
|
||||
|
||||
function spawnApple() {
|
||||
apple = {x:Math.floor(Math.random()*gridW), y:Math.floor(Math.random()*gridH)}
|
||||
// Re-spawn if apple lands on snake
|
||||
for (var i=0; i<snake.length; i++)
|
||||
if (snake[i].x == apple.x && snake[i].y == apple.y) { spawnApple(); return }
|
||||
}
|
||||
|
||||
function wrap(pos) {
|
||||
if (pos.x < 0) pos.x = gridW - 1
|
||||
if (pos.x >= gridW) pos.x = 0
|
||||
if (pos.y < 0) pos.y = gridH - 1
|
||||
if (pos.y >= gridH) pos.y = 0
|
||||
}
|
||||
|
||||
resetGame()
|
||||
|
||||
this.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
|
||||
for (var i=0; i<snake.length; i++) {
|
||||
if (snake[i].x == head.x && snake[i].y == head.y) {
|
||||
gameState = "gameover"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Place head
|
||||
snake.unshift(head)
|
||||
|
||||
// Eat apple?
|
||||
if (head.x == apple.x && head.y == apple.y) spawnApple()
|
||||
else snake.pop()
|
||||
}
|
||||
|
||||
this.hud = function() {
|
||||
// Optional clear screen
|
||||
draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1])
|
||||
|
||||
// Draw snake
|
||||
for (var i=0; i<snake.length; i++) {
|
||||
var s = snake[i]
|
||||
draw.rectangle({x:s.x*cellSize, y:s.y*cellSize, width:cellSize, height:cellSize}, color.green)
|
||||
}
|
||||
|
||||
// Draw apple
|
||||
draw.rectangle({x:apple.x*cellSize, y:apple.y*cellSize, width:cellSize, height:cellSize}, color.red)
|
||||
|
||||
if (gameState == "gameover") {
|
||||
var msg = "GAME OVER! Press SPACE to restart."
|
||||
draw.text(msg, {x:0, y:config.height*0.5-10, width:config.width, height:20}, null, 0, color.white)
|
||||
}
|
||||
}
|
||||
|
||||
// No immediate reversal
|
||||
// "Up" means y=1, so going physically up on screen
|
||||
this.inputs = {
|
||||
up: function() {
|
||||
if (direction.y != -1) nextDirection = {x:0,y:1}
|
||||
},
|
||||
down: function() {
|
||||
if (direction.y != 1) nextDirection = {x:0,y:-1}
|
||||
},
|
||||
left: function() {
|
||||
if (direction.x != 1) nextDirection = {x:-1,y:0}
|
||||
},
|
||||
right: function() {
|
||||
if (direction.x != -1) nextDirection = {x:1,y:0}
|
||||
},
|
||||
space: function() {
|
||||
if (gameState=="gameover") resetGame()
|
||||
}
|
||||
}
|
||||
|
||||
input.player[0].control(this)
|
||||
187
examples/steam_example.ce
Normal file
@@ -0,0 +1,187 @@
|
||||
// Steam Integration Example
|
||||
// This example shows how to use Steam achievements and stats
|
||||
|
||||
var steam = use("steam");
|
||||
|
||||
// Achievement names (these should match your Steam app configuration)
|
||||
var ACHIEVEMENTS = {
|
||||
FIRST_WIN: "ACH_FIRST_WIN",
|
||||
PLAY_10_GAMES: "ACH_PLAY_10_GAMES",
|
||||
HIGH_SCORE: "ACH_HIGH_SCORE_1000"
|
||||
};
|
||||
|
||||
// Stat names
|
||||
var STATS = {
|
||||
GAMES_PLAYED: "stat_games_played",
|
||||
TOTAL_SCORE: "stat_total_score",
|
||||
PLAY_TIME: "stat_play_time"
|
||||
};
|
||||
|
||||
var steam_available = false;
|
||||
var stats_loaded = false;
|
||||
|
||||
// Initialize Steam
|
||||
function init_steam() {
|
||||
if (!steam) {
|
||||
log.console("Steam module not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
log.console("Initializing Steam...");
|
||||
steam_available = steam.steam_init();
|
||||
|
||||
if (steam_available) {
|
||||
log.console("Steam initialized successfully");
|
||||
|
||||
// Request current stats/achievements
|
||||
if (steam.stats.stats_request()) {
|
||||
log.console("Stats requested");
|
||||
stats_loaded = true;
|
||||
}
|
||||
} else {
|
||||
log.console("Failed to initialize Steam");
|
||||
}
|
||||
|
||||
return steam_available;
|
||||
}
|
||||
|
||||
// Update Steam (call this regularly, e.g., once per frame)
|
||||
function update_steam() {
|
||||
if (steam_available) {
|
||||
steam.steam_run_callbacks();
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock an achievement
|
||||
function unlock_achievement(achievement_name) {
|
||||
if (!steam_available || !stats_loaded) return false;
|
||||
|
||||
// Check if already unlocked
|
||||
var unlocked = steam.achievement.achievement_get(achievement_name);
|
||||
if (unlocked) {
|
||||
log.console("Achievement already unlocked:", achievement_name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unlock it
|
||||
if (steam.achievement.achievement_set(achievement_name)) {
|
||||
log.console("Achievement unlocked:", achievement_name);
|
||||
|
||||
// Store stats to make it permanent
|
||||
steam.stats.stats_store();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update a stat
|
||||
function update_stat(stat_name, value, is_float) {
|
||||
if (!steam_available || !stats_loaded) return false;
|
||||
|
||||
var success;
|
||||
if (is_float) {
|
||||
success = steam.stats.stats_set_float(stat_name, value);
|
||||
} else {
|
||||
success = steam.stats.stats_set_int(stat_name, value);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
log.console("Stat updated:", stat_name, "=", value);
|
||||
steam.stats.stats_store();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Get a stat value
|
||||
function get_stat(stat_name, is_float) {
|
||||
if (!steam_available || !stats_loaded) return 0;
|
||||
|
||||
if (is_float) {
|
||||
return steam.stats.stats_get_float(stat_name) || 0;
|
||||
} else {
|
||||
return steam.stats.stats_get_int(stat_name) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Example game logic
|
||||
var games_played = 0;
|
||||
var total_score = 0;
|
||||
var current_score = 0;
|
||||
|
||||
function start_game() {
|
||||
games_played = get_stat(STATS.GAMES_PLAYED, false);
|
||||
total_score = get_stat(STATS.TOTAL_SCORE, false);
|
||||
current_score = 0;
|
||||
|
||||
log.console("Starting game #" + (games_played + 1));
|
||||
}
|
||||
|
||||
function end_game(score) {
|
||||
current_score = score;
|
||||
games_played++;
|
||||
total_score += score;
|
||||
|
||||
// Update stats
|
||||
update_stat(STATS.GAMES_PLAYED, games_played, false);
|
||||
update_stat(STATS.TOTAL_SCORE, total_score, false);
|
||||
|
||||
// Check for achievements
|
||||
if (games_played == 1) {
|
||||
unlock_achievement(ACHIEVEMENTS.FIRST_WIN);
|
||||
}
|
||||
|
||||
if (games_played >= 10) {
|
||||
unlock_achievement(ACHIEVEMENTS.PLAY_10_GAMES);
|
||||
}
|
||||
|
||||
if (score >= 1000) {
|
||||
unlock_achievement(ACHIEVEMENTS.HIGH_SCORE);
|
||||
}
|
||||
}
|
||||
|
||||
// Cloud save example
|
||||
function save_to_cloud(save_data) {
|
||||
if (!steam_available) return false;
|
||||
|
||||
var json_data = JSON.stringify(save_data);
|
||||
return steam.cloud.cloud_write("savegame.json", json_data);
|
||||
}
|
||||
|
||||
function load_from_cloud() {
|
||||
if (!steam_available) return null;
|
||||
|
||||
var data = steam.cloud.cloud_read("savegame.json");
|
||||
if (data) {
|
||||
// Convert ArrayBuffer to string
|
||||
var decoder = new TextDecoder();
|
||||
var json_str = decoder.decode(data);
|
||||
return JSON.parse(json_str);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
function cleanup_steam() {
|
||||
if (steam_available) {
|
||||
steam.steam_shutdown();
|
||||
log.console("Steam shut down");
|
||||
}
|
||||
}
|
||||
|
||||
// Export the API
|
||||
module.exports = {
|
||||
init: init_steam,
|
||||
update: update_steam,
|
||||
cleanup: cleanup_steam,
|
||||
unlock_achievement: unlock_achievement,
|
||||
update_stat: update_stat,
|
||||
get_stat: get_stat,
|
||||
start_game: start_game,
|
||||
end_game: end_game,
|
||||
save_to_cloud: save_to_cloud,
|
||||
load_from_cloud: load_from_cloud,
|
||||
is_available: function() { return steam_available; }
|
||||
};
|
||||
5
examples/tetris/config.cm
Normal file
@@ -0,0 +1,5 @@
|
||||
return {
|
||||
title: "Tetris",
|
||||
width:160,
|
||||
height:144
|
||||
}
|
||||
271
examples/tetris/main.ce
Normal file
@@ -0,0 +1,271 @@
|
||||
var draw = use('draw2d')
|
||||
var input = use('input')
|
||||
var config = use('config')
|
||||
var color = use('color')
|
||||
|
||||
prosperon.camera.transform.pos = [0,0]
|
||||
|
||||
// Board constants
|
||||
var COLS = 10, ROWS = 20
|
||||
var TILE = 6 // each cell is 6x6
|
||||
|
||||
// Board storage (2D), each cell is either 0 or a [r,g,b,a] color
|
||||
var board = []
|
||||
|
||||
// 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
|
||||
|
||||
// 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]] }
|
||||
}
|
||||
var shapeKeys = Object.keys(SHAPES)
|
||||
|
||||
// Initialize board with empty (0)
|
||||
function initBoard() {
|
||||
board = []
|
||||
for (var r=0; r<ROWS; r++) {
|
||||
var row = []
|
||||
for (var c=0; c<COLS; c++) row.push(0)
|
||||
board.push(row)
|
||||
}
|
||||
}
|
||||
initBoard()
|
||||
|
||||
function randomShape() {
|
||||
var key = shapeKeys[Math.floor(Math.random()*shapeKeys.length)]
|
||||
// Make a copy of the shape’s blocks
|
||||
return {
|
||||
type: key,
|
||||
color: SHAPES[key].color,
|
||||
blocks: SHAPES[key].blocks.map(b => [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) {
|
||||
for (var i=0; i<blocks.length; i++) {
|
||||
var x = px + blocks[i][0]
|
||||
var y = py + blocks[i][1]
|
||||
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() {
|
||||
for (var i=0; i<piece.blocks.length; i++) {
|
||||
var x = pieceX + piece.blocks[i][0]
|
||||
var y = pieceY + piece.blocks[i][1]
|
||||
if (y>=0) board[y][x] = piece.color
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate 90° clockwise
|
||||
function rotate(blocks) {
|
||||
// (x,y) => (y,-x)
|
||||
for (var i=0; i<blocks.length; i++) {
|
||||
var x = blocks[i][0]
|
||||
var y = blocks[i][1]
|
||||
blocks[i][0] = y
|
||||
blocks[i][1] = -x
|
||||
}
|
||||
}
|
||||
|
||||
function clearLines() {
|
||||
var lines = 0
|
||||
for (var r=ROWS-1; r>=0;) {
|
||||
if (board[r].every(cell => cell)) {
|
||||
lines++
|
||||
// remove row
|
||||
board.splice(r,1)
|
||||
// add empty row on top
|
||||
var newRow = []
|
||||
for (var c=0; c<COLS; c++) newRow.push(0)
|
||||
board.unshift(newRow)
|
||||
} 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
|
||||
linesCleared += lines
|
||||
level = Math.floor(linesCleared/10)
|
||||
}
|
||||
|
||||
function placePiece() {
|
||||
lockPiece()
|
||||
clearLines()
|
||||
spawnPiece()
|
||||
}
|
||||
|
||||
// Hard drop
|
||||
function hardDrop() {
|
||||
while(!collides(pieceX, pieceY+1, piece.blocks)) pieceY++
|
||||
placePiece()
|
||||
}
|
||||
|
||||
spawnPiece()
|
||||
|
||||
this.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
|
||||
}
|
||||
|
||||
// Same logic for D
|
||||
if (rightPressed && !prevRight) {
|
||||
horizontalMove = 1
|
||||
hMoveTimer = hDelay
|
||||
} else if (rightPressed && hMoveTimer <= 0) {
|
||||
horizontalMove = 1
|
||||
hMoveTimer = hRepeat
|
||||
}
|
||||
|
||||
// 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++
|
||||
|
||||
// If neither A nor D is pressed, reset the timer so next press is immediate
|
||||
if (!leftPressed && !rightPressed) {
|
||||
hMoveTimer = 0
|
||||
}
|
||||
|
||||
// Decrement horizontal timer
|
||||
hMoveTimer -= dt
|
||||
prevLeft = leftPressed
|
||||
prevRight = rightPressed
|
||||
// ======= End Horizontal Movement Gate =======
|
||||
|
||||
// Rotate with W (once per press, no spinning)
|
||||
if (input.keyboard.down('w')) {
|
||||
if (!rotateHeld) {
|
||||
rotateHeld = true
|
||||
var test = piece.blocks.map(b => [b[0], b[1]])
|
||||
rotate(test)
|
||||
if (!collides(pieceX, pieceY, test)) piece.blocks = test
|
||||
}
|
||||
} else {
|
||||
rotateHeld = false
|
||||
}
|
||||
|
||||
// Soft drop if S is held (accelerates gravity)
|
||||
var fallSpeed = input.keyboard.down('s') ? 10 : 1
|
||||
|
||||
// Gravity
|
||||
gravityTimer += dt * fallSpeed
|
||||
var dropInterval = Math.max(0.1, baseGravity - level*0.05)
|
||||
if (gravityTimer >= dropInterval) {
|
||||
gravityTimer = 0
|
||||
if (!collides(pieceX, pieceY+1, piece.blocks)) {
|
||||
pieceY++
|
||||
} else {
|
||||
placePiece()
|
||||
}
|
||||
}
|
||||
|
||||
// Hard drop if space is held
|
||||
if (input.keyboard.down('space')) {
|
||||
// hardDrop()
|
||||
}
|
||||
}
|
||||
|
||||
this.hud = function() {
|
||||
// Clear screen
|
||||
draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1])
|
||||
|
||||
// Draw board
|
||||
for (var r=0; r<ROWS; r++) {
|
||||
for (var c=0; c<COLS; c++) {
|
||||
var cell = board[r][c]
|
||||
if (!cell) continue
|
||||
draw.rectangle({x:c*TILE, y:(ROWS-1-r)*TILE, width:TILE, height:TILE}, cell)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw falling piece
|
||||
if (!gameOver && piece) {
|
||||
for (var i=0; i<piece.blocks.length; i++) {
|
||||
var x = pieceX + piece.blocks[i][0]
|
||||
var y = pieceY + piece.blocks[i][1]
|
||||
draw.rectangle({x:x*TILE, y:(ROWS-1-y)*TILE, width:TILE, height:TILE}, piece.color)
|
||||
}
|
||||
}
|
||||
|
||||
// Next piece window
|
||||
draw.text("Next", {x:70, y:5, width:50, height:10}, null, 0, color.white)
|
||||
if (nextPiece) {
|
||||
for (var i=0; i<nextPiece.blocks.length; i++) {
|
||||
var nx = nextPiece.blocks[i][0]
|
||||
var ny = nextPiece.blocks[i][1]
|
||||
var dx = 12 + nx
|
||||
var dy = 16 - ny
|
||||
draw.rectangle({x:dx*TILE, y:(ROWS-1-dy)*TILE, width:TILE, height:TILE}, nextPiece.color)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
if (gameOver) {
|
||||
draw.text("GAME OVER", {x:10, y:config.height*0.5-5, width:config.width-20, height:20}, null, 0, color.red)
|
||||
}
|
||||
}
|
||||
|
||||
BIN
fonts/c64.ttf
Normal file
BIN
fonts/dos.ttf
Normal file
BIN
fonts/teenytinypixels.ttf
Normal file
248
geometry.cm
Normal file
@@ -0,0 +1,248 @@
|
||||
var geometry = this
|
||||
geometry[cell.DOC] = `
|
||||
A collection of geometry-related functions for circles, spheres, boxes, polygons,
|
||||
and rectangle utilities. Some functionality is implemented in C and exposed here.
|
||||
`
|
||||
|
||||
var math = use('math')
|
||||
|
||||
geometry.box = {}
|
||||
geometry.box[cell.DOC] = `
|
||||
An object for box-related operations. Overridden later by a function definition, so
|
||||
its direct usage is overshadowed. Contains:
|
||||
- points(ll, ur): Return an array of four 2D points for a box from ll (lower-left) to ur (upper-right).
|
||||
`
|
||||
|
||||
geometry.box.points = function (ll, ur) {
|
||||
return [ll, ll.add([ur.x - ll.x, 0]), ur, ll.add([0, ur.y - ll.y])]
|
||||
}
|
||||
geometry.box.points[cell.DOC] = `
|
||||
:param ll: Lower-left coordinate as a 2D vector (x,y).
|
||||
:param ur: Upper-right coordinate as a 2D vector (x,y).
|
||||
:return: An array of four points forming the corners of the box in order [ll, lower-right, ur, upper-left].
|
||||
Compute the four corners of a box given lower-left and upper-right corners.
|
||||
`
|
||||
|
||||
geometry.sphere = {}
|
||||
geometry.sphere[cell.DOC] = `
|
||||
Sphere-related geometry functions:
|
||||
- volume(r): Return the volume of a sphere with radius r.
|
||||
- random(r, theta, phi): Return a random point on or inside a sphere.
|
||||
`
|
||||
|
||||
geometry.circle = {}
|
||||
geometry.circle[cell.DOC] = `
|
||||
Circle-related geometry functions:
|
||||
- area(r): Return the area of a circle with radius r.
|
||||
- random(r, theta): Return a random 2D point on a circle; uses sphere.random internally and extracts x,z.
|
||||
`
|
||||
|
||||
geometry.sphere.volume = function (r) {
|
||||
return (Math.pi * r * r * r * 4) / 3
|
||||
}
|
||||
geometry.sphere.volume[cell.DOC] = `
|
||||
:param r: The sphere radius.
|
||||
:return: The volume of the sphere, calculated as (4/3) * pi * r^3.
|
||||
`
|
||||
|
||||
geometry.sphere.random = function (r, theta = [0, 1], phi = [-0.5, 0.5]) {
|
||||
if (typeof r == "number") r = [r, r]
|
||||
if (typeof theta == "number") theta = [theta, theta]
|
||||
if (typeof phi == "number") phi = [phi, phi]
|
||||
|
||||
var ra = Math.random_range(r[0], r[1])
|
||||
var ta = Math.turn2rad(Math.random_range(theta[0], theta[1]))
|
||||
var pa = Math.turn2rad(Math.random_range(phi[0], phi[1]))
|
||||
return [ra * Math.sin(ta) * Math.cos(pa), ra * Math.sin(ta) * Math.sin(pa), ra * Math.cos(ta)]
|
||||
}
|
||||
geometry.sphere.random[cell.DOC] = `
|
||||
:param r: A single number (radius) or a 2-element array [minRadius, maxRadius].
|
||||
:param theta: A single number or 2-element array defining the range in turns for the theta angle, default [0,1].
|
||||
:param phi: A single number or 2-element array defining the range in turns for the phi angle, default [-0.5,0.5].
|
||||
:return: A 3D point (x,y,z) randomly placed within a sphere.
|
||||
Generate a random point inside a sphere of variable radius, distributing angles in the specified ranges.
|
||||
`
|
||||
|
||||
geometry.circle.area = function (r) {
|
||||
return Math.pi * r * r
|
||||
}
|
||||
geometry.circle.area[cell.DOC] = `
|
||||
:param r: Radius of the circle.
|
||||
:return: The area, pi * r^2.
|
||||
`
|
||||
|
||||
geometry.circle.random = function (r, theta) {
|
||||
return geometry.sphere.random(r, theta).xz
|
||||
}
|
||||
geometry.circle.random[cell.DOC] = `
|
||||
:param r: A radius or [minRadius, maxRadius].
|
||||
:param theta: Angle range in turns (single number or [min,max]).
|
||||
:return: A 2D point (x,z) in the circle, using the sphere random generator and ignoring y.
|
||||
`
|
||||
|
||||
geometry.box = function (w, h) {
|
||||
w /= 2
|
||||
h /= 2
|
||||
var points = [
|
||||
[w, h],
|
||||
[-w, h],
|
||||
[-w, -h],
|
||||
[w, -h],
|
||||
]
|
||||
return points
|
||||
}
|
||||
geometry.box[cell.DOC] = `
|
||||
:param w: The width of the box.
|
||||
:param h: The height of the box.
|
||||
:return: An array of four 2D points representing the corners of a rectangle centered at [0,0].
|
||||
Construct a box centered at the origin with the given width and height. This overrides the box object above.
|
||||
`
|
||||
|
||||
geometry.ngon = function (radius, n) {
|
||||
return geometry.arc(radius, 360, n)
|
||||
}
|
||||
geometry.ngon[cell.DOC] = `
|
||||
:param radius: The radius of the n-gon from center to each vertex.
|
||||
:param n: Number of sides/vertices.
|
||||
:return: An array of 2D points forming a regular n-gon.
|
||||
Generates a regular n-gon by calling geometry.arc with full 360 degrees.
|
||||
`
|
||||
|
||||
geometry.arc = function (radius, angle, n, start = 0) {
|
||||
start = Math.deg2rad(start)
|
||||
if (angle >= 360) angle = 360
|
||||
if (n <= 1) return []
|
||||
var points = []
|
||||
angle = Math.deg2rad(angle)
|
||||
var arclen = angle / n
|
||||
for (var i = 0; i < n; i++) points.push(math.rotate([radius, 0], start + arclen * i))
|
||||
return points
|
||||
}
|
||||
geometry.arc[cell.DOC] = `
|
||||
:param radius: The distance from center to the arc points.
|
||||
:param angle: The total angle (in degrees) over which points are generated, capped at 360.
|
||||
:param n: Number of segments (if <=1, empty array is returned).
|
||||
:param start: Starting angle (in degrees), default 0.
|
||||
:return: An array of 2D points along the arc.
|
||||
Generate an arc (or partial circle) of n points, each angle spread equally over 'angle' degrees from 'start'.
|
||||
`
|
||||
|
||||
geometry.circle.points = function (radius, n) {
|
||||
if (n <= 1) return []
|
||||
return geometry.arc(radius, 360, n)
|
||||
}
|
||||
geometry.circle.points[cell.DOC] = `
|
||||
:param radius: The circle's radius.
|
||||
:param n: Number of points around the circle.
|
||||
:return: An array of 2D points equally spaced around a full 360-degree circle.
|
||||
Shortcut for geometry.arc(radius, 360, n).
|
||||
`
|
||||
|
||||
geometry.corners2points = function (ll, ur) {
|
||||
return [ll, ll.add([ur.x, 0]), ur, ll.add([0, ur.y])]
|
||||
}
|
||||
geometry.corners2points[cell.DOC] = `
|
||||
:param ll: Lower-left 2D coordinate.
|
||||
:param ur: Upper-right 2D coordinate (relative offset in x,y).
|
||||
:return: A four-point array of corners [ll, lower-right, upper-right, upper-left].
|
||||
Similar to box.points, but calculates differently.
|
||||
`
|
||||
|
||||
geometry.sortpointsccw = function (points) {
|
||||
var cm = points2cm(points)
|
||||
var cmpoints = points.map(function (x) { return x.sub(cm) })
|
||||
var ccw = cmpoints.sort(function (a, b) {
|
||||
var aatan = Math.atan2(a.y, a.x)
|
||||
var batan = Math.atan2(b.y, b.x)
|
||||
return aatan - batan
|
||||
})
|
||||
return ccw.map(function (x) { return x.add(cm) })
|
||||
}
|
||||
geometry.sortpointsccw[cell.DOC] = `
|
||||
:param points: An array of 2D points.
|
||||
:return: A new array of the same points, sorted counterclockwise around their centroid.
|
||||
Sort an array of points in CCW order based on their angles from the centroid.
|
||||
`
|
||||
|
||||
function points2cm(pts) {
|
||||
var x = 0
|
||||
var y = 0
|
||||
var n = pts.length
|
||||
pts.forEach(function (p) {
|
||||
x += p[0]
|
||||
y += p[1]
|
||||
})
|
||||
return [x / n, y / n]
|
||||
}
|
||||
|
||||
geometry.points2cm = function(points) {
|
||||
var x = 0
|
||||
var y = 0
|
||||
var n = points.length
|
||||
points.forEach(function (p) {
|
||||
x += p[0]
|
||||
y += p[1]
|
||||
})
|
||||
return [x / n, y / n]
|
||||
}
|
||||
geometry.points2cm[cell.DOC] = `
|
||||
:param points: An array of 2D points.
|
||||
:return: The centroid (average x,y) of the given points.
|
||||
`
|
||||
|
||||
geometry.rect_intersection[cell.DOC] = `
|
||||
:param a: The first rectangle as {x, y, w, h}.
|
||||
:param b: The second rectangle as {x, y, w, h}.
|
||||
:return: A rectangle that is the intersection of the two. May have zero width/height if no overlap.
|
||||
Return the intersection of two rectangles. The result may be empty if no intersection.
|
||||
`
|
||||
|
||||
geometry.rect_intersects[cell.DOC] = `
|
||||
:param a: Rectangle {x,y,w,h}.
|
||||
:param b: Rectangle {x,y,w,h}.
|
||||
:return: A boolean indicating whether the two rectangles overlap.
|
||||
`
|
||||
|
||||
geometry.rect_expand[cell.DOC] = `
|
||||
:param a: Rectangle {x,y,w,h}.
|
||||
:param b: Rectangle {x,y,w,h}.
|
||||
:return: A new rectangle that covers the bounds of both input rectangles.
|
||||
Merge or combine two rectangles, returning their bounding rectangle.
|
||||
`
|
||||
|
||||
geometry.rect_inside[cell.DOC] = `
|
||||
:param inner: A rectangle to test.
|
||||
:param outer: A rectangle that may contain 'inner'.
|
||||
:return: True if 'inner' is completely inside 'outer', otherwise false.
|
||||
`
|
||||
|
||||
geometry.rect_random[cell.DOC] = `
|
||||
:param rect: A rectangle {x,y,w,h}.
|
||||
:return: A random point within the rectangle (uniform distribution).
|
||||
`
|
||||
|
||||
geometry.cwh2rect[cell.DOC] = `
|
||||
:param center: A 2D point [cx, cy].
|
||||
:param wh: A 2D size [width, height].
|
||||
:return: A rectangle {x, y, w, h} with x,y set to center and w,h set to the given size.
|
||||
Helper: convert a center point and width/height vector to a rect object.
|
||||
`
|
||||
|
||||
geometry.rect_point_inside[cell.DOC] = `
|
||||
:param rect: A rectangle {x,y,w,h}.
|
||||
:param point: A 2D point [px, py].
|
||||
:return: True if the point lies inside the rectangle, otherwise false.
|
||||
`
|
||||
|
||||
geometry.rect_pos[cell.DOC] = `
|
||||
:param rect: A rectangle {x,y,w,h}.
|
||||
:return: A 2D vector [x,y] giving the rectangle's position.
|
||||
`
|
||||
|
||||
geometry.rect_move[cell.DOC] = `
|
||||
:param rect: A rectangle {x,y,w,h}.
|
||||
:param offset: A 2D vector to add to the rectangle's position.
|
||||
:return: A new rectangle with updated x,y offset.
|
||||
`
|
||||
|
||||
return geometry
|
||||
BIN
icons/moon.gif
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
icons/no_tex.gif
Normal file
|
After Width: | Height: | Size: 867 B |
BIN
icons_dev/airplane.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
icons_dev/ak47.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
icons_dev/amputation.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
icons_dev/ant.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
icons_dev/archer.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
icons_dev/armadillo.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
icons_dev/atom.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
icons_dev/banana.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
icons_dev/bank.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
icons_dev/banknote.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
icons_dev/barn.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
icons_dev/barrel.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
icons_dev/basket.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
icons_dev/bat.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
icons_dev/bed.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
icons_dev/belt.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
icons_dev/boar.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
icons_dev/broom.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
icons_dev/cabin.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
icons_dev/card-10-clubs.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
icons_dev/card-10-diamonds.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
icons_dev/card-10-hearts.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
icons_dev/card-10-spades.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
icons_dev/card-2-clubs.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
icons_dev/card-2-diamonds.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
icons_dev/card-2-hearts.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
icons_dev/card-2-spades.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
icons_dev/card-3-clubs.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
icons_dev/card-3-diamonds.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
icons_dev/card-3-hearts.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
icons_dev/card-3-spades.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
icons_dev/card-4-clubs.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
icons_dev/card-4-diamonds.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
icons_dev/card-4-hearts.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
icons_dev/card-4-spades.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
icons_dev/card-5-clubs.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
icons_dev/card-5-diamonds.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
icons_dev/card-5-hearts.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
icons_dev/card-5-spades.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
icons_dev/card-6-clubs.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
icons_dev/card-6-diamonds.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
icons_dev/card-6-hearts.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
icons_dev/card-6-spades.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
icons_dev/card-7-clubs.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
icons_dev/card-7-diamonds.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
icons_dev/card-7-hearts.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
icons_dev/card-7-spades.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
icons_dev/card-8-clubs.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
icons_dev/card-8-diamonds.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
icons_dev/card-8-hearts.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
icons_dev/card-8-spades.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
icons_dev/card-9-clubs.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
icons_dev/card-9-diamonds.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
icons_dev/card-9-hearts.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
icons_dev/card-9-spades.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
icons_dev/card-ace-clubs.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
icons_dev/card-ace-diamonds.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |