feat(hemp): wild patches in plains/forest + chest seed injection

Two new ways to find hemp seeds without already having any:

1. Worldgen: minecraft:scatter_feature spawns 1-3 mature
   silverlabs:hemp_crop blocks on grass/dirt in plains/forest/birch/
   flower_forest biomes (~14% scatter chance per chunk surface pass).
2. Chest injection: 8% chance per chest first-open to plant 1-3 seeds
   in a random empty slot. Tracked per-chest via world dynamic property
   (rolling cap of 500 entries) so each chest only contributes once.

Bedrock has no loot-table merge mechanism so we can't add seeds to
vanilla village chests without losing their vanilla loot — script
injection sidesteps that and stays version-independent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 23:14:54 +01:00
parent cc57662468
commit eb82c8307b
4 changed files with 130 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
{
"format_version": "1.21.0",
"minecraft:feature_rules": {
"description": {
"identifier": "silverlabs:hemp_patch_rule",
"places_feature": "silverlabs:hemp_patch_feature"
},
"conditions": {
"placement_pass": "surface_pass",
"minecraft:biome_filter": [
{
"any_of": [
{ "test": "has_biome_tag", "value": "plains" },
{ "test": "has_biome_tag", "value": "forest" },
{ "test": "has_biome_tag", "value": "birch" },
{ "test": "has_biome_tag", "value": "flower_forest" }
]
},
{ "test": "has_biome_tag", "operator": "!=", "value": "monster" }
]
},
"distribution": {
"iterations": 1,
"scatter_chance": 14,
"x": { "distribution": "uniform", "extent": [0, 16] },
"y": {
"distribution": "fixed_grid",
"extent": [0, 64],
"grid_offset": 0,
"step_size": 1
},
"z": { "distribution": "uniform", "extent": [0, 16] }
}
}
}

View File

@@ -0,0 +1,12 @@
{
"format_version": "1.21.0",
"minecraft:scatter_feature": {
"description": { "identifier": "silverlabs:hemp_patch_feature" },
"iterations": 3,
"scatter_chance": 65,
"x": { "distribution": "uniform", "extent": [-3, 4] },
"y": 0,
"z": { "distribution": "uniform", "extent": [-3, 4] },
"places_feature": "silverlabs:hemp_single_feature"
}
}

View File

@@ -0,0 +1,27 @@
{
"format_version": "1.21.0",
"minecraft:single_block_feature": {
"description": { "identifier": "silverlabs:hemp_single_feature" },
"places_block": {
"name": "silverlabs:hemp_crop",
"states": {
"silverlabs:hemp_age": 4,
"silverlabs:hemp_top": false
}
},
"enforce_survivability_rules": true,
"enforce_placement_rules": true,
"may_attach_to": {
"min_sides_must_attach": 1,
"top": [
"minecraft:grass_block",
"minecraft:dirt",
"minecraft:podzol",
"minecraft:coarse_dirt"
]
},
"may_replace": [
"minecraft:air"
]
}
}

View File

@@ -681,6 +681,62 @@ world.afterEvents.itemCompleteUse.subscribe((event) => {
} }
}); });
// --- Chest loot injection: first-time-open chests sometimes contain seeds ---
// Bedrock has no merge-loot-table mechanism, so we hook chest opens and
// deposit hemp_seeds with low probability into a random empty slot.
// Tracking is per-chest via a world dynamic property holding a JSON list
// of "dim:x:y:z" keys; pruned to a rolling cap to bound storage.
const CHEST_TYPES = new Set(["minecraft:chest", "minecraft:trapped_chest"]);
const CHEST_SEED_CHANCE = 0.08;
const SEEDED_PROP = "hemp_seeded_chests_v1";
const SEEDED_CAP = 500;
function chestKey(block) {
const l = block.location;
return `${block.dimension.id}:${l.x}:${l.y}:${l.z}`;
}
function loadSeededChests() {
try {
const raw = world.getDynamicProperty(SEEDED_PROP);
return new Set(raw ? JSON.parse(raw) : []);
} catch (_) { return new Set(); }
}
function saveSeededChests(set) {
// Prune oldest if over cap (Set preserves insertion order in JS)
if (set.size > SEEDED_CAP) {
const arr = Array.from(set).slice(set.size - SEEDED_CAP);
set = new Set(arr);
}
try {
world.setDynamicProperty(SEEDED_PROP, JSON.stringify(Array.from(set)));
} catch (_) {}
}
world.afterEvents.playerInteractWithBlock.subscribe((event) => {
const block = event.block;
if (!block || !CHEST_TYPES.has(block.typeId)) return;
// Only on the open hand (not while holding an item, to avoid placing-into events firing twice)
const stack = event.itemStack;
if (stack) return;
const key = chestKey(block);
const seeded = loadSeededChests();
if (seeded.has(key)) return;
seeded.add(key);
saveSeededChests(seeded);
if (!chance(CHEST_SEED_CHANCE)) return;
// Try to insert into a random empty slot
let inv;
try { inv = block.getComponent("minecraft:inventory")?.container; } catch (_) { return; }
if (!inv) return;
const empties = [];
for (let i = 0; i < inv.size; i++) if (!inv.getItem(i)) empties.push(i);
if (empties.length === 0) return;
const slot = empties[rand(empties.length)];
const count = 1 + rand(3); // 1-3 seeds
try { inv.setItem(slot, new ItemStack(SEEDS, count)); } catch (_) {}
// No chat ping — let the player discover the seeds organically.
});
system.run(() => { system.run(() => {
world.sendMessage("§a[Hemp] §7Hemp pack loaded."); world.sendMessage("§a[Hemp] §7Hemp pack loaded.");
}); });