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)),
|
||||
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: {},
|
||||
|
@ -35,9 +41,9 @@ export default async function createHomestead(Ecs) {
|
|||
impassable: 1,
|
||||
points: [
|
||||
{x: -52, y: -16},
|
||||
{x: 48, y: -16},
|
||||
{x: 44, y: -16},
|
||||
{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;
|
||||
return class ColliderInstance extends super.instanceFromSchema() {
|
||||
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) {
|
||||
const {aabb, aabbs} = this;
|
||||
const {aabb: otherAabb, aabbs: otherAabbs} = other;
|
||||
|
@ -38,36 +60,34 @@ export default class Collider extends Component {
|
|||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
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 + px;
|
||||
const y = point.y + py;
|
||||
const x = point.x;
|
||||
const y = point.y;
|
||||
if (x < x0) x0 = x;
|
||||
if (x < this.aabb.x0) this.aabb.x0 = x;
|
||||
if (x < instance.$$aabb.x0) instance.$$aabb.x0 = x;
|
||||
if (x > x1) x1 = x;
|
||||
if (x > this.aabb.x1) this.aabb.x1 = x;
|
||||
if (x > instance.$$aabb.x1) instance.$$aabb.x1 = x;
|
||||
if (y < y0) y0 = y;
|
||||
if (y < this.aabb.y0) this.aabb.y0 = y;
|
||||
if (y < instance.$$aabb.y0) instance.$$aabb.y0 = y;
|
||||
if (y > y1) y1 = y;
|
||||
if (y > this.aabb.y1) this.aabb.y1 = y;
|
||||
if (y > instance.$$aabb.y1) instance.$$aabb.y1 = y;
|
||||
}
|
||||
this.aabbs.push({
|
||||
instance.$$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) {
|
||||
|
||||
// heavy handed...
|
||||
if ('undefined' !== typeof window) {
|
||||
return;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import gather from '@/util/gather.js';
|
||||
|
||||
const Gathered = gather(
|
||||
import.meta.glob('./*.js', {eager: true, import: 'default'}),
|
||||
import.meta.glob(['./*.js', '!./*.test.js'], {eager: true, import: 'default'}),
|
||||
);
|
||||
|
||||
const Components = {};
|
||||
|
|
|
@ -81,7 +81,6 @@ class ItemProxy {
|
|||
}
|
||||
if (this.scripts.projectionCheckInstance) {
|
||||
this.scripts.projectionCheckInstance.context.ecs = this.Component.ecs;
|
||||
this.scripts.projectionCheckInstance.context.layer = layer;
|
||||
this.scripts.projectionCheckInstance.context.projected = projected;
|
||||
return this.scripts.projectionCheckInstance.evaluateSync();
|
||||
}
|
||||
|
|
|
@ -1,56 +1,84 @@
|
|||
import Component from '@/ecs/component.js';
|
||||
import {floodwalk2D, ortho, removeCollinear} from '@/util/math.js';
|
||||
|
||||
import vector2d from './helpers/vector-2d';
|
||||
|
||||
export default class TileLayers extends Component {
|
||||
insertMany(entities) {
|
||||
for (const [id, {layerChange}] of entities) {
|
||||
if (layerChange) {
|
||||
const component = this.get(id);
|
||||
const {layers} = component;
|
||||
for (const layerIndex in layerChange) {
|
||||
for (const calculated in layerChange[layerIndex]) {
|
||||
const tile = layerChange[layerIndex][calculated];
|
||||
layers[layerIndex].data[calculated] = tile;
|
||||
}
|
||||
layers[layerIndex] = {...layers[layerIndex]};
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.insertMany(entities);
|
||||
}
|
||||
mergeDiff(original, update) {
|
||||
if (!update.layerChange) {
|
||||
return super.mergeDiff(original, update);
|
||||
}
|
||||
const layerChange = {
|
||||
...original.layerChange,
|
||||
};
|
||||
for (const index in update.layerChange) {
|
||||
layerChange[index] = {
|
||||
...layerChange[index],
|
||||
...update.layerChange[index],
|
||||
};
|
||||
}
|
||||
return {layerChange};
|
||||
}
|
||||
instanceFromSchema() {
|
||||
const Instance = super.instanceFromSchema();
|
||||
const Component = this;
|
||||
return class TileLayersInstance extends Instance {
|
||||
layer(index) {
|
||||
const {layers} = this;
|
||||
if (!(index in layers)) {
|
||||
return undefined;
|
||||
}
|
||||
const instance = this;
|
||||
class LayerProxy {
|
||||
constructor(layer) {
|
||||
this.layer = layer;
|
||||
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;
|
||||
}
|
||||
|
@ -70,7 +98,7 @@ export default class TileLayers extends Component {
|
|||
changes[calculated] = tile;
|
||||
}
|
||||
}
|
||||
Component.markChange(instance.entity, 'layerChange', {[index]: changes});
|
||||
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) {
|
||||
|
@ -82,7 +110,50 @@ export default class TileLayers extends Component {
|
|||
return this.layer.tileSize;
|
||||
}
|
||||
}
|
||||
return new LayerProxy(layers[index]);
|
||||
|
||||
export default class TileLayers extends Component {
|
||||
insertMany(entities) {
|
||||
for (const [id, {layerChange}] of entities) {
|
||||
if (layerChange) {
|
||||
const component = this.get(id);
|
||||
const {layers} = component;
|
||||
for (const layerIndex in layerChange) {
|
||||
for (const calculated in layerChange[layerIndex]) {
|
||||
const tile = layerChange[layerIndex][calculated];
|
||||
layers[layerIndex].data[calculated] = tile;
|
||||
}
|
||||
layers[layerIndex] = {...layers[layerIndex]};
|
||||
component.$$layersProxies[layerIndex] = new LayerProxy(component, this, layerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.insertMany(entities);
|
||||
}
|
||||
load(instance) {
|
||||
for (const index in instance.layers) {
|
||||
instance.$$layersProxies[index] = new LayerProxy(instance, this, index);
|
||||
}
|
||||
}
|
||||
mergeDiff(original, update) {
|
||||
if (!update.layerChange) {
|
||||
return super.mergeDiff(original, update);
|
||||
}
|
||||
const layerChange = {
|
||||
...original.layerChange,
|
||||
};
|
||||
for (const index in update.layerChange) {
|
||||
layerChange[index] = {
|
||||
...layerChange[index],
|
||||
...update.layerChange[index],
|
||||
};
|
||||
}
|
||||
return {layerChange};
|
||||
}
|
||||
instanceFromSchema() {
|
||||
return class TileLayersInstance extends super.instanceFromSchema() {
|
||||
$$layersProxies = {};
|
||||
layer(index) {
|
||||
return this.$$layersProxies[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 {intersects} from '@/util/math.js';
|
||||
import SpatialHash from '@/util/spatial-hash.js';
|
||||
|
||||
export default class Colliders extends System {
|
||||
|
@ -34,7 +35,6 @@ export default class Colliders extends System {
|
|||
if (!entity.Collider) {
|
||||
return;
|
||||
}
|
||||
entity.Collider.recalculateAabbs();
|
||||
this.hash.update(entity.Collider.aabb, entity.id);
|
||||
}
|
||||
|
||||
|
@ -79,10 +79,40 @@ export default class Colliders extends System {
|
|||
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) {
|
||||
const j = entity.Collider.bodies.indexOf(body);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,11 +29,14 @@ export default class Component {
|
|||
}
|
||||
|
||||
async create(entityId, values) {
|
||||
this.createMany([[entityId, values]]);
|
||||
const [created] = await this.createMany([[entityId, values]]);
|
||||
return created;
|
||||
}
|
||||
|
||||
async createMany(entries) {
|
||||
if (entries.length > 0) {
|
||||
if (0 === entries.length) {
|
||||
return [];
|
||||
}
|
||||
const allocated = this.allocateMany(entries.length);
|
||||
const {properties} = this.constructor.schema.specification;
|
||||
const keys = Object.keys(properties);
|
||||
|
@ -56,7 +59,11 @@ export default class Component {
|
|||
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) {
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import {simplex2D} from '@leodeslf/simplex-noise';
|
||||
import {Texture} from '@pixi/core';
|
||||
import {Container} from '@pixi/react';
|
||||
import {Sprite} from '@pixi/sprite';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {RESOLUTION} from '@/constants.js';
|
||||
import {usePacket} from '@/context/client.js';
|
||||
import {useEcs, useEcsTick} from '@/context/ecs.js';
|
||||
import {useMainEntity} from '@/context/main-entity.js';
|
||||
import {bresenham, TAU} from '@/util/math.js';
|
||||
|
||||
import Entities from './entities.jsx';
|
||||
import TargetingGhost from './targeting-ghost.jsx';
|
||||
|
@ -28,12 +32,64 @@ function calculateDarkness(hour) {
|
|||
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}) {
|
||||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
const [mainEntity] = useMainEntity();
|
||||
const [hour, setHour] = useState(10);
|
||||
const [night, setNight] = useState();
|
||||
const [mask, setMask] = useState();
|
||||
useEffect(() => {
|
||||
async function buildNightFilter() {
|
||||
const {ColorMatrixFilter} = await import('@pixi/filter-color-matrix');
|
||||
|
@ -79,6 +135,12 @@ export default function Ecs({scale}) {
|
|||
if (update.Time) {
|
||||
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);
|
||||
if (update.Emitter?.emit) {
|
||||
|
@ -101,7 +163,9 @@ export default function Ecs({scale}) {
|
|||
const {Direction, Position, Wielder} = entity;
|
||||
const projected = Wielder.activeItem()?.project(Position.tile, Direction.direction)
|
||||
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 = [];
|
||||
if (night) {
|
||||
filters.push(night);
|
||||
|
@ -112,27 +176,47 @@ export default function Ecs({scale}) {
|
|||
];
|
||||
return (
|
||||
<Container
|
||||
filters={filters}
|
||||
scale={scale}
|
||||
x={-cx}
|
||||
y={-cy}
|
||||
>
|
||||
<TileLayer tileLayer={layer} />
|
||||
<Container
|
||||
filters={filters}
|
||||
>
|
||||
<TileLayer
|
||||
filters={filters}
|
||||
tileLayer={layer0}
|
||||
/>
|
||||
{layer1 && (
|
||||
<TileLayer
|
||||
mask={mask}
|
||||
filters={filters}
|
||||
tileLayer={layer1}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
{WaterEcs && (
|
||||
<Water tileLayer={layer} water={WaterEcs.water} />
|
||||
<Water
|
||||
mask={mask}
|
||||
tileLayer={layer0}
|
||||
water={WaterEcs.water}
|
||||
/>
|
||||
)}
|
||||
{projected && (
|
||||
<TargetingGrid
|
||||
tileLayer={layer}
|
||||
tileLayer={layer0}
|
||||
x={Position.x}
|
||||
y={Position.y}
|
||||
/>
|
||||
)}
|
||||
<Entities entities={entities} />
|
||||
<Entities
|
||||
entities={entities}
|
||||
filters={filters}
|
||||
/>
|
||||
{projected?.length > 0 && (
|
||||
<TargetingGhost
|
||||
projected={projected}
|
||||
tileLayer={layer}
|
||||
tileLayer={layer0}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
|
|
|
@ -8,15 +8,15 @@ import {useMainEntity} from '@/context/main-entity.js';
|
|||
|
||||
import Entity from './entity.jsx';
|
||||
|
||||
export default function Entities({entities}) {
|
||||
export default function Entities({entities, filters}) {
|
||||
const [ecs] = useEcs();
|
||||
const [mainEntity] = useMainEntity();
|
||||
const [radians, setRadians] = 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;
|
||||
filters[0].brightness = (pulse * 0.75) + 1;
|
||||
filters[1].outerStrength = pulse * 0.5;
|
||||
interactionFilters[0].brightness = (pulse * 0.75) + 1;
|
||||
interactionFilters[1].outerStrength = pulse * 0.5;
|
||||
useEffect(() => {
|
||||
setRadians(0);
|
||||
const handle = setInterval(() => {
|
||||
|
@ -40,7 +40,7 @@ export default function Entities({entities}) {
|
|||
const isHighlightedInteraction = id == willInteractWith;
|
||||
renderables.push(
|
||||
<Entity
|
||||
filters={isHighlightedInteraction ? filters : []}
|
||||
filters={filters.concat(isHighlightedInteraction ? interactionFilters : [])}
|
||||
entity={entities[id]}
|
||||
key={id}
|
||||
/>
|
||||
|
|
|
@ -51,9 +51,6 @@ function Entity({entity, ...rest}) {
|
|||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
if (debug) {
|
||||
entity.Collider?.recalculateAabbs();
|
||||
}
|
||||
return (
|
||||
<Container
|
||||
zIndex={entity.Position?.y || 0}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {Container} from '@pixi/display';
|
||||
import {PixiComponent} from '@pixi/react';
|
||||
import '@pixi/spritesheet'; // NECESSARY!
|
||||
import {CompositeTilemap} from '@pixi/tilemap';
|
||||
|
@ -5,14 +6,27 @@ import {CompositeTilemap} from '@pixi/tilemap';
|
|||
import {useAsset} from '@/context/assets.js';
|
||||
|
||||
const TileLayerInternal = PixiComponent('TileLayer', {
|
||||
create: () => new CompositeTilemap(),
|
||||
applyProps: (tilemap, {tileLayer: oldTileLayer}, props) => {
|
||||
const {asset, tileLayer} = props;
|
||||
create: () => {
|
||||
const container = new Container();
|
||||
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 {textures} = asset;
|
||||
if (tileLayer === oldTileLayer) {
|
||||
return;
|
||||
}
|
||||
if (oldMask) {
|
||||
container.removeChildAt(1);
|
||||
container.mask = undefined;
|
||||
}
|
||||
if (mask) {
|
||||
container.addChild(mask);
|
||||
container.mask = mask;
|
||||
}
|
||||
const tilemap = container.children[0];
|
||||
tilemap.clear();
|
||||
let i = 0;
|
||||
for (let y = 0; y < tileLayer.area.y; ++y) {
|
||||
|
@ -24,15 +38,18 @@ const TileLayerInternal = PixiComponent('TileLayer', {
|
|||
})
|
||||
|
||||
export default function TileLayer(props) {
|
||||
const {tileLayer} = props;
|
||||
const {mask, tileLayer} = props;
|
||||
const asset = useAsset(tileLayer.source);
|
||||
if (!asset) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<TileLayerInternal
|
||||
{...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';
|
||||
|
||||
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} />
|
||||
});
|
||||
|
||||
export default function Water({tileLayer, water}) {
|
||||
export default function Water({mask, tileLayer, water}) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
@ -39,13 +39,13 @@ export default function Water({tileLayer, water}) {
|
|||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Container mask={mask}>
|
||||
<WaterTile
|
||||
height={tileLayer.tileSize.y}
|
||||
ref={waterTile}
|
||||
width={tileLayer.tileSize.x}
|
||||
/>
|
||||
{waterTiles}
|
||||
</>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import {useEffect, useState} from 'react';
|
||||
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 styles from './play.module.css';
|
||||
|
@ -12,13 +10,14 @@ export default function Play() {
|
|||
const params = useParams();
|
||||
const [type] = params['*'].split('/');
|
||||
useEffect(() => {
|
||||
async function loadClient() {
|
||||
let Client;
|
||||
switch (type) {
|
||||
case 'local':
|
||||
Client = LocalClient;
|
||||
({default: Client} = await import('@/net/client/local.js'));
|
||||
break;
|
||||
case 'remote':
|
||||
Client = RemoteClient;
|
||||
({default: Client} = await import('@/net/client/remote.js'));
|
||||
break;
|
||||
}
|
||||
class SilphiusClient extends Client {
|
||||
|
@ -30,6 +29,8 @@ export default function Play() {
|
|||
}
|
||||
}
|
||||
setClient(() => SilphiusClient);
|
||||
}
|
||||
loadClient();
|
||||
}, [type]);
|
||||
return (
|
||||
<div className={styles.play}>
|
||||
|
|
185
app/util/math.js
185
app/util/math.js
|
@ -42,6 +42,70 @@ export const {
|
|||
SQRT2,
|
||||
} = 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) {
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (l.x0 > r.x1) return false;
|
||||
if (l.y0 > r.y1) return false;
|
||||
|
@ -71,3 +186,73 @@ export function normalizeVector({x, y}) {
|
|||
export function 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",
|
||||
"dependencies": {
|
||||
"@leodeslf/simplex-noise": "^1.0.0",
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/filter-adjustment": "^5.1.1",
|
||||
"@pixi/filter-color-matrix": "^7.4.2",
|
||||
|
@ -3018,6 +3019,11 @@
|
|||
"integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==",
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz",
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"test": "vitest app"
|
||||
},
|
||||
"dependencies": {
|
||||
"@leodeslf/simplex-noise": "^1.0.0",
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/filter-adjustment": "^5.1.1",
|
||||
"@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 = []
|
||||
|
||||
for (let i = 0; i < projected.length; ++i) {
|
||||
const tile = layer.tile(projected[i])
|
||||
if ([1, 2, 3, 4, 6].includes(tile)) {
|
||||
if (
|
||||
[1, 2, 3, 4, 6].includes(layer0.tile(projected[i]))
|
||||
&& ![7].includes(layer1.tile(projected[i]))
|
||||
) {
|
||||
filtered.push(projected[i])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -115,9 +115,7 @@ if (projected?.length > 0) {
|
|||
}
|
||||
|
||||
for (let i = 0; i < projected.length; ++i) {
|
||||
if ([1, 2, 3, 4].includes(layer.tile(projected[i]))) {
|
||||
layer.stamp(projected[i], [[7]])
|
||||
}
|
||||
TileLayers.layer(1).stamp(projected[i], [[7]])
|
||||
}
|
||||
|
||||
Controlled.locked = 0;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const layer = ecs.get(1).TileLayers.layer(1)
|
||||
|
||||
const filtered = []
|
||||
|
||||
for (let i = 0; i < projected.length; ++i) {
|
||||
|
|
|
@ -27,7 +27,7 @@ if (projected?.length > 0) {
|
|||
Plant: {
|
||||
growScript: '/assets/tomato-plant/grow.js',
|
||||
mayGrowScript: '/assets/tomato-plant/may-grow.js',
|
||||
stages: Array(5).fill(5),
|
||||
stages: Array(5).fill(0.5),
|
||||
},
|
||||
Sprite: {
|
||||
anchorX: 0.5,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const layer = ecs.get(1).TileLayers.layer(1)
|
||||
|
||||
const filtered = []
|
||||
|
||||
for (let i = 0; i < projected.length; ++i) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user