Compare commits
24 Commits
5c619b26c0
...
c9e7b33d25
Author | SHA1 | Date | |
---|---|---|---|
|
c9e7b33d25 | ||
|
01d63e5ee0 | ||
|
a1a6b98639 | ||
|
3191ee83d4 | ||
|
d168b9b580 | ||
|
83d8924517 | ||
|
4b46b2f0ab | ||
|
2becc0afb9 | ||
|
32fa9ee257 | ||
|
4f59ddd731 | ||
|
ba360d6b4f | ||
|
5a43859e56 | ||
|
723d2fc9c3 | ||
|
392735cf99 | ||
|
90ab588133 | ||
|
64df3b882f | ||
|
b719e3227b | ||
|
82e37f9b91 | ||
|
1e588900bb | ||
|
4777a2a3a5 | ||
|
99b8d0f633 | ||
|
ab626f8f9a | ||
|
ce9a1aeba7 | ||
|
b37d3513f6 |
|
@ -699,7 +699,7 @@ export default class Sandbox {
|
|||
stack: [],
|
||||
};
|
||||
}
|
||||
let result = this.executeSync(this.ast, 0);
|
||||
const result = this.executeSync(this.ast, 0);
|
||||
const stepResult = {async: false, done: false, value: undefined};
|
||||
switch (result.yield) {
|
||||
case YIELD_PROMISE: {
|
||||
|
|
|
@ -121,20 +121,7 @@ export default class Component {
|
|||
}
|
||||
|
||||
async insertMany(entities) {
|
||||
const creating = [];
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const [entityId, values] = entities[i];
|
||||
if (!this.get(entityId)) {
|
||||
creating.push([entityId, values]);
|
||||
}
|
||||
else {
|
||||
const instance = this.get(entityId);
|
||||
for (const i in values) {
|
||||
instance[i] = values[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.createMany(creating);
|
||||
await this.createMany(entities);
|
||||
}
|
||||
|
||||
instanceFromSchema() {
|
||||
|
@ -152,12 +139,22 @@ export default class Component {
|
|||
}
|
||||
}
|
||||
destroy() {}
|
||||
toNet() {
|
||||
return Component.constructor.filterDefaults(this);
|
||||
toNet(recipient, data) {
|
||||
return data || Component.constructor.filterDefaults(this);
|
||||
}
|
||||
toJSON() {
|
||||
return Component.constructor.filterDefaults(this);
|
||||
}
|
||||
update(values) {
|
||||
for (const key in values) {
|
||||
if (concrete.properties[key]) {
|
||||
this[`$$${key}`] = values[key];
|
||||
}
|
||||
else {
|
||||
this[key] = values[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const properties = {};
|
||||
properties.entity = {
|
||||
|
@ -216,4 +213,11 @@ export default class Component {
|
|||
return this.constructor.schema.sizeOf(this.get(entityId));
|
||||
}
|
||||
|
||||
async updateMany(entities) {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const [entityId, values] = entities[i];
|
||||
this.get(entityId).update(values);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -167,7 +167,7 @@ export default class Collider extends Component {
|
|||
}
|
||||
closest(aabb) {
|
||||
const entity = ecs.get(this.entity);
|
||||
return Array.from(ecs.system('Colliders').within(aabb))
|
||||
return Array.from(ecs.system('MaintainColliderHash').within(aabb))
|
||||
.filter((other) => other !== entity)
|
||||
.sort(({Position: l}, {Position: r}) => {
|
||||
return distance(entity.Position, l) > distance(entity.Position, r) ? -1 : 1;
|
||||
|
|
|
@ -1,14 +1,39 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Emitter extends Component {
|
||||
import Emitter from '@/particles/emitter.js';
|
||||
import {Ticker as TickerPromise} from '@/util/promise.js';
|
||||
|
||||
export default class EmitterComponent extends Component {
|
||||
instanceFromSchema() {
|
||||
const Component = this;
|
||||
const {ecs} = this;
|
||||
return class EmitterInstance extends super.instanceFromSchema() {
|
||||
emitting = {};
|
||||
id = 0;
|
||||
emit(specification) {
|
||||
if (specification.server) {
|
||||
const {Ticker} = ecs.get(1);
|
||||
if (Ticker) {
|
||||
const emitter = new Emitter(ecs);
|
||||
const promise = new Promise((resolve) => {
|
||||
emitter.emit().onEnd(resolve);
|
||||
});
|
||||
Ticker.add(
|
||||
new TickerPromise(
|
||||
(resolve) => {
|
||||
promise.then(resolve);
|
||||
},
|
||||
(elapsed) => {
|
||||
this.emitter.tick(elapsed);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Component.markChange(this.entity, 'emit', {[this.id++]: specification});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
mergeDiff(original, update) {
|
||||
|
|
|
@ -115,7 +115,7 @@ class ItemProxy {
|
|||
}
|
||||
|
||||
export default class Inventory extends Component {
|
||||
async insertMany(entities) {
|
||||
async updateMany(entities) {
|
||||
for (const [id, {cleared, given, qtyUpdated, swapped}] of entities) {
|
||||
const instance = this.get(id);
|
||||
const {$$items, slots} = instance;
|
||||
|
@ -137,12 +137,14 @@ export default class Inventory extends Component {
|
|||
}
|
||||
}
|
||||
if (swapped) {
|
||||
for (const [l, r] of swapped) {
|
||||
for (const [l, otherEntityId, r] of swapped) {
|
||||
const otherInstance = this.get(otherEntityId);
|
||||
const {$$items: $$otherItems, slots: otherSlots} = otherInstance;
|
||||
const tmp = [$$items[l], slots[l]];
|
||||
[$$items[l], slots[l]] = [$$items[r], slots[r]];
|
||||
[$$items[r], slots[r]] = tmp;
|
||||
if ($$items[r]) {
|
||||
$$items[r].slot = r;
|
||||
[$$items[l], slots[l]] = [$$otherItems[r], otherSlots[r]];
|
||||
[$$otherItems[r], otherSlots[r]] = tmp;
|
||||
if ($$otherItems[r]) {
|
||||
$$otherItems[r].slot = r;
|
||||
}
|
||||
if ($$items[l]) {
|
||||
$$items[l].slot = l;
|
||||
|
@ -150,7 +152,7 @@ export default class Inventory extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
await super.insertMany(entities);
|
||||
await super.updateMany(entities);
|
||||
for (const [id, {slots}] of entities) {
|
||||
if (slots) {
|
||||
const instance = this.get(id);
|
||||
|
@ -189,18 +191,26 @@ export default class Inventory extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
swapSlots(l, r) {
|
||||
swapSlots(l, OtherInventory, r) {
|
||||
const {$$items, slots} = this;
|
||||
const {$$items: $$otherItems, slots: otherSlots} = OtherInventory;
|
||||
const tmp = [$$items[l], slots[l]];
|
||||
[$$items[l], slots[l]] = [$$items[r], slots[r]];
|
||||
[$$items[r], slots[r]] = tmp;
|
||||
[$$items[l], slots[l]] = [$$otherItems[r], otherSlots[r]];
|
||||
[$$otherItems[r], otherSlots[r]] = tmp;
|
||||
if (undefined === slots[l]) {
|
||||
delete slots[l];
|
||||
}
|
||||
if (undefined === slots[r]) {
|
||||
delete slots[r];
|
||||
if (undefined === otherSlots[r]) {
|
||||
delete otherSlots[r];
|
||||
}
|
||||
Component.markChange(this.entity, 'swapped', [[l, r]]);
|
||||
Component.markChange(this.entity, 'swapped', [[l, OtherInventory.entity, r]]);
|
||||
Component.markChange(OtherInventory.entity, 'swapped', [[r, this.entity, l]]);
|
||||
}
|
||||
toNet(recipient, data) {
|
||||
if (recipient.id !== this.entity && this !== recipient.Player.openInventory) {
|
||||
return {};
|
||||
}
|
||||
return super.toNet(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import Component from '@/ecs/component.js';
|
|||
|
||||
export default class Light extends Component {
|
||||
static properties = {
|
||||
brightness: {defaultValue: 1, type: 'float32'},
|
||||
radius: {type: 'uint8'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,51 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Player extends Component {}
|
||||
const State = {
|
||||
CLOSED: 0,
|
||||
OPENING: 1,
|
||||
OPEN: 2,
|
||||
CLOSING: 3,
|
||||
};
|
||||
|
||||
export default class Player extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
return class PlayerInstance extends super.instanceFromSchema() {
|
||||
$$openInventory;
|
||||
$$openInventoryState = State.CLOSED;
|
||||
closeInventory() {
|
||||
this.$$openInventoryState = State.CLOSING;
|
||||
}
|
||||
updateAttachments(update) {
|
||||
if (!this.$$openInventory) {
|
||||
return;
|
||||
}
|
||||
if (![State.OPENING, State.CLOSING].includes(this.$$openInventoryState)) {
|
||||
return;
|
||||
}
|
||||
if (!ecs.get(this.$$openInventory.entity)) {
|
||||
return;
|
||||
}
|
||||
if (!update[this.$$openInventory.entity]) {
|
||||
update[this.$$openInventory.entity] = {};
|
||||
}
|
||||
if (this.$$openInventoryState === State.OPENING) {
|
||||
update[this.$$openInventory.entity].Inventory = this.$$openInventory.toNet(ecs.get(this.entity))
|
||||
this.$$openInventoryState = State.OPEN;
|
||||
}
|
||||
else {
|
||||
update[this.$$openInventory.entity].Inventory = {closed: true};
|
||||
this.$$openInventory = undefined;
|
||||
this.$$openInventoryState = State.CLOSED;
|
||||
}
|
||||
}
|
||||
get openInventory() {
|
||||
return this.$$openInventory;
|
||||
}
|
||||
set openInventory(Inventory) {
|
||||
this.$$openInventoryState = State.OPENING;
|
||||
this.$$openInventory = Inventory;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,23 @@ import Component from '@/ecs/component.js';
|
|||
export default class Sprite extends Component {
|
||||
instanceFromSchema() {
|
||||
return class SpriteInstance extends super.instanceFromSchema() {
|
||||
$$anchor = {x: 0.5, y: 0.5};
|
||||
$$scale = {x: 1, y: 1};
|
||||
$$sourceJson = {};
|
||||
get anchor() {
|
||||
return {x: this.anchorX, y: this.anchorY};
|
||||
return this.$$anchor;
|
||||
}
|
||||
get anchorX() {
|
||||
return this.$$anchor.x;
|
||||
}
|
||||
set anchorX(anchorX) {
|
||||
this.$$anchor = {x: anchorX, y: this.anchorY};
|
||||
}
|
||||
get anchorY() {
|
||||
return this.$$anchor.y;
|
||||
}
|
||||
set anchorY(anchorY) {
|
||||
this.$$anchor = {x: this.anchorX, y: anchorY};
|
||||
}
|
||||
get animation() {
|
||||
return super.animation;
|
||||
|
@ -56,18 +70,41 @@ export default class Sprite extends Component {
|
|||
return this.$$sourceJson.meta.rotation;
|
||||
}
|
||||
get scale() {
|
||||
return {x: this.scaleX, y: this.scaleY};
|
||||
return this.$$scale;
|
||||
}
|
||||
toNet() {
|
||||
get scaleX() {
|
||||
return this.$$scale.x;
|
||||
}
|
||||
set scaleX(scaleX) {
|
||||
this.$$scale = {x: scaleX, y: this.scaleY};
|
||||
}
|
||||
get scaleY() {
|
||||
return this.$$scale.y;
|
||||
}
|
||||
set scaleY(scaleY) {
|
||||
this.$$scale = {x: this.scaleX, y: scaleY};
|
||||
}
|
||||
get size() {
|
||||
if (!this.$$sourceJson.frames) {
|
||||
return {x: 16, y: 16};
|
||||
}
|
||||
const frame = this.animation
|
||||
? this.$$sourceJson.animations[this.animation][this.frame]
|
||||
: '';
|
||||
return this.$$sourceJson.frames[frame].sourceSize;
|
||||
}
|
||||
toNet(recipient, data) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {elapsed, ...rest} = super.toNet();
|
||||
const {elapsed, ...rest} = super.toNet(recipient, data);
|
||||
return rest;
|
||||
}
|
||||
};
|
||||
}
|
||||
async load(instance) {
|
||||
if (instance.source) {
|
||||
instance.$$sourceJson = await this.ecs.readJson(instance.source);
|
||||
}
|
||||
}
|
||||
markChange(entityId, key, value) {
|
||||
if ('elapsed' === key) {
|
||||
return;
|
||||
|
@ -86,5 +123,6 @@ export default class Sprite extends Component {
|
|||
scaleY: {defaultValue: 1, type: 'float32'},
|
||||
source: {type: 'string'},
|
||||
speed: {type: 'float32'},
|
||||
tint: {type: 'uint32'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,29 +4,23 @@ export default class Ticking extends Component {
|
|||
instanceFromSchema() {
|
||||
return class TickingInstance extends super.instanceFromSchema() {
|
||||
|
||||
$$finished = [];
|
||||
$$tickers = [];
|
||||
|
||||
add(ticker) {
|
||||
this.$$tickers.push(ticker);
|
||||
ticker.then(() => {
|
||||
this.$$finished.push(ticker);
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.$$finished = [];
|
||||
this.$$tickers = [];
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const ticker of this.$$finished) {
|
||||
this.$$tickers.splice(
|
||||
this.$$tickers.indexOf(ticker),
|
||||
1,
|
||||
);
|
||||
});
|
||||
}
|
||||
this.$$finished = [];
|
||||
|
||||
destroy() {
|
||||
this.$$tickers = [];
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const ticker of this.$$tickers) {
|
||||
ticker.tick(elapsed);
|
||||
}
|
||||
|
|
|
@ -164,7 +164,7 @@ export default class TileLayers extends Component {
|
|||
}
|
||||
return super.createMany(entities);
|
||||
}
|
||||
async insertMany(entities) {
|
||||
async updateMany(entities) {
|
||||
for (const [, {layers}] of entities) {
|
||||
if (layers) {
|
||||
for (const layer of layers) {
|
||||
|
@ -179,7 +179,7 @@ export default class TileLayers extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
await super.insertMany(entities);
|
||||
await super.updateMany(entities);
|
||||
for (const [id, {layerChange}] of entities) {
|
||||
if (layerChange) {
|
||||
const component = this.get(id);
|
||||
|
|
22
app/ecs/components/ttl.js
Normal file
22
app/ecs/components/ttl.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
|
||||
export default class Ttl extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
return class TtlInstance extends super.instanceFromSchema() {
|
||||
$$elapsed = 0;
|
||||
$$reset() {
|
||||
this.$$elapsed = 0;
|
||||
}
|
||||
tick(elapsed) {
|
||||
this.$$elapsed += elapsed;
|
||||
if (this.$$elapsed >= this.ttl) {
|
||||
ecs.destroy(this.entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static properties = {
|
||||
ttl: {type: 'float32'},
|
||||
};
|
||||
}
|
|
@ -23,6 +23,9 @@ export default class Vulnerable extends Component {
|
|||
id = 0;
|
||||
Types = DamageTypes;
|
||||
damage(specification) {
|
||||
if (this.isInvulnerable) {
|
||||
return;
|
||||
}
|
||||
const {Alive} = Component.ecs.get(this.entity);
|
||||
if (Alive) {
|
||||
switch (specification.type) {
|
||||
|
@ -31,8 +34,15 @@ export default class Vulnerable extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
Component.markChange(this.entity, 'damage', {[this.id++]: specification});
|
||||
Component.markChange(
|
||||
this.entity,
|
||||
'damage',
|
||||
{[`${this.entity}-${this.id++}`]: specification},
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
static properties = {
|
||||
isInvulnerable: {type: 'uint8'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {Encoder, Decoder} from '@msgpack/msgpack';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
import {withResolvers} from '@/util/promise.js';
|
||||
import {Ticker, withResolvers} from '@/util/promise.js';
|
||||
import Script from '@/util/script.js';
|
||||
|
||||
import EntityFactory from './entity-factory.js';
|
||||
|
@ -59,6 +59,7 @@ export default class Ecs {
|
|||
async apply(patch) {
|
||||
const creating = [];
|
||||
const destroying = [];
|
||||
const inserting = [];
|
||||
const removing = [];
|
||||
const updating = [];
|
||||
for (const entityIdString in patch) {
|
||||
|
@ -82,7 +83,23 @@ export default class Ecs {
|
|||
removing.push([entityId, componentsToRemove]);
|
||||
}
|
||||
if (this.$$entities[entityId]) {
|
||||
updating.push([entityId, componentsToUpdate]);
|
||||
const entity = this.$$entities[entityId];
|
||||
const entityInserts = {};
|
||||
const entityUpdates = {};
|
||||
for (const componentName in componentsToUpdate) {
|
||||
if (entity[componentName]) {
|
||||
entityUpdates[componentName] = componentsToUpdate[componentName];
|
||||
}
|
||||
else {
|
||||
entityInserts[componentName] = componentsToUpdate[componentName];
|
||||
}
|
||||
}
|
||||
if (Object.keys(entityInserts).length > 0) {
|
||||
inserting.push([entityId, entityInserts]);
|
||||
}
|
||||
if (Object.keys(entityUpdates).length > 0) {
|
||||
updating.push([entityId, entityUpdates]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
creating.push([entityId, componentsToUpdate]);
|
||||
|
@ -91,14 +108,19 @@ export default class Ecs {
|
|||
if (destroying.length > 0) {
|
||||
this.destroyMany(destroying);
|
||||
}
|
||||
if (updating.length > 0) {
|
||||
await this.insertMany(updating);
|
||||
const promises = [];
|
||||
if (inserting.length > 0) {
|
||||
promises.push(this.insertMany(inserting));
|
||||
}
|
||||
if (removing.length > 0) {
|
||||
this.removeMany(removing);
|
||||
if (updating.length > 0) {
|
||||
promises.push(this.updateMany(updating));
|
||||
}
|
||||
if (creating.length > 0) {
|
||||
await this.createManySpecific(creating);
|
||||
promises.push(this.createManySpecific(creating));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
if (removing.length > 0) {
|
||||
this.removeMany(removing);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -462,8 +484,8 @@ export default class Ecs {
|
|||
}
|
||||
}
|
||||
const destroying = new Set();
|
||||
for (const [entityId, {promises, resolvers}] of this.$$destructionDependencies) {
|
||||
if (0 === promises.size && resolvers) {
|
||||
for (const [entityId, {promises}] of this.$$destructionDependencies) {
|
||||
if (0 === promises.size) {
|
||||
destroying.add(entityId);
|
||||
}
|
||||
}
|
||||
|
@ -495,4 +517,25 @@ export default class Ecs {
|
|||
};
|
||||
}
|
||||
|
||||
async updateMany(entities) {
|
||||
const updating = {};
|
||||
const unique = new Set();
|
||||
for (const [entityId, components] of entities) {
|
||||
this.rebuild(entityId);
|
||||
for (const componentName in components) {
|
||||
if (!updating[componentName]) {
|
||||
updating[componentName] = [];
|
||||
}
|
||||
updating[componentName].push([entityId, components[componentName]]);
|
||||
}
|
||||
unique.add(entityId);
|
||||
}
|
||||
const promises = [];
|
||||
for (const componentName in updating) {
|
||||
promises.push(this.Components[componentName].updateMany(updating[componentName]));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
this.reindex(unique.values());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -43,14 +43,23 @@ export default class EntityFactory {
|
|||
};
|
||||
}
|
||||
Object.defineProperties(Entity.prototype, properties);
|
||||
Entity.prototype.updateAttachments = new Function('update', `
|
||||
${
|
||||
sorted
|
||||
.filter((componentName) => (
|
||||
Components[componentName].Instance.prototype.updateAttachments
|
||||
))
|
||||
.map((type) => `this.${type}.updateAttachments(update)`).join('; ')
|
||||
}
|
||||
`);
|
||||
Entity.prototype.toJSON = new Function('', `
|
||||
return {
|
||||
${sorted.map((type) => `${type}: this.${type}.toJSON()`).join(', ')}
|
||||
};
|
||||
`);
|
||||
Entity.prototype.toNet = new Function('', `
|
||||
Entity.prototype.toNet = new Function('recipient', `
|
||||
return {
|
||||
${sorted.map((type) => `${type}: this.${type}.toNet()`).join(', ')}
|
||||
${sorted.map((type) => `${type}: this.${type}.toNet(recipient)`).join(', ')}
|
||||
};
|
||||
`);
|
||||
walk.class = Entity;
|
||||
|
|
|
@ -20,7 +20,7 @@ export default class Attract extends System {
|
|||
};
|
||||
let s = Magnet.strength;
|
||||
s = s * s;
|
||||
for (const other of this.ecs.system('Colliders').within(aabb)) {
|
||||
for (const other of this.ecs.system('MaintainColliderHash').within(aabb)) {
|
||||
if (other === entity || !other.Magnetic) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,9 @@ export default class ClampPositions extends System {
|
|||
|
||||
tick() {
|
||||
const {AreaSize} = this.ecs.get(1);
|
||||
if (!AreaSize) {
|
||||
return;
|
||||
}
|
||||
for (const {Position} of this.ecs.changed(['Position'])) {
|
||||
if (Position.x < 0) {
|
||||
Position.x = 0;
|
||||
|
|
|
@ -1,53 +1,15 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
import SpatialHash from '@/util/spatial-hash.js';
|
||||
|
||||
export default class Colliders extends System {
|
||||
|
||||
hash;
|
||||
|
||||
deindex(entities) {
|
||||
super.deindex(entities);
|
||||
for (const id of entities) {
|
||||
this.hash.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
static get priority() {
|
||||
return {
|
||||
after: 'IntegratePhysics',
|
||||
after: 'MaintainColliderHash',
|
||||
};
|
||||
}
|
||||
|
||||
reindex(entities) {
|
||||
for (const id of entities) {
|
||||
if (1 === id) {
|
||||
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
|
||||
}
|
||||
}
|
||||
super.reindex(entities);
|
||||
for (const id of entities) {
|
||||
this.updateHash(this.ecs.get(id));
|
||||
}
|
||||
}
|
||||
|
||||
updateHash(entity) {
|
||||
if (!entity.Collider) {
|
||||
return;
|
||||
}
|
||||
this.hash.update(entity.Collider.aabb, entity.id);
|
||||
}
|
||||
|
||||
tick() {
|
||||
const checked = new Map();
|
||||
for (const entity of this.ecs.changed(['Direction'])) {
|
||||
if (!entity.Collider) {
|
||||
continue;
|
||||
}
|
||||
entity.Collider.updateAabbs();
|
||||
}
|
||||
for (const entity of this.ecs.changed(['Position'])) {
|
||||
this.updateHash(entity);
|
||||
}
|
||||
for (const entity of this.ecs.changed(['Position'])) {
|
||||
if (!entity.Collider) {
|
||||
continue;
|
||||
|
@ -55,7 +17,7 @@ export default class Colliders extends System {
|
|||
if (!checked.has(entity)) {
|
||||
checked.set(entity, new Set());
|
||||
}
|
||||
const within = this.within(entity.Collider.aabb);
|
||||
const within = this.ecs.system('MaintainColliderHash').within(entity.Collider.aabb);
|
||||
for (const other of within) {
|
||||
if (entity === other || !other.Collider) {
|
||||
continue;
|
||||
|
@ -77,12 +39,4 @@ export default class Colliders extends System {
|
|||
}
|
||||
}
|
||||
|
||||
within(query) {
|
||||
const within = new Set();
|
||||
for (const id of this.hash.within(query)) {
|
||||
within.add(this.ecs.get(id));
|
||||
}
|
||||
return within;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
24
app/ecs/systems/inventory-closer.js
Normal file
24
app/ecs/systems/inventory-closer.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
import {distance} from '@/util/math.js';
|
||||
|
||||
export default class InventoryCloser extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Player'],
|
||||
};
|
||||
}
|
||||
|
||||
tick() {
|
||||
for (const {Player, Position} of this.select('default')) {
|
||||
const {openInventory} = Player;
|
||||
if (openInventory) {
|
||||
const {Position: inventoryPosition} = this.ecs.get(openInventory.entity);
|
||||
if (distance(Position, inventoryPosition) > 64) {
|
||||
Player.closeInventory();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
17
app/ecs/systems/kill-perishable.js
Normal file
17
app/ecs/systems/kill-perishable.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
|
||||
export default class KillPerishable extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Ttl'],
|
||||
};
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const {Ttl} of this.select('default')) {
|
||||
Ttl.tick(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
63
app/ecs/systems/maintain-collider-hash.js
Normal file
63
app/ecs/systems/maintain-collider-hash.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import {System} from '@/ecs/index.js';
|
||||
import SpatialHash from '@/util/spatial-hash.js';
|
||||
|
||||
export default class MaintainColliderHash extends System {
|
||||
|
||||
hash;
|
||||
|
||||
deindex(entities) {
|
||||
super.deindex(entities);
|
||||
for (const id of entities) {
|
||||
this.hash.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
static get priority() {
|
||||
return {
|
||||
after: 'IntegratePhysics',
|
||||
};
|
||||
}
|
||||
|
||||
reindex(entities) {
|
||||
for (const id of entities) {
|
||||
if (1 === id) {
|
||||
const {AreaSize} = this.ecs.get(1);
|
||||
if (AreaSize) {
|
||||
this.hash = new SpatialHash(AreaSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
super.reindex(entities);
|
||||
for (const id of entities) {
|
||||
this.updateHash(this.ecs.get(id));
|
||||
}
|
||||
}
|
||||
|
||||
updateHash(entity) {
|
||||
if (!entity.Collider || !this.hash) {
|
||||
return;
|
||||
}
|
||||
this.hash.update(entity.Collider.aabb, entity.id);
|
||||
}
|
||||
|
||||
tick() {
|
||||
for (const entity of this.ecs.changed(['Direction'])) {
|
||||
if (!entity.Collider) {
|
||||
continue;
|
||||
}
|
||||
entity.Collider.updateAabbs();
|
||||
}
|
||||
for (const entity of this.ecs.changed(['Position'])) {
|
||||
this.updateHash(entity);
|
||||
}
|
||||
}
|
||||
|
||||
within(query) {
|
||||
const within = new Set();
|
||||
for (const id of this.hash.within(query)) {
|
||||
within.add(this.ecs.get(id));
|
||||
}
|
||||
return within;
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,9 @@ export default class VisibleAabbs extends System {
|
|||
reindex(entities) {
|
||||
for (const id of entities) {
|
||||
if (1 === id) {
|
||||
const {x, y} = this.ecs.get(1).AreaSize;
|
||||
const {AreaSize} = this.ecs.get(1)
|
||||
if (AreaSize) {
|
||||
const {x, y} = AreaSize;
|
||||
if (
|
||||
!this.hash ||
|
||||
(
|
||||
|
@ -33,6 +35,7 @@ export default class VisibleAabbs extends System {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.reindex(entities);
|
||||
for (const id of entities) {
|
||||
this.updateHash(this.ecs.get(id));
|
||||
|
@ -40,6 +43,9 @@ export default class VisibleAabbs extends System {
|
|||
}
|
||||
|
||||
updateHash(entity) {
|
||||
if (!this.hash) {
|
||||
return;
|
||||
}
|
||||
if (!entity.VisibleAabb) {
|
||||
this.hash.remove(entity.id);
|
||||
return;
|
||||
|
@ -53,10 +59,7 @@ export default class VisibleAabbs extends System {
|
|||
if (VisibleAabb) {
|
||||
let size = undefined;
|
||||
if (Sprite) {
|
||||
const frame = Sprite.animation
|
||||
? Sprite.$$sourceJson.animations[Sprite.animation][Sprite.frame]
|
||||
: '';
|
||||
size = Sprite.$$sourceJson.frames[frame].sourceSize;
|
||||
size = Sprite.size;
|
||||
}
|
||||
/* v8 ignore next 3 */
|
||||
if (!size) {
|
||||
|
|
93
app/particles/emitter.js
Normal file
93
app/particles/emitter.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
import K from 'kefir';
|
||||
|
||||
export default class Emitter {
|
||||
constructor(ecs) {
|
||||
this.ecs = ecs;
|
||||
this.scheduled = [];
|
||||
}
|
||||
async allocate({entity, shape}) {
|
||||
const allocated = this.ecs.get(await this.ecs.create(entity));
|
||||
if (shape) {
|
||||
switch (shape.type) {
|
||||
case 'filledRect': {
|
||||
allocated.Position.x += Math.random() * shape.payload.width - (shape.payload.width / 2);
|
||||
allocated.Position.y += Math.random() * shape.payload.height - (shape.payload.height / 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return allocated;
|
||||
}
|
||||
emit(particle) {
|
||||
particle = {
|
||||
...particle,
|
||||
entity: {
|
||||
Position: {},
|
||||
Sprite: {},
|
||||
VisibleAabb: {},
|
||||
...particle.entity,
|
||||
},
|
||||
}
|
||||
let {count = 1} = particle;
|
||||
const {frequency = 0} = particle;
|
||||
const stream = K.stream((emitter) => {
|
||||
if (0 === frequency) {
|
||||
const promises = [];
|
||||
for (let i = 0; i < count; ++i) {
|
||||
promises.push(
|
||||
this.allocate(particle)
|
||||
.then((entity) => {
|
||||
emitter.emit(entity);
|
||||
}),
|
||||
);
|
||||
}
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
emitter.end();
|
||||
});
|
||||
return;
|
||||
}
|
||||
const promise = this.allocate(particle)
|
||||
.then((entity) => {
|
||||
emitter.emit(entity);
|
||||
});
|
||||
count -= 1;
|
||||
if (0 === count) {
|
||||
promise.then(() => {
|
||||
emitter.end();
|
||||
});
|
||||
return;
|
||||
}
|
||||
const promises = [promise];
|
||||
let accumulated = 0;
|
||||
const scheduled = (elapsed) => {
|
||||
accumulated += elapsed;
|
||||
while (accumulated > frequency && count > 0) {
|
||||
promises.push(
|
||||
this.allocate(particle)
|
||||
.then((entity) => {
|
||||
emitter.emit(entity);
|
||||
}),
|
||||
);
|
||||
accumulated -= frequency;
|
||||
count -= 1;
|
||||
}
|
||||
if (0 === count) {
|
||||
this.scheduled.splice(this.scheduled.indexOf(scheduled), 1);
|
||||
Promise.all(promises).then(() => {
|
||||
emitter.end();
|
||||
});
|
||||
}
|
||||
};
|
||||
this.scheduled.push(scheduled);
|
||||
});
|
||||
return stream;
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const ticker of this.scheduled) {
|
||||
ticker(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
52
app/particles/emitter.test.js
Normal file
52
app/particles/emitter.test.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import Components from '@/ecs/components/index.js';
|
||||
import Ecs from '@/ecs/ecs.js';
|
||||
|
||||
import Emitter from './emitter.js';
|
||||
|
||||
test('emits particles at once', async () => {
|
||||
const ecs = new Ecs({
|
||||
Components,
|
||||
});
|
||||
const emitter = new Emitter(ecs);
|
||||
const stream = emitter.emit({
|
||||
count: 5,
|
||||
frequency: 0,
|
||||
entity: {},
|
||||
});
|
||||
expect(await stream.scan((r) => r + 1, 0).toPromise()).to.equal(5);
|
||||
});
|
||||
|
||||
test('emits particles over time', async () => {
|
||||
const ecs = new Ecs({
|
||||
Components,
|
||||
});
|
||||
const emitter = new Emitter(ecs);
|
||||
const stream = emitter.emit({
|
||||
count: 2,
|
||||
frequency: 0.1,
|
||||
});
|
||||
const current = stream.toProperty();
|
||||
expect(await new Promise((resolve) => {
|
||||
current.onValue(resolve);
|
||||
}))
|
||||
.to.deep.include({id: 1});
|
||||
expect(ecs.get(1))
|
||||
.to.not.be.undefined;
|
||||
expect(ecs.get(2))
|
||||
.to.be.undefined;
|
||||
emitter.tick(0.06);
|
||||
expect(await new Promise((resolve) => {
|
||||
current.onValue(resolve);
|
||||
}))
|
||||
.to.deep.include({id: 1});
|
||||
emitter.tick(0.06);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(await new Promise((resolve) => {
|
||||
current.onValue(resolve);
|
||||
}))
|
||||
.to.deep.include({id: 2});
|
||||
expect(ecs.get(2))
|
||||
.to.not.be.undefined;
|
||||
});
|
|
@ -11,6 +11,7 @@ export default class ClientEcs extends Ecs {
|
|||
constructor(specification) {
|
||||
super(specification);
|
||||
[
|
||||
'MaintainColliderHash',
|
||||
].forEach((defaultSystem) => {
|
||||
const System = this.system(defaultSystem);
|
||||
if (System) {
|
||||
|
@ -22,7 +23,7 @@ export default class ClientEcs extends Ecs {
|
|||
if (!cache.has(uri)) {
|
||||
const {promise, resolve, reject} = withResolvers();
|
||||
cache.set(uri, promise);
|
||||
fetch(new URL(uri, window.location.origin))
|
||||
fetch(new URL(uri, location.origin))
|
||||
.then(async (response) => {
|
||||
resolve(response.ok ? response.arrayBuffer() : new ArrayBuffer(0));
|
||||
})
|
||||
|
|
|
@ -10,9 +10,7 @@ import styles from './devtools.module.css';
|
|||
import Tiles from './devtools/tiles.jsx';
|
||||
|
||||
export default function Devtools({
|
||||
applyFilters,
|
||||
eventsChannel,
|
||||
setApplyFilters,
|
||||
}) {
|
||||
const [ecs] = useEcs();
|
||||
const [mainEntity] = useMainEntity();
|
||||
|
@ -34,16 +32,6 @@ export default function Devtools({
|
|||
<div className={styles.dashboard}>
|
||||
<form>
|
||||
<div className={styles.engineBar}>
|
||||
<label>
|
||||
<span>Apply filters</span>
|
||||
<input
|
||||
checked={applyFilters}
|
||||
onChange={() => {
|
||||
setApplyFilters(!applyFilters);
|
||||
}}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<pre><code><small>{mainEntityJson}</small></code></pre>
|
||||
|
|
|
@ -1,54 +1,27 @@
|
|||
import styles from './bag.module.css';
|
||||
import Slot from './slot.jsx';
|
||||
|
||||
import Grid from './grid.jsx';
|
||||
|
||||
/**
|
||||
* Inventory bag. 10-40 slots of inventory.
|
||||
*/
|
||||
export default function Bag({
|
||||
isInventoryOpen,
|
||||
onActivate,
|
||||
slots,
|
||||
}) {
|
||||
const Slots = slots.map((slot, i) => (
|
||||
<div
|
||||
className={
|
||||
[styles.slotWrapper]
|
||||
.filter(Boolean).join(' ')
|
||||
}
|
||||
key={i}
|
||||
>
|
||||
<Slot
|
||||
icon={slot?.icon}
|
||||
// onMouseDown={(event) => {
|
||||
// onActivate(i)
|
||||
// event.stopPropagation();
|
||||
// }}
|
||||
// onMouseUp={(event) => {
|
||||
// event.stopPropagation();
|
||||
// }}
|
||||
// onDragOver={(event) => {
|
||||
// event.preventDefault();
|
||||
// }}
|
||||
// onDragStart={(event) => {
|
||||
// if (!slot) {
|
||||
// event.preventDefault();
|
||||
// }
|
||||
// event.dataTransfer.setData('silphius/item', i);
|
||||
// onActivate(i);
|
||||
// }}
|
||||
// onDrop={(event) => {
|
||||
// event.preventDefault();
|
||||
// onActivate(i);
|
||||
// }}
|
||||
qty={slot?.qty}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div
|
||||
className={styles.bag}
|
||||
style={isInventoryOpen ? {transition: 'none'} : {left: '-440px'}}
|
||||
style={isInventoryOpen ? {transition: 'opacity 50ms'} : {opacity: 0, left: '-440px'}}
|
||||
>
|
||||
{Slots}
|
||||
<Grid
|
||||
color="rgba(02, 02, 28, 0.6)"
|
||||
columns={10}
|
||||
label="Bag"
|
||||
onActivate={onActivate}
|
||||
slots={slots}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,31 +1,7 @@
|
|||
.bag {
|
||||
align-self: left;
|
||||
--border: calc(var(--unit) * 3px);
|
||||
background-color: rgba(02, 02, 28, 0.6);
|
||||
border: var(--border) solid #444444;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
left: calc(var(--unit) * 20px);
|
||||
line-height: 0;
|
||||
left: 20px;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
top: calc(var(--unit) * 90px);
|
||||
transition: left 150ms;
|
||||
max-width: 430.5px;
|
||||
}
|
||||
|
||||
.slotWrapper {
|
||||
border: var(--border) solid #999999;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
padding: 0;
|
||||
&:not(:nth-child(10n)) {
|
||||
border-right: none;
|
||||
}
|
||||
&:not(:nth-last-of-type(-n+10)) {
|
||||
border-bottom: none;
|
||||
}
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
top: 74px;
|
||||
transition: left 150ms, opacity 200ms;
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
import {memo, useEffect, useState} from 'react';
|
||||
|
||||
import {DamageTypes} from '@/ecs/components/vulnerable.js';
|
||||
|
||||
import styles from './damage.module.css';
|
||||
|
||||
const damageTypeMap = {
|
||||
[DamageTypes.PAIN]: styles.pain,
|
||||
[DamageTypes.HEALING]: styles.healing,
|
||||
[DamageTypes.MANA]: styles.mana,
|
||||
};
|
||||
|
||||
function Damage({
|
||||
camera,
|
||||
damage,
|
||||
scale,
|
||||
zIndex,
|
||||
}) {
|
||||
const [randomness] = useState({
|
||||
radians: Math.random() * (Math.PI / 16) - (Math.PI / 32),
|
||||
x: 1 * (Math.random() - 0.5),
|
||||
y: Math.random(),
|
||||
})
|
||||
useEffect(() => {
|
||||
const handle = setTimeout(() => {
|
||||
damage.onClose();
|
||||
}, 1_500);
|
||||
return () => {
|
||||
clearTimeout(handle);
|
||||
};
|
||||
}, [damage]);
|
||||
const {amount, position} = damage;
|
||||
return (
|
||||
<div
|
||||
className={styles.damage}
|
||||
style={{
|
||||
'--magnitude': Math.max(1, Math.floor(Math.log10(Math.abs(amount)))),
|
||||
'--positionX': `${position.x * scale - camera.x}px`,
|
||||
'--positionY': `${position.y * scale - camera.y}px`,
|
||||
'--randomnessX': randomness.x,
|
||||
'--randomnessY': randomness.y,
|
||||
'--shimmer': damageTypeMap[damage.type],
|
||||
rotate: `${randomness.radians}rad`,
|
||||
zIndex,
|
||||
}}
|
||||
>
|
||||
<p>{Math.abs(amount)}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Damage);
|
|
@ -13,11 +13,6 @@
|
|||
inherits: false;
|
||||
syntax: '<number>';
|
||||
}
|
||||
@property --shimmer {
|
||||
initial-value: '';
|
||||
inherits: false;
|
||||
syntax: '<custom-ident>';
|
||||
}
|
||||
@property --offsetX {
|
||||
initial-value: 0;
|
||||
inherits: false;
|
||||
|
@ -47,12 +42,6 @@
|
|||
--scale: 0.35;
|
||||
--background: hsl(var(--hue) 100% 12.5%);
|
||||
--foreground: hsl(var(--hue) 100% 50%);
|
||||
animation:
|
||||
fade 1.5s linear forwards,
|
||||
grow 1.5s ease-in forwards,
|
||||
move 1.5s cubic-bezier(0.5, 1, 0.89, 1),
|
||||
var(--shimmer) 300ms infinite cubic-bezier(0.87, 0, 0.13, 1)
|
||||
;
|
||||
color: var(--foreground);
|
||||
font-size: calc(10px + (var(--magnitude) * 12px));
|
||||
opacity: var(--opacity);
|
||||
|
@ -75,75 +64,8 @@
|
|||
calc(-50% + (1px * var(--offsetY)) + var(--positionY))
|
||||
;
|
||||
user-select: none;
|
||||
will-change: color, scale, opacity, translate, transform;
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
0% {
|
||||
--offsetX: 0;
|
||||
--offsetY: 0;
|
||||
}
|
||||
33%, 91.6% {
|
||||
--offsetX: calc(80 * var(--randomnessX));
|
||||
--offsetY: calc(-1 * (10 + var(--randomnessY) * 90));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% {
|
||||
--opacity: 0.75;
|
||||
}
|
||||
25% {
|
||||
--opacity: 1;
|
||||
}
|
||||
91.6% {
|
||||
--opacity: 1;
|
||||
}
|
||||
100% {
|
||||
--opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes grow {
|
||||
0% {
|
||||
--scale: 0.35;
|
||||
}
|
||||
33% {
|
||||
--scale: 1;
|
||||
}
|
||||
45% {
|
||||
--scale: 1;
|
||||
}
|
||||
40% {
|
||||
--scale: 1.5;
|
||||
}
|
||||
45% {
|
||||
--scale: 1;
|
||||
}
|
||||
91.6% {
|
||||
--scale: 1;
|
||||
}
|
||||
95% {
|
||||
--scale: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pain {
|
||||
0% { --hue: 0 }
|
||||
50% { --hue: 30 }
|
||||
100% { --hue: 0 }
|
||||
}
|
||||
|
||||
@keyframes healing {
|
||||
0% { --hue: 120 }
|
||||
50% { --hue: 90 }
|
||||
100% { --hue: 120 }
|
||||
}
|
||||
|
||||
@keyframes mana {
|
||||
0% { --hue: 220 }
|
||||
50% { --hue: 215 }
|
||||
100% { --hue: 220 }
|
||||
}
|
||||
|
|
|
@ -1,23 +1,172 @@
|
|||
import {memo, useCallback, useRef} from 'react';
|
||||
|
||||
import {DamageTypes} from '@/ecs/components/vulnerable.js';
|
||||
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
|
||||
import {easeInOutExpo, easeInQuint, easeOutQuad, linear} from '@/util/easing.js';
|
||||
|
||||
import styles from './damages.module.css';
|
||||
|
||||
import Damage from './damage.jsx';
|
||||
|
||||
export default function Damages({camera, damages, scale}) {
|
||||
const elements = [];
|
||||
for (const key in damages) {
|
||||
elements.push(
|
||||
<Damage
|
||||
camera={camera}
|
||||
damage={damages[key]}
|
||||
key={key}
|
||||
scale={scale}
|
||||
zIndex={key}
|
||||
/>
|
||||
);
|
||||
function damageHue(type) {
|
||||
let hue;
|
||||
switch(type) {
|
||||
case DamageTypes.PAIN: {
|
||||
hue = [0, 30];
|
||||
break;
|
||||
}
|
||||
if (0 === elements.length) {
|
||||
return false;
|
||||
case DamageTypes.HEALING: {
|
||||
hue = [90, 120];
|
||||
break;
|
||||
}
|
||||
return <div className={styles.damages}>{elements}</div>;
|
||||
case DamageTypes.MANA: {
|
||||
hue = [215, 220];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hue;
|
||||
}
|
||||
|
||||
function createDamageNode() {
|
||||
const damage = document.createElement('div');
|
||||
damage.classList.add(styles.damage);
|
||||
damage.appendChild(document.createElement('p'));
|
||||
return damage;
|
||||
}
|
||||
|
||||
function Damages({damages, scale}) {
|
||||
const animations = useRef({});
|
||||
const pool = useRef([]);
|
||||
const damagesRef = useRef();
|
||||
const frame = useCallback((elapsed) => {
|
||||
if (!damagesRef.current) {
|
||||
return;
|
||||
}
|
||||
if (0 === pool.current.length) {
|
||||
for (let i = 0; i < 512; ++i) {
|
||||
const damage = createDamageNode();
|
||||
damagesRef.current.appendChild(damage);
|
||||
pool.current.push(damage);
|
||||
}
|
||||
}
|
||||
const keys = Object.keys(animations.current);
|
||||
for (const key of keys) {
|
||||
const animation = animations.current[key];
|
||||
if (!damages[key]) {
|
||||
if (animation.element) {
|
||||
if (pool.current.length < 512) {
|
||||
pool.current.push(animation.element);
|
||||
}
|
||||
animation.element = undefined;
|
||||
}
|
||||
delete animations.current[key];
|
||||
continue;
|
||||
}
|
||||
if (!animation.element) {
|
||||
const {amount, position} = damages[key];
|
||||
let damage;
|
||||
if (pool.current.length > 0) {
|
||||
damage = pool.current.pop();
|
||||
}
|
||||
else {
|
||||
damage = createDamageNode();
|
||||
damagesRef.current.appendChild(damage);
|
||||
}
|
||||
const p = damage.querySelector('p');
|
||||
p.style.scale = scale / 2;
|
||||
p.innerText = Math.abs(amount);
|
||||
damage.style.setProperty('--randomnessX', animation['--randomnessX']);
|
||||
damage.style.setProperty('--randomnessY', animation['--randomnessY']);
|
||||
damage.style.setProperty('rotate', animation['rotate']);
|
||||
damage.style.setProperty('rotate', animation['rotate']);
|
||||
damage.style.setProperty('--magnitude', Math.max(1, Math.floor(Math.log10(Math.abs(amount)))));
|
||||
damage.style.setProperty('--positionX', `${position.x * scale}px`);
|
||||
damage.style.setProperty('--positionY', `${position.y * scale}px`);
|
||||
damage.style.setProperty('zIndex', key);
|
||||
animation.element = damage;
|
||||
}
|
||||
animation.elapsed += elapsed;
|
||||
animation.step += elapsed;
|
||||
const offsetX = 20 * scale * animation['--randomnessX'];
|
||||
const offsetY = -1 * (10 + animation['--randomnessY'] * 45 * scale);
|
||||
// offset
|
||||
if (animation.elapsed <= 0.5) {
|
||||
animation['--offsetX'] = easeOutQuad(animation.elapsed, 0, offsetX, 0.5);
|
||||
animation['--offsetY'] = easeOutQuad(animation.elapsed, 0, offsetY, 0.5);
|
||||
}
|
||||
else if (animation.elapsed > 1.375) {
|
||||
animation['--offsetX'] = offsetX - easeOutQuad(animation.elapsed - 1.375, 0, offsetX, 0.125);
|
||||
animation['--offsetY'] = offsetY - easeOutQuad(animation.elapsed - 1.375, 0, offsetY, 0.125);
|
||||
}
|
||||
// scale
|
||||
if (animation.elapsed <= 0.5) {
|
||||
animation['--scale'] = easeOutQuad(animation.elapsed, 0, 1, 0.5);
|
||||
}
|
||||
else if (animation.elapsed > 0.5 && animation.elapsed < 0.6) {
|
||||
animation['--scale'] = linear(animation.elapsed - 0.5, 1, 0.5, 0.1);
|
||||
}
|
||||
else if (animation.elapsed > 0.6 && animation.elapsed < 0.675) {
|
||||
animation['--scale'] = 1.5 - easeInQuint(animation.elapsed - 0.6, 1, 0.5, 0.075);
|
||||
}
|
||||
else if (animation.elapsed > 0.675 && animation.elapsed < 1.375) {
|
||||
animation['--scale'] = 1;
|
||||
}
|
||||
else if (animation.elapsed > 1.375) {
|
||||
animation['--scale'] = 1 - easeOutQuad(animation.elapsed - 1.375, 0, 1, 0.125);
|
||||
}
|
||||
// fade
|
||||
if (animation.elapsed <= 0.375) {
|
||||
animation['--opacity'] = linear(animation.elapsed, 0.75, 0.25, 0.375);
|
||||
}
|
||||
else if (animation.elapsed > 0.375 && animation.elapsed < 1.375) {
|
||||
animation['--opacity'] = 1;
|
||||
}
|
||||
else if (animation.elapsed > 1.375) {
|
||||
animation['--opacity'] = 1 - linear(animation.elapsed - 1.375, 0, 1, 0.125);
|
||||
}
|
||||
// hue
|
||||
const h = Math.abs((animation.elapsed % 0.3) - 0.15) / 0.15;
|
||||
animation['--hue'] = easeInOutExpo(h, animation.hue[0], animation.hue[1], 1);
|
||||
const step = keys.length > 150 ? (keys.length / 500) * 0.25 : elapsed;
|
||||
if (animation.step > step) {
|
||||
animation.step = animation.step % step;
|
||||
animation.element.style.setProperty('--hue', animation['--hue']);
|
||||
animation.element.style.setProperty('--opacity', animation['--opacity']);
|
||||
animation.element.style.setProperty('--offsetX', animation['--offsetX']);
|
||||
animation.element.style.setProperty('--offsetY', animation['--offsetY']);
|
||||
animation.element.style.setProperty('--scale', animation['--scale']);
|
||||
}
|
||||
if (animation.elapsed > 1.5) {
|
||||
if (pool.current.length < 512) {
|
||||
pool.current.push(animation.element);
|
||||
}
|
||||
animation.element.style.setProperty('--opacity', 0);
|
||||
animation.element = undefined;
|
||||
damages[key].onClose();
|
||||
}
|
||||
}
|
||||
}, [damages, scale]);
|
||||
useAnimationFrame(frame);
|
||||
for (const key in damages) {
|
||||
if (!animations.current[key]) {
|
||||
animations.current[key] = {
|
||||
elapsed: 0,
|
||||
hue: damageHue(damages[key].type),
|
||||
rotate: `${Math.random() * (Math.PI / 16) - (Math.PI / 32)}rad`,
|
||||
step: 0,
|
||||
'--scale': 0.35,
|
||||
'--opacity': 0.75,
|
||||
'--offsetX': 0,
|
||||
'--offsetY': 0,
|
||||
'--randomnessX': 1 * (Math.random() - 0.5),
|
||||
'--randomnessY': Math.random(),
|
||||
};
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={styles.damages}
|
||||
ref={damagesRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Damages);
|
||||
|
|
|
@ -1,3 +1,75 @@
|
|||
@property --hue {
|
||||
initial-value: 0;
|
||||
inherits: false;
|
||||
syntax: '<number>';
|
||||
}
|
||||
@property --opacity {
|
||||
initial-value: 0;
|
||||
inherits: false;
|
||||
syntax: '<number>';
|
||||
}
|
||||
@property --scale {
|
||||
initial-value: 0;
|
||||
inherits: false;
|
||||
syntax: '<number>';
|
||||
}
|
||||
@property --offsetX {
|
||||
initial-value: 0;
|
||||
inherits: false;
|
||||
syntax: '<number>';
|
||||
}
|
||||
@property --offsetY {
|
||||
initial-value: 0;
|
||||
inherits: false;
|
||||
syntax: '<number>';
|
||||
}
|
||||
@property --randomnessX {
|
||||
initial-value: 0;
|
||||
inherits: false;
|
||||
syntax: '<number>';
|
||||
}
|
||||
@property --randomnessY {
|
||||
initial-value: 0;
|
||||
inherits: false;
|
||||
syntax: '<number>';
|
||||
}
|
||||
|
||||
.damage {
|
||||
--hue: 0;
|
||||
--opacity: 0.75;
|
||||
--randomnessX: 0;
|
||||
--randomnessY: 0;
|
||||
--scale: 0.35;
|
||||
--background: hsl(var(--hue) 100% 12.5%);
|
||||
--foreground: hsl(var(--hue) 100% 50%);
|
||||
color: var(--foreground);
|
||||
font-size: calc(10px + (var(--magnitude) * 12px));
|
||||
opacity: var(--opacity);
|
||||
overflow-wrap: break-word;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
text-shadow:
|
||||
0px -1px 0px var(--background),
|
||||
1px 0px 0px var(--background),
|
||||
0px 1px 0px var(--background),
|
||||
-1px 0px 0px var(--background),
|
||||
0px -2px 0px var(--background),
|
||||
2px 0px 0px var(--background),
|
||||
0px 2px 0px var(--background),
|
||||
-2px 0px 0px var(--background)
|
||||
;
|
||||
scale: var(--scale);
|
||||
translate:
|
||||
calc(-50% + (1px * var(--offsetX)) + var(--positionX))
|
||||
calc(-50% + (1px * var(--offsetY)) + var(--positionY))
|
||||
;
|
||||
user-select: none;
|
||||
will-change: color, scale, opacity, translate, transform;
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.damages {
|
||||
font-family: Joystix, 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
|
|
@ -26,21 +26,21 @@ export default function DialogueCaret({
|
|||
};
|
||||
const left = Math.max(
|
||||
Math.min(
|
||||
position.x * scale - camera.x,
|
||||
RESOLUTION.x - bounds.x * scale - 16,
|
||||
position.x * scale,
|
||||
RESOLUTION.x - bounds.x * scale - 16 + camera.x,
|
||||
),
|
||||
bounds.x * scale + 16,
|
||||
bounds.x * scale + 16 + camera.x,
|
||||
);
|
||||
const top = Math.max(
|
||||
Math.min(
|
||||
position.y * scale - camera.y,
|
||||
RESOLUTION.y - bounds.y * scale - 16,
|
||||
position.y * scale - dimensions.h / 2,
|
||||
RESOLUTION.y - bounds.y * scale - 16 + camera.y,
|
||||
),
|
||||
bounds.y * scale + 88,
|
||||
bounds.y * scale + 16 + camera.y,
|
||||
);
|
||||
const offsetPosition = {
|
||||
x: ((position.x * scale - camera.x) - left) / scale,
|
||||
y: ((position.y * scale - camera.y) - top) / scale,
|
||||
x: ((position.x * scale) - left) / scale,
|
||||
y: ((position.y * scale) - top) / scale,
|
||||
};
|
||||
const difference = {
|
||||
x: origin.x - position.x + offsetPosition.x,
|
||||
|
|
|
@ -87,25 +87,27 @@ export default function Dialogue({
|
|||
};
|
||||
const left = Math.max(
|
||||
Math.min(
|
||||
position.x * scale - camera.x,
|
||||
RESOLUTION.x - bounds.x * scale - 16,
|
||||
position.x * scale,
|
||||
RESOLUTION.x - bounds.x * scale - 16 + camera.x,
|
||||
),
|
||||
bounds.x * scale + 16,
|
||||
bounds.x * scale + 16 + camera.x,
|
||||
);
|
||||
const top = Math.max(
|
||||
Math.min(
|
||||
position.y * scale - camera.y,
|
||||
RESOLUTION.y - bounds.y * scale - 16,
|
||||
position.y * scale - dimensions.h / 2,
|
||||
RESOLUTION.y - bounds.y * scale - 16 + camera.y,
|
||||
),
|
||||
bounds.y * scale + 16,
|
||||
bounds.y * scale + 16 + camera.y,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={styles.dialogue}
|
||||
ref={ref}
|
||||
style={{
|
||||
left: `${left}px`,
|
||||
top: `${top}px`,
|
||||
translate: `
|
||||
${left}px
|
||||
${top}px
|
||||
`,
|
||||
}}
|
||||
>
|
||||
<DialogueCaret
|
||||
|
@ -113,7 +115,6 @@ export default function Dialogue({
|
|||
dialogue={dialogue}
|
||||
dimensions={dimensions}
|
||||
scale={scale}
|
||||
|
||||
/>
|
||||
<p className={styles.letters}>
|
||||
{localRender(caret, radians)}
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
border: solid 3px white;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
left: 0;
|
||||
overflow-wrap: break-word;
|
||||
padding: 12px;
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
margin-right: -66%;
|
||||
text-shadow:
|
||||
|
@ -14,6 +15,7 @@
|
|||
0px 1px 0px black,
|
||||
-1px 0px 0px black
|
||||
;
|
||||
top: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
user-select: none;
|
||||
max-width: 66%;
|
||||
|
|
|
@ -25,14 +25,13 @@ export default function Dom({children}) {
|
|||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<div className={styles.dom} ref={ref}>
|
||||
{scale > 0 && (
|
||||
<style>{`
|
||||
.${styles.dom}{
|
||||
--scale: ${scale};
|
||||
--unit: calc(${RESOLUTION.x} / 1000);
|
||||
}
|
||||
`}</style>
|
||||
)}
|
||||
|
|
|
@ -16,6 +16,7 @@ export default function Entities({
|
|||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
const [damages, setDamages] = useState({});
|
||||
const [pendingDamage] = useState({accumulated: [], handle: undefined});
|
||||
usePacket('EcsChange', async () => {
|
||||
setEntities({});
|
||||
}, [setEntities]);
|
||||
|
@ -93,17 +94,31 @@ export default function Entities({
|
|||
const {damage} = update.Vulnerable || {};
|
||||
if (damage) {
|
||||
for (const key in damage) {
|
||||
const composite = [id, key].join('-');
|
||||
damage[key].onClose = () => {
|
||||
setDamages((damages) => {
|
||||
const {[composite]: _, ...rest} = damages; // eslint-disable-line no-unused-vars
|
||||
const {[key]: _, ...rest} = damages; // eslint-disable-line no-unused-vars
|
||||
return rest;
|
||||
})
|
||||
};
|
||||
setDamages((damages) => ({
|
||||
}
|
||||
pendingDamage.accumulated.push(damage);
|
||||
if (!pendingDamage.handle) {
|
||||
pendingDamage.handle = setTimeout(() => {
|
||||
const update = {};
|
||||
for (const damage of pendingDamage.accumulated) {
|
||||
for (const key in damage) {
|
||||
update[key] = damage[key];
|
||||
}
|
||||
}
|
||||
pendingDamage.accumulated.length = 0;
|
||||
setDamages((damages) => {
|
||||
return {
|
||||
...damages,
|
||||
[composite]: damage[key],
|
||||
}));
|
||||
...update,
|
||||
};
|
||||
});
|
||||
pendingDamage.handle = undefined;
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -129,13 +144,19 @@ export default function Entities({
|
|||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
translate: `
|
||||
calc(-1px * ${camera.x})
|
||||
calc(-1px * ${camera.y})
|
||||
`,
|
||||
}}
|
||||
>
|
||||
{renderables}
|
||||
<Damages
|
||||
camera={camera}
|
||||
damages={damages}
|
||||
scale={scale}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
27
app/react/components/dom/external.jsx
Normal file
27
app/react/components/dom/external.jsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import styles from './external.module.css';
|
||||
|
||||
import Grid from './grid.jsx';
|
||||
|
||||
/**
|
||||
* External inventory.
|
||||
*/
|
||||
export default function External({
|
||||
isInventoryOpen,
|
||||
onActivate,
|
||||
slots,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={styles.external}
|
||||
style={isInventoryOpen ? {transition: 'opacity 50ms'} : {opacity: 0, top: '450px'}}
|
||||
>
|
||||
<Grid
|
||||
color="rgba(57, 02, 02, 0.6)"
|
||||
columns={10}
|
||||
label="Chest"
|
||||
onActivate={onActivate}
|
||||
slots={slots}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
7
app/react/components/dom/external.module.css
Normal file
7
app/react/components/dom/external.module.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
.external {
|
||||
left: 20px;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
top: 274px;
|
||||
transition: top 150ms, opacity 200ms;
|
||||
}
|
69
app/react/components/dom/grid.jsx
Normal file
69
app/react/components/dom/grid.jsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import styles from './grid.module.css';
|
||||
import Slot from './slot.jsx';
|
||||
|
||||
/**
|
||||
* Inventory grid.
|
||||
*/
|
||||
export default function Grid({
|
||||
active = -1,
|
||||
color,
|
||||
columns,
|
||||
label,
|
||||
onActivate,
|
||||
slots,
|
||||
}) {
|
||||
const Slots = slots.map((slot, i) => (
|
||||
<div
|
||||
className={
|
||||
[styles.slot, active === i && styles.active]
|
||||
.filter(Boolean).join(' ')
|
||||
}
|
||||
key={i}
|
||||
>
|
||||
<Slot
|
||||
icon={slot?.icon}
|
||||
onMouseDown={(event) => {
|
||||
onActivate(i)
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
onDragStart={(event) => {
|
||||
if (!slot) {
|
||||
event.preventDefault();
|
||||
}
|
||||
event.dataTransfer.setData('silphius/item', i);
|
||||
onActivate(i);
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault();
|
||||
onActivate(i);
|
||||
}}
|
||||
qty={slot?.qty}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div className={styles.gridWrapper}>
|
||||
<p className={styles.label}> {label} </p>
|
||||
<div
|
||||
className={styles.grid}
|
||||
style={{
|
||||
'--color': color,
|
||||
'--columns': columns,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.innerGrid}
|
||||
>
|
||||
{Slots}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
45
app/react/components/dom/grid.module.css
Normal file
45
app/react/components/dom/grid.module.css
Normal file
|
@ -0,0 +1,45 @@
|
|||
.grid {
|
||||
--border: 2.5px;
|
||||
border: var(--border) solid #444444;
|
||||
line-height: 0;
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.innerGrid {
|
||||
background-color: var(--color);
|
||||
border-bottom: var(--border) solid #999999;
|
||||
border-right: var(--border) solid #999999;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--columns), 1fr);
|
||||
}
|
||||
|
||||
.label {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||
margin: 0;
|
||||
text-shadow:
|
||||
0px -1px 0px black,
|
||||
1px 0px 0px black,
|
||||
0px 1px 0px black,
|
||||
-1px 0px 0px black
|
||||
;
|
||||
}
|
||||
|
||||
.slot {
|
||||
border-left: var(--border) solid #999999;
|
||||
border-top: var(--border) solid #999999;
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
padding: 0;
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
&.active {
|
||||
border: var(--border) solid yellow;
|
||||
margin-bottom: calc(-1 * var(--border));
|
||||
margin-right: calc(-1 * var(--border));
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import styles from './hotbar.module.css';
|
||||
import Slot from './slot.jsx';
|
||||
import gridStyles from './grid.module.css';
|
||||
|
||||
import Grid from './grid.jsx';
|
||||
|
||||
/**
|
||||
* The hotbar. 10 slots of inventory with an active selection.
|
||||
|
@ -10,48 +12,24 @@ export default function Hotbar({
|
|||
onActivate,
|
||||
slots,
|
||||
}) {
|
||||
const Slots = slots.map((slot, i) => (
|
||||
<div
|
||||
className={
|
||||
[styles.slotWrapper, active === i && styles.active]
|
||||
.filter(Boolean).join(' ')
|
||||
}
|
||||
key={i}
|
||||
>
|
||||
<Slot
|
||||
icon={slot?.icon}
|
||||
onMouseDown={(event) => {
|
||||
onActivate(i)
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onMouseUp={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
onDragStart={(event) => {
|
||||
if (!slot) {
|
||||
event.preventDefault();
|
||||
}
|
||||
event.dataTransfer.setData('silphius/item', i);
|
||||
onActivate(i);
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault();
|
||||
onActivate(i);
|
||||
}}
|
||||
qty={slot?.qty}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div
|
||||
className={styles.hotbar}
|
||||
style={hotbarIsHidden ? {top: '-50px'} : {transition: 'none'}}
|
||||
style={hotbarIsHidden ? {opacity: 0, top: '-50px'} : {transition: 'opacity 50ms'}}
|
||||
>
|
||||
<p className={styles.label}>{slots[active] && slots[active].label}</p>
|
||||
{Slots}
|
||||
<style>{`
|
||||
.${styles.hotbar} .${gridStyles.label} {
|
||||
text-align: center;
|
||||
}
|
||||
`}</style>
|
||||
<Grid
|
||||
active={active}
|
||||
color="rgba(02, 02, 57, 0.6)"
|
||||
columns={10}
|
||||
label={slots[active] && slots[active].label}
|
||||
onActivate={onActivate}
|
||||
slots={slots}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,54 +1,7 @@
|
|||
.hotbar {
|
||||
align-self: left;
|
||||
--border: calc(var(--unit) * 3px);
|
||||
background-color: rgba(02, 02, 57, 0.6);
|
||||
border: var(--border) solid #444444;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
left: calc(var(--unit) * 20px);
|
||||
line-height: 0;
|
||||
left: 20px;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
top: calc(var(--unit) * 20px);
|
||||
transition: top 150ms;
|
||||
}
|
||||
|
||||
.label {
|
||||
background-color: transparent;
|
||||
color: white;
|
||||
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||
left: 50%;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
text-shadow:
|
||||
0px -1px 0px black,
|
||||
1px 0px 0px black,
|
||||
0px 1px 0px black,
|
||||
-1px 0px 0px black
|
||||
;
|
||||
top: -17.5px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.slotWrapper {
|
||||
border: var(--border) solid #999999;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
padding: 0;
|
||||
|
||||
&.active + .slotWrapper {
|
||||
border-left: none;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-right: var(--border) solid #999999;
|
||||
border-color: yellow;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
top: 4px;
|
||||
transition: top 150ms, opacity 200ms;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.slot {
|
||||
--size: calc(var(--unit) * 50px);
|
||||
--space: calc(var(--unit) * 10px);
|
||||
--size: 40px;
|
||||
--space: 7px;
|
||||
background-color: transparent;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
|
|
36
app/react/components/particle-worker.js
Normal file
36
app/react/components/particle-worker.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import Emitter from '@/particles/emitter.js';
|
||||
import createEcs from '@/server/create/ecs.js';
|
||||
|
||||
import ClientEcs from './client-ecs.js';
|
||||
|
||||
const ecs = createEcs(ClientEcs);
|
||||
ecs.$$caret = Math.pow(2, 31);
|
||||
|
||||
const emitter = new Emitter(ecs);
|
||||
|
||||
addEventListener('message', (particle) => {
|
||||
if (!ecs.get(1)) {
|
||||
ecs.createManySpecific([[1, particle.data]]);
|
||||
return;
|
||||
}
|
||||
emitter.emit(particle.data)
|
||||
.onEnd(() => {});
|
||||
});
|
||||
|
||||
let last = Date.now();
|
||||
function tick() {
|
||||
const now = Date.now();
|
||||
const elapsed = (now - last) / 1000;
|
||||
last = now;
|
||||
if (ecs.get(1)) {
|
||||
ecs.tick(elapsed);
|
||||
emitter.tick(elapsed);
|
||||
if ('1' in ecs.diff) {
|
||||
delete ecs.diff['1'];
|
||||
}
|
||||
postMessage(ecs.diff);
|
||||
ecs.setClean();
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
requestAnimationFrame(tick);
|
|
@ -1,10 +1,8 @@
|
|||
import {Container} from '@pixi/react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useState} from 'react';
|
||||
|
||||
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||
// import {useRadians} from '@/react/context/radians.js';
|
||||
// import {TAU} from '@/util/math.js';
|
||||
|
||||
import Entities from './entities.jsx';
|
||||
import TargetingGhost from './targeting-ghost.jsx';
|
||||
|
@ -12,79 +10,14 @@ import TargetingGrid from './targeting-grid.jsx';
|
|||
import TileLayer from './tile-layer.jsx';
|
||||
import Water from './water.jsx';
|
||||
|
||||
const NIGHTNESS = 0.1;
|
||||
|
||||
function calculateDarkness(hour) {
|
||||
let darkness = 0;
|
||||
if (hour >= 21 || hour < 4) {
|
||||
darkness = 0.8;
|
||||
}
|
||||
if (hour >= 4 && hour < 7) {
|
||||
darkness = 0.8 * ((7 - hour) / 3);
|
||||
}
|
||||
if (hour >= 18 && hour < 21) {
|
||||
darkness = 0.8 * ((3 - (21 - hour)) / 3);
|
||||
}
|
||||
return Math.floor(darkness * 1000) / 1000;
|
||||
}
|
||||
|
||||
export default function Ecs({applyFilters, camera, monopolizers, scale}) {
|
||||
export default function Ecs({camera, monopolizers, particleWorker, scale}) {
|
||||
const [ecs] = useEcs();
|
||||
const [filters, setFilters] = useState([]);
|
||||
const [mainEntity] = useMainEntity();
|
||||
const [layers, setLayers] = useState([]);
|
||||
const [hour, setHour] = useState(10);
|
||||
const [night, setNight] = useState();
|
||||
const [projected, setProjected] = useState([]);
|
||||
const [position, setPosition] = useState({x: 0, y: 0});
|
||||
const [water, setWater] = useState();
|
||||
// const radians = useRadians();
|
||||
// const [sine, setSine] = useState();
|
||||
// useEffect(() => {
|
||||
// async function buildSineFilter() {
|
||||
// const {default: SineFilter} = await import('./filters/horizontal-sine.js');
|
||||
// const sine = new SineFilter();
|
||||
// sine.frequency = 1;
|
||||
// sine.magnitude = 3;
|
||||
// setSine(sine);
|
||||
// }
|
||||
// buildSineFilter();
|
||||
// }, []);
|
||||
// useEffect(() => {
|
||||
// if (!sine) {
|
||||
// return;
|
||||
// }
|
||||
// const r = (radians / 8) % TAU;
|
||||
// sine.offset = 6 * (camera.y + r);
|
||||
// sine.magnitude = 2 * (r > Math.PI ? TAU - r : r);
|
||||
// }, [camera, radians, scale, sine]);
|
||||
useEffect(() => {
|
||||
async function buildNightFilter() {
|
||||
const {ColorMatrixFilter} = await import('@pixi/filter-color-matrix');
|
||||
class NightFilter extends ColorMatrixFilter {
|
||||
setIntensity(intensity) {
|
||||
const double = NIGHTNESS * 2;
|
||||
const half = NIGHTNESS / 2;
|
||||
const redDown = 1 - (intensity * (1 + double));
|
||||
const blueUp = 1 - (intensity * (1 - half));
|
||||
const scale = intensity * NIGHTNESS;
|
||||
this.uniforms.m = [
|
||||
redDown, -scale, 0, 0, 0,
|
||||
-scale, (1 - intensity), scale, 0, 0,
|
||||
0, scale, blueUp, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
setNight(new NightFilter());
|
||||
}
|
||||
buildNightFilter();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (night) {
|
||||
night.setIntensity(calculateDarkness(hour));
|
||||
}
|
||||
}, [hour, night]);
|
||||
useEcsTick((payload) => {
|
||||
const entity = ecs.get(mainEntity);
|
||||
for (const id in payload) {
|
||||
|
@ -111,28 +44,15 @@ export default function Ecs({applyFilters, camera, monopolizers, scale}) {
|
|||
setProjected(Wielder.activeItem()?.project(Position.tile, Direction.quantize(4)));
|
||||
}
|
||||
}, [ecs, mainEntity, scale]);
|
||||
useEffect(() => {
|
||||
setFilters(
|
||||
applyFilters
|
||||
? [
|
||||
...(false && night ? [night] : []),
|
||||
// ...(sine ? [sine] : []),
|
||||
]
|
||||
: [],
|
||||
);
|
||||
}, [applyFilters, night])
|
||||
return (
|
||||
<Container
|
||||
scale={scale}
|
||||
x={-camera.x}
|
||||
y={-camera.y}
|
||||
>
|
||||
<Container
|
||||
filters={filters}
|
||||
>
|
||||
<Container>
|
||||
{layers.map((layer, i) => (
|
||||
<TileLayer
|
||||
filters={filters}
|
||||
key={i}
|
||||
tileLayer={layer}
|
||||
/>
|
||||
|
@ -152,8 +72,8 @@ export default function Ecs({applyFilters, camera, monopolizers, scale}) {
|
|||
/>
|
||||
)}
|
||||
<Entities
|
||||
filters={filters}
|
||||
monopolizers={monopolizers}
|
||||
particleWorker={particleWorker}
|
||||
/>
|
||||
{projected?.length > 0 && layers[0] && (
|
||||
<TargetingGhost
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {AdjustmentFilter} from '@pixi/filter-adjustment';
|
||||
import {GlowFilter} from '@pixi/filter-glow';
|
||||
import {Container} from '@pixi/react';
|
||||
import {useState} from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {usePacket} from '@/react/context/client.js';
|
||||
import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
||||
|
@ -10,7 +10,7 @@ import {useRadians} from '@/react/context/radians.js';
|
|||
|
||||
import Entity from './entity.jsx';
|
||||
|
||||
export default function Entities({filters, monopolizers}) {
|
||||
export default function Entities({monopolizers, particleWorker}) {
|
||||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
const [mainEntity] = useMainEntity();
|
||||
|
@ -20,6 +20,37 @@ export default function Entities({filters, monopolizers}) {
|
|||
new AdjustmentFilter(),
|
||||
new GlowFilter({color: 0x0}),
|
||||
]);
|
||||
useEffect(() => {
|
||||
if (!ecs || !particleWorker) {
|
||||
return;
|
||||
}
|
||||
async function onMessage(diff) {
|
||||
await ecs.apply(diff.data);
|
||||
const deleted = {};
|
||||
const updated = {};
|
||||
for (const id in diff.data) {
|
||||
if (!diff.data[id]) {
|
||||
deleted[id] = true;
|
||||
}
|
||||
else {
|
||||
updated[id] = ecs.get(id);
|
||||
}
|
||||
}
|
||||
setEntities((entities) => {
|
||||
for (const id in deleted) {
|
||||
delete entities[id];
|
||||
}
|
||||
return {
|
||||
...entities,
|
||||
...updated,
|
||||
};
|
||||
});
|
||||
}
|
||||
particleWorker.addEventListener('message', onMessage);
|
||||
return () => {
|
||||
particleWorker.removeEventListener('message', onMessage);
|
||||
};
|
||||
}, [ecs, particleWorker]);
|
||||
const pulse = (Math.cos(radians / 4) + 1) * 0.5;
|
||||
interactionFilters[0].brightness = (pulse * 0.75) + 1;
|
||||
interactionFilters[1].outerStrength = pulse * 0.5;
|
||||
|
@ -43,10 +74,9 @@ export default function Entities({filters, monopolizers}) {
|
|||
}
|
||||
updating[id] = ecs.get(id);
|
||||
if (update.Emitter?.emit) {
|
||||
updating[id].Emitter.emitting = {
|
||||
...updating[id].Emitter.emitting,
|
||||
...update.Emitter.emit,
|
||||
};
|
||||
for (const id in update.Emitter.emit) {
|
||||
particleWorker?.postMessage(update.Emitter.emit[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
setEntities((entities) => {
|
||||
|
@ -58,7 +88,7 @@ export default function Entities({filters, monopolizers}) {
|
|||
...updating,
|
||||
};
|
||||
});
|
||||
}, [ecs]);
|
||||
}, [ecs, particleWorker]);
|
||||
useEcsTick(() => {
|
||||
if (!ecs) {
|
||||
return;
|
||||
|
@ -81,7 +111,6 @@ export default function Entities({filters, monopolizers}) {
|
|||
}
|
||||
return (
|
||||
<Container
|
||||
filters={filters}
|
||||
sortableChildren
|
||||
>
|
||||
{renderables}
|
||||
|
|
|
@ -4,9 +4,8 @@ import {memo, useCallback} from 'react';
|
|||
import {useDebug} from '@/react/context/debug.js';
|
||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||
|
||||
import Emitter from './emitter.jsx';
|
||||
// import Light from './light.jsx';
|
||||
import Sprite from './sprite.jsx';
|
||||
import Light from './light.jsx';
|
||||
import SpriteComponent from './sprite.jsx';
|
||||
|
||||
function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) {
|
||||
const draw = useCallback((g) => {
|
||||
|
@ -23,7 +22,7 @@ function Aabb({color, width = 0.5, x0, y0, x1, y1, ...rest}) {
|
|||
);
|
||||
}
|
||||
|
||||
function Crosshair({x, y}) {
|
||||
function Crosshair() {
|
||||
const draw = useCallback((g) => {
|
||||
g.clear();
|
||||
g.lineStyle(1, 0x000000);
|
||||
|
@ -42,7 +41,7 @@ function Crosshair({x, y}) {
|
|||
g.drawCircle(0, 0, 3);
|
||||
}, []);
|
||||
return (
|
||||
<Graphics draw={draw} x={x} y={y} />
|
||||
<Graphics draw={draw} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -52,30 +51,39 @@ function Entity({entity, ...rest}) {
|
|||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
const {Direction, id, Sprite} = entity;
|
||||
return (
|
||||
<>
|
||||
<Container
|
||||
x={entity.Position.x}
|
||||
y={entity.Position.y}
|
||||
zIndex={entity.Position?.y || 0}
|
||||
>
|
||||
{entity.Sprite && (
|
||||
<Sprite
|
||||
entity={entity}
|
||||
<SpriteComponent
|
||||
alpha={Sprite.alpha}
|
||||
anchor={Sprite.anchor}
|
||||
animation={Sprite.animation}
|
||||
direction={Direction?.direction}
|
||||
frame={Sprite.frame}
|
||||
id={id}
|
||||
scale={Sprite.scale}
|
||||
rotates={Sprite.rotates}
|
||||
rotation={Sprite.rotation}
|
||||
source={Sprite.source}
|
||||
tint={Sprite.tint}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
{entity.Emitter && (
|
||||
<Emitter
|
||||
entity={entity}
|
||||
/>
|
||||
)}
|
||||
{/* {entity.Light && (
|
||||
{entity.Light && (
|
||||
<Light
|
||||
x={entity.Position.x}
|
||||
y={entity.Position.y}
|
||||
brightness={entity.Light.brightness}
|
||||
/>
|
||||
)} */}
|
||||
{debug && entity.Position && (
|
||||
<Crosshair x={entity.Position.x} y={entity.Position.y} />
|
||||
)}
|
||||
{debug && entity.Position && (
|
||||
<Crosshair />
|
||||
)}
|
||||
</Container>
|
||||
{debug && (
|
||||
<Aabb
|
||||
color={0xff00ff}
|
||||
|
@ -105,7 +113,7 @@ function Entity({entity, ...rest}) {
|
|||
{...entity.Interacts.aabb()}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,11 @@ import {PixiComponent} from '@pixi/react';
|
|||
import {PointLight} from './lights.js';
|
||||
|
||||
const LightInternal = PixiComponent('Light', {
|
||||
create({x, y}) {
|
||||
const light = new PointLight(0xffffff - 0x2244cc, 1);
|
||||
light.position.set(x, y);
|
||||
create({brightness}) {
|
||||
const light = new PointLight(
|
||||
0xffffff - 0x2244cc,
|
||||
brightness,
|
||||
);
|
||||
// light.shader.program.fragmentSrc = light.shader.program.fragmentSrc.replace(
|
||||
// 'float D = length(lightVector)',
|
||||
// 'float D = length(lightVector) / 1.0',
|
||||
|
@ -16,20 +18,16 @@ const LightInternal = PixiComponent('Light', {
|
|||
// );
|
||||
// light.falloff = [0.5, 5, 50];
|
||||
// light.falloff = light.falloff.map((n, i) => n / (2 + i));
|
||||
// light.parentGroup = entityLighting.lightGroup;
|
||||
// light.parentGroup = deferredLighting.lightGroup;
|
||||
// delete light.parentGroup;
|
||||
return light;
|
||||
},
|
||||
applyProps(light, oldProps, {x, y}) {
|
||||
light.position.set(x, y);
|
||||
},
|
||||
});
|
||||
|
||||
export default function Light({x, y}) {
|
||||
export default function Light({brightness}) {
|
||||
return (
|
||||
<LightInternal
|
||||
x={x}
|
||||
y={y}
|
||||
brightness={brightness}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ export const Stage = ({children, ...props}) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default function Pixi({applyFilters, camera, monopolizers, scale}) {
|
||||
export default function Pixi({camera, monopolizers, particleWorker, scale}) {
|
||||
return (
|
||||
<Stage
|
||||
className={styles.stage}
|
||||
|
@ -68,9 +68,9 @@ export default function Pixi({applyFilters, camera, monopolizers, scale}) {
|
|||
}}
|
||||
>
|
||||
<Ecs
|
||||
applyFilters={applyFilters}
|
||||
camera={camera}
|
||||
monopolizers={monopolizers}
|
||||
particleWorker={particleWorker}
|
||||
scale={scale}
|
||||
/>
|
||||
</Stage>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Sprite as PixiSprite} from '@pixi/react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {memo, useEffect, useState} from 'react';
|
||||
|
||||
import {useAsset} from '@/react/context/assets.js';
|
||||
|
||||
|
@ -22,11 +22,23 @@ function textureFromAsset(asset, animation, frame) {
|
|||
return texture;
|
||||
}
|
||||
|
||||
export default function Sprite({entity, ...rest}) {
|
||||
function Sprite(props) {
|
||||
const {
|
||||
alpha,
|
||||
anchor,
|
||||
animation,
|
||||
direction,
|
||||
frame,
|
||||
scale,
|
||||
rotates,
|
||||
rotation,
|
||||
source,
|
||||
tint,
|
||||
...rest
|
||||
} = props;
|
||||
const [mounted, setMounted] = useState();
|
||||
const [normals, setNormals] = useState();
|
||||
const [normalsMounted, setNormalsMounted] = useState();
|
||||
const {alpha, anchor, animation, frame, scale, rotates, rotation, source} = entity.Sprite;
|
||||
const asset = useAsset(source);
|
||||
const normalsAsset = useAsset(normals);
|
||||
useEffect(() => {
|
||||
|
@ -65,11 +77,10 @@ export default function Sprite({entity, ...rest}) {
|
|||
alpha={alpha}
|
||||
anchor={anchor}
|
||||
ref={setMounted}
|
||||
{...(rotates ? {rotation: entity.Direction.direction + rotation} : {})}
|
||||
{...(rotates ? {rotation: direction + rotation} : {})}
|
||||
scale={scale}
|
||||
texture={texture}
|
||||
x={Math.round(entity.Position.x)}
|
||||
y={Math.round(entity.Position.y)}
|
||||
{...(0 !== tint ? {tint} : {})}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
|
@ -78,14 +89,14 @@ export default function Sprite({entity, ...rest}) {
|
|||
alpha={alpha}
|
||||
anchor={anchor}
|
||||
ref={setNormalsMounted}
|
||||
{...(rotates ? {rotation: entity.Direction.direction + rotation} : {})}
|
||||
{...(rotates ? {rotation: direction + rotation} : {})}
|
||||
scale={scale}
|
||||
texture={normalsTexture}
|
||||
x={Math.round(entity.Position.x)}
|
||||
y={Math.round(entity.Position.y)}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Sprite);
|
||||
|
|
|
@ -10,12 +10,13 @@ const TargetingGhostInternal = PixiComponent('TargetingGhost', {
|
|||
create: () => {
|
||||
// Solid target square.
|
||||
const target = new Graphics();
|
||||
target.alpha = 0.7;
|
||||
target.lineStyle(1, 0xffffff);
|
||||
target.drawRect(0.5, 0.5, tileSize.x, tileSize.y);
|
||||
target.pivot = {x: tileSize.x / 2, y: tileSize.y / 2};
|
||||
// Inner spinny part.
|
||||
const targetInner = new Graphics();
|
||||
targetInner.alpha = 0.6;
|
||||
targetInner.alpha = 0.3;
|
||||
targetInner.lineStyle(3, 0x333333);
|
||||
targetInner.beginFill(0xdddddd);
|
||||
targetInner.pivot = {x: tileSize.x / 2, y: tileSize.y / 2};
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import {Container, Graphics} from '@pixi/react';
|
||||
import {forwardRef, useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {forwardRef, memo, useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
import {deferredLighting} from './lights.js';
|
||||
|
||||
const WaterTile = forwardRef(function WaterTile({height, width}, ref) {
|
||||
const draw = useCallback((g) => {
|
||||
|
@ -15,7 +17,7 @@ const WaterTile = forwardRef(function WaterTile({height, width}, ref) {
|
|||
return <Graphics alpha={0} draw={draw} ref={ref} />
|
||||
});
|
||||
|
||||
export default function Water({tileLayer, water}) {
|
||||
function Water({tileLayer, water}) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
@ -39,7 +41,11 @@ export default function Water({tileLayer, water}) {
|
|||
}
|
||||
}
|
||||
return (
|
||||
<Container>
|
||||
<Container ref={(element) => {
|
||||
if (element) {
|
||||
element.parentGroup = deferredLighting.diffuseGroup;
|
||||
}
|
||||
}}>
|
||||
<WaterTile
|
||||
height={tileLayer.tileSize.y}
|
||||
ref={waterTile}
|
||||
|
@ -49,3 +55,5 @@ export default function Water({tileLayer, water}) {
|
|||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Water);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {memo, useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
import {useClient, usePacket} from '@/react/context/client.js';
|
||||
import {useDebug} from '@/react/context/debug.js';
|
||||
|
@ -14,6 +14,7 @@ import Chat from './dom/chat/chat.jsx';
|
|||
import Bag from './dom/bag.jsx';
|
||||
import Dom from './dom/dom.jsx';
|
||||
import Entities from './dom/entities.jsx';
|
||||
import External from './dom/external.jsx';
|
||||
import HotBar from './dom/hotbar.jsx';
|
||||
import Pixi from './pixi/pixi.jsx';
|
||||
import Devtools from './devtools.jsx';
|
||||
|
@ -25,7 +26,7 @@ const KEY_MAP = {
|
|||
};
|
||||
|
||||
function emptySlots() {
|
||||
return Array(10).fill(undefined);
|
||||
return Array(50).fill(undefined);
|
||||
}
|
||||
|
||||
const devEventsChannel = new EventEmitter();
|
||||
|
@ -43,12 +44,11 @@ function Ui({disconnected}) {
|
|||
const [devtoolsIsOpen, setDevtoolsIsOpen] = useState(false);
|
||||
const ratio = (RESOLUTION.x * (devtoolsIsOpen ? 2 : 1)) / RESOLUTION.y;
|
||||
const [camera, setCamera] = useState({x: 0, y: 0});
|
||||
const [hotbarSlots, setHotbarSlots] = useState(emptySlots());
|
||||
const [inventorySlots, setInventorySlots] = useState(emptySlots());
|
||||
const [activeSlot, setActiveSlot] = useState(0);
|
||||
const [scale, setScale] = useState(2);
|
||||
const [Components, setComponents] = useState();
|
||||
const [Systems, setSystems] = useState();
|
||||
const [applyFilters, setApplyFilters] = useState(true);
|
||||
const [monopolizers, setMonopolizers] = useState([]);
|
||||
const [message, setMessage] = useState('');
|
||||
const [chatIsOpen, setChatIsOpen] = useState(false);
|
||||
|
@ -59,6 +59,15 @@ function Ui({disconnected}) {
|
|||
const [hotbarIsHidden, setHotbarIsHidden] = useState(true);
|
||||
const [hotbarHideHandle, setHotbarHideHandle] = useState();
|
||||
const [isInventoryOpen, setIsInventoryOpen] = useState(false);
|
||||
const [externalInventory, setExternalInventory] = useState();
|
||||
const [externalInventorySlots, setExternalInventorySlots] = useState();
|
||||
const [particleWorker, setParticleWorker] = useState();
|
||||
const refreshEcs = useCallback(() => {
|
||||
class ClientEcsPerf extends ClientEcs {
|
||||
markChange() {}
|
||||
}
|
||||
setEcs(new ClientEcsPerf({Components, Systems}));
|
||||
}, [Components, Systems, setEcs]);
|
||||
useEffect(() => {
|
||||
async function setEcsStuff() {
|
||||
const {default: Components} = await import('@/ecs/components/index.js');
|
||||
|
@ -69,8 +78,8 @@ function Ui({disconnected}) {
|
|||
setEcsStuff();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
setEcs(new ClientEcs({Components, Systems}));
|
||||
}, [Components, setEcs, Systems]);
|
||||
refreshEcs();
|
||||
}, [refreshEcs]);
|
||||
useEffect(() => {
|
||||
let handle;
|
||||
if (disconnected) {
|
||||
|
@ -123,6 +132,17 @@ function Ui({disconnected}) {
|
|||
chatIsOpen,
|
||||
client,
|
||||
]);
|
||||
const keepHotbarOpen = useCallback(() => {
|
||||
if (!isInventoryOpen) {
|
||||
setHotbarIsHidden(false);
|
||||
if (hotbarHideHandle) {
|
||||
clearTimeout(hotbarHideHandle);
|
||||
}
|
||||
setHotbarHideHandle(setTimeout(() => {
|
||||
setHotbarIsHidden(true);
|
||||
}, 4000));
|
||||
}
|
||||
}, [hotbarHideHandle, isInventoryOpen]);
|
||||
useEffect(() => {
|
||||
return addKeyListener(document.body, ({event, type, payload}) => {
|
||||
if ('Escape' === payload && 'keyDown' === type && chatIsOpen) {
|
||||
|
@ -166,7 +186,10 @@ function Ui({disconnected}) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case 'Tab': {
|
||||
case '`': {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if ('keyDown' === type) {
|
||||
if (isInventoryOpen) {
|
||||
setHotbarIsHidden(true);
|
||||
|
@ -199,150 +222,70 @@ function Ui({disconnected}) {
|
|||
}
|
||||
case '1': {
|
||||
if ('keyDown' === type) {
|
||||
if (!isInventoryOpen) {
|
||||
setHotbarIsHidden(false);
|
||||
if (hotbarHideHandle) {
|
||||
clearTimeout(hotbarHideHandle);
|
||||
}
|
||||
setHotbarHideHandle(setTimeout(() => {
|
||||
setHotbarIsHidden(true);
|
||||
}, 4000));
|
||||
}
|
||||
keepHotbarOpen();
|
||||
actionPayload = {type: 'changeSlot', value: 1};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '2': {
|
||||
if ('keyDown' === type) {
|
||||
if (!isInventoryOpen) {
|
||||
setHotbarIsHidden(false);
|
||||
if (hotbarHideHandle) {
|
||||
clearTimeout(hotbarHideHandle);
|
||||
}
|
||||
setHotbarHideHandle(setTimeout(() => {
|
||||
setHotbarIsHidden(true);
|
||||
}, 4000));
|
||||
}
|
||||
keepHotbarOpen();
|
||||
actionPayload = {type: 'changeSlot', value: 2};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '3': {
|
||||
if ('keyDown' === type) {
|
||||
if (!isInventoryOpen) {
|
||||
setHotbarIsHidden(false);
|
||||
if (hotbarHideHandle) {
|
||||
clearTimeout(hotbarHideHandle);
|
||||
}
|
||||
setHotbarHideHandle(setTimeout(() => {
|
||||
setHotbarIsHidden(true);
|
||||
}, 4000));
|
||||
}
|
||||
keepHotbarOpen();
|
||||
actionPayload = {type: 'changeSlot', value: 3};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '4': {
|
||||
if ('keyDown' === type) {
|
||||
if (!isInventoryOpen) {
|
||||
setHotbarIsHidden(false);
|
||||
if (hotbarHideHandle) {
|
||||
clearTimeout(hotbarHideHandle);
|
||||
}
|
||||
setHotbarHideHandle(setTimeout(() => {
|
||||
setHotbarIsHidden(true);
|
||||
}, 4000));
|
||||
}
|
||||
keepHotbarOpen();
|
||||
actionPayload = {type: 'changeSlot', value: 4};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '5': {
|
||||
if ('keyDown' === type) {
|
||||
if (!isInventoryOpen) {
|
||||
setHotbarIsHidden(false);
|
||||
if (hotbarHideHandle) {
|
||||
clearTimeout(hotbarHideHandle);
|
||||
}
|
||||
setHotbarHideHandle(setTimeout(() => {
|
||||
setHotbarIsHidden(true);
|
||||
}, 4000));
|
||||
}
|
||||
keepHotbarOpen();
|
||||
actionPayload = {type: 'changeSlot', value: 5};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '6': {
|
||||
if ('keyDown' === type) {
|
||||
if (!isInventoryOpen) {
|
||||
setHotbarIsHidden(false);
|
||||
if (hotbarHideHandle) {
|
||||
clearTimeout(hotbarHideHandle);
|
||||
}
|
||||
setHotbarHideHandle(setTimeout(() => {
|
||||
setHotbarIsHidden(true);
|
||||
}, 4000));
|
||||
}
|
||||
keepHotbarOpen();
|
||||
actionPayload = {type: 'changeSlot', value: 6};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '7': {
|
||||
if ('keyDown' === type) {
|
||||
if (!isInventoryOpen) {
|
||||
setHotbarIsHidden(false);
|
||||
if (hotbarHideHandle) {
|
||||
clearTimeout(hotbarHideHandle);
|
||||
}
|
||||
setHotbarHideHandle(setTimeout(() => {
|
||||
setHotbarIsHidden(true);
|
||||
}, 4000));
|
||||
}
|
||||
keepHotbarOpen();
|
||||
actionPayload = {type: 'changeSlot', value: 7};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '8': {
|
||||
if ('keyDown' === type) {
|
||||
if (!isInventoryOpen) {
|
||||
setHotbarIsHidden(false);
|
||||
if (hotbarHideHandle) {
|
||||
clearTimeout(hotbarHideHandle);
|
||||
}
|
||||
setHotbarHideHandle(setTimeout(() => {
|
||||
setHotbarIsHidden(true);
|
||||
}, 4000));
|
||||
}
|
||||
keepHotbarOpen();
|
||||
actionPayload = {type: 'changeSlot', value: 8};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '9': {
|
||||
if ('keyDown' === type) {
|
||||
if (!isInventoryOpen) {
|
||||
setHotbarIsHidden(false);
|
||||
if (hotbarHideHandle) {
|
||||
clearTimeout(hotbarHideHandle);
|
||||
}
|
||||
setHotbarHideHandle(setTimeout(() => {
|
||||
setHotbarIsHidden(true);
|
||||
}, 4000));
|
||||
}
|
||||
keepHotbarOpen();
|
||||
actionPayload = {type: 'changeSlot', value: 9};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '0': {
|
||||
if ('keyDown' === type) {
|
||||
if (!isInventoryOpen) {
|
||||
setHotbarIsHidden(false);
|
||||
if (hotbarHideHandle) {
|
||||
clearTimeout(hotbarHideHandle);
|
||||
}
|
||||
setHotbarHideHandle(setTimeout(() => {
|
||||
setHotbarIsHidden(true);
|
||||
}, 4000));
|
||||
}
|
||||
keepHotbarOpen();
|
||||
actionPayload = {type: 'changeSlot', value: 10};
|
||||
}
|
||||
break;
|
||||
|
@ -362,15 +305,16 @@ function Ui({disconnected}) {
|
|||
devtoolsIsOpen,
|
||||
hotbarHideHandle,
|
||||
isInventoryOpen,
|
||||
keepHotbarOpen,
|
||||
monopolizers,
|
||||
setDebug,
|
||||
setScale,
|
||||
]);
|
||||
usePacket('EcsChange', async () => {
|
||||
setEcs(new ClientEcs({Components, Systems}));
|
||||
refreshEcs();
|
||||
setMainEntity(undefined);
|
||||
setMonopolizers([]);
|
||||
}, [Components, Systems, setEcs, setMainEntity]);
|
||||
}, [refreshEcs, setMainEntity, setMonopolizers]);
|
||||
usePacket('Tick', async (payload, client) => {
|
||||
if (0 === Object.keys(payload.ecs).length) {
|
||||
return;
|
||||
|
@ -378,6 +322,22 @@ function Ui({disconnected}) {
|
|||
await ecs.apply(payload.ecs);
|
||||
client.emitter.invoke(':Ecs', payload.ecs);
|
||||
}, [ecs]);
|
||||
useEcsTick((payload) => {
|
||||
if (!('1' in payload) || particleWorker) {
|
||||
return
|
||||
}
|
||||
const localParticleWorker = new Worker(
|
||||
new URL('./particle-worker.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
localParticleWorker.postMessage(ecs.get(1).toJSON());
|
||||
setParticleWorker((particleWorker) => {
|
||||
if (particleWorker) {
|
||||
particleWorker.terminate();
|
||||
}
|
||||
return localParticleWorker;
|
||||
});
|
||||
}, [particleWorker]);
|
||||
useEcsTick((payload) => {
|
||||
let localMainEntity = mainEntity;
|
||||
for (const id in payload) {
|
||||
|
@ -397,15 +357,34 @@ function Ui({disconnected}) {
|
|||
if (update.MainEntity) {
|
||||
setMainEntity(localMainEntity = id);
|
||||
}
|
||||
if (localMainEntity === id) {
|
||||
if (update.Inventory) {
|
||||
if (localMainEntity === id) {
|
||||
setBufferSlot(entity.Inventory.item(0));
|
||||
const newHotbarSlots = emptySlots();
|
||||
for (let i = 1; i < 11; ++i) {
|
||||
newHotbarSlots[i - 1] = entity.Inventory.item(i);
|
||||
const newInventorySlots = emptySlots();
|
||||
for (let i = 1; i < 41; ++i) {
|
||||
newInventorySlots[i - 1] = entity.Inventory.item(i);
|
||||
}
|
||||
setHotbarSlots(newHotbarSlots);
|
||||
setInventorySlots(newInventorySlots);
|
||||
}
|
||||
else if (update.Inventory.slots) {
|
||||
const newInventorySlots = Array(30).fill(undefined);
|
||||
for (let i = 0; i < 30; ++i) {
|
||||
newInventorySlots[i] = entity.Inventory.item(i);
|
||||
}
|
||||
setExternalInventory(entity.id)
|
||||
setExternalInventorySlots(newInventorySlots);
|
||||
setIsInventoryOpen(true);
|
||||
setHotbarIsHidden(false);
|
||||
if (hotbarHideHandle) {
|
||||
clearTimeout(hotbarHideHandle);
|
||||
}
|
||||
}
|
||||
else if (update.Inventory.closed) {
|
||||
setExternalInventory();
|
||||
setExternalInventorySlots();
|
||||
}
|
||||
}
|
||||
if (localMainEntity === id) {
|
||||
if (update.Wielder && 'activeSlot' in update.Wielder) {
|
||||
setActiveSlot(update.Wielder.activeSlot);
|
||||
}
|
||||
|
@ -419,7 +398,7 @@ function Ui({disconnected}) {
|
|||
setCamera({x, y});
|
||||
}
|
||||
}
|
||||
}, [camera, ecs, mainEntity, scale]);
|
||||
}, [camera, ecs, hotbarHideHandle, mainEntity, scale]);
|
||||
useEffect(() => {
|
||||
function onContextMenu(event) {
|
||||
event.preventDefault();
|
||||
|
@ -558,9 +537,9 @@ function Ui({disconnected}) {
|
|||
ref={gameRef}
|
||||
>
|
||||
<Pixi
|
||||
applyFilters={applyFilters}
|
||||
camera={camera}
|
||||
monopolizers={monopolizers}
|
||||
particleWorker={particleWorker}
|
||||
scale={scale}
|
||||
/>
|
||||
<Dom>
|
||||
|
@ -568,17 +547,36 @@ function Ui({disconnected}) {
|
|||
active={activeSlot}
|
||||
hotbarIsHidden={hotbarIsHidden}
|
||||
onActivate={(i) => {
|
||||
keepHotbarOpen();
|
||||
client.send({
|
||||
type: 'Action',
|
||||
payload: {type: 'swapSlots', value: [0, i + 1]},
|
||||
payload: {type: 'swapSlots', value: [0, mainEntity, i + 1]},
|
||||
});
|
||||
}}
|
||||
slots={hotbarSlots}
|
||||
slots={inventorySlots.slice(0, 10)}
|
||||
/>
|
||||
<Bag
|
||||
isInventoryOpen={isInventoryOpen}
|
||||
slots={Array(30).fill(undefined)}
|
||||
onActivate={(i) => {
|
||||
client.send({
|
||||
type: 'Action',
|
||||
payload: {type: 'swapSlots', value: [0, mainEntity, i + 11]},
|
||||
});
|
||||
}}
|
||||
slots={inventorySlots.slice(10, 20)}
|
||||
/>
|
||||
{externalInventory && (
|
||||
<External
|
||||
isInventoryOpen={isInventoryOpen}
|
||||
onActivate={(i) => {
|
||||
client.send({
|
||||
type: 'Action',
|
||||
payload: {type: 'swapSlots', value: [0, externalInventory, i]},
|
||||
});
|
||||
}}
|
||||
slots={externalInventorySlots}
|
||||
/>
|
||||
)}
|
||||
<Entities
|
||||
camera={camera}
|
||||
scale={scale}
|
||||
|
@ -607,15 +605,15 @@ function Ui({disconnected}) {
|
|||
)}
|
||||
</Dom>
|
||||
</div>
|
||||
{devtoolsIsOpen && (
|
||||
<div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
|
||||
<Devtools
|
||||
applyFilters={applyFilters}
|
||||
eventsChannel={devEventsChannel}
|
||||
setApplyFilters={setApplyFilters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Ui);
|
||||
export default Ui;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {Texture} from '@pixi/core';
|
||||
import {Assets} from '@pixi/assets';
|
||||
import {createContext, useContext, useEffect} from 'react';
|
||||
|
||||
|
@ -11,7 +12,7 @@ export function useAsset(source) {
|
|||
const [assets, setAssets] = useContext(context);
|
||||
useEffect(() => {
|
||||
if (!source) {
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
if (!assets[source]) {
|
||||
if (!loading[source]) {
|
||||
|
@ -24,5 +25,5 @@ export function useAsset(source) {
|
|||
}
|
||||
}
|
||||
}, [assets, setAssets, source]);
|
||||
return source ? assets[source] : undefined;
|
||||
return source ? assets[source] : {data: {meta: {}}, textures: {'': Texture.WHITE}};
|
||||
}
|
||||
|
|
19
app/react/hooks/use-animation-frame.js
Normal file
19
app/react/hooks/use-animation-frame.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {useEffect, useRef} from 'react';
|
||||
|
||||
export default function useAnimationFrame(callback) {
|
||||
const handle = useRef();
|
||||
const last = useRef();
|
||||
function animate(time) {
|
||||
if (last.current != undefined) {
|
||||
callback((time - last.current) / 1000)
|
||||
}
|
||||
last.current = time;
|
||||
handle.current = requestAnimationFrame(animate);
|
||||
}
|
||||
useEffect(() => {
|
||||
handle.current = requestAnimationFrame(animate);
|
||||
return () => {
|
||||
cancelAnimationFrame(handle.current);
|
||||
};
|
||||
}, [callback]);
|
||||
}
|
|
@ -14,6 +14,7 @@ export default function createEcs(Ecs) {
|
|||
'PlantGrowth',
|
||||
'FollowCamera',
|
||||
'VisibleAabbs',
|
||||
'MaintainColliderHash',
|
||||
'Colliders',
|
||||
'ControlDirection',
|
||||
'SpriteDirection',
|
||||
|
@ -21,6 +22,8 @@ export default function createEcs(Ecs) {
|
|||
'RunTickingPromises',
|
||||
'Water',
|
||||
'Interactions',
|
||||
'InventoryCloser',
|
||||
'KillPerishable',
|
||||
];
|
||||
defaultSystems.forEach((defaultSystem) => {
|
||||
const System = ecs.system(defaultSystem);
|
||||
|
|
|
@ -98,13 +98,14 @@ export default async function createHomestead(id) {
|
|||
Interactive: {
|
||||
interacting: 1,
|
||||
interactScript: `
|
||||
subject.Interlocutor.dialogue({
|
||||
body: "Sure, I'm a treasure chest. Probably. Do you really think that means you're about to get some treasure? Hah!",
|
||||
monopolizer: true,
|
||||
offset: {x: 0, y: -48},
|
||||
origin: 'track',
|
||||
position: 'track',
|
||||
})
|
||||
initiator.Player.openInventory = subject.Inventory;
|
||||
// subject.Interlocutor.dialogue({
|
||||
// body: "Sure, I'm a treasure chest. Probably. Do you really think that means you're about to get some treasure? Hah!",
|
||||
// monopolizer: true,
|
||||
// offset: {x: 0, y: -48},
|
||||
// origin: 'track',
|
||||
// position: 'track',
|
||||
// })
|
||||
`,
|
||||
},
|
||||
Interlocutor: {},
|
||||
|
@ -132,7 +133,7 @@ export default async function createHomestead(id) {
|
|||
Sprite: {
|
||||
anchorX: 0.5,
|
||||
anchorY: 0.7,
|
||||
source: '/assets/chest.json',
|
||||
source: '/assets/chest/chest.json',
|
||||
},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
|
@ -174,7 +175,7 @@ export default async function createHomestead(id) {
|
|||
subject.Interlocutor.dialogue({
|
||||
body: line,
|
||||
linger: 2,
|
||||
offset: {x: 0, y: -32},
|
||||
offset: {x: 0, y: -16},
|
||||
origin: 'track',
|
||||
position: 'track',
|
||||
})
|
||||
|
@ -194,7 +195,7 @@ export default async function createHomestead(id) {
|
|||
VisibleAabb: {},
|
||||
Vulnerable: {},
|
||||
};
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
for (let i = 0; i < 50; ++i) {
|
||||
entities.push(kitty);
|
||||
}
|
||||
entities.push({
|
||||
|
|
|
@ -33,7 +33,7 @@ export default async function createPlayer(id) {
|
|||
},
|
||||
},
|
||||
Health: {health: 100},
|
||||
Light: {},
|
||||
Light: {brightness: 0},
|
||||
Magnet: {strength: 24},
|
||||
Player: {},
|
||||
Position: {x: 128, y: 448},
|
||||
|
|
|
@ -168,7 +168,7 @@ export default class Engine {
|
|||
Interlocutor.dialogue({
|
||||
body: payload.value,
|
||||
linger: 5,
|
||||
offset: {x: 0, y: -40},
|
||||
offset: {x: 0, y: -24},
|
||||
origin: 'track',
|
||||
position: 'track',
|
||||
});
|
||||
|
@ -201,7 +201,11 @@ export default class Engine {
|
|||
}
|
||||
case 'swapSlots': {
|
||||
if (!Controlled.locked) {
|
||||
Inventory.swapSlots(...payload.value);
|
||||
const [l, other, r] = payload.value;
|
||||
const {Inventory: OtherInventory} = ecs.get(other);
|
||||
if (OtherInventory) {
|
||||
Inventory.swapSlots(l, OtherInventory, r);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -413,7 +417,6 @@ export default class Engine {
|
|||
updateFor(connection) {
|
||||
const update = {};
|
||||
const {entity, memory} = this.connectedPlayers.get(connection);
|
||||
const mainEntityId = entity.id;
|
||||
const ecs = this.ecses[entity.Ecs.path];
|
||||
// Entities within half a screen offscreen.
|
||||
const x0 = entity.Position.x - RESOLUTION.x;
|
||||
|
@ -429,17 +432,24 @@ export default class Engine {
|
|||
nearby.add(master);
|
||||
const lastNearby = new Set(memory.nearby.values());
|
||||
const firstUpdate = 0 === lastNearby.size;
|
||||
for (const entity of nearby) {
|
||||
const {id} = entity;
|
||||
for (const nearbyEntity of nearby) {
|
||||
const {id} = nearbyEntity;
|
||||
lastNearby.delete(id);
|
||||
if (!memory.nearby.has(id)) {
|
||||
update[id] = entity.toNet();
|
||||
if (mainEntityId === id) {
|
||||
update[id] = nearbyEntity.toNet(entity);
|
||||
if (entity.id === id) {
|
||||
update[id].MainEntity = {};
|
||||
}
|
||||
}
|
||||
else if (ecs.diff[id]) {
|
||||
update[id] = ecs.diff[id];
|
||||
const nearbyEntityDiff = {};
|
||||
for (const componentName in ecs.diff[id]) {
|
||||
nearbyEntityDiff[componentName] = nearbyEntity[componentName].toNet(
|
||||
entity,
|
||||
ecs.diff[id][componentName],
|
||||
);
|
||||
}
|
||||
update[id] = nearbyEntityDiff;
|
||||
}
|
||||
memory.nearby.add(id);
|
||||
}
|
||||
|
@ -447,6 +457,7 @@ export default class Engine {
|
|||
memory.nearby.delete(id);
|
||||
update[id] = false;
|
||||
}
|
||||
entity.updateAttachments(update);
|
||||
// Tile layer chunking
|
||||
const {TileLayers} = master;
|
||||
const {layers} = TileLayers;
|
||||
|
|
159
app/util/easing.js
Normal file
159
app/util/easing.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
/* eslint-disable */
|
||||
export function linear(t, b, c, d) {
|
||||
return b + c * t/d
|
||||
}
|
||||
|
||||
export function easeInQuad(t, b, c, d) {
|
||||
return c*(t/=d)*t + b;
|
||||
}
|
||||
|
||||
export function easeOutQuad(t, b, c, d) {
|
||||
return -c *(t/=d)*(t-2) + b;
|
||||
}
|
||||
|
||||
export function easeInOutQuad(t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t + b;
|
||||
return -c/2 * ((--t)*(t-2) - 1) + b;
|
||||
}
|
||||
|
||||
export function easeInCubic(t, b, c, d) {
|
||||
return c*(t/=d)*t*t + b;
|
||||
}
|
||||
|
||||
export function easeOutCubic(t, b, c, d) {
|
||||
return c*((t=t/d-1)*t*t + 1) + b;
|
||||
}
|
||||
|
||||
export function easeInOutCubic(t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t*t + b;
|
||||
return c/2*((t-=2)*t*t + 2) + b;
|
||||
}
|
||||
|
||||
export function easeInQuart(t, b, c, d) {
|
||||
return c*(t/=d)*t*t*t + b;
|
||||
}
|
||||
|
||||
export function easeOutQuart(t, b, c, d) {
|
||||
return -c * ((t=t/d-1)*t*t*t - 1) + b;
|
||||
}
|
||||
|
||||
export function easeInOutQuart(t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t*t*t + b;
|
||||
return -c/2 * ((t-=2)*t*t*t - 2) + b;
|
||||
}
|
||||
|
||||
export function easeInQuint(t, b, c, d) {
|
||||
return c*(t/=d)*t*t*t*t + b;
|
||||
}
|
||||
|
||||
export function easeOutQuint(t, b, c, d) {
|
||||
return c*((t=t/d-1)*t*t*t*t + 1) + b;
|
||||
}
|
||||
|
||||
export function easeInOutQuint(t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b;
|
||||
return c/2*((t-=2)*t*t*t*t + 2) + b;
|
||||
}
|
||||
|
||||
export function easeInSine(t, b, c, d) {
|
||||
return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
|
||||
}
|
||||
|
||||
export function easeOutSine(t, b, c, d) {
|
||||
return c * Math.sin(t/d * (Math.PI/2)) + b;
|
||||
}
|
||||
|
||||
export function easeInOutSine(t, b, c, d) {
|
||||
return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
|
||||
}
|
||||
|
||||
export function easeInExpo(t, b, c, d) {
|
||||
return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
|
||||
}
|
||||
|
||||
export function easeOutExpo(t, b, c, d) {
|
||||
return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
|
||||
}
|
||||
|
||||
export function easeInOutExpo(t, b, c, d) {
|
||||
if (t==0) return b;
|
||||
if (t==d) return b+c;
|
||||
if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
|
||||
return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
|
||||
}
|
||||
|
||||
export function easeInCirc(t, b, c, d) {
|
||||
return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b;
|
||||
}
|
||||
|
||||
export function easeOutCirc(t, b, c, d) {
|
||||
return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
|
||||
}
|
||||
|
||||
export function easeInOutCirc(t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b;
|
||||
return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
|
||||
}
|
||||
|
||||
export function easeInElastic(t, b, c, d) {
|
||||
var s=1.70158;var p=0;var a=c;
|
||||
if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3;
|
||||
if (a < Math.abs(c)) { a=c; var s=p/4; }
|
||||
else var s = p/(2*Math.PI) * Math.asin (c/a);
|
||||
return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
|
||||
}
|
||||
|
||||
export function easeOutElastic(t, b, c, d) {
|
||||
var s=1.70158;var p=0;var a=c;
|
||||
if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3;
|
||||
if (a < Math.abs(c)) { a=c; var s=p/4; }
|
||||
else var s = p/(2*Math.PI) * Math.asin (c/a);
|
||||
return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
|
||||
}
|
||||
|
||||
export function easeInOutElastic(t, b, c, d) {
|
||||
var s=1.70158;var p=0;var a=c;
|
||||
if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5);
|
||||
if (a < Math.abs(c)) { a=c; var s=p/4; }
|
||||
else var s = p/(2*Math.PI) * Math.asin (c/a);
|
||||
if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
|
||||
return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b;
|
||||
}
|
||||
|
||||
export function easeInBack(t, b, c, d, s) {
|
||||
if (s == undefined) s = 1.70158;
|
||||
return c*(t/=d)*t*((s+1)*t - s) + b;
|
||||
}
|
||||
|
||||
export function easeOutBack(t, b, c, d, s) {
|
||||
if (s == undefined) s = 1.70158;
|
||||
return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
|
||||
}
|
||||
|
||||
export function easeInOutBack(t, b, c, d, s) {
|
||||
if (s == undefined) s = 1.70158;
|
||||
if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
|
||||
return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
|
||||
}
|
||||
|
||||
export function easeInBounce(t, b, c, d) {
|
||||
return c - easeOutBounce(d-t, 0, c, d) + b;
|
||||
}
|
||||
|
||||
export function easeOutBounce(t, b, c, d) {
|
||||
if ((t/=d) < (1/2.75)) {
|
||||
return c*(7.5625*t*t) + b;
|
||||
} else if (t < (2/2.75)) {
|
||||
return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
|
||||
} else if (t < (2.5/2.75)) {
|
||||
return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
|
||||
} else {
|
||||
return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
|
||||
}
|
||||
}
|
||||
|
||||
export function easeInOutBounce(t, b, c, d) {
|
||||
if (t < d/2) return easeInBounce(t*2, 0, c, d) * .5 + b;
|
||||
return easeOutBounce(t*2-d, 0, c, d) * .5 + c*.5 + b;
|
||||
}
|
||||
/* eslint-enable */
|
|
@ -24,13 +24,14 @@ export const cache = new LRUCache({
|
|||
|
||||
export default class Script {
|
||||
|
||||
constructor(sandbox) {
|
||||
constructor(sandbox, code) {
|
||||
this.code = code;
|
||||
this.sandbox = sandbox;
|
||||
this.promise = null;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new this.constructor(this.sandbox.clone());
|
||||
return new this.constructor(this.sandbox.clone(), this.code);
|
||||
}
|
||||
|
||||
get context() {
|
||||
|
@ -76,6 +77,7 @@ export default class Script {
|
|||
}
|
||||
return new this(
|
||||
new Sandbox(await cache.get(code), this.createContext(context)),
|
||||
code,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -102,7 +104,19 @@ export default class Script {
|
|||
}
|
||||
while (true) {
|
||||
this.sandbox.context.elapsed = elapsed;
|
||||
const {async, done, value} = this.sandbox.step();
|
||||
let async, done, value;
|
||||
try {
|
||||
({async, done, value} = this.sandbox.step());
|
||||
}
|
||||
catch (error) {
|
||||
const node = this.sandbox.$$execution.stack.pop();
|
||||
console.warn('Script ran into a problem at', this.code.slice(node.start, node.end));
|
||||
console.warn(error);
|
||||
if (resolve) {
|
||||
resolve();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (async) {
|
||||
this.promise = value;
|
||||
value
|
||||
|
|
|
@ -1,136 +1,6 @@
|
|||
import {Ticker} from '@/util/promise.js';
|
||||
|
||||
/* eslint-disable */
|
||||
const Easing = {
|
||||
linear: function (t, b, c, d) {
|
||||
return b + c * t/d
|
||||
},
|
||||
easeInQuad: function (t, b, c, d) {
|
||||
return c*(t/=d)*t + b;
|
||||
},
|
||||
easeOutQuad: function (t, b, c, d) {
|
||||
return -c *(t/=d)*(t-2) + b;
|
||||
},
|
||||
easeInOutQuad: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t + b;
|
||||
return -c/2 * ((--t)*(t-2) - 1) + b;
|
||||
},
|
||||
easeInCubic: function (t, b, c, d) {
|
||||
return c*(t/=d)*t*t + b;
|
||||
},
|
||||
easeOutCubic: function (t, b, c, d) {
|
||||
return c*((t=t/d-1)*t*t + 1) + b;
|
||||
},
|
||||
easeInOutCubic: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t*t + b;
|
||||
return c/2*((t-=2)*t*t + 2) + b;
|
||||
},
|
||||
easeInQuart: function (t, b, c, d) {
|
||||
return c*(t/=d)*t*t*t + b;
|
||||
},
|
||||
easeOutQuart: function (t, b, c, d) {
|
||||
return -c * ((t=t/d-1)*t*t*t - 1) + b;
|
||||
},
|
||||
easeInOutQuart: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t*t*t + b;
|
||||
return -c/2 * ((t-=2)*t*t*t - 2) + b;
|
||||
},
|
||||
easeInQuint: function (t, b, c, d) {
|
||||
return c*(t/=d)*t*t*t*t + b;
|
||||
},
|
||||
easeOutQuint: function (t, b, c, d) {
|
||||
return c*((t=t/d-1)*t*t*t*t + 1) + b;
|
||||
},
|
||||
easeInOutQuint: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b;
|
||||
return c/2*((t-=2)*t*t*t*t + 2) + b;
|
||||
},
|
||||
easeInSine: function (t, b, c, d) {
|
||||
return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
|
||||
},
|
||||
easeOutSine: function (t, b, c, d) {
|
||||
return c * Math.sin(t/d * (Math.PI/2)) + b;
|
||||
},
|
||||
easeInOutSine: function (t, b, c, d) {
|
||||
return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
|
||||
},
|
||||
easeInExpo: function (t, b, c, d) {
|
||||
return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
|
||||
},
|
||||
easeOutExpo: function (t, b, c, d) {
|
||||
return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
|
||||
},
|
||||
easeInOutExpo: function (t, b, c, d) {
|
||||
if (t==0) return b;
|
||||
if (t==d) return b+c;
|
||||
if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
|
||||
return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
|
||||
},
|
||||
easeInCirc: function (t, b, c, d) {
|
||||
return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b;
|
||||
},
|
||||
easeOutCirc: function (t, b, c, d) {
|
||||
return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
|
||||
},
|
||||
easeInOutCirc: function (t, b, c, d) {
|
||||
if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b;
|
||||
return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
|
||||
},
|
||||
easeInElastic: function (t, b, c, d) {
|
||||
var s=1.70158;var p=0;var a=c;
|
||||
if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3;
|
||||
if (a < Math.abs(c)) { a=c; var s=p/4; }
|
||||
else var s = p/(2*Math.PI) * Math.asin (c/a);
|
||||
return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
|
||||
},
|
||||
easeOutElastic: function (t, b, c, d) {
|
||||
var s=1.70158;var p=0;var a=c;
|
||||
if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3;
|
||||
if (a < Math.abs(c)) { a=c; var s=p/4; }
|
||||
else var s = p/(2*Math.PI) * Math.asin (c/a);
|
||||
return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
|
||||
},
|
||||
easeInOutElastic: function (t, b, c, d) {
|
||||
var s=1.70158;var p=0;var a=c;
|
||||
if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5);
|
||||
if (a < Math.abs(c)) { a=c; var s=p/4; }
|
||||
else var s = p/(2*Math.PI) * Math.asin (c/a);
|
||||
if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
|
||||
return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b;
|
||||
},
|
||||
easeInBack: function (t, b, c, d, s) {
|
||||
if (s == undefined) s = 1.70158;
|
||||
return c*(t/=d)*t*((s+1)*t - s) + b;
|
||||
},
|
||||
easeOutBack: function (t, b, c, d, s) {
|
||||
if (s == undefined) s = 1.70158;
|
||||
return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
|
||||
},
|
||||
easeInOutBack: function (t, b, c, d, s) {
|
||||
if (s == undefined) s = 1.70158;
|
||||
if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
|
||||
return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
|
||||
},
|
||||
easeInBounce: function (t, b, c, d) {
|
||||
return c - easing.easeOutBounce (d-t, 0, c, d) + b;
|
||||
},
|
||||
easeOutBounce: function (t, b, c, d) {
|
||||
if ((t/=d) < (1/2.75)) {
|
||||
return c*(7.5625*t*t) + b;
|
||||
} else if (t < (2/2.75)) {
|
||||
return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
|
||||
} else if (t < (2.5/2.75)) {
|
||||
return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
|
||||
} else {
|
||||
return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
|
||||
}
|
||||
},
|
||||
easeInOutBounce: function (t, b, c, d) {
|
||||
if (t < d/2) return easing.easeInBounce (t*2, 0, c, d) * .5 + b;
|
||||
return easing.easeOutBounce (t*2-d, 0, c, d) * .5 + c*.5 + b;
|
||||
}
|
||||
};
|
||||
/* eslint-enable */
|
||||
import * as Easing from './easing';
|
||||
|
||||
export default function transition(object, properties) {
|
||||
const transitions = {};
|
||||
|
|
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -26,6 +26,7 @@
|
|||
"express": "^4.18.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"isbot": "^4.1.0",
|
||||
"kefir": "^3.8.8",
|
||||
"lru-cache": "^10.2.2",
|
||||
"morgan": "^1.10.0",
|
||||
"pixi.js": "^7.4.2",
|
||||
|
@ -12697,6 +12698,11 @@
|
|||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kefir": {
|
||||
"version": "3.8.8",
|
||||
"resolved": "https://registry.npmjs.org/kefir/-/kefir-3.8.8.tgz",
|
||||
"integrity": "sha512-xWga7QCZsR2Wjy2vNL3Kq/irT+IwxwItEWycRRlT5yhqHZK2fmEhziP+LzcJBWSTAMranGKtGTQ6lFpyJS3+jA=="
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"express": "^4.18.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"isbot": "^4.1.0",
|
||||
"kefir": "^3.8.8",
|
||||
"lru-cache": "^10.2.2",
|
||||
"morgan": "^1.10.0",
|
||||
"pixi.js": "^7.4.2",
|
||||
|
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
@ -9,104 +9,30 @@ if (projected?.length > 0) {
|
|||
Controlled.locked = 1
|
||||
const [, direction] = Sprite.animation.split(':')
|
||||
|
||||
const dirtParticles = {
|
||||
behaviors: [
|
||||
{
|
||||
type: 'moveAcceleration',
|
||||
config: {
|
||||
accel: {
|
||||
x: 0,
|
||||
y: 200,
|
||||
},
|
||||
minStart: 0,
|
||||
maxStart: 0,
|
||||
rotate: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'moveSpeed',
|
||||
config: {
|
||||
speed: {
|
||||
list: [
|
||||
{
|
||||
time: 0,
|
||||
value: 60
|
||||
},
|
||||
{
|
||||
time: 1,
|
||||
value: 10
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'rotation',
|
||||
config: {
|
||||
accel: 0,
|
||||
minSpeed: 0,
|
||||
maxSpeed: 0,
|
||||
minStart: 225,
|
||||
maxStart: 320
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'scale',
|
||||
config: {
|
||||
scale: {
|
||||
list: [
|
||||
{
|
||||
value: 0.25,
|
||||
time: 0,
|
||||
},
|
||||
{
|
||||
value: 0.125,
|
||||
time: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'textureSingle',
|
||||
config: {
|
||||
texture: 'tileset/7',
|
||||
}
|
||||
},
|
||||
],
|
||||
lifetime: {
|
||||
min: 0.25,
|
||||
max: 0.25,
|
||||
},
|
||||
frequency: 0.01,
|
||||
emitterLifetime: 0.25,
|
||||
pos: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
};
|
||||
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
Sound.play('/assets/hoe/dig.wav');
|
||||
for (const {x, y} of projected) {
|
||||
Emitter.emit({
|
||||
...dirtParticles,
|
||||
behaviors: [
|
||||
...dirtParticles.behaviors,
|
||||
{
|
||||
type: 'spawnShape',
|
||||
config: {
|
||||
type: 'rect',
|
||||
data: {
|
||||
x: x * layer.tileSize.x,
|
||||
y: y * layer.tileSize.y,
|
||||
w: layer.tileSize.x,
|
||||
h: layer.tileSize.y,
|
||||
count: 25,
|
||||
frequency: 0.01,
|
||||
shape: {
|
||||
type: 'filledRect',
|
||||
payload: {width: 16, height: 16},
|
||||
},
|
||||
entity: {
|
||||
Forces: {forceY: -50},
|
||||
Position: {
|
||||
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
||||
y: y * layer.tileSize.y + (layer.tileSize.y / 2),
|
||||
},
|
||||
Sprite: {
|
||||
scaleX: 0.2,
|
||||
scaleY: 0.2,
|
||||
tint: 0x552200,
|
||||
},
|
||||
Ttl: {ttl: 0.125},
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
}
|
||||
Sprite.animation = ['moving', direction].join(':');
|
||||
await wait(0.3)
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.4 KiB |
|
@ -29,6 +29,7 @@ for (let i = 0; i < N; ++i) {
|
|||
Controlled: {},
|
||||
Direction: {direction: Math.TAU * (i / N)},
|
||||
Forces: {},
|
||||
Light: {brightness: 0},
|
||||
Owned: {owner: Player ? Player.id : 0},
|
||||
Position: {x: Position.x, y: Position.y},
|
||||
Speed: {},
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
const {Sprite, Ticking} = entity;
|
||||
const {Sprite, Ticking, Vulnerable} = entity;
|
||||
if (Vulnerable) {
|
||||
Vulnerable.isInvulnerable = 1;
|
||||
}
|
||||
if (Sprite) {
|
||||
const {promise} = transition(
|
||||
entity.Sprite,
|
||||
|
|
|
@ -17,15 +17,15 @@ for (let i = 0; i < 10; ++i) {
|
|||
],
|
||||
},
|
||||
],
|
||||
collisionStartScript: `
|
||||
if (other.Inventory) {
|
||||
other.Inventory.give({
|
||||
qty: 1,
|
||||
source: '/assets/tomato/tomato.json',
|
||||
})
|
||||
ecs.destroy(entity.id)
|
||||
}
|
||||
`,
|
||||
collisionStartScript: [
|
||||
'if (other.Inventory) {',
|
||||
' other.Inventory.give({',
|
||||
' qty: 1,',
|
||||
" source: '/assets/tomato/tomato.json',",
|
||||
' })',
|
||||
' ecs.destroy(entity.id)',
|
||||
'}',
|
||||
].join('\n'),
|
||||
},
|
||||
Forces: {},
|
||||
Magnetic: {},
|
||||
|
|
|
@ -6,7 +6,7 @@ const filtered = []
|
|||
for (const position of projected) {
|
||||
const x0 = position.x * tileSize.x;
|
||||
const y0 = position.y * tileSize.y;
|
||||
const entities = ecs.system('Colliders').within({
|
||||
const entities = ecs.system('MaintainColliderHash').within({
|
||||
x0,
|
||||
x1: x0 + tileSize.x - 1,
|
||||
y0,
|
||||
|
|
|
@ -38,84 +38,27 @@ if (projected?.length > 0) {
|
|||
VisibleAabb: {},
|
||||
};
|
||||
|
||||
const seedParticles = {
|
||||
behaviors: [
|
||||
{
|
||||
type: 'moveAcceleration',
|
||||
config: {
|
||||
accel: {
|
||||
x: 0,
|
||||
y: 400,
|
||||
},
|
||||
minStart: 0,
|
||||
maxStart: 0,
|
||||
rotate: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'moveSpeed',
|
||||
config: {
|
||||
speed: {
|
||||
list: [
|
||||
{
|
||||
time: 0,
|
||||
value: 100
|
||||
},
|
||||
{
|
||||
time: 1,
|
||||
value: 10
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'rotation',
|
||||
config: {
|
||||
accel: 0,
|
||||
minSpeed: 0,
|
||||
maxSpeed: 0,
|
||||
minStart: 225,
|
||||
maxStart: 320
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'scale',
|
||||
config: {
|
||||
scale: {
|
||||
list: [
|
||||
{
|
||||
value: 0.75,
|
||||
time: 0,
|
||||
},
|
||||
{
|
||||
value: 0.5,
|
||||
time: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'textureSingle',
|
||||
config: {
|
||||
texture: 'tomato-plant/tomato-plant/0',
|
||||
}
|
||||
},
|
||||
],
|
||||
lifetime: {
|
||||
min: 0.25,
|
||||
max: 0.25,
|
||||
},
|
||||
Emitter.emit({
|
||||
count: 25,
|
||||
frequency: 0.01,
|
||||
emitterLifetime: 0.75,
|
||||
pos: {
|
||||
shape: {
|
||||
type: 'filledRect',
|
||||
payload: {width: 16, height: 16},
|
||||
},
|
||||
entity: {
|
||||
Forces: {forceY: -100},
|
||||
Position: {
|
||||
x: Position.x,
|
||||
y: Position.y,
|
||||
},
|
||||
};
|
||||
|
||||
Emitter.emit(seedParticles)
|
||||
Sprite: {
|
||||
scaleX: 0.125,
|
||||
scaleY: 0.125,
|
||||
tint: 0x221100,
|
||||
},
|
||||
Ttl: {ttl: 0.25},
|
||||
}
|
||||
});
|
||||
|
||||
Sound.play('/assets/sow.wav');
|
||||
Sprite.animation = ['moving', direction].join(':');
|
||||
|
|
|
@ -10,98 +10,32 @@ if (projected?.length > 0) {
|
|||
const [, direction] = Sprite.animation.split(':')
|
||||
Sprite.animation = ['idle', direction].join(':');
|
||||
|
||||
const waterParticles = {
|
||||
behaviors: [
|
||||
{
|
||||
type: 'moveAcceleration',
|
||||
config: {
|
||||
accel: {
|
||||
x: 0,
|
||||
y: 1500,
|
||||
},
|
||||
minStart: 0,
|
||||
maxStart: 0,
|
||||
rotate: false,
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'moveSpeed',
|
||||
config: {
|
||||
speed: {
|
||||
list: [
|
||||
{
|
||||
time: 0,
|
||||
value: 30
|
||||
},
|
||||
{
|
||||
time: 1,
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'scale',
|
||||
config: {
|
||||
scale: {
|
||||
list: [
|
||||
{
|
||||
value: 0.25,
|
||||
time: 0,
|
||||
},
|
||||
{
|
||||
value: 0.125,
|
||||
time: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'textureSingle',
|
||||
config: {
|
||||
texture: 'tileset/38',
|
||||
}
|
||||
},
|
||||
],
|
||||
lifetime: {
|
||||
min: 0.25,
|
||||
max: 0.25,
|
||||
},
|
||||
frequency: 0.01,
|
||||
emitterLifetime: 0.25,
|
||||
pos: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
rotation: 0,
|
||||
};
|
||||
|
||||
Sound.play('/assets/watering-can/water.wav');
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
|
||||
for (const {x, y} of projected) {
|
||||
Emitter.emit({
|
||||
...waterParticles,
|
||||
behaviors: [
|
||||
...waterParticles.behaviors,
|
||||
{
|
||||
type: 'spawnShape',
|
||||
config: {
|
||||
type: 'rect',
|
||||
data: {
|
||||
x: x * layer.tileSize.x,
|
||||
y: y * layer.tileSize.y - (layer.tileSize.y * 0.5),
|
||||
w: layer.tileSize.x,
|
||||
h: layer.tileSize.y,
|
||||
count: 25,
|
||||
frequency: 0.01,
|
||||
shape: {
|
||||
type: 'filledRect',
|
||||
payload: {width: 16, height: 16},
|
||||
},
|
||||
entity: {
|
||||
Forces: {forceY: 100},
|
||||
Position: {
|
||||
x: x * layer.tileSize.x + (layer.tileSize.x / 2),
|
||||
y: y * layer.tileSize.y - layer.tileSize.y,
|
||||
},
|
||||
Sprite: {
|
||||
scaleX: 0.2,
|
||||
scaleY: 0.2,
|
||||
tint: 0x0022aa,
|
||||
},
|
||||
Ttl: {ttl: 0.25},
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
await wait(0.25);
|
||||
}
|
||||
await wait(0.5);
|
||||
|
||||
for (const {x, y} of projected) {
|
||||
const tileIndex = layer.area.x * y + x
|
||||
|
|
Loading…
Reference in New Issue
Block a user