chore: initial
This commit is contained in:
commit
ec0487d49c
116
.gitignore
vendored
Normal file
116
.gitignore
vendored
Normal 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
26
app/.eslint.defaults.js
Normal 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
5
app/.eslintrc.js
Normal 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
119
app/.gitignore
vendored
Normal 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
5
app/.mocharc.js
Normal 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
92
app/.neutrinorc.js
Normal 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
17
app/docker-compose.yml
Normal 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
50
app/latus.default.yml
Normal 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
45
app/package.json
Normal 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
1
app/src/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
process.stdout.write('Your application is starting...\n');
|
20
app/src/react/index.jsx
Normal file
20
app/src/react/index.jsx
Normal 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
28
app/webpack.config.js
Normal 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
8921
app/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
24
config/.eslint.defaults.js
Normal file
24
config/.eslint.defaults.js
Normal 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
3
config/.eslintrc.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
const neutrino = require('neutrino');
|
||||||
|
|
||||||
|
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).eslintrc();
|
5
config/.mocharc.js
Normal file
5
config/.mocharc.js
Normal 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
66
config/.neutrinorc.js
Normal 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(),
|
||||||
|
],
|
||||||
|
};
|
1
config/package/.eslintrc.js
Normal file
1
config/package/.eslintrc.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../config/.eslintrc');
|
5
config/package/.gitignore
vendored
Normal file
5
config/package/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
**/*.js
|
||||||
|
**/*.map
|
||||||
|
!/.*
|
||||||
|
!/webpack.config.js
|
||||||
|
!src/**/*.js
|
1
config/package/.neutrinorc.js
Normal file
1
config/package/.neutrinorc.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../config/.neutrinorc');
|
39
config/package/package.json
Normal file
39
config/package/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
0
config/package/src/index.js
Normal file
0
config/package/src/index.js
Normal file
3
config/package/webpack.config.js
Normal file
3
config/package/webpack.config.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
const neutrino = require('neutrino');
|
||||||
|
|
||||||
|
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();
|
52
config/split-config.js
Normal file
52
config/split-config.js
Normal 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
8
config/webpack.config.js
Normal 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
6
lerna.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"packages": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
27
package.json
Normal file
27
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
1
packages/behavior/.eslintrc.js
Normal file
1
packages/behavior/.eslintrc.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../config/.eslintrc');
|
5
packages/behavior/.gitignore
vendored
Normal file
5
packages/behavior/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
**/*.js
|
||||||
|
**/*.map
|
||||||
|
!/.*
|
||||||
|
!/webpack.config.js
|
||||||
|
!src/**/*.js
|
1
packages/behavior/.neutrinorc.js
Normal file
1
packages/behavior/.neutrinorc.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../config/.neutrinorc');
|
44
packages/behavior/package.json
Normal file
44
packages/behavior/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
107
packages/behavior/src/actions.js
Normal file
107
packages/behavior/src/actions.js
Normal 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);
|
44
packages/behavior/src/builders.js
Normal file
44
packages/behavior/src/builders.js
Normal 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)),
|
||||||
|
};
|
||||||
|
}
|
13
packages/behavior/src/compilers/compile.js
Normal file
13
packages/behavior/src/compilers/compile.js
Normal 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}'`));
|
||||||
|
}
|
50
packages/behavior/src/compilers/condition.js
Normal file
50
packages/behavior/src/compilers/condition.js
Normal 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}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
85
packages/behavior/src/compilers/expression.js
Normal file
85
packages/behavior/src/compilers/expression.js
Normal 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));
|
||||||
|
};
|
||||||
|
};
|
5
packages/behavior/src/compilers/expressions.js
Normal file
5
packages/behavior/src/compilers/expressions.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import compile from './expression';
|
||||||
|
|
||||||
|
export default (latus) => ({expressions}) => () => (
|
||||||
|
expressions.map((expression) => compile(expression, latus))
|
||||||
|
);
|
11
packages/behavior/src/compilers/index.js
Normal file
11
packages/behavior/src/compilers/index.js
Normal 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),
|
||||||
|
});
|
1
packages/behavior/src/compilers/literal.js
Normal file
1
packages/behavior/src/compilers/literal.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export default () => ({value}) => () => value;
|
71
packages/behavior/src/context.js
Normal file
71
packages/behavior/src/context.js
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
15
packages/behavior/src/globals/flow.js
Normal file
15
packages/behavior/src/globals/flow.js
Normal 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),
|
||||||
|
|
||||||
|
};
|
10
packages/behavior/src/globals/index.js
Normal file
10
packages/behavior/src/globals/index.js
Normal 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'],
|
||||||
|
});
|
16
packages/behavior/src/globals/timing.js
Normal file
16
packages/behavior/src/globals/timing.js
Normal 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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
};
|
36
packages/behavior/src/globals/utility.js
Normal file
36
packages/behavior/src/globals/utility.js
Normal 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);
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
17
packages/behavior/src/index.js
Normal file
17
packages/behavior/src/index.js
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
122
packages/behavior/src/traits/behaved.js
Normal file
122
packages/behavior/src/traits/behaved.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
5
packages/behavior/src/traits/index.js
Normal file
5
packages/behavior/src/traits/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import Behaved from './behaved';
|
||||||
|
|
||||||
|
export default (latus) => ({
|
||||||
|
Behaved: Behaved(latus),
|
||||||
|
});
|
8
packages/behavior/webpack.config.js
Normal file
8
packages/behavior/webpack.config.js
Normal 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
7748
packages/behavior/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/core/.eslintrc.js
Normal file
1
packages/core/.eslintrc.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../config/.eslintrc');
|
5
packages/core/.gitignore
vendored
Normal file
5
packages/core/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
**/*.js
|
||||||
|
**/*.map
|
||||||
|
!/.*
|
||||||
|
!/webpack.config.js
|
||||||
|
!src/**/*.js
|
1
packages/core/.neutrinorc.js
Normal file
1
packages/core/.neutrinorc.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../config/.neutrinorc');
|
39
packages/core/package.json
Normal file
39
packages/core/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
21
packages/core/src/fast-apply.js
Normal file
21
packages/core/src/fast-apply.js
Normal 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]);
|
||||||
|
};
|
21
packages/core/src/index.js
Normal file
21
packages/core/src/index.js
Normal 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';
|
48
packages/core/src/merge-diff.js
Normal file
48
packages/core/src/merge-diff.js
Normal 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);
|
||||||
|
}
|
160
packages/core/src/property.js
Normal file
160
packages/core/src/property.js
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
50
packages/core/src/ticking-promise.js
Normal file
50
packages/core/src/ticking-promise.js
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
13
packages/core/src/virtualize-static.js
Normal file
13
packages/core/src/virtualize-static.js
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
13
packages/core/src/virtualize.js
Normal file
13
packages/core/src/virtualize.js
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
8
packages/core/webpack.config.js
Normal file
8
packages/core/webpack.config.js
Normal 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
6168
packages/core/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/entity/.eslintrc.js
Normal file
1
packages/entity/.eslintrc.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../config/.eslintrc');
|
5
packages/entity/.gitignore
vendored
Normal file
5
packages/entity/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
**/*.js
|
||||||
|
**/*.map
|
||||||
|
!/.*
|
||||||
|
!/webpack.config.js
|
||||||
|
!src/**/*.js
|
1
packages/entity/.neutrinorc.js
Normal file
1
packages/entity/.neutrinorc.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../config/.neutrinorc');
|
49
packages/entity/package.json
Normal file
49
packages/entity/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
72
packages/entity/src/accessors.js
Normal file
72
packages/entity/src/accessors.js
Normal 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;
|
||||||
|
}
|
377
packages/entity/src/entity.js
Normal file
377
packages/entity/src/entity.js
Normal 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
24
packages/entity/src/index.js
Normal file
24
packages/entity/src/index.js
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
44
packages/entity/src/packets/entity-update-trait.js
Normal file
44
packages/entity/src/packets/entity-update-trait.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
5
packages/entity/src/packets/index.js
Normal file
5
packages/entity/src/packets/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import EntityUpdateTrait from './entity-update-trait';
|
||||||
|
|
||||||
|
export default (latus) => ({
|
||||||
|
EntityUpdateTrait: EntityUpdateTrait(latus),
|
||||||
|
});
|
187
packages/entity/src/trait.js
Normal file
187
packages/entity/src/trait.js
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
207
packages/entity/src/traits/alive.js
Normal file
207
packages/entity/src/traits/alive.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
100
packages/entity/src/traits/directional.js
Normal file
100
packages/entity/src/traits/directional.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
113
packages/entity/src/traits/existent.js
Normal file
113
packages/entity/src/traits/existent.js
Normal 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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
19
packages/entity/src/traits/index.js
Normal file
19
packages/entity/src/traits/index.js
Normal 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),
|
||||||
|
});
|
129
packages/entity/src/traits/listed.js
Normal file
129
packages/entity/src/traits/listed.js
Normal 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');
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
142
packages/entity/src/traits/mobile.js
Normal file
142
packages/entity/src/traits/mobile.js
Normal 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
41
packages/entity/src/traits/perishable.js
Normal file
41
packages/entity/src/traits/perishable.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
156
packages/entity/src/traits/positioned.js
Normal file
156
packages/entity/src/traits/positioned.js
Normal 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
266
packages/entity/src/traits/spawner.js
Normal file
266
packages/entity/src/traits/spawner.js
Normal 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
|
||||||
|
),
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
3
packages/entity/webpack.config.js
Normal file
3
packages/entity/webpack.config.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
const neutrino = require('neutrino');
|
||||||
|
|
||||||
|
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();
|
7757
packages/entity/yarn.lock
Normal file
7757
packages/entity/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/graphics/.eslintrc.js
Normal file
1
packages/graphics/.eslintrc.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../config/.eslintrc');
|
5
packages/graphics/.gitignore
vendored
Normal file
5
packages/graphics/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
**/*.js
|
||||||
|
**/*.map
|
||||||
|
!/.*
|
||||||
|
!/webpack.config.js
|
||||||
|
!src/**/*.js
|
1
packages/graphics/.neutrinorc.js
Normal file
1
packages/graphics/.neutrinorc.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('../../config/.neutrinorc');
|
55
packages/graphics/package.json
Normal file
55
packages/graphics/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
47
packages/graphics/src/canvas.js
Normal file
47
packages/graphics/src/canvas.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
34
packages/graphics/src/color.js
Normal file
34
packages/graphics/src/color.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
167
packages/graphics/src/container.js
Normal file
167
packages/graphics/src/container.js
Normal 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:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
90
packages/graphics/src/image.js
Normal file
90
packages/graphics/src/image.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
18
packages/graphics/src/index.js
Normal file
18
packages/graphics/src/index.js
Normal 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);
|
28
packages/graphics/src/packets/trait-update-visible.js
Normal file
28
packages/graphics/src/packets/trait-update-visible.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
68
packages/graphics/src/primitives.js
Normal file
68
packages/graphics/src/primitives.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
108
packages/graphics/src/renderable.js
Normal file
108
packages/graphics/src/renderable.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
35
packages/graphics/src/renderer.js
Normal file
35
packages/graphics/src/renderer.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
packages/graphics/src/sprite.js
Normal file
32
packages/graphics/src/sprite.js
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
243
packages/graphics/src/stage.js
Normal file
243
packages/graphics/src/stage.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
17
packages/graphics/src/text.js
Normal file
17
packages/graphics/src/text.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
210
packages/graphics/src/traits/pictured.js
Normal file
210
packages/graphics/src/traits/pictured.js
Normal 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
Loading…
Reference in New Issue
Block a user