Compare commits

...

10 Commits

Author SHA1 Message Date
cha0s
f3fe70410f dev: fast please 2024-07-07 17:35:04 -05:00
cha0s
8122018222 feat: layer hull masks 2024-07-07 17:34:40 -05:00
cha0s
df61a43ab5 refactor: layer 1 2024-07-07 17:32:54 -05:00
cha0s
cd875f8025 feat: tile hulls 2024-07-07 17:31:39 -05:00
cha0s
64ece0cb86 refactor: less layer proxy churn 2024-07-07 17:29:36 -05:00
cha0s
1ee8f206de refactor: less overhead 2024-07-07 17:26:29 -05:00
cha0s
0372b0ddf4 refactor: easier testing 2024-07-07 17:26:17 -05:00
cha0s
667eaded8e refactor: dynamic import 2024-07-05 21:40:49 -05:00
cha0s
5429913587 fix: filter combination 2024-07-05 18:25:57 -05:00
cha0s
7a83e1bc7a refactor: aabbs and impassability 2024-07-04 21:47:14 -05:00
22 changed files with 633 additions and 157 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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