Compare commits

..

5 Commits

Author SHA1 Message Date
b28e4d0610 fix(hemp): trader scope to held stack + chest loot guard + wild scatter bridge
Three mid-session fixes to the hemp progression mechanics:

- Trader buyback now operates on the held stack only. Earlier code
  walked the entire inventory to count and consume hemp items, so
  right-clicking with a stack of 64 buds while having more in other
  slots traded everything. Reproduced by user holding 64, ended up
  losing all stacks. Fixed: count = stack.amount, consume modifies
  only the selected hotbar slot. Other stacks of the same item stay
  intact.

- Chest loot handler removed the `if (stack) return` guard. It was
  meant to skip placement events but blocked nearly every legitimate
  chest open (players almost always hold an item). Now every fresh
  chest open rolls; per-chest seeded set still prevents repeats.
  Added a chat ping when seeds drop so players notice.

- Added a script-side wild hemp scatter on a 60s tick to bridge the
  worldgen gap. Bedrock features only fire on FRESH chunk generation,
  so legacy worlds where players have already explored never see the
  feature_rule patches. Scatter picks a random online overworld
  player every minute (40% chance), finds a grass surface within 32
  blocks, places 2-4 mature hemp_crop in a small cluster. Runs in
  parallel with the worldgen feature_rule for new chunks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 01:07:27 +01:00
fd73ac55ec revert(hub-return): drop recovery_compass texture override
All checks were successful
Deploy Addons / deploy (push) Successful in 15s
Three frame-count attempts (256×16 / 512×16 / 16×16) all caused the
same diagonal-line GPU corruption on the compass icon and a strip on
screen. Bedrock's recovery_compass renderer needs metadata beyond the
PNG itself — likely a flipbook_textures.json entry, an animation key
in item_texture.json, or an attachable definition — that this RP
doesn't provide. Without a verified working reference pack to copy
the structure from, every guess corrupts client GPU state.

The directional info already lives on the title-slot HUD (rotating
arrow + distance + label refreshed every 5 ticks), so the icon's
job is just "I'm a compass". Vanilla blue triangle does that fine.

RP bumped 1.0.2 → 1.0.5 across the previous failed attempts to force
client cache flush each time; now pinned at 1.0.5 with an empty
textures/ tree. The pack scaffold stays so future hub-return assets
have a place to land.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:25:45 +01:00
14043fe2a0 fix(addons): silence boot-time warnings on naturalist + dynamite
All checks were successful
Deploy Addons / deploy (push) Successful in 14s
- naturalist-lite: add stub item definitions for silverlabs_nat:frog_leg,
  cooked_frog_leg, snake_egg_block. Owl + snake behaviour files
  reference these but they were never defined, so BDS logged "Unknown
  item during Deferred ItemDescriptor resolution" on every boot. Stubs
  are functional — frog_leg restores 2 hunger, cooked 4, snake egg is
  a placeable nature-tab item. RP texture entries + lang strings added
  for completeness; icons fall back to candy-cane until art lands.
- dynamite: drop the broken spawn_aoe_cloud component on thrown_banger
  (its particle id "minecraft:explosion_manual" doesn't exist in BDS;
  every replacement I tried also failed schema validation). The
  random.fuse hit_sound + impact_damage still fire, the entity is
  destroyed on hit — just no lingering AOE puff.

Also: add *.bak.local to .gitignore so docker-compose.yml.bak.local and
similar local backup files stop showing up in git status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:15:13 +01:00
eb82c8307b 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>
2026-04-27 23:14:54 +01:00
cc57662468 feat(hub-return): rotating-needle recovery compass texture (32 frames)
Replaces vanilla blue right-pointing-triangle with a red/white compass
needle on a lodestone face, rotating through 32 angular positions
(11.25° per frame). RP bumped 1.0.1 → 1.0.2.

Earlier 16-frame attempt caused GPU sampling beyond the texture buffer
(flashing diagonal-line corruption); Bedrock's recovery_compass_atlas
expects 32 frames at 16×16 each = 512×16 total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:14:40 +01:00
12 changed files with 282 additions and 27 deletions

3
.gitignore vendored
View File

@@ -20,6 +20,9 @@ addon/build/
# (LEVEL_NAME, etc.) that must survive deploys. Never committed.
docker-compose.override.yml
# Local-only backup files (e.g. docker-compose.yml.bak.local)
*.bak.local
# Server data (Docker volumes are external, but just in case)
server-data/

View File

@@ -25,12 +25,6 @@
"destroy_on_hit": true,
"semi_random_diff_damage": false
},
"spawn_aoe_cloud": {
"radius": 1.5,
"duration": 0,
"particle": "minecraft:explosion_manual",
"affect_owner": false
},
"remove_on_hit": {}
},
"power": 1.4,

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

@@ -379,32 +379,35 @@ world.beforeEvents.playerInteractWithEntity.subscribe((event) => {
if (!deal) return;
event.cancel = true; // suppress the vanilla trade window for this interaction
const player = event.player;
const held = stack.amount;
system.run(() => {
// Count what the player has of this item across the inventory
const inv = getInv(player);
if (!inv) return;
let have = 0;
for (let i = 0; i < inv.size; i++) {
const it = inv.getItem(i);
if (it && it.typeId === stack.typeId) have += it.amount;
}
if (have < deal.perTrade) {
player.sendMessage(`§7[Trader] Brings me at least §f${deal.perTrade}§7 of those and I'll pay you in emeralds.`);
if (held < deal.perTrade) {
player.sendMessage(`§7[Trader] §fBring me at least §a${deal.perTrade}§f of those and I'll pay in emeralds.`);
return;
}
const trades = Math.floor(have / deal.perTrade);
let consumed = 0;
for (let n = 0; n < trades * deal.perTrade; n++) {
if (!consumeOneOfType(player, stack.typeId)) break;
consumed++;
const inv = getInv(player);
if (!inv) return;
// Trade scope is the held stack only — never walk the rest of the inventory
const sel = player.selectedSlotIndex;
const cur = inv.getItem(sel);
if (!cur || cur.typeId !== stack.typeId) {
// Player swapped item out between the click and this run() — abort safely
return;
}
const actualTrades = Math.floor(consumed / deal.perTrade);
if (actualTrades <= 0) return;
giveItem(player, "minecraft:emerald", actualTrades * deal.emeralds);
const trades = Math.floor(held / deal.perTrade);
const consumed = trades * deal.perTrade;
const remaining = held - consumed;
if (remaining > 0) {
cur.amount = remaining;
inv.setItem(sel, cur);
} else {
inv.setItem(sel, undefined);
}
giveItem(player, "minecraft:emerald", trades * deal.emeralds);
const loc = target.location;
try { target.dimension.runCommand(`particle minecraft:villager_happy ${loc.x} ${loc.y + 1.5} ${loc.z}`); } catch (_) {}
try { target.dimension.runCommand(`playsound mob.villager.yes @a ${loc.x} ${loc.y} ${loc.z} 0.8 1.1`); } catch (_) {}
player.sendMessage(`§a[Trader] §fTraded §e${actualTrades * deal.perTrade}§f for §a${actualTrades * deal.emeralds} emerald${actualTrades * deal.emeralds === 1 ? "" : "s"}§f.`);
player.sendMessage(`§a[Trader] §fTraded §e${consumed}§f for §a${trades * deal.emeralds} emerald${trades * deal.emeralds === 1 ? "" : "s"}§f.`);
});
});
@@ -681,6 +684,129 @@ 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;
// Run on every chest interaction; the per-chest seeded set prevents repeats.
// Earlier `if (stack) return` was over-aggressive — players almost always
// hold an item when opening chests, so it suppressed nearly all opens.
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 (_) {}
try { event.player.sendMessage("§a[Hemp] §7You spot some §dhemp seeds§7 tucked inside the chest."); } catch (_) {}
});
// --- Wild hemp scatter: bridge until worldgen patches show up ---
// Bedrock features only fire on FRESH chunk generation. Pre-existing
// chunks (where the player has already been) won't have patches even
// after we ship the feature_rule. This periodic scatter seeds mature
// hemp_crop on suitable grass blocks in the player's vicinity so the
// mechanic is discoverable in legacy worlds. Cap one patch per minute
// per player to avoid flooding.
const SCATTER_INTERVAL_TICKS = 1200; // 60 s
const SCATTER_CHANCE_PER_TICK = 0.4; // 40% per minute → ~1 patch every 2.5 min
const SCATTER_RADIUS = 32;
const SCATTER_PATCH_SIZE = [2, 4]; // 2-4 plants per patch
function tryScatterHempNearPlayer(player) {
const dim = player.dimension;
if (dim.id !== "minecraft:overworld") return;
const px = Math.floor(player.location.x);
const py = Math.floor(player.location.y);
const pz = Math.floor(player.location.z);
// Sample up to 12 candidate locations; pick the first viable grass block
for (let attempt = 0; attempt < 12; attempt++) {
const dx = rand(SCATTER_RADIUS * 2 + 1) - SCATTER_RADIUS;
const dz = rand(SCATTER_RADIUS * 2 + 1) - SCATTER_RADIUS;
// Search vertically: top-down within ±6 of player y for the surface
let surface = null;
for (let dy = 6; dy >= -6; dy--) {
let b;
try { b = dim.getBlock({ x: px + dx, y: py + dy, z: pz + dz }); } catch (_) { continue; }
if (!b) continue;
if (b.typeId === "minecraft:grass_block") {
// Check above is air (and skylit-ish — not a cave with grass_path glitches)
let above;
try { above = dim.getBlock({ x: px + dx, y: py + dy + 1, z: pz + dz }); } catch (_) { continue; }
if (above && above.isAir) { surface = { x: px + dx, y: py + dy + 1, z: pz + dz }; break; }
}
}
if (!surface) continue;
// Place 2-4 mature hemp_crop in a small cluster around surface
const count = SCATTER_PATCH_SIZE[0] + rand(SCATTER_PATCH_SIZE[1] - SCATTER_PATCH_SIZE[0] + 1);
let placed = 0;
for (let i = 0; i < count * 3 && placed < count; i++) {
const ox = rand(5) - 2;
const oz = rand(5) - 2;
const tx = surface.x + ox, ty = surface.y, tz = surface.z + oz;
let target, ground;
try {
target = dim.getBlock({ x: tx, y: ty, z: tz });
ground = dim.getBlock({ x: tx, y: ty - 1, z: tz });
} catch (_) { continue; }
if (!target || !ground) continue;
if (!target.isAir) continue;
if (ground.typeId !== "minecraft:grass_block" && ground.typeId !== "minecraft:dirt") continue;
try {
const perm = BlockPermutation.resolve(CROP, { [AGE]: 4, [TOP]: false });
target.setPermutation(perm);
placed++;
} catch (_) {}
}
return; // one patch per tick
}
}
system.runInterval(() => {
const players = world.getAllPlayers();
if (players.length === 0) return;
const player = players[rand(players.length)];
if (!chance(SCATTER_CHANCE_PER_TICK)) return;
tryScatterHempNearPlayer(player);
}, SCATTER_INTERVAL_TICKS);
system.run(() => {
world.sendMessage("§a[Hemp] §7Hemp pack loaded.");
});

View File

@@ -7,7 +7,7 @@
"version": [
1,
0,
1
5
],
"min_engine_version": [
1,
@@ -22,7 +22,7 @@
"version": [
1,
0,
1
5
]
}
]

View File

@@ -0,0 +1,16 @@
{
"format_version": "1.21.0",
"minecraft:item": {
"description": {
"identifier": "silverlabs_nat:cooked_frog_leg",
"menu_category": { "category": "items", "group": "itemGroup.name.miscFood" }
},
"components": {
"minecraft:icon": "silverlabs_nat.cooked_frog_leg",
"minecraft:max_stack_size": 64,
"minecraft:food": { "nutrition": 4, "saturation_modifier": 1.2 },
"minecraft:use_animation": "eat",
"minecraft:use_modifiers": { "use_duration": 1.6, "movement_modifier": 0.35 }
}
}
}

View File

@@ -0,0 +1,16 @@
{
"format_version": "1.21.0",
"minecraft:item": {
"description": {
"identifier": "silverlabs_nat:frog_leg",
"menu_category": { "category": "items", "group": "itemGroup.name.miscFood" }
},
"components": {
"minecraft:icon": "silverlabs_nat.frog_leg",
"minecraft:max_stack_size": 64,
"minecraft:food": { "nutrition": 2, "saturation_modifier": 0.6 },
"minecraft:use_animation": "eat",
"minecraft:use_modifiers": { "use_duration": 1.6, "movement_modifier": 0.35 }
}
}
}

View File

@@ -0,0 +1,13 @@
{
"format_version": "1.21.0",
"minecraft:item": {
"description": {
"identifier": "silverlabs_nat:snake_egg_block",
"menu_category": { "category": "nature" }
},
"components": {
"minecraft:icon": "silverlabs_nat.snake_egg_block",
"minecraft:max_stack_size": 16
}
}
}

View File

@@ -16,4 +16,8 @@ entity.silverlabs_nat:coral_snake_egg.name=Coral Snake Egg
tile.silverlabs_nat:cave_snake_egg.name=Cave Snake Egg
tile.silverlabs_nat:coral_snake_egg.name=Coral Snake Egg
item.silverlabs_nat:frog_leg.name=Frog Leg
item.silverlabs_nat:cooked_frog_leg.name=Cooked Frog Leg
item.silverlabs_nat:snake_egg_block.name=Snake Egg

View File

@@ -94,6 +94,15 @@
},
"silverlabs_nat.tiger_spawn_egg": {
"textures": "textures/sf/nba/items/tiger_default_spawn_egg"
},
"silverlabs_nat.frog_leg": {
"textures": "textures/sf/nba/items/frog_leg"
},
"silverlabs_nat.cooked_frog_leg": {
"textures": "textures/sf/nba/items/cooked_frog_leg"
},
"silverlabs_nat.snake_egg_block": {
"textures": "textures/sf/nba/items/snake_egg_block"
}
}
}