Compare commits

..

19 Commits

Author SHA1 Message Date
cha0s
f971295825 dev: town 2024-07-23 17:04:17 -05:00
cha0s
5a92be47c1 fix: index only active 2024-07-23 17:03:53 -05:00
cha0s
c27ab133a9 fix: ECS size change 2024-07-23 17:03:12 -05:00
cha0s
df0e012338 dev: tile dump 2024-07-23 15:06:32 -05:00
cha0s
b04392756b fix: destruction order 2024-07-23 15:05:55 -05:00
cha0s
aacfd6271f refactor: dialogue caret 2024-07-23 12:53:12 -05:00
cha0s
9f0c3f3c07 fix: collisions 2024-07-23 11:46:52 -05:00
cha0s
88f0ec4715 refactor: schema 2024-07-23 10:10:32 -05:00
cha0s
263cf37e27 refactor: gather 2024-07-23 10:10:03 -05:00
cha0s
b9e3ac433b refactor: do better at some point 2024-07-22 18:43:01 -05:00
cha0s
735144df55 fun: dialogue tweaks 2024-07-22 07:55:05 -05:00
cha0s
1ee30434f3 refactor: dialogue position and offset 2024-07-22 04:19:29 -05:00
cha0s
e13d33fbbf fun: kittanspeak 2024-07-22 03:55:49 -05:00
cha0s
2d2adbbfd6 feat: dialogue origin tracking 2024-07-22 03:55:37 -05:00
cha0s
b95a2e2bb9 fun: furballs 2024-07-22 03:18:02 -05:00
cha0s
947e2cf380 refactor: Collider.closest 2024-07-22 03:17:43 -05:00
cha0s
73c6d991a7 fix: outer frame 2024-07-22 03:17:02 -05:00
cha0s
02acecfb5c fix: whoops 2024-07-22 02:35:57 -05:00
cha0s
f7e4bd0e36 fix: adjust interactivity bounds 2024-07-22 01:43:05 -05:00
53 changed files with 969 additions and 499 deletions

View File

@ -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,13 +48,15 @@ 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) {
instance[j] = defaultValue;
else {
const defaultValue = Schema.defaultValue(properties[j]);
if ('undefined' !== typeof defaultValue) {
instance[j] = defaultValue;
}
}
}
promises.push(this.load(this.data[allocated[i]]));
@ -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}`];

View File

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

View File

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

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@ -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);
}
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;
}
deserialize(view, offset = 0) {
return this.constructor.deserialize(view, {value: offset}, this.specification);
}
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);
}

View File

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

View File

@ -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'])) {
this.updateHash(entity);
}
for (const entity of this.ecs.changed(['Position'])) {
if (seen[entity.id]) {
continue;
}
seen[entity.id] = true;
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) {
entity.Collider.collidingWith[other.id] = true;
other.Collider.collidingWith[entity.id] = true;
if (!wasCollidingWith[other.id]) {
collisions.get(entity).add(other);
if (!entity.Collider.collidingWith[other.id]) {
entity.Collider.collidingWith[other.id] = true;
other.Collider.collidingWith[entity.id] = true;
if (entity.Collider.collisionStartScriptInstance) {
const script = entity.Collider.collisionStartScriptInstance.clone();
script.context.intersections = intersections;
@ -117,22 +116,20 @@ 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;
}
if (entity.Collider.collisionEndScriptInstance) {
const script = entity.Collider.collisionEndScriptInstance.clone();
script.context.other = other;
entity.Ticking.add(script.ticker());
}
if (other.Collider.collisionEndScriptInstance) {
const script = other.Collider.collisionEndScriptInstance.clone();
script.context.other = entity;
other.Ticking.add(script.ticker());
else {
if (entity.Collider.collidingWith[other.id]) {
if (entity.Collider.collisionEndScriptInstance) {
const script = entity.Collider.collisionEndScriptInstance.clone();
script.context.other = other;
entity.Ticking.add(script.ticker());
}
if (other.Collider.collisionEndScriptInstance) {
const script = other.Collider.collisionEndScriptInstance.clone();
script.context.other = entity;
other.Ticking.add(script.ticker());
}
delete entity.Collider.collidingWith[other.id];
delete other.Collider.collidingWith[entity.id];
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import Packet from '@/net/packet.js';
export default class Download extends Packet {}

View File

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

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

View File

@ -0,0 +1,8 @@
.caret {
position: absolute;
fill: #ffffff;
stroke: #00000044;
stroke-width: 2px;
left: 50%;
top: 50%;
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
.dialogues {
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
font-size: 16px;
font-size: 22px;
}
@font-face {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
export default class First {
static gathered(id, key) {
this.id = id;
this.key = key;
}
}

View File

@ -1,6 +0,0 @@
export default class Second {
static gathered(id, key) {
this.id = id;
this.key = key;
}
}

View File

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

View File

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

View File

@ -0,0 +1,4 @@
{
"icon": "/assets/brush/brush.png",
"start": "/assets/brush/start.js"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
{
"icon": "/assets/furball/furball.png"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB