game-maker / suite (push) Failing after 3m59s
A shader is an asset carrying a node graph (Texture color, Output, Property, Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite wears a shader through its Shader picker, and the rows below it are the graph's Property nodes. Property schema, resolution and deviation take the project because a sprite's rows now live in another asset. The built-in registry of the prototype is gone; "Adjust colors" ships as a graph in the fixture and the three example games. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
966 lines
36 KiB
JavaScript
966 lines
36 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Writes the Tetris example from assets and graphs only. The board is a list of 200 numbers (-1 empty,
|
||
* else the resting shape); `piece`/`next` are shape/rot/x/y structs. The `Tetris` master owns the state
|
||
* and calls stateless behaviors as nodes, which lets `Fits` serve turning, moving and falling alike.
|
||
* Board and piece are a fixed pool of named objects (`c0…c199`, `p0…p3`); nothing spawns while playing.
|
||
*
|
||
* Usage: node tools/seed-tetris.mjs [projectId] [baseUrl]
|
||
*/
|
||
import {
|
||
edge,
|
||
field,
|
||
node,
|
||
port,
|
||
propertiesOf,
|
||
sceneObject,
|
||
tagged,
|
||
variable,
|
||
written,
|
||
} from './lib/builders.mjs';
|
||
import { PROJECTS, cliArgs, writeProject } from './lib/projects.mjs';
|
||
import { adjustColorsShader } from './lib/shaders.mjs';
|
||
|
||
const { projectId, baseUrl } = cliArgs('seed-tetris.mjs', PROJECTS.tetris);
|
||
|
||
const COLS = 10;
|
||
const ROWS = 20;
|
||
const CELL = 16;
|
||
/** Cell 0 sits at the top left of the board; these turn a column/row into the object's position. */
|
||
const CELL_X = (col) => col * CELL - (COLS * CELL) / 2 + CELL / 2;
|
||
const CELL_Y = (row) => row * CELL - (ROWS * CELL) / 2 + CELL / 2;
|
||
|
||
/** No shape has this number, so it cannot be mistaken for one that came to rest. */
|
||
const EMPTY = -1;
|
||
const EMPTY_GRID = Array.from({ length: COLS * ROWS }, () => EMPTY);
|
||
|
||
const BACKGROUND = 0x151821;
|
||
const WELL = 0x0d1017;
|
||
const WHITE = 0xf2f5f7;
|
||
const HINT = 0x9aa3b2;
|
||
|
||
/** One per shape, in the order of `SHAPES` below – and the `colors` variable as it stands. */
|
||
const SHAPE_COLORS = [0x2ec9d6, 0xf5c542, 0xa35fd6, 0x63c74d, 0xe0553f, 0x4a7de0, 0xe08a3f];
|
||
|
||
/**
|
||
* Each shape as its four cells in a box of `size` × `size`, the box being what turning rotates
|
||
* inside: (x, y) → (size − 1 − y, x) is a quarter turn clockwise. I needs a 4-box, O a 2-box.
|
||
*/
|
||
const SHAPES = [
|
||
{
|
||
name: 'I',
|
||
size: 4,
|
||
cells: [
|
||
[0, 1],
|
||
[1, 1],
|
||
[2, 1],
|
||
[3, 1],
|
||
],
|
||
},
|
||
{
|
||
name: 'O',
|
||
size: 2,
|
||
cells: [
|
||
[0, 0],
|
||
[1, 0],
|
||
[0, 1],
|
||
[1, 1],
|
||
],
|
||
},
|
||
{
|
||
name: 'T',
|
||
size: 3,
|
||
cells: [
|
||
[1, 0],
|
||
[0, 1],
|
||
[1, 1],
|
||
[2, 1],
|
||
],
|
||
},
|
||
{
|
||
name: 'S',
|
||
size: 3,
|
||
cells: [
|
||
[1, 0],
|
||
[2, 0],
|
||
[0, 1],
|
||
[1, 1],
|
||
],
|
||
},
|
||
{
|
||
name: 'Z',
|
||
size: 3,
|
||
cells: [
|
||
[0, 0],
|
||
[1, 0],
|
||
[1, 1],
|
||
[2, 1],
|
||
],
|
||
},
|
||
{
|
||
name: 'J',
|
||
size: 3,
|
||
cells: [
|
||
[0, 0],
|
||
[0, 1],
|
||
[1, 1],
|
||
[2, 1],
|
||
],
|
||
},
|
||
{
|
||
name: 'L',
|
||
size: 3,
|
||
cells: [
|
||
[2, 0],
|
||
[0, 1],
|
||
[1, 1],
|
||
[2, 1],
|
||
],
|
||
},
|
||
];
|
||
|
||
const turned = (cells, size) => cells.map(([x, y]) => [size - 1 - y, x]);
|
||
|
||
/**
|
||
* All 28 rotations in one list, four cells each, so one rotation starts at (shape·4 + rotation)·4
|
||
* and its n-th cell is one entry further along.
|
||
*/
|
||
function shapeTable() {
|
||
const table = [];
|
||
for (const shape of SHAPES) {
|
||
let cells = shape.cells;
|
||
for (let rotation = 0; rotation < 4; rotation++) {
|
||
for (const [x, y] of cells) table.push({ x, y });
|
||
cells = turned(cells, shape.size);
|
||
}
|
||
}
|
||
return table;
|
||
}
|
||
|
||
/** The four numbers that say which piece is where. */
|
||
const PIECE_FIELDS = [field('shape'), field('rot'), field('x'), field('y')];
|
||
const NO_PIECE = { shape: 0, rot: 0, x: 0, y: 0 };
|
||
|
||
/** The column a shape starts in, so every piece appears centred over the well. */
|
||
const spawnColumns = () => SHAPES.map((s) => Math.floor((COLS - s.size) / 2));
|
||
|
||
/** The well, the 200 board cells (switched off = empty) and the four cells of the falling piece. */
|
||
const boardTemplate = [
|
||
sceneObject('well', 'well-sprite', { name: 'Well' }),
|
||
...Array.from({ length: COLS * ROWS }, (_, i) =>
|
||
sceneObject(`cell${i}`, 'cell-sprite', {
|
||
name: `c${i}`,
|
||
x: CELL_X(i % COLS),
|
||
y: CELL_Y(Math.floor(i / COLS)),
|
||
z: 1,
|
||
overrides: { enabled: false },
|
||
}),
|
||
),
|
||
...Array.from({ length: 4 }, (_, i) =>
|
||
sceneObject(`piece${i}`, 'cell-sprite', { name: `p${i}`, z: 2, overrides: { enabled: false } }),
|
||
),
|
||
];
|
||
|
||
/** A `sets` entry whose value arrives through an edge instead of being typed on the node. */
|
||
const LINKED = Symbol('linked');
|
||
|
||
/**
|
||
* One graph under construction. Nodes are laid out in bands, one per strand, so the editor shows a
|
||
* behavior as a stack of readable blocks rather than one cloud. `variables` is what the plural nodes
|
||
* declare their ports from – pass the behavior's own list.
|
||
*/
|
||
function graphBuilder(variables = []) {
|
||
const nodes = [];
|
||
const edges = [];
|
||
let band = 0;
|
||
const kinds = Object.fromEntries(variables.map((v) => [v.key, v.kind]));
|
||
const declare = (keys, write = false) =>
|
||
keys.map((key) => {
|
||
if (!kinds[key]) throw new Error(`no variable "${key}" to declare a port for`);
|
||
return write ? written(key, kinds[key]) : port(key, kinds[key]);
|
||
});
|
||
|
||
const at = (id, type, column, row, params = {}, ports) => {
|
||
nodes.push(node(id, type, column * 220, band * 1400 + row * 90, params, ports));
|
||
return id;
|
||
};
|
||
const link = (from, fromPort, to, toPort) => {
|
||
edges.push(edge(`e${edges.length + 1}`, from, fromPort, to, toPort));
|
||
};
|
||
|
||
return {
|
||
at,
|
||
link,
|
||
/** Starts the next band. */
|
||
strand: () => band++,
|
||
/** Chains exec outputs: `flow(a, b, c)` runs a, then b, then c. */
|
||
flow: (...ids) => {
|
||
for (let i = 0; i < ids.length - 1; i++) link(ids[i], 'exec', ids[i + 1], 'exec');
|
||
},
|
||
/** A Variables node reading one variable; its output is named after it. */
|
||
get: (id, key, column, row) => at(id, 'Variables', column, row, {}, declare([key])),
|
||
/** A Variables node writing one variable, typed in as `value` or through its input of that name. */
|
||
set: (id, key, column, row, value) =>
|
||
at(id, 'Variables', column, row, value === undefined ? {} : { [key]: value }, declare([key], true)),
|
||
/**
|
||
* One node for a whole cluster. A `sets` reads all its inputs before it writes any, so an input
|
||
* fed from a variable it writes sees the value from before.
|
||
*/
|
||
gets: (id, keys, column, row) => at(id, 'Variables', column, row, {}, declare(keys)),
|
||
sets: (id, spec, column, row) => {
|
||
const keys = Array.isArray(spec) ? spec : Object.keys(spec);
|
||
const params = Array.isArray(spec)
|
||
? {}
|
||
: Object.fromEntries(Object.entries(spec).filter(([, v]) => v !== LINKED));
|
||
return at(id, 'Variables', column, row, params, declare(keys, true));
|
||
},
|
||
/** A mistyped id would silently leave an input unconnected, so the seed refuses to write one. */
|
||
done() {
|
||
const ids = new Set();
|
||
for (const n of nodes) {
|
||
if (ids.has(n.id)) throw new Error(`duplicate node id "${n.id}"`);
|
||
ids.add(n.id);
|
||
}
|
||
for (const e of edges) {
|
||
for (const end of [e.from.node, e.to.node]) {
|
||
if (!ids.has(end)) throw new Error(`edge ${e.id} points at unknown node "${end}"`);
|
||
}
|
||
}
|
||
return { nodes, edges };
|
||
},
|
||
};
|
||
}
|
||
|
||
/** `ObjectByName` with a name built at runtime: prefix + the round the loop is in. */
|
||
function objectOfRound(g, prefix, loop, namePrefix, column, row) {
|
||
const text = g.at(`${prefix}-text`, 'ToText', column, row, { decimals: 0 });
|
||
const name = g.at(`${prefix}-name`, 'Join', column + 1, row, { a: namePrefix });
|
||
const object = g.at(`${prefix}-obj`, 'ObjectByName', column + 2, row);
|
||
g.link(loop, 'index', text, 'value');
|
||
g.link(text, 'text', name, 'b');
|
||
g.link(name, 'text', object, 'name');
|
||
return object;
|
||
}
|
||
|
||
/**
|
||
* Where one of the four cells of a piece sits: reads the rotation out of `shapes` and adds the
|
||
* piece's own position. A behavior so the fit test and the drawing share it.
|
||
*/
|
||
function pieceCellGraph() {
|
||
const g = graphBuilder();
|
||
g.at('start', 'BehaviorStart', 0, 0, {}, [
|
||
port('shapes', 'list'),
|
||
port('piece', 'struct'),
|
||
port('corner', 'number'),
|
||
]);
|
||
|
||
// The whole piece arrives as one bundle and is taken apart here, where its parts are needed.
|
||
g.at('of', 'ExpandStruct', 1, 2, {}, PIECE_FIELDS);
|
||
g.at('x4', 'Multiply', 2, 2, { b: 4 });
|
||
g.at('plus', 'Add', 3, 2);
|
||
g.at('x4b', 'Multiply', 4, 2, { b: 4 });
|
||
g.at('index', 'Add', 5, 2);
|
||
g.at('cell', 'ItemAt', 6, 2);
|
||
g.at('at', 'ExpandStruct', 7, 2, {}, [field('x'), field('y')]);
|
||
g.at('sx', 'Add', 8, 1);
|
||
g.at('sy', 'Add', 8, 4);
|
||
g.at('end', 'BehaviorEnd', 9, 0, {}, [port('x', 'number'), port('y', 'number')]);
|
||
|
||
g.link('start', 'piece', 'of', 'value');
|
||
g.link('of', 'shape', 'x4', 'a');
|
||
g.link('x4', 'result', 'plus', 'a');
|
||
g.link('of', 'rot', 'plus', 'b');
|
||
g.link('plus', 'result', 'x4b', 'a');
|
||
g.link('x4b', 'result', 'index', 'a');
|
||
g.link('start', 'corner', 'index', 'b');
|
||
g.link('start', 'shapes', 'cell', 'list');
|
||
g.link('index', 'result', 'cell', 'index');
|
||
g.link('cell', 'item', 'at', 'value');
|
||
g.link('at', 'x', 'sx', 'a');
|
||
g.link('of', 'x', 'sx', 'b');
|
||
g.link('at', 'y', 'sy', 'a');
|
||
g.link('of', 'y', 'sy', 'b');
|
||
g.link('sx', 'result', 'end', 'x');
|
||
g.link('sy', 'result', 'end', 'y');
|
||
g.flow('start', 'end');
|
||
return g.done();
|
||
}
|
||
|
||
/**
|
||
* Whether the four cells of a piece may stand where it is asked to: inside the well, and on empty
|
||
* board cells. Every attempt to turn, move or drop goes through this one answer.
|
||
*/
|
||
const FITS_VARS = [variable('ok', 'boolean', true)];
|
||
|
||
function fitsGraph() {
|
||
const g = graphBuilder(FITS_VARS);
|
||
g.at('start', 'BehaviorStart', 0, 0, {}, [
|
||
port('shapes', 'list'),
|
||
port('grid', 'list'),
|
||
port('piece', 'struct'),
|
||
]);
|
||
g.set('ok', 'ok', 1, 0, true);
|
||
g.at('loop', 'Repeat', 2, 0, { times: 4 });
|
||
g.flow('start', 'ok', 'loop');
|
||
|
||
g.at('cell', 'RunBehavior', 3, 0, { behavior: 'piece-cell' });
|
||
g.link('loop', 'body', 'cell', 'exec');
|
||
for (const key of ['shapes', 'piece']) g.link('start', key, 'cell', key);
|
||
g.link('loop', 'index', 'cell', 'corner');
|
||
|
||
g.at('left', 'Compare', 4, 3, { op: '<', b: 0 });
|
||
g.at('right', 'Compare', 4, 5, { op: '>', b: COLS - 1 });
|
||
g.at('side', 'Or', 5, 4);
|
||
g.at('bottom', 'Compare', 4, 7, { op: '>', b: ROWS - 1 });
|
||
g.at('outside', 'Or', 6, 5);
|
||
g.link('cell', 'x', 'left', 'a');
|
||
g.link('cell', 'x', 'right', 'a');
|
||
g.link('left', 'result', 'side', 'a');
|
||
g.link('right', 'result', 'side', 'b');
|
||
g.link('cell', 'y', 'bottom', 'a');
|
||
g.link('side', 'result', 'outside', 'a');
|
||
g.link('bottom', 'result', 'outside', 'b');
|
||
|
||
// A cell outside the board reads as empty here, but the check above has already caught it.
|
||
g.at('row', 'Multiply', 4, 9, { b: COLS });
|
||
g.at('index', 'Add', 5, 9);
|
||
g.at('taken', 'ItemAt', 6, 9);
|
||
g.at('busy', 'Compare', 7, 9, { op: '>=', b: 0 });
|
||
g.at('blocked', 'Or', 8, 6);
|
||
g.at('br', 'Branch', 9, 0);
|
||
g.set('no', 'ok', 10, 0, false);
|
||
g.link('cell', 'y', 'row', 'a');
|
||
g.link('row', 'result', 'index', 'a');
|
||
g.link('cell', 'x', 'index', 'b');
|
||
g.link('start', 'grid', 'taken', 'list');
|
||
g.link('index', 'result', 'taken', 'index');
|
||
g.link('taken', 'item', 'busy', 'a');
|
||
g.link('outside', 'result', 'blocked', 'a');
|
||
g.link('busy', 'result', 'blocked', 'b');
|
||
g.link('cell', 'exec', 'br', 'exec');
|
||
g.link('blocked', 'result', 'br', 'condition');
|
||
g.link('br', 'then', 'no', 'exec');
|
||
|
||
g.at('end', 'BehaviorEnd', 11, 0, {}, [port('ok', 'boolean')]);
|
||
g.get('result', 'ok', 10, 3);
|
||
g.link('loop', 'done', 'end', 'exec');
|
||
g.link('result', 'ok', 'end', 'ok');
|
||
return g.done();
|
||
}
|
||
|
||
const CLEAR_LINES_VARS = [
|
||
variable('board', 'list', []),
|
||
variable('cleared', 'number', 0),
|
||
variable('row', 'number', 0),
|
||
];
|
||
|
||
/**
|
||
* Removes every full row and counts it. Walks the board from the bottom up; a cleared row does not
|
||
* move the walk on, because everything above has dropped into it – so the same row is looked at
|
||
* again, which is how two full rows on top of each other both go.
|
||
*/
|
||
function clearLinesGraph() {
|
||
const g = graphBuilder(CLEAR_LINES_VARS);
|
||
g.at('start', 'BehaviorStart', 0, 0, {}, [port('grid', 'list'), port('lines', 'number')]);
|
||
g.sets('start-vars', { board: LINKED, cleared: LINKED, row: ROWS - 1 }, 1, 0);
|
||
g.at('loop', 'Repeat', 4, 0, { times: ROWS + 4 });
|
||
g.link('start', 'grid', 'start-vars', 'board');
|
||
g.link('start', 'lines', 'start-vars', 'cleared');
|
||
g.flow('start', 'start-vars', 'loop');
|
||
|
||
// Every round reads all three before it writes any of them, and the loop drops the pulse cache
|
||
// between rounds – so one node serves the whole round.
|
||
g.gets('row-vars', ['row', 'board', 'cleared'], 5, 3);
|
||
g.at('left', 'Compare', 6, 3, { op: '>=', b: 0 });
|
||
g.at('brleft', 'Branch', 6, 0);
|
||
g.at('rowstart', 'Multiply', 7, 3, { b: COLS });
|
||
g.at('cells', 'SubList', 8, 4, { count: COLS });
|
||
g.at('gap', 'IndexOf', 9, 4, { value: EMPTY });
|
||
g.at('full', 'Compare', 10, 4, { op: '<', b: 0 });
|
||
g.at('brfull', 'Branch', 8, 0);
|
||
g.link('loop', 'body', 'brleft', 'exec');
|
||
g.link('row-vars', 'row', 'left', 'a');
|
||
g.link('left', 'result', 'brleft', 'condition');
|
||
g.link('row-vars', 'row', 'rowstart', 'a');
|
||
g.link('row-vars', 'board', 'cells', 'list');
|
||
g.link('rowstart', 'result', 'cells', 'start');
|
||
g.link('cells', 'result', 'gap', 'list');
|
||
g.link('gap', 'index', 'full', 'a');
|
||
g.link('brleft', 'then', 'brfull', 'exec');
|
||
g.link('full', 'result', 'brfull', 'condition');
|
||
|
||
g.at('above', 'SubList', 9, 7, { start: 0 });
|
||
g.at('past', 'Add', 8, 10, { b: COLS });
|
||
g.at('below', 'SubList', 9, 10, { count: COLS * ROWS });
|
||
g.at('blank', 'Fill', 9, 12, { value: EMPTY, count: COLS });
|
||
g.at('top', 'Concat', 10, 8);
|
||
g.at('rest', 'Concat', 11, 9);
|
||
g.at('onemore', 'Add', 12, 3, { b: 1 });
|
||
g.sets('cleared-row', ['board', 'cleared'], 10, 0);
|
||
g.link('brfull', 'then', 'cleared-row', 'exec');
|
||
g.link('row-vars', 'board', 'above', 'list');
|
||
g.link('rowstart', 'result', 'above', 'count');
|
||
g.link('rowstart', 'result', 'past', 'a');
|
||
g.link('row-vars', 'board', 'below', 'list');
|
||
g.link('past', 'result', 'below', 'start');
|
||
g.link('blank', 'result', 'top', 'a');
|
||
g.link('above', 'result', 'top', 'b');
|
||
g.link('top', 'result', 'rest', 'a');
|
||
g.link('below', 'result', 'rest', 'b');
|
||
g.link('rest', 'result', 'cleared-row', 'board');
|
||
g.link('row-vars', 'cleared', 'onemore', 'a');
|
||
g.link('onemore', 'result', 'cleared-row', 'cleared');
|
||
|
||
g.at('back', 'Subtract', 9, 14, { b: 1 });
|
||
g.set('setrow', 'row', 10, 13);
|
||
g.link('brfull', 'else', 'setrow', 'exec');
|
||
g.link('row-vars', 'row', 'back', 'a');
|
||
g.link('back', 'result', 'setrow', 'row');
|
||
|
||
g.at('end', 'BehaviorEnd', 13, 0, {}, [port('grid', 'list'), port('lines', 'number')]);
|
||
g.gets('out-vars', ['board', 'cleared'], 12, 6);
|
||
g.link('loop', 'done', 'end', 'exec');
|
||
g.link('out-vars', 'board', 'end', 'grid');
|
||
g.link('out-vars', 'cleared', 'end', 'lines');
|
||
return g.done();
|
||
}
|
||
|
||
/**
|
||
* The board as it stands, cell by cell: switched off where the list says empty, in the colour of
|
||
* the shape that came to rest there otherwise. No end node – it answers nothing, it only draws.
|
||
*/
|
||
function drawBoardGraph() {
|
||
const g = graphBuilder();
|
||
g.at('start', 'BehaviorStart', 0, 0, {}, [port('grid', 'list'), port('colors', 'list')]);
|
||
g.at('loop', 'Repeat', 1, 0, { times: COLS * ROWS });
|
||
g.flow('start', 'loop');
|
||
|
||
g.at('cell', 'ItemAt', 2, 4);
|
||
g.at('filled', 'Compare', 3, 4, { op: '>=', b: 0 });
|
||
const object = objectOfRound(g, 'c', 'loop', 'c', 2, 6);
|
||
g.at('show', 'Properties', 6, 0, propertiesOf('sprite'), [written('enabled', 'boolean')]);
|
||
g.at('br', 'Branch', 7, 0);
|
||
g.at('color', 'ItemAt', 7, 4);
|
||
g.at('paint', 'Properties', 8, 0, propertiesOf('sprite'), [written('color', 'color')]);
|
||
g.link('start', 'grid', 'cell', 'list');
|
||
g.link('loop', 'index', 'cell', 'index');
|
||
g.link('cell', 'item', 'filled', 'a');
|
||
g.link('loop', 'body', 'show', 'exec');
|
||
g.link(object, 'object', 'show', 'object');
|
||
g.link('filled', 'result', 'show', 'enabled');
|
||
g.link('filled', 'result', 'br', 'condition');
|
||
g.link('start', 'colors', 'color', 'list');
|
||
g.link('cell', 'item', 'color', 'index');
|
||
g.link(object, 'object', 'paint', 'object');
|
||
g.link('color', 'item', 'paint', 'color');
|
||
g.flow('show', 'br');
|
||
g.link('br', 'then', 'paint', 'exec');
|
||
return g.done();
|
||
}
|
||
|
||
/** Which piece appears next and in which column: one of the seven, centred over the well. */
|
||
function spawnPieceGraph() {
|
||
const g = graphBuilder();
|
||
g.at('start', 'BehaviorStart', 0, 0, {}, [port('spawnX', 'list')]);
|
||
g.at('random', 'Random', 1, 2, { min: 0, max: SHAPES.length });
|
||
g.at('shape', 'Floor', 2, 2);
|
||
g.at('column', 'ItemAt', 3, 2);
|
||
// Handed back as a finished piece rather than as parts the caller has to assemble: the two
|
||
// fields it does not choose are the ones a new piece is defined to start at.
|
||
g.at('piece', 'PackStruct', 4, 2, { rot: 0, y: 0 }, PIECE_FIELDS);
|
||
g.at('end', 'BehaviorEnd', 5, 0, {}, [port('piece', 'struct')]);
|
||
g.link('random', 'result', 'shape', 'value');
|
||
g.link('start', 'spawnX', 'column', 'list');
|
||
g.link('shape', 'result', 'column', 'index');
|
||
g.link('shape', 'result', 'piece', 'shape');
|
||
g.link('column', 'item', 'piece', 'x');
|
||
g.link('piece', 'value', 'end', 'piece');
|
||
g.flow('start', 'end');
|
||
return g.done();
|
||
}
|
||
|
||
const NEXT_MOVE_VARS = [
|
||
variable('next', 'struct', NO_PIECE, PIECE_FIELDS),
|
||
variable('kind', 'text', ''),
|
||
variable('outFall', 'number', 0),
|
||
variable('outDx', 'number', 0),
|
||
variable('outRot', 'number', 0),
|
||
];
|
||
|
||
/**
|
||
* The one move a frame attempts, and nothing else: turning beats moving beats falling. It only
|
||
* proposes – whether the piece may go there is `Fits`, one step later. A wish that was granted
|
||
* comes back as 0, which is how a key press is spent exactly once.
|
||
*/
|
||
function nextMoveGraph() {
|
||
const g = graphBuilder(NEXT_MOVE_VARS);
|
||
const { at, link, flow, get, set, sets } = g;
|
||
|
||
at('start', 'BehaviorStart', 0, 0, {}, [
|
||
port('piece', 'struct'),
|
||
port('wantDx', 'number'),
|
||
port('wantRot', 'number'),
|
||
port('fall', 'number'),
|
||
port('dt', 'number'),
|
||
port('stepTime', 'number'),
|
||
]);
|
||
|
||
// The frame starts out having decided nothing: the candidate is the piece itself and no wish has
|
||
// been used up. Each strand below then packs only the field it changes, because writing a bundle
|
||
// keeps the fields it does not name – "the same piece, one row lower" needs no copying.
|
||
at('of', 'ExpandStruct', 1, 3, {}, PIECE_FIELDS);
|
||
link('start', 'piece', 'of', 'value');
|
||
sets('i-vars', { next: LINKED, kind: '', outFall: LINKED, outDx: LINKED, outRot: LINKED }, 1, 0);
|
||
link('start', 'piece', 'i-vars', 'next');
|
||
link('start', 'fall', 'i-vars', 'outFall');
|
||
link('start', 'wantDx', 'i-vars', 'outDx');
|
||
link('start', 'wantRot', 'i-vars', 'outRot');
|
||
flow('start', 'i-vars', 't-br');
|
||
|
||
// Turning wins: a quarter turn clockwise is one step around the four rotations of the shape.
|
||
g.strand();
|
||
at('t-want', 'Compare', 0, 3, { op: '>', b: 0 });
|
||
at('t-br', 'Branch', 0, 0);
|
||
at('t-plus1', 'Add', 1, 3, { b: 1 });
|
||
at('t-wrap', 'Modulo', 2, 3, { b: 4 });
|
||
at('t-pack', 'PackStruct', 3, 3, {}, [field('rot')]);
|
||
sets('t-vars', { next: LINKED, kind: 'turn', outRot: 0 }, 1, 0);
|
||
link('start', 'wantRot', 't-want', 'a');
|
||
link('t-want', 'result', 't-br', 'condition');
|
||
link('t-br', 'then', 't-vars', 'exec');
|
||
link('of', 'rot', 't-plus1', 'a');
|
||
link('t-plus1', 'result', 't-wrap', 'a');
|
||
link('t-wrap', 'result', 't-pack', 'rot');
|
||
link('t-pack', 'value', 't-vars', 'next');
|
||
flow('t-vars', 'end');
|
||
|
||
// Then sideways, one column per key press.
|
||
at('m-still', 'Equals', 4, 3, { b: 0 });
|
||
at('m-moving', 'Not', 5, 3);
|
||
at('m-br', 'Branch', 5, 0);
|
||
at('m-shift', 'Add', 6, 3);
|
||
at('m-pack', 'PackStruct', 7, 3, {}, [field('x')]);
|
||
sets('m-vars', { next: LINKED, kind: 'move', outDx: 0 }, 6, 0);
|
||
link('t-br', 'else', 'm-br', 'exec');
|
||
link('start', 'wantDx', 'm-still', 'a');
|
||
link('m-still', 'result', 'm-moving', 'value');
|
||
link('m-moving', 'result', 'm-br', 'condition');
|
||
link('m-br', 'then', 'm-vars', 'exec');
|
||
link('of', 'x', 'm-shift', 'a');
|
||
link('start', 'wantDx', 'm-shift', 'b');
|
||
link('m-shift', 'result', 'm-pack', 'x');
|
||
link('m-pack', 'value', 'm-vars', 'next');
|
||
flow('m-vars', 'end');
|
||
|
||
// And otherwise the clock: holding the down key lets it run ten times as fast.
|
||
g.strand();
|
||
at('f-down', 'KeyIsDown', 0, 4, { key: 'ArrowDown' });
|
||
at('f-boost', 'Multiply', 1, 4, { b: 9 });
|
||
at('f-rate', 'Add', 2, 4, { a: 1 });
|
||
at('f-step', 'Multiply', 3, 3);
|
||
at('f-add', 'Add', 4, 3);
|
||
set('f-set', 'outFall', 0, 0);
|
||
link('m-br', 'else', 'f-set', 'exec');
|
||
link('f-down', 'down', 'f-boost', 'a');
|
||
link('f-boost', 'result', 'f-rate', 'b');
|
||
link('start', 'dt', 'f-step', 'a');
|
||
link('f-rate', 'result', 'f-step', 'b');
|
||
link('start', 'fall', 'f-add', 'a');
|
||
link('f-step', 'result', 'f-add', 'b');
|
||
link('f-add', 'result', 'f-set', 'outFall');
|
||
|
||
get('f-fall', 'outFall', 5, 3);
|
||
at('f-due', 'Compare', 6, 3, { op: '>=' });
|
||
at('f-br', 'Branch', 5, 0);
|
||
at('f-minus', 'Subtract', 7, 3);
|
||
at('f-down1', 'Add', 7, 5, { b: 1 });
|
||
at('f-pack', 'PackStruct', 8, 5, {}, [field('y')]);
|
||
sets('f-vars', { outFall: LINKED, next: LINKED, kind: 'fall' }, 6, 0);
|
||
flow('f-set', 'f-br');
|
||
link('f-fall', 'outFall', 'f-due', 'a');
|
||
link('start', 'stepTime', 'f-due', 'b');
|
||
link('f-due', 'result', 'f-br', 'condition');
|
||
link('f-br', 'then', 'f-vars', 'exec');
|
||
link('f-fall', 'outFall', 'f-minus', 'a');
|
||
link('start', 'stepTime', 'f-minus', 'b');
|
||
link('f-minus', 'result', 'f-vars', 'outFall');
|
||
link('of', 'y', 'f-down1', 'a');
|
||
link('f-down1', 'result', 'f-pack', 'y');
|
||
link('f-pack', 'value', 'f-vars', 'next');
|
||
flow('f-vars', 'end');
|
||
// Not due yet: nothing moves, and "kind" stays empty so the fit test below changes nothing.
|
||
link('f-br', 'else', 'end', 'exec');
|
||
|
||
// Whatever the frame decided is handed back – the caller owns the variables, not this behavior.
|
||
g.strand();
|
||
at('end', 'BehaviorEnd', 2, 0, {}, [
|
||
port('next', 'struct'),
|
||
port('kind', 'text'),
|
||
port('fall', 'number'),
|
||
port('wantDx', 'number'),
|
||
port('wantRot', 'number'),
|
||
]);
|
||
const back = [
|
||
['next', 'next'],
|
||
['kind', 'kind'],
|
||
['outFall', 'fall'],
|
||
['outDx', 'wantDx'],
|
||
['outRot', 'wantRot'],
|
||
];
|
||
g.gets(
|
||
'e-vars',
|
||
back.map(([key]) => key),
|
||
0,
|
||
2,
|
||
);
|
||
for (const [key, portKey] of back) link('e-vars', key, 'end', portKey);
|
||
return g.done();
|
||
}
|
||
|
||
/**
|
||
* Moves the four cell objects of the falling piece to where the piece is. This is the only place
|
||
* that touches them, so where they stand *is* where the piece is – which is what `Lock piece`
|
||
* reads back instead of working the shape out a second time.
|
||
*/
|
||
function drawPieceGraph() {
|
||
const g = graphBuilder();
|
||
const { at, link, flow } = g;
|
||
|
||
at('start', 'BehaviorStart', 0, 0, {}, [
|
||
port('shapes', 'list'),
|
||
port('piece', 'struct'),
|
||
port('colors', 'list'),
|
||
]);
|
||
at('loop', 'Repeat', 1, 0, { times: 4 });
|
||
at('cell', 'RunBehavior', 2, 0, { behavior: 'piece-cell' });
|
||
flow('start', 'loop');
|
||
link('loop', 'body', 'cell', 'exec');
|
||
for (const key of ['shapes', 'piece']) link('start', key, 'cell', key);
|
||
link('loop', 'index', 'cell', 'corner');
|
||
// Only the shape number is wanted here, for the colour.
|
||
at('of', 'ExpandStruct', 2, 10, {}, [field('shape')]);
|
||
link('start', 'piece', 'of', 'value');
|
||
|
||
const object = objectOfRound(g, 'p', 'loop', 'p', 3, 12);
|
||
at('x', 'Multiply', 3, 4, { b: CELL });
|
||
at('atx', 'Add', 4, 4, { b: CELL_X(0) });
|
||
at('y', 'Multiply', 3, 6, { b: CELL });
|
||
at('aty', 'Add', 4, 6, { b: CELL_Y(0) });
|
||
at('color', 'ItemAt', 4, 8);
|
||
link('cell', 'x', 'x', 'a');
|
||
link('x', 'result', 'atx', 'a');
|
||
link('cell', 'y', 'y', 'a');
|
||
link('y', 'result', 'aty', 'a');
|
||
link('start', 'colors', 'color', 'list');
|
||
link('of', 'shape', 'color', 'index');
|
||
|
||
at('place', 'SetPosition', 6, 0);
|
||
at('paint', 'Properties', 7, 0, { ...propertiesOf('sprite'), enabled: true }, [
|
||
written('enabled', 'boolean'),
|
||
written('color', 'color'),
|
||
]);
|
||
link('cell', 'exec', 'place', 'exec');
|
||
for (const id of ['place', 'paint']) link(object, 'object', id, 'object');
|
||
link('atx', 'result', 'place', 'x');
|
||
link('aty', 'result', 'place', 'y');
|
||
link('color', 'item', 'paint', 'color');
|
||
flow('place', 'paint');
|
||
return g.done();
|
||
}
|
||
|
||
/** Writes the four cells of the landed piece into the board, read back from where they stand. */
|
||
const LOCK_PIECE_VARS = [variable('board', 'list', [])];
|
||
|
||
function lockPieceGraph() {
|
||
const g = graphBuilder(LOCK_PIECE_VARS);
|
||
const { at, link, flow, get, set } = g;
|
||
|
||
at('start', 'BehaviorStart', 0, 0, {}, [port('grid', 'list'), port('shape', 'number')]);
|
||
set('take', 'board', 1, 0);
|
||
at('loop', 'Repeat', 2, 0, { times: 4 });
|
||
link('start', 'grid', 'take', 'board');
|
||
flow('start', 'take', 'loop');
|
||
|
||
const object = objectOfRound(g, 'p', 'loop', 'p', 3, 4);
|
||
at('pos', 'GetPosition', 6, 4);
|
||
at('offx', 'Add', 7, 3, { b: -CELL_X(0) });
|
||
at('offy', 'Add', 7, 6, { b: -CELL_Y(0) });
|
||
at('col', 'Divide', 8, 3, { b: CELL });
|
||
at('rowof', 'Divide', 8, 6, { b: CELL });
|
||
at('colr', 'Round', 9, 3);
|
||
at('rowr', 'Round', 9, 6);
|
||
link(object, 'object', 'pos', 'object');
|
||
link('pos', 'x', 'offx', 'a');
|
||
link('pos', 'y', 'offy', 'a');
|
||
link('offx', 'result', 'col', 'a');
|
||
link('offy', 'result', 'rowof', 'a');
|
||
link('col', 'result', 'colr', 'value');
|
||
link('rowof', 'result', 'rowr', 'value');
|
||
|
||
at('row10', 'Multiply', 10, 6, { b: COLS });
|
||
at('index', 'Add', 11, 5);
|
||
get('board', 'board', 11, 8);
|
||
at('write', 'SetItem', 12, 6);
|
||
set('keep', 'board', 3, 0);
|
||
link('rowr', 'result', 'row10', 'a');
|
||
link('row10', 'result', 'index', 'a');
|
||
link('colr', 'result', 'index', 'b');
|
||
link('board', 'board', 'write', 'list');
|
||
link('index', 'result', 'write', 'index');
|
||
link('start', 'shape', 'write', 'value');
|
||
link('write', 'result', 'keep', 'board');
|
||
link('loop', 'body', 'keep', 'exec');
|
||
|
||
get('done', 'board', 4, 3);
|
||
at('end', 'BehaviorEnd', 4, 0, {}, [port('grid', 'list')]);
|
||
link('loop', 'done', 'end', 'exec');
|
||
link('done', 'board', 'end', 'grid');
|
||
return g.done();
|
||
}
|
||
|
||
/** The one place that writes the counter on screen. */
|
||
function showLinesGraph() {
|
||
const g = graphBuilder();
|
||
g.at('start', 'BehaviorStart', 0, 0, {}, [port('lines', 'number')]);
|
||
g.at('number', 'ToText', 1, 3, { decimals: 0 });
|
||
g.at('caption', 'Join', 2, 3, { a: 'Lines ' });
|
||
g.at('label', 'ObjectByName', 1, 5, { name: 'LinesLabel' });
|
||
g.at('write', 'Properties', 3, 0, propertiesOf('text'), [written('text', 'text')]);
|
||
g.link('start', 'lines', 'number', 'value');
|
||
g.link('number', 'text', 'caption', 'b');
|
||
g.link('caption', 'text', 'write', 'text');
|
||
g.link('label', 'object', 'write', 'object');
|
||
g.flow('start', 'write');
|
||
return g.done();
|
||
}
|
||
|
||
/**
|
||
* The master graph. It owns the state and does none of the work: every step below is a behavior of
|
||
* its own, called as a node. What is left here is the order the steps run in and which variable
|
||
* each answer lands in.
|
||
*/
|
||
function tetrisGraph() {
|
||
const g = graphBuilder(tetrisVariables);
|
||
const { at, link, flow, get, set, gets, sets, strand } = g;
|
||
|
||
// 1) On start: an empty board, no piece, nothing pressed. The world is rebuilt for every run, so
|
||
// this only matters when the behavior was left running in the editor.
|
||
strand();
|
||
at('s-start', 'OnStart', 0, 0);
|
||
at('s-empty', 'Fill', 0, 3, { value: EMPTY, count: COLS * ROWS });
|
||
sets('s-vars', { grid: LINKED, lines: 0, hasPiece: false, fall: 0, wantDx: 0, wantRot: 0 }, 1, 0);
|
||
at('s-show', 'RunBehavior', 2, 0, { behavior: 'show-lines', lines: 0 });
|
||
link('s-empty', 'result', 's-vars', 'grid');
|
||
flow('s-start', 's-vars', 's-show');
|
||
|
||
// 2) The keys do not move anything themselves – they leave a wish that the next frame grants. That
|
||
// way every attempt to move goes through the one test below, whatever asked for it.
|
||
strand();
|
||
at('k-left', 'OnKeyDown', 0, 0, { key: 'ArrowLeft' });
|
||
set('k-setleft', 'wantDx', 1, 0, -1);
|
||
at('k-right', 'OnKeyDown', 0, 3, { key: 'ArrowRight' });
|
||
set('k-setright', 'wantDx', 1, 3, 1);
|
||
at('k-up', 'OnKeyDown', 0, 6, { key: 'ArrowUp' });
|
||
set('k-setup', 'wantRot', 1, 6, 1);
|
||
flow('k-left', 'k-setleft');
|
||
flow('k-right', 'k-setright');
|
||
flow('k-up', 'k-setup');
|
||
|
||
// 3) Every frame does one thing: either a new piece appears, or the piece attempts one move.
|
||
strand();
|
||
at('u-update', 'OnUpdate', 0, 0);
|
||
get('u-has', 'hasPiece', 0, 2);
|
||
at('u-br', 'Branch', 1, 0);
|
||
flow('u-update', 'u-br');
|
||
link('u-has', 'hasPiece', 'u-br', 'condition');
|
||
|
||
// 3a) No piece: ask for one and write down what came back.
|
||
at('n-spawn', 'RunBehavior', 2, 5, { behavior: 'spawn-piece' });
|
||
get('n-columns', 'spawnX', 2, 8);
|
||
sets('n-vars', { piece: LINKED, fall: 0, hasPiece: true }, 3, 5);
|
||
link('u-br', 'else', 'n-spawn', 'exec');
|
||
link('n-columns', 'spawnX', 'n-spawn', 'spawnX');
|
||
link('n-spawn', 'piece', 'n-vars', 'piece');
|
||
flow('n-spawn', 'n-vars');
|
||
|
||
// 3b) A piece is falling: one behavior decides which single move this frame attempts.
|
||
strand();
|
||
at('c-move', 'RunBehavior', 0, 0, { behavior: 'next-move' });
|
||
link('u-br', 'then', 'c-move', 'exec');
|
||
// Every port of the call is a variable of the same name, so one node stands at each side of it.
|
||
const moveIn = ['piece', 'wantDx', 'wantRot', 'fall', 'stepTime'];
|
||
gets('c-in', moveIn, 0, 3);
|
||
for (const key of moveIn) link('c-in', key, 'c-move', key);
|
||
link('u-update', 'dt', 'c-move', 'dt');
|
||
|
||
const moveOut = ['next', 'kind', 'fall', 'wantDx', 'wantRot'];
|
||
sets('c-out', moveOut, 1, 0);
|
||
for (const key of moveOut) link('c-move', key, 'c-out', key);
|
||
flow('c-move', 'c-out');
|
||
|
||
// 4) The one test, and it is one node: does the candidate fit? Every attempt above ends here.
|
||
strand();
|
||
at('t-fits', 'RunBehavior', 0, 0, { behavior: 'fits' });
|
||
gets('t-in', ['shapes', 'grid', 'next'], 0, 3);
|
||
flow('c-out', 't-fits');
|
||
link('t-in', 'shapes', 't-fits', 'shapes');
|
||
link('t-in', 'grid', 't-fits', 'grid');
|
||
link('t-in', 'next', 't-fits', 'piece');
|
||
|
||
// 5) It fits: the candidate becomes the piece. It does not: only a fall means anything – the
|
||
// piece has landed (or, still at the very top, the well is full).
|
||
at('m-br', 'Branch', 1, 0);
|
||
// `piece` is not in here: it is written a line below, and `m-py2` has to see that write.
|
||
gets('m-in', ['next', 'kind'], 2, 3);
|
||
set('m-piece', 'piece', 2, 0);
|
||
flow('t-fits', 'm-br');
|
||
link('t-fits', 'ok', 'm-br', 'condition');
|
||
link('m-br', 'then', 'm-piece', 'exec');
|
||
link('m-in', 'next', 'm-piece', 'piece');
|
||
|
||
at('m-landed', 'Equals', 3, 7, { b: 'fall' });
|
||
at('m-brland', 'Branch', 3, 5);
|
||
get('m-py2', 'piece', 4, 8);
|
||
at('m-pieceY', 'ExpandStruct', 5, 10, {}, [field('y')]);
|
||
at('m-attop', 'Compare', 6, 8, { op: '<=', b: 0 });
|
||
at('m-brover', 'Branch', 5, 5);
|
||
at('m-stop', 'StopGame', 6, 5);
|
||
link('m-br', 'else', 'm-brland', 'exec');
|
||
link('m-in', 'kind', 'm-landed', 'a');
|
||
link('m-landed', 'result', 'm-brland', 'condition');
|
||
link('m-brland', 'then', 'm-brover', 'exec');
|
||
link('m-py2', 'piece', 'm-pieceY', 'value');
|
||
link('m-pieceY', 'y', 'm-attop', 'a');
|
||
link('m-attop', 'result', 'm-brover', 'condition');
|
||
link('m-brover', 'then', 'm-stop', 'exec');
|
||
|
||
// 6) Wherever the piece ended up – newly spawned or moved – it is drawn there.
|
||
strand();
|
||
at('d-draw', 'RunBehavior', 0, 0, { behavior: 'draw-piece' });
|
||
flow('m-piece', 'd-draw');
|
||
flow('n-vars', 'd-draw');
|
||
const drawIn = ['shapes', 'piece', 'colors'];
|
||
gets('d-in', drawIn, 0, 3);
|
||
for (const key of drawIn) link('d-in', key, 'd-draw', key);
|
||
|
||
// 7) It landed: it becomes part of the board, full lines go, the counter and the board follow.
|
||
strand();
|
||
at('l-lock', 'RunBehavior', 0, 0, { behavior: 'lock-piece' });
|
||
gets('l-in', ['grid', 'piece'], 0, 3);
|
||
at('l-shape', 'ExpandStruct', 0, 7, {}, [field('shape')]);
|
||
set('l-setgrid', 'grid', 1, 0);
|
||
link('m-brover', 'else', 'l-lock', 'exec');
|
||
link('l-in', 'grid', 'l-lock', 'grid');
|
||
link('l-in', 'piece', 'l-shape', 'value');
|
||
link('l-shape', 'shape', 'l-lock', 'shape');
|
||
link('l-lock', 'grid', 'l-setgrid', 'grid');
|
||
flow('l-lock', 'l-setgrid');
|
||
|
||
at('r-clear', 'RunBehavior', 2, 0, { behavior: 'clear-lines' });
|
||
gets('r-in', ['grid', 'lines'], 2, 3);
|
||
sets('r-out', ['grid', 'lines'], 3, 0);
|
||
link('r-in', 'grid', 'r-clear', 'grid');
|
||
link('r-in', 'lines', 'r-clear', 'lines');
|
||
link('r-clear', 'grid', 'r-out', 'grid');
|
||
link('r-clear', 'lines', 'r-out', 'lines');
|
||
flow('l-setgrid', 'r-clear', 'r-out');
|
||
|
||
at('b-show', 'RunBehavior', 5, 0, { behavior: 'show-lines' });
|
||
at('b-draw', 'RunBehavior', 6, 0, { behavior: 'draw-board' });
|
||
// A second reader of `grid` and `lines`, because `r-out` wrote them since `r-in` was asked.
|
||
gets('b-in', ['lines', 'grid', 'colors'], 5, 3);
|
||
set('b-nopiece', 'hasPiece', 7, 0, false);
|
||
link('b-in', 'lines', 'b-show', 'lines');
|
||
link('b-in', 'grid', 'b-draw', 'grid');
|
||
link('b-in', 'colors', 'b-draw', 'colors');
|
||
// The board is drawn and the piece is part of it now: the next frame draws a new one.
|
||
flow('r-out', 'b-show', 'b-draw', 'b-nopiece');
|
||
return g.done();
|
||
}
|
||
|
||
const label = (id, name, x, y, text, size, color, align = 'center') =>
|
||
sceneObject(id, 'label', { name, x, y, overrides: { text, fontSize: size, color, align } });
|
||
|
||
/** A behavior asset whose `props` are exactly the declared variables' defaults. */
|
||
function behavior(id, name, variables, graph) {
|
||
const props = {};
|
||
for (const v of variables) props[v.key] = v.default;
|
||
return { id, type: 'behavior', name, props, variables, graph };
|
||
}
|
||
|
||
const tetrisVariables = [
|
||
variable('grid', 'list', EMPTY_GRID),
|
||
variable('shapes', 'list', shapeTable()),
|
||
// Tagged, because a list says nothing about its items and a colour is a number like any other.
|
||
variable(
|
||
'colors',
|
||
'list',
|
||
SHAPE_COLORS.map((color) => tagged('color', color)),
|
||
),
|
||
variable('spawnX', 'list', spawnColumns()),
|
||
// The falling piece and the one move being tried on it – four numbers each, and never apart.
|
||
variable('piece', 'struct', NO_PIECE, PIECE_FIELDS),
|
||
variable('next', 'struct', NO_PIECE, PIECE_FIELDS),
|
||
variable('kind', 'text', ''),
|
||
variable('hasPiece', 'boolean', false),
|
||
variable('fall', 'number', 0),
|
||
variable('stepTime', 'number', 0.5),
|
||
variable('lines', 'number', 0),
|
||
variable('wantDx', 'number', 0),
|
||
variable('wantRot', 'number', 0),
|
||
];
|
||
|
||
const project = {
|
||
formatVersion: 2,
|
||
id: projectId,
|
||
name: 'Tetris',
|
||
activeSceneId: 'scene1',
|
||
assets: [
|
||
{
|
||
id: 'scene1',
|
||
type: 'scene',
|
||
name: 'Tetris',
|
||
props: { backgroundColor: BACKGROUND },
|
||
objects: [
|
||
sceneObject('oboard', 'board', { name: 'Board', x: 150 }),
|
||
label('otitle', 'Title', -110, -110, 'TETRIS', 44, WHITE),
|
||
label('olines', 'LinesLabel', -110, -40, 'Lines 0', 26, WHITE),
|
||
label('ohint1', 'Hint', -110, 60, 'Left / Right · move', 16, HINT),
|
||
label('ohint2', 'Hint 2', -110, 84, 'Up · turn', 16, HINT),
|
||
label('ohint3', 'Hint 3', -110, 108, 'Down · drop faster', 16, HINT),
|
||
sceneObject('ocam', 'cam', { name: 'Main Camera' }),
|
||
sceneObject('ogame', 'tetris', { name: 'Tetris', x: -260, y: 150 }),
|
||
],
|
||
},
|
||
{ id: 'cam', type: 'camera', name: 'Main Camera', props: { width: 640, height: 360, zoom: 1 } },
|
||
{
|
||
id: 'well-sprite',
|
||
type: 'sprite',
|
||
name: 'Well',
|
||
// Wears the shader at its neutral values: the pass runs, the picture stays the same.
|
||
props: { width: COLS * CELL, height: ROWS * CELL, color: WELL, shader: 'adjust-colors' },
|
||
},
|
||
adjustColorsShader(),
|
||
{ id: 'cell-sprite', type: 'sprite', name: 'Cell', props: { width: CELL - 1, height: CELL - 1 } },
|
||
{
|
||
id: 'label',
|
||
type: 'text',
|
||
name: 'Label',
|
||
props: { text: 'Text', fontSize: 26, color: WHITE, align: 'center' },
|
||
},
|
||
{ id: 'board', type: 'container', name: 'Board', props: {}, objects: boardTemplate },
|
||
behavior('piece-cell', 'Piece cell', [], pieceCellGraph()),
|
||
behavior('fits', 'Fits', FITS_VARS, fitsGraph()),
|
||
behavior('clear-lines', 'Clear lines', CLEAR_LINES_VARS, clearLinesGraph()),
|
||
behavior('draw-board', 'Draw board', [], drawBoardGraph()),
|
||
behavior('spawn-piece', 'Spawn piece', [], spawnPieceGraph()),
|
||
behavior('next-move', 'Next move', NEXT_MOVE_VARS, nextMoveGraph()),
|
||
behavior('draw-piece', 'Draw piece', [], drawPieceGraph()),
|
||
behavior('lock-piece', 'Lock piece', LOCK_PIECE_VARS, lockPieceGraph()),
|
||
behavior('show-lines', 'Show lines', [], showLinesGraph()),
|
||
behavior('tetris', 'Tetris', tetrisVariables, tetrisGraph()),
|
||
],
|
||
};
|
||
|
||
await writeProject(projectId, baseUrl, project);
|