M dist/main.css => dist/main.css +6 -1
@@ 12,7 12,12 @@ body {
}
body {
- flex-direction: column;
+ flex-direction: row;
+ width: 100%;
+}
+
+.toolbar {
+ flex-grow: 1;
}
canvas {
M src/game.js => src/game.js +43 -6
@@ 4,7 4,7 @@ import {TILE_SIZE, FONT_SIZE, TICK_RATE} from './main'
const DATA_SPEED = .1
-export function drawConnection(connection) {
+function getConnectionPath (connection) {
const { startBox, startOutput, endBox, endInput, g } = connection;
const outputIndex = startBox.outputs.indexOf(startOutput);
const start = (
@@ 22,12 22,23 @@ export function drawConnection(connection) {
0,
))
);
+
+ return {
+ start, end
+ };
+}
+
+export function drawConnection(connection) {
+ const { start, end } = getConnectionPath(connection);
+ const g = connection.g;
g.clear();
g.lineStyle(TILE_SIZE, hsv(.1, .2, .4));
g.moveTo(start.x * TILE_SIZE, start.y * TILE_SIZE);
g.lineTo(end.x * TILE_SIZE, end.y * TILE_SIZE);
connection.start = start;
connection.end = end;
+
+ g.hitArea = g.getBounds();
}
export function removeConnection(connection, gameData, gs) {
@@ 41,6 52,15 @@ export function addConnection(data, gameData, gs) {
...data,
g: new PIXI.Graphics(),
};
+ connection.g.interactive = true;
+ connection.g.on('rightdown', (e) => {
+ const { start, end } = getConnectionPath(connection);
+ const path = end.sub(start);
+ const distance = gameData.mouse.mul(1 / TILE_SIZE).sub(end).project(path.perp());
+ if (Math.abs(distance) < 0.5) {
+ removeConnection(connection, gameData, gs);
+ }
+ });
gameData.connections.push(connection);
drawConnection(connection);
gs.connections.addChild(connection.g);
@@ 134,7 154,8 @@ export function drawBox(box) {
export function addBox(data, gameData, gs) {
const pos = new Vec(Math.floor(data.pos.x), Math.floor(data.pos.y))
- const boxDef = gameData.boxDefinitions[data.type];
+ const customDef = gameData.customDefinitions[data.type];
+ const boxDef = gameData.boxDefinitions[data.type] || customDef;
if (!boxDef) {
console.error(`No box definition for "${data.type}"`);
}
@@ 148,17 169,28 @@ export function addBox(data, gameData, gs) {
clicked: false,
label: '',
};
- const boxData = typeof data.literalValue !== 'undefined' ? { value: data.literalValue } : boxDef.data;
+ const boxData = (
+ typeof data.literalValue !== 'undefined'
+ ? { value: data.literalValue }
+ : (boxDef.data || {})
+ );
const box = {
...defaults,
...data,
...boxDef,
+ js: !customDef,
data: boxData,
pos,
}
gameData.codeBoxes.push(box);
drawBox(box);
gs.boxes.addChild(box.g);
+
+ // Run init script
+ if (box.init) {
+ interpret(box.init, { value: null, data: box.data })
+ }
+
return box
}
@@ 225,7 257,7 @@ export function evaluateNewPieces (newKey, gameData, gs) {
return []
}
box.triggered = false;
- const boxDef = gameData.boxDefinitions[box.type];
+ const boxDef = gameData.boxDefinitions[box.type] || gameData.customDefinitions[box.type];
if (boxDef.iterateData) {
box.data = boxDef.iterateData({box, data: box.data, ...inputValues});
}
@@ 235,7 267,7 @@ export function evaluateNewPieces (newKey, gameData, gs) {
// output event like: { value: value, output: outputType }
// and queue it for output
// Then returns the value again
- const outputEvents = [];
+ let outputEvents = [];
const out = (boxDef.outputs || []).reduce((r, outputType) => {
r[outputType] = (value) => {
const event = {
@@ 252,7 284,12 @@ export function evaluateNewPieces (newKey, gameData, gs) {
return;
}
const args = { value, out, data: box.data, box };
- func(args);
+ if (box.js) {
+ func(args);
+ } else {
+ const result = interpret(func, { value, data: box.data });
+ outputEvents = result.output;
+ }
});
box.inputQueue = [];
return outputEvents.flatMap(({ value, output }) => emit(value, box, output));
M src/lang.js => src/lang.js +4 -1
@@ 28,11 28,14 @@ export function parse (code) {
}
}
} else {
+ const newContents = line.slice(2);
return {
...r,
queuedBranch: {
...r.queuedBranch,
- contents: r.queuedBranch.contents + line.slice(2)
+ contents: r.queuedBranch.contents
+ ? [r.queuedBranch.contents, newContents].join(' ')
+ : newContents
}
}
}
M src/main.js => src/main.js +112 -16
@@ 1,6 1,6 @@
import * as PIXI from 'pixi.js'
import {Vec, range, nrand, choose, hsv, shuffle, approach} from './utils'
-import {addConnection, removeConnection, drawConnection, addBox, removeBox, drawBox, evaluateNewPieces} from './game'
+import {addConnection, removeConnection, drawConnection, addBox, resetBox, removeBox, drawBox, evaluateNewPieces} from './game'
import {loadCodeBox, boxDefinitions} from './codebox'
import {interpret, parse} from './lang'
@@ 14,7 14,30 @@ PIXI.Loader.shared.add('Pixelzim', 'pixelzim_3x5_7.fnt')
PIXI.Loader.shared.add('CompassPro', 'compasspro_medium_12.fnt')
PIXI.Loader.shared.add('MatchupPro', 'matchuppro_medium_12.fnt')
-let innerSize = Math.min(window.innerWidth, window.innerHeight - 30)
+const $ = (selector) => document.querySelector(selector);
+const el = ({ kind, children, text, value, attributes, classes }) => {
+ const element = document.createElement(kind || 'div');
+ if (text) {
+ element.textContent = text;
+ }
+ if (value) {
+ element.value = value;
+ }
+ (children || []).forEach(child => {
+ element.appendChild(child);
+ });
+ (Object.keys(attributes || {})).forEach(key => {
+ element.setAttribute(key, attributes[key]);
+ });
+ element.classNames = classes || '';
+ return element;
+}
+
+const toolbarWidth = 300;
+let innerSize = Math.min(
+ window.innerWidth - toolbarWidth,
+ window.innerHeight,
+)
let psize = 1
let height = innerSize / psize
let width = innerSize / psize
@@ 47,7 70,7 @@ window.world = world
app.renderer.backgroundColor = hsv(.6, .1, .9)
-document.querySelector('.canvas').appendChild(app.view)
+$('.canvas').appendChild(app.view)
//Start the game loop
setInterval(() => gameLoop(1), 30)
@@ 71,6 94,7 @@ export let gameData = {
connections: [],
dataPieces: [],
boxDefinitions: [],
+ customDefinitions: {},
moving: null,
drawingConnection: null,
gameSpeed: 2,
@@ 127,10 151,10 @@ function save () {
newConnection.startBoxIndex = gameData.codeBoxes.indexOf(connection.startBox);
newConnection.endBoxIndex = gameData.codeBoxes.indexOf(connection.endBox);
return newConnection;
- })
+ }),
+ customDefinitions: gameData.customDefinitions,
});
}
-window.save = save;
function load () {
const rawSave = localStorage.save;
@@ 138,6 162,7 @@ function load () {
return;
}
const save = JSON.parse(rawSave);
+ gameData.customDefinitions = save.customDefinitions;
save.boxes.forEach(box => addBox(box, gameData, gs));
save.connections.map(connection => ({
...connection,
@@ 146,18 171,34 @@ function load () {
})).forEach(connection => addConnection(connection, gameData, gs));
}
+function reset () {
+ save();
+ Array.from(gameData.codeBoxes).forEach(box => removeBox(box, gameData, gs));
+ gameData.dataPieces.forEach(piece => gs.dataPieces.removeChild(piece.g));
+ gameData.dataPieces = [];
+ Array.from(gameData.connections).forEach(connection => removeConnection(piece, gameData, gs))
+ load();
+}
+
+window.save = save;
+window.load = load;
+
function initUI() {
- const toolbarEl = document.querySelector('.toolbar');
- const codeTypeDropdown = document.createElement('select');
- Object.keys(boxDefinitions).forEach(def => {
- const optionEl = document.createElement('option');
- optionEl.textContent = def;
- optionEl.value = def;
- codeTypeDropdown.appendChild(optionEl);
- })
+ const toolbarEl = $('.toolbar');
+
+ const codeTypeDropdown = el({ kind: 'select' });
+ function buildTypeDropdown () {
+ codeTypeDropdown.innerHTML = '';
+ const definitions = Object.keys(gameData.boxDefinitions).concat(Object.keys(gameData.customDefinitions));
+ definitions.map(
+ def => el({ kind: 'option', text: def, value: def })
+ ).forEach(option => {
+ codeTypeDropdown.appendChild(option);
+ })
+ }
+ buildTypeDropdown();
toolbarEl.appendChild(codeTypeDropdown);
- const codeBoxButton = document.createElement('button');
- codeBoxButton.textContent = 'Add';
+ const codeBoxButton = el({ kind: 'button', text: 'Add' });
codeBoxButton.addEventListener('click', () => {
const pos = new Vec(width / TILE_SIZE / 2, height / TILE_SIZE - 15);
if (['literal', 'literalFloat'].includes(codeTypeDropdown.value)) {
@@ 171,6 212,61 @@ function initUI() {
}
});
toolbarEl.appendChild(codeBoxButton);
+
+ const editorTextbox = el({ kind: 'textarea' });
+ const editorSaveButton = el({ kind: 'button', text: 'Save' });
+ const codeEditorEl = el({
+ kind: 'div',
+ children: [
+ el({ kind: 'label', text: 'editor ' }),
+ editorSaveButton,
+ el({
+ kind: 'div',
+ children: [
+ editorTextbox
+ ]
+ })
+ ]
+ });
+ toolbarEl.appendChild(codeEditorEl);
+ editorSaveButton.addEventListener('click', () => {
+ const code = editorTextbox.value.split('\n');
+ const name = code[0];
+ const [inputSig, outputSig] = code[1].split(' -> ');
+
+ // Extra line at the end just in case
+ const rest = code.slice(2).join('\n') + '\n';
+ const parsedCode = parse(rest);
+ const inputs = inputSig.split(' ').reduce((r, input) => {
+ const inputBranch = parsedCode.branches[input];
+ if (!inputBranch) {
+ console.error(`No input handler defined for custom.${input}`)
+ }
+ r[input] = inputBranch || '';
+ return r;
+ }, {});
+ const outputs = outputSig.split(' ').filter(x => x);
+ const init = parsedCode;
+ gameData.customDefinitions[name] = {
+ init,
+ inputs,
+ outputs,
+ code: editorTextbox.value
+ };
+ buildTypeDropdown();
+ });
+
+ codeTypeDropdown.addEventListener('change', () => {
+ const customDef = gameData.customDefinitions[codeTypeDropdown.value];
+ if (customDef) {
+ editorTextbox.value = customDef.code;
+ }
+ });
+ const resetButtonEl = el({ kind: 'button', text: 'Reset' });
+ resetButtonEl.addEventListener('click', () => {
+ reset();
+ });
+ toolbarEl.appendChild(resetButtonEl);
}
function setup () {
@@ 299,7 395,6 @@ def pop:
}
});
- initUI();
app.view.tabIndex = 0
app.view.focus()
app.view.addEventListener('contextmenu', contextmenu)
@@ 345,6 440,7 @@ def pop:
gameData.boxDefinitions = boxDefinitions;
load();
+ initUI();
setInterval(save, 10000);