refactor: distribution

This commit is contained in:
cha0s 2024-09-29 19:10:49 -05:00
parent 0bfd4b3a9f
commit 85e252e423
9 changed files with 200 additions and 61 deletions

View File

@ -1,5 +1,7 @@
import Component from '@/ecs/component.js';
import {distribute} from '@/util/inventory.js';
class ItemProxy {
constructor(Component, instance, slot) {
this.Component = Component;
@ -94,6 +96,9 @@ class ItemProxy {
get label() {
return this.json.label;
}
get maximumStack() {
return this.json.maximumStack ?? Infinity;
}
get projection() {
return this.json.projection;
}
@ -105,7 +110,10 @@ class ItemProxy {
}
set qty(qty) {
const {instance} = this;
if (qty <= 0) {
if (qty === this.qty) {
return;
}
else if (qty <= 0) {
instance.clear(this.slot);
}
else {
@ -121,7 +129,7 @@ class ItemProxy {
export default class Inventory extends Component {
async updateMany(entities) {
for (const [id, {cleared, distributed, given, qtyUpdated, swapped}] of entities) {
for (const [id, {cleared, given, qtyUpdated, swapped}] of entities) {
const instance = this.get(id);
const {$$items, slots} = instance;
if (cleared) {
@ -186,56 +194,62 @@ export default class Inventory extends Component {
if (!slots[slot]) {
return;
}
const destinations = [];
const given = [];
const updated = [];
for (const [entityId, destination] of potentialDestinations) {
const destinations = potentialDestinations
.filter(([entityId, destination]) => {
const {Inventory} = ecs.get(entityId);
const {slots} = Inventory;
if (!slots[destination]) {
slots[destination] = {
qty: 0,
source: slots[slot].source,
return (
!Inventory.slots[destination]
|| Inventory.slots[destination].source === slots[slot].source
);
});
const {maximumStack, qty, source} = this.item(slot);
const item = {
maximumStack,
qty,
};
this.$$items[destination] = new ItemProxy(Component, this, destination);
await this.$$items[destination].load(slots[destination].source);
destinations.push([entityId, destination]);
given.push([entityId, destination]);
}
else if (slots[slot].source === slots[destination].source) {
destinations.push([entityId, destination]);
updated.push([entityId, destination, slots[destination].qty]);
}
}
const {qty} = slots[slot];
slots[slot].qty -= qty;
for (let i = 0; i < qty; ++i) {
const [entityId, destination] = destinations[i % destinations.length];
const qtys = distribute(
item,
destinations.map(([entityId, destination]) => {
const {Inventory} = ecs.get(entityId);
const {slots} = Inventory;
slots[destination].qty += 1;
if (entityId == Inventory.entity && destination === slot) {
return 0;
}
if (0 === slots[slot].qty) {
this.clear(slot);
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);
}
else {
}
// 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;
})
) {
Component.markChange(this.entity, 'qtyUpdated', {[slot]: -qty});
if (0 === item.qty) {
this.clear(slot);
}
else if (item.qty !== qty) {
this.item(slot).qty = item.qty;
}
for (const [entityId, destination] of given) {
const {Inventory} = ecs.get(entityId);
const {slots} = Inventory;
Component.markChange(entityId, 'given', {[destination]: {qty: slots[destination].qty, source: slots[destination].source}});
}
for (const [entityId, destination, qty] of updated) {
const {Inventory} = ecs.get(entityId);
const {slots} = Inventory;
Component.markChange(entityId, 'qtyUpdated', {[destination]: slots[destination].qty - qty});
}
}
item(slot) {
@ -269,9 +283,15 @@ export default class Inventory extends Component {
if (undefined === slots[l]) {
delete slots[l];
}
else {
$$items[l].slot = l;
}
if (undefined === otherSlots[r]) {
delete otherSlots[r];
}
else {
$$items[r].slot = r;
}
Component.markChange(this.entity, 'swapped', [[l, OtherInventory.entity, r]]);
if (this.entity !== OtherInventory.entity) {
Component.markChange(OtherInventory.entity, 'swapped', [[r, this.entity, l]]);

View File

@ -44,6 +44,7 @@ export default function Grid({
onDragStart={(event) => {
event.preventDefault();
}}
temporary={slot?.temporary}
qty={slot?.qty}
/>
</div>

View File

@ -13,6 +13,7 @@ export default function Slot({
onMouseDown,
onMouseMove,
onMouseUp,
temporary = false,
qty = 1,
}) {
return (
@ -38,7 +39,11 @@ export default function Slot({
{qty > 1 && (
<span
className={
[styles.qty, `q-${Math.floor(Math.log10(qty))}`]
[
styles.qty,
temporary && styles.temporary,
`q-${Math.floor(Math.log10(qty))}`,
]
.filter(Boolean).join(' ')
}
>

View File

@ -28,24 +28,34 @@
.qty {
bottom: calc(var(--space) * 0.125);
font-family: monospace;
color: white;
font-family: joystix;
font-size: calc(var(--space) * 2);
line-height: 1;
position: absolute;
right: calc(var(--space) * -0.25);
right: calc(var(--space) * -0.5);
text-shadow:
0px -1px 0px white,
1px 0px 0px white,
0px 1px 0px white,
-1px 0px 0px white
0px -1px 0px black,
1px 0px 0px black,
0px 1px 0px black,
-1px 0px 0px black
;
&:global(.q-2) {
font-size: calc(var(--space) * 1.75);
}
&:global(.q-3) {
font-size: calc(var(--space) * 1.5);
}
&:global(.q-4) {
&:global(.q-3) {
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
;
}
}

View File

@ -6,6 +6,7 @@ import {useEcs, useEcsTick} from '@/react/context/ecs.js';
import {useMainEntity} from '@/react/context/main-entity.js';
import {RESOLUTION} from '@/util/constants.js';
import EventEmitter from '@/util/event-emitter.js';
import {distribute} from '@/util/inventory.js';
import addKeyListener from './add-key-listener.js';
import Disconnected from './dom/disconnected.jsx';
@ -268,7 +269,12 @@ function Ui({disconnected}) {
setHotbarSlots(() => {
const newHotbarSlots = [];
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, qty: item.qty}
: undefined,
);
}
return newHotbarSlots;
});
@ -439,11 +445,57 @@ function Ui({disconnected}) {
const hotbarOnSlotMouseMove = useCallback((i) => {
if (hadBufferSlot.current) {
setDistributing((distributing) => ({
[[mainEntityRef.current, i + 1].join(':')]: true,
...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) {

View File

@ -43,6 +43,14 @@ export default async function createPlayer(id) {
qty: 1,
source: '/resources/hoe/hoe.json',
},
6: {
qty: 95,
source: '/resources/potion/potion.json',
},
7: {
qty: 95,
source: '/resources/potion/potion.json',
},
},
},
Health: {health: 100},

23
app/util/inventory.js Normal file
View 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;
}

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

View File

@ -1,6 +1,7 @@
{
"icon": "/resources/potion/icon.png",
"label": "Potion",
"maximumStack": 100,
"price": 50,
"start": "/resources/potion/start.js"
}