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