From d6bafb9d16cef14cdce000fd4988a223551f5b58 Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Tue, 21 Apr 2026 11:16:19 +0100 Subject: [PATCH] feat(smart-crafting): add smart crafting table using private chest inventory 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) --- docker-compose.yml | 6 + .../textures/blocks/mailbox.png | Bin 317 -> 270 bytes .../textures/blocks/post_office.png | Bin 317 -> 298 bytes scripts/build-textures.py | 238 ++++++++++++++++++ .../blocks/smart_crafting_table.json | 27 ++ .../smart_crafting_BP/manifest.json | 38 +++ .../smart_crafting_BP/pack_icon.png | Bin 0 -> 971 bytes .../recipes/smart_crafting_table.json | 19 ++ .../smart_crafting_BP/scripts/chest-scan.js | 116 +++++++++ .../smart_crafting_BP/scripts/main.js | 110 ++++++++ .../smart_crafting_BP/scripts/recipes.js | 215 ++++++++++++++++ .../smart_crafting_RP/blocks.json | 4 + .../smart_crafting_RP/manifest.json | 17 ++ .../smart_crafting_RP/pack_icon.png | Bin 0 -> 971 bytes .../smart_crafting_RP/texts/en_US.lang | 1 + .../textures/blocks/smart_crafting_table.png | Bin 0 -> 407 bytes .../textures/terrain_texture.json | 11 + 17 files changed, 802 insertions(+) create mode 100644 scripts/build-textures.py create mode 100644 smart-crafting-addon/smart_crafting_BP/blocks/smart_crafting_table.json create mode 100644 smart-crafting-addon/smart_crafting_BP/manifest.json create mode 100644 smart-crafting-addon/smart_crafting_BP/pack_icon.png create mode 100644 smart-crafting-addon/smart_crafting_BP/recipes/smart_crafting_table.json create mode 100644 smart-crafting-addon/smart_crafting_BP/scripts/chest-scan.js create mode 100644 smart-crafting-addon/smart_crafting_BP/scripts/main.js create mode 100644 smart-crafting-addon/smart_crafting_BP/scripts/recipes.js create mode 100644 smart-crafting-addon/smart_crafting_RP/blocks.json create mode 100644 smart-crafting-addon/smart_crafting_RP/manifest.json create mode 100644 smart-crafting-addon/smart_crafting_RP/pack_icon.png create mode 100644 smart-crafting-addon/smart_crafting_RP/texts/en_US.lang create mode 100644 smart-crafting-addon/smart_crafting_RP/textures/blocks/smart_crafting_table.png create mode 100644 smart-crafting-addon/smart_crafting_RP/textures/terrain_texture.json diff --git a/docker-compose.yml b/docker-compose.yml index 22b1f52..a265497 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,6 +62,8 @@ services: - ./hub-return-addon/hub_return_transfer_BP:/data/behavior_packs/hub_return_transfer_BP - ./private-chest-addon/private_chest_BP:/data/behavior_packs/private_chest_BP - ./private-chest-addon/private_chest_RP:/data/resource_packs/private_chest_RP + - ./smart-crafting-addon/smart_crafting_BP:/data/behavior_packs/smart_crafting_BP + - ./smart-crafting-addon/smart_crafting_RP:/data/resource_packs/smart_crafting_RP - ./home-sign-addon/home_sign_BP:/data/behavior_packs/home_sign_BP - ./home-sign-addon/home_sign_RP:/data/resource_packs/home_sign_RP - ./keep-inventory-addon/keep_inventory_BP:/data/behavior_packs/keep_inventory_BP @@ -100,6 +102,8 @@ services: - ./village-evolution-addon/village_evolution_BP:/data/behavior_packs/village_evolution_BP - ./private-chest-addon/private_chest_BP:/data/behavior_packs/private_chest_BP - ./private-chest-addon/private_chest_RP:/data/resource_packs/private_chest_RP + - ./smart-crafting-addon/smart_crafting_BP:/data/behavior_packs/smart_crafting_BP + - ./smart-crafting-addon/smart_crafting_RP:/data/resource_packs/smart_crafting_RP - ./home-sign-addon/home_sign_BP:/data/behavior_packs/home_sign_BP - ./home-sign-addon/home_sign_RP:/data/resource_packs/home_sign_RP - ./keep-inventory-addon/keep_inventory_BP:/data/behavior_packs/keep_inventory_BP @@ -139,6 +143,8 @@ services: - ./village-evolution-addon/village_evolution_BP:/data/behavior_packs/village_evolution_BP - ./private-chest-addon/private_chest_BP:/data/behavior_packs/private_chest_BP - ./private-chest-addon/private_chest_RP:/data/resource_packs/private_chest_RP + - ./smart-crafting-addon/smart_crafting_BP:/data/behavior_packs/smart_crafting_BP + - ./smart-crafting-addon/smart_crafting_RP:/data/resource_packs/smart_crafting_RP - ./home-sign-addon/home_sign_BP:/data/behavior_packs/home_sign_BP - ./home-sign-addon/home_sign_RP:/data/resource_packs/home_sign_RP - ./keep-inventory-addon/keep_inventory_BP:/data/behavior_packs/keep_inventory_BP diff --git a/postal-service-addon/postal_service_RP/textures/blocks/mailbox.png b/postal-service-addon/postal_service_RP/textures/blocks/mailbox.png index 6608d72bca8bfcffd80319a64ffdbf4ec24f0e77..d2066ab9918da67940dfb687b15d7c29b5629c42 100644 GIT binary patch delta 243 zcmVjzcK8(jF}TLT!0K9PI!A002ovPDHLkV1m$-W{Cg* delta 291 zcmeBU+RHRSxt@{5)5S5QVovgwz=yxjJ1{0BCL|<$FrTdI{rOmMzvPUBImeam@l<^@ z?pP$>U2&K<5v0z)lRH8J2=eTHb|}s?f6BmBAJoSj<_r5FE7quEz`nhtHinWf9vV$X(QBan)$S>Q?CnnL|ZgJr%qT{k%O9J3c-$ zn$vss<%uly+&wIY4<3I0&eQL@M{>{JT1km%KMitje3ewBS3j3^P6r$FVp~xkQ&USdf}9`-*c2E_%fO$>%6X(%ySx$DD`+ z+_6Z$yW%i!B1oNoCwGJd5aikY>`oMDF(8a{95nZwJ^ToMp1*brJ=e4LHP za1C3GUHPO&AUJ6IU5^rer6Cxzz65$N&VMu6{1-oD!M<=>&Y; diff --git a/scripts/build-textures.py b/scripts/build-textures.py new file mode 100644 index 0000000..ff4efe0 --- /dev/null +++ b/scripts/build-textures.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +"""Generate 16x16 block textures for smart_crafting_table, post_office, mailbox.""" + +from pathlib import Path +from PIL import Image + +ROOT = Path(__file__).resolve().parent.parent + +# ── Palette ───────────────────────────────────────────────── +OAK_LIGHT = (150, 110, 65) +OAK_DARK = (115, 80, 45) +OAK_SHADOW = (80, 55, 28) +GOLD = (218, 165, 32) +GOLD_BRIGHT = (255, 214, 85) +REDSTONE = (210, 40, 40) +REDSTONE_BRIGHT = (255, 90, 90) + +BRICK_RED = (138, 58, 48) +BRICK_DARK = (92, 38, 30) +MORTAR = (68, 52, 42) +SLATE = (58, 62, 70) +CREAM = (240, 225, 190) +ENVELOPE_LINE = (45, 30, 20) +STAMP_RED = (190, 50, 50) + +MB_RED = (175, 40, 40) +MB_RED_DARK = (115, 25, 25) +MB_RED_HL = (220, 75, 75) +MB_BLACK = (20, 20, 20) +MB_FLAG = (235, 190, 55) +MB_POST = (90, 60, 35) + + +def save(img: Image.Image, rel: str) -> None: + out = ROOT / rel + out.parent.mkdir(parents=True, exist_ok=True) + img.save(out) + print(f"wrote {rel}") + + +def px(img: Image.Image, x: int, y: int, color) -> None: + if 0 <= x < img.width and 0 <= y < img.height: + img.putpixel((x, y), color) + + +def rect(img: Image.Image, x0: int, y0: int, x1: int, y1: int, color) -> None: + for y in range(y0, y1 + 1): + for x in range(x0, x1 + 1): + px(img, x, y, color) + + +# ── Smart Crafting Table ─────────────────────────────────── +# Top-down style: oak planks, bold gold frame, 2x2 inlay with redstone + diamond. +def smart_crafting_table() -> Image.Image: + img = Image.new("RGBA", (16, 16), OAK_LIGHT) + + # Oak plank background with subtle grain + for y in range(16): + band = (y // 4) % 2 + color = OAK_LIGHT if band == 0 else OAK_DARK + rect(img, 0, y, 15, y, color) + # Knots / grain speckle + for (x, y) in [(3, 1), (11, 2), (6, 5), (13, 6), (2, 9), (9, 10), (5, 13), (12, 14)]: + px(img, x, y, OAK_SHADOW) + # Plank seams + for y in (3, 7, 11): + rect(img, 0, y, 15, y, OAK_SHADOW) + + # Bold gold frame (2px thick) + rect(img, 0, 0, 15, 1, GOLD) + rect(img, 0, 14, 15, 15, GOLD) + rect(img, 0, 0, 1, 15, GOLD) + rect(img, 14, 0, 15, 15, GOLD) + # Inner frame highlight + rect(img, 1, 1, 14, 1, GOLD_BRIGHT) + rect(img, 1, 14, 14, 14, (165, 120, 20)) + rect(img, 1, 2, 1, 13, GOLD_BRIGHT) + rect(img, 14, 2, 14, 13, (165, 120, 20)) + # Corner rivets + for (x, y) in [(0, 0), (15, 0), (0, 15), (15, 15)]: + px(img, x, y, (90, 65, 10)) + + # 2x2 inset panels with gold dividers at mid (x=7..8, y=7..8) + # Top-left = redstone, top-right = diamond, bottom-left = emerald, bottom-right = gold ingot + TL = (3, 3, 6, 6) # redstone area + TR = (9, 3, 12, 6) # diamond area + BL = (3, 9, 6, 12) # emerald area + BR = (9, 9, 12, 12) # gold area + + # Dark recessed wells + for (x0, y0, x1, y1) in (TL, TR, BL, BR): + rect(img, x0, y0, x1, y1, (40, 25, 12)) + + # Redstone gem (red) + rect(img, 4, 4, 5, 5, REDSTONE) + px(img, 4, 4, REDSTONE_BRIGHT) + + # Diamond (cyan) + rect(img, 10, 4, 11, 5, (120, 220, 235)) + px(img, 10, 4, (200, 250, 255)) + + # Emerald (green) + rect(img, 4, 10, 5, 11, (50, 170, 90)) + px(img, 4, 10, (120, 220, 150)) + + # Gold nugget + rect(img, 10, 10, 11, 11, GOLD) + px(img, 10, 10, GOLD_BRIGHT) + + # Gold cross dividers + rect(img, 7, 2, 8, 13, GOLD) + rect(img, 2, 7, 13, 8, GOLD) + # Center jewel + px(img, 7, 7, REDSTONE_BRIGHT) + px(img, 8, 7, REDSTONE_BRIGHT) + px(img, 7, 8, REDSTONE) + px(img, 8, 8, REDSTONE) + + return img + + +# ── Post Office ──────────────────────────────────────────── +# Big envelope front: cream background, bold dark fold lines, red stamp, "POST" bar. +def post_office() -> Image.Image: + img = Image.new("RGBA", (16, 16), CREAM) + + # Outer envelope border (1px) + rect(img, 0, 0, 15, 0, ENVELOPE_LINE) + rect(img, 0, 15, 15, 15, ENVELOPE_LINE) + rect(img, 0, 0, 0, 15, ENVELOPE_LINE) + rect(img, 15, 0, 15, 15, ENVELOPE_LINE) + + # Paper shade (slight gradient on bottom half) + for y in range(8, 15): + for x in range(1, 15): + if (x + y) % 3 == 0: + px(img, x, y, (225, 210, 175)) + + # Envelope fold: top-left and top-right diagonals meeting at center-top + # Lines go from (1,1) -> (7,7) and (14,1) -> (8,7) + for i in range(7): + px(img, 1 + i, 1 + i, ENVELOPE_LINE) + px(img, 14 - i, 1 + i, ENVELOPE_LINE) + # Second pass for thickness + for i in range(6): + px(img, 2 + i, 1 + i, (90, 70, 50)) + px(img, 13 - i, 1 + i, (90, 70, 50)) + + # Horizontal seam at y=8 (where flaps meet paper) + rect(img, 1, 8, 14, 8, ENVELOPE_LINE) + + # Red stamp: top-right square (3x3) with crosshatch + rect(img, 11, 2, 13, 4, STAMP_RED) + px(img, 12, 3, (255, 220, 220)) # stamp center highlight + # Stamp border + px(img, 10, 2, ENVELOPE_LINE) + px(img, 10, 3, ENVELOPE_LINE) + px(img, 10, 4, ENVELOPE_LINE) + px(img, 11, 5, ENVELOPE_LINE) + px(img, 12, 5, ENVELOPE_LINE) + px(img, 13, 5, ENVELOPE_LINE) + + # Address bar: dark rectangle bottom-center reading like lines of text + rect(img, 3, 11, 12, 12, (120, 100, 75)) + rect(img, 3, 13, 10, 13, (120, 100, 75)) + + # "POST" mark: a small red circle stamp bottom-right + rect(img, 11, 11, 13, 13, STAMP_RED) + px(img, 11, 11, (0, 0, 0, 0)) + px(img, 13, 11, (0, 0, 0, 0)) + px(img, 11, 13, (0, 0, 0, 0)) + px(img, 13, 13, (0, 0, 0, 0)) + px(img, 12, 12, (255, 230, 230)) + + return img + + +# ── Mailbox ─────────────────────────────────────────────── +# Classic red pillar-mailbox front: domed top, slot, base, flag. +def mailbox() -> Image.Image: + img = Image.new("RGBA", (16, 16), (0, 0, 0, 0)) + + # Post base (wooden) across bottom + rect(img, 6, 14, 9, 15, MB_POST) + px(img, 6, 14, (70, 45, 25)) + px(img, 9, 14, (70, 45, 25)) + + # Mailbox body (rounded: rows 2..13) + # Body rectangle x=2..13, y=4..13 + rect(img, 2, 4, 13, 13, MB_RED) + # Rounded top corners + px(img, 2, 4, (0, 0, 0, 0)) + px(img, 13, 4, (0, 0, 0, 0)) + # Add a dome/top-bump + rect(img, 3, 2, 12, 3, MB_RED) + px(img, 3, 2, (0, 0, 0, 0)) + px(img, 12, 2, (0, 0, 0, 0)) + rect(img, 4, 1, 11, 1, MB_RED) + rect(img, 5, 0, 10, 0, MB_RED_DARK) + + # Left-side highlight (lighter vertical stripe) + rect(img, 3, 5, 3, 12, MB_RED_HL) + px(img, 4, 2, MB_RED_HL) + + # Right-side shadow + rect(img, 12, 5, 12, 12, MB_RED_DARK) + rect(img, 11, 13, 13, 13, MB_RED_DARK) + + # Black letter slot (horizontal bar) + rect(img, 5, 6, 10, 7, MB_BLACK) + # Slot highlight + rect(img, 5, 6, 10, 6, (50, 50, 50)) + + # Circular latch below slot + px(img, 7, 10, MB_BLACK) + px(img, 8, 10, MB_BLACK) + px(img, 7, 11, MB_BLACK) + px(img, 8, 11, MB_BLACK) + px(img, 7, 10, (90, 90, 90)) + + # Yellow flag on the right side + rect(img, 14, 3, 15, 6, MB_FLAG) + px(img, 14, 3, (180, 140, 40)) + px(img, 15, 6, (180, 140, 40)) + # Flag pole + rect(img, 14, 7, 14, 10, (60, 60, 60)) + + return img + + +def main() -> None: + save(smart_crafting_table(), "smart-crafting-addon/smart_crafting_RP/textures/blocks/smart_crafting_table.png") + save(post_office(), "postal-service-addon/postal_service_RP/textures/blocks/post_office.png") + save(mailbox(), "postal-service-addon/postal_service_RP/textures/blocks/mailbox.png") + + +if __name__ == "__main__": + main() diff --git a/smart-crafting-addon/smart_crafting_BP/blocks/smart_crafting_table.json b/smart-crafting-addon/smart_crafting_BP/blocks/smart_crafting_table.json new file mode 100644 index 0000000..448bcea --- /dev/null +++ b/smart-crafting-addon/smart_crafting_BP/blocks/smart_crafting_table.json @@ -0,0 +1,27 @@ +{ + "format_version": "1.21.0", + "minecraft:block": { + "description": { + "identifier": "silverlabs:smart_crafting_table", + "menu_category": { + "category": "items", + "group": "itemGroup.name.crafting" + } + }, + "components": { + "minecraft:destructible_by_mining": { + "seconds_to_destroy": 2.5 + }, + "minecraft:destructible_by_explosion": { + "explosion_resistance": 20.0 + }, + "minecraft:map_color": "#D4AF37", + "minecraft:material_instances": { + "*": { + "texture": "smart_crafting_table", + "render_method": "opaque" + } + } + } + } +} diff --git a/smart-crafting-addon/smart_crafting_BP/manifest.json b/smart-crafting-addon/smart_crafting_BP/manifest.json new file mode 100644 index 0000000..2daa48a --- /dev/null +++ b/smart-crafting-addon/smart_crafting_BP/manifest.json @@ -0,0 +1,38 @@ +{ + "format_version": 2, + "header": { + "name": "Smart Crafting Table", + "description": "Upgraded crafting table that uses items stored in your private chests", + "uuid": "a4c2e1f8-3b7d-4f9e-a1c5-8d2e7b4f3a91", + "version": [1, 0, 0], + "min_engine_version": [1, 21, 0] + }, + "modules": [ + { + "type": "data", + "uuid": "a4c2e1f8-3b7d-4f9e-a1c5-8d2e7b4f3a92", + "version": [1, 0, 0] + }, + { + "type": "script", + "language": "javascript", + "uuid": "a4c2e1f8-3b7d-4f9e-a1c5-8d2e7b4f3a93", + "version": [1, 0, 0], + "entry": "scripts/main.js" + } + ], + "dependencies": [ + { + "module_name": "@minecraft/server", + "version": "1.17.0" + }, + { + "module_name": "@minecraft/server-ui", + "version": "1.3.0" + }, + { + "uuid": "a4c2e1f8-3b7d-4f9e-a1c5-8d2e7b4f3a94", + "version": [1, 0, 0] + } + ] +} diff --git a/smart-crafting-addon/smart_crafting_BP/pack_icon.png b/smart-crafting-addon/smart_crafting_BP/pack_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a507642d37648f9bf0fb7e5ed2a3229eb2392bb9 GIT binary patch literal 971 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV4mdZ;uumf=j~m?JmElr_K%#W z1cNTSgzR&$QsJ9mCBC5F;boALN{hqGm&&hX6oh2OABbfJw=>P#9Cx$g?7mv#&;RbP z_fmNrG4K55b7{hTa{2!Ac{asW*fac4VkkU**#F$Ru(*C^-oJnTWdAjkc93E0P-ZA( zQRrhhBG_QTB;d~Qhy%Oeosa)!e@U${^;uY>IwgkTwuZYSgY^m*M+Wv6hFnZDG^<5e zHZ=GI3OFbSE>>W0f2?8C^uQ%RAntYp?}IbH4GypsJh81YHf!kSXx6{M(CaF-Pf~ip z%nT5R*F|=pB%_Yrrw?Z>Saj5WJ>WJwz_f|yeh+Uz=(2<2ISpP$eD`&3uvyseyi+}U zcl+$4^2Zf7@$KUfJTT>8f4;P0%bUaI+yxvKDL+jA#p>Gf+&@#vd`9-s&y%U2eJ`AT zz`dO};I-(TBH=G)HJcUQGcI~v_@VVQ+ls%dVvD%H*wk!h$Y*M|tEqqdSKt~jeD3n! z*9l-q{UN9nAW45J^l@#@t)iuahWT*&%y zd*8b6nJ+D!avQ|5cU*wpn{?uE083X^;Ucd%Y~EvB)_<-sn-jsx?5 zD=zrXv8b5;z#D&-y=?-u+%3P%KEw;U)GKY~+Q|6rq}`tr{r0X55qy|2>NJ=0&eL}f z)xMmZvN3%1bm{r^KR=#(Iq}VOrG_{6_W#@U&mxK^lj+1Z*0N49=UEI@COrqwsCR@g ze%ZuY_VEVaN+yHX_DzdIdEEQAFZX{s*=N7FxMkDFQr^d*_Sf$ywEYkEyv5Mvq3E-7 z)ygoQVkHT-3GW09#GXzt_&RCZ!v|UUZT~0QF>HRWeqXu8^14x9{82f_52c)I$ztaD0e F0stcJlKlVx literal 0 HcmV?d00001 diff --git a/smart-crafting-addon/smart_crafting_BP/recipes/smart_crafting_table.json b/smart-crafting-addon/smart_crafting_BP/recipes/smart_crafting_table.json new file mode 100644 index 0000000..edfa62c --- /dev/null +++ b/smart-crafting-addon/smart_crafting_BP/recipes/smart_crafting_table.json @@ -0,0 +1,19 @@ +{ + "format_version": "1.21.0", + "minecraft:recipe_shapeless": { + "description": { + "identifier": "silverlabs:smart_crafting_table_recipe" + }, + "tags": ["crafting_table"], + "unlock": { "context": "AlwaysUnlocked" }, + "ingredients": [ + { "item": "minecraft:crafting_table" }, + { "item": "minecraft:gold_ingot" }, + { "item": "minecraft:redstone" } + ], + "result": { + "item": "silverlabs:smart_crafting_table", + "count": 1 + } + } +} diff --git a/smart-crafting-addon/smart_crafting_BP/scripts/chest-scan.js b/smart-crafting-addon/smart_crafting_BP/scripts/chest-scan.js new file mode 100644 index 0000000..3e89019 --- /dev/null +++ b/smart-crafting-addon/smart_crafting_BP/scripts/chest-scan.js @@ -0,0 +1,116 @@ +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 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; +} diff --git a/smart-crafting-addon/smart_crafting_BP/scripts/main.js b/smart-crafting-addon/smart_crafting_BP/scripts/main.js new file mode 100644 index 0000000..395ec50 --- /dev/null +++ b/smart-crafting-addon/smart_crafting_BP/scripts/main.js @@ -0,0 +1,110 @@ +import { world, system, ItemStack } from "@minecraft/server"; +import { ActionFormData } from "@minecraft/server-ui"; +import { RECIPES } from "./recipes.js"; +import { readOwnedChests, tallyItems, craftableCount, consumeIngredients } from "./chest-scan.js"; + +const SMART_TABLE = "silverlabs:smart_crafting_table"; + +function getPlayerInventory(player) { + try { return player.getComponent("inventory")?.container ?? null; } + catch (_) { return null; } +} + +function giveOrDrop(player, itemStack) { + const inv = getPlayerInventory(player); + if (inv) { + const leftover = inv.addItem(itemStack); + if (!leftover) return; + // inventory full — drop whatever didn't fit at player's feet + try { player.dimension.spawnItem(leftover, player.location); } catch (_) {} + return; + } + try { player.dimension.spawnItem(itemStack, player.location); } catch (_) {} +} + +async function openCraftingUI(player) { + const playerInv = getPlayerInventory(player); + if (!playerInv) { + player.sendMessage(`§c[Smart Crafting] §7Couldn't access your inventory.`); + return; + } + + const { sources, skipped } = readOwnedChests(player); + const totals = tallyItems(playerInv, sources); + + const candidates = []; + for (const recipe of RECIPES) { + const count = craftableCount(recipe, totals); + if (count > 0) candidates.push({ recipe, count }); + } + + const bodyLines = [ + `§7Scanned §f${sources.length}§7 of your chests.`, + ]; + if (skipped.length > 0) bodyLines.push(`§8(${skipped.length} chest${skipped.length === 1 ? "" : "s"} unreachable — chunk not loaded)`); + if (candidates.length === 0) { + bodyLines.push("", "§cNo recipes available with your current items."); + } else { + bodyLines.push(`§7§o${candidates.length} recipe${candidates.length === 1 ? "" : "s"} craftable.`); + } + + const form = new ActionFormData() + .title("§6Smart Crafting") + .body(bodyLines.join("\n")); + + for (const c of candidates) { + form.button(`${c.recipe.name} §7(x${c.count})`); + } + form.button("§cClose"); + + let response; + try { response = await form.show(player); } + catch (_) { return; } + if (response.canceled || response.selection === undefined) return; + if (response.selection >= candidates.length) return; // Close button + + const chosen = candidates[response.selection].recipe; + + // Re-scan to ensure items didn't change while the form was open. + const freshInv = getPlayerInventory(player); + if (!freshInv) { player.sendMessage(`§c[Smart Crafting] §7Inventory became unavailable.`); return; } + const fresh = readOwnedChests(player); + const freshTotals = tallyItems(freshInv, fresh.sources); + if (craftableCount(chosen, freshTotals) < 1) { + player.sendMessage(`§c[Smart Crafting] §7Ingredients for §f${chosen.name}§7 are no longer available.`); + return; + } + + const ok = consumeIngredients(chosen, freshInv, fresh.sources); + if (!ok) { + player.sendMessage(`§c[Smart Crafting] §7Something went wrong consuming ingredients.`); + return; + } + + try { + const result = new ItemStack(chosen.result.item, chosen.result.count); + giveOrDrop(player, result); + } catch (e) { + player.sendMessage(`§c[Smart Crafting] §7Couldn't create result item: ${e.message}`); + return; + } + + player.sendMessage(`§6[Smart Crafting] §7Crafted §f${chosen.name}§7.`); +} + +// ─── Interact: open the crafting UI ───────────────────────── +try { + world.beforeEvents.playerInteractWithBlock.subscribe((event) => { + const block = event.block; + if (!block || block.typeId !== SMART_TABLE) return; + event.cancel = true; + const playerRef = event.player; + system.run(() => openCraftingUI(playerRef)); + }); +} catch (e) { + console.warn(`[Smart Crafting] beforeEvents.playerInteractWithBlock unavailable: ${e}`); +} + +system.run(() => { + world.sendMessage("§6[Smart Crafting] §7Smart crafting table loaded."); +}); diff --git a/smart-crafting-addon/smart_crafting_BP/scripts/recipes.js b/smart-crafting-addon/smart_crafting_BP/scripts/recipes.js new file mode 100644 index 0000000..8129301 --- /dev/null +++ b/smart-crafting-addon/smart_crafting_BP/scripts/recipes.js @@ -0,0 +1,215 @@ +// Recipe catalogue for the Smart Crafting Table. +// Each recipe lists ingredients as { any: [...itemIds], count }. The "any" variant +// lets a single slot accept any matching wood-type etc. + +const PLANKS = [ + "minecraft:oak_planks", + "minecraft:spruce_planks", + "minecraft:birch_planks", + "minecraft:jungle_planks", + "minecraft:acacia_planks", + "minecraft:dark_oak_planks", + "minecraft:mangrove_planks", + "minecraft:cherry_planks", + "minecraft:bamboo_planks", + "minecraft:crimson_planks", + "minecraft:warped_planks", +]; + +const LOGS = [ + "minecraft:oak_log", + "minecraft:spruce_log", + "minecraft:birch_log", + "minecraft:jungle_log", + "minecraft:acacia_log", + "minecraft:dark_oak_log", + "minecraft:mangrove_log", + "minecraft:cherry_log", + "minecraft:crimson_stem", + "minecraft:warped_stem", +]; + +const WOOL = [ + "minecraft:white_wool", + "minecraft:orange_wool", + "minecraft:magenta_wool", + "minecraft:light_blue_wool", + "minecraft:yellow_wool", + "minecraft:lime_wool", + "minecraft:pink_wool", + "minecraft:gray_wool", + "minecraft:light_gray_wool", + "minecraft:cyan_wool", + "minecraft:purple_wool", + "minecraft:blue_wool", + "minecraft:brown_wool", + "minecraft:green_wool", + "minecraft:red_wool", + "minecraft:black_wool", +]; + +const STICK = ["minecraft:stick"]; +const COBBLE = ["minecraft:cobblestone"]; +const IRON = ["minecraft:iron_ingot"]; +const GOLD = ["minecraft:gold_ingot"]; +const DIAMOND = ["minecraft:diamond"]; + +export const RECIPES = [ + // ── Basics ───────────────────────────────────────────────── + { id: "stick", name: "Stick", + result: { item: "minecraft:stick", count: 4 }, + ingredients: [{ any: PLANKS, count: 2 }] }, + + { id: "planks", name: "Wooden Planks (oak)", + result: { item: "minecraft:oak_planks", count: 4 }, + ingredients: [{ any: ["minecraft:oak_log"], count: 1 }] }, + + { id: "crafting_table", name: "Crafting Table", + result: { item: "minecraft:crafting_table", count: 1 }, + ingredients: [{ any: PLANKS, count: 4 }] }, + + { id: "chest", name: "Chest", + result: { item: "minecraft:chest", count: 1 }, + ingredients: [{ any: PLANKS, count: 8 }] }, + + { id: "furnace", name: "Furnace", + result: { item: "minecraft:furnace", count: 1 }, + ingredients: [{ any: COBBLE, count: 8 }] }, + + { id: "torch", name: "Torch", + result: { item: "minecraft:torch", count: 4 }, + ingredients: [ + { any: ["minecraft:coal", "minecraft:charcoal"], count: 1 }, + { any: STICK, count: 1 }, + ] }, + + { id: "ladder", name: "Ladder", + result: { item: "minecraft:ladder", count: 3 }, + ingredients: [{ any: STICK, count: 7 }] }, + + { id: "bucket", name: "Bucket", + result: { item: "minecraft:bucket", count: 1 }, + ingredients: [{ any: IRON, count: 3 }] }, + + { id: "bed", name: "Bed (red)", + result: { item: "minecraft:red_bed", count: 1 }, + ingredients: [ + { any: WOOL, count: 3 }, + { any: PLANKS, count: 3 }, + ] }, + + // ── Tools: wood ──────────────────────────────────────────── + { id: "wood_pickaxe", name: "Wooden Pickaxe", + result: { item: "minecraft:wooden_pickaxe", count: 1 }, + ingredients: [{ any: PLANKS, count: 3 }, { any: STICK, count: 2 }] }, + { id: "wood_axe", name: "Wooden Axe", + result: { item: "minecraft:wooden_axe", count: 1 }, + ingredients: [{ any: PLANKS, count: 3 }, { any: STICK, count: 2 }] }, + { id: "wood_shovel", name: "Wooden Shovel", + result: { item: "minecraft:wooden_shovel", count: 1 }, + ingredients: [{ any: PLANKS, count: 1 }, { any: STICK, count: 2 }] }, + { id: "wood_sword", name: "Wooden Sword", + result: { item: "minecraft:wooden_sword", count: 1 }, + ingredients: [{ any: PLANKS, count: 2 }, { any: STICK, count: 1 }] }, + { id: "wood_hoe", name: "Wooden Hoe", + result: { item: "minecraft:wooden_hoe", count: 1 }, + ingredients: [{ any: PLANKS, count: 2 }, { any: STICK, count: 2 }] }, + + // ── Tools: stone ─────────────────────────────────────────── + { id: "stone_pickaxe", name: "Stone Pickaxe", + result: { item: "minecraft:stone_pickaxe", count: 1 }, + ingredients: [{ any: COBBLE, count: 3 }, { any: STICK, count: 2 }] }, + { id: "stone_axe", name: "Stone Axe", + result: { item: "minecraft:stone_axe", count: 1 }, + ingredients: [{ any: COBBLE, count: 3 }, { any: STICK, count: 2 }] }, + { id: "stone_shovel", name: "Stone Shovel", + result: { item: "minecraft:stone_shovel", count: 1 }, + ingredients: [{ any: COBBLE, count: 1 }, { any: STICK, count: 2 }] }, + { id: "stone_sword", name: "Stone Sword", + result: { item: "minecraft:stone_sword", count: 1 }, + ingredients: [{ any: COBBLE, count: 2 }, { any: STICK, count: 1 }] }, + { id: "stone_hoe", name: "Stone Hoe", + result: { item: "minecraft:stone_hoe", count: 1 }, + ingredients: [{ any: COBBLE, count: 2 }, { any: STICK, count: 2 }] }, + + // ── Tools: iron ──────────────────────────────────────────── + { id: "iron_pickaxe", name: "Iron Pickaxe", + result: { item: "minecraft:iron_pickaxe", count: 1 }, + ingredients: [{ any: IRON, count: 3 }, { any: STICK, count: 2 }] }, + { id: "iron_axe", name: "Iron Axe", + result: { item: "minecraft:iron_axe", count: 1 }, + ingredients: [{ any: IRON, count: 3 }, { any: STICK, count: 2 }] }, + { id: "iron_shovel", name: "Iron Shovel", + result: { item: "minecraft:iron_shovel", count: 1 }, + ingredients: [{ any: IRON, count: 1 }, { any: STICK, count: 2 }] }, + { id: "iron_sword", name: "Iron Sword", + result: { item: "minecraft:iron_sword", count: 1 }, + ingredients: [{ any: IRON, count: 2 }, { any: STICK, count: 1 }] }, + { id: "iron_hoe", name: "Iron Hoe", + result: { item: "minecraft:iron_hoe", count: 1 }, + ingredients: [{ any: IRON, count: 2 }, { any: STICK, count: 2 }] }, + + // ── Tools: gold ──────────────────────────────────────────── + { id: "gold_pickaxe", name: "Golden Pickaxe", + result: { item: "minecraft:golden_pickaxe", count: 1 }, + ingredients: [{ any: GOLD, count: 3 }, { any: STICK, count: 2 }] }, + { id: "gold_sword", name: "Golden Sword", + result: { item: "minecraft:golden_sword", count: 1 }, + ingredients: [{ any: GOLD, count: 2 }, { any: STICK, count: 1 }] }, + + // ── Tools: diamond ───────────────────────────────────────── + { id: "diamond_pickaxe", name: "Diamond Pickaxe", + result: { item: "minecraft:diamond_pickaxe", count: 1 }, + ingredients: [{ any: DIAMOND, count: 3 }, { any: STICK, count: 2 }] }, + { id: "diamond_axe", name: "Diamond Axe", + result: { item: "minecraft:diamond_axe", count: 1 }, + ingredients: [{ any: DIAMOND, count: 3 }, { any: STICK, count: 2 }] }, + { id: "diamond_shovel", name: "Diamond Shovel", + result: { item: "minecraft:diamond_shovel", count: 1 }, + ingredients: [{ any: DIAMOND, count: 1 }, { any: STICK, count: 2 }] }, + { id: "diamond_sword", name: "Diamond Sword", + result: { item: "minecraft:diamond_sword", count: 1 }, + ingredients: [{ any: DIAMOND, count: 2 }, { any: STICK, count: 1 }] }, + + // ── Armor: iron ──────────────────────────────────────────── + { id: "iron_helmet", name: "Iron Helmet", + result: { item: "minecraft:iron_helmet", count: 1 }, + ingredients: [{ any: IRON, count: 5 }] }, + { id: "iron_chestplate", name: "Iron Chestplate", + result: { item: "minecraft:iron_chestplate", count: 1 }, + ingredients: [{ any: IRON, count: 8 }] }, + { id: "iron_leggings", name: "Iron Leggings", + result: { item: "minecraft:iron_leggings", count: 1 }, + ingredients: [{ any: IRON, count: 7 }] }, + { id: "iron_boots", name: "Iron Boots", + result: { item: "minecraft:iron_boots", count: 1 }, + ingredients: [{ any: IRON, count: 4 }] }, + + // ── Armor: diamond ───────────────────────────────────────── + { id: "diamond_helmet", name: "Diamond Helmet", + result: { item: "minecraft:diamond_helmet", count: 1 }, + ingredients: [{ any: DIAMOND, count: 5 }] }, + { id: "diamond_chestplate", name: "Diamond Chestplate", + result: { item: "minecraft:diamond_chestplate", count: 1 }, + ingredients: [{ any: DIAMOND, count: 8 }] }, + { id: "diamond_leggings", name: "Diamond Leggings", + result: { item: "minecraft:diamond_leggings", count: 1 }, + ingredients: [{ any: DIAMOND, count: 7 }] }, + { id: "diamond_boots", name: "Diamond Boots", + result: { item: "minecraft:diamond_boots", count: 1 }, + ingredients: [{ any: DIAMOND, count: 4 }] }, + + // ── Utility ──────────────────────────────────────────────── + { id: "shears", name: "Shears", + result: { item: "minecraft:shears", count: 1 }, + ingredients: [{ any: IRON, count: 2 }] }, + { id: "flint_and_steel", name: "Flint and Steel", + result: { item: "minecraft:flint_and_steel", count: 1 }, + ingredients: [{ any: IRON, count: 1 }, { any: ["minecraft:flint"], count: 1 }] }, + { id: "compass", name: "Compass", + result: { item: "minecraft:compass", count: 1 }, + ingredients: [{ any: IRON, count: 4 }, { any: ["minecraft:redstone"], count: 1 }] }, + { id: "clock", name: "Clock", + result: { item: "minecraft:clock", count: 1 }, + ingredients: [{ any: GOLD, count: 4 }, { any: ["minecraft:redstone"], count: 1 }] }, +]; diff --git a/smart-crafting-addon/smart_crafting_RP/blocks.json b/smart-crafting-addon/smart_crafting_RP/blocks.json new file mode 100644 index 0000000..b904abd --- /dev/null +++ b/smart-crafting-addon/smart_crafting_RP/blocks.json @@ -0,0 +1,4 @@ +{ + "format_version": [1, 1, 0], + "silverlabs:smart_crafting_table": { "sound": "wood" } +} diff --git a/smart-crafting-addon/smart_crafting_RP/manifest.json b/smart-crafting-addon/smart_crafting_RP/manifest.json new file mode 100644 index 0000000..47e02e2 --- /dev/null +++ b/smart-crafting-addon/smart_crafting_RP/manifest.json @@ -0,0 +1,17 @@ +{ + "format_version": 2, + "header": { + "name": "Smart Crafting Table Resources", + "description": "Texture and lang for the silverlabs:smart_crafting_table block", + "uuid": "a4c2e1f8-3b7d-4f9e-a1c5-8d2e7b4f3a94", + "version": [1, 0, 0], + "min_engine_version": [1, 21, 0] + }, + "modules": [ + { + "type": "resources", + "uuid": "a4c2e1f8-3b7d-4f9e-a1c5-8d2e7b4f3a95", + "version": [1, 0, 0] + } + ] +} diff --git a/smart-crafting-addon/smart_crafting_RP/pack_icon.png b/smart-crafting-addon/smart_crafting_RP/pack_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a507642d37648f9bf0fb7e5ed2a3229eb2392bb9 GIT binary patch literal 971 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV4mdZ;uumf=j~m?JmElr_K%#W z1cNTSgzR&$QsJ9mCBC5F;boALN{hqGm&&hX6oh2OABbfJw=>P#9Cx$g?7mv#&;RbP z_fmNrG4K55b7{hTa{2!Ac{asW*fac4VkkU**#F$Ru(*C^-oJnTWdAjkc93E0P-ZA( zQRrhhBG_QTB;d~Qhy%Oeosa)!e@U${^;uY>IwgkTwuZYSgY^m*M+Wv6hFnZDG^<5e zHZ=GI3OFbSE>>W0f2?8C^uQ%RAntYp?}IbH4GypsJh81YHf!kSXx6{M(CaF-Pf~ip z%nT5R*F|=pB%_Yrrw?Z>Saj5WJ>WJwz_f|yeh+Uz=(2<2ISpP$eD`&3uvyseyi+}U zcl+$4^2Zf7@$KUfJTT>8f4;P0%bUaI+yxvKDL+jA#p>Gf+&@#vd`9-s&y%U2eJ`AT zz`dO};I-(TBH=G)HJcUQGcI~v_@VVQ+ls%dVvD%H*wk!h$Y*M|tEqqdSKt~jeD3n! z*9l-q{UN9nAW45J^l@#@t)iuahWT*&%y zd*8b6nJ+D!avQ|5cU*wpn{?uE083X^;Ucd%Y~EvB)_<-sn-jsx?5 zD=zrXv8b5;z#D&-y=?-u+%3P%KEw;U)GKY~+Q|6rq}`tr{r0X55qy|2>NJ=0&eL}f z)xMmZvN3%1bm{r^KR=#(Iq}VOrG_{6_W#@U&mxK^lj+1Z*0N49=UEI@COrqwsCR@g ze%ZuY_VEVaN+yHX_DzdIdEEQAFZX{s*=N7FxMkDFQr^d*_Sf$ywEYkEyv5Mvq3E-7 z)ygoQVkHT-3GW09#GXzt_&RCZ!v|UUZT~0QF>HRWeqXu8^14x9{82f_52c)I$ztaD0e F0stcJlKlVx literal 0 HcmV?d00001 diff --git a/smart-crafting-addon/smart_crafting_RP/texts/en_US.lang b/smart-crafting-addon/smart_crafting_RP/texts/en_US.lang new file mode 100644 index 0000000..f874d3a --- /dev/null +++ b/smart-crafting-addon/smart_crafting_RP/texts/en_US.lang @@ -0,0 +1 @@ +tile.silverlabs:smart_crafting_table.name=Smart Crafting Table diff --git a/smart-crafting-addon/smart_crafting_RP/textures/blocks/smart_crafting_table.png b/smart-crafting-addon/smart_crafting_RP/textures/blocks/smart_crafting_table.png new file mode 100644 index 0000000000000000000000000000000000000000..0bb9084c0f9402ab23b29378185cba7f8ae442fa GIT binary patch literal 407 zcmV;I0cie-P)kU5Y$m3&4XG1)!>#?55%X1Y`pxxjY6`HItS#m;!_VDBbGLTFcT&tgoP^ z?fHUu5eo+aXr{B=1lBy&T)zy-)w(XP3m|0F>wa;$v%C?3aRj0Lk5_-dcGP+9^!jWj zrT}?$NL~%OK8oLO0Z=JEdw{wzqk>6^I7^w?9tUt*`u238QnV||p1AWLsR{wvF2UJE z(cfI{4Ea3k`BU?8&jC)#0aP{99_vrOrJ9ZLSrA@VgjhI8%Np=?T=4}=#<0&p&kX>` zr0iC8di`J1(6#O-sa=kYbmMcT;V&u4Wu*UJ=O4dyY$~Mz78?Kn002ovPDHLkV1lO! Bvl;*Z literal 0 HcmV?d00001 diff --git a/smart-crafting-addon/smart_crafting_RP/textures/terrain_texture.json b/smart-crafting-addon/smart_crafting_RP/textures/terrain_texture.json new file mode 100644 index 0000000..0e15db4 --- /dev/null +++ b/smart-crafting-addon/smart_crafting_RP/textures/terrain_texture.json @@ -0,0 +1,11 @@ +{ + "resource_pack_name": "smart_crafting_RP", + "texture_name": "atlas.terrain", + "padding": 8, + "num_mip_levels": 4, + "texture_data": { + "smart_crafting_table": { + "textures": "textures/blocks/smart_crafting_table" + } + } +}