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 [];
|
||||
}
|
||||
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 promises = [];
|
||||
for (let i = 0; i < entries.length; ++i) {
|
||||
|
@ -47,15 +48,17 @@ export default class Component {
|
|||
this.data[allocated[i]].entity = entityId;
|
||||
for (let k = 0; k < keys.length; ++k) {
|
||||
const j = keys[k];
|
||||
const {defaultValue} = properties[j];
|
||||
const instance = this.data[allocated[i]];
|
||||
if (j in values) {
|
||||
instance[j] = values[j];
|
||||
}
|
||||
else if ('undefined' !== typeof defaultValue) {
|
||||
else {
|
||||
const defaultValue = Schema.defaultValue(properties[j]);
|
||||
if ('undefined' !== typeof defaultValue) {
|
||||
instance[j] = defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
promises.push(this.load(this.data[allocated[i]]));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
@ -67,7 +70,7 @@ export default class Component {
|
|||
}
|
||||
|
||||
deserialize(entityId, view, offset) {
|
||||
const {properties} = this.constructor.schema.specification;
|
||||
const {properties} = this.constructor.schema.specification.concrete;
|
||||
const instance = this.get(entityId);
|
||||
const deserialized = this.constructor.schema.deserialize(view, offset);
|
||||
for (const key in properties) {
|
||||
|
@ -96,11 +99,11 @@ export default class Component {
|
|||
}
|
||||
|
||||
static filterDefaults(instance) {
|
||||
const {properties} = this.schema.specification;
|
||||
const {properties} = this.schema.specification.concrete;
|
||||
const Schema = this.schema.constructor;
|
||||
const json = {};
|
||||
for (const key in properties) {
|
||||
const {defaultValue} = properties[key];
|
||||
if (key in instance && instance[key] !== defaultValue) {
|
||||
if (key in instance && instance[key] !== Schema.defaultValue(properties[key])) {
|
||||
json[key] = instance[key];
|
||||
}
|
||||
}
|
||||
|
@ -136,16 +139,16 @@ export default class Component {
|
|||
|
||||
instanceFromSchema() {
|
||||
const Component = this;
|
||||
const {specification} = Component.constructor.schema;
|
||||
const {concrete} = Component.constructor.schema.specification;
|
||||
const Schema = Component.constructor.schema.constructor;
|
||||
const Instance = class {
|
||||
$$entity = 0;
|
||||
constructor() {
|
||||
this.$$reset();
|
||||
}
|
||||
$$reset() {
|
||||
for (const key in specification.properties) {
|
||||
const {defaultValue} = specification.properties[key];
|
||||
this[`$$${key}`] = defaultValue;
|
||||
for (const key in concrete.properties) {
|
||||
this[`$$${key}`] = Schema.defaultValue(concrete.properties[key]);
|
||||
}
|
||||
}
|
||||
destroy() {}
|
||||
|
@ -166,7 +169,7 @@ export default class Component {
|
|||
this.$$reset();
|
||||
},
|
||||
};
|
||||
for (const key in specification.properties) {
|
||||
for (const key in concrete.properties) {
|
||||
properties[key] = {
|
||||
get: function get() {
|
||||
return this[`$$${key}`];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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';
|
||||
|
||||
|
@ -30,6 +30,22 @@ export default class Collider extends Component {
|
|||
}
|
||||
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) {
|
||||
const {aabb, aabbs} = this;
|
||||
const {aabb: otherAabb, aabbs: otherAabbs} = other;
|
||||
|
|
|
@ -9,16 +9,16 @@ export default class Interacts extends Component {
|
|||
let x0 = Position.x - 8;
|
||||
let y0 = Position.y - 8;
|
||||
if (0 === Direction.direction) {
|
||||
y0 -= 16
|
||||
y0 -= 12
|
||||
}
|
||||
if (1 === Direction.direction) {
|
||||
x0 += 16
|
||||
x0 += 12
|
||||
}
|
||||
if (2 === Direction.direction) {
|
||||
y0 += 16
|
||||
y0 += 12
|
||||
}
|
||||
if (3 === Direction.direction) {
|
||||
x0 -= 16
|
||||
x0 -= 12
|
||||
}
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
this.$$entities[entityId] = undefined;
|
||||
this.diff[entityId] = false;
|
||||
}
|
||||
for (const i in destroying) {
|
||||
this.Components[i].destroyMany(destroying[i]);
|
||||
}
|
||||
for (const entityId of entityIds) {
|
||||
this.$$entities[entityId] = undefined;
|
||||
this.diff[entityId] = false;
|
||||
}
|
||||
}
|
||||
|
||||
get entities() {
|
||||
|
@ -382,7 +388,11 @@ export default class Ecs {
|
|||
|
||||
reindex(entityIds) {
|
||||
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 {
|
||||
|
||||
$$size = 0;
|
||||
static $$types = {};
|
||||
|
||||
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) {
|
||||
this.specification = this.constructor.normalize(specification);
|
||||
}
|
||||
|
||||
static deserialize(view, offset, specification) {
|
||||
const viewGetMethod = this.viewGetMethods[specification.type];
|
||||
if (viewGetMethod) {
|
||||
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 '';
|
||||
}
|
||||
static defaultValue({$, concrete}) {
|
||||
if (concrete.defaultValue) {
|
||||
return concrete.defaultValue;
|
||||
}
|
||||
return $.defaultValue(concrete);
|
||||
}
|
||||
|
||||
defaultValue() {
|
||||
return this.constructor.defaultValue(this.specification);
|
||||
}
|
||||
|
||||
static normalize(specification) {
|
||||
let normalized = specification;
|
||||
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 deserialize(view, offset, {$, concrete}) {
|
||||
return $.deserialize(view, offset, concrete);
|
||||
}
|
||||
|
||||
static serialize(source, view, offset, specification) {
|
||||
const viewSetMethod = this.viewSetMethods[specification.type];
|
||||
if (viewSetMethod) {
|
||||
view[viewSetMethod](offset, source, true);
|
||||
return this.size(specification);
|
||||
deserialize(view, offset = 0) {
|
||||
return this.constructor.deserialize(view, {value: offset}, this.specification);
|
||||
}
|
||||
switch (specification.type) {
|
||||
case 'array': {
|
||||
view.setUint32(offset, source.length, true);
|
||||
let arraySize = 4;
|
||||
for (const element of source) {
|
||||
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;
|
||||
|
||||
static normalize({type, ...rest}) {
|
||||
const $$type = this.$$types[type];
|
||||
if (!$$type) {
|
||||
throw new TypeError(`unregistered schema type '${type}'`);
|
||||
}
|
||||
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) {
|
||||
this.constructor.serialize(source, view, offset, this.specification);
|
||||
}
|
||||
|
||||
static sizeOf(concrete, specification) {
|
||||
const size = this.size(specification);
|
||||
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;
|
||||
static sizeOf(instance, {$, concrete}) {
|
||||
return $.sizeOf(instance, concrete);
|
||||
}
|
||||
|
||||
sizeOf(concrete) {
|
||||
return this.constructor.sizeOf(concrete, this.specification);
|
||||
sizeOf(instance) {
|
||||
return this.constructor.sizeOf(instance, this.specification);
|
||||
}
|
||||
|
||||
static size(specification) {
|
||||
switch (specification.type) {
|
||||
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;
|
||||
}
|
||||
static size({$, concrete}) {
|
||||
return $.staticSizeOf(concrete);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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', () => {
|
||||
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())
|
||||
.to.deep.equal(value);
|
||||
};
|
||||
|
@ -18,13 +14,17 @@ test('defaults values', () => {
|
|||
'int16',
|
||||
'uint32',
|
||||
'int32',
|
||||
'uint64',
|
||||
'int64',
|
||||
'float32',
|
||||
'float64',
|
||||
].forEach((type) => {
|
||||
compare({type}, 0);
|
||||
});
|
||||
[
|
||||
'uint64',
|
||||
'int64',
|
||||
].forEach((type) => {
|
||||
compare({type}, 0n);
|
||||
});
|
||||
compare({type: 'string'}, '');
|
||||
compare({type: 'array', subtype: {type: 'string'}}, []);
|
||||
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'}}}, {foo: 64}],
|
||||
];
|
||||
entries.forEach(([specification, concrete]) => {
|
||||
entries.forEach(([specification, instance]) => {
|
||||
const schema = new Schema(specification);
|
||||
const view = new DataView(new ArrayBuffer(schema.sizeOf(concrete)));
|
||||
schema.serialize(concrete, view);
|
||||
expect(concrete)
|
||||
const view = new DataView(new ArrayBuffer(schema.sizeOf(instance)));
|
||||
schema.serialize(instance, view);
|
||||
expect(instance)
|
||||
.to.deep.equal(schema.deserialize(view));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -39,32 +39,31 @@ export default class Colliders extends System {
|
|||
}
|
||||
|
||||
tick() {
|
||||
const seen = {};
|
||||
const collisions = new Map();
|
||||
for (const entity of this.ecs.changed(['Position'])) {
|
||||
if (seen[entity.id]) {
|
||||
continue;
|
||||
this.updateHash(entity);
|
||||
}
|
||||
seen[entity.id] = true;
|
||||
for (const entity of this.ecs.changed(['Position'])) {
|
||||
if (!entity.Collider) {
|
||||
continue;
|
||||
}
|
||||
const {collidingWith: wasCollidingWith} = entity.Collider;
|
||||
entity.Collider.collidingWith = {};
|
||||
this.updateHash(entity);
|
||||
collisions.set(entity, new Set());
|
||||
for (const other of this.within(entity.Collider.aabb)) {
|
||||
if (seen[other.id]) {
|
||||
if (entity === other) {
|
||||
continue;
|
||||
}
|
||||
seen[other.id] = true;
|
||||
if (!other.Collider) {
|
||||
if (!collisions.has(other)) {
|
||||
collisions.set(other, new Set());
|
||||
}
|
||||
if (!other.Collider || collisions.get(other).has(entity)) {
|
||||
continue;
|
||||
}
|
||||
delete other.Collider.collidingWith[entity.id];
|
||||
const intersections = entity.Collider.isCollidingWith(other.Collider);
|
||||
if (intersections.length > 0) {
|
||||
collisions.get(entity).add(other);
|
||||
if (!entity.Collider.collidingWith[other.id]) {
|
||||
entity.Collider.collidingWith[other.id] = true;
|
||||
other.Collider.collidingWith[entity.id] = true;
|
||||
if (!wasCollidingWith[other.id]) {
|
||||
if (entity.Collider.collisionStartScriptInstance) {
|
||||
const script = entity.Collider.collisionStartScriptInstance.clone();
|
||||
script.context.intersections = intersections;
|
||||
|
@ -117,13 +116,8 @@ export default class Colliders extends System {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const otherId in wasCollidingWith) {
|
||||
if (!entity.Collider.collidingWith[otherId]) {
|
||||
const other = this.ecs.get(otherId);
|
||||
if (!other || !other.Collider) {
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
if (entity.Collider.collidingWith[other.id]) {
|
||||
if (entity.Collider.collisionEndScriptInstance) {
|
||||
const script = entity.Collider.collisionEndScriptInstance.clone();
|
||||
script.context.other = other;
|
||||
|
@ -134,6 +128,9 @@ export default class Colliders extends System {
|
|||
script.context.other = entity;
|
||||
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 {distance} from '@/util/math.js';
|
||||
|
||||
export default class Interactions extends System {
|
||||
|
||||
|
@ -11,25 +10,16 @@ export default class Interactions extends System {
|
|||
|
||||
tick() {
|
||||
for (const entity of this.select('default')) {
|
||||
const {Interacts} = entity;
|
||||
let willInteract = false;
|
||||
const entities = Array.from(this.ecs.system('Colliders').within(Interacts.aabb()))
|
||||
.filter((other) => other !== entity)
|
||||
.sort(({Position: l}, {Position: r}) => {
|
||||
return distance(entity.Position, l) > distance(entity.Position, r) ? -1 : 1;
|
||||
});
|
||||
const {Collider, Interacts} = entity;
|
||||
let willInteractWith = 0;
|
||||
const entities = Collider.closest(Interacts.aabb());
|
||||
for (const other of entities) {
|
||||
if (other === entity) {
|
||||
continue;
|
||||
}
|
||||
if (other.Interactive && other.Interactive.interacting) {
|
||||
Interacts.willInteractWith = other.id;
|
||||
willInteract = true;
|
||||
if (other.Interactive?.interacting) {
|
||||
willInteractWith = other.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!willInteract) {
|
||||
Interacts.willInteractWith = 0;
|
||||
}
|
||||
Interacts.willInteractWith = willInteractWith;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,9 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
super.reindex(entities);
|
||||
for (const id of entities) {
|
||||
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 {
|
||||
constructor(specification) {
|
||||
super(specification);
|
||||
[
|
||||
'Colliders',
|
||||
].forEach((defaultSystem) => {
|
||||
const System = this.system(defaultSystem);
|
||||
if (System) {
|
||||
System.active = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
async readAsset(uri) {
|
||||
if (!cache.has(uri)) {
|
||||
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 {render} from '@/util/dialogue.js';
|
||||
|
||||
import DialogueCaret from './dialogue-caret.jsx';
|
||||
import styles from './dialogue.module.css';
|
||||
|
||||
const CARET_SIZE = 12;
|
||||
|
||||
export default function Dialogue({
|
||||
camera,
|
||||
dialogue,
|
||||
|
@ -75,66 +74,31 @@ export default function Dialogue({
|
|||
() => render(dialogue.letters, styles.letter),
|
||||
[dialogue.letters],
|
||||
);
|
||||
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(
|
||||
dialogue.position.x * scale - camera.x,
|
||||
position.x * scale - camera.x,
|
||||
RESOLUTION.x - bounds.x * scale - 16,
|
||||
),
|
||||
bounds.x * scale + 16,
|
||||
);
|
||||
const top = Math.max(
|
||||
Math.min(
|
||||
dialogue.position.y * scale - camera.y,
|
||||
position.y * scale - camera.y,
|
||||
RESOLUTION.y - bounds.y * scale - 16,
|
||||
),
|
||||
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 (
|
||||
<div
|
||||
className={styles.dialogue}
|
||||
|
@ -144,23 +108,13 @@ export default function Dialogue({
|
|||
top: `${top}px`,
|
||||
}}
|
||||
>
|
||||
<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="0 0, 24 0, 12 24" />
|
||||
</svg>
|
||||
<DialogueCaret
|
||||
camera={camera}
|
||||
dialogue={dialogue}
|
||||
dimensions={dimensions}
|
||||
scale={scale}
|
||||
|
||||
/>
|
||||
<p className={styles.letters}>
|
||||
{localRender(caret, radians)}
|
||||
</p>
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
.caret {
|
||||
position: absolute;
|
||||
fill: #00000044;
|
||||
stroke: white;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.dialogue {
|
||||
background-color: #00000044;
|
||||
border: solid 1px white;
|
||||
background-color: #02023999;
|
||||
border: solid 3px white;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
overflow-wrap: break-word;
|
||||
padding: 1em;
|
||||
padding: 12px;
|
||||
position: fixed;
|
||||
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%);
|
||||
user-select: none;
|
||||
max-width: 33%;
|
||||
max-width: 66%;
|
||||
}
|
||||
|
||||
.letters {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.dialogues {
|
||||
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||
font-size: 16px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
|
|
@ -38,6 +38,15 @@ export default function Entities({
|
|||
const {dialogues} = updating[id].Interlocutor;
|
||||
for (const key in dialogue) {
|
||||
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);
|
||||
setChatMessages((chatMessages) => ({
|
||||
[[id, key].join('-')]: dialogues[key].letters,
|
||||
|
|
|
@ -1,35 +1,19 @@
|
|||
import {RenderTexture} from '@pixi/core';
|
||||
import {Container} from '@pixi/display';
|
||||
import {Graphics} from '@pixi/graphics';
|
||||
import {PixiComponent, useApp} from '@pixi/react';
|
||||
import {Sprite} from '@pixi/sprite';
|
||||
import '@pixi/spritesheet'; // NECESSARY!
|
||||
import {CompositeTilemap} from '@pixi/tilemap';
|
||||
|
||||
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';
|
||||
|
||||
const TileLayerInternal = PixiComponent('TileLayer', {
|
||||
create: ({group, tileLayer}) => {
|
||||
create: () => {
|
||||
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;
|
||||
},
|
||||
applyProps: (container, {tileLayer: oldTileLayer}, props) => {
|
||||
|
@ -39,9 +23,67 @@ const TileLayerInternal = PixiComponent('TileLayer', {
|
|||
if (tileLayer === oldTileLayer) {
|
||||
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) {
|
||||
if (!oldTileLayer || oldTileLayer.$$chunks[i] !== tileLayer.$$chunks[i]) {
|
||||
const {texture, tilemap} = container.children[i];
|
||||
const {texture, tilemap} = container.children[parseInt(i) + 1];
|
||||
tilemap.clear();
|
||||
const ax = Math.ceil(tileLayer.area.x / CHUNK_SIZE);
|
||||
const cy = Math.floor(i / ax);
|
||||
|
|
|
@ -98,6 +98,25 @@ export default function PlaySpecific() {
|
|||
client.removePacketListener('ConnectionStatus', onConnectionStatus);
|
||||
};
|
||||
}, [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(() => {
|
||||
if (!client || !disconnected) {
|
||||
return;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import data from '../../../public/assets/dev/homestead.json';
|
||||
|
||||
export default async function createHomestead(id) {
|
||||
const area = {x: 100, y: 60};
|
||||
const entities = [];
|
||||
|
@ -8,7 +10,7 @@ export default async function createHomestead(id) {
|
|||
layers: [
|
||||
{
|
||||
area,
|
||||
data: Array(area.x * area.y).fill(0).map(() => 1 + Math.floor(Math.random() * 4)),
|
||||
data,
|
||||
source: '/assets/tileset.json',
|
||||
tileSize: {x: 16, y: 16},
|
||||
},
|
||||
|
@ -79,8 +81,9 @@ export default async function createHomestead(id) {
|
|||
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,
|
||||
origin: subject.Position.toJSON(),
|
||||
position: {x: subject.Position.x, y: subject.Position.y - 32},
|
||||
offset: {x: 0, y: -48},
|
||||
origin: 'track',
|
||||
position: 'track',
|
||||
})
|
||||
`,
|
||||
},
|
||||
|
@ -115,6 +118,27 @@ export default async function createHomestead(id) {
|
|||
Controlled: {},
|
||||
Direction: {direction: 2},
|
||||
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},
|
||||
Speed: {speed: 20},
|
||||
Sprite: {
|
||||
|
@ -123,11 +147,41 @@ export default async function createHomestead(id) {
|
|||
source: '/assets/kitty/kitty.json',
|
||||
speed: 0.115,
|
||||
},
|
||||
Tags: {tags: ['kittan']},
|
||||
Ticking: {},
|
||||
VisibleAabb: {},
|
||||
};
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
for (let i = 0; i < 30; ++i) {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -38,13 +38,17 @@ export default async function createPlayer(id) {
|
|||
qty: 1,
|
||||
source: '/assets/hoe/hoe.json',
|
||||
},
|
||||
5: {
|
||||
qty: 1,
|
||||
source: '/assets/brush/brush.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
Health: {health: 100},
|
||||
Light: {},
|
||||
Magnet: {strength: 24},
|
||||
Player: {},
|
||||
Position: {x: 128, y: 128},
|
||||
Position: {x: 128, y: 448},
|
||||
Speed: {speed: 100},
|
||||
Sound: {},
|
||||
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 createHouse from './create/house.js';
|
||||
import createPlayer from './create/player.js';
|
||||
import createTown from './create/town.js';
|
||||
|
||||
const cache = new LRUCache({
|
||||
max: 128,
|
||||
|
@ -125,22 +126,47 @@ export default class Engine {
|
|||
Interacts,
|
||||
Interlocutor,
|
||||
Inventory,
|
||||
Position,
|
||||
Wielder,
|
||||
} = entity;
|
||||
const ecs = this.ecses[Ecs.path];
|
||||
for (const payload of payloads) {
|
||||
switch (payload.type) {
|
||||
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({
|
||||
body: payload.value,
|
||||
linger: 5,
|
||||
origin: Position.toJSON(),
|
||||
position: {x: Position.x, y: Position.y - 32},
|
||||
offset: {x: 0, y: -40},
|
||||
origin: 'track',
|
||||
position: 'track',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'paint': {
|
||||
const ecs = this.ecses[Ecs.path];
|
||||
const {TileLayers} = ecs.get(1);
|
||||
const {brush, layer: paintLayer, stamp} = payload.value;
|
||||
const layer = TileLayers.layer(paintLayer);
|
||||
|
@ -181,7 +207,6 @@ export default class Engine {
|
|||
if (!Controlled.locked) {
|
||||
if (payload.value) {
|
||||
if (Interacts.willInteractWith) {
|
||||
const ecs = this.ecses[Ecs.path];
|
||||
const subject = ecs.get(Interacts.willInteractWith);
|
||||
subject.Interactive.interact(entity);
|
||||
}
|
||||
|
@ -233,6 +258,25 @@ export default class Engine {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import createEcs from './create/ecs.js';
|
|||
import './create/forest.js';
|
||||
import './create/homestead.js';
|
||||
import './create/player.js';
|
||||
import './create/town.js';
|
||||
|
||||
import Engine from './engine.js';
|
||||
|
||||
|
@ -111,6 +112,19 @@ if (import.meta.hot) {
|
|||
await engine.saveEcs('homesteads/0', homestead);
|
||||
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 () => {
|
||||
await Promise.all(promises);
|
||||
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export const CHUNK_SIZE = 32;
|
||||
|
||||
export const CLIENT_LATENCY = 100;
|
||||
export const CLIENT_LATENCY = 0;
|
||||
|
||||
export const CLIENT_PREDICTION = true;
|
||||
|
||||
|
@ -11,6 +11,6 @@ export const RESOLUTION = {
|
|||
y: 450,
|
||||
};
|
||||
|
||||
export const SERVER_LATENCY = 100;
|
||||
export const SERVER_LATENCY = 0;
|
||||
|
||||
export const TPS = 60;
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import {createElement} from 'react';
|
||||
import mdx from 'remark-mdx';
|
||||
import parse from 'remark-parse';
|
||||
import {createNoise2D} from 'simplex-noise';
|
||||
import {unified} from 'unified';
|
||||
import {visitParents as visit} from 'unist-util-visit-parents';
|
||||
|
||||
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);
|
||||
|
||||
function computeParams(ancestors) {
|
||||
|
@ -96,8 +100,9 @@ export function render(letters, className) {
|
|||
}
|
||||
if (params.shake) {
|
||||
const {magnitude = 1} = params.shake;
|
||||
left += (Math.random() * magnitude * 2) - magnitude;
|
||||
top += (Math.random() * magnitude * 2) - magnitude;
|
||||
const r = radians + TAU * indices.shake / params.shake.length;
|
||||
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) {
|
||||
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 = {}) {
|
||||
const {
|
||||
mapperForPath = (path) => path.replace(/\.\/(.*)\.js/, '$1'),
|
||||
} = options;
|
||||
const Gathered = {};
|
||||
for (const [path, Component] of Object.entries(imports).sort(([l], [r]) => l < r ? -1 : 1)) {
|
||||
Gathered[
|
||||
mapperForPath(path)
|
||||
pathToKey = (path) => (
|
||||
path.replace(/\.\/(.*)\.js/, '$1')
|
||||
.split('-')
|
||||
.map(capitalize)
|
||||
.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;
|
||||
}
|
||||
|
|
|
@ -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