All checks were successful
Deploy Addons / deploy (push) Successful in 14s
Adds an upgraded crafting block that scans the player's owned private chests and aggregates their contents with the personal inventory when deciding which recipes are craftable. Ingredients are consumed from the player first then from chests; the result goes to the player (or drops at their feet). Also redraws the post_office and mailbox block textures via a new scripts/build-textures.py generator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
3.8 KiB
JavaScript
117 lines
3.8 KiB
JavaScript
import { world } from "@minecraft/server";
|
|
|
|
// Read-only peek into the private-chest addon's world dynamic property.
|
|
const PRIVATE_CHESTS_PROP = "private_chests_v1";
|
|
|
|
function loadRegistry() {
|
|
try {
|
|
const raw = world.getDynamicProperty(PRIVATE_CHESTS_PROP);
|
|
if (raw && typeof raw === "string") return JSON.parse(raw);
|
|
} catch (_) {}
|
|
return {};
|
|
}
|
|
|
|
// Returns [{ container, label }, ...] for every reachable private chest owned by player.
|
|
// Chests in unloaded chunks or that no longer exist are silently skipped.
|
|
export function readOwnedChests(player) {
|
|
const registry = loadRegistry();
|
|
const sources = [];
|
|
const skipped = [];
|
|
|
|
for (const [key, entry] of Object.entries(registry)) {
|
|
if (!entry || entry.ownerId !== player.id) continue;
|
|
const parts = key.split(",");
|
|
if (parts.length < 4) continue;
|
|
const x = parseInt(parts[0], 10);
|
|
const y = parseInt(parts[1], 10);
|
|
const z = parseInt(parts[2], 10);
|
|
const dimId = parts.slice(3).join(",");
|
|
|
|
let dim;
|
|
try { dim = world.getDimension(dimId); } catch (_) { continue; }
|
|
|
|
let block;
|
|
try { block = dim.getBlock({ x, y, z }); } catch (_) { block = null; }
|
|
if (!block) { skipped.push({ x, y, z, dim: dimId }); continue; }
|
|
if (block.typeId !== "minecraft:chest") continue;
|
|
|
|
let inv;
|
|
try { inv = block.getComponent("inventory"); } catch (_) { continue; }
|
|
const container = inv?.container;
|
|
if (!container) continue;
|
|
|
|
sources.push({ container, label: `${x},${y},${z}` });
|
|
}
|
|
|
|
return { sources, skipped };
|
|
}
|
|
|
|
// Build a Map<typeId, totalCount> across the player inventory and chest containers.
|
|
export function tallyItems(playerInv, chestSources) {
|
|
const totals = new Map();
|
|
|
|
const addContainer = (c) => {
|
|
if (!c) return;
|
|
for (let i = 0; i < c.size; i++) {
|
|
const item = c.getItem(i);
|
|
if (!item) continue;
|
|
totals.set(item.typeId, (totals.get(item.typeId) || 0) + item.amount);
|
|
}
|
|
};
|
|
|
|
addContainer(playerInv);
|
|
for (const src of chestSources) addContainer(src.container);
|
|
|
|
return totals;
|
|
}
|
|
|
|
// How many times can a recipe be crafted given the totals map.
|
|
export function craftableCount(recipe, totals) {
|
|
let min = Infinity;
|
|
for (const ing of recipe.ingredients) {
|
|
let available = 0;
|
|
for (const typeId of ing.any) available += totals.get(typeId) || 0;
|
|
const times = Math.floor(available / ing.count);
|
|
if (times < min) min = times;
|
|
if (min === 0) return 0;
|
|
}
|
|
return min === Infinity ? 0 : min;
|
|
}
|
|
|
|
// Remove `count` items matching any of `typeIds` from the sources in order.
|
|
// Returns the number actually removed (caller should verify == count).
|
|
// `sources` is an ordered list of containers: typically [playerInv, ...chestContainers].
|
|
function drain(typeIds, count, sources) {
|
|
let remaining = count;
|
|
for (const container of sources) {
|
|
if (!container) continue;
|
|
for (let i = 0; i < container.size && remaining > 0; i++) {
|
|
const item = container.getItem(i);
|
|
if (!item) continue;
|
|
if (!typeIds.includes(item.typeId)) continue;
|
|
if (item.amount <= remaining) {
|
|
remaining -= item.amount;
|
|
container.setItem(i, undefined);
|
|
} else {
|
|
const clone = item.clone();
|
|
clone.amount = item.amount - remaining;
|
|
container.setItem(i, clone);
|
|
remaining = 0;
|
|
}
|
|
}
|
|
if (remaining === 0) break;
|
|
}
|
|
return count - remaining;
|
|
}
|
|
|
|
// Consume ingredients for a single craft. Returns true on success, false if anything
|
|
// was short (in which case nothing has been consumed — caller should pre-verify).
|
|
export function consumeIngredients(recipe, playerInv, chestSources) {
|
|
const containers = [playerInv, ...chestSources.map((s) => s.container)];
|
|
for (const ing of recipe.ingredients) {
|
|
const removed = drain(ing.any, ing.count, containers);
|
|
if (removed < ing.count) return false;
|
|
}
|
|
return true;
|
|
}
|