Compare commits

...

24 Commits

Author SHA1 Message Date
cha0s
c9e7b33d25 perf: damage 2024-07-31 00:06:29 -05:00
cha0s
01d63e5ee0 perf: less churn 2024-07-30 22:58:24 -05:00
cha0s
a1a6b98639 feat: useAnimationFrame 2024-07-30 22:58:03 -05:00
cha0s
3191ee83d4 refactor: easing 2024-07-30 22:57:52 -05:00
cha0s
d168b9b580 refactor: damage scaling 2024-07-30 17:21:18 -05:00
cha0s
83d8924517 refactor: invulnerability 2024-07-30 17:21:06 -05:00
cha0s
4b46b2f0ab perf: damage clumping 2024-07-30 17:06:04 -05:00
cha0s
2becc0afb9 refactor: brightness 2024-07-30 17:05:33 -05:00
cha0s
32fa9ee257 refactor: simplify splice 2024-07-30 17:05:15 -05:00
cha0s
4f59ddd731 fun: test kittens 2024-07-30 17:03:22 -05:00
cha0s
ba360d6b4f revert: double tick 2024-07-30 17:02:49 -05:00
cha0s
5a43859e56 refactor: camera/offsets 2024-07-30 14:05:27 -05:00
cha0s
723d2fc9c3 refactor: filters 2024-07-30 12:33:27 -05:00
cha0s
392735cf99 perf: no change tracking for client ecs 2024-07-30 11:51:30 -05:00
cha0s
90ab588133 perf: separate insert/update 2024-07-30 11:46:04 -05:00
cha0s
64df3b882f refactor: particles 2024-07-30 09:56:53 -05:00
cha0s
b719e3227b feat: ticking destruction dependencies 2024-07-29 07:44:12 -05:00
cha0s
82e37f9b91 fix: perf and group 2024-07-28 18:51:52 -05:00
cha0s
1e588900bb fix: no templates for now 2024-07-28 18:51:38 -05:00
cha0s
4777a2a3a5 fun: an actual chest 2024-07-28 18:42:28 -05:00
cha0s
99b8d0f633 refactor: collision systems 2024-07-28 18:40:58 -05:00
cha0s
ab626f8f9a refactor: extremely cheap ass error reports 2024-07-28 10:37:07 -05:00
cha0s
ce9a1aeba7 refactor: inventory UI 2024-07-28 08:50:09 -05:00
cha0s
b37d3513f6 refactor: light prep 2024-07-27 15:28:08 -05:00
74 changed files with 1614 additions and 1169 deletions

View File

@ -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: {

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -1,13 +1,38 @@
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) {
Component.markChange(this.entity, 'emit', {[this.id++]: 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});
}
}
};
}

View File

@ -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);
}
}
}

View File

@ -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'},
};
}

View File

@ -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;
}
};
}
}

View File

@ -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,17 +70,40 @@ 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) {
instance.$$sourceJson = await this.ecs.readJson(instance.source);
if (instance.source) {
instance.$$sourceJson = await this.ecs.readJson(instance.source);
}
}
markChange(entityId, key, value) {
if ('elapsed' === key) {
@ -86,5 +123,6 @@ export default class Sprite extends Component {
scaleY: {defaultValue: 1, type: 'float32'},
source: {type: 'string'},
speed: {type: 'float32'},
tint: {type: 'uint32'},
};
}

View File

@ -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);
}

View File

@ -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
View 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'},
};
}

View File

@ -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'},
};
}

View File

@ -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());
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}
}

View 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();
}
}
}
}
}

View 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);
}
}
}

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

View File

@ -21,15 +21,18 @@ export default class VisibleAabbs extends System {
reindex(entities) {
for (const id of entities) {
if (1 === id) {
const {x, y} = this.ecs.get(1).AreaSize;
if (
!this.hash ||
(
this.hash.area.x !== x
|| this.hash.area.y !== y
)
) {
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
const {AreaSize} = this.ecs.get(1)
if (AreaSize) {
const {x, y} = AreaSize;
if (
!this.hash ||
(
this.hash.area.x !== x
|| this.hash.area.y !== y
)
) {
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
}
}
}
}
@ -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
View 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);
}
}
}

View 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;
});

View File

@ -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));
})

View File

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

View File

@ -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>
);
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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 }
}

View File

@ -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;
}
case DamageTypes.HEALING: {
hue = [90, 120];
break;
}
case DamageTypes.MANA: {
hue = [215, 220];
break;
}
}
if (0 === elements.length) {
return false;
}
return <div className={styles.damages}>{elements}</div>;
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);

View File

@ -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;
}

View File

@ -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,

View File

@ -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)}

View File

@ -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%;

View File

@ -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>
)}

View File

@ -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) => ({
...damages,
[composite]: damage[key],
}));
}
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,
...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>
);
}

View 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>
);
}

View File

@ -0,0 +1,7 @@
.external {
left: 20px;
opacity: 1;
position: absolute;
top: 274px;
transition: top 150ms, opacity 200ms;
}

View 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}>&nbsp;{label}&nbsp;</p>
<div
className={styles.grid}
style={{
'--color': color,
'--columns': columns,
}}
>
<div
className={styles.innerGrid}
>
{Slots}
</div>
</div>
</div>
);
}

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

View File

@ -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>
);
}

View File

@ -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;
}

View File

@ -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;

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

View File

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

View File

@ -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}

View File

@ -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
zIndex={entity.Position?.y || 0}
>
{entity.Sprite && (
<Sprite
entity={entity}
{...rest}
/>
)}
{entity.Emitter && (
<Emitter
entity={entity}
/>
)}
{/* {entity.Light && (
<Light
x={entity.Position.x}
y={entity.Position.y}
/>
)} */}
{debug && entity.Position && (
<Crosshair x={entity.Position.x} y={entity.Position.y} />
)}
<>
<Container
x={entity.Position.x}
y={entity.Position.y}
zIndex={entity.Position?.y || 0}
>
{entity.Sprite && (
<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.Light && (
<Light
brightness={entity.Light.brightness}
/>
)}
{debug && entity.Position && (
<Crosshair />
)}
</Container>
{debug && (
<Aabb
color={0xff00ff}
@ -105,7 +113,7 @@ function Entity({entity, ...rest}) {
{...entity.Interacts.aabb()}
/>
)}
</Container>
</>
);
}

View File

@ -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}
/>
)
}

View File

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

View File

@ -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);

View File

@ -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};

View File

@ -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);

View File

@ -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 (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>
<div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
<Devtools
applyFilters={applyFilters}
eventsChannel={devEventsChannel}
setApplyFilters={setApplyFilters}
/>
</div>
{devtoolsIsOpen && (
<div className={[styles.devtools, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}>
<Devtools
eventsChannel={devEventsChannel}
/>
</div>
)}
</div>
);
}
export default memo(Ui);
export default Ui;

View File

@ -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}};
}

View 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]);
}

View File

@ -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);

View File

@ -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({

View File

@ -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},

View File

@ -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
View 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 */

View File

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

View File

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

@ -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",

View File

@ -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",

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

View File

@ -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: {},

View File

@ -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,

View File

@ -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: {},

View File

@ -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,

View File

@ -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: {
x: Position.x,
y: Position.y,
shape: {
type: 'filledRect',
payload: {width: 16, height: 16},
},
};
Emitter.emit(seedParticles)
entity: {
Forces: {forceY: -100},
Position: {
x: Position.x,
y: Position.y,
},
Sprite: {
scaleX: 0.125,
scaleY: 0.125,
tint: 0x221100,
},
Ttl: {ttl: 0.25},
}
});
Sound.play('/assets/sow.wav');
Sprite.animation = ['moving', direction].join(':');

View File

@ -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,
}
}
}
]
})
}
await wait(0.25);
for (const {x, y} of projected) {
Emitter.emit({
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.5);
for (const {x, y} of projected) {
const tileIndex = layer.area.x * y + x