From 85e252e42373e7170808f09db745bd54ffb005bb Mon Sep 17 00:00:00 2001 From: cha0s Date: Sun, 29 Sep 2024 19:10:49 -0500 Subject: [PATCH] refactor: distribution --- app/ecs/components/inventory.js | 114 +++++++++++++---------- app/react/components/dom/grid.jsx | 1 + app/react/components/dom/slot.jsx | 7 +- app/react/components/dom/slot.module.css | 30 ++++-- app/react/components/ui.jsx | 58 +++++++++++- app/server/create/player.js | 8 ++ app/util/inventory.js | 23 +++++ app/util/inventory.test.js | 19 ++++ resources/potion/potion.json | 1 + 9 files changed, 200 insertions(+), 61 deletions(-) create mode 100644 app/util/inventory.js create mode 100644 app/util/inventory.test.js diff --git a/app/ecs/components/inventory.js b/app/ecs/components/inventory.js index 8967768..d6ad682 100644 --- a/app/ecs/components/inventory.js +++ b/app/ecs/components/inventory.js @@ -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); + 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 {slots} = Inventory; - if (!slots[destination]) { - slots[destination] = { - qty: 0, - source: slots[slot].source, - }; - this.$$items[destination] = new ItemProxy(Component, this, destination); - await this.$$items[destination].load(slots[destination].source); - destinations.push([entityId, destination]); - given.push([entityId, destination]); + // 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 if (slots[slot].source === slots[destination].source) { - destinations.push([entityId, destination]); - updated.push([entityId, destination, slots[destination].qty]); + // update qty of existing + else if (Inventory.slots[destination].qty !== qty) { + Inventory.item(destination).qty = qty; } } - const {qty} = slots[slot]; - slots[slot].qty -= qty; - for (let i = 0; i < qty; ++i) { - const [entityId, destination] = destinations[i % destinations.length]; - const {Inventory} = ecs.get(entityId); - const {slots} = Inventory; - slots[destination].qty += 1; - } - if (0 === slots[slot].qty) { - this.clear(slot); - } - else { - if ( - !destinations.find(([entityId, destination]) => { - return entityId == this.entity && destination === slot; - }) - ) { - Component.markChange(this.entity, 'qtyUpdated', {[slot]: -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; } - } - 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]]); diff --git a/app/react/components/dom/grid.jsx b/app/react/components/dom/grid.jsx index 01805fc..4dfdf89 100644 --- a/app/react/components/dom/grid.jsx +++ b/app/react/components/dom/grid.jsx @@ -44,6 +44,7 @@ export default function Grid({ onDragStart={(event) => { event.preventDefault(); }} + temporary={slot?.temporary} qty={slot?.qty} /> diff --git a/app/react/components/dom/slot.jsx b/app/react/components/dom/slot.jsx index 72f07ab..6306aca 100644 --- a/app/react/components/dom/slot.jsx +++ b/app/react/components/dom/slot.jsx @@ -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 && ( diff --git a/app/react/components/dom/slot.module.css b/app/react/components/dom/slot.module.css index 2599f1b..45a3e9d 100644 --- a/app/react/components/dom/slot.module.css +++ b/app/react/components/dom/slot.module.css @@ -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 + ; + } } diff --git a/app/react/components/ui.jsx b/app/react/components/ui.jsx index 15f052a..263003b 100644 --- a/app/react/components/ui.jsx +++ b/app/react/components/ui.jsx @@ -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) { diff --git a/app/server/create/player.js b/app/server/create/player.js index 46c11de..952d423 100644 --- a/app/server/create/player.js +++ b/app/server/create/player.js @@ -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}, diff --git a/app/util/inventory.js b/app/util/inventory.js new file mode 100644 index 0000000..624c1bd --- /dev/null +++ b/app/util/inventory.js @@ -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; +} + diff --git a/app/util/inventory.test.js b/app/util/inventory.test.js new file mode 100644 index 0000000..f873b66 --- /dev/null +++ b/app/util/inventory.test.js @@ -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); +}); diff --git a/resources/potion/potion.json b/resources/potion/potion.json index 46d23d0..80d3bed 100644 --- a/resources/potion/potion.json +++ b/resources/potion/potion.json @@ -1,6 +1,7 @@ { "icon": "/resources/potion/icon.png", "label": "Potion", + "maximumStack": 100, "price": 50, "start": "/resources/potion/start.js" } \ No newline at end of file