chore: initial

This commit is contained in:
cha0s 2020-12-28 21:18:54 -06:00
commit ec0487d49c
186 changed files with 91410 additions and 0 deletions

116
.gitignore vendored Normal file
View File

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

26
app/.eslint.defaults.js Normal file
View File

@ -0,0 +1,26 @@
const config = {
globals: {
__non_webpack_require__: true,
process: true,
window: true,
},
rules: {
'babel/object-curly-spacing': 'off',
'brace-style': ['error', 'stroustrup'],
'no-bitwise': ['error', {int32Hint: true}],
'no-plusplus': 'off',
'no-shadow': 'off',
'no-underscore-dangle': 'off',
'padded-blocks': ['error', {classes: 'always'}],
yoda: 'off',
},
settings: {
'import/resolver': {
webpack: {
config: `${__dirname}/webpack.config.js`,
},
},
},
};
module.exports = config;

5
app/.eslintrc.js Normal file
View File

@ -0,0 +1,5 @@
const neutrino = require('neutrino');
process.env.LATUS_LINTING = true;
module.exports = neutrino(require('./.neutrinorc')).eslintrc();

119
app/.gitignore vendored Normal file
View File

@ -0,0 +1,119 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
/build
/latus.yml

5
app/.mocharc.js Normal file
View File

@ -0,0 +1,5 @@
const neutrino = require('neutrino');
process.env.NODE_ENV = process.env.NODE_ENV || 'test';
module.exports = neutrino().mocha();

92
app/.neutrinorc.js Normal file
View File

@ -0,0 +1,92 @@
require('dotenv/config');
const airbnb = require('@neutrinojs/airbnb');
const clean = require('@neutrinojs/clean');
const copy = require('@neutrinojs/copy');
const mocha = require('@neutrinojs/mocha');
const node = require('@neutrinojs/node');
const {EnvironmentPlugin} = require('webpack');
const nodeExternals = require('webpack-node-externals');
module.exports = {
options: {
root: __dirname,
},
use: [
airbnb({
eslint: {
cache: false,
baseConfig: require('./.eslint.defaults'),
},
}),
clean({
cleanOnceBeforeBuildPatterns: ['**/*.hot-update.*'],
}),
copy({
patterns: [{
from: 'src/assets',
to: 'http',
}],
}),
mocha(),
node(),
(neutrino) => {
[
'components',
'context',
'fonts',
'hooks',
'images',
'scss',
].forEach((path) => {
neutrino.config.resolve.alias
.set(path, `${neutrino.options.source}/react/${path}`);
});
if (process.env.LATUS_LINTING) {
return;
}
neutrino.config.module
.rule('compile')
.use('babel')
.get('options').plugins.push(
[
'babel-plugin-webpack-alias',
{
config: `${__dirname}/webpack.config.js`,
},
],
);
neutrino.config
.plugin('environment')
.use(EnvironmentPlugin, [{
SIDE: 'server',
}]);
neutrino.config
.entry('index')
.prepend('@latus/core/start');
if ('production' !== neutrino.config.get('mode')) {
neutrino.config
.entry('index')
.prepend('dotenv/config');
neutrino.config
.plugin('start-server')
.tap((args) => {
const options = args[0];
const inspectArg = process.argv.find((arg) => -1 !== arg.indexOf('--inspect'));
if (inspectArg) {
options.nodeArgs.push(inspectArg);
}
const profArg = process.argv.find((arg) => -1 !== arg.indexOf('--prof'));
if (profArg) {
options.nodeArgs.push(profArg);
}
options.nodeArgs.push('--experimental-repl-await');
return args;
});
}
neutrino.config
.externals(nodeExternals({
}));
},
],
};

17
app/docker-compose.yml Normal file
View File

@ -0,0 +1,17 @@
version: '2'
services:
redis:
image: redis:6
ports:
- 6380:6379
mysql:
image: mysql:8
command:
- '--default-authentication-plugin=mysql_native_password'
environment:
- MYSQL_DATABASE=db
- MYSQL_ROOT_PASSWORD=UNSAFE_DEV_PASSWORD
ports:
- 32342:3306

50
app/latus.default.yml Normal file
View File

@ -0,0 +1,50 @@
'@latus/core': {
up: [
'@latus/db',
'@latus/redis',
'@latus/user/session',
'@latus/user/passport',
'@latus/user/local',
'@latus/http',
'@latus/repl',
],
}
'@latus/db': {
models.decorate: [
'@latus/user/local',
],
docker: 'cached',
}
'@latus/governor': {}
'@latus/http': {
client.up: [
'@latus/socket/client',
'@latus/react/client',
],
request: [
'@latus/user/session',
'@latus/user/passport',
],
}
'@latus/react': {}
'@latus/redis': {
docker: 'cached',
}
'@latus/redis/session': {}
'@latus/repl': {}
'@latus/socket': {
authenticate: [
'@latus/user/session',
'@latus/user/passport',
],
connect: [
'@latus/socket',
],
packets.decorate: [
'@latus/governor',
]
}
'@latus/user/local': {}
'@latus/user/models': {}
'@latus/user/passport': {}
'@latus/user/session': {}

45
app/package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "latus",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"build": "webpack --mode production",
"dev": "webpack --mode development",
"docker": "yarn run build && docker build",
"forcelatus": "pkgs=$(find node_modules/@latus -maxdepth 1 -mindepth 1 -printf '@latus/%f '); yarn upgrade $pkgs",
"lint": "eslint --cache --format codeframe --ext mjs,jsx,js src",
"repl": "rlwrap -C qmp socat STDIO UNIX:$(ls /tmp/latus-*.sock | tail -n 1)",
"start": "NODE_ENV=production node build/index.js",
"test": "mocha --watch src",
"watch": "webpack --hot --watch --mode development"
},
"dependencies": {
"@latus/core": "^1.0.0",
"@latus/db": "^1.0.0",
"@latus/governor": "^1.0.0",
"@latus/http": "^1.0.0",
"@latus/react": "^1.0.0",
"@latus/redis": "^1.0.0",
"@latus/repl": "^1.0.0",
"@latus/socket": "^1.0.0",
"@latus/user": "^1.0.0",
"dotenv": "8.2.0",
"react": "^17.0.1",
"react-hot-loader": "4.13.0"
},
"devDependencies": {
"@neutrinojs/airbnb": "^9.4.0",
"@neutrinojs/clean": "^9.1.0",
"@neutrinojs/copy": "^9.4.0",
"@neutrinojs/mocha": "^9.1.0",
"@neutrinojs/node": "^9.1.0",
"babel-plugin-webpack-alias": "^2.1.2",
"eslint": "^6",
"eslint-import-resolver-webpack": "^0.12.1",
"js-yaml": "3.14.0",
"neutrino": "^9.1.0",
"source-map-support": "0.5.19",
"webpack": "^4",
"webpack-cli": "^3"
}
}

1
app/src/index.js Normal file
View File

@ -0,0 +1 @@
process.stdout.write('Your application is starting...\n');

20
app/src/react/index.jsx Normal file
View File

@ -0,0 +1,20 @@
import React from 'react';
import {hot} from 'react-hot-loader';
const App = () => (
<div className="app">
<h1>Latus react app</h1>
<p>Yay, you maaaaade it! :)</p>
<form action="/auth/local" method="post">
<input name="email" />
<input name="password" />
<input type="submit" />
</form>
</div>
);
export default {
hooks: {
'@latus/react/components': () => hot(module)(App),
},
};

28
app/webpack.config.js Normal file
View File

@ -0,0 +1,28 @@
require('source-map-support/register');
// Whilst the configuration object can be modified here, the recommended way of making
// changes is via the presets' options or Neutrino's API in `.neutrinorc.js` instead.
// Neutrino's inspect feature can be used to view/export the generated configuration.
const {Latus} = require('@latus/core');
const neutrino = require('neutrino');
if (process.env.LATUS_LINTING) {
// eslint-disable-next-line global-require
module.exports = neutrino(require('./.neutrinorc')).webpack();
}
else {
module.exports = new Promise((resolve, reject) => {
try {
const latus = Latus.create();
const configs = {
// eslint-disable-next-line global-require
app: require('./.neutrinorc'),
};
latus.invokeFlat('@latus/core/build', configs);
const webpackConfigs = Object.values(configs).map((config) => neutrino(config).webpack());
resolve(webpackConfigs);
}
catch (error) {
reject(error);
}
});
}

8921
app/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
const config = {
globals: {
AVOCADO_CLIENT: true,
AVOCADO_SERVER: true,
process: true,
window: true,
},
ignorePatterns: [
'/*',
'!/src',
],
rules: {
'babel/object-curly-spacing': 'off',
'brace-style': ['error', 'stroustrup'],
'no-bitwise': ['error', {int32Hint: true}],
'no-plusplus': 'off',
'no-shadow': 'off',
'no-underscore-dangle': 'off',
'padded-blocks': ['error', {classes: 'always'}],
yoda: 'off',
},
};
module.exports = config;

3
config/.eslintrc.js Normal file
View File

@ -0,0 +1,3 @@
const neutrino = require('neutrino');
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).eslintrc();

5
config/.mocharc.js Normal file
View File

@ -0,0 +1,5 @@
const neutrino = require('neutrino');
process.env.NODE_ENV = process.env.NODE_ENV || 'test';
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).mocha();

66
config/.neutrinorc.js Normal file
View File

@ -0,0 +1,66 @@
const {basename, dirname, extname, join} = require('path');
const airbnbBase = require('@neutrinojs/airbnb-base');
const mocha = require('@neutrinojs/mocha');
const react = require('@neutrinojs/react');
const nodeExternals = require('webpack-node-externals');
module.exports = {
options: {},
use: [
airbnbBase({
eslint: {
cache: false,
baseConfig: require(`${__dirname}/.eslint.defaults`),
},
}),
(neutrino) => {
const {files = [], name} = neutrino.options.packageJson;
files
.filter((file) => {
try {
require.resolve(`${neutrino.options.source}/${file}`);
return true;
}
catch (error) {
return false;
}
})
.forEach((file) => {
const isIndex = 'index.js' === file;
const trimmed = join(dirname(file), basename(file, extname(file)));
neutrino.options.mains[trimmed] = {entry: isIndex ? file : `./src/${trimmed}`};
});
neutrino.options.output = '.';
react({
clean: false,
})(neutrino);
Object.keys(neutrino.options.mains).forEach((main) => {
neutrino.config.plugins.delete(`html-${main}`);
});
neutrino.config
.devtool('source-map')
.target('node')
.optimization
.splitChunks(false)
.runtimeChunk(false)
.end()
.output
.filename('[name].js')
.library(name)
.libraryTarget('umd')
.umdNamedDefine(true)
.end()
.node
.set('__dirname', false)
.set('__filename', false);
const options = neutrino.config.module
.rule('compile')
.use('babel')
.get('options');
options.presets[0][1].targets = {esmodules: true};
neutrino.config.externals(nodeExternals({importType: 'umd'}));
},
mocha(),
],
};

View File

@ -0,0 +1 @@
module.exports = require('../../config/.eslintrc');

5
config/package/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
**/*.js
**/*.map
!/.*
!/webpack.config.js
!src/**/*.js

View File

@ -0,0 +1 @@
module.exports = require('../../config/.neutrinorc');

View File

@ -0,0 +1,39 @@
{
"name": "@avocado/package",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"scripts": {
"build": "NODE_PATH=./node_modules webpack --mode production",
"clean": "rm -rf yarn.lock node_modules && yarn",
"dev": "NODE_PATH=./node_modules webpack --mode development",
"forcepub": "npm unpublish --force $(node -e 'const {name, version} = require(`./package.json`); process.stdout.write(`${name}@${version}`)') && npm publish",
"link": "node -e \"Object.keys(require('./package.json').dependencies).filter((m) => 0 === m.indexOf('@latus/')).forEach((m) => require('child_process').spawn('yarn', ['link', m]));\"",
"lint": "NODE_PATH=./node_modules eslint --format codeframe --ext mjs,js .",
"test": "NODE_PATH=./node_modules mocha --config ../../config/.mocharc.js",
"unlink": "node -e \"Object.keys(require('./package.json').dependencies).filter((m) => 0 === m.indexOf('@latus/')).forEach((m) => require('child_process').spawn('yarn', ['unlink', m]));\" && yarn install --force",
"watch": "NODE_PATH=./node_modules webpack --watch --mode development"
},
"files": [
"index.js",
"index.js.map"
],
"dependencies": {
"debug": "4.3.1"
},
"devDependencies": {
"@neutrinojs/airbnb-base": "^9.4.0",
"@neutrinojs/copy": "9.4.0",
"@neutrinojs/mocha": "^9.4.0",
"@neutrinojs/react": "^9.4.0",
"chai": "4.2.0",
"eslint": "^7",
"eslint-import-resolver-webpack": "0.13.0",
"mocha": "^8",
"neutrino": "^9.4.0",
"webpack": "^4",
"webpack-cli": "^3",
"webpack-node-externals": "2.5.2"
}
}

View File

View File

@ -0,0 +1,3 @@
const neutrino = require('neutrino');
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();

52
config/split-config.js Normal file
View File

@ -0,0 +1,52 @@
const react = require('@neutrinojs/react');
const nodeExternals = require('webpack-node-externals');
const config = require('./.neutrinorc');
module.exports = ({name, files}, clientMains) => {
const mains = files
.filter((file) => file.match(/\.js$/))
.map((file) => file.slice(0, -3))
.reduce((r, file) => ({...r, [file]: file}), {});
if (clientMains.length > 0) {
const serverMains = Object
.entries(mains)
.filter(([key]) => -1 === clientMains.indexOf(key))
.reduce((r, [k, v]) => ({...r, [k]: v}), {});
const serverConfig = config();
serverConfig.options.mains = serverMains;
const clientConfig = config();
clientConfig.options.mains = clientMains.reduce((r, file) => ({...r, [file]: file}), {});
clientConfig.use[2] = (neutrino) => {
react({
clean: false,
})(neutrino);
Object.keys(clientConfig.options.mains).forEach((main) => {
neutrino.config.plugins.delete(`html-${main}`);
});
neutrino.config
.target('web')
.optimization
.splitChunks(false)
.runtimeChunk(false)
.end()
.output
.filename('[name].js')
.library(name)
.libraryTarget('umd')
.umdNamedDefine(true);
};
clientConfig.use.push((neutrino) => {
neutrino.config.node.set('Buffer', true);
});
return [serverConfig, clientConfig];
}
else {
const serverConfig = config();
serverConfig.options = {
mains,
};
return [serverConfig];
}
};

8
config/webpack.config.js Normal file
View File

@ -0,0 +1,8 @@
// Whilst the configuration object can be modified here, the recommended way of making
// changes is via the presets' options or Neutrino's API in `.neutrinorc.js` instead.
// Neutrino's inspect feature can be used to view/export the generated configuration.
const neutrino = require('neutrino');
const configOfConfigs = require(`${__dirname}/.neutrinorc`);
const configs = Array.isArray(configOfConfigs) ? configOfConfigs : [configOfConfigs];
module.exports = configs.map((config) => neutrino(config).webpack());

6
lerna.json Normal file
View File

@ -0,0 +1,6 @@
{
"packages": [
"packages/*"
],
"version": "1.0.0"
}

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "@latus/monorepo",
"private": true,
"scripts": {
"build": "lerna run build",
"clean": "lerna run clean",
"dev": "lerna run dev",
"forcepub": "lerna run forcepub",
"lint": "lerna run lint",
"test": "lerna run test",
"watch": "lerna run watch --parallel"
},
"devDependencies": {
"@neutrinojs/airbnb-base": "^9.4.0",
"@neutrinojs/mocha": "^9.4.0",
"@neutrinojs/react": "^9.4.0",
"chai": "4.2.0",
"eslint": "^7",
"eslint-import-resolver-webpack": "0.13.0",
"lerna": "^3.22.1",
"mocha": "^8",
"neutrino": "^9.4.0",
"webpack": "^4",
"webpack-cli": "^3",
"webpack-node-externals": "2.5.2"
}
}

View File

@ -0,0 +1 @@
module.exports = require('../../config/.eslintrc');

5
packages/behavior/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
**/*.js
**/*.map
!/.*
!/webpack.config.js
!src/**/*.js

View File

@ -0,0 +1 @@
module.exports = require('../../config/.neutrinorc');

View File

@ -0,0 +1,44 @@
{
"name": "@avocado/behavior",
"version": "2.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"scripts": {
"build": "NODE_PATH=./node_modules webpack --mode production",
"clean": "rm -rf yarn.lock node_modules && yarn",
"dev": "NODE_PATH=./node_modules webpack --mode development",
"forcepub": "npm unpublish --force $(node -e 'const {name, version} = require(`./package.json`); process.stdout.write(`${name}@${version}`)') && npm publish",
"link": "node -e \"Object.keys(require('./package.json').dependencies).filter((m) => 0 === m.indexOf('@latus/')).forEach((m) => require('child_process').spawn('yarn', ['link', m]));\"",
"lint": "NODE_PATH=./node_modules eslint --format codeframe --ext mjs,js .",
"test": "NODE_PATH=./node_modules mocha --config ../../config/.mocharc.js",
"unlink": "node -e \"Object.keys(require('./package.json').dependencies).filter((m) => 0 === m.indexOf('@latus/')).forEach((m) => require('child_process').spawn('yarn', ['unlink', m]));\" && yarn install --force",
"watch": "NODE_PATH=./node_modules webpack --watch --mode development"
},
"files": [
"index.js",
"index.js.map"
],
"dependencies": {
"@avocado/core": "^2.0.0",
"@avocado/entity": "^2.0.0",
"@latus/core": "^2.0.0",
"debug": "4.3.1",
"deepmerge": "^4.2.2",
"lodash.mapvalues": "^4.6.0"
},
"devDependencies": {
"@neutrinojs/airbnb-base": "^9.4.0",
"@neutrinojs/copy": "9.4.0",
"@neutrinojs/mocha": "^9.4.0",
"@neutrinojs/react": "^9.4.0",
"chai": "4.2.0",
"eslint": "^7",
"eslint-import-resolver-webpack": "0.13.0",
"mocha": "^8",
"neutrino": "^9.4.0",
"webpack": "^4",
"webpack-cli": "^3",
"webpack-node-externals": "2.5.2"
}
}

View File

@ -0,0 +1,107 @@
import {TickingPromise} from '@avocado/core';
import {compose, EventEmitter} from '@latus/core';
const decorate = compose(
EventEmitter,
);
class Actions {
constructor(expressions) {
this.expressions = 'function' === typeof expressions ? expressions() : expressions;
this._index = 0;
this.promise = null;
}
emitFinished() {
this.emit('@avocado/behavior/actions.finished');
}
get index() {
return this._index;
}
set index(index) {
this._index = index;
}
get() {
return this;
}
tick(context, elapsed) {
// Empty resolves immediately.
if (this.expressions.length === 0) {
this.emitFinished();
return;
}
// If the action promise ticks, tick it.
if (this.promise && this.promise instanceof TickingPromise) {
this.promise.tick(elapsed);
return;
}
// Actions execute immediately until a promise is made, or they're all
// executed.
// eslint-disable-next-line no-constant-condition
while (true) {
// Run the action.
const result = this.expressions[this.index](context);
// Deferred result.
if (result instanceof Promise) {
this.promise = result;
// eslint-disable-next-line no-console
this.promise.catch(console.error).finally(() => this.prologue());
break;
}
// Immediate result.
this.prologue();
// Need to break out immediately if required.
if (0 === this.index) {
break;
}
}
}
parallel(context) {
const results = this.expressions.map((expression) => expression(context));
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result instanceof TickingPromise) {
return TickingPromise.all(results);
}
if (result instanceof Promise) {
return Promise.all(results);
}
}
return results;
}
prologue() {
// Clear out the action promise.
this.promise = null;
// Increment and wrap the index.
this.index = (this.index + 1) % this.expressions.length;
// If rolled over, the actions are finished.
if (0 === this.index) {
this.emitFinished();
}
}
serial(context) {
return this.tickingPromise(context);
}
tickingPromise(context) {
return new TickingPromise(
(resolve) => {
this.once('@avocado/behavior/actions.finished', resolve);
},
(elapsed) => {
this.tick(context, elapsed);
},
);
}
}
export default decorate(Actions);

View File

@ -0,0 +1,44 @@
export function buildValue(value) {
if (
'object' === typeof value
&& (
'expression' === value.type
|| 'expressions' === value.type
|| 'condition' === value.type
)
) {
return value;
}
return {
type: 'literal',
value,
};
}
export function buildExpression(path, value) {
const expression = {
type: 'expression',
ops: path.map((key) => ({type: 'key', key})),
};
if ('undefined' !== typeof value) {
expression.value = buildValue(value);
}
return expression;
}
export function buildInvoke(path, args = []) {
const expression = buildExpression(path);
expression.ops.push({
type: 'invoke',
args: args.map((arg) => buildValue(arg)),
});
return expression;
}
export function buildCondition(operator, operands) {
return {
type: 'condition',
operator,
operands: operands.map((operand) => buildValue(operand)),
};
}

View File

@ -0,0 +1,13 @@
const compilerMap = (latus) => latus.invokeReduce('@avocado/behavior/compilers');
function compilerFor(type, latus) {
const {[type]: compiler} = compilerMap(latus);
return compiler;
}
export default function compile(variant, latus) {
const compiler = compilerFor(variant.type, latus);
return compiler
? compiler(variant)
: () => Promise.reject(new TypeError(`No compiler for '${variant.type}'`));
}

View File

@ -0,0 +1,50 @@
import compile from './compile';
export default (latus) => (condition) => {
const {operator} = condition;
const operands = condition.operands.map((condition) => compile(condition, latus));
return (context) => {
switch (operator) {
case 'is':
return operands[0](context) === operands[1](context);
case 'isnt':
return operands[0](context) !== operands[1](context);
case '>':
return operands[0](context) > operands[1](context);
case '>=':
return operands[0](context) >= operands[1](context);
case '<':
return operands[0](context) < operands[1](context);
case '<=':
return operands[0](context) <= operands[1](context);
case 'or':
if (0 === operands.length) {
return true;
}
for (let i = 0; i < operands.length; i++) {
if (operands[i](context)) {
return true;
}
}
return false;
case 'and':
if (0 === operands.length) {
return true;
}
for (let i = 0; i < operands.length; i++) {
if (!operands[i](context)) {
return false;
}
}
return true;
case 'contains': {
const haystack = operands[0](context);
const needle = operands[1](context);
return -1 !== haystack.indexOf(needle);
}
default: {
throw new TypeError(`Undefined operator '${operator}'`);
}
}
};
};

View File

@ -0,0 +1,85 @@
import {fastApply} from '@avocado/core';
import compile from './compile';
function compileOp(op, latus) {
let args;
if ('invoke' === op.type) {
args = op.args.map((op) => compile(op, latus));
}
return (context, previous, current) => {
switch (op.type) {
case 'key':
return current[op.key];
case 'invoke': {
// Pass the context itself as the last arg.
const evaluated = args.map((fn) => fn(context)).concat(context);
// Promises are resolved transparently.
const apply = (args) => fastApply(previous, current, args);
return evaluated.some((arg) => arg instanceof Promise)
? Promise.all(evaluated).then(apply)
: apply(evaluated);
}
default:
throw new TypeError(`Invalid expression op: '${op.type}'`);
}
};
}
function disambiguateResult(value, fn) {
const apply = (value) => fn(value);
return value instanceof Promise ? value.then(apply) : apply(value);
}
export default (latus) => (expression) => {
if (0 === expression.ops.length) {
return () => undefined;
}
const assign = 'undefined' !== typeof expression.assign
? compile(expression.assign)
: undefined;
const ops = expression.ops.map((op) => compileOp(op, latus));
const {ops: rawOps} = expression;
return (context) => {
let previous = null;
let shorted = false;
const [, ...rest] = ops;
return rest.reduce((current, op, index) => disambiguateResult(current, (current) => {
if (shorted) {
return undefined;
}
let next;
const isLastOp = index === ops.length - 2;
if (!isLastOp || !assign) {
if ('undefined' === typeof current) {
next = undefined;
shorted = true;
}
else {
next = op(context, previous, current);
}
}
else {
const rawOp = rawOps[index + 1];
switch (rawOp.type) {
case 'key':
if ('object' === typeof current) {
// eslint-disable-next-line no-param-reassign
current[rawOp.key] = assign(context);
next = undefined;
}
break;
case 'invoke':
next = undefined;
break;
default:
throw new TypeError(`Invalid expression op: '${op.type}'`);
}
}
previous = current;
// eslint-disable-next-line no-param-reassign
current = next;
return current;
}), context.getValue(rawOps[0].key));
};
};

View File

@ -0,0 +1,5 @@
import compile from './expression';
export default (latus) => ({expressions}) => () => (
expressions.map((expression) => compile(expression, latus))
);

View File

@ -0,0 +1,11 @@
import condition from './condition';
import expression from './expression';
import expressions from './expressions';
import literal from './literal';
export default (latus) => ({
condition: condition(latus),
expression: expression(latus),
expressions: expressions(latus),
literal: literal(latus),
});

View File

@ -0,0 +1 @@
export default () => ({value}) => () => value;

View File

@ -0,0 +1,71 @@
const globals = (latus) => latus.invokeReduced('@avocado/behavior/globals');
export default class Context {
constructor(defaults = {}, latus) {
this.latus = latus;
this.map = new Map();
this.clear();
this.addObjectMap(defaults);
}
add(key, value, type = 'undefined') {
if (key) {
this.map.set(key, [value, type]);
}
}
addObjectMap(map) {
Object.entries(map)
.forEach(([key, [variable, type]]) => (
this.add(key, variable, type)
));
}
all() {
return Array.from(this.map.keys())
.reduce((r, key) => ({
...r,
[key]: this.get(key),
}), {});
}
clear() {
this.destroy();
this.add('context', this, 'context');
this.addObjectMap(globals(this.latus));
}
description() {
const children = Object.entries(this.all())
.reduce((r, [key, [, type]]) => ({
...r,
[key]: {type},
}), {});
return {
children,
type: 'context',
};
}
destroy() {
this.map.clear();
}
get(key) {
return this.has(key) ? this.map.get(key) : [undefined, 'undefined'];
}
getValue(key) {
return this.get(key)[0];
}
getType(key) {
return this.get(key)[1];
}
has(key) {
return this.map.has(key);
}
}

View File

@ -0,0 +1,15 @@
import Actions from '../actions';
export default {
conditional: (condition, expressions, context) => (
condition.get(context) ? (new Actions(expressions)).serial(context) : undefined
),
nop: () => {},
parallel: (expressions, context) => (new Actions(expressions)).parallel(context),
serial: (expressions, context) => (new Actions(expressions)).serial(context),
};

View File

@ -0,0 +1,10 @@
import Flow from './flow';
import Timing from './timing';
import Utility from './utility';
export default (latus) => ({
latus: [latus, 'latus'],
Flow: [Flow, 'Flow'],
Timing: [Timing, 'Timing'],
Utility: [Utility, 'Utility'],
});

View File

@ -0,0 +1,16 @@
import {TickingPromise} from '@avocado/core';
export default {
wait: (duration) => new TickingPromise(
() => {},
(elapsed, resolve) => {
// eslint-disable-next-line no-param-reassign
duration -= elapsed;
if (duration <= 0) {
resolve();
}
},
),
};

View File

@ -0,0 +1,36 @@
import merge from 'deepmerge';
export default {
makeArray: (...args) => {
// No context!
args.pop();
return args;
},
makeObject: (...args) => {
// No context!
args.pop();
const object = {};
while (args.length > 0) {
const key = args.shift();
const value = args.shift();
object[key] = value;
}
return object;
},
makeVector: (x, y) => [x, y],
log: (...args) => {
// eslint-disable-next-line no-console
console.log(...args);
},
merge: (...args) => {
// No context!
args.pop();
return merge(...args);
},
};

View File

@ -0,0 +1,17 @@
import compilers from './compilers';
import globals from './globals';
import traits from './traits';
export * from './builders';
export {
default as Context,
} from './context';
export default {
hooks: {
'@avocado/behavior/compilers': compilers,
'@avocado/behavior/globals': globals,
'@avocado/behavior/traits': traits,
},
};

View File

@ -0,0 +1,122 @@
import {StateProperty, Trait} from '@avocado/entity';
import {compose} from '@latus/core';
import mapValues from 'lodash.mapvalues';
import Actions from '../actions';
import compile from '../compilers/compile';
import Context from '../context';
const decorate = compose(
StateProperty('currentRoutine', {
track: true,
}),
StateProperty('isBehaving'),
);
export default (latus) => class Behaved extends decorate(Trait) {
static behaviorTypes() {
return {
context: {
type: 'context',
label: 'Context',
},
};
}
static defaultParams() {
return {
routines: {},
};
}
static defaultState() {
return {
currentRoutine: 'initial',
isBehaving: true,
};
}
static describeParams() {
return {
routines: {
type: 'routines',
label: 'Routines',
},
};
}
static describeState() {
return {
isBehaving: {
type: 'bool',
label: 'Is behaving',
},
currentRoutine: {
type: 'string',
label: 'Current routine',
options: (entity) => (
Object.keys(entity.traitInstance('behaved').params.routines)
.reduce((r, key) => ({...r, [key]: key}), {})
),
},
};
}
static type() {
return 'behaved';
}
constructor(entity, params, state) {
super(entity, params, state);
this._context = new Context(
{
entity: [this.entity, 'entity'],
},
latus,
);
this._currentRoutine = undefined;
this._routines = mapValues(
this.params.routines,
(routine) => new Actions(compile(routine, latus)),
);
this.updateCurrentRoutine(this.state.currentRoutine);
}
destroy() {
this._context.destroy();
this._currentRoutine = undefined;
this._routines = undefined;
}
get context() {
return this._context;
}
updateCurrentRoutine(currentRoutine) {
this._currentRoutine = this._routines[currentRoutine];
}
listeners() {
return {
currentRoutineChanged: (old, currentRoutine) => {
this.updateCurrentRoutine(currentRoutine);
},
isDyingChanged: (_, isDying) => {
this.entity.isBehaving = !isDying;
},
};
}
tick(elapsed) {
if (AVOCADO_SERVER) {
if (this._currentRoutine && this.entity.isBehaving) {
this._currentRoutine.tick(this._context, elapsed);
}
}
}
};

View File

@ -0,0 +1,5 @@
import Behaved from './behaved';
export default (latus) => ({
Behaved: Behaved(latus),
});

View File

@ -0,0 +1,8 @@
// Whilst the configuration object can be modified here, the recommended way of making
// changes is via the presets' options or Neutrino's API in `.neutrinorc.js` instead.
// Neutrino's inspect feature can be used to view/export the generated configuration.
const neutrino = require('neutrino');
const configOfConfigs = require(`${__dirname}/.neutrinorc`);
const configs = Array.isArray(configOfConfigs) ? configOfConfigs : [configOfConfigs];
module.exports = configs.map((config) => neutrino(config).webpack());

7748
packages/behavior/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
module.exports = require('../../config/.eslintrc');

5
packages/core/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
**/*.js
**/*.map
!/.*
!/webpack.config.js
!src/**/*.js

View File

@ -0,0 +1 @@
module.exports = require('../../config/.neutrinorc');

View File

@ -0,0 +1,39 @@
{
"name": "@avocado/core",
"version": "2.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"scripts": {
"build": "NODE_PATH=./node_modules webpack --mode production",
"clean": "rm -rf yarn.lock node_modules && yarn",
"dev": "NODE_PATH=./node_modules webpack --mode development",
"forcepub": "npm unpublish --force $(node -e 'const {name, version} = require(`./package.json`); process.stdout.write(`${name}@${version}`)') && npm publish",
"link": "node -e \"Object.keys(require('./package.json').dependencies).filter((m) => 0 === m.indexOf('@latus/')).forEach((m) => require('child_process').spawn('yarn', ['link', m]));\"",
"lint": "NODE_PATH=./node_modules eslint --format codeframe --ext mjs,js .",
"test": "NODE_PATH=./node_modules mocha --config ../../config/.mocharc.js",
"unlink": "node -e \"Object.keys(require('./package.json').dependencies).filter((m) => 0 === m.indexOf('@latus/')).forEach((m) => require('child_process').spawn('yarn', ['unlink', m]));\" && yarn install --force",
"watch": "NODE_PATH=./node_modules webpack --watch --mode development"
},
"files": [
"index.js",
"index.js.map"
],
"dependencies": {
"debug": "4.3.1"
},
"devDependencies": {
"@neutrinojs/airbnb-base": "^9.4.0",
"@neutrinojs/copy": "9.4.0",
"@neutrinojs/mocha": "^9.4.0",
"@neutrinojs/react": "^9.4.0",
"chai": "4.2.0",
"eslint": "^7",
"eslint-import-resolver-webpack": "0.13.0",
"mocha": "^8",
"neutrino": "^9.4.0",
"webpack": "^4",
"webpack-cli": "^3",
"webpack-node-externals": "2.5.2"
}
}

View File

@ -0,0 +1,21 @@
export default (holder, fn, args) => {
if (holder || args.length > 5) {
return fn.apply(holder, args);
}
if (0 === args.length) {
return fn();
}
if (1 === args.length) {
return fn(args[0]);
}
if (2 === args.length) {
return fn(args[0], args[1]);
}
if (3 === args.length) {
return fn(args[0], args[1], args[2]);
}
if (4 === args.length) {
return fn(args[0], args[1], args[2], args[3]);
}
return fn(args[0], args[1], args[2], args[3], args[4]);
};

View File

@ -0,0 +1,21 @@
export {
default as fastApply,
} from './fast-apply';
export {
mergeDiff,
mergeDiffArray,
mergeDiffObject,
mergeDiffPrimitive,
} from './merge-diff';
export {
default as Property,
} from './property';
export {
default as TickingPromise,
} from './ticking-promise';
export {
default as virtualize,
} from './virtualize';
export {
default as virtualizeStatic,
} from './virtualize-static';

View File

@ -0,0 +1,48 @@
export function mergeDiffArray(pristine, current) {
if (!Array.isArray(pristine)) {
return current;
}
if (pristine.length !== current.length) {
return current;
}
for (let i = 0; i < pristine.length; i++) {
if (JSON.stringify(current[i]) !== JSON.stringify(pristine[i])) {
return current;
}
}
return undefined;
}
export function mergeDiffObject(pristine, current) {
if ('object' !== typeof pristine) {
return current;
}
const diff = {};
const keys = Object.keys(current);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
// eslint-disable-next-line no-use-before-define
const value = mergeDiff(pristine[key], current[key]);
if (undefined !== value) {
diff[i] = value;
}
}
if (0 === Object.keys(diff).length) {
return undefined;
}
return diff;
}
export function mergeDiffPrimitive(pristine, current) {
return pristine !== current ? current : undefined;
}
export function mergeDiff(pristine, current) {
if (Array.isArray(current)) {
return mergeDiffArray(pristine, current);
}
if ('object' === typeof current) {
return mergeDiffObject(pristine, current);
}
return mergeDiffPrimitive(pristine, current);
}

View File

@ -0,0 +1,160 @@
/* eslint-disable func-names, no-new-func */
/**
* This Mixin contains a lot of nasty looking unrolls and string functions.
* Trying to optimize this very hot spot!
*/
export default function PropertyMixin(key, meta = {}) {
// Param check.
if (!meta || 'object' !== typeof meta) {
throw new TypeError(
`Expected meta for Property(${
key
}) to be object. ${
JSON.stringify(meta)
} given.`,
);
}
return (Superclass) => {
// Sanity check.
if (Superclass.prototype[key]) {
throw new SyntaxError(`redeclaration of Avocado property ${key}`);
}
// Handle defaults.
let metaDefault;
if (null === meta.default) {
metaDefault = null;
}
else if (undefined === meta.default) {
metaDefault = undefined;
}
else {
metaDefault = JSON.parse(JSON.stringify(meta.default));
}
const transformedKey = `$$avocado_property_${key}`;
class Property extends Superclass {
static get propertyList() {
const list = (super.propertyList ? super.propertyList : {});
return {...list, [key]: meta};
}
constructor(...args) {
super(...args);
// Initialize?
if (meta.initialize) {
meta.initialize.call(this);
}
// Set default.
else if (undefined !== metaDefault) {
this[transformedKey] = metaDefault;
}
}
}
// Getter.
const getter = meta.get ? meta.get : new Function(`return this.${transformedKey};`);
// Setter.
let setter;
// Helper to define assigner.
function defineAssigner() {
const assignMethod = `${transformedKey}$assign`;
Object.defineProperty(Property.prototype, assignMethod, {
value: meta.set,
});
return assignMethod;
}
// Tracking?
if (meta.track) {
// Define emitter.
const emitter = meta.emit ? meta.emit : function (...args) {
if (this.emit) {
this.emit(...args);
}
};
const emitMethod = `${transformedKey}$emit`;
Object.defineProperty(Property.prototype, emitMethod, {
value: emitter,
});
// Compare?
if (meta.eq) {
// Define comparator.
const comparator = meta.eq ? meta.eq : function (l, r) {
return l === r;
};
const compareMethod = `${transformedKey}$compare`;
Object.defineProperty(Property.prototype, compareMethod, {
value: comparator,
});
// Assign?
if (meta.set) {
// Define assigner.
const assignMethod = defineAssigner();
// Set with assigner, comparator, and emitter.
setter = new Function('value', `
const old = this.${key};
this.${assignMethod}(value);
if (!this.${compareMethod}(old, value)) {
this.${emitMethod}('${key}Changed', old, value);
}
`);
}
else {
// Set with comparator and emitter.
setter = new Function('value', `
const old = this.${key};
this.${transformedKey} = value;
if (!this.${compareMethod}(old, value)) {
this.${emitMethod}('${key}Changed', old, value);
}
`);
}
}
// No compare.
else if (meta.set) {
// Assign? Define assigner.
const assignMethod = defineAssigner();
// Set with assigner and emitter.
setter = new Function('value', `
const old = this.${key};
this.${assignMethod}(value);
if (old !== value) {
this.${emitMethod}('${key}Changed', old, value);
}
`);
}
else {
// Set with emitter.
setter = new Function('value', `
const old = this.${key};
this.${transformedKey} = value;
if (old !== value) {
this.${emitMethod}('${key}Changed', old, value);
}
`);
}
}
// No tracking?
else if (meta.set) {
// Assign? Define assigner.
const assignMethod = defineAssigner();
// Set with assigner.
setter = new Function('value', `
this.${assignMethod}(value);
`);
}
else {
// Set raw.
setter = new Function('value', `
this.${transformedKey} = value;
`);
}
Object.defineProperty(Property.prototype, key, {
configurable: true,
enumerable: true,
get: getter,
set: setter,
});
return Property;
};
}

View File

@ -0,0 +1,50 @@
export default class TickingPromise extends Promise {
constructor(executor, ticker) {
let _reject;
let _resolve;
super((resolve, reject) => {
_reject = reject;
_resolve = resolve;
if (executor) {
executor(resolve, reject);
}
});
this.executor = executor;
this.reject = _reject;
this.resolve = _resolve;
this.ticker = ticker;
}
static all(promises) {
const tickingPromises = [];
for (let i = 0; i < promises.length; i++) {
const promise = promises[i];
if (promise instanceof TickingPromise) {
tickingPromises.push(promise);
// After resolution, stop ticking the promise.
promise.then(() => {
tickingPromises.splice(tickingPromises.indexOf(promise), 1);
});
}
}
if (0 === tickingPromises.length) {
return super.all(promises);
}
return new TickingPromise(
(resolve) => {
resolve(Promise.all(promises));
},
(elapsed) => {
for (let i = 0; i < tickingPromises.length; i++) {
tickingPromises[i].tick(elapsed);
}
},
);
}
tick(elapsed) {
this.ticker(elapsed, this.resolve, this.reject);
}
}

View File

@ -0,0 +1,13 @@
export default function virtualizeStatic(fields) {
return (Superclass) => {
class Virtualized extends Superclass {}
fields.forEach((field) => {
// eslint-disable-next-line func-names
Virtualized[field] = function () {
const {name} = Virtualized.prototype.constructor;
throw new ReferenceError(`'${name}' has undefined pure virtual static method '${field}'`);
};
});
return Virtualized;
};
}

View File

@ -0,0 +1,13 @@
export default function virtualize(fields) {
return (Superclass) => {
class Virtualized extends Superclass {}
fields.forEach((field) => {
// eslint-disable-next-line func-names
Virtualized.prototype[field] = function () {
const {name} = Object.getPrototypeOf(this).constructor;
throw new ReferenceError(`'${name}' has undefined pure virtual method '${field}'`);
};
});
return Virtualized;
};
}

View File

@ -0,0 +1,8 @@
// Whilst the configuration object can be modified here, the recommended way of making
// changes is via the presets' options or Neutrino's API in `.neutrinorc.js` instead.
// Neutrino's inspect feature can be used to view/export the generated configuration.
const neutrino = require('neutrino');
const configOfConfigs = require(`${__dirname}/.neutrinorc`);
const configs = Array.isArray(configOfConfigs) ? configOfConfigs : [configOfConfigs];
module.exports = configs.map((config) => neutrino(config).webpack());

6168
packages/core/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
module.exports = require('../../config/.eslintrc');

5
packages/entity/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
**/*.js
**/*.map
!/.*
!/webpack.config.js
!src/**/*.js

View File

@ -0,0 +1 @@
module.exports = require('../../config/.neutrinorc');

View File

@ -0,0 +1,49 @@
{
"name": "@avocado/entity",
"version": "2.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"scripts": {
"build": "NODE_PATH=./node_modules webpack --mode production",
"clean": "rm -rf yarn.lock node_modules && yarn",
"dev": "NODE_PATH=./node_modules webpack --mode development",
"forcepub": "npm unpublish --force $(node -e 'const {name, version} = require(`./package.json`); process.stdout.write(`${name}@${version}`)') && npm publish",
"link": "node -e \"Object.keys(require('./package.json').dependencies).filter((m) => 0 === m.indexOf('@latus/')).forEach((m) => require('child_process').spawn('yarn', ['link', m]));\"",
"lint": "NODE_PATH=./node_modules eslint --format codeframe --ext mjs,js .",
"test": "NODE_PATH=./node_modules mocha --config ../../config/.mocharc.js",
"unlink": "node -e \"Object.keys(require('./package.json').dependencies).filter((m) => 0 === m.indexOf('@latus/')).forEach((m) => require('child_process').spawn('yarn', ['unlink', m]));\" && yarn install --force",
"watch": "NODE_PATH=./node_modules webpack --watch --mode development"
},
"files": [
"index.js",
"index.js.map"
],
"dependencies": {
"@avocado/behavior": "^2.0.0",
"@avocado/core": "2.0.0",
"@avocado/math": "2.0.0",
"@avocado/resource": "2.0.0",
"@avocado/s13n": "^2.0.0",
"@avocado/timing": "^2.0.0",
"@latus/core": "^2.0.0",
"@latus/socket": "^2.0.0",
"debug": "4.3.1",
"deepmerge": "^4.2.2",
"lodash.without": "^4.4.0"
},
"devDependencies": {
"@neutrinojs/airbnb-base": "^9.4.0",
"@neutrinojs/copy": "9.4.0",
"@neutrinojs/mocha": "^9.4.0",
"@neutrinojs/react": "^9.4.0",
"chai": "4.2.0",
"eslint": "^7",
"eslint-import-resolver-webpack": "0.13.0",
"mocha": "^8",
"neutrino": "^9.4.0",
"webpack": "^4",
"webpack-cli": "^3",
"webpack-node-externals": "2.5.2"
}
}

View File

@ -0,0 +1,72 @@
const blacklistedAccessorKeys = [
'state',
];
// This really seems like a whole lot of complicated nonsense, but it's an
// unfortunate consequence of V8 (and maybe others) not optimizing mutable
// accessors in fast hidden classes.
const traitAccessorForPropertyMap = {};
function traitAccessorForProperty(type, property) {
if (!(type in traitAccessorForPropertyMap)) {
traitAccessorForPropertyMap[type] = {};
}
if (!(property in traitAccessorForPropertyMap[type])) {
traitAccessorForPropertyMap[type][property] = {
// eslint-disable-next-line no-new-func
get: new Function('', `
return this._traits['${type}']['${property}'];
`),
// eslint-disable-next-line no-new-func
set: new Function('value', `
this._traits['${type}']['${property}'] = value;
`),
};
}
return traitAccessorForPropertyMap[type][property];
}
export function defineTraitAccessors(from, to, instance) {
const type = instance.constructor.type();
do {
// eslint-disable-next-line no-loop-func
Object.getOwnPropertyNames(from).forEach((accessorKey) => {
if (-1 !== blacklistedAccessorKeys.indexOf(accessorKey)) {
return;
}
const descriptor = Object.getOwnPropertyDescriptor(from, accessorKey);
// Make sure it's actually an accessor.
if (!descriptor.get && !descriptor.set) {
return;
}
const accessor = traitAccessorForProperty(type, accessorKey);
if (descriptor.get) {
descriptor.get = accessor.get;
}
if (descriptor.set) {
descriptor.set = accessor.set;
}
Object.defineProperty(to, accessorKey, descriptor);
});
// eslint-disable-next-line no-cond-assign, no-param-reassign
} while (Object.prototype !== (from = Object.getPrototypeOf(from)));
}
export function enumerateTraitAccessorKeys(prototype) {
const keys = [];
do {
// eslint-disable-next-line no-loop-func
Object.getOwnPropertyNames(prototype).forEach((accessorKey) => {
if (-1 !== blacklistedAccessorKeys.indexOf(accessorKey)) {
return;
}
const descriptor = Object.getOwnPropertyDescriptor(prototype, accessorKey);
// Make sure it's actually an accessor.
if (!descriptor.get && !descriptor.set) {
return;
}
keys.push(accessorKey);
});
// eslint-disable-next-line no-cond-assign, no-param-reassign
} while (Object.prototype !== (prototype = Object.getPrototypeOf(prototype)));
return keys;
}

View File

@ -0,0 +1,377 @@
import D from 'debug';
import merge from 'deepmerge';
import without from 'lodash.without';
import {fastApply} from '@avocado/core';
import {Synchronized} from '@avocado/s13n';
import Resource from '@avocado/resource';
import {
compose,
EventEmitter,
mergeDiff,
} from '@latus/core';
import {
defineTraitAccessors,
enumerateTraitAccessorKeys,
} from './accessors';
import {traitFromType} from './trait';
const debug = D('@avocado:entity:traits');
const decorate = compose(
EventEmitter,
Synchronized,
);
let numericUid = AVOCADO_SERVER ? 1 : 1000000000;
export default (latus) => class Entity extends decorate(Resource) {
constructor(json, jsonext) {
super();
this._fastDirtyCheck = true;
this._hooks = {};
this._hydrationPromise = undefined;
this._json = Entity.jsonWithDefaults(json);
this._tickingPromisesTickers = [];
this._traits = {};
this._traitsFlat = [];
this._traitTickers = [];
this._traitRenderTickers = [];
this._traitsAcceptingPackets = [];
this.once('destroyed', () => {
this.removeAllTraits();
});
// Bind to prevent lookup overhead.
this.tick = this.tick.bind(this);
// Fast props.
this.numericUid = numericUid++;
this.instanceUuid = this.numericUid;
this.position = [0, 0];
this.room = null;
this.visibleAabb = [0, 0, 0, 0];
// Fast path for instance.
if ('undefined' !== typeof json) {
this.fromJSON(merge(json, jsonext));
}
}
acceptPacket(packet) {
if ('EntityUpdateTraitPacket' === packet.constructor.name) {
const {traits} = packet.data;
for (let i = 0; i < traits.length; i++) {
const {type, packets} = traits[i];
for (let j = 0; j < packets.length; j++) {
this._traits[type].acceptPacket(packets[j]);
}
}
}
}
addTickingPromise(tickingPromise) {
const ticker = tickingPromise.tick.bind(tickingPromise);
this._tickingPromisesTickers.push(ticker);
return tickingPromise.then(() => {
const index = this._tickingPromisesTickers.indexOf(ticker);
if (-1 !== index) {
this._tickingPromisesTickers.splice(index, 1);
}
});
}
addTrait(type, json = {}) {
if (this.is(type)) {
debug(`Tried to add trait "${type}" when it already exists!`);
return;
}
const {type: Trait} = traitFromType(latus);
if (!Trait) {
debug(`Tried to add trait "${type}" which isn't registered!`);
return;
}
// Ensure dependencies.
const dependencies = Trait.dependencies();
const allTypes = this.allTraitTypes();
const lacking = without(dependencies, ...allTypes);
if (lacking.length > 0) {
debug(
`Tried to add trait "${type}" but lack one or more dependents: "${
lacking.join('", "')
}"!`,
);
// return;
}
// Instantiate.
const {params, state} = json;
const instance = new Trait(this, params, state);
// Proxy properties.
defineTraitAccessors(Trait.prototype, this, instance);
// Attach listeners.
const listeners = Object.entries(instance.memoizedListeners());
for (let i = 0; i < listeners.length; i++) {
const [event, listener] = listeners[i];
this.on(event, listener);
}
// Proxy methods.
const methods = Object.entries(instance.methods());
for (let i = 0; i < methods.length; i++) {
const [key, method] = methods[i];
this[key] = method;
}
// Register hook listeners.
const hooks = Object.entries(instance.hooks());
for (let i = 0; i < hooks.length; i++) {
const [key, fn] = hooks[i];
this._hooks[key] = this._hooks[key] || [];
this._hooks[key].push({
fn,
type: Trait.type(),
});
}
// Track trait.
this._traits[type] = instance;
this._traitsFlat.push(instance);
if ('tick' in instance) {
this._traitTickers.push(instance.tick);
}
if ('renderTick' in instance) {
this._traitRenderTickers.push(instance.renderTick);
}
if ('acceptPacket' in instance) {
this._traitsAcceptingPackets.push(instance);
}
this.emit('traitAdded', type, instance);
}
addTraits(traits) {
const entries = Object.entries(traits);
for (let i = 0; i < entries.length; i++) {
const [type, trait] = entries[i];
this.addTrait(type, trait);
}
}
allTraitInstances() {
return this._traits;
}
allTraitTypes() {
return Object.keys(this._traits);
}
cleanPackets() {
if (!this._fastDirtyCheck) {
return;
}
for (let i = 0; i < this._traitsFlat.length; i++) {
this._traitsFlat[i].cleanPackets();
}
this._fastDirtyCheck = false;
}
fromJSON(json) {
super.fromJSON(json);
if (json.instanceUuid) {
this.instanceUuid = json.instanceUuid;
}
this.addTraits(json.traits);
return this;
}
hydrate() {
if (!this._hydrationPromise) {
const promises = [];
for (let i = 0; i < this._traitsFlat.length; i++) {
promises.push(this._traitsFlat[i].hydrate());
}
this._hydrationPromise = Promise.all(promises);
this._hydrationPromise.then(() => {
this.tick(0);
});
}
return this._hydrationPromise;
}
invokeHook(hook, ...args) {
const results = {};
if (!(hook in this._hooks)) {
return results;
}
const values = Object.values(this._hooks[hook]);
for (let i = 0; i < values.length; i++) {
const {fn, type} = values[i];
results[type] = fastApply(null, fn, args);
}
return results;
}
invokeHookFlat(hook, ...args) {
return Object.values(this.invokeHook(hook, ...args));
}
is(type) {
return type in this._traits;
}
static jsonWithDefaults(json) {
if ('undefined' === typeof json) {
return undefined;
}
const pristine = JSON.parse(JSON.stringify(json));
const traits = Object.entries(json.traits);
for (let i = 0; i < traits.length; i++) {
const [type, trait] = traits[i];
const {type: Trait} = traitFromType(latus);
if (Trait) {
pristine.traits[type] = merge(Trait.defaultJSON(), trait);
}
}
return pristine;
}
mergeDiff(json) {
if (!this.uri) {
return json;
}
return mergeDiff(this._json, json);
}
packets(informed) {
const packets = [];
const updates = [];
const traits = Object.entries(this._traits);
for (let i = 0; i < traits.length; i++) {
const [type, trait] = traits[i];
let traitPackets = trait.packets(informed);
if (traitPackets) {
if (!Array.isArray(traitPackets)) {
traitPackets = [traitPackets];
}
if (traitPackets.length > 0) {
updates.push({
type,
packets: traitPackets,
});
}
}
}
if (updates.length > 0) {
packets.push(['EntityUpdateTraitPacket', {traits: updates}]);
}
return packets;
}
renderTick(elapsed) {
for (let i = 0; i < this._traitRenderTickers.length; i++) {
this._traitRenderTickers[i](elapsed);
}
}
removeAllTraits() {
const types = this.allTraitTypes();
this.removeTraits(types);
}
removeTrait(type) {
if (!this.is(type)) {
debug(`Tried to remove trait "${type}" when it doesn't exist!`);
return;
}
// Destroy instance.
const instance = this._traits[type];
instance.destroy();
// Remove methods, hooks, and properties.
const methods = instance.methods();
const keys = Object.keys(methods);
for (let i = 0; i < keys.length; i++) {
delete this[keys[i]];
}
const hooks = Object.keys(instance.hooks());
for (let i = 0; i < hooks.length; i++) {
const hook = hooks[i];
const implementation = this._hooks[hook].find(({type: hookType}) => hookType === type);
this._hooks[hook].splice(this._hooks[hook].indexOf(implementation), 1);
}
const {type: Trait} = traitFromType(latus);
const properties = enumerateTraitAccessorKeys(Trait.prototype);
for (let i = 0; i < properties.length; ++i) {
const property = properties[i];
delete this[property];
}
// Remove all event listeners.
const listeners = Object.entries(instance.memoizedListeners());
for (let i = 0; i < listeners.length; i++) {
const [event, listener] = listeners[i];
this.off(event, listener);
}
instance._memoizedListeners = {};
// Remove instance.
delete this._traits[type];
this._traitsFlat.splice(this._traitsFlat.indexOf(instance), 1);
if ('tick' in instance) {
this._traitTickers.splice(this._traitTickers.indexOf(instance.tick), 1);
}
if ('renderTick' in instance) {
this._traitRenderTickers.splice(this._traitRenderTickers.indexOf(instance.renderTick), 1);
}
const acceptPacketIndex = this._traitsAcceptingPackets.indexOf(instance);
if (-1 !== acceptPacketIndex) {
this._traitsAcceptingPackets.splice(acceptPacketIndex, 1);
}
// Unloop.
instance.entity = undefined;
}
removeTraits(types) {
types.forEach((type) => this.removeTrait(type));
}
synchronizationId() {
return this.instanceUuid;
}
tick(elapsed) {
for (let i = 0; i < this._traitTickers.length; i++) {
this._traitTickers[i](elapsed);
}
for (let i = 0; i < this._tickingPromisesTickers.length; i++) {
this._tickingPromisesTickers[i](elapsed);
}
}
toNetwork(informed) {
const json = {
traits: {},
};
const traits = Object.entries(this._traits);
for (let i = 0; i < traits.length; i++) {
const [type, trait] = traits[i];
json.traits[type] = trait.toNetwork(informed);
}
return {
...super.toJSON(),
instanceUuid: this.instanceUuid,
...this.mergeDiff(json),
};
}
toJSON() {
const json = {};
const traits = Object.entries(this._traits);
for (let i = 0; i < traits.length; i++) {
const [type, trait] = traits[i];
json[type] = trait.toJSON();
}
return {
...super.toJSON(),
instanceUuid: this.instanceUuid,
traits: json,
};
}
traitInstance(type) {
return this._traits[type];
}
};

View File

@ -0,0 +1,24 @@
import {gather} from '@latus/core';
import Entity from './entity';
import Packets from './packets';
import Traits from './traits';
export default {
hooks: {
'@avocado/entity/traits': Traits,
'@avocado/s13n/synchronized': (latus) => ({
Entity: Entity(latus),
}),
'@latus/core/starting': (latus) => {
// eslint-disable-next-line no-param-reassign
latus.config['%traits'] = gather(
latus,
'@avocado/entity/traits',
'id',
'type',
);
},
'@latus/socket/packets': Packets,
},
};

View File

@ -0,0 +1,44 @@
import {SynchronizedUpdatePacket} from '@avocado/s13n';
import {packetFromName} from '@latus/socket';
import {traitFromId, traitFromType} from '../trait';
export default (latus) => class EntityUpdateTraitPacket extends SynchronizedUpdatePacket {
static pack(packet) {
const {BundlePacket} = packetFromName(latus);
const data = packet.data[1];
const fromType = traitFromType(latus);
for (let i = 0; i < data.traits.length; i++) {
const Trait = fromType[data.traits[i].type];
data.traits[i].type = Trait.id;
data.traits[i].packets = BundlePacket.packPacket(new BundlePacket(data.traits[i].packets));
}
return super.pack(packet);
}
static get synchronizationSchema() {
return {
traits: [
{
type: 'uint8',
packets: 'buffer',
},
],
};
}
static unpack(packet) {
const {BundlePacket} = packetFromName(latus);
const unpacked = super.unpack(packet);
const {data} = unpacked;
const fromId = traitFromId(latus);
for (let i = 0; i < data.traits.length; i++) {
const {default: Trait} = fromId[data.traits[i].type];
data.traits[i].type = Trait.type();
data.traits[i].packets = BundlePacket.unpack(data.traits[i].packets).data;
}
return unpacked;
}
};

View File

@ -0,0 +1,5 @@
import EntityUpdateTrait from './entity-update-trait';
export default (latus) => ({
EntityUpdateTrait: EntityUpdateTrait(latus),
});

View File

@ -0,0 +1,187 @@
import {Property} from '@avocado/core';
import {Synchronized} from '@avocado/s13n';
import {compose, Class} from '@latus/core';
const decorate = compose(
Synchronized,
);
export const traitFromId = (latus) => latus.config['%traits'].fromId;
export const traitFromType = (latus) => latus.config['%traits'].fromType;
export default class Trait extends decorate(Class) {
constructor(entity, params, state) {
super();
this.entity = entity;
const ctor = this.constructor;
this._fastDirtyCheck = true;
this._memoizedListeners = undefined;
this.params = ctor.defaultParamsWith(params);
this.state = ctor.defaultStateWith(state);
this.previousState = JSON.parse(JSON.stringify(this.state));
if (this.tick) {
this.tick = this.tick.bind(this);
}
if (this.renderTick) {
this.renderTick = this.renderTick.bind(this);
}
}
// eslint-disable-next-line class-methods-use-this, no-unused-vars
acceptPacket(packet) {}
cleanPackets() {
if (!this._fastDirtyCheck) {
return;
}
const keys = Object.keys(this.state);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
this.previousState[key] = this.state[key];
}
this._fastDirtyCheck = false;
}
static behaviorTypes() {
return {};
}
static defaultJSON() {
return {
params: this.defaultParams(),
state: this.defaultState(),
};
}
static defaultParams() {
return {};
}
static defaultParamsWith(defaults) {
return {...this.defaultParams(), ...defaults};
}
static defaultState() {
return {};
}
static defaultStateWith(defaults) {
return {...this.defaultState(), ...defaults};
}
static dependencies() {
return [];
}
static describeParams() {
return {};
}
static describeState() {
return {};
}
// eslint-disable-next-line class-methods-use-this
destroy() {}
// eslint-disable-next-line class-methods-use-this
hooks() {
return {};
}
// eslint-disable-next-line class-methods-use-this, no-empty-function
async hydrate() {}
get isDirty() {
const keys = Object.keys(this.state);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (this.state[key] !== this.previousState[key]) {
return true;
}
}
return false;
}
// eslint-disable-next-line class-methods-use-this
listeners() {
return {};
}
memoizedListeners() {
if (!this._memoizedListeners) {
this._memoizedListeners = this.listeners();
}
return this._memoizedListeners;
}
// eslint-disable-next-line class-methods-use-this
methods() {
return {};
}
// eslint-disable-next-line class-methods-use-this, no-unused-vars
packets(informed) {
return [];
}
// eslint-disable-next-line class-methods-use-this
packetsAreIdempotent() {
return false;
}
setDirty() {
this._fastDirtyCheck = true;
this.entity._fastDirtyCheck = true;
}
stateDifferences() {
const differences = {};
const keys = Object.keys(this.state);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (this.state[key] !== this.previousState[key]) {
differences[key] = {
old: this.previousState[key],
value: this.state[key],
};
}
}
return differences;
}
toJSON() {
return {
params: this.params,
state: this.state,
};
}
}
export function StateProperty(key, meta = {}) {
const transformedKey = `$$avocado_state_property_${key}`;
return (Superclass) => {
/* eslint-disable func-names, no-new-func, no-param-reassign */
meta.emit = meta.emit || function (...args) {
this.entity.emit(...args);
};
meta.initialize = meta.initialize || function () {
this[transformedKey] = this.state[key];
};
meta.get = meta.get || new Function(`
return this.${transformedKey};
`);
meta.set = meta.set || new Function('value', `
if (value !== this.${transformedKey}) {
this.setDirty();
}
this.${transformedKey} = value;
this.state['${key}'] = value;
`);
/* eslint-enable func-names, no-new-func, no-param-reassign */
return Property(key, meta)(Superclass);
};
}

View File

@ -0,0 +1,207 @@
import {
Actions,
buildCondition,
buildInvoke,
buildExpression,
compile,
Context,
} from '@avocado/behavior';
import {compose} from '@avocado/core';
import Trait, {StateProperty} from '../trait';
const decorate = compose(
StateProperty('isDying', {
track: true,
}),
StateProperty('life', {
track: true,
}),
StateProperty('maxLife', {
track: true,
}),
);
export default class Alive extends decorate(Trait) {
static behaviorTypes() {
return {
deathSound: {
type: 'string',
label: 'Death sound',
},
forceDeath: {
type: 'void',
label: 'Force death',
},
};
}
static defaultParams() {
const playDeathSound = buildInvoke(['entity', 'playSound'], [
buildExpression(['entity', 'deathSound']),
]);
const squeeze = buildInvoke(['entity', 'transition'], [
{
opacity: 0,
visibleScaleX: 0.3,
visibleScaleY: 3,
},
0.2,
]);
const isLifeGone = buildCondition('<=', [
buildExpression(['entity', 'life']),
0,
]);
return {
deathActions: {
type: 'expressions',
expressions: [
playDeathSound,
squeeze,
],
},
deathCondition: isLifeGone,
deathSound: 'deathSound',
};
}
static defaultState() {
return {
isDying: false,
life: 100,
maxLife: 100,
};
}
static describeParams() {
return {
deathSound: {
type: 'string',
label: 'Death sound',
},
deathActions: {
type: 'expressions',
label: 'Death actions',
},
deathCondition: {
type: 'condition',
label: 'Death condition',
},
};
}
static describeState() {
return {
isDying: {
type: 'bool',
label: 'Is dying',
},
life: {
type: 'number',
label: 'Life points',
},
maxLife: {
type: 'number',
label: 'Maximum life points',
},
};
}
static type() {
return 'alive';
}
constructor(entity, params, state) {
super(entity, params, state);
this._context = new Context({
entity: [this.entity, 'entity'],
});
this._deathActions = new Actions(compile(this.params.deathActions));
this._deathCondition = compile(this.params.deathCondition);
}
destroy() {
this._context.destroy();
}
acceptPacket(packet) {
switch (packet.constructor.name) {
case 'DiedPacket':
this.entity.forceDeath();
break;
case 'TraitUpdateAlivePacket':
this.entity.life = packet.data.life;
this.entity.maxLife = packet.data.maxLife;
break;
default:
}
}
get deathSound() {
return this.params.deathSound;
}
packets() {
const packets = [];
const {isDying, life, maxLife} = this.stateDifferences();
if (life || maxLife) {
packets.push([
'TraitUpdateAlivePacket',
{
life: this.state.life,
maxLife: this.state.maxLife,
},
]);
}
if (isDying) {
packets.push(['DiedPacket']);
}
return packets;
}
listeners() {
return {
tookHarm: (harm) => {
if (harm.isHarm) {
this.entity.life -= harm.amount;
}
else {
this.entity.life += harm.amount;
}
// Clamp health between 0 and max.
this.entity.life = Math.min(
Math.max(0, this.entity.life),
this.entity.maxLife,
);
},
};
}
methods() {
return {
forceDeath: async () => {
if (this.entity.isDying) {
return;
}
this.entity.isDying = true;
await this.entity.addTickingPromise(this._deathActions.tickingPromise(this._context));
await Promise.all(this.entity.invokeHookFlat('died'));
this.entity.destroy();
},
};
}
tick() {
if (AVOCADO_SERVER) {
if (!this.entity.isDying && this._deathCondition(this._context)) {
this.entity.forceDeath();
}
}
}
}

View File

@ -0,0 +1,100 @@
import {compose} from '@avocado/core';
import {Vector} from '@avocado/math';
import Trait, {StateProperty} from '../trait';
const decorate = compose(
StateProperty('direction', {
label: 'Direction',
track: true,
type: 'number',
}),
);
export default class Directional extends decorate(Trait) {
static defaultParams() {
return {
directionCount: 1,
trackMovement: true,
};
}
static defaultState() {
return {
direction: 0,
};
}
static describeParams() {
return {
directionCount: {
type: 'number',
label: '# of directions',
options: {
1: 1,
4: 4,
},
},
trackMovement: {
type: 'bool',
label: 'Track movement',
},
};
}
static describeState() {
return {
direction: {
type: 'number',
label: 'Direction',
options: {
0: 'Up',
1: 'Right',
2: 'Down',
3: 'Left',
},
},
};
}
static type() {
return 'directional';
}
constructor(entity, params, state) {
super(entity, params, state);
this.directionCount = this.params.directionCount;
}
acceptPacket(packet) {
if ('TraitUpdateDirectionalDirectionPacket' === packet.constructor.name) {
this.entity.direction = packet.data;
}
}
packets() {
const {direction} = this.stateDifferences();
if (direction) {
return [
'TraitUpdateDirectionalDirectionPacket',
direction.value,
];
}
return undefined;
}
listeners() {
const listeners = {};
if (this.params.trackMovement) {
listeners.movementRequest = (vector) => {
if (Vector.isZero(vector)) {
return;
}
this.entity.direction = Vector.toDirection(vector, this.directionCount);
};
}
return listeners;
}
}

View File

@ -0,0 +1,113 @@
import {compose, TickingPromise} from '@avocado/core';
import {TransitionResult} from '@avocado/timing';
import Trait, {StateProperty} from '../trait';
const decorate = compose(
StateProperty('name'),
StateProperty('isTicking'),
);
export default class Existent extends decorate(Trait) {
static behaviorTypes() {
return {
destroy: {
type: 'void',
label: 'Destroy',
},
destroyGently: {
type: 'void',
label: 'Kill? Then destroy',
},
transition: {
type: 'void',
label: 'Transition $1 for $2 seconds using $3.',
args: [
['props', {
type: 'object',
}],
['duration', {
type: 'number',
}],
['easing', {
type: 'string',
}],
],
},
};
}
static defaultState() {
return {
isTicking: true,
name: 'Untitled entity',
};
}
static describeState() {
return {
isTicking: {
type: 'bool',
label: 'Is ticking',
},
name: {
type: 'string',
label: 'Name',
},
};
}
static type() {
return 'existent';
}
constructor(entity, params, state) {
super(entity, params, state);
this._isDestroying = false;
this._isTicking = this.params.isTicking;
}
methods() {
return {
destroy: async () => {
if (this._isDestroying) {
return;
}
this._isDestroying = true;
this.entity.isTicking = false;
this.entity.emit('destroy');
this.entity.emit('destroyed');
},
destroyGently: () => {
if (this.entity.is('alive')) {
this.entity.forceDeath();
}
else {
this.entity.destroy();
}
},
transition: (props, duration, easing) => {
const result = new TransitionResult(
this.entity,
props,
duration,
easing,
);
return new TickingPromise(
(resolve) => {
resolve(result.promise);
},
(elapsed) => {
result.tick(elapsed);
},
);
},
};
}
}

View File

@ -0,0 +1,19 @@
import Alive from './alive';
import Directional from './directional';
import Existent from './existent';
import Listed from './listed';
import Mobile from './mobile';
import Perishable from './perishable';
import Positioned from './positioned';
import Spawner from './spawner';
export default (latus) => ({
Alive,
Directional,
Existent,
Listed,
Mobile,
Perishable,
Positioned,
Spawner: Spawner(latus),
});

View File

@ -0,0 +1,129 @@
import {Rectangle} from '@avocado/math';
import Trait from '../trait';
export default class Listed extends Trait {
static behaviorTypes() {
return {
detachFromList: {
type: 'void',
label: 'Detach from list.',
},
attachToList: {
type: 'void',
label: 'Attach to $1.',
args: [
['list', {
type: 'entity-list',
}],
],
},
};
}
static type() {
return 'listed';
}
constructor(entity, params, state) {
super(entity, params, state);
this.entity.list = null;
this.quadTreeAabb = [];
this.quadTreeNodes = [];
}
destroy() {
this.entity.detachFromList();
}
addQuadTreeNodes() {
const {list} = this.entity;
if (!list) {
return;
}
if (!this.entity.is('visible')) {
return;
}
const aabb = this.entity.visibleAabb;
if (Rectangle.isNull(aabb)) {
return;
}
// Expand the AABB so we don't have to update it every single tick.
const expandedAabb = Rectangle.expand(aabb, [32, 32]);
this.quadTreeAabb = expandedAabb;
const points = Rectangle.toPoints(expandedAabb);
this.quadTreeNodes = points.map((point) => [...point, this.entity, aabb]);
// Add points to quad tree.
const {quadTree} = list;
for (let i = 0; i < this.quadTreeNodes.length; i++) {
quadTree.add(this.quadTreeNodes[i]);
}
}
removeQuadTreeNodes() {
const {list} = this.entity;
if (!list) {
return;
}
if (this.quadTreeNodes.length > 0) {
const {quadTree} = list;
for (let i = 0; i < this.quadTreeNodes.length; i++) {
quadTree.remove(this.quadTreeNodes[i]);
}
this.quadTreeAabb = [];
this.quadTreeNodes = [];
}
}
resetQuadTreeNodes() {
if (AVOCADO_SERVER) {
const aabb = this.entity.visibleAabb;
if (
this.quadTreeAabb.length > 0
&& Rectangle.isInside(this.quadTreeAabb, aabb)
) {
return;
}
this.removeQuadTreeNodes();
this.addQuadTreeNodes();
}
}
listeners() {
return {
visibleAabbChanged: () => {
this.resetQuadTreeNodes();
},
traitAdded: () => {
this.resetQuadTreeNodes();
},
};
}
methods() {
return {
detachFromList: () => {
const {list} = this.entity;
if (!list) {
return;
}
this.removeQuadTreeNodes();
this.entity.list = null;
this.entity.emit('removedFromList', list);
},
attachToList: (list) => {
this.entity.list = list;
this.addQuadTreeNodes();
this.entity.emit('addedToList');
},
};
}
}

View File

@ -0,0 +1,142 @@
import {compose, TickingPromise} from '@avocado/core';
import {Vector} from '@avocado/math';
import Trait, {StateProperty} from '../trait';
const decorate = compose(
StateProperty('isMobile'),
StateProperty('speed'),
);
export default class Mobile extends decorate(Trait) {
static behaviorTypes() {
return {
moveFor: {
type: 'void',
label: 'Move toward $1 for $2 seconds.',
args: [
['movement', {
type: 'vector',
}],
['duration', {
type: 'number',
}],
],
},
applyMovement: {
type: 'void',
label: 'Apply movement of $1.',
args: [
['movement', {
type: 'vector',
}],
],
},
forceMovement: {
type: 'void',
label: 'Force movement of $1.',
args: [
['movement', {
type: 'vector',
}],
],
},
requestMovement: {
type: 'void',
label: 'Request movement of $1.',
args: [
['movement', {
type: 'vector',
}],
],
},
};
}
static defaultState() {
return {
isMobile: true,
speed: 0,
};
}
static describeState() {
return {
isMobile: {
type: 'bool',
label: 'Is mobile',
},
speed: {
type: 'number',
label: 'Speed',
},
};
}
static type() {
return 'mobile';
}
constructor(entity, params, state) {
super(entity, params, state);
this.appliedMovement = [0, 0];
}
methods() {
return {
moveFor: (vector, duration) => new TickingPromise(
() => {},
(elapsed, resolve) => {
// eslint-disable-next-line no-param-reassign
duration -= elapsed;
if (duration <= 0) {
resolve();
return;
}
this.entity.requestMovement(Vector.normalize(vector));
},
),
applyMovement: (vector) => {
this.appliedMovement = Vector.add(this.appliedMovement, vector);
},
forceMovement: (movement) => {
this.entity.x += movement[0];
this.entity.y += movement[1];
},
requestMovement: (vector) => {
if (!this.isMobile) {
return;
}
this.entity.applyMovement(Vector.scale(
Vector.normalize(vector),
this.speed,
));
this.entity.emit('movementRequest', this.appliedMovement);
},
};
}
tick(elapsed) {
if (Vector.isZero(this.appliedMovement)) {
return;
}
if (this.entity.is('physical')) {
this.entity.applyImpulse(this.appliedMovement);
}
else {
const appliedMovement = Vector.scale(
this.appliedMovement,
elapsed,
);
this.entity.forceMovement(appliedMovement);
}
this.appliedMovement = [0, 0];
}
}

View File

@ -0,0 +1,41 @@
import {compose} from '@avocado/core';
import Trait from '../trait';
const decorate = compose(
);
export default class Perishable extends decorate(Trait) {
static defaultParams() {
return {
ttl: 300,
};
}
static describeParams() {
return {
ttl: {
type: 'number',
label: 'Time-to-live in seconds',
},
};
}
static type() {
return 'perishable';
}
constructor(entity, params, state) {
super(entity, params, state);
this.ttl = this.params.ttl;
}
tick(elapsed) {
this.ttl -= elapsed;
if (this.ttl <= 0) {
this.entity.destroy();
}
}
}

View File

@ -0,0 +1,156 @@
import {compose, EventEmitter} from '@avocado/core';
import {Vector} from '@avocado/math';
import Trait from '../trait';
const decorate = compose(
EventEmitter,
Vector.Mixin('_position', 'x', 'y', {
track: true,
}),
Vector.Mixin('serverPosition', 'serverX', 'serverY', {
track: true,
}),
);
// < 16768 will pack into 1 short per axe and give +/- 0.25 precision.
export default class Positioned extends decorate(Trait) {
static behaviorTypes() {
return {
position: {
type: 'vector',
label: 'Position',
},
setPosition: {
type: 'void',
label: 'Set position to $1.',
args: [
['position', {
type: 'vector',
}],
],
},
};
}
static defaultState() {
return {
x: 0,
y: 0,
};
}
static describeState() {
return {
x: {
type: 'number',
label: 'X',
},
y: {
type: 'number',
label: 'Y',
},
};
}
static type() {
return 'positioned';
}
constructor(entity, params, state) {
super(entity, params, state);
this.on('_positionChanged', this.on_positionChanged, this);
const {x, y} = this.state;
this._position = [x, y];
this.entity.position[0] = x;
this.entity.position[1] = y;
if (AVOCADO_CLIENT) {
this.serverPosition = this._position;
this.serverPositionDirty = false;
this.on('serverPositionChanged', this.onServerPositionChanged, this);
}
}
destroy() {
this.off('_positionChanged', this.on_positionChanged);
if (AVOCADO_CLIENT) {
this.off('serverPositionChanged', this.onServerPositionChanged);
}
}
acceptPacket(packet) {
if ('TraitUpdatePositionedPositionPacket' === packet.constructor.name) {
[this.serverX, this.serverY] = packet.data.position;
}
}
// eslint-disable-next-line camelcase
on_positionChanged(oldPosition, newPosition) {
[this.entity.position[0], this.entity.position[1]] = newPosition;
if (AVOCADO_SERVER) {
[this.state.x, this.state.y] = newPosition;
this.setDirty();
}
this.entity.emit('positionChanged', oldPosition, newPosition);
}
onServerPositionChanged() {
this.serverPositionDirty = true;
}
packets() {
const {x, y} = this.stateDifferences();
if (x || y) {
return [
'TraitUpdatePositionedPositionPacket',
{
position: this.entity.position,
},
];
}
return undefined;
}
listeners() {
return {
isTickingChanged: () => {
// Snap position on ticking change.
if (AVOCADO_CLIENT) {
this._position = this.serverPosition;
}
},
};
}
methods() {
return {
setPosition: (position) => {
this._position = position;
},
};
}
renderTick() {
if (!this.serverPositionDirty) {
return;
}
if (Vector.equals(this._position, this.serverPosition)) {
this.serverPositionDirty = false;
return;
}
if (Vector.equalsClose(this._position, this.serverPosition, 0.1)) {
this._position = this.serverPosition;
this.serverPositionDirty = false;
return;
}
const diff = Vector.sub(this.serverPosition, this._position);
const lerp = 0.5;
this._position = Vector.add(this._position, Vector.scale(diff, lerp));
}
}

View File

@ -0,0 +1,266 @@
import {compose} from '@latus/core';
import {synchronized} from '@avocado/s13n';
import merge from 'deepmerge';
import Trait, {StateProperty} from '../trait';
const decorate = compose(
StateProperty('isSpawning', {
track: true,
}),
StateProperty('maxSpawns'),
);
export default (latus) => class Spawner extends decorate(Trait) {
static behaviorTypes() {
return {
killAllChildren: {
type: 'void',
label: 'Kill all spawned children',
},
spawn: {
cycle: true,
type: 'void|entity',
label: 'Spawn $1 with $2 extensions.',
args: [
['key', {
type: 'string',
options: (entity) => this.optionsForSpawn(entity),
}],
['json', {
type: 'object',
}],
],
},
spawnAt: {
cycle: true,
type: 'void|entity',
label: 'Spawn $1 as $2 with $3 extensions.',
args: [
['key', {
type: 'string',
options: (entity) => this.optionsForSpawn(entity),
}],
['position', {
type: 'vector',
}],
['json', {
type: 'object',
}],
],
},
spawnRaw: {
cycle: true,
type: 'void|entity',
label: 'Spawn $1.',
args: [
['json', {
type: 'object',
}],
],
},
spawnRawAt: {
cycle: true,
type: 'void|entity',
label: 'Spawn $1 at $2.',
args: [
['position', {
type: 'vector',
}],
['json', {
type: 'object',
}],
],
},
};
}
static defaultParams() {
return {
spawns: {},
};
}
static describeParams() {
return {
spawns: {
type: 'object',
label: 'Entities that may be spawned',
},
};
}
static defaultState() {
return {
isSpawning: true,
maxSpawns: Infinity,
};
}
static describeState() {
return {
isSpawning: {
type: 'bool',
label: 'Is spawning',
},
maxSpawns: {
type: 'number',
label: 'Maximum concurrent spawns',
},
};
}
static optionsForSpawn(entity) {
if (!entity) {
return undefined;
}
return Object.keys(entity.traitInstance('spawner').params.spawns)
.reduce((r, key) => ({...r, [key]: key}), {});
}
static type() {
return 'spawner';
}
destroy() {
while (this.children.length > 0) {
const child = this.children.pop();
if (child) {
this.removeChild(child);
}
}
}
constructor(entity, params, state) {
super(entity, params, state);
this.children = [];
this.childrenListeners = new Map();
this.spawnJSONs = this.params.spawns;
}
// eslint-disable-next-line class-methods-use-this
augmentJSONWithPosition(json, position) {
/* eslint-disable no-param-reassign */
if (!json.traits) {
json.traits = {};
}
if (!json.traits.positioned) {
json.traits.positioned = {};
}
if (!json.traits.positioned.state) {
json.traits.positioned.state = {};
}
[
json.traits.positioned.state.x,
json.traits.positioned.state.y,
] = position;
/* eslint-enable no-param-reassign */
return json;
}
destinationEntityList() {
if (
this.entity.is('listed')
&& this.entity.list
) {
return this.entity.list;
}
if (
this.entity.wielder
&& this.entity.wielder.is('listed')
&& this.entity.wielder.list
) {
return this.entity.wielder.list;
}
return undefined;
}
maySpawn() {
if (this.maxSpawns <= this.children.length) {
return false;
}
if (!this.destinationEntityList()) {
return false;
}
return true;
}
removeChild(child) {
const index = this.children.indexOf(child);
if (-1 !== index) {
this.children.splice(index, 1);
const listener = this.childrenListeners.get(child);
child.off('destroy', listener);
this.childrenListeners.delete(child);
}
}
listeners() {
return {
isDyingChanged: (_, isDying) => {
this.isSpawning = !isDying;
},
};
}
methods() {
return {
killAllChildren: () => {
// Juggle children since this may cause splices and mess up the array.
const children = this.children.slice(0);
for (let i = 0; i < children.length; i++) {
children[i].destroyGently();
}
},
spawn: (key, json = {}) => {
if (!this.maySpawn()) {
return undefined;
}
const spawnJSON = this.spawnJSONs[key];
if (!spawnJSON) {
return undefined;
}
return this.entity.spawnRaw(merge(spawnJSON, json));
},
spawnAt: (key, position, json = {}) => (
this.maySpawn()
? this.entity.spawn(key, this.augmentJSONWithPosition(json, position))
: undefined
),
spawnRaw: async (json) => {
if (!this.maySpawn()) {
return undefined;
}
// Add null to children to prevent race.
const childIndex = this.children.length;
this.children.push(null);
const list = this.destinationEntityList();
const {fromName: {Entity}} = synchronized(latus);
const child = await Entity.loadOrInstance(json);
this.children[childIndex] = child;
// Listen for destroy event.
const listener = this.removeChild.bind(this, child);
this.childrenListeners.set(child, listener);
child.once('destroy', listener);
// Add child to list.
list.addEntity(child);
return child;
},
spawnRawAt: (position, json = {}) => (
this.maySpawn()
? this.entity.spawnRaw(this.augmentJSONWithPosition(json, position))
: undefined
),
};
}
};

View File

@ -0,0 +1,3 @@
const neutrino = require('neutrino');
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();

7757
packages/entity/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
module.exports = require('../../config/.eslintrc');

5
packages/graphics/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
**/*.js
**/*.map
!/.*
!/webpack.config.js
!src/**/*.js

View File

@ -0,0 +1 @@
module.exports = require('../../config/.neutrinorc');

View File

@ -0,0 +1,55 @@
{
"name": "@avocado/graphics",
"version": "2.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"scripts": {
"build": "NODE_PATH=./node_modules webpack --mode production",
"clean": "rm -rf yarn.lock node_modules && yarn",
"dev": "NODE_PATH=./node_modules webpack --mode development",
"forcepub": "npm unpublish --force $(node -e 'const {name, version} = require(`./package.json`); process.stdout.write(`${name}@${version}`)') && npm publish",
"link": "node -e \"Object.keys(require('./package.json').dependencies).filter((m) => 0 === m.indexOf('@latus/')).forEach((m) => require('child_process').spawn('yarn', ['link', m]));\"",
"lint": "NODE_PATH=./node_modules eslint --format codeframe --ext mjs,js .",
"test": "NODE_PATH=./node_modules mocha --config ../../config/.mocharc.js",
"unlink": "node -e \"Object.keys(require('./package.json').dependencies).filter((m) => 0 === m.indexOf('@latus/')).forEach((m) => require('child_process').spawn('yarn', ['unlink', m]));\" && yarn install --force",
"watch": "NODE_PATH=./node_modules webpack --watch --mode development"
},
"files": [
"index.js",
"index.js.map"
],
"dependencies": {
"@avocado/core": "2.0.0",
"@avocado/entity": "2.0.0",
"@avocado/input": "2.0.0",
"@avocado/math": "2.0.0",
"@avocado/resource": "^2.0.0",
"@latus/core": "^2.0.0",
"@latus/socket": "^2.0.0",
"@pixi/constants": "^5.3.6",
"@pixi/core": "^5.3.6",
"@pixi/display": "^5.3.6",
"@pixi/filter-advanced-bloom": "^3.2.0",
"@pixi/filter-color-matrix": "^5.3.6",
"@pixi/graphics": "^5.3.6",
"@pixi/settings": "^5.3.6",
"@pixi/sprite": "^5.3.6",
"@pixi/text": "^5.3.6",
"debug": "4.3.1"
},
"devDependencies": {
"@neutrinojs/airbnb-base": "^9.4.0",
"@neutrinojs/copy": "9.4.0",
"@neutrinojs/mocha": "^9.4.0",
"@neutrinojs/react": "^9.4.0",
"chai": "4.2.0",
"eslint": "^7",
"eslint-import-resolver-webpack": "0.13.0",
"mocha": "^8",
"neutrino": "^9.4.0",
"webpack": "^4",
"webpack-cli": "^3",
"webpack-node-externals": "2.5.2"
}
}

View File

@ -0,0 +1,47 @@
import {Vector} from '@avocado/math';
import {Class, compose} from '@avocado/core';
import {RenderTexture} from '@pixi/core';
import Image from './image';
const decorate = compose(
Vector.Mixin('size', 'width', 'height', {
default: [0, 0],
}),
);
export default class Canvas extends decorate(Class) {
constructor(size = [0, 0]) {
super();
this.renderTexture = RenderTexture.create(size[0], size[1]);
}
destroy() {
this.renderTexture.destroy();
}
get internal() {
return this.renderTexture;
}
render(renderable, renderer) {
renderer.render(renderable, this);
}
get size() {
return super.size;
}
set size(size) {
this.renderTexture.resize(size[0], size[1]);
super.size = size;
}
toImage() {
const image = new Image();
image.texture = this.renderTexture.clone();
return image;
}
}

View File

@ -0,0 +1,34 @@
export default class Color {
static fromCss(css) {
if ('#'.charCodeAt(0) === css.charCodeAt(0)) {
let hex = css.substr(1);
if (3 === hex.length) {
hex = hex.split('').map((c) => c + c).join('');
}
const red = parseInt(hex.substr(0, 2), 16);
const green = parseInt(hex.substr(2, 2), 16);
const blue = parseInt(hex.substr(4, 2), 16);
return new Color(red, green, blue);
}
const colors = css.replace(/\s/g, '').match(/rgba?\((.*)\)/)[1].split(',');
return new Color(colors[0], colors[1], colors[2], colors[3] || 1);
}
constructor(red = 255, green = 0, blue = 255, alpha = 1) {
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
}
toCss() {
return `rgba(${this.red}, ${this.green}, ${this.blue}, ${this.alpha})`;
}
toInteger() {
// eslint-disable-next-line no-bitwise
return (this.red << 16) | (this.green << 8) | this.blue;
}
}

View File

@ -0,0 +1,167 @@
import {Container as PIXIContainer} from '@pixi/display';
import {AdvancedBloomFilter} from '@pixi/filter-advanced-bloom';
import {ColorMatrixFilter} from '@pixi/filter-color-matrix';
import Renderable from './renderable';
export default class Container extends Renderable {
constructor() {
super();
this._children = [];
this._childrenIndexes = new Map();
this.container = new PIXIContainer();
}
addChild(child) {
// eslint-disable-next-line no-param-reassign
child.parent = this;
this.isDirty = true;
const index = this._children.push(child) - 1;
this._childrenIndexes.set(child, index);
this.container.addChild(child.internal);
}
get children() {
return this._children;
}
desaturate() {
const filter = new ColorMatrixFilter();
filter.desaturate();
this.container.filters = [filter];
}
destroy() {
this.container.filters = [];
this.children.forEach((child) => {
this.removeChild(child);
child.destroy();
});
super.destroy();
}
get internal() {
return this.container;
}
night(intensity = 1) {
let filter;
const {filters} = this.container;
if (filters && filters.length > 0 && filters[0] instanceof ColorMatrixFilter) {
[filter] = filters;
}
else {
filter = new ColorMatrixFilter();
}
const nightness = 0.1;
const double = nightness * 2;
const half = nightness / 2;
const redDown = 1 - (intensity * (1 + double));
const blueUp = 1 - (intensity * (1 - half));
const scale = intensity * nightness;
const matrix = [
redDown, -scale, 0, 0, 0,
-scale, (1 - intensity), scale, 0, 0,
0, scale, blueUp, 0, 0,
0, 0, 0, 1, 0,
];
filter._loadMatrix(matrix);
this.container.filters = [filter];
}
paused(intensity = 1) {
const filter = new ColorMatrixFilter();
filter.sepia();
filter.brightness(1 - intensity, true);
this.container.filters = [filter];
}
removeAllFilters() {
this.container.filters = [];
}
_removeChild(child) {
const index = this._children.indexOf(child);
if (-1 === index) {
return;
}
this._children.splice(index, 1);
this.container.removeChild(child.internal);
}
removeChild(child) {
this._removeChild(child);
this._resetChildrenIndexes();
}
removeAllChildren() {
for (let i = 0; i < this._children.length; i++) {
this._removeChild(this._children[i]);
}
this._childrenIndexes = new Map();
}
renderTick(elapsed) {
for (let i = 0; i < this._children.length; i++) {
const child = this._children[i];
if (child instanceof Container) {
child.renderTick(elapsed);
}
}
let needsSort = false;
let currentZ = -Infinity;
for (let i = 0; i < this._children.length; i++) {
const child = this._children[i];
if (currentZ > child.zIndex) {
needsSort = true;
break;
}
currentZ = child.zIndex;
}
if (!needsSort) {
return;
}
this._children.sort((l, r) => {
if (l.zIndex !== r.zIndex) {
return l.zIndex - r.zIndex;
}
const lIndex = this._childrenIndexes.get(l);
const rIndex = this._childrenIndexes.get(r);
return lIndex - rIndex;
});
this.container.children = this._children.map((child) => child.internal);
this._resetChildrenIndexes();
}
_resetChildrenIndexes() {
this._childrenIndexes = new Map();
for (let i = 0; i < this._children.length; i++) {
this._childrenIndexes.set(this._children[i], i);
}
}
sepia() {
const filter = new ColorMatrixFilter();
filter.sepia();
this.container.filters = [filter];
}
setFilter(filter) {
switch (filter) {
case 'bloom':
this.container.filters = [
new AdvancedBloomFilter({
threshold: 0.5,
bloomScale: 0.8,
brightness: 0.7,
blur: 2,
quality: 6,
}),
];
break;
default:
}
}
}

View File

@ -0,0 +1,90 @@
import {BaseTexture, Texture} from '@pixi/core';
import {SCALE_MODES} from '@pixi/constants';
import {Resource} from '@avocado/resource';
export default class Image extends Resource {
constructor() {
super();
this.texture = null;
}
destroy() {
this.texture.destroy();
}
static fromHtmlCanvas(htmlCanvas) {
const baseTexture = BaseTexture.from(
htmlCanvas,
SCALE_MODES.NEAREST,
);
const image = new Image();
image.texture = new Texture(baseTexture);
return image;
}
static load(uri) {
return this.loadBaseTexture(uri).then((baseTexture) => {
const image = new Image();
image.uri = uri;
image.texture = new Texture(baseTexture);
return image;
});
}
static loadBaseTexture(uri) {
if (!this.baseTextureCache) {
this.baseTextureCache = {};
}
if (this.baseTextureCache[uri]) {
return Promise.resolve(this.baseTextureCache[uri]);
}
return new Promise((resolve, reject) => {
const baseTexture = BaseTexture.from(uri);
baseTexture.once('error', () => {
reject(new Error(`Couldn't load image "${uri}"`));
});
baseTexture.once('loaded', () => {
resolve(this.baseTextureCache[uri] = baseTexture);
});
});
}
get height() {
if (!this.texture) {
return 0;
}
return this.texture.height;
}
get size() {
return [this.width, this.height];
}
subimage(rectangle) {
const frame = {
x: rectangle[0],
y: rectangle[1],
width: rectangle[2],
height: rectangle[3],
};
const subimage = new Image();
subimage.texture = new Texture(this.texture, frame);
return subimage;
}
updateBaseTexture() {
if (this.texture && this.texture.baseTexture) {
this.texture.baseTexture.update();
}
}
get width() {
if (!this.texture) {
return 0;
}
return this.texture.width;
}
}

View File

@ -0,0 +1,18 @@
import {SCALE_MODES} from '@pixi/constants';
import {Renderer, BatchRenderer} from '@pixi/core';
import {settings} from '@pixi/settings';
export {default as Canvas} from './canvas';
export {default as Color} from './color';
export {default as Container} from './container';
export {default as Image} from './image';
export {default as Primitives} from './primitives';
export {default as Renderable} from './renderable';
export {default as Renderer} from './renderer';
export {default as Sprite} from './sprite';
export {default as Stage} from './stage';
export {default as Text} from './text';
// Pixelly!
settings.SCALE_MODE = SCALE_MODES.NEAREST;
// Lil pixi management.
Renderer.registerPlugin('batch', BatchRenderer);

View File

@ -0,0 +1,28 @@
import {Packet} from '@latus/socket';
export default class TraitUpdateVisiblePacket extends Packet {
static pack(packet) {
const data = packet.data[1];
data.opacity = Math.floor(data.opacity * 255);
return super.pack(packet);
}
static get schema() {
return {
...super.schema,
data: {
isVisible: 'bool',
opacity: 'uint8',
},
};
}
static unpack(packet) {
const unpacked = super.unpack(packet);
const {data} = unpacked;
data.opacity /= 255;
return unpacked;
}
}

View File

@ -0,0 +1,68 @@
import {Graphics} from '@pixi/graphics';
import Renderable from './renderable';
export default class Primitives extends Renderable {
static fillStyle(color) {
return {color};
}
static lineStyle(color, thickness = 1) {
return {color, thickness};
}
constructor() {
super();
this.primitives = new Graphics();
}
clear() {
this.primitives.clear();
}
drawCircle(position, radius, lineStyle, fillStyle) {
this._wrapStyle('drawCircle', '3rd', lineStyle, fillStyle, () => {
this.primitives.drawCircle(position[0], position[1], radius);
});
}
drawLine(p1, p2, lineStyle, fillStyle) {
this._wrapStyle('drawLine', '3rd', lineStyle, fillStyle, () => {
this.primitives.moveTo(p1[0], p1[1]);
this.primitives.lineTo(p2[0], p2[1]);
});
}
drawRectangle(rectangle, lineStyle, fillStyle) {
this._wrapStyle('drawLine', '2nd', lineStyle, fillStyle, () => {
this.primitives.drawRect(
rectangle[0],
rectangle[1],
rectangle[2],
rectangle[3],
);
});
}
get internal() {
return this.primitives;
}
_wrapStyle(method, where, lineStyle, fillStyle, fn) {
if (!lineStyle) {
throw new TypeError(`Primitives::${method} expects lineStyle as ${where} parameter`);
}
if (fillStyle) {
const {color} = fillStyle;
this.primitives.beginFill(color.toInteger(), color.alpha);
}
const {color, thickness} = lineStyle;
this.primitives.lineStyle(thickness, color.toInteger(), color.alpha);
fn();
if (fillStyle) {
this.primitives.endFill();
}
}
}

View File

@ -0,0 +1,108 @@
import {Class, compose, EventEmitter} from '@latus/core';
const decorate = compose(
EventEmitter,
);
export default class Renderable extends decorate(Class) {
constructor() {
super();
this.parent = null;
this._zIndex = 0;
}
destroy() {
if (this.internal) {
this.internal.destroy();
delete this.internal;
}
}
get alpha() {
return this.internal.alpha;
}
set alpha(alpha) {
this.internal.alpha = alpha;
}
get anchor() {
const anchor = this.internal.anchor || {x: 0, y: 0};
return [
anchor.x,
anchor.y,
];
}
set anchor(anchor) {
this.internal.anchor = {
x: anchor[0],
y: anchor[1],
};
}
get position() {
return [this.internal.x, this.internal.y];
}
set position(position) {
[this.internal.x, this.internal.y] = position;
}
get rotation() {
return this.internal.rotation;
}
set rotation(rotation) {
this.internal.rotation = rotation;
}
get scale() {
const scale = this.internal.scale || {x: 1, y: 1};
return [
scale.x,
scale.y,
];
}
set scale(scale) {
this.internal.scale = {
x: scale[0],
y: scale[1],
};
}
get visible() {
return this.internal.visible;
}
set visible(isVisible) {
this.internal.visible = isVisible;
}
get x() {
return this.internal.x;
}
set x(x) {
this.internal.x = x;
}
get y() {
return this.internal.y;
}
set y(y) {
this.internal.y = y;
}
get zIndex() {
return this._zIndex;
}
set zIndex(zIndex) {
this._zIndex = zIndex;
}
}

View File

@ -0,0 +1,35 @@
import {Renderer as PIXIRenderer} from '@pixi/core';
export default class Renderer {
constructor(size = [0, 0]) {
this.renderer = new PIXIRenderer({width: size[0], height: size[1]});
[this.renderer.view.width, this.renderer.view.height] = size;
}
destroy() {
this.renderer.destroy();
}
get element() {
return this.renderer.view;
}
get height() {
return this.element.height;
}
render(item, canvas) {
const canvasInternal = canvas ? canvas.internal : undefined;
this.renderer.render(item.internal, canvasInternal);
}
get size() {
return [this.width, this.height];
}
get width() {
return this.element.width;
}
}

View File

@ -0,0 +1,32 @@
import {Sprite as PIXISprite} from '@pixi/sprite';
import Renderable from './renderable';
export default class Sprite extends Renderable {
constructor(image) {
super();
this._image = image;
this.sprite = new PIXISprite(image.texture);
this.anchor = [0.5, 0.5];
}
get internal() {
return this.sprite;
}
get image() {
return this._image;
}
set sourceRectangle(rectangle) {
this._image.texture.frame = {
x: rectangle[0],
y: rectangle[1],
width: rectangle[2],
height: rectangle[3],
};
this._image.texture.updateUvs();
}
}

View File

@ -0,0 +1,243 @@
import {compose, Property} from '@avocado/core';
import {InputNormalizer} from '@avocado/input';
import {Vector} from '@avocado/math';
import Container from './container';
import Renderer from './renderer';
const ASPECT = 16 / 9;
const decorate = compose(
Property('camera', {
track: true,
}),
);
export default class Stage extends decorate(Container) {
constructor(visibleSize, visibleScale) {
super();
const size = Vector.mul(visibleSize, visibleScale);
// Container element.
this.element = window.document.createElement('div');
this.element.className = 'avocado-stage';
this.element.style.height = '100%';
this.element.style.lineHeight = '0';
this.element.style.position = 'relative';
this.element.style.width = '100%';
// DOM parent.
this.parent = undefined;
// Set scale.
this.scale = visibleScale;
// Canvas renderer.
this.renderer = new Renderer(size);
this.renderer.element.style.width = '100%';
this.renderer.element.style.height = '100%';
// "real" dimensions.
this.size = size;
// Precalc for position/dimension transformation.
this._transformRatio = 1;
// UI DOM node.
this.ui = this.createUiLayer();
this._queuedFindSelectors = {};
// Event handlers.
this.onWindowResize = this.onWindowResize.bind(this);
window.addEventListener('resize', this.onWindowResize);
// Normalize input.
this.inputNormalizer = new InputNormalizer();
this.inputNormalizer.listen(this.element);
this.inputNormalizer.on('keyDown', this.onKeyDown, this);
this.inputNormalizer.on('keyUp', this.onKeyUp, this);
this.inputNormalizer.on('pointerDown', this.onPointerDown, this);
this.inputNormalizer.on('pointerMove', this.onPointerMove, this);
this.inputNormalizer.on('pointerUp', this.onPointerUp, this);
this.inputNormalizer.on('wheel', this.onWheel, this);
// Put the renderer and UI in the container element, and mark it
// focusable.
this.element.appendChild(this.renderer.element);
this.element.appendChild(this.ui);
this.element.tabIndex = 0;
}
addToDom(parent) {
// Had a parent? Remove.
this.removeFromDom();
this.parent = parent;
// Add to new parent (if any) and focus.
if (parent) {
parent.appendChild(this.element);
}
// Recalculate size and ratio.
this.onWindowResize();
}
createUiLayer() {
const node = window.document.createElement('div');
node.className = 'ui';
node.style.overflow = 'hidden';
node.style.position = 'absolute';
node.style.width = `${this.size[0] / this.scale[0]}px`;
node.style.height = `${this.size[1] / this.scale[1]}px`;
node.style.left = 0;
node.style.top = 0;
node.style.transformOrigin = '0 0 0';
return node;
}
destroy() {
window.removeEventListener('resize', this.onWindowResize);
this.renderer.destroy();
super.destroy();
if (this.parent) {
this.parent.removeChild(this.renderer.element);
}
this.inputNormalizer.off('keyDown', this.onKeyDown);
this.inputNormalizer.off('keyUp', this.onKeyUp);
this.inputNormalizer.off('pointerDown', this.onPointerDown);
this.inputNormalizer.off('pointerMove', this.onPointerMove);
this.inputNormalizer.off('pointerUp', this.onPointerUp);
this.inputNormalizer.off('wheel', this.onWheel);
this.inputNormalizer.destroy();
}
get displaySize() {
return [
this.element.style.width,
this.element.style.height,
];
}
findUiElement(selector) {
const node = this.ui.querySelector(selector);
if (node) {
return Promise.resolve(node);
}
const queued = this._queuedFindSelectors[selector];
if (queued) {
return queued.promise;
}
let resolve;
const promise = new Promise((resolve_) => {
resolve = resolve_;
});
this._queuedFindSelectors[selector] = {resolve, promise};
return promise;
}
flushUiElements() {
const selectors = Object.keys(this._queuedFindSelectors);
for (let i = 0; i < selectors.length; i++) {
const selector = selectors[i];
const {resolve} = this._queuedFindSelectors[selector];
resolve(this.ui.querySelector(selector));
}
this._queuedFindSelectors = {};
}
focus() {
this.element.focus();
}
onKeyDown(event) {
this.emit('keyDown', event);
}
onKeyUp(event) {
this.emit('keyUp', event);
}
onPointerDown(event) {
// eslint-disable-next-line no-param-reassign
event.position = this.transformCanvasPosition(event.position);
this.emit('pointerDown', event);
}
onPointerMove(event) {
// eslint-disable-next-line no-param-reassign
event.position = this.transformCanvasPosition(event.position);
this.emit('pointerMove', event);
}
onPointerUp(event) {
// eslint-disable-next-line no-param-reassign
event.position = this.transformCanvasPosition(event.position);
this.emit('pointerUp', event);
}
onWheel(event) {
this.emit('wheel', event);
}
onWindowResize() {
if (!this.parent) {
return;
}
// Find the biggest axe, width or height.
const ratio = this.parent.clientWidth / this.parent.clientHeight;
const biggest = ratio > ASPECT ? 'height' : 'width';
// Key parent client size by axe.
const parentClient = {
width: this.parent.clientWidth,
height: this.parent.clientHeight,
};
['height', 'width'].forEach((axe) => {
// Biggest axe? Inherit parent axe size.
if (axe === biggest) {
this.element.style[axe] = `${parentClient[biggest]}px`;
}
// Derive height from width.
else if ('width' === biggest) {
this.element.style.height = `${parentClient[biggest] * (1 / ASPECT)}px`;
}
// Derive width from height.
else {
this.element.style.width = `${parentClient[biggest] * ASPECT}px`;
}
});
// Precalc the transformation ratio and apply it to the UI layer.
this._transformRatio = this.size[0] / this.element.clientWidth;
const scaleFactor = 1 / this._transformRatio;
const scaleX = scaleFactor * this.scale[0];
const scaleY = scaleFactor * this.scale[1];
this.ui.style.transform = `scaleX(${scaleX}) scaleY(${scaleY})`;
this.emit('displaySizeChanged', this.displaySize);
}
removeFromDom() {
if (this.parent) {
this.parent.removeChild(this.element);
this.parent = undefined;
}
}
renderTick(elapsed) {
super.renderTick(elapsed);
this.renderer.render(this);
if (this.camera) {
const inverseOffset = Vector.mul(
this.camera.realOffset,
Vector.scale(this.scale, -1),
);
this.position = inverseOffset;
}
}
resolveUiRendered() {
this._uiResolve();
}
transformCanvasPosition(position) {
const rect = this.renderer.element.getBoundingClientRect();
const topLeft = [rect.x, rect.y];
const offset = Vector.sub(position, topLeft);
return Vector.div(
Vector.scale(offset, this._transformRatio),
this.scale,
);
}
get transformRatio() {
return this._transformRatio;
}
}

View File

@ -0,0 +1,17 @@
import {Text as PIXIText} from '@pixi/text';
import Renderable from './renderable';
export default class Text extends Renderable {
constructor(text, style) {
super();
this.text = new PIXIText(text, style);
this.anchor = [0.5, 0.5];
}
get internal() {
return this.text;
}
}

View File

@ -0,0 +1,210 @@
import {StateProperty, Trait} from '@avocado/entity';
import {Rectangle, Vector} from '@avocado/math';
import {compose} from '@latus/core';
import Image from '../image';
import Sprite from '../sprite';
const decorate = compose(
StateProperty('currentImage', {
track: true,
}),
);
export default class Pictured extends decorate(Trait) {
static defaultParams() {
return {
images: {},
};
}
static defaultState() {
return {
currentImage: 'initial',
};
}
static describeParams() {
return {
images: {
type: 'object',
label: 'Images',
},
};
}
static describeState() {
return {
currentImage: {
type: 'string',
label: 'Current image',
},
};
}
static type() {
return 'pictured';
}
constructor(entity, params, state) {
super(entity, params, state);
this._cachedAabbs = {};
this._images = this.params.images;
this.sprites = undefined;
}
destroy() {
if (this.sprites) {
const sprites = Object.entries(this.sprites);
for (let i = 0; i < sprites.length; i++) {
const [key, sprite] = sprites[i];
this.hideImage(key);
sprite.image.destroy();
sprite.destroy();
}
this.sprites = undefined;
}
}
hideImage(key) {
if (!this.sprites) {
return;
}
const sprite = this.sprites[key];
if (!sprite) {
return;
}
this.entity.container.removeChild(sprite);
}
async loadImagesIfPossible() {
if (!this.entity.container) {
return;
}
if (this.sprites) {
return;
}
// Load all images.
const imagePromises = [];
this.sprites = {};
const images = Object.entries(this._images);
for (let i = 0; i < images.length; i++) {
const [key, {uri}] = images[i];
const imagePromise = Image.load(uri).then((image) => {
const sprite = new Sprite(image);
// Calculate any offset.
sprite.position = this.offsetFor(key);
// Set current image upfront.
const isCurrentImage = key === this.entity.currentImage;
if (isCurrentImage) {
this.showImage(key);
}
this.sprites[key] = sprite;
});
imagePromises.push(imagePromise);
}
await Promise.all(imagePromises);
this.entity.updateVisibleBoundingBox();
this.setSpriteScale();
}
offsetFor(key) {
if (!this._images[key] || !this._images[key].offset) {
return [0, 0];
}
return this._images[key].offset;
}
setSpriteScale() {
if (!this.sprites) {
return;
}
const rawScale = this.entity.rawVisibleScale;
const sprites = Object.values(this.sprites);
for (let i = 0; i < sprites.length; i++) {
sprites[i].scale = rawScale;
}
}
showImage(key) {
if (!this.sprites) {
return;
}
const sprite = this.sprites[key];
if (!sprite) {
return;
}
this.entity.container.addChild(sprite);
}
sizeFor(key) {
if (!this._images[key] || !this._images[key].size) {
return [0, 0];
}
return this._images[key].size;
}
hooks() {
return {
visibleAabbs: () => {
const key = this.entity.currentImage;
if (this._cachedAabbs[key]) {
return this._cachedAabbs[key];
}
const image = this._images[key];
if (!image) {
return [0, 0, 0, 0];
}
const size = this.sizeFor(key);
const scaledSize = Vector.mul(size, this.entity.rawVisibleScale);
const viewPosition = Vector.sub(
this.offsetFor(key),
Vector.scale(scaledSize, 0.5),
);
const rectangle = Rectangle.compose(viewPosition, size);
const expanded = Rectangle.expand(
rectangle,
Vector.sub(scaledSize, size),
);
this._cachedAabbs[key] = expanded;
return expanded;
},
};
}
listeners() {
return {
currentImageChanged: (oldKey) => {
// Bounding box update.
this.entity.updateVisibleBoundingBox();
// Only client/graphics.
if (!this.sprites) {
return;
}
// Swap the image.
this.hideImage(oldKey);
this.showImage(this.entity.currentImage);
},
visibleScaleChanged: () => {
this.setSpriteScale();
},
traitAdded: (type) => {
if (-1 === [
'visible',
'pictured',
].indexOf(type)) {
return;
}
this.loadImagesIfPossible();
},
};
}
}

Some files were not shown because too many files have changed in this diff Show More