chore: initial

This commit is contained in:
cha0s 2019-03-20 15:28:18 -05:00
commit f6ca8e7925
21 changed files with 9916 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
dist
node_modules

16
babel.config.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

View File

@ -0,0 +1,7 @@
{
"frameRate": 0.1,
"frameCount": 8,
"frameSize": [128, 128],
"directionCount": 4,
"imageUri": "/idle.png"
}

BIN
resource/idle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,7 @@
{
"frameRate": 0.1,
"frameCount": 8,
"frameSize": [128, 128],
"directionCount": 4,
"imageUri": "/moving.png"
}

BIN
resource/moving.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

0
server/connection.js Normal file
View File

126
server/game.js Normal file
View 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
View 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);

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

4632
yarn.lock Normal file

File diff suppressed because it is too large Load Diff