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

View File

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

View File

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

View File

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

View File

@ -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;
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;
}
@ -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) {
@ -81,8 +109,51 @@ export default class TileLayers extends Component {
get tileSize() {
return this.layer.tileSize;
}
}
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;
}
return new LayerProxy(layers[index]);
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];
}
}
}

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

View File

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

View File

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

View File

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

View File

@ -51,9 +51,6 @@ function Entity({entity, ...rest}) {
if (!entity) {
return false;
}
if (debug) {
entity.Collider?.recalculateAabbs();
}
return (
<Container
zIndex={entity.Position?.y || 0}

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
const layer = ecs.get(1).TileLayers.layer(1)
const filtered = []
for (let i = 0; i < projected.length; ++i) {

View File

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

View File

@ -1,3 +1,5 @@
const layer = ecs.get(1).TileLayers.layer(1)
const filtered = []
for (let i = 0; i < projected.length; ++i) {