Compare commits
45 Commits
9853edec44
...
6b60877711
Author | SHA1 | Date | |
---|---|---|---|
|
6b60877711 | ||
|
1b35b03eb1 | ||
|
2204c2cdbf | ||
|
066abec937 | ||
|
0639c825a9 | ||
|
3bbaf83140 | ||
|
85e252e423 | ||
|
0bfd4b3a9f | ||
|
9e82b3a5c1 | ||
|
0cdabd0858 | ||
|
fec717cbe1 | ||
|
1c88b32bad | ||
|
889498b243 | ||
|
9d9f94a7cc | ||
|
997ef691ca | ||
|
0cb5cdbe1f | ||
|
46cff307b1 | ||
|
e6f54c3694 | ||
|
f328ce4ccb | ||
|
b5019153f3 | ||
|
0588dbf6c2 | ||
|
da51111106 | ||
|
ffa391fcef | ||
|
c1bdae1c8c | ||
|
46f0a0cc07 | ||
|
92c85e57d3 | ||
|
493042b913 | ||
|
f673833a1d | ||
|
512c5a470a | ||
|
39e7082a28 | ||
|
3106788ca7 | ||
|
55f915d4a1 | ||
|
f1d3ad6a6d | ||
|
73b7a9e0a5 | ||
|
5492ec32bd | ||
|
9d176c2930 | ||
|
b20795137e | ||
|
749a3356c3 | ||
|
6a45622b9d | ||
|
e2c0ec7638 | ||
|
946a06e78a | ||
|
c24adc47a8 | ||
|
b137b91ced | ||
|
acaa930fe1 | ||
|
cd8a933a5b |
|
@ -61,6 +61,9 @@ module.exports = {
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
|
'jsx-a11y/label-has-associated-control': [2, {
|
||||||
|
controlComponents: ['SliderText'],
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -71,7 +74,6 @@ module.exports = {
|
||||||
'.eslintrc.cjs',
|
'.eslintrc.cjs',
|
||||||
'server.js',
|
'server.js',
|
||||||
'vite.config.js',
|
'vite.config.js',
|
||||||
'public/assets/tileset.js',
|
|
||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
|
@ -81,7 +83,7 @@ module.exports = {
|
||||||
// Assets
|
// Assets
|
||||||
{
|
{
|
||||||
files: [
|
files: [
|
||||||
'public/assets/**/*.js',
|
'resources/**/*.js',
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'no-undef': 0,
|
'no-undef': 0,
|
||||||
|
|
|
@ -1,24 +1,14 @@
|
||||||
import {LRUCache} from 'lru-cache';
|
|
||||||
|
|
||||||
import Components from '@/ecs/components/index.js';
|
import Components from '@/ecs/components/index.js';
|
||||||
import Ecs from '@/ecs/ecs.js';
|
import Ecs from '@/ecs/ecs.js';
|
||||||
import Systems from '@/ecs/systems/index.js';
|
import Systems from '@/ecs/systems/index.js';
|
||||||
import {withResolvers} from '@/util/promise.js';
|
import {readAsset} from '@/util/resources.js';
|
||||||
|
|
||||||
const cache = new LRUCache({
|
|
||||||
max: 128,
|
|
||||||
});
|
|
||||||
|
|
||||||
class PredictionEcs extends Ecs {
|
class PredictionEcs extends Ecs {
|
||||||
async readAsset(uri) {
|
async readAsset(path) {
|
||||||
if (!cache.has(uri)) {
|
const resource = await readAsset(path);
|
||||||
const {promise, resolve, reject} = withResolvers();
|
return resource
|
||||||
cache.set(uri, promise);
|
? resource
|
||||||
fetch(uri)
|
: new ArrayBuffer(0);
|
||||||
.then((response) => resolve(response.ok ? response.arrayBuffer() : new ArrayBuffer(0)))
|
|
||||||
.catch(reject);
|
|
||||||
}
|
|
||||||
return cache.get(uri);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +39,6 @@ function applyClientActions(elapsed) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
action.steps.push(elapsed);
|
|
||||||
}
|
}
|
||||||
if (1 === action.finished) {
|
if (1 === action.finished) {
|
||||||
if (!Controlled.locked) {
|
if (!Controlled.locked) {
|
||||||
|
@ -65,20 +54,17 @@ function applyClientActions(elapsed) {
|
||||||
}
|
}
|
||||||
action.finished = 2;
|
action.finished = 2;
|
||||||
}
|
}
|
||||||
|
if (!action.ack) {
|
||||||
|
action.stop += elapsed;
|
||||||
|
}
|
||||||
if (action.ack && 2 === action.finished) {
|
if (action.ack && 2 === action.finished) {
|
||||||
action.steps.shift();
|
action.start += elapsed;
|
||||||
if (0 === action.steps.length) {
|
if (action.start >= action.stop) {
|
||||||
finished.push(id);
|
finished.push(id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let leap = 0;
|
ecs.predict(main, action.stop - action.start);
|
||||||
for (const step of action.steps) {
|
|
||||||
leap += step;
|
|
||||||
}
|
|
||||||
if (leap > 0) {
|
|
||||||
ecs.predict(main, leap);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const id of finished) {
|
for (const id of finished) {
|
||||||
actions.delete(id);
|
actions.delete(id);
|
||||||
|
@ -108,10 +94,13 @@ onmessage = async (event) => {
|
||||||
pending.delete(packet.payload.type);
|
pending.delete(packet.payload.type);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
const now = performance.now() / 1000;
|
||||||
const tx = {
|
const tx = {
|
||||||
action: packet.payload,
|
action: packet.payload,
|
||||||
ack: false,finished: 0,
|
ack: false,
|
||||||
steps: [],
|
finished: 0,
|
||||||
|
start: now,
|
||||||
|
stop: now,
|
||||||
};
|
};
|
||||||
packet.payload.ack = Math.random();
|
packet.payload.ack = Math.random();
|
||||||
pending.set(packet.payload.type, packet.payload.ack);
|
pending.set(packet.payload.type, packet.payload.ack);
|
||||||
|
@ -152,9 +141,20 @@ onmessage = async (event) => {
|
||||||
const authoritative = structuredClone(main.toNet(main));
|
const authoritative = structuredClone(main.toNet(main));
|
||||||
applyClientActions(packet.payload.elapsed);
|
applyClientActions(packet.payload.elapsed);
|
||||||
if (ecs.diff[mainEntityId]) {
|
if (ecs.diff[mainEntityId]) {
|
||||||
packet.payload.ecs[mainEntityId] = ecs.diff[mainEntityId];
|
packet.payload.ecs[mainEntityId] ??= {};
|
||||||
|
ecs.mergeDiff(
|
||||||
|
packet.payload.ecs[mainEntityId],
|
||||||
|
ecs.diff[mainEntityId],
|
||||||
|
);
|
||||||
|
const reset = {};
|
||||||
|
for (const componentName in ecs.diff[mainEntityId]) {
|
||||||
|
reset[componentName] = {};
|
||||||
|
for (const property in ecs.diff[mainEntityId][componentName]) {
|
||||||
|
reset[componentName][property] = authoritative[componentName][property];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await ecs.apply({[mainEntityId]: reset});
|
||||||
}
|
}
|
||||||
await ecs.apply({[mainEntityId]: authoritative});
|
|
||||||
}
|
}
|
||||||
ecs.setClean();
|
ecs.setClean();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default class Alive extends Component {
|
||||||
}
|
}
|
||||||
static properties = {
|
static properties = {
|
||||||
deathScript: {
|
deathScript: {
|
||||||
defaultValue: '/assets/misc/death-default.js',
|
defaultValue: '/resources/misc/death-default.js',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
health: {type: 'uint32'},
|
health: {type: 'uint32'},
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import Component from '@/ecs/component.js';
|
import Component from '@/ecs/component.js';
|
||||||
|
|
||||||
|
import {distribute} from '@/util/inventory.js';
|
||||||
|
|
||||||
class ItemProxy {
|
class ItemProxy {
|
||||||
constructor(Component, instance, slot) {
|
constructor(Component, instance, slot) {
|
||||||
this.Component = Component;
|
this.Component = Component;
|
||||||
|
@ -94,24 +96,35 @@ class ItemProxy {
|
||||||
get label() {
|
get label() {
|
||||||
return this.json.label;
|
return this.json.label;
|
||||||
}
|
}
|
||||||
|
get maximumStack() {
|
||||||
|
return this.json.maximumStack ?? Infinity;
|
||||||
|
}
|
||||||
get projection() {
|
get projection() {
|
||||||
return this.json.projection;
|
return this.json.projection;
|
||||||
}
|
}
|
||||||
|
get price() {
|
||||||
|
return this.json.price;
|
||||||
|
}
|
||||||
get qty() {
|
get qty() {
|
||||||
return this.instance.slots[this.slot].qty;
|
return this.instance.slots[this.slot].qty;
|
||||||
}
|
}
|
||||||
set qty(qty) {
|
set qty(qty) {
|
||||||
const {instance} = this;
|
const {instance} = this;
|
||||||
if (qty <= 0) {
|
if (qty === this.qty) {
|
||||||
this.Component.markChange(instance.entity, 'cleared', {[this.slot]: true});
|
return;
|
||||||
delete instance.slots[this.slot];
|
}
|
||||||
delete instance.$$items[this.slot];
|
else if (qty <= 0) {
|
||||||
|
instance.clear(this.slot);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
const difference = qty - instance.slots[this.slot].qty;
|
||||||
instance.slots[this.slot].qty = qty;
|
instance.slots[this.slot].qty = qty;
|
||||||
this.Component.markChange(instance.entity, 'qtyUpdated', {[this.slot]: qty});
|
this.Component.markChange(instance.entity, 'qtyUpdated', {[this.slot]: difference});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
get source() {
|
||||||
|
return this.instance.slots[this.slot].source;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Inventory extends Component {
|
export default class Inventory extends Component {
|
||||||
|
@ -121,6 +134,7 @@ export default class Inventory extends Component {
|
||||||
const {$$items, slots} = instance;
|
const {$$items, slots} = instance;
|
||||||
if (cleared) {
|
if (cleared) {
|
||||||
for (const slot in cleared) {
|
for (const slot in cleared) {
|
||||||
|
delete $$items[slot];
|
||||||
delete slots[slot];
|
delete slots[slot];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +147,7 @@ export default class Inventory extends Component {
|
||||||
}
|
}
|
||||||
if (qtyUpdated) {
|
if (qtyUpdated) {
|
||||||
for (const slot in qtyUpdated) {
|
for (const slot in qtyUpdated) {
|
||||||
slots[slot].qty = qtyUpdated[slot];
|
slots[slot].qty += qtyUpdated[slot];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (swapped) {
|
if (swapped) {
|
||||||
|
@ -167,8 +181,77 @@ export default class Inventory extends Component {
|
||||||
instanceFromSchema() {
|
instanceFromSchema() {
|
||||||
const Instance = super.instanceFromSchema();
|
const Instance = super.instanceFromSchema();
|
||||||
const Component = this;
|
const Component = this;
|
||||||
|
const {ecs} = Component;
|
||||||
return class InventoryInstance extends Instance {
|
return class InventoryInstance extends Instance {
|
||||||
$$items = {};
|
$$items = {};
|
||||||
|
clear(slot) {
|
||||||
|
Component.markChange(this.entity, 'cleared', {[slot]: true});
|
||||||
|
delete this.slots[slot];
|
||||||
|
delete this.$$items[slot];
|
||||||
|
}
|
||||||
|
async distribute(slot, potentialDestinations) {
|
||||||
|
const {slots} = this;
|
||||||
|
if (!slots[slot]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const destinations = potentialDestinations
|
||||||
|
.filter(([entityId, destination]) => {
|
||||||
|
const {Inventory} = ecs.get(entityId);
|
||||||
|
return (
|
||||||
|
!Inventory.slots[destination]
|
||||||
|
|| Inventory.slots[destination].source === slots[slot].source
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const {maximumStack, qty, source} = this.item(slot);
|
||||||
|
const item = {
|
||||||
|
maximumStack,
|
||||||
|
qty,
|
||||||
|
};
|
||||||
|
const qtys = distribute(
|
||||||
|
item,
|
||||||
|
destinations.map(([entityId, destination]) => {
|
||||||
|
const {Inventory} = ecs.get(entityId);
|
||||||
|
if (entityId == Inventory.entity && destination === slot) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Inventory.slots[destination]
|
||||||
|
? Inventory.slots[destination].qty
|
||||||
|
: 0;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
for (let i = 0; i < qtys.length; ++i) {
|
||||||
|
const qty = qtys[i];
|
||||||
|
const [entityId, destination] = destinations[i];
|
||||||
|
const {Inventory} = ecs.get(entityId);
|
||||||
|
// create new
|
||||||
|
if (!Inventory.slots[destination]) {
|
||||||
|
if (qty > 0) {
|
||||||
|
const stack = {qty, source};
|
||||||
|
Inventory.slots[destination] = {...stack};
|
||||||
|
Inventory.$$items[destination] = new ItemProxy(Component, Inventory, destination);
|
||||||
|
Component.markChange(entityId, 'given', {[destination]: {...stack}});
|
||||||
|
await Inventory.$$items[destination].load(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update qty of existing
|
||||||
|
else if (Inventory.slots[destination].qty !== qty) {
|
||||||
|
Inventory.item(destination).qty = qty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// handle source separately if not also destination
|
||||||
|
if (
|
||||||
|
!destinations.find(([entityId, destination]) => {
|
||||||
|
return entityId == this.entity && destination === slot;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
if (0 === item.qty) {
|
||||||
|
this.clear(slot);
|
||||||
|
}
|
||||||
|
else if (item.qty !== qty) {
|
||||||
|
this.item(slot).qty = item.qty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
item(slot) {
|
item(slot) {
|
||||||
return this.$$items[slot];
|
return this.$$items[slot];
|
||||||
}
|
}
|
||||||
|
@ -177,16 +260,16 @@ export default class Inventory extends Component {
|
||||||
for (let slot = 1; slot < 11; ++slot) {
|
for (let slot = 1; slot < 11; ++slot) {
|
||||||
if (slots[slot]?.source === stack.source) {
|
if (slots[slot]?.source === stack.source) {
|
||||||
slots[slot].qty += stack.qty;
|
slots[slot].qty += stack.qty;
|
||||||
Component.markChange(this.entity, 'qtyUpdated', {[slot]: slots[slot].qty});
|
Component.markChange(this.entity, 'qtyUpdated', {[slot]: stack.qty});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let slot = 1; slot < 11; ++slot) {
|
for (let slot = 1; slot < 11; ++slot) {
|
||||||
if (!slots[slot]) {
|
if (!slots[slot]) {
|
||||||
slots[slot] = stack;
|
slots[slot] = {...stack};
|
||||||
this.$$items[slot] = new ItemProxy(Component, this, slot);
|
this.$$items[slot] = new ItemProxy(Component, this, slot);
|
||||||
await this.$$items[slot].load();
|
Component.markChange(this.entity, 'given', {[slot]: {...stack}});
|
||||||
Component.markChange(this.entity, 'given', {[slot]: slots[slot]});
|
await this.$$items[slot].load(stack.source);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,17 +283,25 @@ export default class Inventory extends Component {
|
||||||
if (undefined === slots[l]) {
|
if (undefined === slots[l]) {
|
||||||
delete slots[l];
|
delete slots[l];
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
$$items[l].slot = l;
|
||||||
|
}
|
||||||
if (undefined === otherSlots[r]) {
|
if (undefined === otherSlots[r]) {
|
||||||
delete otherSlots[r];
|
delete otherSlots[r];
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
$$items[r].slot = r;
|
||||||
|
}
|
||||||
Component.markChange(this.entity, 'swapped', [[l, OtherInventory.entity, r]]);
|
Component.markChange(this.entity, 'swapped', [[l, OtherInventory.entity, r]]);
|
||||||
|
if (this.entity !== OtherInventory.entity) {
|
||||||
Component.markChange(OtherInventory.entity, 'swapped', [[r, this.entity, l]]);
|
Component.markChange(OtherInventory.entity, 'swapped', [[r, this.entity, l]]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
toNet(recipient, data) {
|
toNet(recipient, data) {
|
||||||
if (recipient.id !== this.entity && this !== recipient.Player.openInventory) {
|
if (recipient.id !== this.entity && this !== recipient.Player.openInventory) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return super.toNet(data);
|
return super.toNet(recipient, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -223,10 +314,6 @@ export default class Inventory extends Component {
|
||||||
mergeDiff(original, update) {
|
mergeDiff(original, update) {
|
||||||
const merged = {
|
const merged = {
|
||||||
...original,
|
...original,
|
||||||
qtyUpdated: {
|
|
||||||
...original.qtyUpdated,
|
|
||||||
...update.qtyUpdated,
|
|
||||||
},
|
|
||||||
cleared: {
|
cleared: {
|
||||||
...original.cleared,
|
...original.cleared,
|
||||||
...update.cleared,
|
...update.cleared,
|
||||||
|
@ -240,6 +327,14 @@ export default class Inventory extends Component {
|
||||||
...(update.swapped || []),
|
...(update.swapped || []),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
if (update.qtyUpdated) {
|
||||||
|
if (!merged.qtyUpdated) {
|
||||||
|
merged.qtyUpdated = {};
|
||||||
|
}
|
||||||
|
for (const slot in update.qtyUpdated) {
|
||||||
|
merged.qtyUpdated[slot] = (merged.qtyUpdated[slot] ?? 0) + update.qtyUpdated[slot];
|
||||||
|
}
|
||||||
|
}
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
static properties = {
|
static properties = {
|
||||||
|
@ -248,7 +343,7 @@ export default class Inventory extends Component {
|
||||||
value: {
|
value: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
quantity: {type: 'uint16'},
|
qty: {type: 'uint16'},
|
||||||
source: {type: 'string'},
|
source: {type: 'string'},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
3
app/ecs/components/shop.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import Component from '@/ecs/component.js';
|
||||||
|
|
||||||
|
export default class Shop extends Component {}
|
|
@ -29,6 +29,7 @@ export default class Vulnerable extends Component {
|
||||||
const {Alive} = Component.ecs.get(this.entity);
|
const {Alive} = Component.ecs.get(this.entity);
|
||||||
if (Alive) {
|
if (Alive) {
|
||||||
switch (specification.type) {
|
switch (specification.type) {
|
||||||
|
case DamageTypes.HEALING:
|
||||||
case DamageTypes.PAIN: {
|
case DamageTypes.PAIN: {
|
||||||
Alive.acceptDamage(specification.amount);
|
Alive.acceptDamage(specification.amount);
|
||||||
}
|
}
|
||||||
|
|
7
app/ecs/components/wallet.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import Component from '@/ecs/component.js';
|
||||||
|
|
||||||
|
export default class Wallet extends Component {
|
||||||
|
static properties = {
|
||||||
|
gold: {type: 'uint32'},
|
||||||
|
};
|
||||||
|
}
|
|
@ -52,7 +52,7 @@ export default class Ecs {
|
||||||
promises.add(promise);
|
promises.add(promise);
|
||||||
promise.then(() => {
|
promise.then(() => {
|
||||||
promises.delete(promise);
|
promises.delete(promise);
|
||||||
if (!this.$$destructionDependencies.get(id)?.resolvers) {
|
if (0 === promises.size && !this.$$destructionDependencies.get(id)?.resolvers) {
|
||||||
this.$$destructionDependencies.delete(id);
|
this.$$destructionDependencies.delete(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -397,16 +397,20 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
// Otherwise, merge.
|
// Otherwise, merge.
|
||||||
else {
|
else {
|
||||||
for (const componentName in components) {
|
this.mergeDiff(this.diff[entityId], components);
|
||||||
this.diff[entityId][componentName] = false === components[componentName]
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeDiff(l, r) {
|
||||||
|
for (const componentName in r) {
|
||||||
|
l[componentName] = false === r[componentName]
|
||||||
? false
|
? false
|
||||||
: this.Components[componentName].mergeDiff(
|
: this.Components[componentName].mergeDiff(
|
||||||
this.diff[entityId][componentName] || {},
|
l[componentName] || {},
|
||||||
components[componentName],
|
r[componentName],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
predict(entity, elapsed) {
|
predict(entity, elapsed) {
|
||||||
for (const systemName in this.Systems) {
|
for (const systemName in this.Systems) {
|
||||||
|
|
|
@ -13,15 +13,9 @@ import { renderToPipeableStream } from "react-dom/server";
|
||||||
|
|
||||||
const ABORT_DELAY = 5_000;
|
const ABORT_DELAY = 5_000;
|
||||||
|
|
||||||
export async function websocket(server, viteDevServer) {
|
export async function handleUpgrade(request, socket, head) {
|
||||||
if (viteDevServer) {
|
const {handleUpgrade} = await import('./server/websocket.js');
|
||||||
const {createViteRuntime} = await import('vite');
|
handleUpgrade(request, socket, head);
|
||||||
const runtime = await createViteRuntime(viteDevServer);
|
|
||||||
(await runtime.executeEntrypoint('/app/server/websocket.js')).default(server);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
(await import('./server/websocket.js')).default(server);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function handleRequest(
|
export default function handleRequest(
|
||||||
|
@ -120,6 +114,11 @@ function handleBrowserRequest(
|
||||||
const stream = createReadableStreamFromReadable(body);
|
const stream = createReadableStreamFromReadable(body);
|
||||||
|
|
||||||
responseHeaders.set("Content-Type", "text/html");
|
responseHeaders.set("Content-Type", "text/html");
|
||||||
|
// security
|
||||||
|
// responseHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
|
||||||
|
// responseHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||||
|
// responseHeaders.set("Cross-Origin-Resource-Policy", "same-site");
|
||||||
|
// responseHeaders.set("Content-Security-Policy", "default-src 'self'");
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
new Response(stream, {
|
new Response(stream, {
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import {LRUCache} from 'lru-cache';
|
|
||||||
|
|
||||||
import Ecs from '@/ecs/ecs.js';
|
import Ecs from '@/ecs/ecs.js';
|
||||||
import {withResolvers} from '@/util/promise.js';
|
import {readAsset} from '@/util/resources.js';
|
||||||
|
|
||||||
const cache = new LRUCache({
|
|
||||||
max: 128,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default class ClientEcs extends Ecs {
|
export default class ClientEcs extends Ecs {
|
||||||
constructor(specification) {
|
constructor(specification) {
|
||||||
|
@ -19,16 +13,10 @@ export default class ClientEcs extends Ecs {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async readAsset(uri) {
|
async readAsset(path) {
|
||||||
if (!cache.has(uri)) {
|
const resource = await readAsset(path);
|
||||||
const {promise, resolve, reject} = withResolvers();
|
return resource
|
||||||
cache.set(uri, promise);
|
? resource
|
||||||
fetch(new URL(uri, location.origin))
|
: new ArrayBuffer(0);
|
||||||
.then(async (response) => {
|
|
||||||
resolve(response.ok ? response.arrayBuffer() : new ArrayBuffer(0));
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
}
|
|
||||||
return cache.get(uri);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
43
app/react/components/dev/slider-text.jsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import {useState} from 'react';
|
||||||
|
|
||||||
|
function SliderText({
|
||||||
|
defaultValue,
|
||||||
|
max,
|
||||||
|
min,
|
||||||
|
name,
|
||||||
|
onChange,
|
||||||
|
step,
|
||||||
|
}) {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
value={value}
|
||||||
|
max={max}
|
||||||
|
min={min}
|
||||||
|
name={name}
|
||||||
|
onChange={({currentTarget: {value}}) => {
|
||||||
|
setValue(value);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
step={step}
|
||||||
|
type="range"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={value}
|
||||||
|
name={name}
|
||||||
|
onChange={({currentTarget: {value}}) => {
|
||||||
|
setValue(value);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SliderText;
|
|
@ -1,6 +1,7 @@
|
||||||
import {memo} from 'react';
|
import {memo} from 'react';
|
||||||
|
|
||||||
import styles from './bag.module.css';
|
import styles from './bag.module.css';
|
||||||
|
import gridStyles from './grid.module.css';
|
||||||
|
|
||||||
import Grid from './grid.jsx';
|
import Grid from './grid.jsx';
|
||||||
|
|
||||||
|
@ -8,8 +9,9 @@ import Grid from './grid.jsx';
|
||||||
* Inventory bag. 10-40 slots of inventory.
|
* Inventory bag. 10-40 slots of inventory.
|
||||||
*/
|
*/
|
||||||
function Bag({
|
function Bag({
|
||||||
|
highlighted,
|
||||||
isInventoryOpen,
|
isInventoryOpen,
|
||||||
onActivate,
|
onSlotMouseDown,
|
||||||
slots,
|
slots,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
@ -17,11 +19,20 @@ function Bag({
|
||||||
className={styles.bag}
|
className={styles.bag}
|
||||||
style={isInventoryOpen ? {transition: 'opacity 50ms'} : {opacity: 0, left: '-440px'}}
|
style={isInventoryOpen ? {transition: 'opacity 50ms'} : {opacity: 0, left: '-440px'}}
|
||||||
>
|
>
|
||||||
|
<style>{`
|
||||||
|
.${styles.bag} .${gridStyles.highlighted} {
|
||||||
|
background-color: rgba(255, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
.${styles.bag} .${gridStyles.highlighted} button {
|
||||||
|
border: 2.5px dashed rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
<Grid
|
<Grid
|
||||||
color="rgba(02, 02, 28, 0.6)"
|
color="rgba(02, 02, 28, 0.6)"
|
||||||
columns={10}
|
columns={10}
|
||||||
|
highlighted={highlighted}
|
||||||
label="Bag"
|
label="Bag"
|
||||||
onActivate={onActivate}
|
onSlotMouseDown={onSlotMouseDown}
|
||||||
slots={slots}
|
slots={slots}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,7 +28,6 @@ function damageHue(type) {
|
||||||
|
|
||||||
class Damage {
|
class Damage {
|
||||||
elapsed = 0;
|
elapsed = 0;
|
||||||
hue = [0, 30];
|
|
||||||
offsetX = 0;
|
offsetX = 0;
|
||||||
offsetY = 0;
|
offsetY = 0;
|
||||||
step = 0;
|
step = 0;
|
||||||
|
@ -111,7 +110,7 @@ class Damage {
|
||||||
easeInOutExpo(
|
easeInOutExpo(
|
||||||
Math.abs((this.elapsed % 0.3) - 0.15) / 0.15,
|
Math.abs((this.elapsed % 0.3) - 0.15) / 0.15,
|
||||||
this.hueStart,
|
this.hueStart,
|
||||||
this.hueEnd,
|
this.hueEnd - this.hueStart,
|
||||||
1,
|
1,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
40
app/react/components/dom/date.jsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import {useState} from 'react';
|
||||||
|
|
||||||
|
import styles from './date.module.css';
|
||||||
|
|
||||||
|
export function Date({season, date}) {
|
||||||
|
const seasonLabel = (() => {
|
||||||
|
switch (season) {
|
||||||
|
case 0: return 'Spring';
|
||||||
|
case 1: return 'Summer';
|
||||||
|
case 2: return 'Autumn';
|
||||||
|
case 3: return 'Winter';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
const dateLabel = (() => {
|
||||||
|
if (date >= 10 && date < 13) {
|
||||||
|
return <><span className={styles.number}>{date + 1}</span>th</>;
|
||||||
|
}
|
||||||
|
switch (date % 10) {
|
||||||
|
case 0: return <><span className={styles.number}>{date + 1}</span>st</>;
|
||||||
|
case 1: return <><span className={styles.number}>{date + 1}</span>nd</>;
|
||||||
|
case 2: return <><span className={styles.number}>{date + 1}</span>rd</>;
|
||||||
|
default: return <><span className={styles.number}>{date + 1}</span>th</>;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return (
|
||||||
|
<div className={styles.date}>
|
||||||
|
{seasonLabel}
|
||||||
|
|
||||||
|
{dateLabel}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConnectedDate() {
|
||||||
|
const [season, setSeason] = useState(0);
|
||||||
|
const [date, setDate] = useState(0);
|
||||||
|
return <Date season={season} date={date} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConnectedDate;
|
11
app/react/components/dom/date.module.css
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.date {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||||
|
font-size: 16px;
|
||||||
|
text-shadow: 1px 0 0 black, 0 1px 0 black, -1px 0 0 black, 0 -1px 0 black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
}
|
15
app/react/components/dom/datetime.jsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import Date from './date.jsx';
|
||||||
|
import Time from './time.jsx';
|
||||||
|
|
||||||
|
import styles from './datetime.module.css';
|
||||||
|
|
||||||
|
function DateTime() {
|
||||||
|
return (
|
||||||
|
<div className={styles.datetime}>
|
||||||
|
<Date />
|
||||||
|
<Time />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DateTime;
|
12
app/react/components/dom/datetime.module.css
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.datetime {
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 4px;
|
||||||
|
div:first-child:not(:last-child):after {
|
||||||
|
content: ', ';
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import {useCallback, useState} from 'react';
|
||||||
|
|
||||||
import {usePacket} from '@/react/context/client.js';
|
import {usePacket} from '@/react/context/client.js';
|
||||||
import {useEcsTick} from '@/react/context/ecs.js';
|
import {useEcsTick} from '@/react/context/ecs.js';
|
||||||
|
import {RESOLUTION} from '@/util/constants.js';
|
||||||
import {parseLetters} from '@/util/dialogue.js';
|
import {parseLetters} from '@/util/dialogue.js';
|
||||||
|
|
||||||
import Damages from './damages.jsx';
|
import Damages from './damages.jsx';
|
||||||
|
@ -14,6 +15,10 @@ export default function Entities({
|
||||||
setChatMessages,
|
setChatMessages,
|
||||||
}) {
|
}) {
|
||||||
const [entities, setEntities] = useState({});
|
const [entities, setEntities] = useState({});
|
||||||
|
const translatedCamera = {
|
||||||
|
x: Math.round((camera.x * scale) - RESOLUTION.x / 2),
|
||||||
|
y: Math.round((camera.y * scale) - RESOLUTION.y / 2),
|
||||||
|
};
|
||||||
usePacket('EcsChange', async () => {
|
usePacket('EcsChange', async () => {
|
||||||
setEntities({});
|
setEntities({});
|
||||||
});
|
});
|
||||||
|
@ -98,7 +103,7 @@ export default function Entities({
|
||||||
for (const id in entities) {
|
for (const id in entities) {
|
||||||
renderables.push(
|
renderables.push(
|
||||||
<Entity
|
<Entity
|
||||||
camera={camera}
|
camera={translatedCamera}
|
||||||
entity={entities[id]}
|
entity={entities[id]}
|
||||||
key={id}
|
key={id}
|
||||||
scale={scale}
|
scale={scale}
|
||||||
|
@ -109,8 +114,8 @@ export default function Entities({
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
translate: `
|
translate: `
|
||||||
calc(-1px * ${camera.x})
|
calc(-1px * ${translatedCamera.x})
|
||||||
calc(-1px * ${camera.y})
|
calc(-1px * ${translatedCamera.y})
|
||||||
`,
|
`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
|
import {memo} from 'react';
|
||||||
|
|
||||||
import styles from './external.module.css';
|
import styles from './external.module.css';
|
||||||
|
import gridStyles from './grid.module.css';
|
||||||
|
|
||||||
import Grid from './grid.jsx';
|
import Grid from './grid.jsx';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* External inventory.
|
* External inventory.
|
||||||
*/
|
*/
|
||||||
export default function External({
|
function External({
|
||||||
|
highlighted,
|
||||||
isInventoryOpen,
|
isInventoryOpen,
|
||||||
onActivate,
|
onSlotMouseDown,
|
||||||
slots,
|
slots,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
@ -15,13 +19,24 @@ export default function External({
|
||||||
className={styles.external}
|
className={styles.external}
|
||||||
style={isInventoryOpen ? {transition: 'opacity 50ms'} : {opacity: 0, top: '450px'}}
|
style={isInventoryOpen ? {transition: 'opacity 50ms'} : {opacity: 0, top: '450px'}}
|
||||||
>
|
>
|
||||||
|
<style>{`
|
||||||
|
.${styles.external} .${gridStyles.highlighted} {
|
||||||
|
background-color: rgba(0, 255, 0, 0.4);
|
||||||
|
}
|
||||||
|
.${styles.external} .${gridStyles.highlighted} button {
|
||||||
|
border: 2.5px dashed rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
<Grid
|
<Grid
|
||||||
color="rgba(57, 02, 02, 0.6)"
|
color="rgba(57, 02, 02, 0.6)"
|
||||||
columns={10}
|
columns={10}
|
||||||
|
highlighted={highlighted}
|
||||||
label="Chest"
|
label="Chest"
|
||||||
onActivate={onActivate}
|
onSlotMouseDown={onSlotMouseDown}
|
||||||
slots={slots}
|
slots={slots}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(External);
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
.external {
|
.external {
|
||||||
left: 20px;
|
--nothing: 0;
|
||||||
opacity: 1;
|
|
||||||
position: absolute;
|
|
||||||
top: 274px;
|
|
||||||
transition: top 150ms, opacity 200ms;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,21 @@ export default function Grid({
|
||||||
active = -1,
|
active = -1,
|
||||||
color,
|
color,
|
||||||
columns,
|
columns,
|
||||||
|
highlighted,
|
||||||
label,
|
label,
|
||||||
onActivate,
|
onSlotMouseDown,
|
||||||
|
onSlotMouseMove,
|
||||||
|
onSlotMouseUp,
|
||||||
slots,
|
slots,
|
||||||
}) {
|
}) {
|
||||||
const Slots = slots.map((slot, i) => (
|
const Slots = slots.map((slot, i) => (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
[styles.slot, active === i && styles.active]
|
[
|
||||||
|
styles.slot,
|
||||||
|
active === i && styles.active,
|
||||||
|
highlighted && highlighted.includes(i) && styles.highlighted,
|
||||||
|
]
|
||||||
.filter(Boolean).join(' ')
|
.filter(Boolean).join(' ')
|
||||||
}
|
}
|
||||||
key={i}
|
key={i}
|
||||||
|
@ -23,26 +30,21 @@ export default function Grid({
|
||||||
<Slot
|
<Slot
|
||||||
icon={slot?.icon}
|
icon={slot?.icon}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
onActivate(i)
|
onSlotMouseDown?.(i, event)
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onMouseMove={(event) => {
|
||||||
|
onSlotMouseMove?.(i, event)
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
onMouseUp={(event) => {
|
onMouseUp={(event) => {
|
||||||
|
onSlotMouseUp?.(i, event)
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
onDragOver={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
}}
|
|
||||||
onDragStart={(event) => {
|
onDragStart={(event) => {
|
||||||
if (!slot) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
|
||||||
event.dataTransfer.setData('silphius/item', i);
|
|
||||||
onActivate(i);
|
|
||||||
}}
|
|
||||||
onDrop={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
onActivate(i);
|
|
||||||
}}
|
}}
|
||||||
|
temporary={slot?.temporary}
|
||||||
qty={slot?.qty}
|
qty={slot?.qty}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -43,3 +43,7 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlighted {
|
||||||
|
--nothing: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -11,8 +11,8 @@ import Grid from './grid.jsx';
|
||||||
function Hotbar({
|
function Hotbar({
|
||||||
active,
|
active,
|
||||||
hotbarIsHidden,
|
hotbarIsHidden,
|
||||||
onActivate,
|
|
||||||
slots,
|
slots,
|
||||||
|
...rest
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -23,14 +23,20 @@ function Hotbar({
|
||||||
.${styles.hotbar} .${gridStyles.label} {
|
.${styles.hotbar} .${gridStyles.label} {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.${styles.hotbar} .${gridStyles.highlighted} {
|
||||||
|
background-color: rgba(255, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
.${styles.hotbar} .${gridStyles.highlighted} button {
|
||||||
|
border: 2.5px dashed rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<Grid
|
<Grid
|
||||||
active={active}
|
active={active}
|
||||||
color="rgba(02, 02, 57, 0.6)"
|
color="rgba(02, 02, 57, 0.6)"
|
||||||
columns={10}
|
columns={10}
|
||||||
label={slots[active] && slots[active].label}
|
label={slots[active] && slots[active].label}
|
||||||
onActivate={onActivate}
|
|
||||||
slots={slots}
|
slots={slots}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,7 +11,9 @@ export default function Slot({
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDrop,
|
onDrop,
|
||||||
onMouseDown,
|
onMouseDown,
|
||||||
|
onMouseMove,
|
||||||
onMouseUp,
|
onMouseUp,
|
||||||
|
temporary = false,
|
||||||
qty = 1,
|
qty = 1,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
@ -27,6 +29,7 @@ export default function Slot({
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}}
|
}}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
|
onMouseMove={onMouseMove}
|
||||||
onMouseUp={onMouseUp}
|
onMouseUp={onMouseUp}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -36,7 +39,11 @@ export default function Slot({
|
||||||
{qty > 1 && (
|
{qty > 1 && (
|
||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
[styles.qty, `q-${Math.round(Math.log10(qty))}`]
|
[
|
||||||
|
styles.qty,
|
||||||
|
temporary && styles.temporary,
|
||||||
|
`q-${Math.floor(Math.log10(qty))}`,
|
||||||
|
]
|
||||||
.filter(Boolean).join(' ')
|
.filter(Boolean).join(' ')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -28,24 +28,34 @@
|
||||||
|
|
||||||
.qty {
|
.qty {
|
||||||
bottom: calc(var(--space) * 0.125);
|
bottom: calc(var(--space) * 0.125);
|
||||||
font-family: monospace;
|
color: white;
|
||||||
|
font-family: joystix;
|
||||||
font-size: calc(var(--space) * 2);
|
font-size: calc(var(--space) * 2);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: calc(var(--space) * -0.25);
|
right: calc(var(--space) * -0.5);
|
||||||
text-shadow:
|
text-shadow:
|
||||||
0px -1px 0px white,
|
0px -1px 0px black,
|
||||||
1px 0px 0px white,
|
1px 0px 0px black,
|
||||||
0px 1px 0px white,
|
0px 1px 0px black,
|
||||||
-1px 0px 0px white
|
-1px 0px 0px black
|
||||||
;
|
;
|
||||||
&:global(.q-2) {
|
&:global(.q-2) {
|
||||||
font-size: calc(var(--space) * 1.75);
|
|
||||||
}
|
|
||||||
&:global(.q-3) {
|
|
||||||
font-size: calc(var(--space) * 1.5);
|
font-size: calc(var(--space) * 1.5);
|
||||||
}
|
}
|
||||||
&:global(.q-4) {
|
&:global(.q-3) {
|
||||||
font-size: calc(var(--space) * 1.25);
|
font-size: calc(var(--space) * 1.25);
|
||||||
}
|
}
|
||||||
|
&:global(.q-4) {
|
||||||
|
font-size: calc(var(--space) * 1.125);
|
||||||
|
}
|
||||||
|
&.temporary {
|
||||||
|
color: gold;
|
||||||
|
text-shadow:
|
||||||
|
0px -1px 0px black,
|
||||||
|
1px 0px 0px black,
|
||||||
|
0px 1px 0px black,
|
||||||
|
-1px 0px 0px black
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
43
app/react/components/dom/time.jsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import {memo, useCallback, useState} from 'react';
|
||||||
|
|
||||||
|
import {useEcsTick} from '@/react/context/ecs.js';
|
||||||
|
|
||||||
|
import styles from './time.module.css';
|
||||||
|
|
||||||
|
export function Time({hour, minute}) {
|
||||||
|
const formatter = new Intl.DateTimeFormat(undefined, {timeStyle: 'short'});
|
||||||
|
const {hour12} = formatter.resolvedOptions();
|
||||||
|
const formatted = formatter.format(new Date(2012, 11, 20, hour, minute));
|
||||||
|
return (
|
||||||
|
<div className={styles.time}>
|
||||||
|
{
|
||||||
|
hour12
|
||||||
|
? <>{formatted.slice(0, -3)}<span className={styles.meridian}>{formatted.slice(-2)}</span></>
|
||||||
|
: formatted
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemoTime = memo(Time);
|
||||||
|
|
||||||
|
function ConnectedTime() {
|
||||||
|
const [hour, setHour] = useState(-1);
|
||||||
|
const [minute, setMinute] = useState(0);
|
||||||
|
useEcsTick(useCallback((payload, ecs) => {
|
||||||
|
const master = ecs.get(1);
|
||||||
|
if (!master) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {Time: {hour}} = ecs.get(1);
|
||||||
|
const wholeHour = Math.floor(hour);
|
||||||
|
setHour(wholeHour);
|
||||||
|
setMinute(Math.floor(60 * (hour - wholeHour)));
|
||||||
|
}, []));
|
||||||
|
if (-1 === hour) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return <MemoTime hour={hour} minute={minute} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConnectedTime;
|
14
app/react/components/dom/time.module.css
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
.time {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
text-shadow: 1px 0 0 black, 0 1px 0 black, -1px 0 0 black, 0 -1px 0 black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meridian {
|
||||||
|
align-self: self-end;
|
||||||
|
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||||
|
font-size: 10px;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
86
app/react/components/dom/trade.jsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import styles from './trade.module.css';
|
||||||
|
|
||||||
|
function reducePrice(r, item) {
|
||||||
|
return r + item.price * item.qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Trade({
|
||||||
|
isInventoryOpen,
|
||||||
|
losing: losingUnfiltered,
|
||||||
|
onTradeAccepted,
|
||||||
|
gaining: gainingUnfiltered,
|
||||||
|
wallet,
|
||||||
|
}) {
|
||||||
|
const losing = losingUnfiltered.filter(Boolean);
|
||||||
|
const gaining = gainingUnfiltered.filter(Boolean);
|
||||||
|
const nop = 0 === losing.length && 0 === gaining.length;
|
||||||
|
let earn = losing.reduce(reducePrice, 0) - gaining.reduce(reducePrice, 0);
|
||||||
|
const canAfford = -earn <= wallet;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.trade}
|
||||||
|
style={isInventoryOpen ? {transition: 'opacity 50ms'} : {opacity: 0, top: '450px'}}
|
||||||
|
>
|
||||||
|
<div className={styles.tradeInner}>
|
||||||
|
{
|
||||||
|
nop
|
||||||
|
? (
|
||||||
|
<div className={styles.nop}>Let's trade!</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<ul className={styles.summary}>
|
||||||
|
{losing.length > 0 && (
|
||||||
|
<li className={styles.lose}>
|
||||||
|
Shop receives:
|
||||||
|
{' '}
|
||||||
|
<ul className={styles.items}>
|
||||||
|
{losing.map(({label, qty}, i) => (
|
||||||
|
<li key={i}>{label} x<span className={styles.qty}>{qty}</span></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{gaining.length > 0 && (
|
||||||
|
<li className={styles.gain}>
|
||||||
|
You receive:
|
||||||
|
{' '}
|
||||||
|
<ul className={styles.items}>
|
||||||
|
{gaining.map(({label, qty}, i) => (
|
||||||
|
<li key={i}>{label} x<span className={styles.qty}>{qty}</span></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li className={styles.subtotal}>
|
||||||
|
{
|
||||||
|
earn > 0
|
||||||
|
? (
|
||||||
|
<span className={styles.cost}>You <span className={styles.make}>earn <span className={styles.amount}>{earn}</span></span><span className={styles.gold}>🄶</span></span>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
earn < 0
|
||||||
|
? <span className={styles.cost}>You <span className={styles.pay}>pay <span className={styles.amount}>{-earn}</span></span><span className={styles.gold}>🄶</span></span>
|
||||||
|
: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
className={styles.agree}
|
||||||
|
disabled={!canAfford}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onTradeAccepted(event);
|
||||||
|
}}
|
||||||
|
title={canAfford ? "Finish the transaction" : `Transaction requires ${-earn}g but you only have ${wallet}g`}
|
||||||
|
>
|
||||||
|
{canAfford ? "Agree" : "Can't afford"}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Trade;
|
129
app/react/components/dom/trade.module.css
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
.trade {
|
||||||
|
border: 2.5px solid #444444;
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin: 16px 40px 0 8px;
|
||||||
|
text-shadow:
|
||||||
|
1px 0 0 black,
|
||||||
|
0 1px 0 black,
|
||||||
|
-1px 0 0 black,
|
||||||
|
0 -1px 0 black
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tradeInner {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
border: 2.5px solid #999999;
|
||||||
|
display: flex;
|
||||||
|
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||||
|
height: 100%;
|
||||||
|
padding: 2.5px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.agree {
|
||||||
|
background-color: rgba(255, 90, 0, 1);
|
||||||
|
border: 2.5px solid #cccccc;
|
||||||
|
box-shadow: inset 0 0 10px #666666;
|
||||||
|
color: white;
|
||||||
|
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 5px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2.5px;
|
||||||
|
right: 2.5px;
|
||||||
|
text-shadow:
|
||||||
|
1px 0 0 black,
|
||||||
|
0 1px 0 black,
|
||||||
|
-1px 0 0 black,
|
||||||
|
0 -1px 0 black
|
||||||
|
;
|
||||||
|
transition: box-shadow 200ms;
|
||||||
|
&:disabled {
|
||||||
|
background-color: #333333;
|
||||||
|
border-color: #777777;
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
&:not(:disabled):hover {
|
||||||
|
box-shadow: inset 0 0 10px #222222;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2.5px;
|
||||||
|
width: 100%;
|
||||||
|
> li {
|
||||||
|
padding: 2.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gain {
|
||||||
|
background-color: rgba(0, 255, 0, 0.5);
|
||||||
|
border: 2.5px solid rgba(0, 255, 0, 0.5);
|
||||||
|
max-height: 40.5px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lose {
|
||||||
|
background-color: rgba(255, 0, 0, 0.5);
|
||||||
|
border: 2.5px solid rgba(255, 0, 0, 0.5);
|
||||||
|
max-height: 40.5px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
display: inline;
|
||||||
|
li {
|
||||||
|
display: inline;
|
||||||
|
&:not(:last-child):after {
|
||||||
|
content: ', ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.make {
|
||||||
|
color: rgb(0, 255, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gold {
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty {
|
||||||
|
font-family: Joystix;
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2.5px;
|
||||||
|
left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-size: 1.5em;
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nop {
|
||||||
|
align-self: center;
|
||||||
|
font-size: 2em;
|
||||||
|
padding: 1em;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
17
app/react/components/dom/wallet.jsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import styles from './wallet.module.css';
|
||||||
|
|
||||||
|
function Wallet({gold}) {
|
||||||
|
const goldString = gold.toString();
|
||||||
|
const pad = ''.padStart(7 - goldString.length, '0');
|
||||||
|
return (
|
||||||
|
<span className={styles.wallet}>
|
||||||
|
<span className={styles.number}>
|
||||||
|
<span className={styles.pad}>{pad}</span>
|
||||||
|
<span className={styles.amount}>{goldString}</span>
|
||||||
|
</span>
|
||||||
|
<span className={styles.gold}>🄶</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Wallet;
|
19
app/react/components/dom/wallet.module.css
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
.wallet {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
text-shadow: 1px 0 0 black, 0 1px 0 black, -1px 0 0 black, 0 -1px 0 black;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gold {
|
||||||
|
color: gold;
|
||||||
|
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
font-family: Joystix;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad {
|
||||||
|
color: #555555;
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import TargetingGrid from './targeting-grid.jsx';
|
||||||
import TileLayer from './tile-layer.jsx';
|
import TileLayer from './tile-layer.jsx';
|
||||||
import Water from './water.jsx';
|
import Water from './water.jsx';
|
||||||
|
|
||||||
export default function Ecs({camera, monopolizers, particleWorker, scale}) {
|
export default function Ecs({monopolizers, particleWorker}) {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
const mainEntityRef = useMainEntity();
|
const mainEntityRef = useMainEntity();
|
||||||
const [layers, setLayers] = useState([]);
|
const [layers, setLayers] = useState([]);
|
||||||
|
@ -120,17 +120,22 @@ export default function Ecs({camera, monopolizers, particleWorker, scale}) {
|
||||||
}
|
}
|
||||||
if (entity) {
|
if (entity) {
|
||||||
const {Direction, Position, Wielder} = entity;
|
const {Direction, Position, Wielder} = entity;
|
||||||
setPosition(Position.toJSON());
|
setPosition((position) => {
|
||||||
setProjected(Wielder.activeItem()?.project(Position.tile, Direction.quantize(4)));
|
return position.x !== Position.x || position.y !== Position.y
|
||||||
|
? Position.toJSON()
|
||||||
|
: position;
|
||||||
|
});
|
||||||
|
setProjected((projected) => {
|
||||||
|
const newProjected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4));
|
||||||
|
return JSON.stringify(projected) !== JSON.stringify(newProjected)
|
||||||
|
? newProjected
|
||||||
|
: projected;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [app.ambientLight, mainEntityRef]);
|
}, [app.ambientLight, mainEntityRef]);
|
||||||
useEcsTick(onEcsTick);
|
useEcsTick(onEcsTick);
|
||||||
return (
|
return (
|
||||||
<Container
|
<>
|
||||||
scale={scale}
|
|
||||||
x={-camera.x}
|
|
||||||
y={-camera.y}
|
|
||||||
>
|
|
||||||
<Container>
|
<Container>
|
||||||
{layers.map((layer, i) => (
|
{layers.map((layer, i) => (
|
||||||
<TileLayer
|
<TileLayer
|
||||||
|
@ -162,6 +167,6 @@ export default function Ecs({camera, monopolizers, particleWorker, scale}) {
|
||||||
monopolizers={monopolizers}
|
monopolizers={monopolizers}
|
||||||
particleWorker={particleWorker}
|
particleWorker={particleWorker}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,8 @@ export default class Entity {
|
||||||
this.debug.removeChild(this.interactionAabb);
|
this.debug.removeChild(this.interactionAabb);
|
||||||
}
|
}
|
||||||
this.interactionAabb = undefined;
|
this.interactionAabb = undefined;
|
||||||
|
this.diffuse.rotation = 0;
|
||||||
|
this.normals.rotation = 0;
|
||||||
}
|
}
|
||||||
setDebug(isDebugging) {
|
setDebug(isDebugging) {
|
||||||
if (isDebugging) {
|
if (isDebugging) {
|
||||||
|
@ -141,7 +143,9 @@ export default class Entity {
|
||||||
this.entity.Sprite.animation,
|
this.entity.Sprite.animation,
|
||||||
this.entity.Sprite.frame,
|
this.entity.Sprite.frame,
|
||||||
);
|
);
|
||||||
|
if (diffuse.texture !== texture) {
|
||||||
diffuse.texture = texture;
|
diffuse.texture = texture;
|
||||||
|
}
|
||||||
if (asset.data.meta.normals) {
|
if (asset.data.meta.normals) {
|
||||||
const {pathname} = new URL(
|
const {pathname} = new URL(
|
||||||
Sprite.source
|
Sprite.source
|
||||||
|
@ -157,17 +161,14 @@ export default class Entity {
|
||||||
this.entity.Sprite.animation,
|
this.entity.Sprite.animation,
|
||||||
this.entity.Sprite.frame,
|
this.entity.Sprite.frame,
|
||||||
);
|
);
|
||||||
|
if (normals.texture !== texture) {
|
||||||
normals.texture = texture;
|
normals.texture = texture;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.attached) {
|
|
||||||
const {diffuse, normals} = this;
|
|
||||||
diffuse.rotation = 0;
|
|
||||||
normals.rotation = 0;
|
|
||||||
}
|
|
||||||
if (Direction) {
|
if (Direction) {
|
||||||
const {diffuse, normals} = this;
|
const {diffuse, normals} = this;
|
||||||
if (!this.attached || 'direction' in Direction) {
|
if (!this.attached || 'direction' in Direction) {
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import {SCALE_MODES} from '@pixi/constants';
|
import {SCALE_MODES} from '@pixi/constants';
|
||||||
import {BaseTexture, extensions} from '@pixi/core';
|
import {BaseTexture, extensions} from '@pixi/core';
|
||||||
import {Stage as PixiStage} from '@pixi/react';
|
import {Container, Stage as PixiStage} from '@pixi/react';
|
||||||
import {createElement, useContext} from 'react';
|
import {createElement, forwardRef, memo, useContext} from 'react';
|
||||||
|
|
||||||
import AssetsContext from '@/react/context/assets.js';
|
import AssetsContext from '@/react/context/assets.js';
|
||||||
import ClientContext from '@/react/context/client.js';
|
import ClientContext from '@/react/context/client.js';
|
||||||
import DebugContext from '@/react/context/debug.js';
|
import DebugContext from '@/react/context/debug.js';
|
||||||
import EcsContext from '@/react/context/ecs.js';
|
import EcsContext from '@/react/context/ecs.js';
|
||||||
import MainEntityContext from '@/react/context/main-entity.js';
|
import MainEntityContext from '@/react/context/main-entity.js';
|
||||||
import RadiansContext from '@/react/context/radians.js';
|
|
||||||
import {RESOLUTION} from '@/util/constants.js';
|
import {RESOLUTION} from '@/util/constants.js';
|
||||||
|
|
||||||
import Ecs from './ecs.jsx';
|
import Ecs from './ecs.jsx';
|
||||||
|
@ -28,7 +27,6 @@ const Contexts = [
|
||||||
DebugContext,
|
DebugContext,
|
||||||
EcsContext,
|
EcsContext,
|
||||||
MainEntityContext,
|
MainEntityContext,
|
||||||
RadiansContext,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const ContextBridge = ({children, render}) => {
|
const ContextBridge = ({children, render}) => {
|
||||||
|
@ -57,7 +55,7 @@ export const Stage = ({children, ...props}) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Pixi({camera, monopolizers, particleWorker, scale}) {
|
function Pixi({monopolizers, particleWorker}, ref) {
|
||||||
return (
|
return (
|
||||||
<Stage
|
<Stage
|
||||||
className={styles.stage}
|
className={styles.stage}
|
||||||
|
@ -66,14 +64,17 @@ export default function Pixi({camera, monopolizers, particleWorker, scale}) {
|
||||||
options={{
|
options={{
|
||||||
background: 0x0,
|
background: 0x0,
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<Container
|
||||||
|
ref={ref}
|
||||||
>
|
>
|
||||||
<Ecs
|
<Ecs
|
||||||
camera={camera}
|
|
||||||
monopolizers={monopolizers}
|
monopolizers={monopolizers}
|
||||||
particleWorker={particleWorker}
|
particleWorker={particleWorker}
|
||||||
scale={scale}
|
|
||||||
/>
|
/>
|
||||||
|
</Container>
|
||||||
</Stage>
|
</Stage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default memo(forwardRef(Pixi));
|
||||||
|
|
|
@ -18,7 +18,7 @@ const TileLayerInternal = PixiComponent('TileLayer', {
|
||||||
},
|
},
|
||||||
applyProps: (container, {tileLayer: oldTileLayer}, props) => {
|
applyProps: (container, {tileLayer: oldTileLayer}, props) => {
|
||||||
const {asset, group, renderer, tileLayer} = props;
|
const {asset, group, renderer, tileLayer} = props;
|
||||||
const extless = tileLayer.source.slice('/assets/'.length, -'.json'.length);
|
const extless = tileLayer.source.slice('/resources/'.length, -'.json'.length);
|
||||||
const {textures} = asset;
|
const {textures} = asset;
|
||||||
if (tileLayer === oldTileLayer) {
|
if (tileLayer === oldTileLayer) {
|
||||||
return;
|
return;
|
||||||
|
@ -134,14 +134,14 @@ export default function TileLayer(props) {
|
||||||
<>
|
<>
|
||||||
<TileLayerInternal
|
<TileLayerInternal
|
||||||
{...props}
|
{...props}
|
||||||
asset={asset}
|
asset={normalsAsset}
|
||||||
group={deferredLighting.diffuseGroup}
|
group={deferredLighting.normalGroup}
|
||||||
renderer={renderer}
|
renderer={renderer}
|
||||||
/>
|
/>
|
||||||
<TileLayerInternal
|
<TileLayerInternal
|
||||||
{...props}
|
{...props}
|
||||||
asset={normalsAsset}
|
asset={asset}
|
||||||
group={deferredLighting.normalGroup}
|
group={deferredLighting.diffuseGroup}
|
||||||
renderer={renderer}
|
renderer={renderer}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -6,15 +6,18 @@ import {useEcs, useEcsTick} from '@/react/context/ecs.js';
|
||||||
import {useMainEntity} from '@/react/context/main-entity.js';
|
import {useMainEntity} from '@/react/context/main-entity.js';
|
||||||
import {RESOLUTION} from '@/util/constants.js';
|
import {RESOLUTION} from '@/util/constants.js';
|
||||||
import EventEmitter from '@/util/event-emitter.js';
|
import EventEmitter from '@/util/event-emitter.js';
|
||||||
|
import {distribute} from '@/util/inventory.js';
|
||||||
|
|
||||||
import addKeyListener from './add-key-listener.js';
|
import addKeyListener from './add-key-listener.js';
|
||||||
import ClientEcs from './client-ecs.js';
|
|
||||||
import Disconnected from './dom/disconnected.jsx';
|
import Disconnected from './dom/disconnected.jsx';
|
||||||
import Chat from './dom/chat/chat.jsx';
|
import Chat from './dom/chat/chat.jsx';
|
||||||
import Bag from './dom/bag.jsx';
|
import Bag from './dom/bag.jsx';
|
||||||
|
import DateTime from './dom/datetime.jsx';
|
||||||
import Dom from './dom/dom.jsx';
|
import Dom from './dom/dom.jsx';
|
||||||
import Entities from './dom/entities.jsx';
|
import Entities from './dom/entities.jsx';
|
||||||
import External from './dom/external.jsx';
|
import External from './dom/external.jsx';
|
||||||
|
import Trade from './dom/trade.jsx';
|
||||||
|
import Wallet from './dom/wallet.jsx';
|
||||||
import HotBar from './dom/hotbar.jsx';
|
import HotBar from './dom/hotbar.jsx';
|
||||||
import Pixi from './pixi/pixi.jsx';
|
import Pixi from './pixi/pixi.jsx';
|
||||||
import Devtools from './devtools.jsx';
|
import Devtools from './devtools.jsx';
|
||||||
|
@ -37,11 +40,14 @@ function Ui({disconnected}) {
|
||||||
const chatInputRef = useRef();
|
const chatInputRef = useRef();
|
||||||
const latestTick = useRef();
|
const latestTick = useRef();
|
||||||
const gameRef = useRef();
|
const gameRef = useRef();
|
||||||
|
const pixiRef = useRef();
|
||||||
const mainEntityRef = useMainEntity();
|
const mainEntityRef = useMainEntity();
|
||||||
const [, setDebug] = useDebug();
|
const [, setDebug] = useDebug();
|
||||||
const ecsRef = useEcs();
|
const ecsRef = useEcs();
|
||||||
const [showDisconnected, setShowDisconnected] = useState(false);
|
const [showDisconnected, setShowDisconnected] = useState(false);
|
||||||
const [bufferSlot, setBufferSlot] = useState();
|
const [bufferSlot, setBufferSlot] = useState();
|
||||||
|
const hadBufferSlot = useRef();
|
||||||
|
const [distributing, setDistributing] = useState({});
|
||||||
const [devtoolsIsOpen, setDevtoolsIsOpen] = useState(false);
|
const [devtoolsIsOpen, setDevtoolsIsOpen] = useState(false);
|
||||||
const ratio = (RESOLUTION.x * (devtoolsIsOpen ? 2 : 1)) / RESOLUTION.y;
|
const ratio = (RESOLUTION.x * (devtoolsIsOpen ? 2 : 1)) / RESOLUTION.y;
|
||||||
const [camera, setCamera] = useState({x: 0, y: 0});
|
const [camera, setCamera] = useState({x: 0, y: 0});
|
||||||
|
@ -49,8 +55,6 @@ function Ui({disconnected}) {
|
||||||
const [inventorySlots, setInventorySlots] = useState(emptySlots());
|
const [inventorySlots, setInventorySlots] = useState(emptySlots());
|
||||||
const [activeSlot, setActiveSlot] = useState(0);
|
const [activeSlot, setActiveSlot] = useState(0);
|
||||||
const [scale, setScale] = useState(2);
|
const [scale, setScale] = useState(2);
|
||||||
const [Components, setComponents] = useState();
|
|
||||||
const [Systems, setSystems] = useState();
|
|
||||||
const monopolizers = useRef([]);
|
const monopolizers = useRef([]);
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [chatIsOpen, setChatIsOpen] = useState(false);
|
const [chatIsOpen, setChatIsOpen] = useState(false);
|
||||||
|
@ -63,25 +67,11 @@ function Ui({disconnected}) {
|
||||||
const [isInventoryOpen, setIsInventoryOpen] = useState(false);
|
const [isInventoryOpen, setIsInventoryOpen] = useState(false);
|
||||||
const [externalInventory, setExternalInventory] = useState();
|
const [externalInventory, setExternalInventory] = useState();
|
||||||
const [externalInventorySlots, setExternalInventorySlots] = useState();
|
const [externalInventorySlots, setExternalInventorySlots] = useState();
|
||||||
|
const [gaining, setGaining] = useState([]);
|
||||||
|
const [losing, setLosing] = useState([]);
|
||||||
|
const [wallet, setWallet] = useState(0);
|
||||||
const [particleWorker, setParticleWorker] = useState();
|
const [particleWorker, setParticleWorker] = useState();
|
||||||
const refreshEcs = useCallback(() => {
|
const [trading, setTrading] = useState(false);
|
||||||
class ClientEcsPerf extends ClientEcs {
|
|
||||||
markChange() {}
|
|
||||||
}
|
|
||||||
ecsRef.current = new ClientEcsPerf({Components, Systems});
|
|
||||||
}, [ecsRef, Components, Systems]);
|
|
||||||
useEffect(() => {
|
|
||||||
async function setEcsStuff() {
|
|
||||||
const {default: Components} = await import('@/ecs/components/index.js');
|
|
||||||
const {default: Systems} = await import('@/ecs/systems/index.js');
|
|
||||||
setComponents(Components);
|
|
||||||
setSystems(Systems);
|
|
||||||
}
|
|
||||||
setEcsStuff();
|
|
||||||
}, []);
|
|
||||||
useEffect(() => {
|
|
||||||
refreshEcs();
|
|
||||||
}, [refreshEcs]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let handle;
|
let handle;
|
||||||
if (disconnected) {
|
if (disconnected) {
|
||||||
|
@ -236,20 +226,27 @@ function Ui({disconnected}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [client, keepHotbarOpen, setDebug]);
|
}, [client, keepHotbarOpen, mainEntityRef, setDebug]);
|
||||||
const onEcsChangePacket = useCallback(() => {
|
const onEcsChangePacket = useCallback(() => {
|
||||||
refreshEcs();
|
|
||||||
mainEntityRef.current = undefined;
|
mainEntityRef.current = undefined;
|
||||||
monopolizers.current = [];
|
monopolizers.current = [];
|
||||||
}, [refreshEcs, mainEntityRef]);
|
}, [
|
||||||
|
mainEntityRef,
|
||||||
|
]);
|
||||||
usePacket('EcsChange', onEcsChangePacket);
|
usePacket('EcsChange', onEcsChangePacket);
|
||||||
const onTickPacket = useCallback(async (payload, client) => {
|
const onTickPacket = useCallback(async (payload, client) => {
|
||||||
if (0 === Object.keys(payload.ecs).length) {
|
if (!ecsRef.current || 0 === Object.keys(payload.ecs).length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
latestTick.current = Promise.resolve(latestTick.current).then(async () => {
|
latestTick.current = Promise.resolve(latestTick.current).then(async () => {
|
||||||
|
try {
|
||||||
await ecsRef.current.apply(payload.ecs);
|
await ecsRef.current.apply(payload.ecs);
|
||||||
client.emitter.invoke(':Ecs', payload.ecs);
|
client.emitter.invoke(':Ecs', payload.ecs);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
ecsRef.current = undefined;
|
||||||
|
console.error('tick crash', error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [ecsRef]);
|
}, [ecsRef]);
|
||||||
usePacket('Tick', onTickPacket);
|
usePacket('Tick', onTickPacket);
|
||||||
|
@ -263,13 +260,26 @@ function Ui({disconnected}) {
|
||||||
if (update.MainEntity) {
|
if (update.MainEntity) {
|
||||||
mainEntityRef.current = id;
|
mainEntityRef.current = id;
|
||||||
}
|
}
|
||||||
|
if (update.Wallet && mainEntityRef.current === id) {
|
||||||
|
setWallet(update.Wallet.gold);
|
||||||
|
}
|
||||||
if (update.Inventory) {
|
if (update.Inventory) {
|
||||||
if (mainEntityRef.current === id) {
|
if (mainEntityRef.current === id) {
|
||||||
setBufferSlot(entity.Inventory.item(0));
|
setBufferSlot(entity.Inventory.item(0));
|
||||||
setHotbarSlots(() => {
|
setHotbarSlots(() => {
|
||||||
const newHotbarSlots = [];
|
const newHotbarSlots = [];
|
||||||
for (let i = 1; i < 11; ++i) {
|
for (let i = 1; i < 11; ++i) {
|
||||||
newHotbarSlots.push(entity.Inventory.item(i));
|
const item = entity.Inventory.item(i);
|
||||||
|
newHotbarSlots.push(
|
||||||
|
item
|
||||||
|
? {
|
||||||
|
icon: item.icon,
|
||||||
|
label: item.label,
|
||||||
|
price: item.price,
|
||||||
|
qty: item.qty,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return newHotbarSlots;
|
return newHotbarSlots;
|
||||||
});
|
});
|
||||||
|
@ -285,6 +295,7 @@ function Ui({disconnected}) {
|
||||||
newInventorySlots[i] = entity.Inventory.item(i);
|
newInventorySlots[i] = entity.Inventory.item(i);
|
||||||
}
|
}
|
||||||
setExternalInventory(entity.id)
|
setExternalInventory(entity.id)
|
||||||
|
setTrading(!!entity.Shop);
|
||||||
setExternalInventorySlots(newInventorySlots);
|
setExternalInventorySlots(newInventorySlots);
|
||||||
setIsInventoryOpen(true);
|
setIsInventoryOpen(true);
|
||||||
setHotbarIsHidden(false);
|
setHotbarIsHidden(false);
|
||||||
|
@ -295,6 +306,9 @@ function Ui({disconnected}) {
|
||||||
else if (update.Inventory.closed) {
|
else if (update.Inventory.closed) {
|
||||||
setExternalInventory();
|
setExternalInventory();
|
||||||
setExternalInventorySlots();
|
setExternalInventorySlots();
|
||||||
|
setGaining([]);
|
||||||
|
setLosing([]);
|
||||||
|
setTrading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mainEntityRef.current === id) {
|
if (mainEntityRef.current === id) {
|
||||||
|
@ -346,16 +360,15 @@ function Ui({disconnected}) {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
useEcsTick(onEcsTickAabbs);
|
useEcsTick(onEcsTickAabbs);
|
||||||
const onEcsTickCamera = useCallback((payload, ecs) => {
|
const onEcsTickCamera = useCallback((payload) => {
|
||||||
if (mainEntityRef.current) {
|
if (mainEntityRef.current && payload[mainEntityRef.current]?.Camera) {
|
||||||
const mainEntityEntity = ecs.get(mainEntityRef.current);
|
setCamera((camera) => ({
|
||||||
const x = Math.round((mainEntityEntity.Camera.x * scale) - RESOLUTION.x / 2);
|
x: camera.x,
|
||||||
const y = Math.round((mainEntityEntity.Camera.y * scale) - RESOLUTION.y / 2);
|
y: camera.y,
|
||||||
if (x !== camera.x || y !== camera.y) {
|
...payload[mainEntityRef.current].Camera,
|
||||||
setCamera({x, y});
|
}))
|
||||||
}
|
}
|
||||||
}
|
}, [mainEntityRef]);
|
||||||
}, [camera, mainEntityRef, scale]);
|
|
||||||
useEcsTick(onEcsTickCamera);
|
useEcsTick(onEcsTickCamera);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onContextMenu(event) {
|
function onContextMenu(event) {
|
||||||
|
@ -367,7 +380,7 @@ function Ui({disconnected}) {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
const computePosition = useCallback(({clientX, clientY}) => {
|
const computePosition = useCallback(({clientX, clientY}) => {
|
||||||
if (!gameRef.current || !mainEntityRef.current) {
|
if (!gameRef.current || !mainEntityRef.current || !ecsRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {top, left, width} = gameRef.current.getBoundingClientRect();
|
const {top, left, width} = gameRef.current.getBoundingClientRect();
|
||||||
|
@ -390,27 +403,207 @@ function Ui({disconnected}) {
|
||||||
mainEntityRef,
|
mainEntityRef,
|
||||||
scale,
|
scale,
|
||||||
]);
|
]);
|
||||||
const hotbarOnActivate = useCallback((i) => {
|
const hotbarOnSlotMouseDown = useCallback((i, event) => {
|
||||||
keepHotbarOpen();
|
keepHotbarOpen();
|
||||||
|
if (trading) {
|
||||||
|
const index = losing.indexOf(i + 1);
|
||||||
|
if (-1 === index) {
|
||||||
|
losing.push(i + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
losing.splice(index, 1);
|
||||||
|
}
|
||||||
|
setLosing([...losing]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
hadBufferSlot.current = bufferSlot;
|
||||||
|
switch (event.button) {
|
||||||
|
case 0:
|
||||||
|
if (bufferSlot) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
else {
|
||||||
client.send({
|
client.send({
|
||||||
type: 'Action',
|
type: 'Action',
|
||||||
payload: {type: 'swapSlots', value: [0, mainEntityRef.current, i + 1]},
|
payload: {type: 'swapSlots', value: [0, mainEntityRef.current, i + 1]},
|
||||||
});
|
});
|
||||||
}, [client, keepHotbarOpen, mainEntityRef]);
|
}
|
||||||
const bagOnActivate = useCallback((i) => {
|
break;
|
||||||
|
case 2:
|
||||||
|
client.send({
|
||||||
|
type: 'Action',
|
||||||
|
payload: {
|
||||||
|
type: 'distribute',
|
||||||
|
value: [
|
||||||
|
i + 1,
|
||||||
|
[
|
||||||
|
[mainEntityRef.current, 0],
|
||||||
|
[mainEntityRef.current, i + 1],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [bufferSlot, client, keepHotbarOpen, losing, mainEntityRef, trading]);
|
||||||
|
const hotbarOnSlotMouseMove = useCallback((i) => {
|
||||||
|
if (hadBufferSlot.current) {
|
||||||
|
setDistributing((distributing) => ({
|
||||||
|
...distributing,
|
||||||
|
[[mainEntityRef.current, i + 1].join(':')]: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [mainEntityRef]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!bufferSlot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entity = ecsRef.current.get(mainEntityRef.current);
|
||||||
|
setHotbarSlots(() => {
|
||||||
|
const newHotbarSlots = [];
|
||||||
|
for (let i = 1; i < 11; ++i) {
|
||||||
|
const item = entity.Inventory.item(i);
|
||||||
|
newHotbarSlots.push(
|
||||||
|
item
|
||||||
|
? {icon: item.icon, qty: item.qty}
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const qtys = [];
|
||||||
|
for (const key in distributing) {
|
||||||
|
const [entityId, slot] = key.split(':');
|
||||||
|
const {Inventory} = ecsRef.current.get(entityId);
|
||||||
|
qtys.push(Inventory.slots[slot]?.qty ?? 0);
|
||||||
|
}
|
||||||
|
const item = {
|
||||||
|
maximumStack: bufferSlot.maximumStack,
|
||||||
|
qty: bufferSlot.qty,
|
||||||
|
};
|
||||||
|
const distributed = distribute(item, qtys);
|
||||||
|
let i = 0;
|
||||||
|
for (const key in distributing) {
|
||||||
|
const [entityId, slot] = key.split(':');
|
||||||
|
if (
|
||||||
|
entityId == mainEntityRef.current
|
||||||
|
&& (slot >= 1 && slot <= 11)
|
||||||
|
) {
|
||||||
|
if (!newHotbarSlots[slot - 1]) {
|
||||||
|
newHotbarSlots[slot - 1] = {
|
||||||
|
icon: bufferSlot.icon,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
newHotbarSlots[slot - 1].qty = distributed[i];
|
||||||
|
newHotbarSlots[slot - 1].temporary = true;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return newHotbarSlots;
|
||||||
|
});
|
||||||
|
}, [bufferSlot, distributing, ecsRef, mainEntityRef])
|
||||||
|
const hotbarOnSlotMouseUp = useCallback((i, event) => {
|
||||||
|
keepHotbarOpen();
|
||||||
|
if (trading) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch (event.button) {
|
||||||
|
case 0:
|
||||||
|
if (Object.keys(distributing).length > 0) {
|
||||||
|
client.send({
|
||||||
|
type: 'Action',
|
||||||
|
payload: {
|
||||||
|
type: 'distribute',
|
||||||
|
value: [
|
||||||
|
0,
|
||||||
|
Object.keys(distributing).map((idAndSlot) => idAndSlot.split(':')),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setDistributing({});
|
||||||
|
}
|
||||||
|
else if (hadBufferSlot.current) {
|
||||||
|
client.send({
|
||||||
|
type: 'Action',
|
||||||
|
payload: {
|
||||||
|
type: 'distribute',
|
||||||
|
value: [
|
||||||
|
0,
|
||||||
|
[
|
||||||
|
[mainEntityRef.current, i + 1],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hadBufferSlot.current = undefined;
|
||||||
|
}, [client, distributing, keepHotbarOpen, mainEntityRef, trading]);
|
||||||
|
const bagOnSlotMouseDown = useCallback((i) => {
|
||||||
|
if (trading) {
|
||||||
|
const index = losing.indexOf(i + 11);
|
||||||
|
if (-1 === index) {
|
||||||
|
losing.push(i + 11);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
losing.splice(index, 1);
|
||||||
|
}
|
||||||
|
setLosing([...losing]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
client.send({
|
client.send({
|
||||||
type: 'Action',
|
type: 'Action',
|
||||||
payload: {type: 'swapSlots', value: [0, mainEntityRef.current, i + 11]},
|
payload: {type: 'swapSlots', value: [0, mainEntityRef.current, i + 11]},
|
||||||
});
|
});
|
||||||
}, [client, mainEntityRef]);
|
}
|
||||||
|
}, [client, losing, mainEntityRef, trading]);
|
||||||
|
const externalInventoryOnSlotMouseDown = useCallback((i) => {
|
||||||
|
if (trading) {
|
||||||
|
const index = gaining.indexOf(i);
|
||||||
|
if (-1 === index) {
|
||||||
|
gaining.push(i);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
gaining.splice(index, 1);
|
||||||
|
}
|
||||||
|
setGaining([...gaining]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
client.send({
|
||||||
|
type: 'Action',
|
||||||
|
payload: {type: 'swapSlots', value: [0, externalInventory, i]},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [client, externalInventory, gaining, trading]);
|
||||||
|
const onTradeAccepted = useCallback(() => {
|
||||||
|
client.send({
|
||||||
|
type: 'Action',
|
||||||
|
payload: {type: 'acceptTrade', value: {gaining, losing}},
|
||||||
|
});
|
||||||
|
setGaining([]);
|
||||||
|
setLosing([]);
|
||||||
|
}, [client, gaining, losing]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pixiRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pixiRef.current.scale.set(scale);
|
||||||
|
pixiRef.current.x = -Math.round((camera.x * scale) - RESOLUTION.x / 2);
|
||||||
|
pixiRef.current.y = -Math.round((camera.y * scale) - RESOLUTION.y / 2);
|
||||||
|
}, [camera, scale])
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.ui}
|
className={styles.ui}
|
||||||
>
|
>
|
||||||
<style>
|
<style>
|
||||||
{`
|
{`
|
||||||
@media (max-aspect-ratio: ${ratio}) { .${styles.game} { width: 100%; } }
|
@media (max-aspect-ratio: ${ratio}) { .${styles.game}, .${styles.ui} { width: 100%; } }
|
||||||
@media (min-aspect-ratio: ${ratio}) { .${styles.game} { height: 100%; } }
|
@media (min-aspect-ratio: ${ratio}) { .${styles.game}, .${styles.ui} { height: 100%; } }
|
||||||
.${styles.game} {
|
.${styles.game} {
|
||||||
cursor: ${
|
cursor: ${
|
||||||
bufferSlot
|
bufferSlot
|
||||||
|
@ -437,7 +630,7 @@ function Ui({disconnected}) {
|
||||||
where,
|
where,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else {
|
else if (!isInventoryOpen) {
|
||||||
client.send({
|
client.send({
|
||||||
type: 'Action',
|
type: 'Action',
|
||||||
payload: {type: 'use', value: [1, where]},
|
payload: {type: 'use', value: [1, where]},
|
||||||
|
@ -508,34 +701,46 @@ function Ui({disconnected}) {
|
||||||
ref={gameRef}
|
ref={gameRef}
|
||||||
>
|
>
|
||||||
<Pixi
|
<Pixi
|
||||||
camera={camera}
|
|
||||||
monopolizers={monopolizers.current}
|
monopolizers={monopolizers.current}
|
||||||
particleWorker={particleWorker}
|
particleWorker={particleWorker}
|
||||||
scale={scale}
|
ref={pixiRef}
|
||||||
/>
|
/>
|
||||||
<Dom>
|
<Dom>
|
||||||
<HotBar
|
<HotBar
|
||||||
active={activeSlot}
|
active={activeSlot}
|
||||||
|
highlighted={trading && losing.filter((i) => i < 11).map((i) => i - 1)}
|
||||||
hotbarIsHidden={hotbarIsHidden}
|
hotbarIsHidden={hotbarIsHidden}
|
||||||
onActivate={hotbarOnActivate}
|
onSlotMouseDown={hotbarOnSlotMouseDown}
|
||||||
|
onSlotMouseMove={hotbarOnSlotMouseMove}
|
||||||
|
onSlotMouseUp={hotbarOnSlotMouseUp}
|
||||||
slots={hotbarSlots}
|
slots={hotbarSlots}
|
||||||
/>
|
/>
|
||||||
<Bag
|
<Bag
|
||||||
|
highlighted={trading && losing.filter((i) => i >= 11).map((i) => i - 11)}
|
||||||
isInventoryOpen={isInventoryOpen}
|
isInventoryOpen={isInventoryOpen}
|
||||||
onActivate={bagOnActivate}
|
onSlotMouseDown={bagOnSlotMouseDown}
|
||||||
slots={inventorySlots}
|
slots={inventorySlots}
|
||||||
/>
|
/>
|
||||||
{externalInventory && (
|
{externalInventory && (
|
||||||
|
<div className={styles.external}>
|
||||||
<External
|
<External
|
||||||
|
highlighted={trading && gaining}
|
||||||
isInventoryOpen={isInventoryOpen}
|
isInventoryOpen={isInventoryOpen}
|
||||||
onActivate={(i) => {
|
onSlotMouseDown={externalInventoryOnSlotMouseDown}
|
||||||
client.send({
|
|
||||||
type: 'Action',
|
|
||||||
payload: {type: 'swapSlots', value: [0, externalInventory, i]},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
slots={externalInventorySlots}
|
slots={externalInventorySlots}
|
||||||
/>
|
/>
|
||||||
|
{trading && (
|
||||||
|
<Trade
|
||||||
|
isInventoryOpen={isInventoryOpen}
|
||||||
|
onTradeAccepted={onTradeAccepted}
|
||||||
|
gaining={gaining.map((slot) => externalInventorySlots[slot])}
|
||||||
|
losing={losing.map((slot) => {
|
||||||
|
return slot < 11 ? hotbarSlots[slot - 1] : inventorySlots[slot - 11];
|
||||||
|
})}
|
||||||
|
wallet={wallet}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<Entities
|
<Entities
|
||||||
camera={camera}
|
camera={camera}
|
||||||
|
@ -560,6 +765,8 @@ function Ui({disconnected}) {
|
||||||
{showDisconnected && (
|
{showDisconnected && (
|
||||||
<Disconnected />
|
<Disconnected />
|
||||||
)}
|
)}
|
||||||
|
<DateTime />
|
||||||
|
<Wallet gold={wallet} />
|
||||||
</Dom>
|
</Dom>
|
||||||
</div>
|
</div>
|
||||||
{devtoolsIsOpen && (
|
{devtoolsIsOpen && (
|
||||||
|
|
|
@ -22,3 +22,13 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.external {
|
||||||
|
display: flex;
|
||||||
|
left: 20px;
|
||||||
|
opacity: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: 274px;
|
||||||
|
transition: top 150ms, opacity 200ms;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import {createContext, useContext} from 'react';
|
import {useCallback, useState} from 'react';
|
||||||
|
|
||||||
const context = createContext();
|
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
|
||||||
|
import {TAU} from '@/util/math.js';
|
||||||
export default context;
|
|
||||||
|
|
||||||
export function useRadians() {
|
export function useRadians() {
|
||||||
return useContext(context);
|
const [radians, setRadians] = useState(0);
|
||||||
|
useAnimationFrame(
|
||||||
|
useCallback((elapsed) => {
|
||||||
|
setRadians((radians) => radians + (elapsed * TAU));
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
return radians;
|
||||||
}
|
}
|
||||||
|
|
14
app/root.css
|
@ -1,5 +1,6 @@
|
||||||
html, body {
|
html, body {
|
||||||
background-color: #333333;
|
background-color: #333333;
|
||||||
|
color: #cccccc;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
|
@ -29,10 +30,19 @@ body {
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Cookbook";
|
font-family: "Cookbook";
|
||||||
src: url("/assets/fonts/Cookbook.woff");
|
src: url("/fonts/Cookbook.woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Joystix";
|
font-family: "Joystix";
|
||||||
src: url("/assets/fonts/Joystix.woff");
|
src: url("/fonts/Joystix.woff");
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
button, input {
|
||||||
|
background-color: #222222;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
color: #dddddd;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import {json} from "@remix-run/node";
|
|
||||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||||
import {useOutletContext, useParams} from 'react-router-dom';
|
import {useOutletContext, useParams} from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -8,15 +7,8 @@ import ClientContext from '@/react/context/client.js';
|
||||||
import DebugContext from '@/react/context/debug.js';
|
import DebugContext from '@/react/context/debug.js';
|
||||||
import EcsContext from '@/react/context/ecs.js';
|
import EcsContext from '@/react/context/ecs.js';
|
||||||
import MainEntityContext from '@/react/context/main-entity.js';
|
import MainEntityContext from '@/react/context/main-entity.js';
|
||||||
import RadiansContext from '@/react/context/radians.js';
|
|
||||||
import useAnimationFrame from '@/react/hooks/use-animation-frame.js';
|
|
||||||
import {juggleSession} from '@/server/session.server.js';
|
|
||||||
import {TAU} from '@/util/math.js';
|
|
||||||
|
|
||||||
export async function loader({request}) {
|
import ClientEcs from '@/react/components/client-ecs.js';
|
||||||
await juggleSession(request);
|
|
||||||
return json({});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PlaySpecific() {
|
export default function PlaySpecific() {
|
||||||
const Client = useOutletContext();
|
const Client = useOutletContext();
|
||||||
|
@ -24,15 +16,12 @@ export default function PlaySpecific() {
|
||||||
const [client, setClient] = useState();
|
const [client, setClient] = useState();
|
||||||
const mainEntityRef = useRef();
|
const mainEntityRef = useRef();
|
||||||
const debugTuple = useState(false);
|
const debugTuple = useState(false);
|
||||||
|
const [Components, setComponents] = useState();
|
||||||
|
const [Systems, setSystems] = useState();
|
||||||
const ecsRef = useRef();
|
const ecsRef = useRef();
|
||||||
const [disconnected, setDisconnected] = useState(false);
|
const [disconnected, setDisconnected] = useState(false);
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [type, url] = params['*'].split('/');
|
const [type, url] = params['*'].split('/');
|
||||||
const [radians, setRadians] = useState(0);
|
|
||||||
const spin = useCallback((elapsed) => {
|
|
||||||
setRadians((radians) => radians + (elapsed * TAU));
|
|
||||||
}, []);
|
|
||||||
useAnimationFrame(spin);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!Client) {
|
if (!Client) {
|
||||||
return;
|
return;
|
||||||
|
@ -64,6 +53,37 @@ export default function PlaySpecific() {
|
||||||
removeEventListener('beforeunload', onBeforeUnload);
|
removeEventListener('beforeunload', onBeforeUnload);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const refreshEcs = useCallback(() => {
|
||||||
|
class ClientEcsPerf extends ClientEcs {
|
||||||
|
markChange() {}
|
||||||
|
}
|
||||||
|
ecsRef.current = new ClientEcsPerf({Components, Systems});
|
||||||
|
mainEntityRef.current = undefined;
|
||||||
|
}, [ecsRef, Components, Systems]);
|
||||||
|
useEffect(() => {
|
||||||
|
async function setEcsStuff() {
|
||||||
|
const {default: Components} = await import('@/ecs/components/index.js');
|
||||||
|
const {default: Systems} = await import('@/ecs/systems/index.js');
|
||||||
|
setComponents(Components);
|
||||||
|
setSystems(Systems);
|
||||||
|
}
|
||||||
|
setEcsStuff();
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
refreshEcs();
|
||||||
|
}, [refreshEcs]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function onEcsChangePacket() {
|
||||||
|
refreshEcs();
|
||||||
|
}
|
||||||
|
client.addPacketListener('EcsChange', onEcsChangePacket);
|
||||||
|
return () => {
|
||||||
|
client.removePacketListener('EcsChange', onEcsChangePacket);
|
||||||
|
};
|
||||||
|
}, [client, refreshEcs]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return;
|
return;
|
||||||
|
@ -71,6 +91,7 @@ export default function PlaySpecific() {
|
||||||
function onConnectionStatus(status) {
|
function onConnectionStatus(status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'aborted': {
|
case 'aborted': {
|
||||||
|
client.disconnect();
|
||||||
setDisconnected(true);
|
setDisconnected(true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -108,21 +129,16 @@ export default function PlaySpecific() {
|
||||||
if (!client || !disconnected) {
|
if (!client || !disconnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mainEntityRef.current = undefined;
|
|
||||||
async function reconnect() {
|
async function reconnect() {
|
||||||
await client.connect(url);
|
await client.connect(url);
|
||||||
}
|
}
|
||||||
reconnect();
|
reconnect();
|
||||||
const handle = setInterval(reconnect, 1000);
|
|
||||||
return () => {
|
|
||||||
clearInterval(handle);
|
|
||||||
};
|
|
||||||
}, [client, disconnected, mainEntityRef, url]);
|
}, [client, disconnected, mainEntityRef, url]);
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// let source = true;
|
// let source = true;
|
||||||
// async function play() {
|
// async function play() {
|
||||||
// const ctx = new AudioContext();
|
// const ctx = new AudioContext();
|
||||||
// const response = await fetch(new URL('/assets/yuff.wav', window.location.origin));
|
// const response = await fetch(new URL('/resources/yuff.wav', window.location.origin));
|
||||||
// const buffer = await ctx.decodeAudioData(await response.arrayBuffer());
|
// const buffer = await ctx.decodeAudioData(await response.arrayBuffer());
|
||||||
// if (!source) {
|
// if (!source) {
|
||||||
// return;
|
// return;
|
||||||
|
@ -147,9 +163,7 @@ export default function PlaySpecific() {
|
||||||
<EcsContext.Provider value={ecsRef}>
|
<EcsContext.Provider value={ecsRef}>
|
||||||
<DebugContext.Provider value={debugTuple}>
|
<DebugContext.Provider value={debugTuple}>
|
||||||
<AssetsContext.Provider value={assetsTuple}>
|
<AssetsContext.Provider value={assetsTuple}>
|
||||||
<RadiansContext.Provider value={radians}>
|
|
||||||
<Ui disconnected={disconnected} />
|
<Ui disconnected={disconnected} />
|
||||||
</RadiansContext.Provider>
|
|
||||||
</AssetsContext.Provider>
|
</AssetsContext.Provider>
|
||||||
</DebugContext.Provider>
|
</DebugContext.Provider>
|
||||||
</EcsContext.Provider>
|
</EcsContext.Provider>
|
||||||
|
|
|
@ -1,12 +1,66 @@
|
||||||
|
import {settings} from '@pixi/core';
|
||||||
|
import {json, useLoaderData} from "@remix-run/react";
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {Outlet, useParams} from 'react-router-dom';
|
import {Outlet, useParams} from 'react-router-dom';
|
||||||
|
|
||||||
|
import {
|
||||||
|
computeMissing,
|
||||||
|
fetchResources,
|
||||||
|
get,
|
||||||
|
readAsset,
|
||||||
|
set,
|
||||||
|
} from '@/util/resources.js';
|
||||||
|
|
||||||
import styles from './play.module.css';
|
import styles from './play.module.css';
|
||||||
|
|
||||||
|
settings.ADAPTER.fetch = async (path) => {
|
||||||
|
const resource = await readAsset(path);
|
||||||
|
return resource ? new Response(resource) : new Response(undefined, {status: 404});
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loader({request}) {
|
||||||
|
const {juggleSession} = await import('@/server/session.server.js');
|
||||||
|
const {loadManifest} = await import('@/util/resources.server.js');
|
||||||
|
await juggleSession(request);
|
||||||
|
return json({
|
||||||
|
manifest: await loadManifest(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default function Play() {
|
export default function Play() {
|
||||||
|
const {manifest} = useLoaderData();
|
||||||
const [Client, setClient] = useState();
|
const [Client, setClient] = useState();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [type] = params['*'].split('/');
|
const [type] = params['*'].split('/');
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const {signal} = controller;
|
||||||
|
async function receiveResources() {
|
||||||
|
const current = await get();
|
||||||
|
const paths = await computeMissing(current, manifest);
|
||||||
|
if (paths.length > 0 && !signal.aborted) {
|
||||||
|
try {
|
||||||
|
const resources = await fetchResources(paths, {signal});
|
||||||
|
if (resources) {
|
||||||
|
for (const key in resources) {
|
||||||
|
current[key] = resources[key];
|
||||||
|
}
|
||||||
|
await set(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if ((e instanceof DOMException) && 'AbortError' === e.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
receiveResources();
|
||||||
|
return () => {
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, [manifest]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadClient() {
|
async function loadClient() {
|
||||||
let Client;
|
let Client;
|
||||||
|
|
10
app/routes/dev.gen._index/layer.jsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import styles from './layer.module.css';
|
||||||
|
|
||||||
|
function Layer() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Layer;
|
0
app/routes/dev.gen._index/layer.module.css
Normal file
51
app/routes/dev.gen._index/noise-field.jsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import SliderText from '@/react/components/dev/slider-text.jsx';
|
||||||
|
|
||||||
|
import styles from './noise-field.module.css';
|
||||||
|
|
||||||
|
function NoiseField(props) {
|
||||||
|
const {children, ...field} = props;
|
||||||
|
return (
|
||||||
|
<div className={styles.noiseField}>
|
||||||
|
<input
|
||||||
|
name="label[]"
|
||||||
|
type="text"
|
||||||
|
defaultValue={field.label}
|
||||||
|
/>
|
||||||
|
<label>
|
||||||
|
<SliderText
|
||||||
|
name="percent[]"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.05"
|
||||||
|
defaultValue={field.percent}
|
||||||
|
/>
|
||||||
|
<span>%</span>
|
||||||
|
</label>
|
||||||
|
<div className={styles.scale}>
|
||||||
|
<label>
|
||||||
|
<span>x</span>
|
||||||
|
<SliderText
|
||||||
|
defaultValue={field.scale.x}
|
||||||
|
max="100"
|
||||||
|
min="0.01"
|
||||||
|
name="scaleX[]"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>y</span>
|
||||||
|
<SliderText
|
||||||
|
defaultValue={field.scale.y}
|
||||||
|
max="100"
|
||||||
|
min="0.01"
|
||||||
|
name="scaleY[]"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NoiseField;
|
16
app/routes/dev.gen._index/noise-field.module.css
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
.noiseField {
|
||||||
|
gap: 8px;
|
||||||
|
&, label {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
[type="text"][name^="label"] {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
[type="text"][name^="percent"] {
|
||||||
|
width: 3em;
|
||||||
|
}
|
||||||
|
[type="text"][name^="scale"] {
|
||||||
|
width: 4em;
|
||||||
|
}
|
||||||
|
}
|
107
app/routes/dev.gen._index/route.jsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
// import {Container, Sprite, Stage} from '@pixi/react';
|
||||||
|
import {Form, json, redirect, useLoaderData} from '@remix-run/react';
|
||||||
|
import {useState} from 'react';
|
||||||
|
|
||||||
|
import {commitSession, getSession, juggleSession} from '@/server/session.server.js';
|
||||||
|
|
||||||
|
import NoiseField from './noise-field.jsx';
|
||||||
|
|
||||||
|
function getFormSession(session) {
|
||||||
|
return session.get('formSession') || {
|
||||||
|
fields: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function action({request}) {
|
||||||
|
const session = await getSession(request.headers.get('Cookie'));
|
||||||
|
const formSession = getFormSession(session);
|
||||||
|
const formData = await request.formData();
|
||||||
|
const op = formData.get('op');
|
||||||
|
if ('add-field' === op) {
|
||||||
|
formSession.fields.push({
|
||||||
|
label: '',
|
||||||
|
percent: 100,
|
||||||
|
scale: {x: 1, y: 1},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (op.startsWith('delete/')) {
|
||||||
|
const index = parseInt(op.slice('delete/'.length));
|
||||||
|
if (index >= 0 && index < formSession.fields.length) {
|
||||||
|
formSession.fields.splice(index, 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error('delete out of bounds');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (op.startsWith('update/')) {
|
||||||
|
const index = parseInt(op.slice('update/'.length));
|
||||||
|
if (index >= 0 && index < formSession.fields.length) {
|
||||||
|
formSession.fields[index] = {
|
||||||
|
label: formData.getAll('label[]')[index],
|
||||||
|
percent: formData.getAll('percent[]')[index],
|
||||||
|
scale: {
|
||||||
|
x: formData.getAll('scaleX[]')[index],
|
||||||
|
y: formData.getAll('scaleY[]')[index],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error('delete out of bounds');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error('unknown operation');
|
||||||
|
}
|
||||||
|
session.set('formSession', formSession);
|
||||||
|
return json(null, {
|
||||||
|
headers: {
|
||||||
|
'Set-Cookie': await commitSession(session),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loader({request}) {
|
||||||
|
const session = await juggleSession(request);
|
||||||
|
return json({
|
||||||
|
formSession: getFormSession(session),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function Gen() {
|
||||||
|
const {formSession: {fields}} = useLoaderData();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
|
{fields.map((field, i) => (
|
||||||
|
<NoiseField
|
||||||
|
key={i}
|
||||||
|
{...field}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
name="op"
|
||||||
|
value={['update', i].join('/')}
|
||||||
|
>
|
||||||
|
o
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
name="op"
|
||||||
|
value={['delete', i].join('/')}
|
||||||
|
>
|
||||||
|
x
|
||||||
|
</button>
|
||||||
|
</NoiseField>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
name="op"
|
||||||
|
value="add-field"
|
||||||
|
>
|
||||||
|
Add field
|
||||||
|
</button>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Gen;
|
185
app/routes/dev.gen.forest/route.jsx
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
import {Container, Sprite, Stage} from '@pixi/react';
|
||||||
|
import {useState} from 'react';
|
||||||
|
|
||||||
|
import SliderText from '@/react/components/dev/slider-text.jsx';
|
||||||
|
import TileLayer from '@/react/components/pixi/tile-layer.jsx';
|
||||||
|
import AssetsContext from '@/react/context/assets.js';
|
||||||
|
import {CHUNK_SIZE} from '@/util/constants.js';
|
||||||
|
|
||||||
|
import alea from 'alea';
|
||||||
|
import {createNoise2D} from 'simplex-noise';
|
||||||
|
|
||||||
|
// const seed = 3;
|
||||||
|
|
||||||
|
const dirt = [342, 456];
|
||||||
|
const grass = [1, 3, 4, 10, 11, 12];
|
||||||
|
// const stone = [407, 408, 423, 424];
|
||||||
|
// const water = [103, 104];
|
||||||
|
|
||||||
|
class Generator {
|
||||||
|
constructor({
|
||||||
|
area,
|
||||||
|
passes,
|
||||||
|
seed,
|
||||||
|
}) {
|
||||||
|
const prng = alea(seed);
|
||||||
|
const rawNoise = createNoise2D(prng);
|
||||||
|
this.noise = (x, y) => (1 + rawNoise(x, y)) / 2;
|
||||||
|
this.area = area;
|
||||||
|
this.passes = passes;
|
||||||
|
}
|
||||||
|
generate(fn) {
|
||||||
|
for (let y = 0; y < this.area.y; ++y) {
|
||||||
|
for (let x = 0; x < this.area.x; ++x) {
|
||||||
|
for (let j = this.passes.length - 1; j >=0; --j) {
|
||||||
|
if (this.passes[j].test(x, y, this.noise)) {
|
||||||
|
fn(this.passes[j].compute(x, y, this.noise), x, y);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Gen() {
|
||||||
|
const area = {x: 80, y: 80};
|
||||||
|
const assetsTuple = useState({});
|
||||||
|
const [seed, setSeed] = useState(0);
|
||||||
|
const [dirtClump, setDirtClump] = useState(60);
|
||||||
|
const [dirtPer, setDirtPer] = useState(0.6);
|
||||||
|
|
||||||
|
const dirtCheck = (x, y, noise) => noise(x / dirtClump, y / dirtClump) < dirtPer;
|
||||||
|
|
||||||
|
const tilePasses = [
|
||||||
|
{
|
||||||
|
test: () => 1,
|
||||||
|
compute: (x, y, noise) => grass[Math.floor(noise(x, y) * grass.length)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: dirtCheck,
|
||||||
|
compute: (x, y, noise) => dirt[Math.floor(noise(x, y) * dirt.length)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: (x, y, noise) => noise(x / 60, y / 60) < 0.25,
|
||||||
|
compute: () => 103,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: (x, y, noise) => noise(x / 60, y / 60) < 0.14,
|
||||||
|
compute: () => 104,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const entityPasses = [
|
||||||
|
{
|
||||||
|
test: (x, y, noise) => (
|
||||||
|
dirtCheck(x, y, noise)
|
||||||
|
&& !(noise(x / 60, y / 60) < 0.25)
|
||||||
|
&& noise(x, y) < 0.2
|
||||||
|
),
|
||||||
|
compute: (x, y) => ({
|
||||||
|
anchor:{x: 0.5, y: 0.875},
|
||||||
|
image: '/resources/ambient/tree.png',
|
||||||
|
x: x * 16,
|
||||||
|
y: y * 16,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: (x, y, noise) => (
|
||||||
|
!dirtCheck(x, y, noise)
|
||||||
|
&& !(noise(x / 60, y / 60) < 0.25)
|
||||||
|
&& noise(x / 5, y / 5) < 0.15
|
||||||
|
),
|
||||||
|
compute: (x, y, noise) => ({
|
||||||
|
anchor:{x: 0.5, y: 0.7},
|
||||||
|
image: '/resources/ambient/flower.png',
|
||||||
|
x: x * 16 + (noise(x, y) * 8 - 4),
|
||||||
|
y: y * 16 + (noise(y, x) * 8 - 4),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: (x, y, noise) => (
|
||||||
|
!dirtCheck(x, y, noise)
|
||||||
|
&& !(noise(x / 60, y / 60) < 0.25)
|
||||||
|
&& noise(x / 10, y / 10) < 0.2
|
||||||
|
),
|
||||||
|
compute: (x, y, noise) => ({
|
||||||
|
anchor:{x: 0.5, y: 0.7},
|
||||||
|
image: '/resources/ambient/shrub.png',
|
||||||
|
x: x * 16 + (noise(x, y) * 8 - 4),
|
||||||
|
y: y * 16 + (noise(y, x) * 8 - 4),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const layer = {
|
||||||
|
area,
|
||||||
|
$$chunks: Array(
|
||||||
|
Math.ceil(area.x / CHUNK_SIZE) * Math.ceil(area.y / CHUNK_SIZE)
|
||||||
|
).fill(0).map(() => ({})),
|
||||||
|
data: Array(area.x * area.y).fill(1),
|
||||||
|
source: '/resources/tileset.json',
|
||||||
|
tileSize: {x: 16, y: 16},
|
||||||
|
};
|
||||||
|
const tileGenerator = new Generator({
|
||||||
|
area,
|
||||||
|
passes: tilePasses,
|
||||||
|
seed,
|
||||||
|
});
|
||||||
|
tileGenerator.generate((tile, x, y) => {
|
||||||
|
layer.data[y * area.x + x] = tile;
|
||||||
|
});
|
||||||
|
const entities = [];
|
||||||
|
const entityGenerator = new Generator({
|
||||||
|
area,
|
||||||
|
passes: entityPasses,
|
||||||
|
seed,
|
||||||
|
});
|
||||||
|
entityGenerator.generate((entity) => {
|
||||||
|
entities.push(entity);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stage
|
||||||
|
width={640}
|
||||||
|
height={640}
|
||||||
|
options={{
|
||||||
|
background: 0x0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AssetsContext.Provider value={assetsTuple}>
|
||||||
|
<Container
|
||||||
|
scale={0.5}
|
||||||
|
>
|
||||||
|
<TileLayer
|
||||||
|
tileLayer={layer}
|
||||||
|
/>
|
||||||
|
{entities.map((entity, i) => (
|
||||||
|
<Sprite
|
||||||
|
key={i}
|
||||||
|
{...entity}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
</AssetsContext.Provider>
|
||||||
|
</Stage>
|
||||||
|
<input type="text" onChange={({currentTarget: {value}}) => setSeed(value || 0)} value={seed} />
|
||||||
|
<SliderText
|
||||||
|
max="1"
|
||||||
|
min="0"
|
||||||
|
onChange={(value) => setDirtPer(value)}
|
||||||
|
step="0.05"
|
||||||
|
defaultValue={dirtPer}
|
||||||
|
/>
|
||||||
|
<SliderText
|
||||||
|
max="100"
|
||||||
|
min="1"
|
||||||
|
onChange={(value) => setDirtClump(value)}
|
||||||
|
step="1"
|
||||||
|
defaultValue={dirtClump}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Gen;
|
8
app/routes/resources.stream/route.jsx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import {encodeResources, loadResources} from '@/util/resources.server.js';
|
||||||
|
|
||||||
|
export async function action({request}) {
|
||||||
|
const paths = await request.json();
|
||||||
|
const assets = await loadResources();
|
||||||
|
const buffer = await encodeResources(paths.map((path) => assets[path]));
|
||||||
|
return new Response(buffer);
|
||||||
|
}
|
|
@ -63,13 +63,13 @@ export default async function createForest() {
|
||||||
{
|
{
|
||||||
area,
|
area,
|
||||||
data: Array(w * h).fill(0),
|
data: Array(w * h).fill(0),
|
||||||
source: '/assets/tileset.json',
|
source: '/resources/tileset.json',
|
||||||
tileSize: {x: 16, y: 16},
|
tileSize: {x: 16, y: 16},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area,
|
area,
|
||||||
data: Array(w * h).fill(0),
|
data: Array(w * h).fill(0),
|
||||||
source: '/assets/tileset.json',
|
source: '/resources/tileset.json',
|
||||||
tileSize: {x: 16, y: 16},
|
tileSize: {x: 16, y: 16},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -113,7 +113,7 @@ export default async function createForest() {
|
||||||
Position: entityPosition(x, y),
|
Position: entityPosition(x, y),
|
||||||
Sprite: {
|
Sprite: {
|
||||||
anchorY: 0.7,
|
anchorY: 0.7,
|
||||||
source: '/assets/ambient/shrub.json',
|
source: '/resources/ambient/shrub.json',
|
||||||
},
|
},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
});
|
});
|
||||||
|
@ -123,7 +123,7 @@ export default async function createForest() {
|
||||||
Position: entityPosition(x, y),
|
Position: entityPosition(x, y),
|
||||||
Sprite: {
|
Sprite: {
|
||||||
anchorY: 0.875,
|
anchorY: 0.875,
|
||||||
source: '/assets/ambient/tree.json',
|
source: '/resources/ambient/tree.json',
|
||||||
},
|
},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
});
|
});
|
||||||
|
@ -134,7 +134,7 @@ export default async function createForest() {
|
||||||
Position: entityPosition(x, y),
|
Position: entityPosition(x, y),
|
||||||
Sprite: {
|
Sprite: {
|
||||||
anchorY: 0.7,
|
anchorY: 0.7,
|
||||||
source: '/assets/ambient/flower.json',
|
source: '/resources/ambient/flower.json',
|
||||||
},
|
},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import data from '../../../public/assets/dev/homestead.json';
|
import data from './homestead.json';
|
||||||
|
|
||||||
function animal() {
|
function animal() {
|
||||||
return {
|
return {
|
||||||
|
@ -22,10 +22,9 @@ function animal() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function createHomestead(id) {
|
function createMaster() {
|
||||||
const area = {x: 100, y: 60};
|
const area = {x: 100, y: 60};
|
||||||
const entities = [];
|
return {
|
||||||
entities.push({
|
|
||||||
AreaSize: {x: area.x * 16, y: area.y * 16},
|
AreaSize: {x: area.x * 16, y: area.y * 16},
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
TileLayers: {
|
TileLayers: {
|
||||||
|
@ -33,21 +32,24 @@ export default async function createHomestead(id) {
|
||||||
{
|
{
|
||||||
area,
|
area,
|
||||||
data,
|
data,
|
||||||
source: '/assets/tileset.json',
|
source: '/resources/tileset.json',
|
||||||
tileSize: {x: 16, y: 16},
|
tileSize: {x: 16, y: 16},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area,
|
area,
|
||||||
data: Array(area.x * area.y).fill(0),
|
data: Array(area.x * area.y).fill(0),
|
||||||
source: '/assets/tileset.json',
|
source: '/resources/tileset.json',
|
||||||
tileSize: {x: 16, y: 16},
|
tileSize: {x: 16, y: 16},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
Time: {},
|
Time: {},
|
||||||
Water: {water: {}},
|
Water: {water: {}},
|
||||||
});
|
};
|
||||||
entities.push({
|
}
|
||||||
|
|
||||||
|
function createShitShack(id) {
|
||||||
|
return {
|
||||||
Collider: {
|
Collider: {
|
||||||
bodies: [
|
bodies: [
|
||||||
{
|
{
|
||||||
|
@ -68,12 +70,15 @@ export default async function createHomestead(id) {
|
||||||
Sprite: {
|
Sprite: {
|
||||||
anchorX: 0.5,
|
anchorX: 0.5,
|
||||||
anchorY: 0.8,
|
anchorY: 0.8,
|
||||||
source: '/assets/shit-shack/shit-shack.json',
|
source: '/resources/shit-shack/shit-shack.json',
|
||||||
},
|
},
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
});
|
};
|
||||||
entities.push({
|
}
|
||||||
|
|
||||||
|
function createHouseTeleport(id) {
|
||||||
|
return {
|
||||||
Collider: {
|
Collider: {
|
||||||
bodies: [
|
bodies: [
|
||||||
{
|
{
|
||||||
|
@ -102,8 +107,11 @@ export default async function createHomestead(id) {
|
||||||
},
|
},
|
||||||
Position: {x: 71, y: 113},
|
Position: {x: 71, y: 113},
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
});
|
};
|
||||||
entities.push({
|
}
|
||||||
|
|
||||||
|
function createChest() {
|
||||||
|
return {
|
||||||
Collider: {
|
Collider: {
|
||||||
bodies: [
|
bodies: [
|
||||||
{
|
{
|
||||||
|
@ -135,38 +143,83 @@ export default async function createHomestead(id) {
|
||||||
slots: {
|
slots: {
|
||||||
2: {
|
2: {
|
||||||
qty: 1,
|
qty: 1,
|
||||||
source: '/assets/watering-can/watering-can.json',
|
source: '/resources/watering-can/watering-can.json',
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
qty: 1,
|
qty: 1,
|
||||||
source: '/assets/tomato-seeds/tomato-seeds.json',
|
source: '/resources/tomato-seeds/tomato-seeds.json',
|
||||||
},
|
},
|
||||||
4: {
|
4: {
|
||||||
qty: 1,
|
qty: 1,
|
||||||
source: '/assets/hoe/hoe.json',
|
source: '/resources/hoe/hoe.json',
|
||||||
},
|
},
|
||||||
5: {
|
5: {
|
||||||
qty: 1,
|
qty: 1,
|
||||||
source: '/assets/brush/brush.json',
|
source: '/resources/brush/brush.json',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Position: {x: 200, y: 200},
|
Position: {x: 200, y: 200},
|
||||||
|
Shop: {},
|
||||||
Sprite: {
|
Sprite: {
|
||||||
anchorX: 0.5,
|
anchorX: 0.5,
|
||||||
anchorY: 0.7,
|
anchorY: 0.7,
|
||||||
source: '/assets/chest/chest.json',
|
source: '/resources/chest/chest.json',
|
||||||
},
|
},
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTomato() {
|
||||||
|
return {
|
||||||
|
Collider: {
|
||||||
|
bodies: [
|
||||||
|
{
|
||||||
|
points: [
|
||||||
|
{x: -4, y: -4},
|
||||||
|
{x: 3, y: -4},
|
||||||
|
{x: 3, y: 3},
|
||||||
|
{x: -4, y: 3},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
collisionStartScript: [
|
||||||
|
'if (other.Inventory) {',
|
||||||
|
' other.Inventory.give({',
|
||||||
|
' qty: 1,',
|
||||||
|
" source: '/resources/tomato/tomato.json',",
|
||||||
|
' })',
|
||||||
|
' ecs.destroy(entity.id)',
|
||||||
|
' undefined;',
|
||||||
|
'}',
|
||||||
|
].join('\n'),
|
||||||
|
},
|
||||||
|
Forces: {},
|
||||||
|
Magnetic: {},
|
||||||
|
Position: {
|
||||||
|
x: 168 + Math.random() * 30,
|
||||||
|
y: 448 + Math.random() * 30,
|
||||||
|
},
|
||||||
|
Sprite: {
|
||||||
|
anchorX: 0.5,
|
||||||
|
anchorY: 0.5,
|
||||||
|
scaleX: 0.333,
|
||||||
|
scaleY: 0.333,
|
||||||
|
source: '/resources/tomato/tomato-sprite.json',
|
||||||
|
},
|
||||||
|
Ticking: {},
|
||||||
|
VisibleAabb: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTestKitten() {
|
||||||
const animalJson = animal();
|
const animalJson = animal();
|
||||||
for (let i = 0; i < 50; ++i) {
|
return {
|
||||||
entities.push({
|
|
||||||
...animalJson,
|
...animalJson,
|
||||||
Behaving: {
|
Behaving: {
|
||||||
routines: {
|
routines: {
|
||||||
initial: '/assets/kitty/initial.js',
|
initial: '/resources/kitty/initial.js',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Collider: {
|
Collider: {
|
||||||
|
@ -209,18 +262,20 @@ export default async function createHomestead(id) {
|
||||||
...animalJson.Sprite,
|
...animalJson.Sprite,
|
||||||
anchorX: 0.5,
|
anchorX: 0.5,
|
||||||
anchorY: 0.7,
|
anchorY: 0.7,
|
||||||
source: '/assets/kitty/kitty.json',
|
source: '/resources/kitty/kitty.json',
|
||||||
speed: 0.115,
|
speed: 0.115,
|
||||||
},
|
},
|
||||||
Tags: {tags: ['kittan']},
|
Tags: {tags: ['kittan']},
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
for (let i = 0; i < 50; ++i) {
|
|
||||||
entities.push({
|
function createTestCow() {
|
||||||
|
const animalJson = animal();
|
||||||
|
return {
|
||||||
...animalJson,
|
...animalJson,
|
||||||
Behaving: {
|
Behaving: {
|
||||||
routines: {
|
routines: {
|
||||||
initial: '/assets/farm/animals/cow-adult/initial.js',
|
initial: '/resources/farm/animals/cow-adult/initial.js',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Collider: {
|
Collider: {
|
||||||
|
@ -260,17 +315,19 @@ export default async function createHomestead(id) {
|
||||||
...animalJson.Sprite,
|
...animalJson.Sprite,
|
||||||
anchorX: 0.5,
|
anchorX: 0.5,
|
||||||
anchorY: 0.8,
|
anchorY: 0.8,
|
||||||
source: '/assets/farm/animals/cow-adult/cow-adult.json',
|
source: '/resources/farm/animals/cow-adult/cow-adult.json',
|
||||||
speed: 0.25,
|
speed: 0.25,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
for (let i = 0; i < 50; ++i) {
|
|
||||||
entities.push({
|
function createTestGoat() {
|
||||||
|
const animalJson = animal();
|
||||||
|
return {
|
||||||
...animalJson,
|
...animalJson,
|
||||||
Behaving: {
|
Behaving: {
|
||||||
routines: {
|
routines: {
|
||||||
initial: '/assets/farm/animals/goat-white/initial.js',
|
initial: '/resources/farm/animals/goat-white/initial.js',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Collider: {
|
Collider: {
|
||||||
|
@ -309,12 +366,14 @@ export default async function createHomestead(id) {
|
||||||
...animalJson.Sprite,
|
...animalJson.Sprite,
|
||||||
anchorX: 0.5,
|
anchorX: 0.5,
|
||||||
anchorY: 0.8,
|
anchorY: 0.8,
|
||||||
source: '/assets/farm/animals/goat-white/goat-white.json',
|
source: '/resources/farm/animals/goat-white/goat-white.json',
|
||||||
speed: 0.25,
|
speed: 0.25,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
entities.push({
|
|
||||||
|
function createTownTeleport() {
|
||||||
|
return {
|
||||||
Collider: {
|
Collider: {
|
||||||
bodies: [
|
bodies: [
|
||||||
{
|
{
|
||||||
|
@ -344,6 +403,65 @@ export default async function createHomestead(id) {
|
||||||
|
|
||||||
Position: {x: 8, y: 432},
|
Position: {x: 8, y: 432},
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTomatoPlant() {
|
||||||
|
return {
|
||||||
|
Collider: {
|
||||||
|
bodies: [
|
||||||
|
{
|
||||||
|
points: [
|
||||||
|
{x: -8, y: -8},
|
||||||
|
{x: 7, y: -8},
|
||||||
|
{x: -8, y: 7},
|
||||||
|
{x: 7, y: 7},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Interactive: {
|
||||||
|
interactScript: '/resources/tomato-plant/interact.js',
|
||||||
|
},
|
||||||
|
Plant: {
|
||||||
|
growScript: '/resources/tomato-plant/grow.js',
|
||||||
|
growthFactor: Math.floor(Math.random() * 256),
|
||||||
|
mayGrowScript: '/resources/tomato-plant/may-grow.js',
|
||||||
|
stages: [0.5, 0.5, 0.5, 0.5, 0.5],
|
||||||
|
},
|
||||||
|
Position: {
|
||||||
|
x: 168,
|
||||||
|
y: 440,
|
||||||
|
},
|
||||||
|
Sprite: {
|
||||||
|
anchorY: 0.75,
|
||||||
|
animation: 'stage/0',
|
||||||
|
source: '/resources/tomato-plant/tomato-plant.json',
|
||||||
|
},
|
||||||
|
Ticking: {},
|
||||||
|
VisibleAabb: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function createHomestead(id) {
|
||||||
|
const entities = [];
|
||||||
|
entities.push(createMaster());
|
||||||
|
entities.push(createShitShack(id));
|
||||||
|
entities.push(createHouseTeleport(id));
|
||||||
|
entities.push(createChest());
|
||||||
|
// for (let i = 0; i < 200; ++i) {
|
||||||
|
// entities.push(createTomato());
|
||||||
|
// }
|
||||||
|
entities.push(createTomatoPlant());
|
||||||
|
// for (let i = 0; i < 10; ++i) {
|
||||||
|
// entities.push(createTestKitten());
|
||||||
|
// }
|
||||||
|
// for (let i = 0; i < 10; ++i) {
|
||||||
|
// entities.push(createTestCow());
|
||||||
|
// }
|
||||||
|
// for (let i = 0; i < 10; ++i) {
|
||||||
|
// entities.push(createTestGoat());
|
||||||
|
// }
|
||||||
|
entities.push(createTownTeleport());
|
||||||
return entities;
|
return entities;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default async function createHouse(Ecs, id) {
|
||||||
{
|
{
|
||||||
area,
|
area,
|
||||||
data: Array(area.x * area.y).fill(0).map(() => 5 + Math.floor(Math.random() * 2)),
|
data: Array(area.x * area.y).fill(0).map(() => 5 + Math.floor(Math.random() * 2)),
|
||||||
source: '/assets/tileset.json',
|
source: '/resources/tileset.json',
|
||||||
tileSize: {x: 16, y: 16},
|
tileSize: {x: 16, y: 16},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export default async function createPlayer(id) {
|
export default async function createPlayer(id) {
|
||||||
const player = {
|
const player = {
|
||||||
|
Alive: {health: 100},
|
||||||
Camera: {},
|
Camera: {},
|
||||||
Collider: {
|
Collider: {
|
||||||
bodies: [
|
bodies: [
|
||||||
|
@ -24,11 +25,31 @@ export default async function createPlayer(id) {
|
||||||
slots: {
|
slots: {
|
||||||
1: {
|
1: {
|
||||||
qty: 100,
|
qty: 100,
|
||||||
source: '/assets/potion/potion.json',
|
source: '/resources/potion/potion.json',
|
||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
qty: 1,
|
qty: 1,
|
||||||
source: '/assets/magic-swords/magic-swords.json',
|
source: '/resources/magic-swords/magic-swords.json',
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
qty: 1,
|
||||||
|
source: '/resources/watering-can/watering-can.json',
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
qty: 1,
|
||||||
|
source: '/resources/tomato-seeds/tomato-seeds.json',
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
qty: 1,
|
||||||
|
source: '/resources/hoe/hoe.json',
|
||||||
|
},
|
||||||
|
6: {
|
||||||
|
qty: 95,
|
||||||
|
source: '/resources/potion/potion.json',
|
||||||
|
},
|
||||||
|
7: {
|
||||||
|
qty: 95,
|
||||||
|
source: '/resources/potion/potion.json',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -44,11 +65,15 @@ export default async function createPlayer(id) {
|
||||||
anchorY: 0.9,
|
anchorY: 0.9,
|
||||||
animation: 'moving:down',
|
animation: 'moving:down',
|
||||||
frame: 0,
|
frame: 0,
|
||||||
source: '/assets/dude/dude.json',
|
source: '/resources/dude/dude.json',
|
||||||
speed: 0.115,
|
speed: 0.115,
|
||||||
},
|
},
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
|
Vulnerable: {},
|
||||||
|
Wallet: {
|
||||||
|
gold: 1000,
|
||||||
|
},
|
||||||
Wielder: {
|
Wielder: {
|
||||||
activeSlot: 1,
|
activeSlot: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import data from '../../../public/assets/dev/town.json';
|
import data from './town.json';
|
||||||
|
|
||||||
export default async function createTown() {
|
export default async function createTown() {
|
||||||
const area = {x: 60, y: 60};
|
const area = {x: 60, y: 60};
|
||||||
|
@ -11,13 +11,13 @@ export default async function createTown() {
|
||||||
{
|
{
|
||||||
area,
|
area,
|
||||||
data,
|
data,
|
||||||
source: '/assets/tileset.json',
|
source: '/resources/tileset.json',
|
||||||
tileSize: {x: 16, y: 16},
|
tileSize: {x: 16, y: 16},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area,
|
area,
|
||||||
data: Array(area.x * area.y).fill(0),
|
data: Array(area.x * area.y).fill(0),
|
||||||
source: '/assets/tileset.json',
|
source: '/resources/tileset.json',
|
||||||
tileSize: {x: 16, y: 16},
|
tileSize: {x: 16, y: 16},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -130,6 +130,9 @@ export default class Engine {
|
||||||
});
|
});
|
||||||
this.server.addPacketListener('Heartbeat', (connection) => {
|
this.server.addPacketListener('Heartbeat', (connection) => {
|
||||||
const playerData = this.connectedPlayers.get(connection);
|
const playerData = this.connectedPlayers.get(connection);
|
||||||
|
if (!playerData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const {distance} = playerData;
|
const {distance} = playerData;
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
distance.rtt = (now - distance.last) / 1000;
|
distance.rtt = (now - distance.last) / 1000;
|
||||||
|
@ -163,6 +166,8 @@ export default class Engine {
|
||||||
Interacts,
|
Interacts,
|
||||||
Interlocutor,
|
Interlocutor,
|
||||||
Inventory,
|
Inventory,
|
||||||
|
Player,
|
||||||
|
Wallet,
|
||||||
Wielder,
|
Wielder,
|
||||||
} = entity;
|
} = entity;
|
||||||
const ecs = this.ecses[Ecs.path];
|
const ecs = this.ecses[Ecs.path];
|
||||||
|
@ -228,6 +233,40 @@ export default class Engine {
|
||||||
Controlled[payload.type] = payload.value;
|
Controlled[payload.type] = payload.value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'acceptTrade': {
|
||||||
|
const {losing, gaining} = payload.value;
|
||||||
|
const gainingSlots = gaining.filter((slot) => Player.openInventory.item(slot));
|
||||||
|
const losingSlots = losing.filter((slot) => Inventory.item(slot));
|
||||||
|
const nop = 0 === gainingSlots.length && 0 === losingSlots.length;
|
||||||
|
const gainingItems = gainingSlots.map((slot) => Player.openInventory.item(slot));
|
||||||
|
const losingItems = losingSlots.map((slot) => Inventory.item(slot));
|
||||||
|
if (nop) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const reducePrice = (r, item) => {
|
||||||
|
return r + item.price * item.qty;
|
||||||
|
};
|
||||||
|
const earn = losingItems.reduce(reducePrice, 0) - gainingItems.reduce(reducePrice, 0);
|
||||||
|
const canAfford = -earn <= Wallet.gold;
|
||||||
|
if (!canAfford) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (const slot of losingSlots) {
|
||||||
|
Inventory.clear(slot);
|
||||||
|
}
|
||||||
|
for (const item of gainingItems) {
|
||||||
|
Inventory.give({qty: item.qty, source: item.source});
|
||||||
|
}
|
||||||
|
Wallet.gold += earn;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'distribute': {
|
||||||
|
if (!Controlled.locked) {
|
||||||
|
const [slot, destinations] = payload.value;
|
||||||
|
Inventory.distribute(slot, destinations);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'swapSlots': {
|
case 'swapSlots': {
|
||||||
if (!Controlled.locked) {
|
if (!Controlled.locked) {
|
||||||
const [l, other, r] = payload.value;
|
const [l, other, r] = payload.value;
|
||||||
|
@ -449,7 +488,13 @@ export default class Engine {
|
||||||
loop();
|
loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
async stop() {
|
||||||
|
const promises = [];
|
||||||
|
for (const [connection] of this.connectedPlayers) {
|
||||||
|
promises.push(this.disconnectPlayer(connection));
|
||||||
|
}
|
||||||
|
await Promise.all(promises);
|
||||||
|
await this.saveEcses();
|
||||||
clearTimeout(this.handle);
|
clearTimeout(this.handle);
|
||||||
this.handle = undefined;
|
this.handle = undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,40 +8,11 @@ import {getSession} from '@/server/session.server.js';
|
||||||
|
|
||||||
import Engine from './engine.js';
|
import Engine from './engine.js';
|
||||||
|
|
||||||
const isInsecure = process.env.SILPHIUS_INSECURE_HTTP;
|
const {
|
||||||
|
RESOURCES_PATH = [process.cwd(), 'resources'].join('/'),
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
const wss = new WebSocketServer({
|
global.__silphiusWebsocket = null;
|
||||||
noServer: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
function onUpgrade(request, socket, head) {
|
|
||||||
const {pathname} = new URL(request.url, 'wss://base.url');
|
|
||||||
if (pathname === '/ws') {
|
|
||||||
wss.handleUpgrade(request, socket, head, function done(ws) {
|
|
||||||
wss.emit('connection', ws, request);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
socket.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let engine;
|
|
||||||
let onConnect;
|
|
||||||
|
|
||||||
function createOnConnect(engine) {
|
|
||||||
onConnect = async (ws, request) => {
|
|
||||||
ws.on('close', async () => {
|
|
||||||
await engine.disconnectPlayer(ws);
|
|
||||||
})
|
|
||||||
ws.on('message', (packed) => {
|
|
||||||
engine.server.accept(ws, new DataView(packed.buffer, packed.byteOffset, packed.length));
|
|
||||||
});
|
|
||||||
const session = await getSession(request.headers['cookie']);
|
|
||||||
await engine.connectPlayer(ws, session.get('id'));
|
|
||||||
};
|
|
||||||
wss.on('connection', onConnect);
|
|
||||||
}
|
|
||||||
|
|
||||||
class SocketServer extends Server {
|
class SocketServer extends Server {
|
||||||
async ensurePath(path) {
|
async ensurePath(path) {
|
||||||
|
@ -51,13 +22,18 @@ class SocketServer extends Server {
|
||||||
return join(import.meta.dirname, '..', '..', 'data', 'remote', 'UNIVERSE', path);
|
return join(import.meta.dirname, '..', '..', 'data', 'remote', 'UNIVERSE', path);
|
||||||
}
|
}
|
||||||
async readAsset(path) {
|
async readAsset(path) {
|
||||||
const url = new URL(path, 'https://localhost:3000')
|
const {pathname} = new URL(path, 'http://example.org');
|
||||||
if (isInsecure) {
|
const resourcePath = pathname.slice('/resources/'.length);
|
||||||
url.protocol = 'http:';
|
try {
|
||||||
|
const {buffer} = await readFile([RESOURCES_PATH, resourcePath].join('/'));
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if ('ENOENT' !== error.code) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return new ArrayBuffer(0);
|
||||||
}
|
}
|
||||||
return fetch(url.href).then((response) => (
|
|
||||||
response.ok ? response.arrayBuffer() : new ArrayBuffer(0)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
async readData(path) {
|
async readData(path) {
|
||||||
const qualified = this.constructor.qualify(path);
|
const qualified = this.constructor.qualify(path);
|
||||||
|
@ -75,35 +51,55 @@ class SocketServer extends Server {
|
||||||
transmit(ws, packed) { ws.send(packed); }
|
transmit(ws, packed) { ws.send(packed); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createEngine(Engine) {
|
export async function handleUpgrade(request, socket, head) {
|
||||||
engine = new Engine(SocketServer);
|
if (!global.__silphiusWebsocket) {
|
||||||
|
const engine = new Engine(SocketServer);
|
||||||
await engine.load();
|
await engine.load();
|
||||||
engine.start();
|
engine.start();
|
||||||
return engine;
|
const handleConnection = async (ws, request) => {
|
||||||
|
ws.on('close', async () => {
|
||||||
|
await engine.disconnectPlayer(ws);
|
||||||
|
})
|
||||||
|
ws.on('message', (packed) => {
|
||||||
|
engine.server.accept(ws, new DataView(packed.buffer, packed.byteOffset, packed.length));
|
||||||
|
});
|
||||||
|
const session = await getSession(request.headers['cookie']);
|
||||||
|
await engine.connectPlayer(ws, session.get('id'));
|
||||||
|
};
|
||||||
|
const wss = new WebSocketServer({
|
||||||
|
noServer: true,
|
||||||
|
});
|
||||||
|
wss.on('connection', handleConnection);
|
||||||
|
global.__silphiusWebsocket = {engine, handleConnection, wss};
|
||||||
}
|
}
|
||||||
|
const {pathname} = new URL(request.url, 'wss://base.url');
|
||||||
async function remakeServer(Engine) {
|
if (pathname === '/ws') {
|
||||||
if (onConnect) {
|
const {wss} = global.__silphiusWebsocket;
|
||||||
wss.off('connection', onConnect);
|
wss.handleUpgrade(request, socket, head, function done(ws) {
|
||||||
}
|
wss.emit('connection', ws, request);
|
||||||
if (engine) {
|
|
||||||
for (const [connection] of engine.connectedPlayers) {
|
|
||||||
connection.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
createOnConnect(await createEngine(Engine));
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
await remakeServer(Engine);
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (import.meta.hot) {
|
|
||||||
import.meta.hot.accept('./engine.js', async ({default: Engine}) => {
|
|
||||||
await remakeServer(Engine);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
export default async function listen(server) {
|
socket.destroy();
|
||||||
server.on('upgrade', onUpgrade);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.on('vite:beforeUpdate', async () => {
|
||||||
|
if (global.__silphiusWebsocket) {
|
||||||
|
const {engine, handleConnection, wss} = global.__silphiusWebsocket;
|
||||||
|
wss.off('connection', handleConnection);
|
||||||
|
const connections = [];
|
||||||
|
for (const [connection] of engine.connectedPlayers) {
|
||||||
|
engine.server.send(connection, {type: 'EcsChange'});
|
||||||
|
connections.push(connection);
|
||||||
|
}
|
||||||
|
await engine.stop();
|
||||||
|
for (const connection of connections) {
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
global.__silphiusWebsocket = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
import.meta.hot.accept();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {del, get, set} from 'idb-keyval';
|
||||||
import {encode} from '@/net/packets/index.js';
|
import {encode} from '@/net/packets/index.js';
|
||||||
import Server from '@/net/server.js';
|
import Server from '@/net/server.js';
|
||||||
import {withResolvers} from '@/util/promise.js';
|
import {withResolvers} from '@/util/promise.js';
|
||||||
|
import {readAsset} from '@/util/resources.js';
|
||||||
|
|
||||||
import createEcs from './create/ecs.js';
|
import createEcs from './create/ecs.js';
|
||||||
import './create/forest.js';
|
import './create/forest.js';
|
||||||
|
@ -21,9 +22,10 @@ class WorkerServer extends Server {
|
||||||
return ['UNIVERSE', path].join('/');
|
return ['UNIVERSE', path].join('/');
|
||||||
}
|
}
|
||||||
async readAsset(path) {
|
async readAsset(path) {
|
||||||
return fetch(path).then((response) => (
|
const resource = await readAsset(path);
|
||||||
response.ok ? response.arrayBuffer() : new ArrayBuffer(0)
|
return resource
|
||||||
));
|
? resource
|
||||||
|
: new ArrayBuffer(0);
|
||||||
}
|
}
|
||||||
async readData(path) {
|
async readData(path) {
|
||||||
const data = await get(this.constructor.qualify(path));
|
const data = await get(this.constructor.qualify(path));
|
||||||
|
@ -66,7 +68,19 @@ onmessage = async (event) => {
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
const before = withResolvers();
|
const before = withResolvers();
|
||||||
const promises = [before.promise];
|
const promises = [before.promise];
|
||||||
import.meta.hot.on('vite:beforeUpdate', async () => {
|
const accepted = [
|
||||||
|
'/app/server/engine.js',
|
||||||
|
'/app/server/create/player.js',
|
||||||
|
'/app/server/create/forest.js',
|
||||||
|
'/app/server/create/town.js',
|
||||||
|
'/app/server/create/homestead.js',
|
||||||
|
];
|
||||||
|
let isAccepted = false;
|
||||||
|
import.meta.hot.on('vite:beforeUpdate', async ({updates}) => {
|
||||||
|
isAccepted = !!updates.find(({acceptedPath}) => accepted.includes(acceptedPath));
|
||||||
|
if (!isAccepted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await engine.disconnectPlayer(0);
|
await engine.disconnectPlayer(0);
|
||||||
engine.stop();
|
engine.stop();
|
||||||
await engine.saveEcses();
|
await engine.saveEcses();
|
||||||
|
@ -126,8 +140,12 @@ import.meta.hot.accept('./create/town.js', async ({default: createTown}) => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
import.meta.hot.on('vite:afterUpdate', async () => {
|
import.meta.hot.on('vite:afterUpdate', async () => {
|
||||||
|
if (!isAccepted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
||||||
|
postMessage(encode({type: 'EcsChange'}));
|
||||||
close();
|
close();
|
||||||
});
|
});
|
||||||
import.meta.hot.accept();
|
import.meta.hot.accept();
|
||||||
|
|
23
app/util/inventory.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
export function distribute(item, qtys) {
|
||||||
|
const {maximumStack} = item;
|
||||||
|
qtys = [...qtys];
|
||||||
|
const eligible = qtys.map((_, i) => i);
|
||||||
|
let i = 0;
|
||||||
|
while (item.qty > 0) {
|
||||||
|
const key = eligible[i];
|
||||||
|
if (qtys[key] < maximumStack) {
|
||||||
|
qtys[key] += 1;
|
||||||
|
i = (i + 1) % eligible.length;
|
||||||
|
item.qty -= 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
eligible.splice(i, 1);
|
||||||
|
i = i % eligible.length;
|
||||||
|
if (0 === eligible.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return qtys;
|
||||||
|
}
|
||||||
|
|
19
app/util/inventory.test.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import {expect, test} from 'vitest';
|
||||||
|
|
||||||
|
import {distribute} from './inventory.js';
|
||||||
|
|
||||||
|
test('distributes', async () => {
|
||||||
|
let item;
|
||||||
|
item = {maximumStack: 20, qty: 10};
|
||||||
|
expect(distribute(
|
||||||
|
item,
|
||||||
|
[15, 15, 0],
|
||||||
|
)).to.deep.equal([19, 18, 3]);
|
||||||
|
expect(item.qty).to.equal(0);
|
||||||
|
item = {maximumStack: 20, qty: 20};
|
||||||
|
expect(distribute(
|
||||||
|
item,
|
||||||
|
[15, 15, 0],
|
||||||
|
)).to.deep.equal([20, 20, 10]);
|
||||||
|
expect(item.qty).to.equal(0);
|
||||||
|
});
|
78
app/util/resources.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import {LRUCache} from 'lru-cache';
|
||||||
|
|
||||||
|
const cache = new LRUCache({
|
||||||
|
max: 128,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function computeMissing(current, manifest) {
|
||||||
|
const missing = [];
|
||||||
|
for (const path in manifest) {
|
||||||
|
if (!current || !current[path] || current[path].hash !== manifest[path]) {
|
||||||
|
missing.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const octets = [];
|
||||||
|
for (let i = 0; i < 256; ++i) {
|
||||||
|
octets.push(i.toString(16).padStart(2, '0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseResources(resources, buffer) {
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
let caret = 0;
|
||||||
|
const count = view.getUint32(caret);
|
||||||
|
caret += 4;
|
||||||
|
const manifest = {};
|
||||||
|
for (let i = 0; i < count; ++i) {
|
||||||
|
let hash = '';
|
||||||
|
for (let j = 0; j < 20; ++j) {
|
||||||
|
hash += octets[view.getUint8(caret)]
|
||||||
|
caret += 1;
|
||||||
|
}
|
||||||
|
const byteLength = view.getUint32(caret);
|
||||||
|
caret += 4;
|
||||||
|
const asset = new ArrayBuffer(byteLength);
|
||||||
|
const assetView = new DataView(asset);
|
||||||
|
for (let j = 0; j < asset.byteLength; ++j) {
|
||||||
|
assetView.setUint8(j, view.getUint8(caret));
|
||||||
|
caret += 1;
|
||||||
|
}
|
||||||
|
manifest[resources[i]] = {asset, hash};
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchResources(paths, {signal} = {}) {
|
||||||
|
const response = await fetch('/resources/stream', {
|
||||||
|
body: JSON.stringify(paths),
|
||||||
|
method: 'post',
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
if (signal.aborted) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return parseResources(paths, await response.arrayBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get() {
|
||||||
|
const {get} = await import('idb-keyval');
|
||||||
|
const resources = await get('$$silphius_resources');
|
||||||
|
return resources || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function set(resources) {
|
||||||
|
const {set} = await import('idb-keyval');
|
||||||
|
cache.clear();
|
||||||
|
await set('$$silphius_resources', resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readAsset(path) {
|
||||||
|
if (!cache.has(path)) {
|
||||||
|
const {pathname} = new URL(path, 'http://example.org');
|
||||||
|
const resourcePath = pathname.slice('/resources/'.length);
|
||||||
|
cache.set(path, get().then((resources) => resources[resourcePath]?.asset));
|
||||||
|
}
|
||||||
|
return cache.get(path);
|
||||||
|
}
|
114
app/util/resources.server.js
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import {createHash} from 'node:crypto';
|
||||||
|
import {readFile, realpath} from 'node:fs/promises';
|
||||||
|
|
||||||
|
import chokidar from 'chokidar';
|
||||||
|
import {glob} from 'glob';
|
||||||
|
|
||||||
|
import {singleton} from './singleton.js';
|
||||||
|
|
||||||
|
const {
|
||||||
|
RESOURCES_PATH = [process.cwd(), 'resources'].join('/'),
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
const resources = singleton('resources', {});
|
||||||
|
const manifest = singleton('manifest', {});
|
||||||
|
const RESOURCES_PATH_REAL = await realpath(RESOURCES_PATH);
|
||||||
|
const RESOURCES_GLOB = [RESOURCES_PATH_REAL, '**', '*'].join('/');
|
||||||
|
|
||||||
|
async function computeAsset(path) {
|
||||||
|
const buffer = await readFile(path);
|
||||||
|
return {
|
||||||
|
asset: buffer,
|
||||||
|
hash: createHash('sha1').update(buffer).digest(),
|
||||||
|
path: path.slice(RESOURCES_PATH_REAL.length + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assetPaths() {
|
||||||
|
return glob(RESOURCES_GLOB, {nodir: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createManifest() {
|
||||||
|
const paths = await assetPaths();
|
||||||
|
const entries = await Promise.all(paths.map(async (path) => {
|
||||||
|
const asset = await computeAsset(path);
|
||||||
|
return [asset.path, asset.hash.toString('hex')];
|
||||||
|
}))
|
||||||
|
return Object.fromEntries(
|
||||||
|
entries.filter(Boolean),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadManifest() {
|
||||||
|
emptyCheck: {
|
||||||
|
for (const path in manifest) { // eslint-disable-line no-unused-vars
|
||||||
|
break emptyCheck;
|
||||||
|
}
|
||||||
|
const created = await createManifest();
|
||||||
|
for (const path in created) {
|
||||||
|
manifest[path] = created[path];
|
||||||
|
}
|
||||||
|
const watcher = chokidar.watch(RESOURCES_GLOB);
|
||||||
|
watcher.on('add', async (path) => {
|
||||||
|
const asset = await computeAsset(path);
|
||||||
|
manifest[asset.path] = asset.hash.toString('hex');
|
||||||
|
resources[asset.path] = {asset: asset.asset.buffer, hash: asset.hash.buffer};
|
||||||
|
});
|
||||||
|
watcher.on('change', async (path) => {
|
||||||
|
const asset = await computeAsset(path);
|
||||||
|
manifest[asset.path] = asset.hash.toString('hex');
|
||||||
|
resources[asset.path] = {asset: asset.asset.buffer, hash: asset.hash.buffer};
|
||||||
|
});
|
||||||
|
watcher.on('unlink', async (path) => {
|
||||||
|
const assetPath = path.slice(RESOURCES_PATH_REAL.length + 1);
|
||||||
|
delete resources[assetPath];
|
||||||
|
delete manifest[assetPath];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function encodeResources(resources) {
|
||||||
|
let byteLength = 4;
|
||||||
|
for (const {asset} of resources) {
|
||||||
|
byteLength += 20;
|
||||||
|
byteLength += 4;
|
||||||
|
byteLength += asset.byteLength;
|
||||||
|
}
|
||||||
|
const buffer = new ArrayBuffer(byteLength);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
let caret = 0;
|
||||||
|
view.setUint32(caret, resources.length);
|
||||||
|
caret += 4;
|
||||||
|
for (const {asset, hash} of resources) {
|
||||||
|
const hashView = new DataView(hash);
|
||||||
|
for (let j = 0; j < 20; ++j) {
|
||||||
|
view.setUint8(caret, hashView.getUint8(j));
|
||||||
|
caret += 1;
|
||||||
|
}
|
||||||
|
view.setUint32(caret, asset.byteLength);
|
||||||
|
caret += 4;
|
||||||
|
const assetView = new DataView(asset);
|
||||||
|
for (let j = 0; j < asset.byteLength; ++j) {
|
||||||
|
view.setUint8(caret, assetView.getUint8(j));
|
||||||
|
caret += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadResources() {
|
||||||
|
emptyCheck: {
|
||||||
|
for (const path in resources) { // eslint-disable-line no-unused-vars
|
||||||
|
break emptyCheck;
|
||||||
|
}
|
||||||
|
const paths = await assetPaths();
|
||||||
|
await Promise.all(
|
||||||
|
paths.map(async (path) => {
|
||||||
|
const {asset, hash, path: assetPath} = await computeAsset(path);
|
||||||
|
resources[assetPath] = {asset: asset.buffer, hash: hash.buffer};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return resources;
|
||||||
|
}
|
45
app/util/resources.test.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import {expect, test} from 'vitest';
|
||||||
|
|
||||||
|
import {computeMissing, parseResources} from './resources.js';
|
||||||
|
import {encodeResources} from './resources.server.js';
|
||||||
|
|
||||||
|
test('cache', async () => {
|
||||||
|
class TestCache {
|
||||||
|
current;
|
||||||
|
async get() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve(this.current);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async set(manifest) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.current = manifest;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cache = new TestCache();
|
||||||
|
expect(await computeMissing(await cache.get(), {foo: 'bar'})).to.deep.equal(['foo']);
|
||||||
|
await cache.set({foo: {hash: 'bar'}});
|
||||||
|
expect(await computeMissing(await cache.get(), {foo: 'bar'})).to.deep.equal([]);
|
||||||
|
expect(await computeMissing(await cache.get(), {foo: 'bar', baz: '32'})).to.deep.equal(['baz']);
|
||||||
|
await cache.set({foo: {hash: 'bar'}, baz: '32'});
|
||||||
|
expect(await computeMissing(await cache.get(), {foo: 'bar', baz: '64'})).to.deep.equal(['baz']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('codec', async () => {
|
||||||
|
const asset = {
|
||||||
|
asset: (new Uint8Array('hello'.split('').map((letter) => letter.charCodeAt(0)))).buffer,
|
||||||
|
hash: (new Uint8Array(Array(20).fill(0).map((_, i) => i))).buffer,
|
||||||
|
};
|
||||||
|
const buffer = await encodeResources([
|
||||||
|
asset,
|
||||||
|
]);
|
||||||
|
const parsed = await parseResources(['hello.txt'], buffer);
|
||||||
|
expect('hello.txt' in parsed).to.equal(true);
|
||||||
|
expect(parsed['hello.txt'].hash).to.equal('000102030405060708090a0b0c0d0e0f10111213');
|
||||||
|
const hello = new Uint8Array(parsed['hello.txt'].asset);
|
||||||
|
for (let i = 0; i < 5; ++i) {
|
||||||
|
expect(hello[i]).to.equal('hello'.charCodeAt(i));
|
||||||
|
}
|
||||||
|
});
|
9
app/util/singleton.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export function singleton(key, value) {
|
||||||
|
global.__singletons ??= {};
|
||||||
|
global.__singletons[key] ??= value;
|
||||||
|
return global.__singletons[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
singleton.reset = function (key) {
|
||||||
|
delete global.__singletons[key];
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"icon": "/assets/brush/brush.png",
|
|
||||||
"label": "Brush",
|
|
||||||
"start": "/assets/brush/start.js"
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"icon": "/assets/furball/furball.png",
|
|
||||||
"label": "Fur Ball"
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"icon": "/assets/hoe/icon.png",
|
|
||||||
"label": "Hoe",
|
|
||||||
"projectionCheck": "/assets/hoe/projection-check.js",
|
|
||||||
"projection": {
|
|
||||||
"distance": [3, -1],
|
|
||||||
"grid": [
|
|
||||||
[1, 1, 1],
|
|
||||||
[1, 1, 1],
|
|
||||||
[1, 1, 1]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"start": "/assets/hoe/start.js"
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"icon": "/assets/magic-swords/icon.png",
|
|
||||||
"label": "Magic swords",
|
|
||||||
"start": "/assets/magic-swords/start.js"
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"icon": "/assets/potion/icon.png",
|
|
||||||
"label": "Potion",
|
|
||||||
"start": "/assets/potion/start.js"
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
wielder.Health.health += 10
|
|
||||||
item.qty -= 1
|
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"icon": "/assets/tomato-seeds/icon.png",
|
|
||||||
"label": "Tomato Seeds",
|
|
||||||
"projection": {
|
|
||||||
"distance": [1, -1],
|
|
||||||
"grid": [
|
|
||||||
[1, 1, 1],
|
|
||||||
[1, 1, 1],
|
|
||||||
[1, 1, 1]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"projectionCheck": "/assets/tomato-seeds/projection-check.js",
|
|
||||||
"start": "/assets/tomato-seeds/start.js"
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"icon": "/assets/tomato/tomato.png"
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"icon": "/assets/watering-can/icon.png",
|
|
||||||
"label": "Watering Can",
|
|
||||||
"projectionCheck": "/assets/watering-can/projection-check.js",
|
|
||||||
"projection": {
|
|
||||||
"distance": [3, -1],
|
|
||||||
"grid": [
|
|
||||||
[1, 1, 1],
|
|
||||||
[1, 1, 1],
|
|
||||||
[1, 1, 1]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"start": "/assets/watering-can/start.js"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
6
resources/brush/brush.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"icon": "/resources/brush/brush.png",
|
||||||
|
"label": "Brush",
|
||||||
|
"price": 100,
|
||||||
|
"start": "/resources/brush/start.js"
|
||||||
|
}
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
@ -6,7 +6,7 @@ for (const entity of entities) {
|
||||||
Controlled.locked = 1
|
Controlled.locked = 1
|
||||||
const [, direction] = Sprite.animation.split(':')
|
const [, direction] = Sprite.animation.split(':')
|
||||||
for (let i = 0; i < 2; ++i) {
|
for (let i = 0; i < 2; ++i) {
|
||||||
Sound.play('/assets/brush/brush.wav');
|
Sound.play('/resources/brush/brush.wav');
|
||||||
Sprite.animation = ['moving', direction].join(':');
|
Sprite.animation = ['moving', direction].join(':');
|
||||||
await wait(0.3)
|
await wait(0.3)
|
||||||
Sprite.animation = ['idle', direction].join(':');
|
Sprite.animation = ['idle', direction].join(':');
|
||||||
|
@ -14,7 +14,7 @@ for (const entity of entities) {
|
||||||
}
|
}
|
||||||
Inventory.give({
|
Inventory.give({
|
||||||
qty: 1,
|
qty: 1,
|
||||||
source: '/assets/furball/furball.json',
|
source: '/resources/furball/furball.json',
|
||||||
});
|
});
|
||||||
Controlled.locked = 0;
|
Controlled.locked = 0;
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ for (const entity of entities) {
|
||||||
{
|
{
|
||||||
type: 'textureSingle',
|
type: 'textureSingle',
|
||||||
config: {
|
config: {
|
||||||
texture: '/assets/heart/heart.png',
|
texture: '/resources/heart/heart.png',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |