From 7535ab228884c4713301b69bd8f4a2f3bfd8f97b Mon Sep 17 00:00:00 2001 From: Louis Capitanchik <contact@louiscap.co> Date: Thu, 11 Jul 2019 22:56:55 +0100 Subject: [PATCH] Initial version, basic interactions such as 'push' and 'you'. Emergent features implemented such as noun 'is' noun --- .gitattributes | 1 + package-lock.json | 114 +++++++++++++++ package.json | 8 +- public/charles-text.png | 3 + public/charles.png | 3 + public/index.html | 1 + public/is-text.png | 3 + public/push-text.png | 3 + public/stop-text.png | 3 + public/wall-text.png | 3 + public/wall.png | 3 + public/you-text.png | 3 + src/App.css | 63 +++++--- src/App.js | 62 +++++--- src/data/adjectives/push.json | 7 + src/data/adjectives/you.json | 7 + src/data/testlevel1.json | 146 +++++++++++++++++++ src/game/Adjective.js | 34 +++++ src/game/Entity.js | 36 +++++ src/game/Level.js | 260 ++++++++++++++++++++++++++++++++++ src/game/Noun.js | 74 ++++++++++ src/render/Level.js | 136 ++++++++++++++++++ src/store/level/reducer.js | 4 + src/store/reducers.js | 6 + src/store/store.js | 8 ++ 25 files changed, 947 insertions(+), 44 deletions(-) create mode 100644 .gitattributes create mode 100644 public/charles-text.png create mode 100644 public/charles.png create mode 100644 public/is-text.png create mode 100644 public/push-text.png create mode 100644 public/stop-text.png create mode 100644 public/wall-text.png create mode 100644 public/wall.png create mode 100644 public/you-text.png create mode 100644 src/data/adjectives/push.json create mode 100644 src/data/adjectives/you.json create mode 100644 src/data/testlevel1.json create mode 100644 src/game/Adjective.js create mode 100644 src/game/Entity.js create mode 100644 src/game/Level.js create mode 100644 src/game/Noun.js create mode 100644 src/render/Level.js create mode 100644 src/store/level/reducer.js create mode 100644 src/store/reducers.js create mode 100644 src/store/store.js diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..24a8e87 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/package-lock.json b/package-lock.json index 39aaa35..e49b2cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -916,6 +916,11 @@ "minimist": "^1.2.0" } }, + "@commander-lol/redux-reducer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@commander-lol/redux-reducer/-/redux-reducer-1.0.1.tgz", + "integrity": "sha1-u2Vvl13M8/3qd/aB9768ul6PiG8=" + }, "@csstools/convert-colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", @@ -1469,6 +1474,16 @@ } } }, + "@wasm-tool/wasm-pack-plugin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@wasm-tool/wasm-pack-plugin/-/wasm-pack-plugin-1.0.0.tgz", + "integrity": "sha512-8Z0rGItT0sMSj2gDQT6ws2pxbwVQzXmfaeArcQdh+AUQK0zjpbBBreIs9aEQS4G+2XnvDwUbuibj/C2ezAFXyQ==", + "requires": { + "chalk": "^2.4.1", + "command-exists": "^1.2.7", + "watchpack": "^1.6.0" + } + }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", @@ -3464,6 +3479,11 @@ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.7.tgz", "integrity": "sha512-Jrx3xsP4pPv4AwJUDWY9wOXGtwPXARej6Xd99h4TUGotmf8APuquKMpK+dnD3UgyxK7OEWaisjZz+3b5jtL6xQ==" }, + "command-exists": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.8.tgz", + "integrity": "sha512-PM54PkseWbiiD/mMsbvW351/u+dafwTJ0ye2qB60G1aGQP9j3xK2gmMDc+R34L3nDtx4qMCitXT75mkbkGJDLw==" + }, "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", @@ -3614,6 +3634,55 @@ "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, + "copy-webpack-plugin": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.0.3.tgz", + "integrity": "sha512-PlZRs9CUMnAVylZq+vg2Juew662jWtwOXOqH4lbQD9ZFhRG9R7tVStOgHt21CBGVq7k5yIJaz8TXDLSjV+Lj8Q==", + "requires": { + "cacache": "^11.3.2", + "find-cache-dir": "^2.1.0", + "glob-parent": "^3.1.0", + "globby": "^7.1.1", + "is-glob": "^4.0.1", + "loader-utils": "^1.2.3", + "minimatch": "^3.0.4", + "normalize-path": "^3.0.0", + "p-limit": "^2.2.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^1.7.0", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "requires": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + } + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + } + } + }, "core-js": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.0.1.tgz", @@ -5801,6 +5870,14 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz", + "integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==", + "requires": { + "react-is": "^16.7.0" + } + }, "hosted-git-info": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", @@ -10150,6 +10227,29 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" }, + "react-redux": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.0.tgz", + "integrity": "sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw==", + "requires": { + "@babel/runtime": "^7.4.5", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.8.6" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.4.tgz", + "integrity": "sha512-Na84uwyImZZc3FKf4aUF1tysApzwf3p2yuFBIyBfbzT5glzKTdvYI4KVW4kcgjrzoGUjC7w3YyCHcJKaRxsr2Q==", + "requires": { + "regenerator-runtime": "^0.13.2" + } + } + } + }, "react-scripts": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.0.1.tgz", @@ -10269,6 +10369,15 @@ "minimatch": "3.0.4" } }, + "redux": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz", + "integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -11508,6 +11617,11 @@ "util.promisify": "~1.0.0" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index d7ba051..37cef72 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,15 @@ "version": "0.1.0", "private": true, "dependencies": { + "@commander-lol/redux-reducer": "^1.0.1", + "@wasm-tool/wasm-pack-plugin": "^1.0.0", + "copy-webpack-plugin": "^5.0.3", "react": "^16.8.6", "react-dom": "^16.8.6", - "react-scripts": "3.0.1" + "react-redux": "^7.1.0", + "react-scripts": "3.0.1", + "redux": "^4.0.4", + "rimraf": "^2.6.3" }, "scripts": { "start": "react-scripts start", diff --git a/public/charles-text.png b/public/charles-text.png new file mode 100644 index 0000000..2fbe318 --- /dev/null +++ b/public/charles-text.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:005619c7fe65d2069ab2b3edfb0725f9c7c788f642f13399c1f1350ed43b62c5 +size 917 diff --git a/public/charles.png b/public/charles.png new file mode 100644 index 0000000..1c4d32f --- /dev/null +++ b/public/charles.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:765e0a52b1d1fde1fe9e7b1e967b749f93adf72e5819d7b60ffdaf68c69aedf6 +size 1110 diff --git a/public/index.html b/public/index.html index dd1ccfd..e6a8f6a 100644 --- a/public/index.html +++ b/public/index.html @@ -20,6 +20,7 @@ Learn how to configure a non-root public URL by running `npm run build`. --> <title>React App</title> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" integrity="sha256-l85OmPOjvil/SOvVt3HnSSjzF1TUMyT9eV0c2BzEGzU=" crossorigin="anonymous" /> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> diff --git a/public/is-text.png b/public/is-text.png new file mode 100644 index 0000000..3229500 --- /dev/null +++ b/public/is-text.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59613d3d54161d14a241077e229c3072e0cc1fdbc5d34207082e74270a6acd00 +size 640 diff --git a/public/push-text.png b/public/push-text.png new file mode 100644 index 0000000..b436f12 --- /dev/null +++ b/public/push-text.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:366ba976d5979c4d54399c3dc2495c65a7a03afa68fe2848ff440c528523ccc7 +size 955 diff --git a/public/stop-text.png b/public/stop-text.png new file mode 100644 index 0000000..c00d1f5 --- /dev/null +++ b/public/stop-text.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0ce96244e628b6eec722eee3ee46370447a8279736be6e1ee4b60f5745bed47 +size 1084 diff --git a/public/wall-text.png b/public/wall-text.png new file mode 100644 index 0000000..d7b3c52 --- /dev/null +++ b/public/wall-text.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b0bcb344992105740bae641f816c9e740b0092733bd20628eae218f572a79d3 +size 823 diff --git a/public/wall.png b/public/wall.png new file mode 100644 index 0000000..a39f104 --- /dev/null +++ b/public/wall.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69fb8cba0dc4a193be0b3301c47ef33b566c78d38211174b03a410a6f83cb3c9 +size 182 diff --git a/public/you-text.png b/public/you-text.png new file mode 100644 index 0000000..46f341c --- /dev/null +++ b/public/you-text.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1af3dda4b19d5b0ef1506377d681b3c19b685678ebd99a94d80790170f9be47 +size 787 diff --git a/src/App.css b/src/App.css index b41d297..04abfb9 100644 --- a/src/App.css +++ b/src/App.css @@ -1,33 +1,50 @@ -.App { - text-align: center; +#root { + width: 100%; + height: 100%; + position: relative; } -.App-logo { - animation: App-logo-spin infinite 20s linear; - height: 40vmin; - pointer-events: none; +.root-container { + margin-left: 0; + transition: margin-left 0.2s linear; + min-width: min-content; } -.App-header { - background-color: #282c34; - min-height: 100vh; +.level-container { + position: relative; + width: 800px; + height: 800px; +} + +.level-tile { + background-color: rgba(12,55,66,0.2); + position: absolute; + z-index: -1; +} + +.level-entity { + position: absolute; + z-index: 10; +} + +.render-controls { display: flex; flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; + width: 400px; + padding: 10px; + position: absolute; + margin-left: -420px; + transition: margin-left 0.2s linear; } -.App-link { - color: #61dafb; +.render-controls-row { + display: flex; + height: 100px; + align-items: center; } - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +.render-controls-row label{ + flex: 1; } +.render-controls-row input { + flex: 3; +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index ce9cbd2..705702a 100644 --- a/src/App.js +++ b/src/App.js @@ -1,26 +1,48 @@ -import React from 'react'; +import React, {Component} from 'react'; import logo from './logo.svg'; import './App.css'; +import Level from './render/Level' +import LevelData from './game/Level' +import testLevel from './data/testlevel1' -function App() { - return ( - <div className="App"> - <header className="App-header"> - <img src={logo} className="App-logo" alt="logo" /> - <p> - Edit <code>src/App.js</code> and save to reload. - </p> - <a - className="App-link" - href="https://reactjs.org" - target="_blank" - rel="noopener noreferrer" - > - Learn React - </a> - </header> - </div> - ); +class App extends Component<{}> { + state = { + margin: 1, + scale: 1, + showOptions: false, + levelData: null + } + + async componentDidMount() { + const levelData = await LevelData.from(testLevel) + levelData.tick() + this.setState({ levelData }) + } + + 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> + <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)}>Show Options</button> + <br/> + { levelData == null ? <span>LOADING</span> : <Level level={levelData} {...opts} /> } + </div> + </React.Fragment> + ); + } + + set = (name, value = null) => e => this.setState(s => ({ [name]: value && value.call ? value(s) : value || e.currentTarget.value })) } export default App; diff --git a/src/data/adjectives/push.json b/src/data/adjectives/push.json new file mode 100644 index 0000000..ce7798a --- /dev/null +++ b/src/data/adjectives/push.json @@ -0,0 +1,7 @@ +{ + "id": "push", + "interactions": { + "stop": "stop", + "push": "is-moved" + } +} \ No newline at end of file diff --git a/src/data/adjectives/you.json b/src/data/adjectives/you.json new file mode 100644 index 0000000..fed56c5 --- /dev/null +++ b/src/data/adjectives/you.json @@ -0,0 +1,7 @@ +{ + "id": "you", + "interactions": { + "push": "is-moved", + "stop": "stop" + } +} \ No newline at end of file diff --git a/src/data/testlevel1.json b/src/data/testlevel1.json new file mode 100644 index 0000000..7e2fcca --- /dev/null +++ b/src/data/testlevel1.json @@ -0,0 +1,146 @@ +{ + "name": "Ba Ba Test Sheep", + "width": 10, + "height": 10, + "connectors": [ + { + "type": "is", + "x": 2, + "y": 5 + }, + { + "type": "is", + "x": 2, + "y": 2 + } + ], + "nouns": [ + { + "name": "charles", + "images": { + "text": "/charles-text.png", + "entity": "/charles.png" + }, + "text_start_locations": [ + { + "x": 1, + "y": 5 + } + ], + "entity_start_locations": [ + { + "x": 6, + "y": 5 + } + ] + }, + { + "name": "wall", + "images": { + "text": "/wall-text.png", + "entity": "/wall.png" + }, + "text_start_locations": [ + { + "x": 1, + "y": 2 + } + ], + "entity_start_locations": [ + { + "x": 0, + "y": 0 + }, + { + "x": 1, + "y": 0 + }, + { + "x": 2, + "y": 0 + }, + { + "x": 3, + "y": 0 + }, + { + "x": 4, + "y": 0 + }, + { + "x": 5, + "y": 0 + }, + { + "x": 6, + "y": 0 + }, + { + "x": 7, + "y": 0 + }, + { + "x": 8, + "y": 0 + }, + { + "x": 9, + "y": 0 + } + ] + } + ], + "adjectives": [ + { + "name": "you", + "source": { + "type": "internal", + "data": "adjectives/you" + }, + "images": { + "text": "/you-text.png" + }, + "text_start_locations": [ + { + "x": 3, + "y": 5 + } + ] + }, + { + "name": "push", + "source": { + "type": "internal", + "data": "adjectives/push" + }, + "images": { + "text": "/push-text.png" + }, + "text_start_locations": [ + { + "x": 8, + "y": 8 + } + ] + }, + { + "name": "stop", + "source": { + "type": "inline", + "data": { + "id": "stop", + "interactions": {} + } + }, + "images": { + "text": "/stop-text.png" + }, + "text_start_locations": [ + { + "x": 3, + "y": 2 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/game/Adjective.js b/src/game/Adjective.js new file mode 100644 index 0000000..25db050 --- /dev/null +++ b/src/game/Adjective.js @@ -0,0 +1,34 @@ + + +export default class Adjective { + id: string + interactions: { [id: string]: string } + + meta = {} + + static async resolveInteractionSource(source) { + switch (source.type) { + case "inline": return source.data.interactions + case "internal": { + const data = await import(`../data/${ source.data }`) + if (data.default) { + return data.default.interactions + } + return data.interactions + } + default: + throw TypeError(`Invalid interactions source for ${ JSON.stringify(source) }`) + } + } + + constructor(id, interactions) { + this.id = id + this.interactions = interactions + } + + interact(other) { + console.log(this.id, this.interactions) + console.log(other.id, other.interactions) + return this.interactions[other.id] + } +} \ No newline at end of file diff --git a/src/game/Entity.js b/src/game/Entity.js new file mode 100644 index 0000000..0b754d0 --- /dev/null +++ b/src/game/Entity.js @@ -0,0 +1,36 @@ +// @flow + +import typeof Noun from './Noun' + +export default class Entity { + x: number + y: number + noun: Noun + + fallbackImage: ?string = null + + meta = {} + + constructor([x, y], noun) { + this.x = x + this.y = y + this.noun = noun + } + + get position(): [number, number] { + return [this.x, this.y] + } + + get renderProps() { + return { + x: this.x, + y: this.y, + image: this.noun.entityImage || this.fallbackImage, + } + } + + onCollide(other: Entity) { + return this.noun.interact(other.noun, this.position, other.position) + } + +} \ No newline at end of file diff --git a/src/game/Level.js b/src/game/Level.js new file mode 100644 index 0000000..ee4d4a5 --- /dev/null +++ b/src/game/Level.js @@ -0,0 +1,260 @@ +import Adjective from './Adjective' +import Noun from './Noun' +import Entity from './Entity' + +type MapOf<Type> = { [id: string]: Type } + +type ConstructorParams = { adjectives: MapOf<Adjective>, nouns: MapOf<Noun>, entities: Entity[], width: number, height: number, name: string, connectors: Object[] } + +export const Inputs = { + Controls: { + Up: 'controls_up', + Left: 'controls_left', + Right: 'controls_right', + Down: 'controls_down', + }, + Actions: { + Wait: 'action_wait', + }, +} + +export default class Level { + adjectives: MapOf<Adjective> + nouns: MapOf<Noun> + entities: Entity[] + width: number + height: number + name: string + connectors: Object[] + + static async from(source) { + const width = source.width + const height = source.height + const name = source.name || 'A Level' + + const entities = [] + const nouns = {} + const adjectives = {} + + const textNoun = await Noun.createDefaultTextNoun() + + const rawNouns = source.nouns || [] + for (const src of rawNouns) { + const noun = new Noun(src.images) + nouns[src.name] = noun + if (src.hasOwnProperty('text_start_locations')) { + for (const {x, y} of src.text_start_locations) { + const entity = new Entity([x, y], textNoun) + entity.fallbackImage = noun.textImage + entity.meta = { + type: 'noun', + name: src.name, + } + entities.push(entity) + } + } + if (src.hasOwnProperty('entity_start_locations')) { + for (const {x, y} of src.entity_start_locations) { + const entity = new Entity([x, y], noun) + entities.push(entity) + } + } + } + + const rawAdjectives = source.adjectives || [] + for (const src of rawAdjectives) { + const interactions = await Adjective.resolveInteractionSource(src.source) + const adjective = new Adjective(src.name, interactions) + adjectives[src.name] = adjective + if (src.hasOwnProperty('text_start_locations')) { + for (const {x, y} of src.text_start_locations) { + const entity = new Entity([x, y], textNoun) + entity.fallbackImage = src.images.text + entity.meta = { + type: 'adjective', + name: src.name, + } + entities.push(entity) + } + } + } + + if (source.hasOwnProperty('connectors')) { + for (const { x, y, type: cType } of source.connectors) { + const entity = new Entity([x, y], textNoun) + entity.fallbackImage = `/${ cType }-text.png` + entity.meta = { + type: 'connector', + name: cType, + } + entities.push(entity) + } + } + + return new Level({ + width, + height, + name, + entities, + nouns, + adjectives, + }) + } + + + constructor(opts: ConstructorParams) { + this.adjectives = opts.adjectives; + this.nouns = opts.nouns; + this.entities = opts.entities; + this.width = opts.width; + this.height = opts.height; + this.name = opts.name; + } + + get connectors() { + const connectors = this.findEntities(e => e.meta.type === 'connector') + return connectors.map(c => ({ x: c.x, y: c.y, type: c.meta.name })) + } + + findEntities(p) { + return this.entities.filter(p) + } + + findEntitiesAt(x, y, p = () => true) { + return this.entities.filter(e => e.x === x && e.y === y && p(e)) + } + + findEntitiesFor(noun) { + return this.entities.filter(e => e.noun === noun || e.noun.meta.name === noun) + } + + findEntitiesThat(adjective) { + if (typeof adjective === 'string') { + return this.entities.filter(e => { + return Array.from(e.noun.adjectives).some(a => a.id === adjective) + }) + } else { + return this.entities.filter(e => e.noun.adjectives.has(adjective)) + } + } + + swapNouns(oldNoun, newNoun) { + this.entities.forEach(entity => { + if (entity.noun === oldNoun) { + entity.noun = newNoun + } + }) + } + + moveYou({ x = 0, y = 0 } = {}) { + const you = this.adjectives.you + if (you) { + console.log(you) + this.findEntitiesThat(you).forEach(entity => { + entity.x += x + entity.y += y + }) + } + } + + async processInput(input = Inputs.Actions.Wait) { + switch(input) { + case Inputs.Controls.Up: { + const md = { x: 0, y: -1 } + this.moveYou(md) + return this.tick(md) + } + case Inputs.Controls.Left: { + const md = { x: -1, y: 0 } + this.moveYou(md) + return this.tick(md) + } + case Inputs.Controls.Right: { + const md = { x: 1, y: 0 } + this.moveYou(md) + return this.tick(md) + } + case Inputs.Controls.Down: { + const md = { x: 0, y: 1 } + this.moveYou(md) + return this.tick(md) + } + case Inputs.Actions.Wait: { + return this.tick() + } + } + } + + async resolveEntityInteractions(entity, delta) { + const others = this.findEntitiesAt(entity.x, entity.y, e => e !== entity) + if (others.length > 0) { + for (const other of others) { + const result = await entity.onCollide(other) + + if (result === 'is-moved' && delta) { + other.x += delta.x + other.y += delta.y + await this.resolveEntityInteractions(other, delta) + } else if (result === 'stop' && delta) { + entity.x -= delta.x + entity.y -= delta.y + await this.resolveEntityInteractions(entity, delta) + } + } + } + } + + async tick(delta= null) { + for (const entity of this.entities) { + await this.resolveEntityInteractions(entity, delta) + } + + const newDelta = this.connectors.map(connector => { + let leftright = null + const [left] = this.findEntitiesAt(connector.x - 1, connector.y, e => e.meta && e.meta.type) + const [right] = this.findEntitiesAt(connector.x + 1, connector.y, e => e.meta && e.meta.type) + + if (left && right) { + leftright = { from: left, to: right } + } + + let updown = null + const [up] = this.findEntitiesAt(connector.x, connector.y - 1, e => e.meta && e.meta.type) + const [down] = this.findEntitiesAt(connector.x, connector.y + 1, e => e.meta && e.meta.type) + + if (up && down) { + updown = { from: up, to: down } + } + + return [leftright, updown].filter(Boolean) + }).reduce((acc, c) => (acc || []).concat(c)).reduce((acc, connection) => { + if (connection.from.meta.type === 'noun') { + const { from, to } = connection + const fromName = from.meta.name + acc[fromName] = acc[fromName] || [] + acc[fromName].push(to) + } + + return acc + }, {}) + + Object.values(this.nouns).forEach(noun => noun.clear()) + + outer: for (const [name, newSet] of Object.entries(newDelta)) { + const noun = this.nouns[name] + if (noun) { + const adjectives = [] + for (const updated of newSet) { + if (updated.meta.type === 'noun') { + this.swapNouns(noun, this.nouns[updated.meta.name]) + continue outer + } else if (updated.meta.type === 'adjective') { + adjectives.push(this.adjectives[updated.meta.name]) + } + } + + noun.set(adjectives.filter(Boolean)) + } + } + } +} \ No newline at end of file diff --git a/src/game/Noun.js b/src/game/Noun.js new file mode 100644 index 0000000..28b5d10 --- /dev/null +++ b/src/game/Noun.js @@ -0,0 +1,74 @@ +// @flow + +import Adjective from './Adjective' + +export type ImageOptions = { + text: string, + entity?: string, +} + +export default class Noun { + adjectives: Set<Adjective> + + textImage: string + entityImage: string + + meta = {} + + static async createDefaultTextNoun() { + const text = new Noun({ text: '/text-text.png' }) + const pushData = await Adjective.resolveInteractionSource({ type: 'internal', data: 'adjectives/push' }) + const push = new Adjective('push', pushData) + text.adjectives.add(push) + text.meta.defaultTextNoun = true + return text + } + + constructor(images: ImageOptions) { + this.textImage = images.text + this.entityImage = images.entity + this.adjectives = new Set() + } + + async interact(otherNoun: Noun, ownLocation: [number, number], previousLocation: [number, number]) { + const otherAdjectives = otherNoun.adjectives + console.log(this.adjectives) + outer: for (const adjective of this.adjectives) { + for (const other of otherAdjectives) { + console.log(adjective) + const outcome = await adjective.interact(other, ownLocation, previousLocation) + return outcome + // + // console.log(adjective.id, other.id, outcome) + // switch (outcome) { + // case "stop": + // break outer + // case "destroy-self": + // console.log("DESTROY_SELF") + // break outer + // case "destroy-other": + // console.log("DESTORY_OTHER") + // break outer + // case "destroy-both": + // console.log("DESTROY_BOTH") + // break outer + // case "is-moved": + // console.log("MOVE_BOTH") + // break outer + // } + } + } + } + + clear() { + this.set([]) + } + + set(adjectives) { + this.adjectives = new Set(adjectives) + } + + add(adjective) { + this.adjectives.add(adjective) + } +} \ No newline at end of file diff --git a/src/render/Level.js b/src/render/Level.js new file mode 100644 index 0000000..4567396 --- /dev/null +++ b/src/render/Level.js @@ -0,0 +1,136 @@ +import React from 'react' +import LevelData, { Inputs } from '../game/Level' + +function* __rangeInner(start, end, step) { + let c = start + while (c < end) { + yield c + c += step + } +} + +function range(...args) { + if (args.length === 1) { + const [end] = args + return __rangeInner(0, end, 1) + } + if (args.length === 2) { + const [start, end] = args + return __rangeInner(start, end, 1) + } + if (args.length === 3) { + const [start, end, step] = args + return __rangeInner(start, end, step) + } + throw new TypeError('Invalid number of arguments') +} + +type Props = { + scale?: number, + size?: number, + margin?: number, + level: LevelData, +} + +export default class Level extends React.Component<Props> { + static defaultProps = { + scale: 1, + size: 32, + margin: 1, + active: false, + } + + + state = { + nonce: Math.random(), + } + + + componentDidMount(): void { + window.addEventListener('keyup', this.handleKeyPress) + } + + handleKeyPress = async e => { + e.preventDefault() + if (this.state.active > 0) { + return + } + const { key } = e + let input = null + switch (key.toLowerCase()) { + case 'arrowleft': + case 'a': + input = Inputs.Controls.Left + break + case 'arrowright': + case 'd': + input = Inputs.Controls.Right + break + case 'arrowdown': + case 's': + input = Inputs.Controls.Down + break + case 'arrowup': + case 'w': + input = Inputs.Controls.Up + break + case ' ': + input = Inputs.Actions.Wait + break + } + + if (input) { + this.setState(s => ({ active: s.active + 1 })) + await this.props.level.processInput(input) + this.setState(s => ({ nonce: Math.random(), active: s.active - 1 })) + } + } + + render() { + const {scale, size, margin, level} = this.props + const {width, height} = level + return ( + <div className="level-container"> + {[...range(width)].map(x => + [...range(height)].map(y => ( + <div className="level-tile" style={{ + left: (margin * x) + (x * 32 * scale), + top: (margin * y) + (y * 32 * scale), + width: size * scale, + height: size * scale + }}/> + )) + ).reduce((a, c) => (a || []).concat(c))} + {level.entities.map(entity => { + const {x, y, image} = entity.renderProps + const left = (margin * x) + (x * 32 * scale) + const top = (margin * y) + (y * 32 * scale) + + return ( + <img className="level-entity" src={image} style={{ + left, + top, + width: size * scale, + height: size * scale + }}/> + ) + })} + {level.connectors.map(connector => { + const {x, y, type: cType} = connector + const left = (margin * x) + (x * 32 * scale) + const top = (margin * y) + (y * 32 * scale) + + return ( + <img className="level-entity" src={`/${ cType }-text.png`} style={{ + left, + top, + width: size * scale, + height: size * scale + }}/> + ) + })} + </div> + ) + } + +} \ No newline at end of file diff --git a/src/store/level/reducer.js b/src/store/level/reducer.js new file mode 100644 index 0000000..bb4d648 --- /dev/null +++ b/src/store/level/reducer.js @@ -0,0 +1,4 @@ + +const initial = { + entities: [], +} \ No newline at end of file diff --git a/src/store/reducers.js b/src/store/reducers.js new file mode 100644 index 0000000..985eb16 --- /dev/null +++ b/src/store/reducers.js @@ -0,0 +1,6 @@ +import { combineReducers } from "redux"; +import level from './level/reducer' + +export default combineReducers({ + level, +}) \ No newline at end of file diff --git a/src/store/store.js b/src/store/store.js new file mode 100644 index 0000000..e8d5388 --- /dev/null +++ b/src/store/store.js @@ -0,0 +1,8 @@ +import {createStore} from "redux" +import reducers from './reducers' + +const store = createStore(reducers) + +window._s = store + +export default store -- GitLab