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 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,6 +96,9 @@ 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;
} }
@ -105,7 +110,10 @@ class ItemProxy {
} }
set qty(qty) { set qty(qty) {
const {instance} = this; const {instance} = this;
if (qty <= 0) { if (qty === this.qty) {
return;
}
else if (qty <= 0) {
instance.clear(this.slot); instance.clear(this.slot);
} }
else { else {
@ -121,7 +129,7 @@ class ItemProxy {
export default class Inventory extends Component { export default class Inventory extends Component {
async updateMany(entities) { 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 instance = this.get(id);
const {$$items, slots} = instance; const {$$items, slots} = instance;
if (cleared) { if (cleared) {
@ -186,56 +194,62 @@ export default class Inventory extends Component {
if (!slots[slot]) { if (!slots[slot]) {
return; return;
} }
const destinations = []; const destinations = potentialDestinations
const given = []; .filter(([entityId, destination]) => {
const updated = []; const {Inventory} = ecs.get(entityId);
for (const [entityId, destination] of potentialDestinations) { 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); const {Inventory} = ecs.get(entityId);
const {slots} = Inventory; // create new
if (!slots[destination]) { if (!Inventory.slots[destination]) {
slots[destination] = { if (qty > 0) {
qty: 0, const stack = {qty, source};
source: slots[slot].source, Inventory.slots[destination] = {...stack};
}; Inventory.$$items[destination] = new ItemProxy(Component, Inventory, destination);
this.$$items[destination] = new ItemProxy(Component, this, destination); Component.markChange(entityId, 'given', {[destination]: {...stack}});
await this.$$items[destination].load(slots[destination].source); await Inventory.$$items[destination].load(source);
destinations.push([entityId, destination]); }
given.push([entityId, destination]);
} }
else if (slots[slot].source === slots[destination].source) { // update qty of existing
destinations.push([entityId, destination]); else if (Inventory.slots[destination].qty !== qty) {
updated.push([entityId, destination, slots[destination].qty]); Inventory.item(destination).qty = qty;
} }
} }
const {qty} = slots[slot]; // handle source separately if not also destination
slots[slot].qty -= qty; if (
for (let i = 0; i < qty; ++i) { !destinations.find(([entityId, destination]) => {
const [entityId, destination] = destinations[i % destinations.length]; return entityId == this.entity && destination === slot;
const {Inventory} = ecs.get(entityId); })
const {slots} = Inventory; ) {
slots[destination].qty += 1; if (0 === item.qty) {
} this.clear(slot);
if (0 === slots[slot].qty) { }
this.clear(slot); else if (item.qty !== qty) {
} this.item(slot).qty = item.qty;
else {
if (
!destinations.find(([entityId, destination]) => {
return entityId == this.entity && destination === slot;
})
) {
Component.markChange(this.entity, 'qtyUpdated', {[slot]: -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) { item(slot) {
@ -269,9 +283,15 @@ 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) { if (this.entity !== OtherInventory.entity) {
Component.markChange(OtherInventory.entity, 'swapped', [[r, this.entity, l]]); Component.markChange(OtherInventory.entity, 'swapped', [[r, this.entity, l]]);

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ 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 Disconnected from './dom/disconnected.jsx'; import Disconnected from './dom/disconnected.jsx';
@ -268,7 +269,12 @@ function Ui({disconnected}) {
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, qty: item.qty}
: undefined,
);
} }
return newHotbarSlots; return newHotbarSlots;
}); });
@ -439,11 +445,57 @@ function Ui({disconnected}) {
const hotbarOnSlotMouseMove = useCallback((i) => { const hotbarOnSlotMouseMove = useCallback((i) => {
if (hadBufferSlot.current) { if (hadBufferSlot.current) {
setDistributing((distributing) => ({ setDistributing((distributing) => ({
[[mainEntityRef.current, i + 1].join(':')]: true,
...distributing, ...distributing,
})) [[mainEntityRef.current, i + 1].join(':')]: true,
}));
} }
}, [mainEntityRef]); }, [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) => { const hotbarOnSlotMouseUp = useCallback((i, event) => {
keepHotbarOpen(); keepHotbarOpen();
if (trading) { if (trading) {

View File

@ -43,6 +43,14 @@ export default async function createPlayer(id) {
qty: 1, qty: 1,
source: '/resources/hoe/hoe.json', source: '/resources/hoe/hoe.json',
}, },
6: {
qty: 95,
source: '/resources/potion/potion.json',
},
7: {
qty: 95,
source: '/resources/potion/potion.json',
},
}, },
}, },
Health: {health: 100}, 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", "icon": "/resources/potion/icon.png",
"label": "Potion", "label": "Potion",
"maximumStack": 100,
"price": 50, "price": 50,
"start": "/resources/potion/start.js" "start": "/resources/potion/start.js"
} }