Compare commits
16 Commits
15d4abaad1
...
3d4d29625d
Author | SHA1 | Date | |
---|---|---|---|
|
3d4d29625d | ||
|
41baef2571 | ||
|
532792595a | ||
|
9f33e4df8a | ||
|
4397011b25 | ||
|
f9bf9096ee | ||
|
466e80aa23 | ||
|
10d6638452 | ||
|
c848b07668 | ||
|
ed5d12b4a8 | ||
|
e0a30ddb0b | ||
|
348a24d8a2 | ||
|
c344b65534 | ||
|
ccb0be9dd9 | ||
|
8e4fd46959 | ||
|
8907658710 |
|
@ -25,8 +25,21 @@ export default class ClientEcs extends Ecs {
|
|||
return cache.get(uri);
|
||||
}
|
||||
async readJson(uri) {
|
||||
const chars = await this.readAsset(uri);
|
||||
return chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {};
|
||||
const key = ['$$json', uri].join(':');
|
||||
if (!cache.has(key)) {
|
||||
let promise, resolve, reject;
|
||||
promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
cache.set(key, promise);
|
||||
this.readAsset(uri)
|
||||
.then((chars) => {
|
||||
resolve(chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {});
|
||||
})
|
||||
.catch(reject);
|
||||
}
|
||||
return cache.get(key);
|
||||
}
|
||||
async readScript(uri, context = {}) {
|
||||
const code = await this.readAsset(uri);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export const CHUNK_SIZE = 32;
|
||||
|
||||
export const CLIENT_LATENCY = 0;
|
||||
|
||||
export const CLIENT_PREDICTION = true;
|
||||
|
|
147
app/create-forest.js
Normal file
147
app/create-forest.js
Normal file
|
@ -0,0 +1,147 @@
|
|||
import alea from 'alea';
|
||||
import {createNoise2D} from 'simplex-noise';
|
||||
|
||||
import {createRandom, Generator} from '@/util/math.js';
|
||||
|
||||
import createEcs from './create-ecs.js';
|
||||
|
||||
const seed = 42069;
|
||||
const prng = alea(seed);
|
||||
const rawNoise = createNoise2D(prng);
|
||||
const noise = (x, y) => (1 + rawNoise(x, y)) / 2;
|
||||
const random = createRandom(seed);
|
||||
|
||||
const indexComputer = (indices, randomizer) => (
|
||||
(position) => indices[Math.floor(indices.length * randomizer(position))]
|
||||
);
|
||||
|
||||
const [w, h] = [256, 256];
|
||||
|
||||
const Forest = new Generator({
|
||||
calculate: indexComputer(
|
||||
[1, 3, 4, 10, 11, 12], // grass
|
||||
({x, y}) => noise(x * 30, y * 30),
|
||||
),
|
||||
covers: () => true,
|
||||
size: {w, h},
|
||||
children: [
|
||||
new Generator({
|
||||
calculate: indexComputer(
|
||||
[342, 456], // dirt
|
||||
({x, y}) => noise(x * 30, y * 30),
|
||||
),
|
||||
covers: ({x, y}) => noise(x * (1 / 16), y * (1 / 16)) < 0.5,
|
||||
size: {w, h},
|
||||
children: [
|
||||
new Generator({
|
||||
calculate: indexComputer(
|
||||
[407, 408, 423, 424], // stone path
|
||||
({x, y}) => noise(x * 30, y * 30),
|
||||
),
|
||||
covers: ({x, y}) => noise(x * (1 / 16), y * (1 / 16)) < 0.1625,
|
||||
size: {w, h},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Generator({
|
||||
calculate: indexComputer(
|
||||
[103, 104], // water
|
||||
({x, y}) => noise(x * 30, y * 30),
|
||||
),
|
||||
covers: ({x, y}) => noise(x * (1 / 32), y * (1 / 32)) < 0.3,
|
||||
size: {w, h},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export default async function createForest(Ecs) {
|
||||
const ecs = createEcs(Ecs);
|
||||
const area = {x: w, y: h};
|
||||
const master = ecs.get(await ecs.create({
|
||||
AreaSize: {x: area.x * 16, y: area.y * 16},
|
||||
Ticking: {},
|
||||
TileLayers: {
|
||||
layers: [
|
||||
{
|
||||
area,
|
||||
data: Array(w * h).fill(0),
|
||||
source: '/assets/tileset.json',
|
||||
tileSize: {x: 16, y: 16},
|
||||
},
|
||||
{
|
||||
area,
|
||||
data: Array(w * h).fill(0),
|
||||
source: '/assets/tileset.json',
|
||||
tileSize: {x: 16, y: 16},
|
||||
},
|
||||
],
|
||||
},
|
||||
Time: {},
|
||||
}));
|
||||
Forest.generate();
|
||||
const layer0 = master.TileLayers.layers[0];
|
||||
layer0.data = Forest.matrix;
|
||||
for (let y = 0; y < h; ++y) {
|
||||
for (let x = 0; x < w; ++x) {
|
||||
const dirt = Forest.children[0].matrix[y * w + x];
|
||||
if (dirt) {
|
||||
layer0.data[y * w + x] = dirt;
|
||||
}
|
||||
const stone = Forest.children[0].children[0].matrix[y * w + x];
|
||||
if (stone) {
|
||||
layer0.data[y * w + x] = stone;
|
||||
}
|
||||
const water = Forest.children[1].matrix[y * w + x];
|
||||
if (water) {
|
||||
layer0.data[y * w + x] = water;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const entityPosition = (x, y) => ({
|
||||
x: (8 + (x * 16) + random() * 8 - 4),
|
||||
y: (8 + (y * 16) + random() * 8 - 4),
|
||||
});
|
||||
|
||||
for (let y = 0; y < h; ++y) {
|
||||
for (let x = 0; x < w; ++x) {
|
||||
let v = noise(x * (1 / 24), y * (1 / 24));
|
||||
if (v > 0.2) {
|
||||
v = noise(x * (1 / 7), y * (1 / 7));
|
||||
if (v < 0.15) {
|
||||
await ecs.create({
|
||||
Position: entityPosition(x, y),
|
||||
Sprite: {
|
||||
anchorY: 0.7,
|
||||
source: '/assets/ambient/shrub.json',
|
||||
},
|
||||
VisibleAabb: {},
|
||||
});
|
||||
}
|
||||
else if (v < 0.17) {
|
||||
await ecs.create({
|
||||
Position: entityPosition(x, y),
|
||||
Sprite: {
|
||||
anchorY: 0.875,
|
||||
source: '/assets/ambient/tree.json',
|
||||
},
|
||||
VisibleAabb: {},
|
||||
});
|
||||
}
|
||||
v = noise(x * (1 / 12), y * (1 / 12));
|
||||
if (v < 0.08) {
|
||||
await ecs.create({
|
||||
Position: entityPosition(x, y),
|
||||
Sprite: {
|
||||
anchorY: 0.7,
|
||||
source: '/assets/ambient/flower.json',
|
||||
},
|
||||
VisibleAabb: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ecs;
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ export default async function createPlayer(id) {
|
|||
},
|
||||
Controlled: {},
|
||||
Direction: {direction: 2},
|
||||
Ecs: {path: ['homesteads', `${id}`].join('/')},
|
||||
Ecs: {path: ['forests', `${id}`].join('/')},
|
||||
Emitter: {},
|
||||
Forces: {},
|
||||
Interacts: {},
|
||||
|
|
|
@ -2,7 +2,7 @@ import Component from '@/ecs/component.js';
|
|||
|
||||
export default class AreaSize extends Component {
|
||||
static properties = {
|
||||
x: {type: 'uint16'},
|
||||
y: {type: 'uint16'},
|
||||
x: {type: 'uint32'},
|
||||
y: {type: 'uint32'},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,18 +1,36 @@
|
|||
import {CHUNK_SIZE} from '@/constants.js';
|
||||
import Component from '@/ecs/component.js';
|
||||
import {floodwalk2D, ortho, removeCollinear} from '@/util/math.js';
|
||||
|
||||
import vector2d from './helpers/vector-2d';
|
||||
|
||||
class LayerProxy {
|
||||
$$sourceJson;
|
||||
$$chunks = [];
|
||||
$$sourceJson = {};
|
||||
constructor(instance, Component, index) {
|
||||
this.instance = instance;
|
||||
this.Component = Component;
|
||||
this.index = index;
|
||||
this.$$chunks = Array(
|
||||
Math.ceil(this.area.x / CHUNK_SIZE) * Math.ceil(this.area.y / CHUNK_SIZE)
|
||||
).fill(0).map(() => ({}));
|
||||
}
|
||||
get area() {
|
||||
return this.layer.area;
|
||||
}
|
||||
clone() {
|
||||
const {$$chunks, $$sourceJson} = this.instance.$$layersProxies[this.index];
|
||||
const proxy = new LayerProxy(this.instance, this.Component, this.index);
|
||||
proxy.$$chunks = [...$$chunks];
|
||||
proxy.$$sourceJson = $$sourceJson;
|
||||
return proxy;
|
||||
}
|
||||
compute(index) {
|
||||
const cx = Math.floor((index % this.area.x) / CHUNK_SIZE);
|
||||
const cy = Math.floor(Math.floor(index / this.area.x) / CHUNK_SIZE);
|
||||
const ax = Math.ceil(this.area.x / CHUNK_SIZE);
|
||||
return cy * ax + cx;
|
||||
}
|
||||
get data() {
|
||||
return this.layer.data;
|
||||
}
|
||||
|
@ -119,23 +137,66 @@ class LayerProxy {
|
|||
}
|
||||
|
||||
export default class TileLayers extends Component {
|
||||
async insertMany(entities) {
|
||||
for (const [id, {layerChange}] of entities) {
|
||||
async createMany(entities) {
|
||||
for (const [, {layerChange, layers}] of entities) {
|
||||
if (layers) {
|
||||
for (const layer of layers) {
|
||||
const area = layer.area.x * layer.area.y;
|
||||
if (layer.data.length < area) {
|
||||
for (let i = 0; i < area; ++i) {
|
||||
if (!layer.data[i]) {
|
||||
layer.data[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
await component.$$layersProxies[layerIndex].load();
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.insertMany(entities);
|
||||
return super.createMany(entities);
|
||||
}
|
||||
async insertMany(entities) {
|
||||
for (const [, {layers}] of entities) {
|
||||
if (layers) {
|
||||
for (const layer of layers) {
|
||||
const area = layer.area.x * layer.area.y;
|
||||
if (layer.data.length < area) {
|
||||
for (let i = 0; i < area; ++i) {
|
||||
if (!layer.data[i]) {
|
||||
layer.data[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await super.insertMany(entities);
|
||||
for (const [id, {layerChange}] of entities) {
|
||||
if (layerChange) {
|
||||
const component = this.get(id);
|
||||
const {layers} = component;
|
||||
for (const layerIndex in layerChange) {
|
||||
const proxy = component.$$layersProxies[layerIndex].clone();
|
||||
const chunksChanged = new Set();
|
||||
for (const tileIndex in layerChange[layerIndex]) {
|
||||
chunksChanged.add(proxy.compute(tileIndex));
|
||||
const tile = layerChange[layerIndex][tileIndex];
|
||||
layers[layerIndex].data[tileIndex] = tile;
|
||||
}
|
||||
for (const chunkChanged of chunksChanged) {
|
||||
proxy.$$chunks[chunkChanged] = {};
|
||||
}
|
||||
component.$$layersProxies[layerIndex] = proxy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
instanceFromSchema() {
|
||||
return class TileLayersInstance extends super.instanceFromSchema() {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
// import {RESOLUTION} from '@/constants.js'
|
||||
import {System} from '@/ecs/index.js';
|
||||
|
||||
// const [hx, hy] = [RESOLUTION.x / 2, RESOLUTION.y / 2];
|
||||
|
||||
export default class FollowCamera extends System {
|
||||
|
||||
static queries() {
|
||||
|
@ -24,6 +27,11 @@ export default class FollowCamera extends System {
|
|||
updateCamera(portion, entity) {
|
||||
const {Camera, Position} = entity;
|
||||
if (Camera && Position) {
|
||||
// const {AreaSize: {x, y}} = this.ecs.get(1);
|
||||
// const [px, py] = [
|
||||
// Math.max(hx, Math.min(Math.round(Position.x), x - hx)),
|
||||
// Math.max(hy, Math.min(Math.round(Position.y), y - hy)),
|
||||
// ];
|
||||
const [px, py] = [
|
||||
Math.round(Position.x),
|
||||
Math.round(Position.y),
|
||||
|
|
148
app/engine.js
148
app/engine.js
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
CHUNK_SIZE,
|
||||
RESOLUTION,
|
||||
TPS,
|
||||
} from '@/constants.js';
|
||||
|
@ -7,10 +8,17 @@ import {decode, encode} from '@/packets/index.js';
|
|||
import Script from '@/util/script.js';
|
||||
|
||||
import createEcs from './create-ecs.js';
|
||||
import createForest from './create-forest.js';
|
||||
import createHomestead from './create-homestead.js';
|
||||
import createHouse from './create-house.js';
|
||||
import createPlayer from './create-player.js';
|
||||
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
const cache = new LRUCache({
|
||||
max: 128,
|
||||
});
|
||||
|
||||
export default class Engine {
|
||||
|
||||
connectedPlayers = new Map();
|
||||
|
@ -38,11 +46,35 @@ export default class Engine {
|
|||
return engine.frame;
|
||||
}
|
||||
async readAsset(uri) {
|
||||
return server.readAsset(uri);
|
||||
if (!cache.has(uri)) {
|
||||
let promise, resolve, reject;
|
||||
promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
cache.set(uri, promise);
|
||||
server.readAsset(uri)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
return cache.get(uri);
|
||||
}
|
||||
async readJson(uri) {
|
||||
const chars = await this.readAsset(uri);
|
||||
return chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {};
|
||||
const key = ['$$json', uri].join(':');
|
||||
if (!cache.has(key)) {
|
||||
let promise, resolve, reject;
|
||||
promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
cache.set(key, promise);
|
||||
this.readAsset(uri)
|
||||
.then((chars) => {
|
||||
resolve(chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {});
|
||||
})
|
||||
.catch(reject);
|
||||
}
|
||||
return cache.get(key);
|
||||
}
|
||||
async readScript(uri, context) {
|
||||
if (!uri) {
|
||||
|
@ -61,7 +93,10 @@ export default class Engine {
|
|||
// remove entity link to connection to start queueing actions and pause updates
|
||||
delete connectedPlayer.entity;
|
||||
// forget previous state
|
||||
connectedPlayer.memory.clear();
|
||||
connectedPlayer.memory = {
|
||||
chunks: new Map(),
|
||||
nearby: new Set(),
|
||||
};
|
||||
// inform client of the upcoming change
|
||||
server.send(
|
||||
connection,
|
||||
|
@ -183,7 +218,10 @@ export default class Engine {
|
|||
{
|
||||
entity: ecs.get(entity),
|
||||
id,
|
||||
memory: new Set(),
|
||||
memory: {
|
||||
chunks: new Map(),
|
||||
nearby: new Set(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -230,6 +268,11 @@ export default class Engine {
|
|||
['houses', `${id}`].join('/'),
|
||||
house,
|
||||
);
|
||||
const forest = await createForest(this.Ecs, id);
|
||||
await this.saveEcs(
|
||||
['forests', `${id}`].join('/'),
|
||||
forest,
|
||||
);
|
||||
buffer = await createPlayer(id);
|
||||
await this.server.writeData(
|
||||
['players', `${id}`].join('/'),
|
||||
|
@ -323,12 +366,14 @@ export default class Engine {
|
|||
y1: y0 + (RESOLUTION.y * 2),
|
||||
});
|
||||
// Master entity.
|
||||
nearby.add(ecs.get(1));
|
||||
const lastMemory = new Set(memory.values());
|
||||
const master = ecs.get(1);
|
||||
nearby.add(master);
|
||||
const lastNearby = new Set(memory.nearby.values());
|
||||
const firstUpdate = 0 === lastNearby.size;
|
||||
for (const entity of nearby) {
|
||||
const {id} = entity;
|
||||
lastMemory.delete(id);
|
||||
if (!memory.has(id)) {
|
||||
lastNearby.delete(id);
|
||||
if (!memory.nearby.has(id)) {
|
||||
update[id] = entity.toJSON();
|
||||
if (mainEntityId === id) {
|
||||
update[id].MainEntity = {};
|
||||
|
@ -337,12 +382,91 @@ export default class Engine {
|
|||
else if (ecs.diff[id]) {
|
||||
update[id] = ecs.diff[id];
|
||||
}
|
||||
memory.add(id);
|
||||
memory.nearby.add(id);
|
||||
}
|
||||
for (const id of lastMemory) {
|
||||
memory.delete(id);
|
||||
for (const id of lastNearby) {
|
||||
memory.nearby.delete(id);
|
||||
update[id] = false;
|
||||
}
|
||||
// Tile layer chunking
|
||||
const {TileLayers} = master;
|
||||
const {layers} = TileLayers;
|
||||
let layerChange;
|
||||
for (const i in layers) {
|
||||
const layer = TileLayers.layer(i);
|
||||
const cx = CHUNK_SIZE * layer.tileSize.x;
|
||||
const cy = CHUNK_SIZE * layer.tileSize.y;
|
||||
const rx = 1 + Math.ceil((RESOLUTION.x * 2) / cx);
|
||||
const ry = 1 + Math.ceil((RESOLUTION.y * 2) / cy);
|
||||
const lx = Math.floor((entity.Position.x - RESOLUTION.x) / cx);
|
||||
const ly = Math.floor((entity.Position.y - RESOLUTION.y) / cy);
|
||||
const ax = Math.ceil(layer.area.x / CHUNK_SIZE);
|
||||
for (let wy = 0; wy < ry; ++wy) {
|
||||
for (let wx = 0; wx < rx; ++wx) {
|
||||
const iy = wy + ly;
|
||||
const ix = wx + lx;
|
||||
if (
|
||||
ix >= 0
|
||||
&& iy >= 0
|
||||
&& ix < Math.ceil(layer.area.x / CHUNK_SIZE)
|
||||
&& iy < Math.ceil(layer.area.y / CHUNK_SIZE)
|
||||
) {
|
||||
const chunk = iy * ax + ix;
|
||||
if (!memory.chunks.has(i)) {
|
||||
memory.chunks.set(i, new Set());
|
||||
}
|
||||
if (!memory.chunks.get(i).has(chunk)) {
|
||||
memory.chunks.get(i).add(chunk);
|
||||
if (!layerChange) {
|
||||
layerChange = {};
|
||||
}
|
||||
if (!layerChange[i]) {
|
||||
layerChange[i] = {};
|
||||
}
|
||||
for (let y = 0; y < CHUNK_SIZE; ++y) {
|
||||
for (let x = 0; x < CHUNK_SIZE; ++x) {
|
||||
const ty = (iy * CHUNK_SIZE) + y;
|
||||
const tx = (ix * CHUNK_SIZE) + x;
|
||||
if (
|
||||
tx < 0
|
||||
|| ty < 0
|
||||
|| tx >= layers[i].area.x
|
||||
|| ty >= layers[i].area.y
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const computed = ((iy * CHUNK_SIZE) + y) * layers[i].area.x + ((ix * CHUNK_SIZE) + x);
|
||||
layerChange[i][computed] = layers[i].data[computed];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (firstUpdate && update['1']) {
|
||||
const {TileLayers} = update['1'];
|
||||
if (TileLayers) {
|
||||
const layersUpdate = [];
|
||||
const {layers} = TileLayers;
|
||||
for (const l in layers) {
|
||||
layersUpdate[l] = {
|
||||
...layers[l],
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
update['1'].TileLayers = {layers: layersUpdate};
|
||||
}
|
||||
}
|
||||
if (layerChange) {
|
||||
if (!update['1']) {
|
||||
update['1'] = {};
|
||||
}
|
||||
if (!update['1'].TileLayers) {
|
||||
update['1'].TileLayers = {};
|
||||
}
|
||||
update['1'].TileLayers.layerChange = layerChange;
|
||||
}
|
||||
return update;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import {del, get, set} from 'idb-keyval';
|
|||
|
||||
import {encode} from '@/packets/index.js';
|
||||
|
||||
import '../../create-forest.js';
|
||||
import '../../create-homestead.js';
|
||||
import '../../create-player.js';
|
||||
|
||||
|
@ -92,6 +93,18 @@ if (import.meta.hot) {
|
|||
// await engine.server.writeData('players/0', (new TextEncoder()).encode(JSON.stringify(player)));
|
||||
resolver.resolve();
|
||||
});
|
||||
import.meta.hot.accept('../../create-forest.js', async ({default: createForest}) => {
|
||||
const resolver = createResolver();
|
||||
resolvers.push(resolver);
|
||||
await beforeResolver;
|
||||
delete engine.ecses['forests/0'];
|
||||
await engine.server.removeData('forests/0');
|
||||
const last = performance.now();
|
||||
const forest = await createForest(engine.Ecs, '0');
|
||||
console.log((performance.now() - last) / 1000);
|
||||
await engine.saveEcs('forests/0', forest);
|
||||
resolver.resolve();
|
||||
});
|
||||
import.meta.hot.accept('../../create-homestead.js', async ({default: createHomestead}) => {
|
||||
const resolver = createResolver();
|
||||
resolvers.push(resolver);
|
||||
|
@ -99,7 +112,6 @@ if (import.meta.hot) {
|
|||
// delete engine.ecses['homesteads/0'];
|
||||
// await engine.server.removeData('homesteads/0');
|
||||
// const homestead = await createHomestead(engine.Ecs, '0');
|
||||
// homestead.get(2).Ecs.path = 'houses/0';
|
||||
// await engine.saveEcs('homesteads/0', homestead);
|
||||
resolver.resolve();
|
||||
});
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import {useState} from 'react';
|
||||
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs';
|
||||
import 'react-tabs/style/react-tabs.css';
|
||||
|
||||
import {useEcs, useEcsTick} from '@/context/ecs.js';
|
||||
import {useMainEntity} from '@/context/main-entity.js';
|
||||
|
||||
import styles from './devtools.module.css';
|
||||
|
||||
import Tiles from './devtools/tiles.jsx';
|
||||
|
@ -10,15 +14,22 @@ export default function Devtools({
|
|||
eventsChannel,
|
||||
setApplyFilters,
|
||||
}) {
|
||||
const [ecs] = useEcs();
|
||||
const [mainEntity] = useMainEntity();
|
||||
const [mainEntityJson, setMainEntityJson] = useState({});
|
||||
useEcsTick(() => {
|
||||
if (!ecs || !mainEntity) {
|
||||
return;
|
||||
}
|
||||
setMainEntityJson(ecs.get(mainEntity).toJSON());
|
||||
}, [ecs, mainEntity]);
|
||||
return (
|
||||
<div className={styles.devtools}>
|
||||
|
||||
<Tabs>
|
||||
<TabList>
|
||||
<Tab>Dashboard</Tab>
|
||||
<Tab>Tiles</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel>
|
||||
<div className={styles.dashboard}>
|
||||
<form>
|
||||
|
@ -35,9 +46,9 @@ export default function Devtools({
|
|||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<pre><code><small>{JSON.stringify(mainEntityJson, null, 2)}</small></code></pre>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<Tiles
|
||||
eventsChannel={eventsChannel}
|
||||
|
|
|
@ -11,6 +11,7 @@ export default function Tiles({eventsChannel}) {
|
|||
const wrapperRef = useRef();
|
||||
const imageRef = useRef();
|
||||
const imageRect = useRect(imageRef);
|
||||
const [indices, setIndices] = useState([]);
|
||||
const [selection, setSelection] = useState({x: 0, y: 0, w: 1, h: 1});
|
||||
const [moveStart, setMoveStart] = useState();
|
||||
const [layer, setLayer] = useState(0);
|
||||
|
@ -35,17 +36,19 @@ export default function Tiles({eventsChannel}) {
|
|||
if (at.x < 0 || at.y < 0 || at.x >= area.x || at.y >= area.y) {
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
client.send({
|
||||
type: 'AdminAction',
|
||||
payload: {
|
||||
type: 'paint',
|
||||
value: {
|
||||
brush,
|
||||
layer,
|
||||
stamp: {
|
||||
at,
|
||||
data: stamp,
|
||||
},
|
||||
}
|
||||
client.send({
|
||||
type: 'AdminAction',
|
||||
payload: {type: 'paint', value: payload},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
eventsChannel.addListener('click', onClick);
|
||||
|
@ -64,6 +67,7 @@ export default function Tiles({eventsChannel}) {
|
|||
const {sourceJson, tileSize} = TileLayers.layer(0);
|
||||
const {w, h} = sourceJson.meta.size;
|
||||
const factor = (imageRect?.width ?? 1) / w;
|
||||
const wt = w / tileSize.x;
|
||||
return (
|
||||
<div className={styles.tiles}>
|
||||
<form>
|
||||
|
@ -125,8 +129,14 @@ export default function Tiles({eventsChannel}) {
|
|||
const y = Math.floor((c.y - top) / (tileSize.y * factor));
|
||||
setMoveStart({x, y});
|
||||
setSelection({x, y, w: 1, h: 1});
|
||||
setStamp([[y * wt + x]]);
|
||||
setIndices([y * wt + x]);
|
||||
}}
|
||||
onMouseMove={(event) => {
|
||||
if (0 === event.buttons) {
|
||||
setMoveStart();
|
||||
return;
|
||||
}
|
||||
if (!moveStart) {
|
||||
return;
|
||||
}
|
||||
|
@ -145,21 +155,39 @@ export default function Tiles({eventsChannel}) {
|
|||
);
|
||||
const mx = Math.min(sx, x);
|
||||
const my = Math.min(sy, y);
|
||||
setSelection({x: mx, y: my, w: Math.abs(sx - x) + 1, h: Math.abs(sy - y) + 1});
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
setMoveStart();
|
||||
const sw = Math.abs(sx - x) + 1;
|
||||
const sh = Math.abs(sy - y) + 1;
|
||||
const newSelection = {
|
||||
x: mx,
|
||||
y: my,
|
||||
w: sw,
|
||||
h: sh,
|
||||
};
|
||||
if (
|
||||
selection.x === newSelection.x
|
||||
&& selection.y === newSelection.y
|
||||
&& selection.w === newSelection.w
|
||||
&& selection.h === newSelection.h
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setSelection(newSelection);
|
||||
const stamp = [];
|
||||
const {x, y, w: sw, h: sh} = selection;
|
||||
const tw = w / tileSize.x;
|
||||
for (let iy = 0; iy < sh; ++iy) {
|
||||
const row = [];
|
||||
for (let ix = 0; ix < sw; ++ix) {
|
||||
row.push((y + iy) * tw + x + ix);
|
||||
row.push((my + iy) * wt + mx + ix);
|
||||
}
|
||||
stamp.push(row);
|
||||
}
|
||||
setStamp(stamp);
|
||||
const indices = [];
|
||||
for (let sy = 0; sy < newSelection.h; ++sy) {
|
||||
for (let sx = 0; sx < newSelection.w; ++sx) {
|
||||
indices.push(((newSelection.y + sy) * wt) + (newSelection.x + sx));
|
||||
}
|
||||
}
|
||||
setIndices(indices);
|
||||
}}
|
||||
className={styles.selectionWrapper}
|
||||
ref={wrapperRef}
|
||||
|
@ -179,6 +207,25 @@ export default function Tiles({eventsChannel}) {
|
|||
src={TileLayers.layer(0).source.replace('.json', '.png')}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={styles.status}
|
||||
>
|
||||
<code className={styles.selectionStatus}>
|
||||
Sel:
|
||||
{' {'}
|
||||
{selection.x}{', '}
|
||||
{selection.y}{', '}
|
||||
{selection.w}{', '}
|
||||
{selection.h}
|
||||
{'}'}
|
||||
</code>
|
||||
<code>
|
||||
Idx:
|
||||
{' ['}
|
||||
{indices.join(', ')}
|
||||
{']'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -28,3 +28,16 @@
|
|||
background-color: #ffffff44;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.status {
|
||||
align-items: end;
|
||||
display: flex;
|
||||
font-size: 0.6rem;
|
||||
gap: 32px;
|
||||
justify-content: space-between;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.selectionStatus {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
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';
|
||||
|
@ -32,60 +28,13 @@ 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 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({applyFilters, scale}) {
|
||||
const [ecs] = useEcs();
|
||||
const [entities, setEntities] = useState({});
|
||||
const [filters, setFilters] = 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');
|
||||
|
@ -131,12 +80,6 @@ export default function Ecs({applyFilters, 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) {
|
||||
|
@ -149,6 +92,13 @@ export default function Ecs({applyFilters, scale}) {
|
|||
}
|
||||
setEntities(updatedEntities);
|
||||
}, [ecs, entities, mainEntity]);
|
||||
useEffect(() => {
|
||||
setFilters(
|
||||
applyFilters
|
||||
? [night]
|
||||
: [],
|
||||
);
|
||||
}, [applyFilters, night])
|
||||
if (!ecs || !mainEntity) {
|
||||
return false;
|
||||
}
|
||||
|
@ -162,10 +112,6 @@ export default function Ecs({applyFilters, scale}) {
|
|||
const {TileLayers, Water: WaterEcs} = ecs.get(1);
|
||||
const layer0 = TileLayers.layer(0);
|
||||
const layer1 = TileLayers.layer(1);
|
||||
const filters = [];
|
||||
if (applyFilters && night) {
|
||||
filters.push(night);
|
||||
}
|
||||
const [cx, cy] = [
|
||||
Math.round((Camera.x * scale) - RESOLUTION.x / 2),
|
||||
Math.round((Camera.y * scale) - RESOLUTION.y / 2),
|
||||
|
@ -185,7 +131,6 @@ export default function Ecs({applyFilters, scale}) {
|
|||
/>
|
||||
{layer1 && (
|
||||
<TileLayer
|
||||
mask={mask}
|
||||
filters={filters}
|
||||
tileLayer={layer1}
|
||||
/>
|
||||
|
@ -193,7 +138,6 @@ export default function Ecs({applyFilters, scale}) {
|
|||
</Container>
|
||||
{WaterEcs && (
|
||||
<Water
|
||||
mask={mask}
|
||||
tileLayer={layer0}
|
||||
water={WaterEcs.water}
|
||||
/>
|
||||
|
|
|
@ -40,7 +40,7 @@ export default function Entities({entities, filters}) {
|
|||
const isHighlightedInteraction = id == willInteractWith;
|
||||
renderables.push(
|
||||
<Entity
|
||||
filters={filters.concat(isHighlightedInteraction ? interactionFilters : [])}
|
||||
filters={isHighlightedInteraction ? interactionFilters : null}
|
||||
entity={entities[id]}
|
||||
key={id}
|
||||
/>
|
||||
|
@ -48,6 +48,7 @@ export default function Entities({entities, filters}) {
|
|||
}
|
||||
return (
|
||||
<Container
|
||||
filters={filters}
|
||||
sortableChildren
|
||||
>
|
||||
{renderables}
|
||||
|
|
|
@ -3,42 +3,64 @@ import {PixiComponent} from '@pixi/react';
|
|||
import '@pixi/spritesheet'; // NECESSARY!
|
||||
import {CompositeTilemap} from '@pixi/tilemap';
|
||||
|
||||
import {CHUNK_SIZE} from '@/constants.js';
|
||||
import {useAsset} from '@/context/assets.js';
|
||||
|
||||
const TileLayerInternal = PixiComponent('TileLayer', {
|
||||
create: () => {
|
||||
create: ({tileLayer}) => {
|
||||
const container = new Container();
|
||||
container.addChild(new CompositeTilemap());
|
||||
const cy = Math.ceil(tileLayer.area.y / CHUNK_SIZE);
|
||||
const cx = Math.ceil(tileLayer.area.x / CHUNK_SIZE);
|
||||
for (let iy = 0; iy < cy; ++iy) {
|
||||
for (let ix = 0; ix < cx; ++ix) {
|
||||
const tilemap = new CompositeTilemap();
|
||||
tilemap.x = tileLayer.tileSize.x * CHUNK_SIZE * ix;
|
||||
tilemap.y = tileLayer.tileSize.y * CHUNK_SIZE * iy;
|
||||
container.addChild(tilemap);
|
||||
}
|
||||
}
|
||||
return container;
|
||||
},
|
||||
applyProps: (container, {mask: oldMask, tileLayer: oldTileLayer}, props) => {
|
||||
const {asset, mask, tileLayer} = props;
|
||||
applyProps: (container, {tileLayer: oldTileLayer}, props) => {
|
||||
const {asset, 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];
|
||||
for (const i in tileLayer.$$chunks) {
|
||||
if (!oldTileLayer || oldTileLayer.$$chunks[i] !== tileLayer.$$chunks[i]) {
|
||||
const tilemap = container.children[i];
|
||||
tilemap.clear();
|
||||
let i = 0;
|
||||
for (let y = 0; y < tileLayer.area.y; ++y) {
|
||||
for (let x = 0; x < tileLayer.area.x; ++x) {
|
||||
tilemap.tile(textures[`${extless}/${tileLayer.data[i++]}`], tileLayer.tileSize.x * x, tileLayer.tileSize.y * y);
|
||||
const ax = Math.ceil(tileLayer.area.x / CHUNK_SIZE);
|
||||
const cy = Math.floor(i / ax);
|
||||
const cx = i % ax;
|
||||
for (let y = 0; y < CHUNK_SIZE; ++y) {
|
||||
for (let x = 0; x < CHUNK_SIZE; ++x) {
|
||||
const ty = (cy * CHUNK_SIZE) + y;
|
||||
const tx = (cx * CHUNK_SIZE) + x;
|
||||
if (
|
||||
tx < 0
|
||||
|| ty < 0
|
||||
|| tx >= tileLayer.area.x
|
||||
|| ty >= tileLayer.area.y
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
tilemap.tile(
|
||||
textures[`${extless}/${tileLayer.data[ty * tileLayer.area.x + tx]}`],
|
||||
tileLayer.tileSize.x * x,
|
||||
tileLayer.tileSize.y * y,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export default function TileLayer(props) {
|
||||
const {mask, tileLayer} = props;
|
||||
const {tileLayer} = props;
|
||||
const asset = useAsset(tileLayer.source);
|
||||
if (!asset) {
|
||||
return false;
|
||||
|
@ -48,7 +70,6 @@ export default function TileLayer(props) {
|
|||
<TileLayerInternal
|
||||
{...props}
|
||||
asset={asset}
|
||||
mask={mask}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -98,7 +98,7 @@ export default function Ui({disconnected}) {
|
|||
switch (payload) {
|
||||
case '-':
|
||||
if ('keyDown' === type) {
|
||||
setScale((scale) => scale > 1 ? scale - 1 : 0.666);
|
||||
setScale((scale) => scale > 1 ? scale - 1 : 1);
|
||||
}
|
||||
break;
|
||||
case '=':
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import alea from 'alea';
|
||||
import {createNoise2D as createSimplexNoise2D} from 'simplex-noise';
|
||||
|
||||
export const {
|
||||
abs,
|
||||
acos,
|
||||
|
@ -110,6 +113,14 @@ export function clamp(n, min, max) {
|
|||
return Math.max(min, Math.min(max, n));
|
||||
}
|
||||
|
||||
export function createNoise2D(seed = 0) {
|
||||
return createSimplexNoise2D(createRandom(seed));
|
||||
}
|
||||
|
||||
export function createRandom(seed = 0) {
|
||||
return alea(seed);
|
||||
}
|
||||
|
||||
export function distance({x: lx, y: ly}, {x: rx, y: ry}) {
|
||||
const xd = lx - rx;
|
||||
const yd = ly - ry;
|
||||
|
@ -167,6 +178,50 @@ export function floodwalk2D(eligible, data, {x, y, w, h}, {diagonal = false} = {
|
|||
return points;
|
||||
}
|
||||
|
||||
export class Generator {
|
||||
|
||||
constructor({
|
||||
calculate,
|
||||
children = [],
|
||||
covers,
|
||||
size: {w, h},
|
||||
}) {
|
||||
this.calculate = calculate;
|
||||
this.children = children;
|
||||
this.covers = covers;
|
||||
this.size = {w, h};
|
||||
this.matrix = new Array(w * h).fill(0);
|
||||
}
|
||||
|
||||
compute(i, position) {
|
||||
if (!this.covers(position)) {
|
||||
return;
|
||||
}
|
||||
this.matrix[i] = this.calculate(position);
|
||||
if (!this.children) {
|
||||
return;
|
||||
}
|
||||
for (let j = 0; j < this.children.length; j++) {
|
||||
this.children[j].compute(i, position);
|
||||
}
|
||||
}
|
||||
|
||||
generate() {
|
||||
const {w, h} = this.size;
|
||||
let i = 0;
|
||||
const position = {x: 0, y: 0};
|
||||
for (let y = 0; y < h; ++y) {
|
||||
for (let x = 0; x < w; ++x) {
|
||||
this.compute(i++, position);
|
||||
position.x += 1;
|
||||
}
|
||||
position.x -= w;
|
||||
position.y += 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function intersects(l, r) {
|
||||
if (l.x0 > r.x1) return false;
|
||||
if (l.y0 > r.y1) return false;
|
||||
|
|
56
app/util/noise.js
Normal file
56
app/util/noise.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
// import {Texture} from '@pixi/core';
|
||||
// import {Sprite} from '@pixi/sprite';
|
||||
|
||||
import {bresenham, createNoise2D, TAU} from '@/util/math.js';
|
||||
|
||||
const simplex2D = createNoise2D();
|
||||
|
||||
// TODO - 3d noise around a circumference of length polygon perimeter
|
||||
|
||||
export 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 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);
|
||||
}
|
||||
|
43
package-lock.json
generated
43
package-lock.json
generated
|
@ -6,7 +6,6 @@
|
|||
"": {
|
||||
"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",
|
||||
|
@ -20,6 +19,7 @@
|
|||
"@remix-run/node": "^2.9.2",
|
||||
"@remix-run/react": "^2.9.2",
|
||||
"acorn": "^8.12.0",
|
||||
"alea": "^1.0.1",
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.18.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
|
@ -30,6 +30,7 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-tabs": "^6.0.2",
|
||||
"simplex-noise": "^4.0.1",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -51,6 +52,7 @@
|
|||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"image-size": "^1.1.1",
|
||||
"storybook": "^8.1.6",
|
||||
"vite": "^5.1.0",
|
||||
"vitest": "^1.6.0"
|
||||
|
@ -3026,11 +3028,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||
},
|
||||
"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",
|
||||
|
@ -7452,6 +7449,11 @@
|
|||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/alea": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz",
|
||||
"integrity": "sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA=="
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
|
@ -11355,6 +11357,21 @@
|
|||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/image-size": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz",
|
||||
"integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"queue": "6.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"image-size": "bin/image-size.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.x"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
|
@ -15196,6 +15213,15 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
|
@ -16488,6 +16514,11 @@
|
|||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/simplex-noise": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-4.0.1.tgz",
|
||||
"integrity": "sha512-zl/+bdSqW7HJOQ0oDbxrNYaF4F5ik0i7M6YOYmEoIJNtg16NpvWaTTM1Y7oV/7T0jFljawLgYPS81Uu2rsfo1A=="
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
"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",
|
||||
|
@ -27,6 +26,7 @@
|
|||
"@remix-run/node": "^2.9.2",
|
||||
"@remix-run/react": "^2.9.2",
|
||||
"acorn": "^8.12.0",
|
||||
"alea": "^1.0.1",
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.18.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
|
@ -37,6 +37,7 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-tabs": "^6.0.2",
|
||||
"simplex-noise": "^4.0.1",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -58,6 +59,7 @@
|
|||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"image-size": "^1.1.1",
|
||||
"storybook": "^8.1.6",
|
||||
"vite": "^5.1.0",
|
||||
"vitest": "^1.6.0"
|
||||
|
|
1
public/assets/ambient/blossom.json
Normal file
1
public/assets/ambient/blossom.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"frames":{"":{"frame":{"x":0,"y":0,"w":128,"h":160},"spriteSourceSize":{"x":0,"y":0,"w":128,"h":160},"sourceSize":{"w":128,"h":160}}},"meta":{"format":"RGBA8888","image":"./blossom.png","scale":1,"size":{"w":128,"h":160}}}
|
BIN
public/assets/ambient/blossom.png
Normal file
BIN
public/assets/ambient/blossom.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
1
public/assets/ambient/flower.json
Normal file
1
public/assets/ambient/flower.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"frames":{"":{"frame":{"x":0,"y":0,"w":32,"h":32},"spriteSourceSize":{"x":0,"y":0,"w":32,"h":32},"sourceSize":{"w":32,"h":32}}},"meta":{"format":"RGBA8888","image":"./flower.png","scale":1,"size":{"w":32,"h":32}}}
|
BIN
public/assets/ambient/flower.png
Normal file
BIN
public/assets/ambient/flower.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
1
public/assets/ambient/shrub.json
Normal file
1
public/assets/ambient/shrub.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"frames":{"":{"frame":{"x":0,"y":0,"w":32,"h":32},"spriteSourceSize":{"x":0,"y":0,"w":32,"h":32},"sourceSize":{"w":32,"h":32}}},"meta":{"format":"RGBA8888","image":"./shrub.png","scale":1,"size":{"w":32,"h":32}}}
|
BIN
public/assets/ambient/shrub.png
Normal file
BIN
public/assets/ambient/shrub.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
1
public/assets/ambient/tree.json
Normal file
1
public/assets/ambient/tree.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"frames":{"":{"frame":{"x":0,"y":0,"w":96,"h":160},"spriteSourceSize":{"x":0,"y":0,"w":96,"h":160},"sourceSize":{"w":96,"h":160}}},"meta":{"format":"RGBA8888","image":"./tree.png","scale":1,"size":{"w":96,"h":160}}}
|
BIN
public/assets/ambient/tree.png
Normal file
BIN
public/assets/ambient/tree.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -6,16 +6,25 @@ import {basename, dirname, extname, join} from 'node:path';
|
|||
import imageSize from 'image-size';
|
||||
|
||||
const tileset = process.argv[2];
|
||||
const w = parseInt(process.argv[3]);
|
||||
const h = parseInt(process.argv[4]);
|
||||
let w = parseInt(process.argv[3]);
|
||||
let h = parseInt(process.argv[4]);
|
||||
|
||||
const {width, height} = imageSize(tileset);
|
||||
|
||||
if (0 === w) {
|
||||
w = width;
|
||||
}
|
||||
if (0 === h) {
|
||||
h = height;
|
||||
}
|
||||
|
||||
const total = (width / w) * (height / h);
|
||||
|
||||
const json = {
|
||||
frames: {},
|
||||
meta: {
|
||||
format: 'RGBA8888',
|
||||
image: tileset,
|
||||
image: ['.', basename(tileset)].join('/'),
|
||||
scale: 1,
|
||||
size: {w: width, h: height},
|
||||
},
|
||||
|
@ -26,7 +35,7 @@ const extlessPath = join(dirname(tileset), basename(tileset, extname(tileset)));
|
|||
let i = 0;
|
||||
for (let y = 0; y < height; y += h) {
|
||||
for (let x = 0; x < width; x += w) {
|
||||
json.frames[join(extlessPath, `${i++}`)] = {
|
||||
json.frames[1 === total ? '' : join(extlessPath, `${i++}`)] = {
|
||||
frame: {x, y, w, h},
|
||||
spriteSourceSize: {x: 0, y: 0, w, h},
|
||||
sourceSize: {w, h},
|
||||
|
|
Loading…
Reference in New Issue
Block a user