Compare commits
19 Commits
6eac298671
...
f971295825
Author | SHA1 | Date | |
---|---|---|---|
|
f971295825 | ||
|
5a92be47c1 | ||
|
c27ab133a9 | ||
|
df0e012338 | ||
|
b04392756b | ||
|
aacfd6271f | ||
|
9f0c3f3c07 | ||
|
88f0ec4715 | ||
|
263cf37e27 | ||
|
b9e3ac433b | ||
|
735144df55 | ||
|
1ee30434f3 | ||
|
e13d33fbbf | ||
|
2d2adbbfd6 | ||
|
b95a2e2bb9 | ||
|
947e2cf380 | ||
|
73c6d991a7 | ||
|
02acecfb5c | ||
|
f7e4bd0e36 |
|
@ -38,7 +38,8 @@ export default class Component {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const allocated = this.allocateMany(entries.length);
|
const allocated = this.allocateMany(entries.length);
|
||||||
const {properties} = this.constructor.schema.specification;
|
const {properties} = this.constructor.schema.specification.concrete;
|
||||||
|
const Schema = this.constructor.schema.constructor;
|
||||||
const keys = Object.keys(properties);
|
const keys = Object.keys(properties);
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (let i = 0; i < entries.length; ++i) {
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
|
@ -47,15 +48,17 @@ export default class Component {
|
||||||
this.data[allocated[i]].entity = entityId;
|
this.data[allocated[i]].entity = entityId;
|
||||||
for (let k = 0; k < keys.length; ++k) {
|
for (let k = 0; k < keys.length; ++k) {
|
||||||
const j = keys[k];
|
const j = keys[k];
|
||||||
const {defaultValue} = properties[j];
|
|
||||||
const instance = this.data[allocated[i]];
|
const instance = this.data[allocated[i]];
|
||||||
if (j in values) {
|
if (j in values) {
|
||||||
instance[j] = values[j];
|
instance[j] = values[j];
|
||||||
}
|
}
|
||||||
else if ('undefined' !== typeof defaultValue) {
|
else {
|
||||||
|
const defaultValue = Schema.defaultValue(properties[j]);
|
||||||
|
if ('undefined' !== typeof defaultValue) {
|
||||||
instance[j] = defaultValue;
|
instance[j] = defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
promises.push(this.load(this.data[allocated[i]]));
|
promises.push(this.load(this.data[allocated[i]]));
|
||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
@ -67,7 +70,7 @@ export default class Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
deserialize(entityId, view, offset) {
|
deserialize(entityId, view, offset) {
|
||||||
const {properties} = this.constructor.schema.specification;
|
const {properties} = this.constructor.schema.specification.concrete;
|
||||||
const instance = this.get(entityId);
|
const instance = this.get(entityId);
|
||||||
const deserialized = this.constructor.schema.deserialize(view, offset);
|
const deserialized = this.constructor.schema.deserialize(view, offset);
|
||||||
for (const key in properties) {
|
for (const key in properties) {
|
||||||
|
@ -96,11 +99,11 @@ export default class Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
static filterDefaults(instance) {
|
static filterDefaults(instance) {
|
||||||
const {properties} = this.schema.specification;
|
const {properties} = this.schema.specification.concrete;
|
||||||
|
const Schema = this.schema.constructor;
|
||||||
const json = {};
|
const json = {};
|
||||||
for (const key in properties) {
|
for (const key in properties) {
|
||||||
const {defaultValue} = properties[key];
|
if (key in instance && instance[key] !== Schema.defaultValue(properties[key])) {
|
||||||
if (key in instance && instance[key] !== defaultValue) {
|
|
||||||
json[key] = instance[key];
|
json[key] = instance[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,16 +139,16 @@ export default class Component {
|
||||||
|
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
const Component = this;
|
const Component = this;
|
||||||
const {specification} = Component.constructor.schema;
|
const {concrete} = Component.constructor.schema.specification;
|
||||||
|
const Schema = Component.constructor.schema.constructor;
|
||||||
const Instance = class {
|
const Instance = class {
|
||||||
$$entity = 0;
|
$$entity = 0;
|
||||||
constructor() {
|
constructor() {
|
||||||
this.$$reset();
|
this.$$reset();
|
||||||
}
|
}
|
||||||
$$reset() {
|
$$reset() {
|
||||||
for (const key in specification.properties) {
|
for (const key in concrete.properties) {
|
||||||
const {defaultValue} = specification.properties[key];
|
this[`$$${key}`] = Schema.defaultValue(concrete.properties[key]);
|
||||||
this[`$$${key}`] = defaultValue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
destroy() {}
|
destroy() {}
|
||||||
|
@ -166,7 +169,7 @@ export default class Component {
|
||||||
this.$$reset();
|
this.$$reset();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
for (const key in specification.properties) {
|
for (const key in concrete.properties) {
|
||||||
properties[key] = {
|
properties[key] = {
|
||||||
get: function get() {
|
get: function get() {
|
||||||
return this[`$$${key}`];
|
return this[`$$${key}`];
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Component from '@/ecs/component.js';
|
import Component from '@/ecs/component.js';
|
||||||
import {intersects} from '@/util/math.js';
|
import {distance, intersects} from '@/util/math.js';
|
||||||
|
|
||||||
import vector2d from './helpers/vector-2d';
|
import vector2d from './helpers/vector-2d';
|
||||||
|
|
||||||
|
@ -30,6 +30,22 @@ export default class Collider extends Component {
|
||||||
}
|
}
|
||||||
return aabbs;
|
return aabbs;
|
||||||
}
|
}
|
||||||
|
closest(aabb) {
|
||||||
|
const entity = ecs.get(this.entity);
|
||||||
|
return Array.from(ecs.system('Colliders').within(aabb))
|
||||||
|
.filter((other) => other !== entity)
|
||||||
|
.sort(({Position: l}, {Position: r}) => {
|
||||||
|
return distance(entity.Position, l) > distance(entity.Position, r) ? -1 : 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
const entity = ecs.get(this.entity);
|
||||||
|
for (const otherId in this.collidingWith) {
|
||||||
|
const other = ecs.get(otherId);
|
||||||
|
delete entity.Collider.collidingWith[other.id];
|
||||||
|
delete other.Collider.collidingWith[entity.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
isCollidingWith(other) {
|
isCollidingWith(other) {
|
||||||
const {aabb, aabbs} = this;
|
const {aabb, aabbs} = this;
|
||||||
const {aabb: otherAabb, aabbs: otherAabbs} = other;
|
const {aabb: otherAabb, aabbs: otherAabbs} = other;
|
||||||
|
|
|
@ -9,16 +9,16 @@ export default class Interacts extends Component {
|
||||||
let x0 = Position.x - 8;
|
let x0 = Position.x - 8;
|
||||||
let y0 = Position.y - 8;
|
let y0 = Position.y - 8;
|
||||||
if (0 === Direction.direction) {
|
if (0 === Direction.direction) {
|
||||||
y0 -= 16
|
y0 -= 12
|
||||||
}
|
}
|
||||||
if (1 === Direction.direction) {
|
if (1 === Direction.direction) {
|
||||||
x0 += 16
|
x0 += 12
|
||||||
}
|
}
|
||||||
if (2 === Direction.direction) {
|
if (2 === Direction.direction) {
|
||||||
y0 += 16
|
y0 += 12
|
||||||
}
|
}
|
||||||
if (3 === Direction.direction) {
|
if (3 === Direction.direction) {
|
||||||
x0 -= 16
|
x0 -= 12
|
||||||
}
|
}
|
||||||
return {x0, x1: x0 + 15, y0, y1: y0 + 15};
|
return {x0, x1: x0 + 15, y0, y1: y0 + 15};
|
||||||
}
|
}
|
||||||
|
|
19
app/ecs/components/tags.js
Normal file
19
app/ecs/components/tags.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import Component from '@/ecs/component.js';
|
||||||
|
|
||||||
|
export default class Tags extends Component {
|
||||||
|
instanceFromSchema() {
|
||||||
|
return class TagsInstance extends super.instanceFromSchema() {
|
||||||
|
has(tag) {
|
||||||
|
return this.tags.includes(tag);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
static properties = {
|
||||||
|
tags: {
|
||||||
|
type: 'array',
|
||||||
|
subtype: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -178,7 +178,11 @@ export default class Ecs {
|
||||||
|
|
||||||
deindex(entityIds) {
|
deindex(entityIds) {
|
||||||
for (const systemName in this.Systems) {
|
for (const systemName in this.Systems) {
|
||||||
this.Systems[systemName].deindex(entityIds);
|
const System = this.Systems[systemName];
|
||||||
|
if (!System.active) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
System.deindex(entityIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,12 +237,14 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
destroying[componentName].push(entityId);
|
destroying[componentName].push(entityId);
|
||||||
}
|
}
|
||||||
this.$$entities[entityId] = undefined;
|
|
||||||
this.diff[entityId] = false;
|
|
||||||
}
|
}
|
||||||
for (const i in destroying) {
|
for (const i in destroying) {
|
||||||
this.Components[i].destroyMany(destroying[i]);
|
this.Components[i].destroyMany(destroying[i]);
|
||||||
}
|
}
|
||||||
|
for (const entityId of entityIds) {
|
||||||
|
this.$$entities[entityId] = undefined;
|
||||||
|
this.diff[entityId] = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get entities() {
|
get entities() {
|
||||||
|
@ -382,7 +388,11 @@ export default class Ecs {
|
||||||
|
|
||||||
reindex(entityIds) {
|
reindex(entityIds) {
|
||||||
for (const systemName in this.Systems) {
|
for (const systemName in this.Systems) {
|
||||||
this.Systems[systemName].reindex(entityIds);
|
const System = this.Systems[systemName];
|
||||||
|
if (!System.active) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
System.reindex(entityIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
40
app/ecs/schema-types/array.js
Normal file
40
app/ecs/schema-types/array.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
export default function (Schema) {
|
||||||
|
return {
|
||||||
|
defaultValue: () => [],
|
||||||
|
deserialize: (view, offset, {subtype}) => {
|
||||||
|
const length = view.getUint32(offset.value, true);
|
||||||
|
offset.value += 4;
|
||||||
|
const value = [];
|
||||||
|
for (let i = 0; i < length; ++i) {
|
||||||
|
value.push(Schema.deserialize(view, offset, subtype));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
normalize: ({subtype}) => {
|
||||||
|
return {
|
||||||
|
subtype: Schema.normalize(subtype),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset, {subtype}) => {
|
||||||
|
view.setUint32(offset, source.length, true);
|
||||||
|
let size = 4;
|
||||||
|
for (const element of source) {
|
||||||
|
size += Schema.serialize(
|
||||||
|
element,
|
||||||
|
view,
|
||||||
|
offset + size,
|
||||||
|
subtype,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
},
|
||||||
|
sizeOf: (instance, {subtype}) => {
|
||||||
|
let size = 4;
|
||||||
|
for (const element of instance) {
|
||||||
|
size += Schema.sizeOf(element, subtype);
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
},
|
||||||
|
staticSizeOf: () => 0,
|
||||||
|
};
|
||||||
|
}
|
16
app/ecs/schema-types/float32.js
Normal file
16
app/ecs/schema-types/float32.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export default function () {
|
||||||
|
return {
|
||||||
|
defaultValue: () => 0,
|
||||||
|
deserialize: (view, offset) => {
|
||||||
|
const value = view.getFloat32(offset.value, true);
|
||||||
|
offset.value += 4;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset) => {
|
||||||
|
view.setFloat32(offset, source, true);
|
||||||
|
return 4;
|
||||||
|
},
|
||||||
|
sizeOf: () => 4,
|
||||||
|
staticSizeOf: () => 4,
|
||||||
|
};
|
||||||
|
}
|
16
app/ecs/schema-types/float64.js
Normal file
16
app/ecs/schema-types/float64.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export default function () {
|
||||||
|
return {
|
||||||
|
defaultValue: () => 0,
|
||||||
|
deserialize: (view, offset) => {
|
||||||
|
const value = view.getFloat64(offset.value, true);
|
||||||
|
offset.value += 8;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset) => {
|
||||||
|
view.setFloat64(offset, source, true);
|
||||||
|
return 8;
|
||||||
|
},
|
||||||
|
sizeOf: () => 8,
|
||||||
|
staticSizeOf: () => 8,
|
||||||
|
};
|
||||||
|
}
|
16
app/ecs/schema-types/int16.js
Normal file
16
app/ecs/schema-types/int16.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export default function () {
|
||||||
|
return {
|
||||||
|
defaultValue: () => 0,
|
||||||
|
deserialize: (view, offset) => {
|
||||||
|
const value = view.getInt16(offset.value, true);
|
||||||
|
offset.value += 2;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset) => {
|
||||||
|
view.setInt16(offset, source, true);
|
||||||
|
return 2;
|
||||||
|
},
|
||||||
|
sizeOf: () => 2,
|
||||||
|
staticSizeOf: () => 2,
|
||||||
|
};
|
||||||
|
}
|
16
app/ecs/schema-types/int32.js
Normal file
16
app/ecs/schema-types/int32.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export default function () {
|
||||||
|
return {
|
||||||
|
defaultValue: () => 0,
|
||||||
|
deserialize: (view, offset) => {
|
||||||
|
const value = view.getInt32(offset.value, true);
|
||||||
|
offset.value += 4;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset) => {
|
||||||
|
view.setInt32(offset, source, true);
|
||||||
|
return 4;
|
||||||
|
},
|
||||||
|
sizeOf: () => 4,
|
||||||
|
staticSizeOf: () => 4,
|
||||||
|
};
|
||||||
|
}
|
16
app/ecs/schema-types/int64.js
Normal file
16
app/ecs/schema-types/int64.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export default function () {
|
||||||
|
return {
|
||||||
|
defaultValue: () => 0n,
|
||||||
|
deserialize: (view, offset) => {
|
||||||
|
const value = view.getBigInt64(offset.value, true);
|
||||||
|
offset.value += 8;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset) => {
|
||||||
|
view.setBigInt64(offset, source, true);
|
||||||
|
return 8;
|
||||||
|
},
|
||||||
|
sizeOf: () => 8,
|
||||||
|
staticSizeOf: () => 8,
|
||||||
|
};
|
||||||
|
}
|
16
app/ecs/schema-types/int8.js
Normal file
16
app/ecs/schema-types/int8.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export default function () {
|
||||||
|
return {
|
||||||
|
defaultValue: () => 0,
|
||||||
|
deserialize: (view, offset) => {
|
||||||
|
const value = view.getInt8(offset.value, true);
|
||||||
|
offset.value += 1;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset) => {
|
||||||
|
view.setInt8(offset, source, true);
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
sizeOf: () => 1,
|
||||||
|
staticSizeOf: () => 1,
|
||||||
|
};
|
||||||
|
}
|
50
app/ecs/schema-types/map.js
Normal file
50
app/ecs/schema-types/map.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
export default function (Schema) {
|
||||||
|
return {
|
||||||
|
defaultValue: () => ({}),
|
||||||
|
deserialize: (view, offset, concrete) => {
|
||||||
|
const length = view.getUint32(offset.value, true);
|
||||||
|
offset.value += 4;
|
||||||
|
const value = {};
|
||||||
|
for (let i = 0; i < length; ++i) {
|
||||||
|
const key = Schema.deserialize(view, offset, concrete.key);
|
||||||
|
value[key] = Schema.deserialize(view, offset, concrete.value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
normalize: ({value}) => {
|
||||||
|
return {
|
||||||
|
key: Schema.normalize({type: 'string'}),
|
||||||
|
value: Schema.normalize(value),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset, concrete) => {
|
||||||
|
const keys = Object.keys(source);
|
||||||
|
view.setUint32(offset, keys.length, true);
|
||||||
|
let size = 4;
|
||||||
|
for (const key of keys) {
|
||||||
|
size += Schema.serialize(
|
||||||
|
key,
|
||||||
|
view,
|
||||||
|
offset + size,
|
||||||
|
concrete.key,
|
||||||
|
);
|
||||||
|
size += Schema.serialize(
|
||||||
|
source[key],
|
||||||
|
view,
|
||||||
|
offset + size,
|
||||||
|
concrete.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
},
|
||||||
|
sizeOf: (instance, concrete) => {
|
||||||
|
let size = 4;
|
||||||
|
for (const key in instance) {
|
||||||
|
size += Schema.sizeOf(key, concrete.key);
|
||||||
|
size += Schema.sizeOf(instance[key], concrete.value);
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
},
|
||||||
|
staticSizeOf: () => 0,
|
||||||
|
};
|
||||||
|
}
|
55
app/ecs/schema-types/object.js
Normal file
55
app/ecs/schema-types/object.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
export default function (Schema) {
|
||||||
|
return {
|
||||||
|
defaultValue: ({properties}) => {
|
||||||
|
const object = {};
|
||||||
|
for (const key in properties) {
|
||||||
|
object[key] = Schema.defaultValue(properties[key]);
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
},
|
||||||
|
deserialize: (view, offset, {properties}) => {
|
||||||
|
const value = {};
|
||||||
|
for (const key in properties) {
|
||||||
|
value[key] = Schema.deserialize(view, offset, properties[key]);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
normalize: ({properties}) => {
|
||||||
|
const normalized = {properties: {}};
|
||||||
|
for (const key in properties) {
|
||||||
|
normalized.properties[key] = Schema.normalize(properties[key])
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset, {properties}) => {
|
||||||
|
let size = 0;
|
||||||
|
for (const key in properties) {
|
||||||
|
size += Schema.serialize(
|
||||||
|
source[key],
|
||||||
|
view,
|
||||||
|
offset + size,
|
||||||
|
properties[key],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
},
|
||||||
|
sizeOf: (instance, {properties}) => {
|
||||||
|
let size = 0;
|
||||||
|
for (const key in properties) {
|
||||||
|
size += Schema.sizeOf(instance[key], properties[key]);
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
},
|
||||||
|
staticSizeOf: ({properties}) => {
|
||||||
|
let size = 0;
|
||||||
|
for (const key in properties) {
|
||||||
|
const propertySize = Schema.size(properties[key]);
|
||||||
|
if (0 === propertySize) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
size += propertySize;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
31
app/ecs/schema-types/string.js
Normal file
31
app/ecs/schema-types/string.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return {
|
||||||
|
defaultValue: () => '',
|
||||||
|
deserialize: (view, offset) => {
|
||||||
|
const length = view.getUint32(offset.value, true);
|
||||||
|
offset.value += 4;
|
||||||
|
const {buffer, byteOffset} = view;
|
||||||
|
const value = decoder.decode(new DataView(buffer, byteOffset + offset.value, length));
|
||||||
|
offset.value += length;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset) => {
|
||||||
|
const bytes = encoder.encode(source);
|
||||||
|
view.setUint32(offset, bytes.length, true);
|
||||||
|
offset += 4;
|
||||||
|
for (let i = 0; i < bytes.length; ++i) {
|
||||||
|
view.setUint8(offset++, bytes[i]);
|
||||||
|
}
|
||||||
|
return 4 + bytes.length;
|
||||||
|
},
|
||||||
|
sizeOf: (instance) => {
|
||||||
|
let size = 4;
|
||||||
|
size += (encoder.encode(instance)).length;
|
||||||
|
return size;
|
||||||
|
},
|
||||||
|
staticSizeOf: () => 0,
|
||||||
|
};
|
||||||
|
}
|
16
app/ecs/schema-types/uint16.js
Normal file
16
app/ecs/schema-types/uint16.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export default function () {
|
||||||
|
return {
|
||||||
|
defaultValue: () => 0,
|
||||||
|
deserialize: (view, offset) => {
|
||||||
|
const value = view.getUint16(offset.value, true);
|
||||||
|
offset.value += 2;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset) => {
|
||||||
|
view.setUint16(offset, source, true);
|
||||||
|
return 2;
|
||||||
|
},
|
||||||
|
sizeOf: () => 2,
|
||||||
|
staticSizeOf: () => 2,
|
||||||
|
};
|
||||||
|
}
|
16
app/ecs/schema-types/uint32.js
Normal file
16
app/ecs/schema-types/uint32.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export default function () {
|
||||||
|
return {
|
||||||
|
defaultValue: () => 0,
|
||||||
|
deserialize: (view, offset) => {
|
||||||
|
const value = view.getUint32(offset.value, true);
|
||||||
|
offset.value += 4;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset) => {
|
||||||
|
view.setUint32(offset, source, true);
|
||||||
|
return 4;
|
||||||
|
},
|
||||||
|
sizeOf: () => 4,
|
||||||
|
staticSizeOf: () => 4,
|
||||||
|
};
|
||||||
|
}
|
16
app/ecs/schema-types/uint64.js
Normal file
16
app/ecs/schema-types/uint64.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export default function () {
|
||||||
|
return {
|
||||||
|
defaultValue: () => 0n,
|
||||||
|
deserialize: (view, offset) => {
|
||||||
|
const value = view.getBigUint64(offset.value, true);
|
||||||
|
offset.value += 8;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset) => {
|
||||||
|
view.setBigUint64(offset, source, true);
|
||||||
|
return 8;
|
||||||
|
},
|
||||||
|
sizeOf: () => 8,
|
||||||
|
staticSizeOf: () => 8,
|
||||||
|
};
|
||||||
|
}
|
16
app/ecs/schema-types/uint8.js
Normal file
16
app/ecs/schema-types/uint8.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export default function () {
|
||||||
|
return {
|
||||||
|
defaultValue: () => 0,
|
||||||
|
deserialize: (view, offset) => {
|
||||||
|
const value = view.getUint8(offset.value, true);
|
||||||
|
offset.value += 1;
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
serialize: (source, view, offset) => {
|
||||||
|
view.setUint8(offset, source, true);
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
sizeOf: () => 1,
|
||||||
|
staticSizeOf: () => 1,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,303 +1,66 @@
|
||||||
const encoder = new TextEncoder();
|
|
||||||
|
|
||||||
export default class Schema {
|
export default class Schema {
|
||||||
|
|
||||||
$$size = 0;
|
static $$types = {};
|
||||||
|
|
||||||
specification;
|
specification;
|
||||||
|
|
||||||
static viewGetMethods = {
|
|
||||||
uint8: 'getUint8',
|
|
||||||
int8: 'getInt8',
|
|
||||||
uint16: 'getUint16',
|
|
||||||
int16: 'getInt16',
|
|
||||||
uint32: 'getUint32',
|
|
||||||
int32: 'getInt32',
|
|
||||||
float32: 'getFloat32',
|
|
||||||
float64: 'getFloat64',
|
|
||||||
int64: 'getBigInt64',
|
|
||||||
uint64: 'getBigUint64',
|
|
||||||
};
|
|
||||||
|
|
||||||
static viewSetMethods = {
|
|
||||||
uint8: 'setUint8',
|
|
||||||
int8: 'setInt8',
|
|
||||||
uint16: 'setUint16',
|
|
||||||
int16: 'setInt16',
|
|
||||||
uint32: 'setUint32',
|
|
||||||
int32: 'setInt32',
|
|
||||||
float32: 'setFloat32',
|
|
||||||
float64: 'setFloat64',
|
|
||||||
int64: 'setBigInt64',
|
|
||||||
uint64: 'setBigUint64',
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(specification) {
|
constructor(specification) {
|
||||||
this.specification = this.constructor.normalize(specification);
|
this.specification = this.constructor.normalize(specification);
|
||||||
}
|
}
|
||||||
|
|
||||||
static deserialize(view, offset, specification) {
|
static defaultValue({$, concrete}) {
|
||||||
const viewGetMethod = this.viewGetMethods[specification.type];
|
if (concrete.defaultValue) {
|
||||||
if (viewGetMethod) {
|
return concrete.defaultValue;
|
||||||
const value = view[viewGetMethod](offset.value, true);
|
|
||||||
offset.value += this.size(specification);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
switch (specification.type) {
|
|
||||||
case 'array': {
|
|
||||||
const length = view.getUint32(offset.value, true);
|
|
||||||
offset.value += 4;
|
|
||||||
const value = [];
|
|
||||||
for (let i = 0; i < length; ++i) {
|
|
||||||
value.push(this.deserialize(view, offset, specification.subtype));
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
case 'map': {
|
|
||||||
const length = view.getUint32(offset.value, true);
|
|
||||||
offset.value += 4;
|
|
||||||
const value = {};
|
|
||||||
for (let i = 0; i < length; ++i) {
|
|
||||||
const key = this.deserialize(view, offset, {type: 'string'});
|
|
||||||
value[key] = this.deserialize(view, offset, specification.value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
const value = {};
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
value[key] = this.deserialize(view, offset, specification.properties[key]);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
case 'string': {
|
|
||||||
const length = view.getUint32(offset.value, true);
|
|
||||||
offset.value += 4;
|
|
||||||
const {buffer, byteOffset} = view;
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
const value = decoder.decode(new DataView(buffer, byteOffset + offset.value, length));
|
|
||||||
offset.value += length;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserialize(view, offset = 0) {
|
|
||||||
const wrapped = {value: offset};
|
|
||||||
return this.constructor.deserialize(view, wrapped, this.specification);
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultValue(specification) {
|
|
||||||
if (specification.defaultValue) {
|
|
||||||
return specification.defaultValue;
|
|
||||||
}
|
|
||||||
switch (specification.type) {
|
|
||||||
case 'uint8': case 'int8':
|
|
||||||
case 'uint16': case 'int16':
|
|
||||||
case 'uint32': case 'int32':
|
|
||||||
case 'uint64': case 'int64':
|
|
||||||
case 'float32': case 'float64': {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
case 'array': {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
case 'map': {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
const object = {};
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
object[key] = this.defaultValue(specification.properties[key]);
|
|
||||||
}
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
case 'string': {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return $.defaultValue(concrete);
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultValue() {
|
defaultValue() {
|
||||||
return this.constructor.defaultValue(this.specification);
|
return this.constructor.defaultValue(this.specification);
|
||||||
}
|
}
|
||||||
|
|
||||||
static normalize(specification) {
|
static deserialize(view, offset, {$, concrete}) {
|
||||||
let normalized = specification;
|
return $.deserialize(view, offset, concrete);
|
||||||
switch (specification.type) {
|
|
||||||
case 'array': {
|
|
||||||
normalized.subtype = this.normalize(specification.subtype);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'map': {
|
|
||||||
normalized.value = this.normalize(specification.value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
normalized.properties[key] = this.normalize(specification.properties[key])
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'uint8':
|
|
||||||
case 'int8':
|
|
||||||
case 'uint16':
|
|
||||||
case 'int16':
|
|
||||||
case 'uint32':
|
|
||||||
case 'int32':
|
|
||||||
case 'uint64':
|
|
||||||
case 'int64':
|
|
||||||
case 'float32':
|
|
||||||
case 'float64':
|
|
||||||
case 'string': {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
/* v8 ignore next 2 */
|
|
||||||
default:
|
|
||||||
throw new TypeError(`invalid specification: ${JSON.stringify(specification)}`);
|
|
||||||
}
|
|
||||||
return {...normalized, defaultValue: this.defaultValue(normalized)};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static serialize(source, view, offset, specification) {
|
deserialize(view, offset = 0) {
|
||||||
const viewSetMethod = this.viewSetMethods[specification.type];
|
return this.constructor.deserialize(view, {value: offset}, this.specification);
|
||||||
if (viewSetMethod) {
|
|
||||||
view[viewSetMethod](offset, source, true);
|
|
||||||
return this.size(specification);
|
|
||||||
}
|
}
|
||||||
switch (specification.type) {
|
|
||||||
case 'array': {
|
static normalize({type, ...rest}) {
|
||||||
view.setUint32(offset, source.length, true);
|
const $$type = this.$$types[type];
|
||||||
let arraySize = 4;
|
if (!$$type) {
|
||||||
for (const element of source) {
|
throw new TypeError(`unregistered schema type '${type}'`);
|
||||||
arraySize += this.serialize(
|
|
||||||
element,
|
|
||||||
view,
|
|
||||||
offset + arraySize,
|
|
||||||
specification.subtype,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return arraySize;
|
|
||||||
}
|
|
||||||
case 'map': {
|
|
||||||
const keys = Object.keys(source);
|
|
||||||
view.setUint32(offset, keys.length, true);
|
|
||||||
let mapSize = 4;
|
|
||||||
for (const key of keys) {
|
|
||||||
mapSize += this.serialize(
|
|
||||||
key,
|
|
||||||
view,
|
|
||||||
offset + mapSize,
|
|
||||||
{type: 'string'},
|
|
||||||
);
|
|
||||||
mapSize += this.serialize(
|
|
||||||
source[key],
|
|
||||||
view,
|
|
||||||
offset + mapSize,
|
|
||||||
specification.value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return mapSize;
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
let objectSize = 0;
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
objectSize += this.serialize(
|
|
||||||
source[key],
|
|
||||||
view,
|
|
||||||
offset + objectSize,
|
|
||||||
specification.properties[key],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return objectSize;
|
|
||||||
}
|
|
||||||
case 'string': {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const bytes = encoder.encode(source);
|
|
||||||
view.setUint32(offset, bytes.length, true);
|
|
||||||
offset += 4;
|
|
||||||
for (let i = 0; i < bytes.length; ++i) {
|
|
||||||
view.setUint8(offset++, bytes[i]);
|
|
||||||
}
|
|
||||||
return 4 + bytes.length;
|
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
$: this.$$types[type],
|
||||||
|
concrete: $$type.normalize ? $$type.normalize(rest) : rest,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static serialize(source, view, offset, {$, concrete}) {
|
||||||
|
return $.serialize(source, view, offset, concrete);
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(source, view, offset = 0) {
|
serialize(source, view, offset = 0) {
|
||||||
this.constructor.serialize(source, view, offset, this.specification);
|
this.constructor.serialize(source, view, offset, this.specification);
|
||||||
}
|
}
|
||||||
|
|
||||||
static sizeOf(concrete, specification) {
|
static sizeOf(instance, {$, concrete}) {
|
||||||
const size = this.size(specification);
|
return $.sizeOf(instance, concrete);
|
||||||
if (size > 0) {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
let fullSize = 0;
|
|
||||||
const {type} = specification;
|
|
||||||
switch (type) {
|
|
||||||
case 'array': {
|
|
||||||
fullSize += 4;
|
|
||||||
for (const element of concrete) {
|
|
||||||
fullSize += this.sizeOf(element, specification.subtype);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'map': {
|
|
||||||
fullSize += 4;
|
|
||||||
for (const key in concrete) {
|
|
||||||
fullSize += this.sizeOf(key, {type: 'string'});
|
|
||||||
fullSize += this.sizeOf(concrete[key], specification.value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
fullSize += this.sizeOf(concrete[key], specification.properties[key]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'string':
|
|
||||||
fullSize += 4;
|
|
||||||
fullSize += (encoder.encode(concrete)).length;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return fullSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sizeOf(concrete) {
|
sizeOf(instance) {
|
||||||
return this.constructor.sizeOf(concrete, this.specification);
|
return this.constructor.sizeOf(instance, this.specification);
|
||||||
}
|
}
|
||||||
|
|
||||||
static size(specification) {
|
static size({$, concrete}) {
|
||||||
switch (specification.type) {
|
return $.staticSizeOf(concrete);
|
||||||
case 'array': return 0;
|
|
||||||
case 'map': return 0; // TODO could be fixed-size w/ fixed-size key
|
|
||||||
case 'object': {
|
|
||||||
let size = 0;
|
|
||||||
for (const key in specification.properties) {
|
|
||||||
const propertySize = this.size(specification.properties[key]);
|
|
||||||
if (0 === propertySize) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
size += propertySize;
|
|
||||||
}
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
case 'uint8': case 'int8': {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
case 'uint16': case 'int16': {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
case 'uint32': case 'int32': case 'float32': {
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
case 'uint64': case 'int64': case 'float64': {
|
|
||||||
return 8;
|
|
||||||
}
|
|
||||||
default: return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imports = import.meta.glob('./schema-types/*.js', {eager: true, import: 'default'});
|
||||||
|
for (const path in imports) {
|
||||||
|
Schema.$$types[path.replace(/.\/schema-types\/(.*)\.js/, '$1')] = imports[path](Schema);
|
||||||
|
}
|
||||||
|
|
|
@ -4,10 +4,6 @@ import Schema from './schema.js';
|
||||||
|
|
||||||
test('defaults values', () => {
|
test('defaults values', () => {
|
||||||
const compare = (specification, value) => {
|
const compare = (specification, value) => {
|
||||||
expect(Schema.defaultValue(specification))
|
|
||||||
.to.deep.equal(value);
|
|
||||||
expect(new Schema(specification).specification.defaultValue)
|
|
||||||
.to.deep.equal(value);
|
|
||||||
expect(new Schema(specification).defaultValue())
|
expect(new Schema(specification).defaultValue())
|
||||||
.to.deep.equal(value);
|
.to.deep.equal(value);
|
||||||
};
|
};
|
||||||
|
@ -18,13 +14,17 @@ test('defaults values', () => {
|
||||||
'int16',
|
'int16',
|
||||||
'uint32',
|
'uint32',
|
||||||
'int32',
|
'int32',
|
||||||
'uint64',
|
|
||||||
'int64',
|
|
||||||
'float32',
|
'float32',
|
||||||
'float64',
|
'float64',
|
||||||
].forEach((type) => {
|
].forEach((type) => {
|
||||||
compare({type}, 0);
|
compare({type}, 0);
|
||||||
});
|
});
|
||||||
|
[
|
||||||
|
'uint64',
|
||||||
|
'int64',
|
||||||
|
].forEach((type) => {
|
||||||
|
compare({type}, 0n);
|
||||||
|
});
|
||||||
compare({type: 'string'}, '');
|
compare({type: 'string'}, '');
|
||||||
compare({type: 'array', subtype: {type: 'string'}}, []);
|
compare({type: 'array', subtype: {type: 'string'}}, []);
|
||||||
compare(
|
compare(
|
||||||
|
@ -167,11 +167,11 @@ test('encodes and decodes', () => {
|
||||||
[{type: 'object', properties: {foo: {type: 'uint8'}, bar: {type: 'string'}}}, {foo: 64, bar: 'baz'}],
|
[{type: 'object', properties: {foo: {type: 'uint8'}, bar: {type: 'string'}}}, {foo: 64, bar: 'baz'}],
|
||||||
[{type: 'object', properties: {foo: {type: 'uint8'}}}, {foo: 64}],
|
[{type: 'object', properties: {foo: {type: 'uint8'}}}, {foo: 64}],
|
||||||
];
|
];
|
||||||
entries.forEach(([specification, concrete]) => {
|
entries.forEach(([specification, instance]) => {
|
||||||
const schema = new Schema(specification);
|
const schema = new Schema(specification);
|
||||||
const view = new DataView(new ArrayBuffer(schema.sizeOf(concrete)));
|
const view = new DataView(new ArrayBuffer(schema.sizeOf(instance)));
|
||||||
schema.serialize(concrete, view);
|
schema.serialize(instance, view);
|
||||||
expect(concrete)
|
expect(instance)
|
||||||
.to.deep.equal(schema.deserialize(view));
|
.to.deep.equal(schema.deserialize(view));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,32 +39,31 @@ export default class Colliders extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
tick() {
|
tick() {
|
||||||
const seen = {};
|
const collisions = new Map();
|
||||||
for (const entity of this.ecs.changed(['Position'])) {
|
for (const entity of this.ecs.changed(['Position'])) {
|
||||||
if (seen[entity.id]) {
|
this.updateHash(entity);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
seen[entity.id] = true;
|
for (const entity of this.ecs.changed(['Position'])) {
|
||||||
if (!entity.Collider) {
|
if (!entity.Collider) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const {collidingWith: wasCollidingWith} = entity.Collider;
|
collisions.set(entity, new Set());
|
||||||
entity.Collider.collidingWith = {};
|
|
||||||
this.updateHash(entity);
|
|
||||||
for (const other of this.within(entity.Collider.aabb)) {
|
for (const other of this.within(entity.Collider.aabb)) {
|
||||||
if (seen[other.id]) {
|
if (entity === other) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
seen[other.id] = true;
|
if (!collisions.has(other)) {
|
||||||
if (!other.Collider) {
|
collisions.set(other, new Set());
|
||||||
|
}
|
||||||
|
if (!other.Collider || collisions.get(other).has(entity)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
delete other.Collider.collidingWith[entity.id];
|
|
||||||
const intersections = entity.Collider.isCollidingWith(other.Collider);
|
const intersections = entity.Collider.isCollidingWith(other.Collider);
|
||||||
if (intersections.length > 0) {
|
if (intersections.length > 0) {
|
||||||
|
collisions.get(entity).add(other);
|
||||||
|
if (!entity.Collider.collidingWith[other.id]) {
|
||||||
entity.Collider.collidingWith[other.id] = true;
|
entity.Collider.collidingWith[other.id] = true;
|
||||||
other.Collider.collidingWith[entity.id] = true;
|
other.Collider.collidingWith[entity.id] = true;
|
||||||
if (!wasCollidingWith[other.id]) {
|
|
||||||
if (entity.Collider.collisionStartScriptInstance) {
|
if (entity.Collider.collisionStartScriptInstance) {
|
||||||
const script = entity.Collider.collisionStartScriptInstance.clone();
|
const script = entity.Collider.collisionStartScriptInstance.clone();
|
||||||
script.context.intersections = intersections;
|
script.context.intersections = intersections;
|
||||||
|
@ -117,13 +116,8 @@ export default class Colliders extends System {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
else {
|
||||||
for (const otherId in wasCollidingWith) {
|
if (entity.Collider.collidingWith[other.id]) {
|
||||||
if (!entity.Collider.collidingWith[otherId]) {
|
|
||||||
const other = this.ecs.get(otherId);
|
|
||||||
if (!other || !other.Collider) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (entity.Collider.collisionEndScriptInstance) {
|
if (entity.Collider.collisionEndScriptInstance) {
|
||||||
const script = entity.Collider.collisionEndScriptInstance.clone();
|
const script = entity.Collider.collisionEndScriptInstance.clone();
|
||||||
script.context.other = other;
|
script.context.other = other;
|
||||||
|
@ -134,6 +128,9 @@ export default class Colliders extends System {
|
||||||
script.context.other = entity;
|
script.context.other = entity;
|
||||||
other.Ticking.add(script.ticker());
|
other.Ticking.add(script.ticker());
|
||||||
}
|
}
|
||||||
|
delete entity.Collider.collidingWith[other.id];
|
||||||
|
delete other.Collider.collidingWith[entity.id];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {System} from '@/ecs/index.js';
|
import {System} from '@/ecs/index.js';
|
||||||
import {distance} from '@/util/math.js';
|
|
||||||
|
|
||||||
export default class Interactions extends System {
|
export default class Interactions extends System {
|
||||||
|
|
||||||
|
@ -11,25 +10,16 @@ export default class Interactions extends System {
|
||||||
|
|
||||||
tick() {
|
tick() {
|
||||||
for (const entity of this.select('default')) {
|
for (const entity of this.select('default')) {
|
||||||
const {Interacts} = entity;
|
const {Collider, Interacts} = entity;
|
||||||
let willInteract = false;
|
let willInteractWith = 0;
|
||||||
const entities = Array.from(this.ecs.system('Colliders').within(Interacts.aabb()))
|
const entities = Collider.closest(Interacts.aabb());
|
||||||
.filter((other) => other !== entity)
|
|
||||||
.sort(({Position: l}, {Position: r}) => {
|
|
||||||
return distance(entity.Position, l) > distance(entity.Position, r) ? -1 : 1;
|
|
||||||
});
|
|
||||||
for (const other of entities) {
|
for (const other of entities) {
|
||||||
if (other === entity) {
|
if (other.Interactive?.interacting) {
|
||||||
continue;
|
willInteractWith = other.id;
|
||||||
}
|
break;
|
||||||
if (other.Interactive && other.Interactive.interacting) {
|
|
||||||
Interacts.willInteractWith = other.id;
|
|
||||||
willInteract = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!willInteract) {
|
Interacts.willInteractWith = willInteractWith;
|
||||||
Interacts.willInteractWith = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,18 @@ export default class VisibleAabbs extends System {
|
||||||
reindex(entities) {
|
reindex(entities) {
|
||||||
for (const id of entities) {
|
for (const id of entities) {
|
||||||
if (1 === id) {
|
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);
|
this.hash = new SpatialHash(this.ecs.get(1).AreaSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
super.reindex(entities);
|
super.reindex(entities);
|
||||||
for (const id of entities) {
|
for (const id of entities) {
|
||||||
this.updateHash(this.ecs.get(id));
|
this.updateHash(this.ecs.get(id));
|
||||||
|
|
3
app/net/packets/download.js
Normal file
3
app/net/packets/download.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import Packet from '@/net/packet.js';
|
||||||
|
|
||||||
|
export default class Download extends Packet {}
|
|
@ -8,6 +8,17 @@ const cache = new LRUCache({
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class ClientEcs extends Ecs {
|
export default class ClientEcs extends Ecs {
|
||||||
|
constructor(specification) {
|
||||||
|
super(specification);
|
||||||
|
[
|
||||||
|
'Colliders',
|
||||||
|
].forEach((defaultSystem) => {
|
||||||
|
const System = this.system(defaultSystem);
|
||||||
|
if (System) {
|
||||||
|
System.active = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
async readAsset(uri) {
|
async readAsset(uri) {
|
||||||
if (!cache.has(uri)) {
|
if (!cache.has(uri)) {
|
||||||
const {promise, resolve, reject} = withResolvers();
|
const {promise, resolve, reject} = withResolvers();
|
||||||
|
|
103
app/react/components/dom/dialogue-caret.jsx
Normal file
103
app/react/components/dom/dialogue-caret.jsx
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import {RESOLUTION} from '@/util/constants.js';
|
||||||
|
|
||||||
|
import styles from './dialogue-caret.module.css';
|
||||||
|
|
||||||
|
const CARET_SIZE = 18;
|
||||||
|
|
||||||
|
export default function DialogueCaret({
|
||||||
|
camera,
|
||||||
|
dialogue,
|
||||||
|
dimensions,
|
||||||
|
scale,
|
||||||
|
}) {
|
||||||
|
const origin = 'function' === typeof dialogue.origin
|
||||||
|
? dialogue.origin()
|
||||||
|
: dialogue.origin || {x: 0, y: 0};
|
||||||
|
let position = 'function' === typeof dialogue.position
|
||||||
|
? dialogue.position()
|
||||||
|
: dialogue.position || {x: 0, y: 0};
|
||||||
|
position = {
|
||||||
|
x: position.x + dialogue.offset.x,
|
||||||
|
y: position.y + dialogue.offset.y,
|
||||||
|
};
|
||||||
|
const bounds = {
|
||||||
|
x: dimensions.w / (2 * scale),
|
||||||
|
y: dimensions.h / (2 * scale),
|
||||||
|
};
|
||||||
|
const left = Math.max(
|
||||||
|
Math.min(
|
||||||
|
position.x * scale - camera.x,
|
||||||
|
RESOLUTION.x - bounds.x * scale - 16,
|
||||||
|
),
|
||||||
|
bounds.x * scale + 16,
|
||||||
|
);
|
||||||
|
const top = Math.max(
|
||||||
|
Math.min(
|
||||||
|
position.y * scale - camera.y,
|
||||||
|
RESOLUTION.y - bounds.y * scale - 16,
|
||||||
|
),
|
||||||
|
bounds.y * scale + 88,
|
||||||
|
);
|
||||||
|
const offsetPosition = {
|
||||||
|
x: ((position.x * scale - camera.x) - left) / scale,
|
||||||
|
y: ((position.y * scale - camera.y) - top) / scale,
|
||||||
|
};
|
||||||
|
const difference = {
|
||||||
|
x: origin.x - position.x + offsetPosition.x,
|
||||||
|
y: origin.y - position.y + offsetPosition.y,
|
||||||
|
};
|
||||||
|
const within = {
|
||||||
|
x: Math.abs(difference.x) < bounds.x,
|
||||||
|
y: Math.abs(difference.y) < bounds.y,
|
||||||
|
};
|
||||||
|
const caretPosition = {
|
||||||
|
x: Math.max(-bounds.x, Math.min(origin.x - position.x + offsetPosition.x, bounds.x)),
|
||||||
|
y: Math.max(-bounds.y, Math.min(origin.y - position.y + offsetPosition.y, bounds.y)),
|
||||||
|
};
|
||||||
|
let caretRotation = Math.atan2(
|
||||||
|
difference.y - caretPosition.y,
|
||||||
|
difference.x - caretPosition.x,
|
||||||
|
);
|
||||||
|
caretRotation += Math.PI * 1.5;
|
||||||
|
if (within.x) {
|
||||||
|
caretPosition.y = bounds.y * Math.sign(difference.y);
|
||||||
|
if (within.y) {
|
||||||
|
if (Math.sign(difference.y) > 0) {
|
||||||
|
caretRotation = Math.PI;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
caretRotation = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (within.y) {
|
||||||
|
caretPosition.x = bounds.x * Math.sign(difference.x);
|
||||||
|
}
|
||||||
|
// const corner = {
|
||||||
|
// x: (bounds.x - Math.abs(caretPosition.x)) * Math.sign(caretPosition.x),
|
||||||
|
// y: (bounds.y - Math.abs(caretPosition.y)) * Math.sign(caretPosition.y),
|
||||||
|
// };
|
||||||
|
caretPosition.x *= scale;
|
||||||
|
caretPosition.y *= scale;
|
||||||
|
caretPosition.x += -Math.sin(caretRotation) * (CARET_SIZE / 2);
|
||||||
|
caretPosition.y += Math.cos(caretRotation) * (CARET_SIZE / 2);
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={styles.caret}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width={CARET_SIZE}
|
||||||
|
height={CARET_SIZE}
|
||||||
|
style={{
|
||||||
|
transform: `
|
||||||
|
translate(
|
||||||
|
calc(-50% + ${caretPosition.x}px),
|
||||||
|
calc(-50% + ${caretPosition.y}px)
|
||||||
|
)
|
||||||
|
rotate(${caretRotation}rad)
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<polygon points="12 0, 18 10, 12 23, 23 10, 20 0" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
8
app/react/components/dom/dialogue-caret.module.css
Normal file
8
app/react/components/dom/dialogue-caret.module.css
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.caret {
|
||||||
|
position: absolute;
|
||||||
|
fill: #ffffff;
|
||||||
|
stroke: #00000044;
|
||||||
|
stroke-width: 2px;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
}
|
|
@ -5,10 +5,9 @@ import {useRadians} from '@/react/context/radians.js';
|
||||||
import {RESOLUTION} from '@/util/constants.js';
|
import {RESOLUTION} from '@/util/constants.js';
|
||||||
import {render} from '@/util/dialogue.js';
|
import {render} from '@/util/dialogue.js';
|
||||||
|
|
||||||
|
import DialogueCaret from './dialogue-caret.jsx';
|
||||||
import styles from './dialogue.module.css';
|
import styles from './dialogue.module.css';
|
||||||
|
|
||||||
const CARET_SIZE = 12;
|
|
||||||
|
|
||||||
export default function Dialogue({
|
export default function Dialogue({
|
||||||
camera,
|
camera,
|
||||||
dialogue,
|
dialogue,
|
||||||
|
@ -75,66 +74,31 @@ export default function Dialogue({
|
||||||
() => render(dialogue.letters, styles.letter),
|
() => render(dialogue.letters, styles.letter),
|
||||||
[dialogue.letters],
|
[dialogue.letters],
|
||||||
);
|
);
|
||||||
const origin = 'function' === typeof dialogue.origin
|
let position = 'function' === typeof dialogue.position
|
||||||
? dialogue.origin()
|
? dialogue.position()
|
||||||
: dialogue.origin || {x: 0, y: 0};
|
: dialogue.position || {x: 0, y: 0};
|
||||||
|
position = {
|
||||||
|
x: position.x + dialogue.offset.x,
|
||||||
|
y: position.y + dialogue.offset.y,
|
||||||
|
};
|
||||||
const bounds = {
|
const bounds = {
|
||||||
x: dimensions.w / (2 * scale),
|
x: dimensions.w / (2 * scale),
|
||||||
y: dimensions.h / (2 * scale),
|
y: dimensions.h / (2 * scale),
|
||||||
};
|
};
|
||||||
const left = Math.max(
|
const left = Math.max(
|
||||||
Math.min(
|
Math.min(
|
||||||
dialogue.position.x * scale - camera.x,
|
position.x * scale - camera.x,
|
||||||
RESOLUTION.x - bounds.x * scale - 16,
|
RESOLUTION.x - bounds.x * scale - 16,
|
||||||
),
|
),
|
||||||
bounds.x * scale + 16,
|
bounds.x * scale + 16,
|
||||||
);
|
);
|
||||||
const top = Math.max(
|
const top = Math.max(
|
||||||
Math.min(
|
Math.min(
|
||||||
dialogue.position.y * scale - camera.y,
|
position.y * scale - camera.y,
|
||||||
RESOLUTION.y - bounds.y * scale - 16,
|
RESOLUTION.y - bounds.y * scale - 16,
|
||||||
),
|
),
|
||||||
bounds.y * scale + 88,
|
bounds.y * scale + 88,
|
||||||
);
|
);
|
||||||
const offsetPosition = {
|
|
||||||
x: ((dialogue.position.x * scale - camera.x) - left) / scale,
|
|
||||||
y: ((dialogue.position.y * scale - camera.y) - top) / scale,
|
|
||||||
};
|
|
||||||
const difference = {
|
|
||||||
x: origin.x - dialogue.position.x + offsetPosition.x,
|
|
||||||
y: origin.y - dialogue.position.y + offsetPosition.y,
|
|
||||||
};
|
|
||||||
const within = {
|
|
||||||
x: Math.abs(difference.x) < bounds.x,
|
|
||||||
y: Math.abs(difference.y) < bounds.y,
|
|
||||||
};
|
|
||||||
const caretPosition = {
|
|
||||||
x: Math.max(-bounds.x, Math.min(origin.x - dialogue.position.x + offsetPosition.x, bounds.x)),
|
|
||||||
y: Math.max(-bounds.y, Math.min(origin.y - dialogue.position.y + offsetPosition.y, bounds.y)),
|
|
||||||
};
|
|
||||||
let caretRotation = Math.atan2(
|
|
||||||
difference.y - caretPosition.y,
|
|
||||||
difference.x - caretPosition.x,
|
|
||||||
);
|
|
||||||
caretRotation += Math.PI * 1.5;
|
|
||||||
if (within.x) {
|
|
||||||
caretPosition.y = bounds.y * Math.sign(difference.y);
|
|
||||||
if (within.y) {
|
|
||||||
if (Math.sign(difference.y) > 0) {
|
|
||||||
caretRotation = Math.PI;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
caretRotation = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (within.y) {
|
|
||||||
caretPosition.x = bounds.x * Math.sign(difference.x);
|
|
||||||
}
|
|
||||||
caretPosition.x *= scale;
|
|
||||||
caretPosition.y *= scale;
|
|
||||||
caretPosition.x += -Math.sin(caretRotation) * (CARET_SIZE / 2);
|
|
||||||
caretPosition.y += Math.cos(caretRotation) * (CARET_SIZE / 2);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.dialogue}
|
className={styles.dialogue}
|
||||||
|
@ -144,23 +108,13 @@ export default function Dialogue({
|
||||||
top: `${top}px`,
|
top: `${top}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<DialogueCaret
|
||||||
className={styles.caret}
|
camera={camera}
|
||||||
viewBox="0 0 24 24"
|
dialogue={dialogue}
|
||||||
width={CARET_SIZE}
|
dimensions={dimensions}
|
||||||
height={CARET_SIZE}
|
scale={scale}
|
||||||
style={{
|
|
||||||
transform: `
|
/>
|
||||||
translate(
|
|
||||||
calc(-50% + ${caretPosition.x}px),
|
|
||||||
calc(-50% + ${caretPosition.y}px)
|
|
||||||
)
|
|
||||||
rotate(${caretRotation}rad)
|
|
||||||
`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<polygon points="0 0, 24 0, 12 24" />
|
|
||||||
</svg>
|
|
||||||
<p className={styles.letters}>
|
<p className={styles.letters}>
|
||||||
{localRender(caret, radians)}
|
{localRender(caret, radians)}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
.caret {
|
|
||||||
position: absolute;
|
|
||||||
fill: #00000044;
|
|
||||||
stroke: white;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialogue {
|
.dialogue {
|
||||||
background-color: #00000044;
|
background-color: #02023999;
|
||||||
border: solid 1px white;
|
border: solid 3px white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: white;
|
color: white;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
padding: 1em;
|
padding: 12px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-right: -33%;
|
margin-right: -66%;
|
||||||
|
text-shadow:
|
||||||
|
0px -1px 0px black,
|
||||||
|
1px 0px 0px black,
|
||||||
|
0px 1px 0px black,
|
||||||
|
-1px 0px 0px black
|
||||||
|
;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
max-width: 33%;
|
max-width: 66%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.letters {
|
.letters {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.dialogues {
|
.dialogues {
|
||||||
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||||
font-size: 16px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|
|
@ -38,6 +38,15 @@ export default function Entities({
|
||||||
const {dialogues} = updating[id].Interlocutor;
|
const {dialogues} = updating[id].Interlocutor;
|
||||||
for (const key in dialogue) {
|
for (const key in dialogue) {
|
||||||
dialogues[key] = dialogue[key];
|
dialogues[key] = dialogue[key];
|
||||||
|
if (!dialogues[key].offset) {
|
||||||
|
dialogues[key].offset = {x: 0, y: 0};
|
||||||
|
}
|
||||||
|
if ('track' === dialogues[key].origin) {
|
||||||
|
dialogues[key].origin = () => updating[id].Position;
|
||||||
|
}
|
||||||
|
if ('track' === dialogues[key].position) {
|
||||||
|
dialogues[key].position = () => updating[id].Position;
|
||||||
|
}
|
||||||
dialogues[key].letters = parseLetters(dialogues[key].body);
|
dialogues[key].letters = parseLetters(dialogues[key].body);
|
||||||
setChatMessages((chatMessages) => ({
|
setChatMessages((chatMessages) => ({
|
||||||
[[id, key].join('-')]: dialogues[key].letters,
|
[[id, key].join('-')]: dialogues[key].letters,
|
||||||
|
|
|
@ -1,35 +1,19 @@
|
||||||
import {RenderTexture} from '@pixi/core';
|
import {RenderTexture} from '@pixi/core';
|
||||||
import {Container} from '@pixi/display';
|
import {Container} from '@pixi/display';
|
||||||
|
import {Graphics} from '@pixi/graphics';
|
||||||
import {PixiComponent, useApp} from '@pixi/react';
|
import {PixiComponent, useApp} from '@pixi/react';
|
||||||
import {Sprite} from '@pixi/sprite';
|
import {Sprite} from '@pixi/sprite';
|
||||||
import '@pixi/spritesheet'; // NECESSARY!
|
import '@pixi/spritesheet'; // NECESSARY!
|
||||||
import {CompositeTilemap} from '@pixi/tilemap';
|
import {CompositeTilemap} from '@pixi/tilemap';
|
||||||
|
|
||||||
import {useAsset} from '@/react/context/assets.js';
|
import {useAsset} from '@/react/context/assets.js';
|
||||||
import {CHUNK_SIZE} from '@/util/constants.js';
|
import {CHUNK_SIZE, RESOLUTION} from '@/util/constants.js';
|
||||||
|
|
||||||
import {deferredLighting} from './lights.js';
|
import {deferredLighting} from './lights.js';
|
||||||
|
|
||||||
const TileLayerInternal = PixiComponent('TileLayer', {
|
const TileLayerInternal = PixiComponent('TileLayer', {
|
||||||
create: ({group, tileLayer}) => {
|
create: () => {
|
||||||
const container = new Container();
|
const container = new Container();
|
||||||
const cy = Math.ceil(tileLayer.area.y / CHUNK_SIZE);
|
|
||||||
const cx = Math.ceil(tileLayer.area.x / CHUNK_SIZE);
|
|
||||||
for (let iy = 0; iy < cy; ++iy) {
|
|
||||||
for (let ix = 0; ix < cx; ++ix) {
|
|
||||||
const tilemap = new CompositeTilemap();
|
|
||||||
const renderTexture = RenderTexture.create({
|
|
||||||
width: tileLayer.tileSize.x * CHUNK_SIZE,
|
|
||||||
height: tileLayer.tileSize.y * CHUNK_SIZE,
|
|
||||||
});
|
|
||||||
const sprite = new Sprite(renderTexture);
|
|
||||||
sprite.x = tileLayer.tileSize.x * CHUNK_SIZE * ix;
|
|
||||||
sprite.y = tileLayer.tileSize.y * CHUNK_SIZE * iy;
|
|
||||||
sprite.parentGroup = group;
|
|
||||||
sprite.tilemap = tilemap;
|
|
||||||
container.addChild(sprite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return container;
|
return container;
|
||||||
},
|
},
|
||||||
applyProps: (container, {tileLayer: oldTileLayer}, props) => {
|
applyProps: (container, {tileLayer: oldTileLayer}, props) => {
|
||||||
|
@ -39,9 +23,67 @@ const TileLayerInternal = PixiComponent('TileLayer', {
|
||||||
if (tileLayer === oldTileLayer) {
|
if (tileLayer === oldTileLayer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
!oldTileLayer
|
||||||
|
|| (
|
||||||
|
oldTileLayer.area.x !== tileLayer.area.x
|
||||||
|
|| oldTileLayer.area.y !== tileLayer.area.y
|
||||||
|
|| oldTileLayer.tileSize.x !== tileLayer.tileSize.x
|
||||||
|
|| oldTileLayer.tileSize.y !== tileLayer.tileSize.y
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
container.removeChildren();
|
||||||
|
const {area, tileSize} = tileLayer;
|
||||||
|
const g = new Graphics();
|
||||||
|
g.beginFill(group === deferredLighting.diffuseGroup ? 0x000000 : 0x7777ff);
|
||||||
|
// outer frame
|
||||||
|
g.drawRect(
|
||||||
|
-RESOLUTION.x / 2,
|
||||||
|
-RESOLUTION.y / 2,
|
||||||
|
area.x * tileSize.x + RESOLUTION.x,
|
||||||
|
RESOLUTION.y / 2,
|
||||||
|
);
|
||||||
|
g.drawRect(
|
||||||
|
-RESOLUTION.x / 2,
|
||||||
|
-RESOLUTION.y / 2,
|
||||||
|
RESOLUTION.x / 2,
|
||||||
|
area.y * tileSize.y + RESOLUTION.y,
|
||||||
|
);
|
||||||
|
g.drawRect(
|
||||||
|
area.x * tileSize.x,
|
||||||
|
0,
|
||||||
|
RESOLUTION.x / 2,
|
||||||
|
area.y * tileSize.y,
|
||||||
|
);
|
||||||
|
g.drawRect(
|
||||||
|
0,
|
||||||
|
area.y * tileSize.y,
|
||||||
|
area.x * tileSize.x,
|
||||||
|
RESOLUTION.y / 2,
|
||||||
|
);
|
||||||
|
g.parentGroup = group;
|
||||||
|
container.addChild(g);
|
||||||
|
const cy = Math.ceil(area.y / CHUNK_SIZE);
|
||||||
|
const cx = Math.ceil(area.x / CHUNK_SIZE);
|
||||||
|
for (let iy = 0; iy < cy; ++iy) {
|
||||||
|
for (let ix = 0; ix < cx; ++ix) {
|
||||||
|
const tilemap = new CompositeTilemap();
|
||||||
|
const renderTexture = RenderTexture.create({
|
||||||
|
width: tileSize.x * CHUNK_SIZE,
|
||||||
|
height: tileSize.y * CHUNK_SIZE,
|
||||||
|
});
|
||||||
|
const sprite = new Sprite(renderTexture);
|
||||||
|
sprite.x = tileSize.x * CHUNK_SIZE * ix;
|
||||||
|
sprite.y = tileSize.y * CHUNK_SIZE * iy;
|
||||||
|
sprite.parentGroup = group;
|
||||||
|
sprite.tilemap = tilemap;
|
||||||
|
container.addChild(sprite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const i in tileLayer.$$chunks) {
|
for (const i in tileLayer.$$chunks) {
|
||||||
if (!oldTileLayer || oldTileLayer.$$chunks[i] !== tileLayer.$$chunks[i]) {
|
if (!oldTileLayer || oldTileLayer.$$chunks[i] !== tileLayer.$$chunks[i]) {
|
||||||
const {texture, tilemap} = container.children[i];
|
const {texture, tilemap} = container.children[parseInt(i) + 1];
|
||||||
tilemap.clear();
|
tilemap.clear();
|
||||||
const ax = Math.ceil(tileLayer.area.x / CHUNK_SIZE);
|
const ax = Math.ceil(tileLayer.area.x / CHUNK_SIZE);
|
||||||
const cy = Math.floor(i / ax);
|
const cy = Math.floor(i / ax);
|
||||||
|
|
|
@ -98,6 +98,25 @@ export default function PlaySpecific() {
|
||||||
client.removePacketListener('ConnectionStatus', onConnectionStatus);
|
client.removePacketListener('ConnectionStatus', onConnectionStatus);
|
||||||
};
|
};
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function onDownload({data, filename}) {
|
||||||
|
var blob = new Blob(
|
||||||
|
[(new TextEncoder()).encode(JSON.stringify(data))],
|
||||||
|
{type: 'application/json'},
|
||||||
|
);
|
||||||
|
var link = document.createElement('a');
|
||||||
|
link.href = window.URL.createObjectURL(blob);
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
client.addPacketListener('Download', onDownload);
|
||||||
|
return () => {
|
||||||
|
client.removePacketListener('Download', onDownload);
|
||||||
|
};
|
||||||
|
}, [client]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!client || !disconnected) {
|
if (!client || !disconnected) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import data from '../../../public/assets/dev/homestead.json';
|
||||||
|
|
||||||
export default async function createHomestead(id) {
|
export default async function createHomestead(id) {
|
||||||
const area = {x: 100, y: 60};
|
const area = {x: 100, y: 60};
|
||||||
const entities = [];
|
const entities = [];
|
||||||
|
@ -8,7 +10,7 @@ export default async function createHomestead(id) {
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
area,
|
area,
|
||||||
data: Array(area.x * area.y).fill(0).map(() => 1 + Math.floor(Math.random() * 4)),
|
data,
|
||||||
source: '/assets/tileset.json',
|
source: '/assets/tileset.json',
|
||||||
tileSize: {x: 16, y: 16},
|
tileSize: {x: 16, y: 16},
|
||||||
},
|
},
|
||||||
|
@ -79,8 +81,9 @@ export default async function createHomestead(id) {
|
||||||
subject.Interlocutor.dialogue({
|
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!",
|
body: "Sure, I'm a treasure chest. Probably. Do you really think that means you're about to get some treasure? Hah!",
|
||||||
monopolizer: true,
|
monopolizer: true,
|
||||||
origin: subject.Position.toJSON(),
|
offset: {x: 0, y: -48},
|
||||||
position: {x: subject.Position.x, y: subject.Position.y - 32},
|
origin: 'track',
|
||||||
|
position: 'track',
|
||||||
})
|
})
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
@ -115,6 +118,27 @@ export default async function createHomestead(id) {
|
||||||
Controlled: {},
|
Controlled: {},
|
||||||
Direction: {direction: 2},
|
Direction: {direction: 2},
|
||||||
Forces: {},
|
Forces: {},
|
||||||
|
Interactive: {
|
||||||
|
interacting: 1,
|
||||||
|
interactScript: `
|
||||||
|
const lines = [
|
||||||
|
'mrowwr',
|
||||||
|
'p<shake>rrr</shake>o<wave>wwwww</wave>',
|
||||||
|
'mew<rate frequency={0.5}> </rate>mew!',
|
||||||
|
'me<wave>wwwww</wave>',
|
||||||
|
'\\\\*pu<shake>rrrrr</shake>\\\\*',
|
||||||
|
];
|
||||||
|
const line = lines[Math.floor(Math.random() * lines.length)];
|
||||||
|
subject.Interlocutor.dialogue({
|
||||||
|
body: line,
|
||||||
|
linger: 2,
|
||||||
|
offset: {x: 0, y: -32},
|
||||||
|
origin: 'track',
|
||||||
|
position: 'track',
|
||||||
|
})
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
Interlocutor: {},
|
||||||
Position: {x: 250, y: 250},
|
Position: {x: 250, y: 250},
|
||||||
Speed: {speed: 20},
|
Speed: {speed: 20},
|
||||||
Sprite: {
|
Sprite: {
|
||||||
|
@ -123,11 +147,41 @@ export default async function createHomestead(id) {
|
||||||
source: '/assets/kitty/kitty.json',
|
source: '/assets/kitty/kitty.json',
|
||||||
speed: 0.115,
|
speed: 0.115,
|
||||||
},
|
},
|
||||||
|
Tags: {tags: ['kittan']},
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
};
|
};
|
||||||
for (let i = 0; i < 10; ++i) {
|
for (let i = 0; i < 30; ++i) {
|
||||||
entities.push(kitty);
|
entities.push(kitty);
|
||||||
}
|
}
|
||||||
|
entities.push({
|
||||||
|
Collider: {
|
||||||
|
bodies: [
|
||||||
|
{
|
||||||
|
points: [
|
||||||
|
{x: -8, y: -16},
|
||||||
|
{x: 7, y: -16},
|
||||||
|
{x: 7, y: 15},
|
||||||
|
{x: -8, y: 15},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
collisionStartScript: `
|
||||||
|
ecs.switchEcs(
|
||||||
|
other,
|
||||||
|
'town',
|
||||||
|
{
|
||||||
|
Position: {
|
||||||
|
x: 940,
|
||||||
|
y: 480,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
Position: {x: 8, y: 432},
|
||||||
|
Ticking: {},
|
||||||
|
});
|
||||||
return entities;
|
return entities;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,13 +38,17 @@ export default async function createPlayer(id) {
|
||||||
qty: 1,
|
qty: 1,
|
||||||
source: '/assets/hoe/hoe.json',
|
source: '/assets/hoe/hoe.json',
|
||||||
},
|
},
|
||||||
|
5: {
|
||||||
|
qty: 1,
|
||||||
|
source: '/assets/brush/brush.json',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Health: {health: 100},
|
Health: {health: 100},
|
||||||
Light: {},
|
Light: {},
|
||||||
Magnet: {strength: 24},
|
Magnet: {strength: 24},
|
||||||
Player: {},
|
Player: {},
|
||||||
Position: {x: 128, y: 128},
|
Position: {x: 128, y: 448},
|
||||||
Speed: {speed: 100},
|
Speed: {speed: 100},
|
||||||
Sound: {},
|
Sound: {},
|
||||||
Sprite: {
|
Sprite: {
|
||||||
|
|
57
app/server/create/town.js
Normal file
57
app/server/create/town.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import data from '../../../public/assets/dev/town.json';
|
||||||
|
|
||||||
|
export default async function createTown() {
|
||||||
|
const area = {x: 60, y: 60};
|
||||||
|
const entities = [];
|
||||||
|
entities.push({
|
||||||
|
AreaSize: {x: area.x * 16, y: area.y * 16},
|
||||||
|
Ticking: {},
|
||||||
|
TileLayers: {
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
area,
|
||||||
|
data,
|
||||||
|
source: '/assets/tileset.json',
|
||||||
|
tileSize: {x: 16, y: 16},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
area,
|
||||||
|
data: Array(area.x * area.y).fill(0),
|
||||||
|
source: '/assets/tileset.json',
|
||||||
|
tileSize: {x: 16, y: 16},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Time: {},
|
||||||
|
Water: {water: {}},
|
||||||
|
});
|
||||||
|
entities.push({
|
||||||
|
Collider: {
|
||||||
|
bodies: [
|
||||||
|
{
|
||||||
|
points: [
|
||||||
|
{x: -8, y: -16},
|
||||||
|
{x: 7, y: -16},
|
||||||
|
{x: 7, y: 15},
|
||||||
|
{x: -8, y: 15},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
collisionStartScript: `
|
||||||
|
ecs.switchEcs(
|
||||||
|
other,
|
||||||
|
['homesteads', '0'].join('/'),
|
||||||
|
{
|
||||||
|
Position: {
|
||||||
|
x: 20,
|
||||||
|
y: 438,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
Position: {x: 952, y: 480},
|
||||||
|
Ticking: {},
|
||||||
|
});
|
||||||
|
return entities;
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import createForest from './create/forest.js';
|
||||||
import createHomestead from './create/homestead.js';
|
import createHomestead from './create/homestead.js';
|
||||||
import createHouse from './create/house.js';
|
import createHouse from './create/house.js';
|
||||||
import createPlayer from './create/player.js';
|
import createPlayer from './create/player.js';
|
||||||
|
import createTown from './create/town.js';
|
||||||
|
|
||||||
const cache = new LRUCache({
|
const cache = new LRUCache({
|
||||||
max: 128,
|
max: 128,
|
||||||
|
@ -125,22 +126,47 @@ export default class Engine {
|
||||||
Interacts,
|
Interacts,
|
||||||
Interlocutor,
|
Interlocutor,
|
||||||
Inventory,
|
Inventory,
|
||||||
Position,
|
|
||||||
Wielder,
|
Wielder,
|
||||||
} = entity;
|
} = entity;
|
||||||
|
const ecs = this.ecses[Ecs.path];
|
||||||
for (const payload of payloads) {
|
for (const payload of payloads) {
|
||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case 'chat': {
|
case 'chat': {
|
||||||
|
if (payload.value.startsWith('/')) {
|
||||||
|
const [command, ...args] = payload.value.slice(1).split(' ');
|
||||||
|
switch (command) {
|
||||||
|
case 'dump': {
|
||||||
|
switch (args[0]) {
|
||||||
|
case 'tiles': {
|
||||||
|
const {TileLayers} = ecs.get(1);
|
||||||
|
this.server.send(
|
||||||
|
connection,
|
||||||
|
{
|
||||||
|
type: 'Download',
|
||||||
|
payload: {
|
||||||
|
data: TileLayers.layer(0).data,
|
||||||
|
filename: 'tiles.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
Interlocutor.dialogue({
|
Interlocutor.dialogue({
|
||||||
body: payload.value,
|
body: payload.value,
|
||||||
linger: 5,
|
linger: 5,
|
||||||
origin: Position.toJSON(),
|
offset: {x: 0, y: -40},
|
||||||
position: {x: Position.x, y: Position.y - 32},
|
origin: 'track',
|
||||||
|
position: 'track',
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'paint': {
|
case 'paint': {
|
||||||
const ecs = this.ecses[Ecs.path];
|
|
||||||
const {TileLayers} = ecs.get(1);
|
const {TileLayers} = ecs.get(1);
|
||||||
const {brush, layer: paintLayer, stamp} = payload.value;
|
const {brush, layer: paintLayer, stamp} = payload.value;
|
||||||
const layer = TileLayers.layer(paintLayer);
|
const layer = TileLayers.layer(paintLayer);
|
||||||
|
@ -181,7 +207,6 @@ export default class Engine {
|
||||||
if (!Controlled.locked) {
|
if (!Controlled.locked) {
|
||||||
if (payload.value) {
|
if (payload.value) {
|
||||||
if (Interacts.willInteractWith) {
|
if (Interacts.willInteractWith) {
|
||||||
const ecs = this.ecses[Ecs.path];
|
|
||||||
const subject = ecs.get(Interacts.willInteractWith);
|
const subject = ecs.get(Interacts.willInteractWith);
|
||||||
subject.Interactive.interact(entity);
|
subject.Interactive.interact(entity);
|
||||||
}
|
}
|
||||||
|
@ -233,6 +258,25 @@ export default class Engine {
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
let townData;
|
||||||
|
try {
|
||||||
|
townData = await this.server.readData('town');
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if ('ENOENT' !== error.code) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const town = this.createEcs();
|
||||||
|
for (const entity of await createTown()) {
|
||||||
|
await town.create(entity);
|
||||||
|
}
|
||||||
|
await this.saveEcs('town', town);
|
||||||
|
townData = await this.server.readData('town');
|
||||||
|
}
|
||||||
|
this.ecses['town'] = await this.Ecs.deserialize(
|
||||||
|
this.createEcs(),
|
||||||
|
townData,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadEcs(path) {
|
async loadEcs(path) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import createEcs from './create/ecs.js';
|
||||||
import './create/forest.js';
|
import './create/forest.js';
|
||||||
import './create/homestead.js';
|
import './create/homestead.js';
|
||||||
import './create/player.js';
|
import './create/player.js';
|
||||||
|
import './create/town.js';
|
||||||
|
|
||||||
import Engine from './engine.js';
|
import Engine from './engine.js';
|
||||||
|
|
||||||
|
@ -111,6 +112,19 @@ if (import.meta.hot) {
|
||||||
await engine.saveEcs('homesteads/0', homestead);
|
await engine.saveEcs('homesteads/0', homestead);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
import.meta.hot.accept('./create/town.js', async ({default: createTown}) => {
|
||||||
|
const {promise, resolve} = withResolvers();
|
||||||
|
promises.push(promise);
|
||||||
|
await before.promise;
|
||||||
|
delete engine.ecses['town'];
|
||||||
|
await engine.server.removeData('town');
|
||||||
|
const town = createEcs(engine.Ecs);
|
||||||
|
for (const entity of await createTown()) {
|
||||||
|
await town.create(entity);
|
||||||
|
}
|
||||||
|
await engine.saveEcs('town', town);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
import.meta.hot.on('vite:afterUpdate', async () => {
|
import.meta.hot.on('vite:afterUpdate', async () => {
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export const CHUNK_SIZE = 32;
|
export const CHUNK_SIZE = 32;
|
||||||
|
|
||||||
export const CLIENT_LATENCY = 100;
|
export const CLIENT_LATENCY = 0;
|
||||||
|
|
||||||
export const CLIENT_PREDICTION = true;
|
export const CLIENT_PREDICTION = true;
|
||||||
|
|
||||||
|
@ -11,6 +11,6 @@ export const RESOLUTION = {
|
||||||
y: 450,
|
y: 450,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SERVER_LATENCY = 100;
|
export const SERVER_LATENCY = 0;
|
||||||
|
|
||||||
export const TPS = 60;
|
export const TPS = 60;
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import {createElement} from 'react';
|
import {createElement} from 'react';
|
||||||
import mdx from 'remark-mdx';
|
import mdx from 'remark-mdx';
|
||||||
import parse from 'remark-parse';
|
import parse from 'remark-parse';
|
||||||
|
import {createNoise2D} from 'simplex-noise';
|
||||||
import {unified} from 'unified';
|
import {unified} from 'unified';
|
||||||
import {visitParents as visit} from 'unist-util-visit-parents';
|
import {visitParents as visit} from 'unist-util-visit-parents';
|
||||||
|
|
||||||
import {TAU} from '@/util/math.js';
|
import {TAU} from '@/util/math.js';
|
||||||
|
|
||||||
|
const rawNoise = createNoise2D();
|
||||||
|
const noise = (x, y) => (1 + rawNoise(x, y)) / 2;
|
||||||
|
|
||||||
const parser = unified().use(parse).use(mdx);
|
const parser = unified().use(parse).use(mdx);
|
||||||
|
|
||||||
function computeParams(ancestors) {
|
function computeParams(ancestors) {
|
||||||
|
@ -96,8 +100,9 @@ export function render(letters, className) {
|
||||||
}
|
}
|
||||||
if (params.shake) {
|
if (params.shake) {
|
||||||
const {magnitude = 1} = params.shake;
|
const {magnitude = 1} = params.shake;
|
||||||
left += (Math.random() * magnitude * 2) - magnitude;
|
const r = radians + TAU * indices.shake / params.shake.length;
|
||||||
top += (Math.random() * magnitude * 2) - magnitude;
|
left += (noise(-Math.sin(r) * 32, Math.cos(r) * 32) * magnitude * 2) - magnitude;
|
||||||
|
top += (noise(Math.sin(r) * 32, -Math.cos(r) * 32) * magnitude * 2) - magnitude;
|
||||||
}
|
}
|
||||||
if (params.em) {
|
if (params.em) {
|
||||||
fontStyle = 'italic';
|
fontStyle = 'italic';
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
export default class First {
|
|
||||||
static gathered(id, key) {
|
|
||||||
this.id = id;
|
|
||||||
this.key = key;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
export default class Second {
|
|
||||||
static gathered(id, key) {
|
|
||||||
this.id = id;
|
|
||||||
this.key = key;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,16 +4,16 @@ function capitalize(string) {
|
||||||
|
|
||||||
export default function gather(imports, options = {}) {
|
export default function gather(imports, options = {}) {
|
||||||
const {
|
const {
|
||||||
mapperForPath = (path) => path.replace(/\.\/(.*)\.js/, '$1'),
|
pathToKey = (path) => (
|
||||||
} = options;
|
path.replace(/\.\/(.*)\.js/, '$1')
|
||||||
const Gathered = {};
|
|
||||||
for (const [path, Component] of Object.entries(imports).sort(([l], [r]) => l < r ? -1 : 1)) {
|
|
||||||
Gathered[
|
|
||||||
mapperForPath(path)
|
|
||||||
.split('-')
|
.split('-')
|
||||||
.map(capitalize)
|
.map(capitalize)
|
||||||
.join('')
|
.join('')
|
||||||
] = Component;
|
),
|
||||||
|
} = options;
|
||||||
|
const Gathered = {};
|
||||||
|
for (const [path, Component] of Object.entries(imports).sort(([l], [r]) => l < r ? -1 : 1)) {
|
||||||
|
Gathered[pathToKey(path)] = Component;
|
||||||
}
|
}
|
||||||
return Gathered;
|
return Gathered;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import {expect, test} from 'vitest';
|
|
||||||
|
|
||||||
import gather from './gather.js';
|
|
||||||
|
|
||||||
import First from './gather-test/first.js';
|
|
||||||
import Second from './gather-test/second.js';
|
|
||||||
|
|
||||||
test('gathers', async () => {
|
|
||||||
const Gathered = gather(
|
|
||||||
import.meta.glob('./gather-test/*.js', {eager: true, import: 'default'}),
|
|
||||||
{mapperForPath: (path) => path.replace(/\.\/gather-test\/(.*)\.js/, '$1')},
|
|
||||||
);
|
|
||||||
expect(Gathered.First)
|
|
||||||
.to.equal(First);
|
|
||||||
expect(Gathered.Second)
|
|
||||||
.to.equal(Second);
|
|
||||||
});
|
|
4
public/assets/brush/brush.json
Normal file
4
public/assets/brush/brush.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"icon": "/assets/brush/brush.png",
|
||||||
|
"start": "/assets/brush/start.js"
|
||||||
|
}
|
BIN
public/assets/brush/brush.png
Normal file
BIN
public/assets/brush/brush.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
public/assets/brush/brush.wav
Normal file
BIN
public/assets/brush/brush.wav
Normal file
Binary file not shown.
22
public/assets/brush/start.js
Normal file
22
public/assets/brush/start.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
const {Collider, Controlled, Interacts, Inventory, Sound, Sprite} = wielder
|
||||||
|
const entities = Collider.closest(Interacts.aabb());
|
||||||
|
for (const entity of entities) {
|
||||||
|
const {Tags} = entity;
|
||||||
|
if (Tags && Tags.has('kittan')) {
|
||||||
|
Controlled.locked = 1
|
||||||
|
const [, direction] = Sprite.animation.split(':')
|
||||||
|
for (let i = 0; i < 2; ++i) {
|
||||||
|
Sound.play('/assets/brush/brush.wav');
|
||||||
|
Sprite.animation = ['moving', direction].join(':');
|
||||||
|
await wait(0.3)
|
||||||
|
Sprite.animation = ['idle', direction].join(':');
|
||||||
|
await wait(0.1)
|
||||||
|
}
|
||||||
|
Inventory.give({
|
||||||
|
qty: 1,
|
||||||
|
source: '/assets/furball/furball.json',
|
||||||
|
});
|
||||||
|
Controlled.locked = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
1
public/assets/dev/homestead.json
Normal file
1
public/assets/dev/homestead.json
Normal file
File diff suppressed because one or more lines are too long
1
public/assets/dev/town.json
Normal file
1
public/assets/dev/town.json
Normal file
File diff suppressed because one or more lines are too long
3
public/assets/furball/furball.json
Normal file
3
public/assets/furball/furball.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"icon": "/assets/furball/furball.png"
|
||||||
|
}
|
BIN
public/assets/furball/furball.png
Normal file
BIN
public/assets/furball/furball.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
Loading…
Reference in New Issue
Block a user