From d6b8d489afff470529a870ec66701706140eac5e Mon Sep 17 00:00:00 2001 From: Louis Capitanchik <contact@louiscap.co> Date: Fri, 12 Jul 2019 16:07:59 +0100 Subject: [PATCH] Add text editor, add reset button --- package-lock.json | 85 +++++++++++++++++++++++ package.json | 3 + src/App.js | 57 ++++++++++++--- src/editor/Editor.js | 162 +++++++++++++++++++++++++++++++++++++++++++ src/render/Level.js | 4 +- 5 files changed, 297 insertions(+), 14 deletions(-) create mode 100644 src/editor/Editor.js diff --git a/package-lock.json b/package-lock.json index e49b2cb..243db1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1195,6 +1195,11 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, + "@sphinxxxx/color-conversion": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.1.tgz", + "integrity": "sha512-5+ofCE09lF6C7DPSVyvQ2Nf0oaue3Cl+SosT45DYy5nhgUXsOq3TetArC1q8mVfAOjhG0WReQPPFBdc4xXVNkg==" + }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", @@ -2491,6 +2496,11 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, + "brace": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz", + "integrity": "sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg=" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4379,6 +4389,11 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-4.2.0.tgz", "integrity": "sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU=" }, + "drag-tracker": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/drag-tracker/-/drag-tracker-1.0.0.tgz", + "integrity": "sha1-m9M9OAvDBW22m9Wzz24GL+xYvWQ=" + }, "duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", @@ -6547,6 +6562,11 @@ "handlebars": "^4.1.2" } }, + "javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k=" + }, "jest": { "version": "24.7.1", "resolved": "https://registry.npmjs.org/jest/-/jest-24.7.1.tgz", @@ -7581,6 +7601,11 @@ } } }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, "js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", @@ -7670,6 +7695,11 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-source-map": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-source-map/-/json-source-map-0.4.0.tgz", + "integrity": "sha1-7qg3/jzi8r/VsTaHd5QGNUQjw1U=" + }, "json-stable-stringify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", @@ -7701,6 +7731,42 @@ "minimist": "^1.2.0" } }, + "jsoneditor": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsoneditor/-/jsoneditor-6.1.0.tgz", + "integrity": "sha512-jZ5WUFqC+9MuYy5W7qz4VQa9xblfY1Xmzw79Odzz9pA6VyMRv7DIGvHLIzTOWl3KuWDQL/hsxliS6IBJeCev6Q==", + "requires": { + "ajv": "6.10.0", + "brace": "0.11.1", + "javascript-natural-sort": "0.7.1", + "jmespath": "0.15.0", + "json-source-map": "0.4.0", + "mobius1-selectr": "2.4.12", + "picomodal": "3.0.0", + "vanilla-picker": "2.8.1" + }, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } + } + }, + "jsoneditor-react": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/jsoneditor-react/-/jsoneditor-react-1.0.1.tgz", + "integrity": "sha512-dfFavJi8MuAKi6vUVCRCoxghunS04mDwUzumqN25gl+yD/Db0gsHF+Nj4//QEVF/iSOe0b+sdmzin+5N5kldug==", + "requires": { + "prop-types": "^15.6.0" + } + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -8267,6 +8333,11 @@ } } }, + "mobius1-selectr": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/mobius1-selectr/-/mobius1-selectr-2.4.12.tgz", + "integrity": "sha512-zyGyhFaPCja2oHOud+9vOpLtIbIGv79jf0X1sfbBCCZ7UFHQIbx6yladAlyYU9Qq5zvsYw2Boa1CivSKvxLEHA==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -8921,6 +8992,11 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "picomodal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/picomodal/-/picomodal-3.0.0.tgz", + "integrity": "sha1-+s0w9PvzSoCcHgTqUl8ATzmcC4I=" + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -12183,6 +12259,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "vanilla-picker": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/vanilla-picker/-/vanilla-picker-2.8.1.tgz", + "integrity": "sha512-mzjMw0WbeS6qi+wzXSCfHFL7Jmvp7sJfXq0FfOvUEAAnCI6cmgCUVJ+wpr2c3g+Gt9AypLpHks3oeIkX6nCM9A==", + "requires": { + "@sphinxxxx/color-conversion": "^2.2.1", + "drag-tracker": "^1.0.0" + } + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 37cef72..fcdd128 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,10 @@ "dependencies": { "@commander-lol/redux-reducer": "^1.0.1", "@wasm-tool/wasm-pack-plugin": "^1.0.0", + "ajv": "^6.10.1", "copy-webpack-plugin": "^5.0.3", + "jsoneditor": "^6.1.0", + "jsoneditor-react": "^1.0.1", "react": "^16.8.6", "react-dom": "^16.8.6", "react-redux": "^7.1.0", diff --git a/src/App.js b/src/App.js index 1853f6f..3193bc8 100644 --- a/src/App.js +++ b/src/App.js @@ -4,38 +4,73 @@ import Level from './render/Level' import LevelData from './game/Level' import testLevel from './data/testlevel1' +import Editor from './editor/Editor' + class App extends Component<{}> { state = { margin: 0, scale: 1.5, showOptions: false, - levelData: null + rawLevelData: null, + levelData: null, + screen: 'editor', + } + + componentDidMount() { + this.loadLevelData(testLevel) } - async componentDidMount() { - const levelData = await LevelData.from(testLevel) + async loadLevelData(data = this.state.rawLevelData) { + const levelData = await LevelData.from(data) await levelData.tick() - this.setState({ levelData }) + this.setState({ levelData, rawLevelData: data }) } render() { const { margin, scale, showOptions, levelData } = this.state - const opts = {margin, scale} + return ( <React.Fragment> <div className="render-controls" style={showOptions && {marginLeft: 0} || {}}> - <div className="render-controls-row"><label>Margin: </label><input type="number" value={margin} - onChange={this.set('margin')}/> + <div className="render-controls-row"> + <label>Margin: </label> + <input type="number" value={margin} onChange={this.set('margin')}/> + </div> + <div className="render-controls-row"> + <label>Scale: </label> + <input type="number" value={scale} onChange={this.set('scale')} step={0.25}/> </div> - <div className="render-controls-row"><label>Scale: </label><input type="number" value={scale} - onChange={this.set('scale')} - step={0.25}/></div> </div> <div className="root-container" style={showOptions && {marginLeft: 420} || {}}> <button onClick={this.set('showOptions', s => !s.showOptions)}>{ showOptions ? 'Hide' : 'Show' } Options</button> + <button onClick={this.set('screen', s => s.screen === 'editor' ? 'game' : 'editor')}>Toggle Editor</button> + <button onClick={() => this.loadLevelData()}>Reset Level</button> <br/> - { levelData == null ? <span>LOADING</span> : <Level level={levelData} {...opts} /> } + { + (() => { + switch (this.state.screen) { + case 'game': + if (levelData == null) { + return <span>LOADING</span> + } else { + return <Level level={levelData} {...opts} /> + } + case 'editor': { + return this.state.rawLevelData && ( + <Editor + data={this.state.rawLevelData} + onChange={data => { + this.loadLevelData(data) + }} + /> + ) + } + default: + return null + } + })() + } </div> </React.Fragment> ); diff --git a/src/editor/Editor.js b/src/editor/Editor.js new file mode 100644 index 0000000..112d975 --- /dev/null +++ b/src/editor/Editor.js @@ -0,0 +1,162 @@ +// @flow + +import React from 'react' + +import { range } from '../render/Level' + +type Props = { + data: Object, + onChange: Function, +} + +class Point extends React.PureComponent { + render() { + const { point: { x, y }, onChange } = this.props + return ( + <div> + <label>X:</label><input type="number" value={x} onChange={e => onChange({ x: e.currentTarget.value, y })} /> + <label>Y:</label><input type="number" value={y} onChange={e => onChange({ y: e.currentTarget.value, x })} /> + </div> + ) + } +} + +class Section extends React.Component { + render() { + const { children, entries, onNew } = this.props + return entries.map(children) + } +} + +class PointGrid extends React.PureComponent { + render() { + const { width, height, points, pointWidth = 10, pointHeight = 10, margin = 1, onChange } = this.props + const index = new Set() + const createKey = (x, y) => `${ x }|${ y }` + + + points.forEach(point => index.add(createKey(point.x, point.y))) + + return ( + <div style={{ width: width * pointWidth * margin, height: height * pointHeight * margin, position: 'relative' }}> + { [...range(width)].map(x => ( + [...range(height)].map(y => ( + <div + style={{ + position: 'absolute', + left: (x * pointWidth) + (margin * pointWidth), + top: (y * pointHeight) + (margin * pointHeight), + width: pointWidth, + height: pointHeight, + backgroundColor: index.has(createKey(x, y)) ? 'red' : 'grey' + }} + onClick={() => { + const alreadyActive = index.has(createKey(x, y)) + if (alreadyActive) { + const index = points.findIndex(item => item.x === x && item.y === y) + const newPoints = [...points.slice(0, index), ...points.slice(index + 1)] + onChange(newPoints) + } else { + onChange(Array.from(points).concat([{ x, y }])) + } + }} + /> + )) + ))} + </div> + ) + } +} + +export default class Editor extends React.Component<Props> { + + bindMerge = (mergefn) => { + return e => { + const value = e.currentTarget.value + const newData = { ...this.props.data } + mergefn(newData, value) + this.props.onChange(newData) + } + } + + + + render() { + const { width, height, name, connectors, nouns, adjectives } = this.props.data + return ( + <div> + <div> + Name: <input value={name} onChange={this.bindMerge((d, n) => d.name = n)} /> + Width: <input value={width} onChange={this.bindMerge((d, n) => d.width = n)} type="number" /> + Height: <input value={height} onChange={this.bindMerge((d, n) => d.height = n)} type="number" /> + </div> + + <h3>Connectors</h3> + <Section entries={connectors}> + { (data, i) => ( + <div key={i}> + <span>Type:</span><code>is</code> + <Point point={data} onChange={point => { + const data = { ...this.props.data } + data.connectors[i] = { ...point, type: 'is' } + this.props.onChange(data) + }} /> + </div> + )} + </Section> + + <h3>Nouns</h3> + <Section entries={nouns}> + { (data, i) => ( + <div key={i} style={{ border: '1px solid black', padding: '15px'}}> + <label>Name: </label><input value={data.name} /> + <div style={{ display: 'flex', justifyContent: 'space-evenly', marginBottom: '20px' }}> + <div style={{ display: 'inline-block' }}> + <h4>Text</h4> + { data.text_start_locations && <PointGrid width={width} height={height} points={data.text_start_locations} onChange={newList => { + const newData = {...this.props.data} + newData.nouns[i].text_start_locations = newList + this.props.onChange(newData) + }}/> } + </div> + <div style={{ display: 'inline-block' }}> + <h4>Entities</h4> + { data.entity_start_locations && <PointGrid width={width} height={height} points={data.entity_start_locations} onChange={newList => { + const newData = {...this.props.data} + newData.nouns[i].entity_start_locations = newList + this.props.onChange(newData) + }}/> } + </div> + </div> + </div> + )} + </Section> + + <h3>Adjectives</h3> + <Section entries={adjectives}> + { (data, i) => ( + <div key={i} style={{ border: '1px solid black', padding: '15px'}}> + <label>Name: </label><input value={data.name} /> + <div style={{ display: 'flex', justifyContent: 'space-evenly', marginBottom: '20px' }}> + <div style={{ display: 'inline-block' }}> + <h4>Text</h4> + { data.text_start_locations && <PointGrid width={width} height={height} points={data.text_start_locations} onChange={newList => { + const newData = {...this.props.data} + newData.adjectives[i].text_start_locations = newList + this.props.onChange(newData) + }}/> } + </div> + {/*<div style={{ display: 'inline-block' }}>*/} + {/* <h4>Entities</h4>*/} + {/* { data.entity_start_locations && <PointGrid width={width} height={height} points={data.entity_start_locations} /> }*/} + {/*</div>*/} + </div> + </div> + )} + </Section> + + <pre><code>{ JSON.stringify(this.props.data, null, 2) }</code></pre> + </div> + ) + } +} \ No newline at end of file diff --git a/src/render/Level.js b/src/render/Level.js index f6bff2c..dd66268 100644 --- a/src/render/Level.js +++ b/src/render/Level.js @@ -9,7 +9,7 @@ function* __rangeInner(start, end, step) { } } -function range(...args) { +export function range(...args) { if (args.length === 1) { const [end] = args return __rangeInner(0, end, 1) @@ -40,12 +40,10 @@ export default class Level extends React.Component<Props> { active: false, } - state = { nonce: Math.random(), } - componentDidMount(): void { window.addEventListener('keyup', this.handleKeyPress) } -- GitLab