// Priority: 1000 const calculateQualityValue = (number, quality, doubled) => { let value; if (quality) { if (quality == 1.0) value = Math.round(number * (doubled ? 1.5 : 1.25)); if (quality == 2.0) value = Math.round(number * (doubled ? 2 : 1.5)); if (quality == 3.0) value = Math.round(number * (doubled ? 3 : 2)); } else { value = number; } return value; }; // Returns array of coins from price, prioritizing high value coins const calculateCoinsFromValue = (price, output, coinMap) => { for (let i = 0; i < coinMap.length; i++) { let { coin, value } = coinMap[i]; if (value <= price) { if (price % value === 0) { output.push({ coin: coin, count: price / value }); return output; } else { output.push({ coin: coin, count: Math.floor(price / value) }); calculateCoinsFromValue(price % value, output, coinMap); } return output; } } }; const textComponentToLegacy = (data) => { const colorMap = { black: "§0", dark_blue: "§1", dark_green: "§2", dark_aqua: "§3", dark_red: "§4", dark_purple: "§5", gold: "§6", gray: "§7", dark_gray: "§8", blue: "§9", green: "§a", aqua: "§b", red: "§c", light_purple: "§d", yellow: "§e", white: "§f" }; let prefix = ""; if (data.color) prefix += colorMap[data.color.toLowerCase()] ?? "§f"; if (data.bold) prefix += "§l"; if (data.italic) prefix += "§o"; if (data.underlined) prefix += "§n"; if (data.strikethrough) prefix += "§m"; if (data.obfuscated) prefix += "§k"; const text = data.text ?? ""; return prefix + text; }; global.getShippingBinName = (nbt, legacy) => { if (nbt && nbt.contains('customName')) { let json = nbt.getString('customName'); let data = JSON.parse(json); if (legacy) { return textComponentToLegacy(data); } else { return Text.of(data); } } return null; }; global.getAttributeMultiplier = (attributes, multiplier) => { return ( Number( attributes.filter((obj) => { return obj.Name === multiplier; })[0]?.Base ) || 1 ); }; global.processShippingBinInventory = ( inventory, inventorySlots, attributes, stages, returnRemoved, simulated ) => { let calculatedValue = 0; let itemValue = 0; let removedItems = []; let slotItem; let isSellable; let isLongwing for (let i = 0; i < inventorySlots; i++) { slotItem = inventory.getStackInSlot(i).item; isLongwing = ["longwings:moth", "longwings:butterfly"].includes(slotItem.id); isSellable = global.trades.has(String(slotItem.id)) || isLongwing || ["splendid_slimes:plort", "splendid_slimes:slime_heart"].includes(slotItem.id); if (isSellable) { let trade = global.trades.get(String(slotItem.id)); let quality = undefined; let doubleQuality = false; let slotNbt = undefined; if (inventory.getStackInSlot(i).hasNBT()) { slotNbt = inventory.getStackInSlot(i).nbt; } if (slotNbt && ((slotNbt.slime && slotNbt.slime.id) || (slotNbt.plort && slotNbt.plort.id))) { if (slotNbt.slime) trade = global.trades.get(`${slotItem.id}/${slotNbt.slime.id}`); if (slotNbt.plort) trade = global.trades.get(`${slotItem.id}/${slotNbt.plort.id}`); } if (slotNbt && isLongwing && slotNbt.variant) { trade = global.trades.get(`longwings:variant/${slotNbt.variant}`); } if (slotNbt && slotNbt.quality_food) { quality = slotNbt.quality_food.quality; } // UPDATE CACHING METHOD WHEN ADDING STAGES if ( quality && quality > 0 && stages.toString().includes("the_quality_of_the_earth") && trade.multiplier.equals("shippingbin:crop_sell_multiplier") && !Item.of(slotItem).hasTag("minecraft:fishes") ) { doubleQuality = true; } itemValue = calculateQualityValue(trade.value, quality, doubleQuality); if ( stages.toString().includes("bluegill_meridian") && slotItem.id == "aquaculture:bluegill" ) { itemValue = calculateQualityValue(666, quality); } if ( stages.toString().includes("phenomenology_of_treasure") && (Item.of(slotItem).hasTag("society:artifacts") || Item.of(slotItem).hasTag("society:relics")) ) { itemValue *= 3; } if ( stages.toString().includes("brine_and_punishment") && Item.of(slotItem).hasTag("society:brine_and_punishment") ) { itemValue *= 2; } if (isLongwing) { let size = 1.0; if (slotNbt.nbt) { if (item.nbt.size && typeof item.nbt.size !== "function") { size = slotNbt.size; } } if (stages.toString().includes("the_metamorphosize")) { itemValue += (Math.round((size / 0.01) * 4) * 3); } else { itemValue += Math.round((size / 0.01) * 4) } } let additionalValue = Math.round( itemValue * inventory.getStackInSlot(i).count * global.getAttributeMultiplier(attributes, trade.multiplier) ); if (isNaN(additionalValue)) { console.log("SHIPPING BIN VALUE IS NAN! DEBUG LOG:") console.log("itemValue: " + itemValue) console.log("attrbMult: " + global.getAttributeMultiplier(attributes, trade.multiplier)) console.log("Trade Value: " + trade.value) calculatedValue += itemValue * inventory.getStackInSlot(i).count } else { calculatedValue += additionalValue } } if (isSellable && !simulated) { if (returnRemoved) removedItems.push(i); else inventory.setStackInSlot(i, "minecraft:air"); } } return { calculatedValue: calculatedValue, removedItems: removedItems }; }; const debugValueProcess = false; global.handleShippingBinDebt = ( value, player, server, block, inventory, extenalOutput ) => { if (!player) return value; let playerUUID = player.getUuid().toString(); let binDebt = 0; let debtPaid; let totalDebt; let receipt; let newValue = value; if (!playerUUID) return value; if (server.persistentData.debts) { binDebt = server.persistentData.debts.filter((debt) => { return debt.uuid === playerUUID; }); } if (binDebt.length > 0 && binDebt[0].amount > 0) { totalDebt = binDebt[0].amount; if (value >= totalDebt) { newValue = value - totalDebt; debtPaid = totalDebt; server.runCommandSilent( global.getEmbersTextAPICommand( player.username, `{anchor:"TOP_LEFT",background:1,color:"#55FF55",size:1,offsetY:36,offsetX:6,typewriter:1,align:"TOP_LEFT"}`, 160, Text.translatable( "society.shipping_bin.debt_paid_all", global.formatPrice(debtPaid.toFixed()) ).toJson() ) ); global.setDebt(server, playerUUID, 0); } else { debtPaid = value; newValue = 0; server.runCommandSilent( global.getEmbersTextAPICommand( player.username, `{anchor:"TOP_LEFT",background:1,color:"#FFFFFF",size:1,offsetY:36,offsetX:6,typewriter:1,align:"TOP_LEFT"}`, 160, Text.translatable( "society.shipping_bin.debt_paid", global.formatPrice(debtPaid.toFixed()) ).toJson() ) ); global.setDebt(server, playerUUID, totalDebt - debtPaid); } } if (debtPaid > 0) { let receiptAuthor = global.translatableWithFallback("society.hospital_receipt.author", "Sunlit Valley Hospital").getString(); let receiptText = Text.translatable( "society.shipping_bin.debt_paid_note", player.username, global.formatPrice(debtPaid.toFixed()), global.formatPrice(totalDebt.toFixed()) ).toJson(); let receiptTitle = global.translatableWithFallback("society.shipping_bin.debt_paid_note.title", "Debt Payment Receipt").getString(); receipt = global.getNotePaperItem(receiptAuthor, receiptText, receiptTitle); if (extenalOutput) { block.popItemFromFace(receipt, block.properties.get("facing")); } else { for (let j = 0; j < inventory.slots; j++) { if (inventory.getStackInSlot(j) === Item.of("minecraft:air")) { inventory.insertItem(j, receipt, false); break; } } } } return newValue; }; global.processValueOutput = ( value, slots, removedSlots, player, server, block, inventory, extenalOutput, ownerUUID ) => { if (value > 0) { let hasRoom = false; value = Math.round( global.handleShippingBinDebt( value, player, server, block, inventory, extenalOutput ) ); let outputs = calculateCoinsFromValue( value, [], extenalOutput ? global.coinMap : basicCoinMap ); if (!outputs) outputs = []; if (debugValueProcess) { console.log(`slots: ${slots}`); console.log(`countNonEmpty: ${inventory.countNonEmpty()}`); console.log(`RemovedSlots: ${removedSlots.length}`); console.log(`calculateSlotsNeeded: ${calculateSlotsNeeded(outputs)}`); } hasRoom = extenalOutput || slots - inventory.countNonEmpty() + removedSlots.length - calculateSlotsNeeded(outputs) >= 0; if (hasRoom) { if (!block.level.hasNeighborSignal(block.pos) && player) { server.runCommandSilent( `playsound etcetera:item.handbell.ring block @a ${player.x} ${player.y} ${player.z} 0.3` ); let customName = global.getShippingBinName(block.getEntityData().data, true); let finalName = customName ? ` ${customName} ` : ""; server.runCommandSilent( global.getEmbersTextAPICommand( player.username, `{anchor:"TOP_LEFT",background:1,color:"#FFFFFF",size:1,offsetY:36,offsetX:6,typewriter:1,align:"TOP_LEFT"}`, 160, Text.translatable( "society.shipping_bin.goods_sold", finalName, global.formatPrice(value.toFixed()), ).toJson() ) ); } if (extenalOutput) { let facing = block.properties.get("facing"); let account = global.GLOBAL_BANK.getAccount(ownerUUID); let card = inventory.getStackInSlot(0); if (card && card.hasTag("numismatics:cards")) { account = global.GLOBAL_BANK.getAccount( card.nbt.getUUID("AccountID") ); } if (account && account.getBalance() + value < 2147483000) { account.deposit(value); } else { outputs.forEach((output) => { let { coin, count } = output; for (let index = 0; index <= count; index += 64) { let difference = count - index; block.popItemFromFace( `${difference > 64 ? 64 : difference}x ${coin}`, facing ); } }); } } else { /** * Basic Shipping Bins only remove items when they're sure there's enough room. * Smart Shipping Bins remove items upon calculation because they output externally. */ for (let i = 0; i < removedSlots.length; i++) { inventory.setStackInSlot(removedSlots[i], "minecraft:air"); } outputs.forEach((output) => { let { coin, count } = output; for (let index = 0; index <= count; index += 64) { let difference = count - index; for (let i = 0; i < slots; i++) { if (inventory.getStackInSlot(i).item.id === "minecraft:air") { inventory.setStackInSlot( i, Item.of(`${difference > 64 ? 64 : difference}x ${coin}`) ); break; } } } }); } } if (!hasRoom && player) { server.runCommandSilent( `playsound stardew_fishing:fish_escape block @a ${player.x} ${player.y} ${player.z} 0.3` ); server.runCommandSilent( global.getEmbersTextAPICommand( player.username, `{anchor:"TOP_LEFT",background:1,color:"#FF5555",size:1,offsetY:36,offsetX:6,typewriter:1,align:"TOP_LEFT"}`, 160, Text.translatable("society.shipping_bin.full").toJson() ) ); } } }; global.cacheShippingBin = (entity) => { const { level, block } = entity; const attributeMapping = [ "shippingbin:crop_sell_multiplier", "shippingbin:wood_sell_multiplier", "shippingbin:gem_sell_multiplier", "shippingbin:meat_sell_multiplier", ]; const stagesToFind = [ "bluegill_meridian", "phenomenology_of_treasure", "brine_and_punishment", "the_quality_of_the_earth", "the_metamorphosize" ]; let binPlayer; level.getServer().players.forEach((p) => { if (p.getUuid().toString() === block.getEntityData().data.owner) { let newAttributes = []; let stages = []; attributeMapping.forEach((attr) => { newAttributes.push({ Base: Number( p.nbt.Attributes.filter((obj) => { return obj.Name === attr; })[0]?.Base ), Name: attr, }); }); stagesToFind.forEach((stage) => { if (p.stages.has(stage)) stages.push(stage); }); let nbt = block.getEntityData(); nbt.merge({ data: { attributes: newAttributes, stages: stages } }); global.setBlockEntityData(block, nbt) binPlayer = p; } }); return binPlayer; };