chore: initial
This commit is contained in:
commit
f6ca8e7925
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
dist
|
||||
node_modules
|
16
babel.config.js
Normal file
16
babel.config.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
|
||||
const presets = [
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-env',
|
||||
];
|
||||
const plugins = [
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
];
|
||||
|
||||
return {
|
||||
presets,
|
||||
plugins
|
||||
};
|
||||
}
|
27
client/index.html
Normal file
27
client/index.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Avocado</title>
|
||||
<style>
|
||||
html, body {
|
||||
background-color: #333333;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.app, canvas {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app"></div>
|
||||
</body>
|
||||
</html>
|
90
client/index.js
Normal file
90
client/index.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
import {create as createClient} from '@avocado/client';
|
||||
import {create as createEntity, EntityList} from '@avocado/entity';
|
||||
import {ActionRegistry} from '@avocado/input';
|
||||
import {AnimationView, Color, Container, Primitives, Renderer} from '@avocado/graphics';
|
||||
import {StateSynchronizer} from '@avocado/state';
|
||||
import {Animation} from '@avocado/timing';
|
||||
|
||||
const stage = new Container();
|
||||
|
||||
const entityList = new EntityList();
|
||||
const stateSynchronizer = new StateSynchronizer({
|
||||
entityList,
|
||||
});
|
||||
|
||||
entityList.on('entityAdded', (entity) => {
|
||||
if ('container' in entity) {
|
||||
stage.addChild(entity.container);
|
||||
// Debug circle.
|
||||
const primitives = new Primitives();
|
||||
primitives.drawCircle(
|
||||
[0, 0],
|
||||
16,
|
||||
Primitives.lineStyle(new Color(255, 0, 255), 1)
|
||||
);
|
||||
primitives.zIndex = 1;
|
||||
entity.container.addChild(primitives);
|
||||
}
|
||||
});
|
||||
entityList.on('entityRemoved', (entity) => {
|
||||
if ('container' in entity) {
|
||||
stage.removeChild(entity.container);
|
||||
}
|
||||
});
|
||||
|
||||
const actionRegistry = new ActionRegistry();
|
||||
actionRegistry.mapKeysToActions({
|
||||
'w': 'MoveUp',
|
||||
'a': 'MoveLeft',
|
||||
's': 'MoveDown',
|
||||
'd': 'MoveRight',
|
||||
});
|
||||
let actionState = actionRegistry.state();
|
||||
|
||||
const socket = createClient(window.location.href);
|
||||
const renderer = new Renderer([1280, 720]);
|
||||
const appNode = document.querySelector('.app');
|
||||
appNode.appendChild(renderer.element);
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.dispose(() => {
|
||||
appNode.removeChild(renderer.element);
|
||||
stage.destroy();
|
||||
renderer.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
// Input.
|
||||
actionRegistry.listen();
|
||||
// Messages sent.
|
||||
const handle = setInterval(() => {
|
||||
if (actionState !== actionRegistry.state()) {
|
||||
actionState = actionRegistry.state();
|
||||
socket.send({
|
||||
type: 'input',
|
||||
payload: actionState.toJS()
|
||||
});
|
||||
}
|
||||
}, 1000 / 60);
|
||||
// Messages received.
|
||||
let dirty = false;
|
||||
function onMessage({type, payload}) {
|
||||
switch (type) {
|
||||
case 'state-update':
|
||||
stateSynchronizer.acceptStateChange(payload);
|
||||
dirty = true;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
socket.on('message', onMessage);
|
||||
// Render.
|
||||
function render() {
|
||||
stage.tick();
|
||||
if (!dirty) {
|
||||
return;
|
||||
}
|
||||
renderer.render(stage);
|
||||
dirty = false;
|
||||
}
|
||||
const renderHandle = setInterval(render, 1000 / 60);
|
34
common-mistakes.md
Normal file
34
common-mistakes.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
```
|
||||
webpack:///../lib/@avocado/packages/entity/trait.js?:29
|
||||
entity.on("".concat(type, ".trait-").concat(traitType), listeners[type], this);
|
||||
```
|
||||
|
||||
This is because you subclassed `Trait` but forgot to pass through the arguments
|
||||
from your subclass constructor to the superclass constructor. Always default
|
||||
to writing your Trait definition like:
|
||||
|
||||
```
|
||||
class MyTrait extends Trait {
|
||||
|
||||
initialize() {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
All trait initialization should be done in `initialize`.
|
||||
|
||||
If you need really need to access arguments in your subclass constructor, opt
|
||||
to pass along `arguments` instead, such as:
|
||||
|
||||
```
|
||||
class MyTrait extends Trait {
|
||||
|
||||
constructor(entity, params, state) {
|
||||
super(...arguments);
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
```
|
34
generate-traits.js
Normal file
34
generate-traits.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
const path = require('path');
|
||||
|
||||
const glob = require('glob');
|
||||
|
||||
// Dynamically require all traits.
|
||||
module.exports = (source) => {
|
||||
const modeModulesPath = path.resolve(
|
||||
__dirname, 'node_modules',
|
||||
);
|
||||
const traitPaths = [
|
||||
path.resolve(
|
||||
modeModulesPath, '@avocado', 'entity', 'traits', '*.js',
|
||||
),
|
||||
];
|
||||
const files = glob.sync(traitPaths.join());
|
||||
const modules = files.map((file) => {
|
||||
let dirname = path.dirname(file);
|
||||
dirname = dirname.replace(`${modeModulesPath}/`, '');
|
||||
const basename = path.basename(file, '.js');
|
||||
return `${dirname}/${basename}`;
|
||||
});
|
||||
const defs = modules.map((module_) => {
|
||||
const basename = path.basename(module_);
|
||||
const parts = basename.split('-');
|
||||
const className = parts.reduce((className, part) => {
|
||||
const firstLetter = part.charAt(0).toUpperCase();
|
||||
const rest = part.substr(1).toLowerCase();
|
||||
return className + firstLetter + rest;
|
||||
}, '');
|
||||
return `import {${className}} from '${module_}';\nregisterTrait(${className});\n`;
|
||||
});
|
||||
defs.unshift(`import {registerTrait} from '@avocado/entity/trait-registry';\n`);
|
||||
return defs.join('\n');
|
||||
}
|
40
package.json
Normal file
40
package.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "avocado-examples",
|
||||
"version": "1.0.0",
|
||||
"author": "cha0s",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.3.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.3.4",
|
||||
"@babel/polyfill": "7.2.5",
|
||||
"@babel/preset-env": "7.3.4",
|
||||
"@babel/preset-react": "7.0.0",
|
||||
"babel-loader": "8.0.5",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"start-server-webpack-plugin": "2.2.5",
|
||||
"webpack": "4.29.6",
|
||||
"webpack-cli": "3.2.3",
|
||||
"webpack-dev-server": "3.2.1",
|
||||
"webpack-node-externals": "1.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"client": "webpack-dev-server --config webpack.client.config.js",
|
||||
"server": "webpack --config webpack.server.config.js",
|
||||
"start": "webpack --config webpack.server.config.js && webpack-dev-server --config webpack.client.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@avocado/client": "1.x",
|
||||
"@avocado/core": "1.x",
|
||||
"@avocado/entity": "1.x",
|
||||
"@avocado/graphics": "1.x",
|
||||
"@avocado/input": "1.x",
|
||||
"@avocado/mixins": "1.x",
|
||||
"@avocado/resource": "1.x",
|
||||
"@avocado/server": "1.x",
|
||||
"@avocado/state": "1.x",
|
||||
"@avocado/timing": "1.x",
|
||||
"glob": "^7.1.3",
|
||||
"immutablediff": "^0.4.4",
|
||||
"source-map-support": "^0.5.11"
|
||||
}
|
||||
}
|
0
register-traits.js
Normal file
0
register-traits.js
Normal file
7
resource/idle.animation.json
Normal file
7
resource/idle.animation.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"frameRate": 0.1,
|
||||
"frameCount": 8,
|
||||
"frameSize": [128, 128],
|
||||
"directionCount": 4,
|
||||
"imageUri": "/idle.png"
|
||||
}
|
BIN
resource/idle.png
Normal file
BIN
resource/idle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
7
resource/moving.animation.json
Normal file
7
resource/moving.animation.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"frameRate": 0.1,
|
||||
"frameCount": 8,
|
||||
"frameSize": [128, 128],
|
||||
"directionCount": 4,
|
||||
"imageUri": "/moving.png"
|
||||
}
|
BIN
resource/moving.png
Normal file
BIN
resource/moving.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.7 KiB |
0
server/connection.js
Normal file
0
server/connection.js
Normal file
126
server/game.js
Normal file
126
server/game.js
Normal file
|
@ -0,0 +1,126 @@
|
|||
// Node.
|
||||
import {performance} from 'perf_hooks';
|
||||
// 3rd party.
|
||||
import immutablediff from 'immutablediff';
|
||||
// 2nd party.
|
||||
import {
|
||||
create as createEntity,
|
||||
EntityList,
|
||||
registerTrait,
|
||||
} from '@avocado/entity';
|
||||
import {StateSynchronizer} from '@avocado/state';
|
||||
// Traits.
|
||||
import {Controllable} from './traits/controllable';
|
||||
registerTrait(Controllable);
|
||||
// Create game.
|
||||
export default function(avocadoServer) {
|
||||
avocadoServer.on('connect', createConnectionListener(avocadoServer));
|
||||
setInterval(createMainLoop(avocadoServer), 1000 / 80);
|
||||
}
|
||||
// Entity tracking.
|
||||
const entityList = new EntityList();
|
||||
const stateSynchronizer = new StateSynchronizer({
|
||||
entityList,
|
||||
});
|
||||
// Connection listener.
|
||||
function createConnectionListener(avocadoServer) {
|
||||
return (socket) => {
|
||||
// Create and track a new entity for the connection.
|
||||
const entity = createEntityForConnection();
|
||||
entityList.addEntity(entity);
|
||||
socket.entity = entity;
|
||||
// Send complete state.
|
||||
socket.send({
|
||||
type: 'state-update',
|
||||
payload: stateSynchronizer.state().toJS(),
|
||||
});
|
||||
// Listen for events.
|
||||
socket.on('message', createMessageListener(avocadoServer, socket));
|
||||
socket.on('disconnect', createDisconnectionListener(avocadoServer, socket));
|
||||
}
|
||||
}
|
||||
// Handle incoming messages.
|
||||
function createMessageListener(avocadoServer, socket) {
|
||||
const {entity} = socket;
|
||||
return ({type, payload}) => {
|
||||
switch (type) {
|
||||
case 'input':
|
||||
entity.inputState = payload;
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
// Handle disconnection.
|
||||
function createDisconnectionListener(avocadoServer, socket) {
|
||||
const {entity} = socket;
|
||||
return () => {
|
||||
entityList.removeEntity(entity);
|
||||
avocadoServer.broadcast({
|
||||
type: 'state-update',
|
||||
payload: {
|
||||
entityList: {
|
||||
[entity.instanceUuid]: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
// Create an entity for a new connection.
|
||||
function createEntityForConnection() {
|
||||
const entity = createEntity();
|
||||
return entity.fromJSON({
|
||||
traits: {
|
||||
animated: {
|
||||
params: {
|
||||
animations: {
|
||||
idle: {
|
||||
offset: [0, -48],
|
||||
uri: '/idle.animation.json',
|
||||
},
|
||||
moving: {
|
||||
offset: [0, -48],
|
||||
uri: '/moving.animation.json',
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
controllable: {},
|
||||
directional: {
|
||||
params: {
|
||||
directionCount: 4,
|
||||
},
|
||||
},
|
||||
existent: {},
|
||||
graphical: {},
|
||||
mobile: {
|
||||
state: {
|
||||
speed: 400,
|
||||
},
|
||||
},
|
||||
positioned: {
|
||||
state: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
// Main loop.
|
||||
let lastTime = performance.now();
|
||||
function createMainLoop(avocadoServer) {
|
||||
return () => {
|
||||
const now = performance.now();
|
||||
const elapsed = (now - lastTime) / 1000;
|
||||
lastTime = now;
|
||||
entityList.tick(elapsed);
|
||||
const diff = stateSynchronizer.diff();
|
||||
if (StateSynchronizer.noChange === diff) {
|
||||
return;
|
||||
}
|
||||
avocadoServer.broadcast({
|
||||
type: 'state-update',
|
||||
payload: diff,
|
||||
});
|
||||
}
|
||||
}
|
9
server/index.js
Normal file
9
server/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import http from 'http';
|
||||
import {Server} from '@avocado/server/socket';
|
||||
const httpServer = http.createServer();
|
||||
// Listen.
|
||||
httpServer.listen(8420, '0.0.0.0');
|
||||
// Start game server.
|
||||
import createGame from './game';
|
||||
const avocadoServer = new Server(httpServer);
|
||||
createGame(avocadoServer);
|
47
server/traits/controllable.js
Normal file
47
server/traits/controllable.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import * as I from 'immutable';
|
||||
|
||||
import {Trait} from '@avocado/entity';
|
||||
|
||||
// Input handling.
|
||||
export class Controllable extends Trait {
|
||||
|
||||
initialize() {
|
||||
this._inputState = I.Set();
|
||||
}
|
||||
|
||||
listeners() {
|
||||
return {
|
||||
tick: (elapsed) => {
|
||||
const {_inputState: inputState} = this;
|
||||
if (0 === inputState.size) {
|
||||
this.entity.currentAnimation = 'idle';
|
||||
return;
|
||||
}
|
||||
const movementVector = [0, 0];
|
||||
if (inputState.has('MoveUp')) {
|
||||
movementVector[1] -= 1;
|
||||
}
|
||||
if (inputState.has('MoveRight')) {
|
||||
movementVector[0] += 1;
|
||||
}
|
||||
if (inputState.has('MoveDown')) {
|
||||
movementVector[1] += 1;
|
||||
}
|
||||
if (inputState.has('MoveLeft')) {
|
||||
movementVector[0] -= 1;
|
||||
}
|
||||
if (0 === movementVector[0] && 0 === movementVector[1]) {
|
||||
return;
|
||||
}
|
||||
this.entity.requestMovement(movementVector);
|
||||
this.entity.currentAnimation = 'moving';
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
set inputState(inputState) {
|
||||
this._inputState = I.Set(inputState);
|
||||
}
|
||||
|
||||
}
|
||||
|
44
webpack.client.config.js
Normal file
44
webpack.client.config.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
const path = require('path');
|
||||
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
const config = require('./webpack.common.config');
|
||||
|
||||
config.entry = {
|
||||
client: [
|
||||
'@babel/polyfill',
|
||||
path.join(__dirname, 'client', 'index.js'),
|
||||
path.join(__dirname, 'register-traits.js'),
|
||||
],
|
||||
};
|
||||
config.devServer = {
|
||||
compress: true,
|
||||
contentBase: path.resolve(__dirname, 'resource'),
|
||||
disableHostCheck: true,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
|
||||
},
|
||||
host: '0.0.0.0',
|
||||
overlay: true,
|
||||
port: 8421,
|
||||
proxy: {
|
||||
'/avocado': {
|
||||
target: 'http://localhost:8420',
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
stats: 'minimal',
|
||||
watchContentBase: true,
|
||||
};
|
||||
config.devtool = 'eval-source-map';
|
||||
config.node = {
|
||||
fs: 'empty',
|
||||
path: 'empty',
|
||||
};
|
||||
config.plugins.push(new HtmlWebpackPlugin({
|
||||
template: path.resolve(__dirname, 'client', 'index.html'),
|
||||
}));
|
||||
|
||||
module.exports = config;
|
41
webpack.common.config.js
Normal file
41
webpack.common.config.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
const path = require('path');
|
||||
|
||||
const webpack = require('webpack');
|
||||
|
||||
const config = {
|
||||
devtool: 'inline-source-map',
|
||||
mode: 'development',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: [
|
||||
/(node_modules\/(?!@avocado))/,
|
||||
],
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /register-traits.js/,
|
||||
use: {
|
||||
loader: './generate-traits',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: '[name].js',
|
||||
},
|
||||
plugins: [
|
||||
],
|
||||
resolve: {
|
||||
modules: [path.resolve(__dirname, 'node_modules')],
|
||||
},
|
||||
resolveLoader: {
|
||||
modules: [path.resolve(__dirname, 'node_modules')],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
27
webpack.server.config.js
Normal file
27
webpack.server.config.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
const path = require('path');
|
||||
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
const StartServerPlugin = require('start-server-webpack-plugin');
|
||||
|
||||
const config = require('./webpack.common.config');
|
||||
|
||||
config.entry = {
|
||||
server: [
|
||||
'source-map-support/register',
|
||||
'@babel/polyfill',
|
||||
path.join(__dirname, 'server', 'index.js'),
|
||||
path.join(__dirname, 'register-traits.js'),
|
||||
],
|
||||
};
|
||||
config.externals = [
|
||||
nodeExternals({
|
||||
whitelist: /@avocado/,
|
||||
}),
|
||||
];
|
||||
config.plugins.push(new StartServerPlugin({
|
||||
name: 'server.js',
|
||||
restartable: false,
|
||||
}));
|
||||
config.target = 'node';
|
||||
|
||||
module.exports = config;
|
4733
yarn-error.log
Normal file
4733
yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user