Compare commits
10 Commits
6ff824ace6
...
f3fe70410f
Author | SHA1 | Date | |
---|---|---|---|
|
f3fe70410f | ||
|
8122018222 | ||
|
df61a43ab5 | ||
|
cd875f8025 | ||
|
64ece0cb86 | ||
|
1ee8f206de | ||
|
0372b0ddf4 | ||
|
667eaded8e | ||
|
5429913587 | ||
|
7a83e1bc7a |
|
@ -13,7 +13,13 @@ export default async function createHomestead(Ecs) {
|
||||||
data: Array(area.x * area.y).fill(0).map(() => 1 + Math.floor(Math.random() * 4)),
|
data: Array(area.x * area.y).fill(0).map(() => 1 + Math.floor(Math.random() * 4)),
|
||||||
source: '/assets/tileset.json',
|
source: '/assets/tileset.json',
|
||||||
tileSize: {x: 16, y: 16},
|
tileSize: {x: 16, y: 16},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
area,
|
||||||
|
data: Array(area.x * area.y).fill(0),
|
||||||
|
source: '/assets/tileset.json',
|
||||||
|
tileSize: {x: 16, y: 16},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
Time: {},
|
Time: {},
|
||||||
|
@ -35,9 +41,9 @@ export default async function createHomestead(Ecs) {
|
||||||
impassable: 1,
|
impassable: 1,
|
||||||
points: [
|
points: [
|
||||||
{x: -52, y: -16},
|
{x: -52, y: -16},
|
||||||
{x: 48, y: -16},
|
{x: 44, y: -16},
|
||||||
{x: -52, y: 15},
|
{x: -52, y: 15},
|
||||||
{x: 48, y: 15},
|
{x: 44, y: 15},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -8,6 +8,28 @@ export default class Collider extends Component {
|
||||||
const {ecs} = this;
|
const {ecs} = this;
|
||||||
return class ColliderInstance extends super.instanceFromSchema() {
|
return class ColliderInstance extends super.instanceFromSchema() {
|
||||||
collidingWith = {};
|
collidingWith = {};
|
||||||
|
get aabb() {
|
||||||
|
const {Position: {x: px, y: py}} = ecs.get(this.entity);
|
||||||
|
return {
|
||||||
|
x0: this.$$aabb.x0 + px,
|
||||||
|
x1: this.$$aabb.x1 + px,
|
||||||
|
y0: this.$$aabb.y0 + py,
|
||||||
|
y1: this.$$aabb.y1 + py,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
get aabbs() {
|
||||||
|
const {Position: {x: px, y: py}} = ecs.get(this.entity);
|
||||||
|
const aabbs = [];
|
||||||
|
for (const aabb of this.$$aabbs) {
|
||||||
|
aabbs.push({
|
||||||
|
x0: aabb.x0 + px,
|
||||||
|
x1: aabb.x1 + px,
|
||||||
|
y0: aabb.y0 + py,
|
||||||
|
y1: aabb.y1 + py,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return aabbs;
|
||||||
|
}
|
||||||
isCollidingWith(other) {
|
isCollidingWith(other) {
|
||||||
const {aabb, aabbs} = this;
|
const {aabb, aabbs} = this;
|
||||||
const {aabb: otherAabb, aabbs: otherAabbs} = other;
|
const {aabb: otherAabb, aabbs: otherAabbs} = other;
|
||||||
|
@ -38,36 +60,34 @@ export default class Collider extends Component {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
recalculateAabbs() {
|
|
||||||
const {Position: {x: px, y: py}} = ecs.get(this.entity);
|
|
||||||
this.aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
|
||||||
this.aabbs = [];
|
|
||||||
const {bodies} = this;
|
|
||||||
for (const body of bodies) {
|
|
||||||
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
||||||
for (const point of body.points) {
|
|
||||||
const x = point.x + px;
|
|
||||||
const y = point.y + py;
|
|
||||||
if (x < x0) x0 = x;
|
|
||||||
if (x < this.aabb.x0) this.aabb.x0 = x;
|
|
||||||
if (x > x1) x1 = x;
|
|
||||||
if (x > this.aabb.x1) this.aabb.x1 = x;
|
|
||||||
if (y < y0) y0 = y;
|
|
||||||
if (y < this.aabb.y0) this.aabb.y0 = y;
|
|
||||||
if (y > y1) y1 = y;
|
|
||||||
if (y > this.aabb.y1) this.aabb.y1 = y;
|
|
||||||
}
|
|
||||||
this.aabbs.push({
|
|
||||||
x0: x0 > x1 ? x1 : x0,
|
|
||||||
x1: x0 > x1 ? x0 : x1,
|
|
||||||
y0: y0 > y1 ? y1 : y0,
|
|
||||||
y1: y0 > y1 ? y0 : y1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async load(instance) {
|
async load(instance) {
|
||||||
|
instance.$$aabb = {x0: Infinity, x1: -Infinity, y0: Infinity, y1: -Infinity};
|
||||||
|
instance.$$aabbs = [];
|
||||||
|
const {bodies} = instance;
|
||||||
|
for (const body of bodies) {
|
||||||
|
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
||||||
|
for (const point of body.points) {
|
||||||
|
const x = point.x;
|
||||||
|
const y = point.y;
|
||||||
|
if (x < x0) x0 = x;
|
||||||
|
if (x < instance.$$aabb.x0) instance.$$aabb.x0 = x;
|
||||||
|
if (x > x1) x1 = x;
|
||||||
|
if (x > instance.$$aabb.x1) instance.$$aabb.x1 = x;
|
||||||
|
if (y < y0) y0 = y;
|
||||||
|
if (y < instance.$$aabb.y0) instance.$$aabb.y0 = y;
|
||||||
|
if (y > y1) y1 = y;
|
||||||
|
if (y > instance.$$aabb.y1) instance.$$aabb.y1 = y;
|
||||||
|
}
|
||||||
|
instance.$$aabbs.push({
|
||||||
|
x0: x0 > x1 ? x1 : x0,
|
||||||
|
x1: x0 > x1 ? x0 : x1,
|
||||||
|
y0: y0 > y1 ? y1 : y0,
|
||||||
|
y1: y0 > y1 ? y0 : y1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// heavy handed...
|
// heavy handed...
|
||||||
if ('undefined' !== typeof window) {
|
if ('undefined' !== typeof window) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import gather from '@/util/gather.js';
|
import gather from '@/util/gather.js';
|
||||||
|
|
||||||
const Gathered = gather(
|
const Gathered = gather(
|
||||||
import.meta.glob('./*.js', {eager: true, import: 'default'}),
|
import.meta.glob(['./*.js', '!./*.test.js'], {eager: true, import: 'default'}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const Components = {};
|
const Components = {};
|
||||||
|
|
|
@ -81,7 +81,6 @@ class ItemProxy {
|
||||||
}
|
}
|
||||||
if (this.scripts.projectionCheckInstance) {
|
if (this.scripts.projectionCheckInstance) {
|
||||||
this.scripts.projectionCheckInstance.context.ecs = this.Component.ecs;
|
this.scripts.projectionCheckInstance.context.ecs = this.Component.ecs;
|
||||||
this.scripts.projectionCheckInstance.context.layer = layer;
|
|
||||||
this.scripts.projectionCheckInstance.context.projected = projected;
|
this.scripts.projectionCheckInstance.context.projected = projected;
|
||||||
return this.scripts.projectionCheckInstance.evaluateSync();
|
return this.scripts.projectionCheckInstance.evaluateSync();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,116 @@
|
||||||
import Component from '@/ecs/component.js';
|
import Component from '@/ecs/component.js';
|
||||||
|
import {floodwalk2D, ortho, removeCollinear} from '@/util/math.js';
|
||||||
|
|
||||||
import vector2d from './helpers/vector-2d';
|
import vector2d from './helpers/vector-2d';
|
||||||
|
|
||||||
|
class LayerProxy {
|
||||||
|
constructor(instance, Component, index) {
|
||||||
|
this.instance = instance;
|
||||||
|
this.Component = Component;
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
get area() {
|
||||||
|
return this.layer.area;
|
||||||
|
}
|
||||||
|
get data() {
|
||||||
|
return this.layer.data;
|
||||||
|
}
|
||||||
|
get hulls() {
|
||||||
|
const {data, area, tileSize} = this;
|
||||||
|
const hulls = [];
|
||||||
|
const seen = {};
|
||||||
|
const n = data.length;
|
||||||
|
const {x: w, y: h} = area;
|
||||||
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
|
for (let i = 0; i < n; ++i) {
|
||||||
|
if (data[i]) {
|
||||||
|
if (!seen[i]) {
|
||||||
|
const indices = floodwalk2D(new Set([data[i]]), data, {x, y, w, h});
|
||||||
|
if (indices.size > 0) {
|
||||||
|
const pointHash = Object.create(null);
|
||||||
|
const points = [];
|
||||||
|
const seePoint = ({x, y}) => {
|
||||||
|
if (pointHash[y]?.[x]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!pointHash[y]) {
|
||||||
|
pointHash[y] = Object.create({});
|
||||||
|
}
|
||||||
|
return pointHash[y][x] = true;
|
||||||
|
};
|
||||||
|
for (const index of indices) {
|
||||||
|
seen[index] = true;
|
||||||
|
const op = {
|
||||||
|
x: tileSize.x * (index % area.x),
|
||||||
|
y: tileSize.y * (Math.floor(index / area.x)),
|
||||||
|
};
|
||||||
|
let p;
|
||||||
|
const tsq = {x: tileSize.x / 4, y: tileSize.y / 4};
|
||||||
|
p = {x: op.x + tsq.x, y: op.y + tsq.y};
|
||||||
|
if (seePoint(p)) {
|
||||||
|
points.push(p);
|
||||||
|
}
|
||||||
|
p = {x: op.x + tileSize.x - tsq.x, y: op.y + tsq.y};
|
||||||
|
if (seePoint(p)) {
|
||||||
|
points.push(p);
|
||||||
|
}
|
||||||
|
p = {x: op.x + tileSize.x - tsq.x, y: op.y + tileSize.y - tsq.y};
|
||||||
|
if (seePoint(p)) {
|
||||||
|
points.push(p);
|
||||||
|
}
|
||||||
|
p = {x: op.x + tsq.x, y: op.y + tileSize.y - tsq.y};
|
||||||
|
if (seePoint(p)) {
|
||||||
|
points.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hulls.push(removeCollinear(ortho(points, {x: tileSize.x / 2, y: tileSize.y / 2})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x += 1;
|
||||||
|
if (x === w) {
|
||||||
|
x -= w;
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hulls;
|
||||||
|
}
|
||||||
|
get layer() {
|
||||||
|
return this.instance.layers[this.index];
|
||||||
|
}
|
||||||
|
get source() {
|
||||||
|
return this.layer.source;
|
||||||
|
}
|
||||||
|
stamp(at, data) {
|
||||||
|
const changes = {};
|
||||||
|
for (const row in data) {
|
||||||
|
const columns = data[row];
|
||||||
|
for (const column in columns) {
|
||||||
|
const tile = columns[column];
|
||||||
|
const x = at.x + parseInt(column);
|
||||||
|
const y = at.y + parseInt(row);
|
||||||
|
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const calculated = y * this.layer.area.x + x;
|
||||||
|
this.layer.data[calculated] = tile;
|
||||||
|
changes[calculated] = tile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.Component.markChange(this.instance.entity, 'layerChange', {[this.index]: changes});
|
||||||
|
}
|
||||||
|
tile({x, y}) {
|
||||||
|
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.layer.data[y * this.layer.area.x + x];
|
||||||
|
}
|
||||||
|
get tileSize() {
|
||||||
|
return this.layer.tileSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class TileLayers extends Component {
|
export default class TileLayers extends Component {
|
||||||
insertMany(entities) {
|
insertMany(entities) {
|
||||||
for (const [id, {layerChange}] of entities) {
|
for (const [id, {layerChange}] of entities) {
|
||||||
|
@ -14,11 +123,17 @@ export default class TileLayers extends Component {
|
||||||
layers[layerIndex].data[calculated] = tile;
|
layers[layerIndex].data[calculated] = tile;
|
||||||
}
|
}
|
||||||
layers[layerIndex] = {...layers[layerIndex]};
|
layers[layerIndex] = {...layers[layerIndex]};
|
||||||
|
component.$$layersProxies[layerIndex] = new LayerProxy(component, this, layerIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.insertMany(entities);
|
return super.insertMany(entities);
|
||||||
}
|
}
|
||||||
|
load(instance) {
|
||||||
|
for (const index in instance.layers) {
|
||||||
|
instance.$$layersProxies[index] = new LayerProxy(instance, this, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
mergeDiff(original, update) {
|
mergeDiff(original, update) {
|
||||||
if (!update.layerChange) {
|
if (!update.layerChange) {
|
||||||
return super.mergeDiff(original, update);
|
return super.mergeDiff(original, update);
|
||||||
|
@ -35,54 +150,10 @@ export default class TileLayers extends Component {
|
||||||
return {layerChange};
|
return {layerChange};
|
||||||
}
|
}
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
const Instance = super.instanceFromSchema();
|
return class TileLayersInstance extends super.instanceFromSchema() {
|
||||||
const Component = this;
|
$$layersProxies = {};
|
||||||
return class TileLayersInstance extends Instance {
|
|
||||||
layer(index) {
|
layer(index) {
|
||||||
const {layers} = this;
|
return this.$$layersProxies[index];
|
||||||
if (!(index in layers)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const instance = this;
|
|
||||||
class LayerProxy {
|
|
||||||
constructor(layer) {
|
|
||||||
this.layer = layer;
|
|
||||||
}
|
|
||||||
get area() {
|
|
||||||
return this.layer.area;
|
|
||||||
}
|
|
||||||
get source() {
|
|
||||||
return this.layer.source;
|
|
||||||
}
|
|
||||||
stamp(at, data) {
|
|
||||||
const changes = {};
|
|
||||||
for (const row in data) {
|
|
||||||
const columns = data[row];
|
|
||||||
for (const column in columns) {
|
|
||||||
const tile = columns[column];
|
|
||||||
const x = at.x + parseInt(column);
|
|
||||||
const y = at.y + parseInt(row);
|
|
||||||
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const calculated = y * this.layer.area.x + x;
|
|
||||||
this.layer.data[calculated] = tile;
|
|
||||||
changes[calculated] = tile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component.markChange(instance.entity, 'layerChange', {[index]: changes});
|
|
||||||
}
|
|
||||||
tile({x, y}) {
|
|
||||||
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return this.layer.data[y * this.layer.area.x + x];
|
|
||||||
}
|
|
||||||
get tileSize() {
|
|
||||||
return this.layer.tileSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new LayerProxy(layers[index]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
45
app/ecs-components/tile-layers.test.js
Normal file
45
app/ecs-components/tile-layers.test.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import {expect, test} from 'vitest';
|
||||||
|
|
||||||
|
import Ecs from '@/ecs/ecs.js';
|
||||||
|
|
||||||
|
import TileLayers from './tile-layers.js';
|
||||||
|
|
||||||
|
test('creates hulls', async () => {
|
||||||
|
const Component = new TileLayers(new Ecs());
|
||||||
|
const data = Array(64).fill(0);
|
||||||
|
data[9] = 1;
|
||||||
|
data[10] = 1;
|
||||||
|
data[17] = 1;
|
||||||
|
data[18] = 1;
|
||||||
|
const layers = await Component.create(1, {
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
area: {x: 8, y: 8},
|
||||||
|
data,
|
||||||
|
source: '',
|
||||||
|
tileSize: {x: 16, y: 16},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(layers.layer(0).hulls)
|
||||||
|
.to.deep.equal([
|
||||||
|
[
|
||||||
|
{x: 20, y: 20},
|
||||||
|
{x: 44, y: 20},
|
||||||
|
{x: 44, y: 44},
|
||||||
|
{x: 20, y: 44},
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
data[11] = 1;
|
||||||
|
expect(layers.layer(0).hulls)
|
||||||
|
.to.deep.equal([
|
||||||
|
[
|
||||||
|
{x: 20, y: 20},
|
||||||
|
{x: 60, y: 20},
|
||||||
|
{x: 60, y: 28},
|
||||||
|
{x: 44, y: 28},
|
||||||
|
{x: 44, y: 44},
|
||||||
|
{x: 20, y: 44},
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
});
|
|
@ -1,4 +1,5 @@
|
||||||
import {System} from '@/ecs/index.js';
|
import {System} from '@/ecs/index.js';
|
||||||
|
import {intersects} from '@/util/math.js';
|
||||||
import SpatialHash from '@/util/spatial-hash.js';
|
import SpatialHash from '@/util/spatial-hash.js';
|
||||||
|
|
||||||
export default class Colliders extends System {
|
export default class Colliders extends System {
|
||||||
|
@ -34,7 +35,6 @@ export default class Colliders extends System {
|
||||||
if (!entity.Collider) {
|
if (!entity.Collider) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
entity.Collider.recalculateAabbs();
|
|
||||||
this.hash.update(entity.Collider.aabb, entity.id);
|
this.hash.update(entity.Collider.aabb, entity.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,10 +79,40 @@ export default class Colliders extends System {
|
||||||
other.Ticking.addTickingPromise(script.tickingPromise());
|
other.Ticking.addTickingPromise(script.tickingPromise());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const [, {impassable}] of intersections) {
|
for (const i in intersections) {
|
||||||
|
const [body, otherBody] = intersections[i];
|
||||||
|
const {impassable} = otherBody;
|
||||||
if (impassable) {
|
if (impassable) {
|
||||||
entity.Position.x = entity.Position.lastX
|
const j = entity.Collider.bodies.indexOf(body);
|
||||||
entity.Position.y = entity.Position.lastY
|
const oj = other.Collider.bodies.indexOf(otherBody);
|
||||||
|
const aabb = entity.Collider.$$aabbs[j];
|
||||||
|
const otherAabb = other.Collider.aabbs[oj];
|
||||||
|
if (!intersects(
|
||||||
|
{
|
||||||
|
x0: aabb.x0 + entity.Position.lastX,
|
||||||
|
x1: aabb.x1 + entity.Position.lastX,
|
||||||
|
y0: aabb.y0 + entity.Position.y,
|
||||||
|
y1: aabb.y1 + entity.Position.y,
|
||||||
|
},
|
||||||
|
otherAabb,
|
||||||
|
)) {
|
||||||
|
entity.Position.x = entity.Position.lastX
|
||||||
|
}
|
||||||
|
else if (!intersects(
|
||||||
|
{
|
||||||
|
x0: aabb.x0 + entity.Position.x,
|
||||||
|
x1: aabb.x1 + entity.Position.x,
|
||||||
|
y0: aabb.y0 + entity.Position.lastY,
|
||||||
|
y1: aabb.y1 + entity.Position.lastY,
|
||||||
|
},
|
||||||
|
otherAabb,
|
||||||
|
)) {
|
||||||
|
entity.Position.y = entity.Position.lastY
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
entity.Position.x = entity.Position.lastX
|
||||||
|
entity.Position.y = entity.Position.lastY
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,34 +29,41 @@ export default class Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(entityId, values) {
|
async create(entityId, values) {
|
||||||
this.createMany([[entityId, values]]);
|
const [created] = await this.createMany([[entityId, values]]);
|
||||||
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createMany(entries) {
|
async createMany(entries) {
|
||||||
if (entries.length > 0) {
|
if (0 === entries.length) {
|
||||||
const allocated = this.allocateMany(entries.length);
|
return [];
|
||||||
const {properties} = this.constructor.schema.specification;
|
|
||||||
const keys = Object.keys(properties);
|
|
||||||
const promises = [];
|
|
||||||
for (let i = 0; i < entries.length; ++i) {
|
|
||||||
const [entityId, values = {}] = entries[i];
|
|
||||||
this.map[entityId] = allocated[i];
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
promises.push(this.load(this.data[allocated[i]]));
|
|
||||||
}
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
}
|
||||||
|
const allocated = this.allocateMany(entries.length);
|
||||||
|
const {properties} = this.constructor.schema.specification;
|
||||||
|
const keys = Object.keys(properties);
|
||||||
|
const promises = [];
|
||||||
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
|
const [entityId, values = {}] = entries[i];
|
||||||
|
this.map[entityId] = allocated[i];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
promises.push(this.load(this.data[allocated[i]]));
|
||||||
|
}
|
||||||
|
await Promise.all(promises);
|
||||||
|
const created = [];
|
||||||
|
for (let i = 0; i < allocated.length; ++i) {
|
||||||
|
created.push(this.data[allocated[i]]);
|
||||||
|
}
|
||||||
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
deserialize(entityId, view, offset) {
|
deserialize(entityId, view, offset) {
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
|
import {simplex2D} from '@leodeslf/simplex-noise';
|
||||||
|
import {Texture} from '@pixi/core';
|
||||||
import {Container} from '@pixi/react';
|
import {Container} from '@pixi/react';
|
||||||
|
import {Sprite} from '@pixi/sprite';
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
import {RESOLUTION} from '@/constants.js';
|
import {RESOLUTION} from '@/constants.js';
|
||||||
import {usePacket} from '@/context/client.js';
|
import {usePacket} from '@/context/client.js';
|
||||||
import {useEcs, useEcsTick} from '@/context/ecs.js';
|
import {useEcs, useEcsTick} from '@/context/ecs.js';
|
||||||
import {useMainEntity} from '@/context/main-entity.js';
|
import {useMainEntity} from '@/context/main-entity.js';
|
||||||
|
import {bresenham, TAU} from '@/util/math.js';
|
||||||
|
|
||||||
import Entities from './entities.jsx';
|
import Entities from './entities.jsx';
|
||||||
import TargetingGhost from './targeting-ghost.jsx';
|
import TargetingGhost from './targeting-ghost.jsx';
|
||||||
|
@ -28,12 +32,64 @@ function calculateDarkness(hour) {
|
||||||
return Math.floor(darkness * 1000) / 1000;
|
return Math.floor(darkness * 1000) / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createLayerMask(layer) {
|
||||||
|
const {area, hulls, tileSize} = layer;
|
||||||
|
const canvas = window.document.createElement('canvas');
|
||||||
|
if (0 === hulls.length) {
|
||||||
|
return Texture.from(canvas);
|
||||||
|
}
|
||||||
|
[canvas.width, canvas.height] = [area.x * tileSize.x, area.y * tileSize.y];
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.fillStyle = 'rgba(0, 0, 0, 1)';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
|
||||||
|
for (let i = 0; i < hulls.length; ++i) {
|
||||||
|
const {x, y} = hulls[i][0];
|
||||||
|
if (7 !== layer.tile({x: Math.floor(x / tileSize.x), y: Math.floor(y / tileSize.y)})) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const hull = [...hulls[i]];
|
||||||
|
hull.push(hull[0]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(hull[0].x, hull[0].y);
|
||||||
|
for (let j = 0; j < hull.length - 1; j++) {
|
||||||
|
const p0 = hull[j + 0];
|
||||||
|
const p1 = hull[j + 1];
|
||||||
|
const points = bresenham(p0, p1);
|
||||||
|
const isReversed = (
|
||||||
|
(p0.x > p1.x && points[0].x < points[points.length - 1].x)
|
||||||
|
|| (p0.y > p1.y && points[0].y < points[points.length - 1].y)
|
||||||
|
);
|
||||||
|
for (
|
||||||
|
let k = (isReversed ? points.length - 1 : 0);
|
||||||
|
(isReversed ? k >= 0 : k < points.length);
|
||||||
|
k += (isReversed ? -1 : 1)
|
||||||
|
) {
|
||||||
|
const {x, y} = points[k];
|
||||||
|
const ANGLE_SCALE = 1000;
|
||||||
|
const WALK_SCALE = 10;
|
||||||
|
const MAGNITUDE = ((tileSize.x + tileSize.y) / 2) * simplex2D(x * 100, y * 100);
|
||||||
|
const w = simplex2D(x * WALK_SCALE, y * WALK_SCALE);
|
||||||
|
const r = TAU * simplex2D(x * ANGLE_SCALE, y * ANGLE_SCALE)
|
||||||
|
ctx.lineTo(
|
||||||
|
x + Math.cos(r) * w * MAGNITUDE,
|
||||||
|
y + -Math.sin(r) * w * MAGNITUDE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
return Texture.from(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Ecs({scale}) {
|
export default function Ecs({scale}) {
|
||||||
const [ecs] = useEcs();
|
const [ecs] = useEcs();
|
||||||
const [entities, setEntities] = useState({});
|
const [entities, setEntities] = useState({});
|
||||||
const [mainEntity] = useMainEntity();
|
const [mainEntity] = useMainEntity();
|
||||||
const [hour, setHour] = useState(10);
|
const [hour, setHour] = useState(10);
|
||||||
const [night, setNight] = useState();
|
const [night, setNight] = useState();
|
||||||
|
const [mask, setMask] = useState();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function buildNightFilter() {
|
async function buildNightFilter() {
|
||||||
const {ColorMatrixFilter} = await import('@pixi/filter-color-matrix');
|
const {ColorMatrixFilter} = await import('@pixi/filter-color-matrix');
|
||||||
|
@ -79,6 +135,12 @@ export default function Ecs({scale}) {
|
||||||
if (update.Time) {
|
if (update.Time) {
|
||||||
setHour(Math.round(ecs.get(1).Time.hour * 60) / 60);
|
setHour(Math.round(ecs.get(1).Time.hour * 60) / 60);
|
||||||
}
|
}
|
||||||
|
if (update.TileLayers) {
|
||||||
|
const layer1 = ecs.get(1).TileLayers.layer(1);
|
||||||
|
if (layer1) {
|
||||||
|
setMask(new Sprite(createLayerMask(layer1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updatedEntities[id] = ecs.get(id);
|
updatedEntities[id] = ecs.get(id);
|
||||||
if (update.Emitter?.emit) {
|
if (update.Emitter?.emit) {
|
||||||
|
@ -101,7 +163,9 @@ export default function Ecs({scale}) {
|
||||||
const {Direction, Position, Wielder} = entity;
|
const {Direction, Position, Wielder} = entity;
|
||||||
const projected = Wielder.activeItem()?.project(Position.tile, Direction.direction)
|
const projected = Wielder.activeItem()?.project(Position.tile, Direction.direction)
|
||||||
const {Camera} = entity;
|
const {Camera} = entity;
|
||||||
const {TileLayers: {layers: [layer]}, Water: WaterEcs} = ecs.get(1);
|
const {TileLayers, Water: WaterEcs} = ecs.get(1);
|
||||||
|
const layer0 = TileLayers.layer(0);
|
||||||
|
const layer1 = TileLayers.layer(1);
|
||||||
const filters = [];
|
const filters = [];
|
||||||
if (night) {
|
if (night) {
|
||||||
filters.push(night);
|
filters.push(night);
|
||||||
|
@ -112,27 +176,47 @@ export default function Ecs({scale}) {
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
filters={filters}
|
|
||||||
scale={scale}
|
scale={scale}
|
||||||
x={-cx}
|
x={-cx}
|
||||||
y={-cy}
|
y={-cy}
|
||||||
>
|
>
|
||||||
<TileLayer tileLayer={layer} />
|
<Container
|
||||||
|
filters={filters}
|
||||||
|
>
|
||||||
|
<TileLayer
|
||||||
|
filters={filters}
|
||||||
|
tileLayer={layer0}
|
||||||
|
/>
|
||||||
|
{layer1 && (
|
||||||
|
<TileLayer
|
||||||
|
mask={mask}
|
||||||
|
filters={filters}
|
||||||
|
tileLayer={layer1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
{WaterEcs && (
|
{WaterEcs && (
|
||||||
<Water tileLayer={layer} water={WaterEcs.water} />
|
<Water
|
||||||
|
mask={mask}
|
||||||
|
tileLayer={layer0}
|
||||||
|
water={WaterEcs.water}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{projected && (
|
{projected && (
|
||||||
<TargetingGrid
|
<TargetingGrid
|
||||||
tileLayer={layer}
|
tileLayer={layer0}
|
||||||
x={Position.x}
|
x={Position.x}
|
||||||
y={Position.y}
|
y={Position.y}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Entities entities={entities} />
|
<Entities
|
||||||
|
entities={entities}
|
||||||
|
filters={filters}
|
||||||
|
/>
|
||||||
{projected?.length > 0 && (
|
{projected?.length > 0 && (
|
||||||
<TargetingGhost
|
<TargetingGhost
|
||||||
projected={projected}
|
projected={projected}
|
||||||
tileLayer={layer}
|
tileLayer={layer0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -8,15 +8,15 @@ import {useMainEntity} from '@/context/main-entity.js';
|
||||||
|
|
||||||
import Entity from './entity.jsx';
|
import Entity from './entity.jsx';
|
||||||
|
|
||||||
export default function Entities({entities}) {
|
export default function Entities({entities, filters}) {
|
||||||
const [ecs] = useEcs();
|
const [ecs] = useEcs();
|
||||||
const [mainEntity] = useMainEntity();
|
const [mainEntity] = useMainEntity();
|
||||||
const [radians, setRadians] = useState(0);
|
const [radians, setRadians] = useState(0);
|
||||||
const [willInteractWith, setWillInteractWith] = useState(0);
|
const [willInteractWith, setWillInteractWith] = useState(0);
|
||||||
const [filters] = useState([new AdjustmentFilter(), new GlowFilter({color: 0x0})]);
|
const [interactionFilters] = useState([new AdjustmentFilter(), new GlowFilter({color: 0x0})]);
|
||||||
const pulse = (Math.cos(radians) + 1) * 0.5;
|
const pulse = (Math.cos(radians) + 1) * 0.5;
|
||||||
filters[0].brightness = (pulse * 0.75) + 1;
|
interactionFilters[0].brightness = (pulse * 0.75) + 1;
|
||||||
filters[1].outerStrength = pulse * 0.5;
|
interactionFilters[1].outerStrength = pulse * 0.5;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRadians(0);
|
setRadians(0);
|
||||||
const handle = setInterval(() => {
|
const handle = setInterval(() => {
|
||||||
|
@ -40,7 +40,7 @@ export default function Entities({entities}) {
|
||||||
const isHighlightedInteraction = id == willInteractWith;
|
const isHighlightedInteraction = id == willInteractWith;
|
||||||
renderables.push(
|
renderables.push(
|
||||||
<Entity
|
<Entity
|
||||||
filters={isHighlightedInteraction ? filters : []}
|
filters={filters.concat(isHighlightedInteraction ? interactionFilters : [])}
|
||||||
entity={entities[id]}
|
entity={entities[id]}
|
||||||
key={id}
|
key={id}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -51,9 +51,6 @@ function Entity({entity, ...rest}) {
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (debug) {
|
|
||||||
entity.Collider?.recalculateAabbs();
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
zIndex={entity.Position?.y || 0}
|
zIndex={entity.Position?.y || 0}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import {Container} from '@pixi/display';
|
||||||
import {PixiComponent} from '@pixi/react';
|
import {PixiComponent} from '@pixi/react';
|
||||||
import '@pixi/spritesheet'; // NECESSARY!
|
import '@pixi/spritesheet'; // NECESSARY!
|
||||||
import {CompositeTilemap} from '@pixi/tilemap';
|
import {CompositeTilemap} from '@pixi/tilemap';
|
||||||
|
@ -5,14 +6,27 @@ import {CompositeTilemap} from '@pixi/tilemap';
|
||||||
import {useAsset} from '@/context/assets.js';
|
import {useAsset} from '@/context/assets.js';
|
||||||
|
|
||||||
const TileLayerInternal = PixiComponent('TileLayer', {
|
const TileLayerInternal = PixiComponent('TileLayer', {
|
||||||
create: () => new CompositeTilemap(),
|
create: () => {
|
||||||
applyProps: (tilemap, {tileLayer: oldTileLayer}, props) => {
|
const container = new Container();
|
||||||
const {asset, tileLayer} = props;
|
container.addChild(new CompositeTilemap());
|
||||||
|
return container;
|
||||||
|
},
|
||||||
|
applyProps: (container, {mask: oldMask, tileLayer: oldTileLayer}, props) => {
|
||||||
|
const {asset, mask, tileLayer} = props;
|
||||||
const extless = tileLayer.source.slice('/assets/'.length, -'.json'.length);
|
const extless = tileLayer.source.slice('/assets/'.length, -'.json'.length);
|
||||||
const {textures} = asset;
|
const {textures} = asset;
|
||||||
if (tileLayer === oldTileLayer) {
|
if (tileLayer === oldTileLayer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (oldMask) {
|
||||||
|
container.removeChildAt(1);
|
||||||
|
container.mask = undefined;
|
||||||
|
}
|
||||||
|
if (mask) {
|
||||||
|
container.addChild(mask);
|
||||||
|
container.mask = mask;
|
||||||
|
}
|
||||||
|
const tilemap = container.children[0];
|
||||||
tilemap.clear();
|
tilemap.clear();
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (let y = 0; y < tileLayer.area.y; ++y) {
|
for (let y = 0; y < tileLayer.area.y; ++y) {
|
||||||
|
@ -24,15 +38,18 @@ const TileLayerInternal = PixiComponent('TileLayer', {
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function TileLayer(props) {
|
export default function TileLayer(props) {
|
||||||
const {tileLayer} = props;
|
const {mask, tileLayer} = props;
|
||||||
const asset = useAsset(tileLayer.source);
|
const asset = useAsset(tileLayer.source);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TileLayerInternal
|
<>
|
||||||
{...props}
|
<TileLayerInternal
|
||||||
asset={asset}
|
{...props}
|
||||||
/>
|
asset={asset}
|
||||||
|
mask={mask}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {Graphics} from '@pixi/react';
|
import {Container, Graphics} from '@pixi/react';
|
||||||
import {forwardRef, useCallback, useEffect, useRef, useState} from 'react';
|
import {forwardRef, useCallback, useEffect, useRef, useState} from 'react';
|
||||||
|
|
||||||
const WaterTile = forwardRef(function WaterTile({height, width}, ref) {
|
const WaterTile = forwardRef(function WaterTile({height, width}, ref) {
|
||||||
|
@ -15,7 +15,7 @@ const WaterTile = forwardRef(function WaterTile({height, width}, ref) {
|
||||||
return <Graphics alpha={0} draw={draw} ref={ref} />
|
return <Graphics alpha={0} draw={draw} ref={ref} />
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function Water({tileLayer, water}) {
|
export default function Water({mask, tileLayer, water}) {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
|
@ -39,13 +39,13 @@ export default function Water({tileLayer, water}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<Container mask={mask}>
|
||||||
<WaterTile
|
<WaterTile
|
||||||
height={tileLayer.tileSize.y}
|
height={tileLayer.tileSize.y}
|
||||||
ref={waterTile}
|
ref={waterTile}
|
||||||
width={tileLayer.tileSize.x}
|
width={tileLayer.tileSize.x}
|
||||||
/>
|
/>
|
||||||
{waterTiles}
|
{waterTiles}
|
||||||
</>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {Outlet, useParams} from 'react-router-dom';
|
import {Outlet, useParams} from 'react-router-dom';
|
||||||
|
|
||||||
import LocalClient from '@/net/client/local.js';
|
|
||||||
import RemoteClient from '@/net/client/remote.js';
|
|
||||||
import {decode, encode} from '@/packets/index.js';
|
import {decode, encode} from '@/packets/index.js';
|
||||||
|
|
||||||
import styles from './play.module.css';
|
import styles from './play.module.css';
|
||||||
|
@ -12,24 +10,27 @@ export default function Play() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [type] = params['*'].split('/');
|
const [type] = params['*'].split('/');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let Client;
|
async function loadClient() {
|
||||||
switch (type) {
|
let Client;
|
||||||
case 'local':
|
switch (type) {
|
||||||
Client = LocalClient;
|
case 'local':
|
||||||
break;
|
({default: Client} = await import('@/net/client/local.js'));
|
||||||
case 'remote':
|
break;
|
||||||
Client = RemoteClient;
|
case 'remote':
|
||||||
break;
|
({default: Client} = await import('@/net/client/remote.js'));
|
||||||
}
|
break;
|
||||||
class SilphiusClient extends Client {
|
|
||||||
accept(packed) {
|
|
||||||
super.accept(decode(packed));
|
|
||||||
}
|
}
|
||||||
transmit(packet) {
|
class SilphiusClient extends Client {
|
||||||
super.transmit(encode(packet));
|
accept(packed) {
|
||||||
|
super.accept(decode(packed));
|
||||||
|
}
|
||||||
|
transmit(packet) {
|
||||||
|
super.transmit(encode(packet));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
setClient(() => SilphiusClient);
|
||||||
}
|
}
|
||||||
setClient(() => SilphiusClient);
|
loadClient();
|
||||||
}, [type]);
|
}, [type]);
|
||||||
return (
|
return (
|
||||||
<div className={styles.play}>
|
<div className={styles.play}>
|
||||||
|
|
185
app/util/math.js
185
app/util/math.js
|
@ -42,6 +42,70 @@ export const {
|
||||||
SQRT2,
|
SQRT2,
|
||||||
} = Math;
|
} = Math;
|
||||||
|
|
||||||
|
export const TAU = Math.PI * 2;
|
||||||
|
|
||||||
|
export function bresenham({x: x1, y: y1}, {x: x2, y: y2}) {
|
||||||
|
const points = [];
|
||||||
|
let x; let y; let px; let py; let xe; let ye;
|
||||||
|
const dx = x2 - x1;
|
||||||
|
const dy = y2 - y1;
|
||||||
|
const dx1 = Math.abs(dx);
|
||||||
|
const dy1 = Math.abs(dy);
|
||||||
|
px = 2 * dy1 - dx1;
|
||||||
|
py = 2 * dx1 - dy1;
|
||||||
|
if (dy1 <= dx1) {
|
||||||
|
if (dx >= 0) {
|
||||||
|
x = x1; y = y1; xe = x2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
x = x2; y = y2; xe = x1;
|
||||||
|
}
|
||||||
|
points.push({x, y});
|
||||||
|
for (let i = 0; x < xe; i++) {
|
||||||
|
x += 1;
|
||||||
|
if (px < 0) {
|
||||||
|
px += 2 * dy1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ((dx < 0 && dy < 0) || (dx > 0 && dy > 0)) {
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
px += 2 * (dy1 - dx1);
|
||||||
|
}
|
||||||
|
points.push({x, y});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (dy >= 0) {
|
||||||
|
x = x1; y = y1; ye = y2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
x = x2; y = y2; ye = y1;
|
||||||
|
}
|
||||||
|
points.push({x, y});
|
||||||
|
for (let i = 0; y < ye; i++) {
|
||||||
|
y += 1;
|
||||||
|
if (py <= 0) {
|
||||||
|
py += 2 * dx1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ((dx < 0 && dy < 0) || (dx > 0 && dy > 0)) {
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
x -= 1;
|
||||||
|
}
|
||||||
|
py += 2 * (dx1 - dy1);
|
||||||
|
}
|
||||||
|
points.push({x, y});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
export function clamp(n, min, max) {
|
export function clamp(n, min, max) {
|
||||||
return Math.max(min, Math.min(max, n));
|
return Math.max(min, Math.min(max, n));
|
||||||
}
|
}
|
||||||
|
@ -52,6 +116,57 @@ export function distance({x: lx, y: ly}, {x: rx, y: ry}) {
|
||||||
return Math.sqrt(xd * xd + yd * yd);
|
return Math.sqrt(xd * xd + yd * yd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function floodwalk2D(eligible, data, {x, y, w, h}, {diagonal = false} = {}) {
|
||||||
|
const n = data.length;
|
||||||
|
let i = x + w * y;
|
||||||
|
const points = new Set();
|
||||||
|
if (i < 0 || i >= n) {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
const seen = [];
|
||||||
|
seen[-1] = true;
|
||||||
|
seen[i] = true;
|
||||||
|
const todo = [i];
|
||||||
|
while (todo.length > 0) {
|
||||||
|
i = todo.pop();
|
||||||
|
const v = data[i];
|
||||||
|
if (!eligible.has(v)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
points.add(i);
|
||||||
|
const xx = i % w;
|
||||||
|
const yy = Math.floor(i / w);
|
||||||
|
const us = yy >= 1;
|
||||||
|
const rs = xx + 1 < w;
|
||||||
|
const ds = yy + 1 < h;
|
||||||
|
const ls = xx >= 1;
|
||||||
|
const sel = [
|
||||||
|
us ? i - w : -1,
|
||||||
|
rs ? i + 1 : -1,
|
||||||
|
ds ? i + w : -1,
|
||||||
|
ls ? i - 1 : -1,
|
||||||
|
...(
|
||||||
|
diagonal
|
||||||
|
? [
|
||||||
|
us && rs ? i - w + 1 : -1,
|
||||||
|
rs && ds ? i + w + 1 : -1,
|
||||||
|
ds && ls ? i + w - 1 : -1,
|
||||||
|
ls && us ? i - w - 1 : -1,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
];
|
||||||
|
for (let i = 0; i < sel.length; ++i) {
|
||||||
|
const j = sel[i];
|
||||||
|
if (!seen[j]) {
|
||||||
|
todo.push(j);
|
||||||
|
seen[j] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
export function intersects(l, r) {
|
export function intersects(l, r) {
|
||||||
if (l.x0 > r.x1) return false;
|
if (l.x0 > r.x1) return false;
|
||||||
if (l.y0 > r.y1) return false;
|
if (l.y0 > r.y1) return false;
|
||||||
|
@ -71,3 +186,73 @@ export function normalizeVector({x, y}) {
|
||||||
export function random() {
|
export function random() {
|
||||||
return Math.random();
|
return Math.random();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isCollinear({x: ax, y: ay}, {x: bx, y: by}, {x: cx, y: cy}) {
|
||||||
|
return (ay - by) * (ax - cx) === (ay - cy) * (ax - bx);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const directionToVector = [
|
||||||
|
{x: 0, y: -1},
|
||||||
|
{x: 1, y: 0},
|
||||||
|
{x: 0, y: 1},
|
||||||
|
{x: -1, y: 0},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ortho(points, k = {x: 1, y: 1}) {
|
||||||
|
if (points.length < 4) {
|
||||||
|
throw new TypeError('Math.ortho(): points.length < 4');
|
||||||
|
}
|
||||||
|
const index = Object.create(null);
|
||||||
|
for (const i in points) {
|
||||||
|
const point = points[i];
|
||||||
|
if (!index[point.y]) {
|
||||||
|
index[point.y] = Object.create(null);
|
||||||
|
}
|
||||||
|
index[point.y][point.x] = i;
|
||||||
|
}
|
||||||
|
const sorted = [points[0]];
|
||||||
|
let direction = 0;
|
||||||
|
let walk = 0;
|
||||||
|
navigate:
|
||||||
|
while (true) {
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
// ccw rotation
|
||||||
|
const nextDirection = (i + direction + 3) & 3;
|
||||||
|
const {x: px, y: py} = points[walk];
|
||||||
|
const {x: dx, y: dy} = directionToVector[nextDirection];
|
||||||
|
const nextIndex = index[py + k.y * dy]?.[px + k.x * dx];
|
||||||
|
const nextPoint = points[nextIndex];
|
||||||
|
// loop = done
|
||||||
|
if (points[0] === nextPoint) {
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
if (nextPoint) {
|
||||||
|
direction = nextDirection;
|
||||||
|
sorted.push(nextPoint);
|
||||||
|
walk = nextIndex;
|
||||||
|
continue navigate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeCollinear([...vertices]) {
|
||||||
|
if (vertices.length < 3) {
|
||||||
|
return vertices;
|
||||||
|
}
|
||||||
|
vertices.push(vertices[0]);
|
||||||
|
vertices.push(vertices[1]);
|
||||||
|
const trimmed = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i < vertices.length - 2) {
|
||||||
|
if (isCollinear(vertices[i], vertices[i + 1], vertices[i + 2])) {
|
||||||
|
vertices.splice(i + 1, 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
trimmed.push(vertices[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -6,6 +6,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "silphius-next",
|
"name": "silphius-next",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@leodeslf/simplex-noise": "^1.0.0",
|
||||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||||
"@pixi/filter-adjustment": "^5.1.1",
|
"@pixi/filter-adjustment": "^5.1.1",
|
||||||
"@pixi/filter-color-matrix": "^7.4.2",
|
"@pixi/filter-color-matrix": "^7.4.2",
|
||||||
|
@ -3018,6 +3019,11 @@
|
||||||
"integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==",
|
"integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@leodeslf/simplex-noise": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@leodeslf/simplex-noise/-/simplex-noise-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-hsp+CfDnT9jxUjDUqiV2+eLkkdAKz6szlpcyMyXvrgs+LO8a0Gepyri1V+c6FbOfqc3ReWB282GtIE8y3Idi1A=="
|
||||||
|
},
|
||||||
"node_modules/@mdx-js/mdx": {
|
"node_modules/@mdx-js/mdx": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz",
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"test": "vitest app"
|
"test": "vitest app"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@leodeslf/simplex-noise": "^1.0.0",
|
||||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||||
"@pixi/filter-adjustment": "^5.1.1",
|
"@pixi/filter-adjustment": "^5.1.1",
|
||||||
"@pixi/filter-color-matrix": "^7.4.2",
|
"@pixi/filter-color-matrix": "^7.4.2",
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
|
const layer0 = ecs.get(1).TileLayers.layer(0)
|
||||||
|
const layer1 = ecs.get(1).TileLayers.layer(1)
|
||||||
|
|
||||||
const filtered = []
|
const filtered = []
|
||||||
|
|
||||||
for (let i = 0; i < projected.length; ++i) {
|
for (let i = 0; i < projected.length; ++i) {
|
||||||
const tile = layer.tile(projected[i])
|
if (
|
||||||
if ([1, 2, 3, 4, 6].includes(tile)) {
|
[1, 2, 3, 4, 6].includes(layer0.tile(projected[i]))
|
||||||
|
&& ![7].includes(layer1.tile(projected[i]))
|
||||||
|
) {
|
||||||
filtered.push(projected[i])
|
filtered.push(projected[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,9 +115,7 @@ if (projected?.length > 0) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < projected.length; ++i) {
|
for (let i = 0; i < projected.length; ++i) {
|
||||||
if ([1, 2, 3, 4].includes(layer.tile(projected[i]))) {
|
TileLayers.layer(1).stamp(projected[i], [[7]])
|
||||||
layer.stamp(projected[i], [[7]])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Controlled.locked = 0;
|
Controlled.locked = 0;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
const layer = ecs.get(1).TileLayers.layer(1)
|
||||||
|
|
||||||
const filtered = []
|
const filtered = []
|
||||||
|
|
||||||
for (let i = 0; i < projected.length; ++i) {
|
for (let i = 0; i < projected.length; ++i) {
|
||||||
|
|
|
@ -27,7 +27,7 @@ if (projected?.length > 0) {
|
||||||
Plant: {
|
Plant: {
|
||||||
growScript: '/assets/tomato-plant/grow.js',
|
growScript: '/assets/tomato-plant/grow.js',
|
||||||
mayGrowScript: '/assets/tomato-plant/may-grow.js',
|
mayGrowScript: '/assets/tomato-plant/may-grow.js',
|
||||||
stages: Array(5).fill(5),
|
stages: Array(5).fill(0.5),
|
||||||
},
|
},
|
||||||
Sprite: {
|
Sprite: {
|
||||||
anchorX: 0.5,
|
anchorX: 0.5,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
const layer = ecs.get(1).TileLayers.layer(1)
|
||||||
|
|
||||||
const filtered = []
|
const filtered = []
|
||||||
|
|
||||||
for (let i = 0; i < projected.length; ++i) {
|
for (let i = 0; i < projected.length; ++i) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user