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