Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fb2b27d7c | ||
|
|
1e82825f8f | ||
|
|
db8931784e | ||
|
|
8ded29cbcc | ||
|
|
b28e4d0610 | ||
|
|
fd73ac55ec | ||
|
|
14043fe2a0 | ||
|
|
eb82c8307b | ||
|
|
cc57662468 | ||
|
|
17a9faf206 | ||
|
|
9789906277 | ||
|
|
6bda72598d | ||
|
|
af9d37462c | ||
|
|
7c8cd5b075 | ||
|
|
b9e3380f6c | ||
|
|
145f5d9beb |
@@ -14,6 +14,8 @@ on:
|
||||
- 'keep-inventory-addon/**'
|
||||
- 'postal-service-addon/**'
|
||||
- 'camping-supplies-addon/**'
|
||||
- 'family-goals-addon/**'
|
||||
- 'redstone-link-addon/**'
|
||||
- 'docker-compose.yml'
|
||||
- 'scripts/**'
|
||||
|
||||
@@ -31,7 +33,12 @@ jobs:
|
||||
script: |
|
||||
set -e
|
||||
APP_DIR="$HOME/minecraft-multiworld"
|
||||
PATHS="addon/ lobby-addon/ hub-return-addon/ village-evolution-addon/ monkey-addon/ private-chest-addon/ home-sign-addon/ keep-inventory-addon/ postal-service-addon/ camping-supplies-addon/ docker-compose.yml"
|
||||
# Anything mounted in docker-compose.yml but missing here is simply
|
||||
# never refreshed by CI. family-goals-addon and redstone-link-addon
|
||||
# are listed because docker-compose.yml mounts them; omitting either
|
||||
# while checking out docker-compose.yml would leave a mount pointing
|
||||
# at a directory this job never delivers.
|
||||
PATHS="addon/ lobby-addon/ hub-return-addon/ village-evolution-addon/ monkey-addon/ private-chest-addon/ home-sign-addon/ keep-inventory-addon/ postal-service-addon/ camping-supplies-addon/ family-goals-addon/ redstone-link-addon/ docker-compose.yml"
|
||||
|
||||
# First run: clone. Subsequent: pull.
|
||||
if [ ! -d "$APP_DIR/.git" ]; then
|
||||
|
||||
@@ -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/
|
||||
|
||||
|
||||
@@ -2,22 +2,38 @@
|
||||
"format_version": 2,
|
||||
"header": {
|
||||
"name": "Hey Hey Chicken Pet",
|
||||
"description": "Hey Hey the loyal Moana chicken — tameable, following pet with waddly personality.",
|
||||
"description": "Hey Hey the loyal Moana chicken \u2014 tameable, following pet with waddly personality.",
|
||||
"uuid": "fc811a53-dbdb-4701-bc63-c3ca1d793c47",
|
||||
"version": [1, 0, 0],
|
||||
"min_engine_version": [1, 21, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min_engine_version": [
|
||||
1,
|
||||
21,
|
||||
0
|
||||
]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "data",
|
||||
"uuid": "f7aae24a-06d8-432b-85dc-359c4fda9dfd",
|
||||
"version": [1, 0, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"uuid": "45ec17df-0d84-456f-b2cc-2d990f96e6d5",
|
||||
"version": [1, 0, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,22 +2,38 @@
|
||||
"format_version": 2,
|
||||
"header": {
|
||||
"name": "Pets 4 Jamie's STARS",
|
||||
"description": "Tameable dragon pets with eggs, gravestones and personality — built for Jamie's STARS!",
|
||||
"description": "Tameable dragon pets with eggs, gravestones and personality \u2014 built for Jamie's STARS!",
|
||||
"uuid": "7cf924ce-e246-4c8c-998c-f420edb26451",
|
||||
"version": [1, 0, 0],
|
||||
"min_engine_version": [1, 21, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min_engine_version": [
|
||||
1,
|
||||
21,
|
||||
0
|
||||
]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "data",
|
||||
"uuid": "6806e843-c0b4-4a4d-9cb3-d5352e396fe5",
|
||||
"version": [1, 0, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"uuid": "5f25d547-00bb-49ce-8be3-d86cd3941c9b",
|
||||
"version": [1, 0, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
{ "fire_breathing": "query.property('silverlabs:firing')" },
|
||||
{ "flying": "!query.is_on_ground && query.property('silverlabs:growth_stage') == 2" },
|
||||
{ "walking": "query.modified_move_speed > 0.1" },
|
||||
{ "sleeping": "query.is_sitting && (query.day_light_level < 4)" },
|
||||
{ "sleeping": "query.is_sitting && (query.time_of_day > 0.5)" },
|
||||
{ "sitting": "query.is_sitting" },
|
||||
{ "grooming": "query.is_on_ground && query.modified_move_speed <= 0.05 && math.mod(math.floor((query.life_time + math.mod(math.abs(math.floor(query.position(0) + query.position(2))), 5) * 36.0) / 36.0), 5) == 1" },
|
||||
{ "sniffing": "query.is_on_ground && query.modified_move_speed <= 0.15 && math.mod(math.floor((query.life_time + math.mod(math.abs(math.floor(query.position(0) + query.position(2))), 5) * 36.0) / 36.0), 5) == 2" },
|
||||
@@ -52,14 +52,14 @@
|
||||
"animations": ["sit"],
|
||||
"transitions": [
|
||||
{ "idle": "!query.is_sitting" },
|
||||
{ "sleeping": "query.is_sitting && (query.day_light_level < 4)" }
|
||||
{ "sleeping": "query.is_sitting && (query.time_of_day > 0.5)" }
|
||||
]
|
||||
},
|
||||
"sleeping": {
|
||||
"animations": ["sleep"],
|
||||
"transitions": [
|
||||
{ "idle": "!query.is_sitting" },
|
||||
{ "sitting": "query.is_sitting && (query.day_light_level >= 4)" }
|
||||
{ "sitting": "query.is_sitting && (query.time_of_day <= 0.5)" }
|
||||
]
|
||||
},
|
||||
"flying": {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"particle_effects": {
|
||||
"beacon_beam": "minecraft:endrod",
|
||||
"mood_happy": "minecraft:villager_happy",
|
||||
"mood_needy": "minecraft:large_smoke"
|
||||
"mood_needy": "minecraft:basic_smoke_particle"
|
||||
},
|
||||
"spawn_egg": {
|
||||
"base_color": "#7B2FBE",
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"name": "Pets 4 Jamie's STARS Resources",
|
||||
"description": "Textures, models and animations for Pets 4 Jamie's STARS",
|
||||
"uuid": "5f25d547-00bb-49ce-8be3-d86cd3941c9b",
|
||||
"version": [1, 0, 0],
|
||||
"version": [1, 0, 1],
|
||||
"min_engine_version": [1, 21, 0]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "resources",
|
||||
"uuid": "6adc20cd-25a5-4ca5-aa12-f5e43ee9ea22",
|
||||
"version": [1, 0, 0]
|
||||
"version": [1, 0, 1]
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"explosion_resistance": 1.0
|
||||
},
|
||||
"minecraft:map_color": "#547A4E",
|
||||
"minecraft:geometry": "minecraft:geometry.full_block",
|
||||
"minecraft:material_instances": {
|
||||
"*": {
|
||||
"texture": "tent_canvas",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:item": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:ore_detector_advanced",
|
||||
"menu_category": {
|
||||
"category": "equipment",
|
||||
"group": "itemGroup.name.miscellaneous"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"minecraft:max_stack_size": 1,
|
||||
"minecraft:icon": "ore_detector_advanced",
|
||||
"minecraft:hand_equipped": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:item": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:ore_detector_basic",
|
||||
"menu_category": {
|
||||
"category": "equipment",
|
||||
"group": "itemGroup.name.miscellaneous"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"minecraft:max_stack_size": 1,
|
||||
"minecraft:icon": "ore_detector_basic",
|
||||
"minecraft:hand_equipped": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:item": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:ore_detector_improved",
|
||||
"menu_category": {
|
||||
"category": "equipment",
|
||||
"group": "itemGroup.name.miscellaneous"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"minecraft:max_stack_size": 1,
|
||||
"minecraft:icon": "ore_detector_improved",
|
||||
"minecraft:hand_equipped": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,36 @@
|
||||
"name": "Camping Supplies",
|
||||
"description": "Craftable tent and hammock for overnight camping without setting your spawn point",
|
||||
"uuid": "bcf569fa-8b2c-403e-9f75-6b405132c5cd",
|
||||
"version": [1, 0, 0],
|
||||
"min_engine_version": [1, 21, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min_engine_version": [
|
||||
1,
|
||||
21,
|
||||
0
|
||||
]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "data",
|
||||
"uuid": "f306e1d8-3c13-4554-9715-4799ce6d41d8",
|
||||
"version": [1, 0, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "script",
|
||||
"language": "javascript",
|
||||
"uuid": "1e496657-0c83-4707-a1e8-29b757dcce79",
|
||||
"version": [1, 0, 0],
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"entry": "scripts/main.js"
|
||||
}
|
||||
],
|
||||
@@ -28,7 +44,11 @@
|
||||
},
|
||||
{
|
||||
"uuid": "36f12107-10c6-484c-a0f2-b5dd88cd5baa",
|
||||
"version": [1, 0, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:recipe_shapeless": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:ore_detector_advanced_recipe"
|
||||
},
|
||||
"tags": [
|
||||
"crafting_table"
|
||||
],
|
||||
"unlock": [
|
||||
{
|
||||
"item": "silverlabs:ore_detector_improved"
|
||||
}
|
||||
],
|
||||
"ingredients": [
|
||||
{ "item": "silverlabs:ore_detector_improved" },
|
||||
{ "item": "minecraft:gold_ingot" },
|
||||
{ "item": "minecraft:gold_ingot" },
|
||||
{ "item": "minecraft:amethyst_shard" },
|
||||
{ "item": "minecraft:amethyst_shard" }
|
||||
],
|
||||
"result": {
|
||||
"item": "silverlabs:ore_detector_advanced",
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:recipe_shaped": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:ore_detector_basic_recipe"
|
||||
},
|
||||
"tags": [
|
||||
"crafting_table"
|
||||
],
|
||||
"unlock": [
|
||||
{
|
||||
"item": "minecraft:redstone"
|
||||
}
|
||||
],
|
||||
"pattern": [
|
||||
" R ",
|
||||
"CSC",
|
||||
" C "
|
||||
],
|
||||
"key": {
|
||||
"R": {
|
||||
"item": "minecraft:redstone"
|
||||
},
|
||||
"C": {
|
||||
"item": "minecraft:cobblestone"
|
||||
},
|
||||
"S": {
|
||||
"item": "minecraft:stick"
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"item": "silverlabs:ore_detector_basic",
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:recipe_shapeless": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:ore_detector_improved_recipe"
|
||||
},
|
||||
"tags": [
|
||||
"crafting_table"
|
||||
],
|
||||
"unlock": [
|
||||
{
|
||||
"item": "silverlabs:ore_detector_basic"
|
||||
}
|
||||
],
|
||||
"ingredients": [
|
||||
{ "item": "silverlabs:ore_detector_basic" },
|
||||
{ "item": "minecraft:iron_ingot" },
|
||||
{ "item": "minecraft:iron_ingot" },
|
||||
{ "item": "minecraft:iron_ingot" },
|
||||
{ "item": "minecraft:iron_ingot" },
|
||||
{ "item": "minecraft:redstone" }
|
||||
],
|
||||
"result": {
|
||||
"item": "silverlabs:ore_detector_improved",
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -703,6 +703,116 @@ function dismantleHammockAt(loc, dimId, player) {
|
||||
if (player) player.sendMessage("§7[Camping] Hammock taken down.");
|
||||
}
|
||||
|
||||
// ─── Ore Detector ───────────────────────────────────────────
|
||||
// Three tiered items share one scan handler. Tier → max scan range.
|
||||
// Faraday: any ore within 4 blocks of a silverlabs:private_chest is hidden
|
||||
// (mirrors the source DetectOre faraday-cage mechanic, but built into private chests).
|
||||
const DETECTOR_RANGES = {
|
||||
"silverlabs:ore_detector_basic": 8,
|
||||
"silverlabs:ore_detector_improved": 16,
|
||||
"silverlabs:ore_detector_advanced": 32,
|
||||
};
|
||||
const PRIVATE_CHEST_ID = "silverlabs:private_chest";
|
||||
const FARADAY_RADIUS = 4; // 9³ cube around each ore candidate
|
||||
|
||||
const ORE_IDS = new Set([
|
||||
"minecraft:coal_ore", "minecraft:deepslate_coal_ore",
|
||||
"minecraft:iron_ore", "minecraft:deepslate_iron_ore",
|
||||
"minecraft:copper_ore", "minecraft:deepslate_copper_ore",
|
||||
"minecraft:gold_ore", "minecraft:deepslate_gold_ore", "minecraft:nether_gold_ore",
|
||||
"minecraft:redstone_ore", "minecraft:deepslate_redstone_ore",
|
||||
"minecraft:lit_redstone_ore", "minecraft:lit_deepslate_redstone_ore",
|
||||
"minecraft:lapis_ore", "minecraft:deepslate_lapis_ore",
|
||||
"minecraft:emerald_ore", "minecraft:deepslate_emerald_ore",
|
||||
"minecraft:diamond_ore", "minecraft:deepslate_diamond_ore",
|
||||
"minecraft:nether_quartz_ore",
|
||||
"minecraft:ancient_debris",
|
||||
]);
|
||||
|
||||
function prettyOreName(typeId) {
|
||||
const bare = typeId.replace(/^minecraft:/, "")
|
||||
.replace(/^lit_/, "")
|
||||
.replace(/^deepslate_/, "Deepslate ")
|
||||
.replace(/^nether_/, "Nether ");
|
||||
return bare.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
||||
}
|
||||
|
||||
function isPrivateChestNearby(dim, cx, cy, cz) {
|
||||
for (let dx = -FARADAY_RADIUS; dx <= FARADAY_RADIUS; dx++) {
|
||||
for (let dy = -FARADAY_RADIUS; dy <= FARADAY_RADIUS; dy++) {
|
||||
for (let dz = -FARADAY_RADIUS; dz <= FARADAY_RADIUS; dz++) {
|
||||
const b = dim.getBlock({ x: cx + dx, y: cy + dy, z: cz + dz });
|
||||
if (b && b.typeId === PRIVATE_CHEST_ID) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function runOreScan(player, range) {
|
||||
const dim = player.dimension;
|
||||
const head = player.getHeadLocation();
|
||||
const dir = player.getViewDirection();
|
||||
|
||||
// Walk integer-block steps along the view ray. Resolution 0.5 blocks
|
||||
// catches ores the cardinal-aligned steps would skip on diagonals.
|
||||
const stepCount = Math.floor(range * 2);
|
||||
const seen = new Set();
|
||||
let foundOre = null;
|
||||
let foundDist = 0;
|
||||
|
||||
for (let i = 1; i <= stepCount; i++) {
|
||||
const t = i * 0.5;
|
||||
const px = head.x + dir.x * t;
|
||||
const py = head.y + dir.y * t;
|
||||
const pz = head.z + dir.z * t;
|
||||
const bx = Math.floor(px);
|
||||
const by = Math.floor(py);
|
||||
const bz = Math.floor(pz);
|
||||
const key = `${bx},${by},${bz}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
|
||||
let block;
|
||||
try { block = dim.getBlock({ x: bx, y: by, z: bz }); } catch (_) { continue; }
|
||||
if (!block) continue;
|
||||
|
||||
if (ORE_IDS.has(block.typeId)) {
|
||||
if (isPrivateChestNearby(dim, bx, by, bz)) continue; // faraday
|
||||
foundOre = block.typeId;
|
||||
foundDist = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundOre) {
|
||||
const distRounded = Math.round(foundDist * 10) / 10;
|
||||
const pitch = Math.max(0.8, Math.min(2.0, 2.0 - (foundDist / range)));
|
||||
try {
|
||||
player.playSound("random.orb", { pitch, volume: 0.7 });
|
||||
} catch (_) {}
|
||||
try {
|
||||
player.onScreenDisplay.setActionBar(`§a● §f${prettyOreName(foundOre)} §7at §b${distRounded}m`);
|
||||
} catch (_) {}
|
||||
} else {
|
||||
try {
|
||||
player.playSound("note.bass", { pitch: 0.7, volume: 0.5 });
|
||||
} catch (_) {}
|
||||
try {
|
||||
player.onScreenDisplay.setActionBar(`§7○ No ores within §f${range}m`);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
world.afterEvents.itemUse.subscribe((event) => {
|
||||
const player = event.source;
|
||||
const stack = event.itemStack;
|
||||
if (!stack || !player) return;
|
||||
const range = DETECTOR_RANGES[stack.typeId];
|
||||
if (!range) return;
|
||||
system.run(() => runOreScan(player, range));
|
||||
});
|
||||
|
||||
// ─── Boot ───────────────────────────────────────────────────
|
||||
system.run(() => {
|
||||
loadState();
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"name": "Camping Supplies Resources",
|
||||
"description": "Textures and lang for camping supplies (tent, hammock, canvas, cloth)",
|
||||
"uuid": "36f12107-10c6-484c-a0f2-b5dd88cd5baa",
|
||||
"version": [1, 0, 0],
|
||||
"version": [1, 0, 1],
|
||||
"min_engine_version": [1, 21, 0]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "resources",
|
||||
"uuid": "c9ee429f-9374-4083-843b-4b195e8db130",
|
||||
"version": [1, 0, 0]
|
||||
"version": [1, 0, 1]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
item.silverlabs:tent.name=Tent
|
||||
item.silverlabs:hammock.name=Hammock
|
||||
item.silverlabs:ore_detector_basic.name=Ore Detector
|
||||
item.silverlabs:ore_detector_improved.name=Ore Detector (Improved)
|
||||
item.silverlabs:ore_detector_advanced.name=Ore Detector (Advanced)
|
||||
tile.silverlabs:tent_canvas.name=Tent Canvas
|
||||
tile.silverlabs:hammock_cloth.name=Hammock Cloth
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
},
|
||||
"hammock_item": {
|
||||
"textures": "textures/items/hammock"
|
||||
},
|
||||
"ore_detector_basic": {
|
||||
"textures": "textures/items/ore_detector_basic"
|
||||
},
|
||||
"ore_detector_improved": {
|
||||
"textures": "textures/items/ore_detector_improved"
|
||||
},
|
||||
"ore_detector_advanced": {
|
||||
"textures": "textures/items/ore_detector_advanced"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 228 B |
|
After Width: | Height: | Size: 227 B |
|
After Width: | Height: | Size: 227 B |
@@ -38,6 +38,8 @@ services:
|
||||
- ./dynamite-addon/dynamite_RP:/data/resource_packs/dynamite_RP
|
||||
- ./tow-boat-addon/tow_boat_BP:/data/behavior_packs/tow_boat_BP
|
||||
- ./tow-boat-addon/tow_boat_RP:/data/resource_packs/tow_boat_RP
|
||||
- ./redstone-link-addon/redstone_link_BP:/data/behavior_packs/redstone_link_BP
|
||||
- ./redstone-link-addon/redstone_link_RP:/data/resource_packs/redstone_link_RP
|
||||
restart: unless-stopped
|
||||
# Cap each Bedrock service so a runaway/hung server can't OOM-kill its
|
||||
# neighbours. Host has 8 GB; 4 × 1500 MB leaves headroom for the OS and
|
||||
@@ -61,6 +63,7 @@ services:
|
||||
volumes:
|
||||
- jamie-data:/data
|
||||
- ./hub-return-addon/hub_return_transfer_BP:/data/behavior_packs/hub_return_transfer_BP
|
||||
- ./hub-return-addon/hub_return_transfer_RP:/data/resource_packs/hub_return_transfer_RP
|
||||
- ./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
|
||||
@@ -76,6 +79,11 @@ services:
|
||||
- ./dynamite-addon/dynamite_RP:/data/resource_packs/dynamite_RP
|
||||
- ./tow-boat-addon/tow_boat_BP:/data/behavior_packs/tow_boat_BP
|
||||
- ./tow-boat-addon/tow_boat_RP:/data/resource_packs/tow_boat_RP
|
||||
- ./trees-features-addon/trees_features_BP:/data/behavior_packs/trees_features_BP
|
||||
- ./trees-features-addon/trees_features_RP:/data/resource_packs/trees_features_RP
|
||||
- ./hemp-addon/hemp_BP:/data/behavior_packs/hemp_BP
|
||||
- ./hemp-addon/hemp_RP:/data/resource_packs/hemp_RP
|
||||
- ./family-goals-addon/family_goals_BP:/data/behavior_packs/family_goals_BP
|
||||
restart: unless-stopped
|
||||
mem_limit: 1500m
|
||||
memswap_limit: 2500m
|
||||
@@ -94,6 +102,7 @@ services:
|
||||
volumes:
|
||||
- lyla-data:/data
|
||||
- ./hub-return-addon/hub_return_transfer_BP:/data/behavior_packs/hub_return_transfer_BP
|
||||
- ./hub-return-addon/hub_return_transfer_RP:/data/resource_packs/hub_return_transfer_RP
|
||||
- ./addon/spark_pet_BP:/data/behavior_packs/spark_pet_BP
|
||||
- ./addon/spark_pet_RP:/data/resource_packs/spark_pet_RP
|
||||
- ./addon/heyhe_pet_BP:/data/behavior_packs/heyhe_pet_BP
|
||||
@@ -135,6 +144,7 @@ services:
|
||||
volumes:
|
||||
- mya-data:/data
|
||||
- ./hub-return-addon/hub_return_transfer_BP:/data/behavior_packs/hub_return_transfer_BP
|
||||
- ./hub-return-addon/hub_return_transfer_RP:/data/resource_packs/hub_return_transfer_RP
|
||||
- ./addon/spark_pet_BP:/data/behavior_packs/spark_pet_BP
|
||||
- ./addon/spark_pet_RP:/data/resource_packs/spark_pet_RP
|
||||
- ./addon/heyhe_pet_BP:/data/behavior_packs/heyhe_pet_BP
|
||||
@@ -157,7 +167,13 @@ services:
|
||||
- ./dynamite-addon/dynamite_RP:/data/resource_packs/dynamite_RP
|
||||
- ./tow-boat-addon/tow_boat_BP:/data/behavior_packs/tow_boat_BP
|
||||
- ./tow-boat-addon/tow_boat_RP:/data/resource_packs/tow_boat_RP
|
||||
- ./naturalist-lite-addon/naturalist_lite_BP:/data/behavior_packs/naturalist_lite_BP
|
||||
- ./naturalist-lite-addon/naturalist_lite_RP:/data/resource_packs/naturalist_lite_RP
|
||||
- ./village-evolution-addon/enabled_packs.json:/data/config/default/enabled_packs.json
|
||||
- ./trees-features-addon/trees_features_BP:/data/behavior_packs/trees_features_BP
|
||||
- ./trees-features-addon/trees_features_RP:/data/resource_packs/trees_features_RP
|
||||
- ./hemp-addon/hemp_BP:/data/behavior_packs/hemp_BP
|
||||
- ./hemp-addon/hemp_RP:/data/resource_packs/hemp_RP
|
||||
restart: unless-stopped
|
||||
mem_limit: 1500m
|
||||
memswap_limit: 2500m
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,20 +4,36 @@
|
||||
"name": "Dynamite",
|
||||
"description": "Throwable explosives: bangers, dynamite sticks, and bundles.",
|
||||
"uuid": "fac83943-16bc-4790-aa05-631894f59a03",
|
||||
"version": [1, 0, 0],
|
||||
"min_engine_version": [1, 21, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min_engine_version": [
|
||||
1,
|
||||
21,
|
||||
0
|
||||
]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "data",
|
||||
"uuid": "1354002c-fdd5-4f7e-b89b-f5dd2c38799c",
|
||||
"version": [1, 0, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"uuid": "a18bdde1-53f8-49aa-b06d-6f0ec6c45b46",
|
||||
"version": [1, 0, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
2
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"minecraft:client_entity": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:thrown_banger",
|
||||
"materials": { "default": "entity_alphatest" },
|
||||
"textures": { "default": "textures/entity/thrown_banger" },
|
||||
"geometry": { "default": "geometry.snowball" },
|
||||
"render_controllers": [ "controller.render.snowball" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"minecraft:client_entity": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:thrown_bundle",
|
||||
"materials": { "default": "entity_alphatest" },
|
||||
"textures": { "default": "textures/entity/thrown_bundle" },
|
||||
"geometry": { "default": "geometry.snowball" },
|
||||
"render_controllers": [ "controller.render.snowball" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"minecraft:client_entity": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:thrown_dynamite",
|
||||
"materials": { "default": "entity_alphatest" },
|
||||
"textures": { "default": "textures/entity/thrown_dynamite" },
|
||||
"geometry": { "default": "geometry.snowball" },
|
||||
"render_controllers": [ "controller.render.snowball" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@
|
||||
"name": "Dynamite Resources",
|
||||
"description": "Textures and language for the Dynamite addon.",
|
||||
"uuid": "a18bdde1-53f8-49aa-b06d-6f0ec6c45b46",
|
||||
"version": [1, 0, 0],
|
||||
"version": [1, 0, 2],
|
||||
"min_engine_version": [1, 21, 0]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "resources",
|
||||
"uuid": "587281f2-f159-4ad9-85a6-d20ff4899717",
|
||||
"version": [1, 0, 0]
|
||||
"version": [1, 0, 2]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 298 B |
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 372 B |
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"format_version": 2,
|
||||
"header": {
|
||||
"name": "Family Goals",
|
||||
"description": "Shared goals, family bank, loot share, pity timer, and PvP-off for the family survival world",
|
||||
"uuid": "f1a2b3c4-d5e6-4789-90ab-cdef12345001",
|
||||
"version": [1, 0, 6],
|
||||
"min_engine_version": [1, 21, 0]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "script",
|
||||
"language": "javascript",
|
||||
"uuid": "f1a2b3c4-d5e6-4789-90ab-cdef12345002",
|
||||
"version": [1, 0, 6],
|
||||
"entry": "scripts/main.js"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"module_name": "@minecraft/server",
|
||||
"version": "1.17.0"
|
||||
},
|
||||
{
|
||||
"module_name": "@minecraft/server-ui",
|
||||
"version": "1.3.0"
|
||||
},
|
||||
{
|
||||
"module_name": "@minecraft/server-admin",
|
||||
"version": "1.0.0-beta"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
// Family Goals Addon
|
||||
//
|
||||
// Lives in jamie's world (the shared family survival world). Provides:
|
||||
// • PvP-off (siblings can't hurt each other)
|
||||
// • Family roster ( !family setup adds online players )
|
||||
// • Family Bank ( !family bank registers a vanilla chest as shared )
|
||||
// • Five shared goals with per-player contribution tracking
|
||||
// • Loot share within 32 blocks (sibling helpers get a kill bonus)
|
||||
// • Pity timer (idle siblings get an encouragement bundle every 60 min)
|
||||
//
|
||||
// All persistent state lives in world dynamic properties so it survives
|
||||
// container restarts. No resource pack — vanilla items + titleraw + actionforms.
|
||||
|
||||
import { world, system, ItemStack } from "@minecraft/server";
|
||||
import { ActionFormData } from "@minecraft/server-ui";
|
||||
// We don't actually use @minecraft/server-admin, but importing from it flips
|
||||
// the beta-API gate for this pack so world.beforeEvents.chatSend resolves.
|
||||
// If we ever drop this import, fall back to afterEvents.chatSend (no cancel).
|
||||
import { variables } from "@minecraft/server-admin";
|
||||
void variables; // silence unused warning
|
||||
|
||||
// ─── Tunables ──────────────────────────────────────────────────
|
||||
const FAMILY_PROP = "family_roster_v1"; // { members: [<playerId>, ...] }
|
||||
const GOALS_PROP = "family_goals_v1"; // see initGoals() for shape
|
||||
const BANK_PROP = "family_bank_v1"; // { x, y, z, dim } | null
|
||||
const PROGRESS_PROP = "family_last_progress_at"; // per-player dyn prop, Date.now()
|
||||
|
||||
const VANILLA_CHEST = "minecraft:chest";
|
||||
const LOOT_SHARE_RADIUS = 32;
|
||||
const PITY_THRESHOLD_MS = 60 * 60 * 1000; // 60 min of no progress
|
||||
const PITY_TICK_INTERVAL = 6000; // poll every 5 min (20 tps)
|
||||
const NIGHTS_TICK_INTERVAL = 200; // every 10s
|
||||
|
||||
const GOAL_DEFS = [
|
||||
{ id: "logs", label: "Logs Chopped", target: 5000 },
|
||||
{ id: "diamonds", label: "Diamonds Mined", target: 50 },
|
||||
{ id: "mobs", label: "Monsters Defeated", target: 500 },
|
||||
{ id: "crops", label: "Crops Harvested", target: 1000 },
|
||||
{ id: "nights", label: "Nights Survived", target: 30 },
|
||||
];
|
||||
|
||||
const LOG_IDS = new Set([
|
||||
"minecraft:oak_log", "minecraft:spruce_log", "minecraft:birch_log",
|
||||
"minecraft:jungle_log", "minecraft:acacia_log", "minecraft:dark_oak_log",
|
||||
"minecraft:cherry_log", "minecraft:mangrove_log",
|
||||
"minecraft:stripped_oak_log", "minecraft:stripped_spruce_log",
|
||||
"minecraft:stripped_birch_log", "minecraft:stripped_jungle_log",
|
||||
"minecraft:stripped_acacia_log", "minecraft:stripped_dark_oak_log",
|
||||
"minecraft:stripped_cherry_log", "minecraft:stripped_mangrove_log",
|
||||
]);
|
||||
const DIAMOND_IDS = new Set([
|
||||
"minecraft:diamond_ore", "minecraft:deepslate_diamond_ore",
|
||||
]);
|
||||
// Block id → "growth"-like state value at full maturity
|
||||
const CROP_AGES = {
|
||||
"minecraft:wheat": 7,
|
||||
"minecraft:carrots": 7,
|
||||
"minecraft:potatoes": 7,
|
||||
"minecraft:beetroot": 3,
|
||||
};
|
||||
|
||||
// ─── In-memory mirror of dynamic properties ────────────────────
|
||||
let family = { members: [] };
|
||||
let goals = null;
|
||||
let bank = null;
|
||||
let lastDay = -1; // tracks day-rollover for the "nights survived" goal
|
||||
|
||||
// ─── Persistence ───────────────────────────────────────────────
|
||||
function loadState() {
|
||||
try {
|
||||
const f = world.getDynamicProperty(FAMILY_PROP);
|
||||
if (typeof f === "string") family = JSON.parse(f);
|
||||
} catch (_) { family = { members: [] }; }
|
||||
|
||||
try {
|
||||
const g = world.getDynamicProperty(GOALS_PROP);
|
||||
if (typeof g === "string") goals = JSON.parse(g);
|
||||
} catch (_) { goals = null; }
|
||||
if (!goals || !goals.goals) goals = initGoals();
|
||||
// Backfill any newly added goal definitions on upgrade
|
||||
for (const def of GOAL_DEFS) {
|
||||
if (!goals.goals[def.id]) {
|
||||
goals.goals[def.id] = { current: 0, target: def.target, completed: false, contributions: {} };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const b = world.getDynamicProperty(BANK_PROP);
|
||||
if (typeof b === "string") bank = JSON.parse(b);
|
||||
} catch (_) { bank = null; }
|
||||
}
|
||||
|
||||
function initGoals() {
|
||||
const g = { goals: {} };
|
||||
for (const def of GOAL_DEFS) {
|
||||
g.goals[def.id] = { current: 0, target: def.target, completed: false, contributions: {} };
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
function saveFamily() {
|
||||
try { world.setDynamicProperty(FAMILY_PROP, JSON.stringify(family)); } catch (_) {}
|
||||
}
|
||||
function saveGoals() {
|
||||
try {
|
||||
const json = JSON.stringify(goals);
|
||||
world.setDynamicProperty(GOALS_PROP, json);
|
||||
const verify = world.getDynamicProperty(GOALS_PROP);
|
||||
console.warn(`[Family] saveGoals: wrote ${json.length} chars, readback typeof=${typeof verify} len=${typeof verify === "string" ? verify.length : "n/a"}`);
|
||||
} catch (e) { console.warn(`[Family] saveGoals FAILED: ${e}`); }
|
||||
}
|
||||
function saveBank() {
|
||||
try { world.setDynamicProperty(BANK_PROP, bank ? JSON.stringify(bank) : undefined); } catch (_) {}
|
||||
}
|
||||
|
||||
function isFamily(player) {
|
||||
if (!player || !family.members) return false;
|
||||
return family.members.includes(player.id);
|
||||
}
|
||||
|
||||
// ─── Goal mutation ─────────────────────────────────────────────
|
||||
function incrGoal(goalId, player, n = 1) {
|
||||
if (!isFamily(player)) return;
|
||||
const g = goals.goals[goalId];
|
||||
if (!g || g.completed) return;
|
||||
g.current += n;
|
||||
g.contributions[player.id] = (g.contributions[player.id] || 0) + n;
|
||||
try { player.setDynamicProperty(PROGRESS_PROP, Date.now()); } catch (_) {}
|
||||
|
||||
if (g.current >= g.target) {
|
||||
g.completed = true;
|
||||
g.current = g.target;
|
||||
fireGoalReward(goalId);
|
||||
}
|
||||
saveGoals();
|
||||
}
|
||||
|
||||
function fireGoalReward(goalId) {
|
||||
const def = GOAL_DEFS.find(d => d.id === goalId);
|
||||
const label = def ? def.label : goalId;
|
||||
for (const p of world.getAllPlayers()) {
|
||||
try {
|
||||
p.onScreenDisplay.setTitle(`§6✦ Family Goal Complete!`, {
|
||||
subtitle: `§e${label}`,
|
||||
fadeInDuration: 10,
|
||||
stayDuration: 80,
|
||||
fadeOutDuration: 20,
|
||||
});
|
||||
p.dimension.runCommand(`summon firework_rocket ${p.location.x} ${p.location.y + 2} ${p.location.z}`);
|
||||
// v1 trophy: a named gold ingot. Easy to tell apart in inventory.
|
||||
p.runCommand(`give @s gold_ingot 1`);
|
||||
} catch (_) {}
|
||||
}
|
||||
world.sendMessage(`§6[Family] §eGoal complete: §f${label}§e! §7Trophy gold awarded.`);
|
||||
}
|
||||
|
||||
// ─── Family roster command ─────────────────────────────────────
|
||||
function familySetup(player) {
|
||||
const online = world.getAllPlayers();
|
||||
const before = new Set(family.members);
|
||||
let added = 0;
|
||||
for (const p of online) {
|
||||
if (!before.has(p.id)) {
|
||||
family.members.push(p.id);
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
saveFamily();
|
||||
|
||||
const names = online.map(p => p.name).join(", ");
|
||||
if (added === 0) {
|
||||
player.sendMessage(`§7[Family] §fAll currently-online players are already in the roster (${family.members.length} members).`);
|
||||
} else {
|
||||
player.sendMessage(`§a[Family] §fAdded ${added} new member(s). Roster (${family.members.length}): §e${names}§f.`);
|
||||
world.sendMessage(`§a[Family] §fRoster updated by §e${player.name}§f.`);
|
||||
}
|
||||
}
|
||||
|
||||
function familyReset(player) {
|
||||
family = { members: [] };
|
||||
saveFamily();
|
||||
player.sendMessage("§c[Family] §fRoster cleared. Run §e!family setup§f again to repopulate.");
|
||||
}
|
||||
|
||||
// ─── Family Bank command ───────────────────────────────────────
|
||||
function familyBank(player) {
|
||||
if (!isFamily(player)) {
|
||||
player.sendMessage("§c[Family] §7Only family members can register the bank — run §e!family setup§7 first while everyone's online.");
|
||||
return;
|
||||
}
|
||||
let view;
|
||||
try {
|
||||
view = player.getBlockFromViewDirection({ maxDistance: 6 });
|
||||
} catch (e) {
|
||||
player.sendMessage(`§c[Family] §7Couldn't look at a block: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
const target = view?.block;
|
||||
if (!target || target.typeId !== VANILLA_CHEST) {
|
||||
player.sendMessage("§c[Family] §7Look at a vanilla chest within 6 blocks, then re-run §e!family bank§7.");
|
||||
return;
|
||||
}
|
||||
bank = {
|
||||
x: target.location.x,
|
||||
y: target.location.y,
|
||||
z: target.location.z,
|
||||
dim: target.dimension.id,
|
||||
};
|
||||
saveBank();
|
||||
player.sendMessage(`§a[Family] §fBank registered at §e${bank.x}, ${bank.y}, ${bank.z}§f. All family members can now open it.`);
|
||||
}
|
||||
|
||||
// ─── Bank gating (interact + break) ────────────────────────────
|
||||
function isBankAt(loc, dimId) {
|
||||
if (!bank) return false;
|
||||
return Math.floor(loc.x) === bank.x
|
||||
&& Math.floor(loc.y) === bank.y
|
||||
&& Math.floor(loc.z) === bank.z
|
||||
&& bank.dim === dimId;
|
||||
}
|
||||
|
||||
try {
|
||||
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
|
||||
const block = event.block;
|
||||
if (block.typeId !== VANILLA_CHEST) return;
|
||||
if (!isBankAt(block.location, block.dimension.id)) return;
|
||||
if (isFamily(event.player)) return;
|
||||
event.cancel = true;
|
||||
const playerRef = event.player;
|
||||
system.run(() => playerRef.sendMessage("§c[Family Bank] §7Only family members can open this chest."));
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`[Family] beforeEvents.playerInteractWithBlock unavailable: ${e}`);
|
||||
}
|
||||
|
||||
try {
|
||||
world.beforeEvents.playerBreakBlock.subscribe((event) => {
|
||||
const block = event.block;
|
||||
if (block.typeId !== VANILLA_CHEST) return;
|
||||
if (!isBankAt(block.location, block.dimension.id)) return;
|
||||
|
||||
if (isFamily(event.player)) {
|
||||
// Family member breaking the bank → un-register so it stops being protected
|
||||
bank = null;
|
||||
saveBank();
|
||||
const p = event.player;
|
||||
system.run(() => p.sendMessage("§7[Family Bank] §fBank chest broken — un-registered. Place a new chest and run §e!family bank§f to re-register."));
|
||||
return;
|
||||
}
|
||||
event.cancel = true;
|
||||
const playerRef = event.player;
|
||||
system.run(() => playerRef.sendMessage("§c[Family Bank] §7Only family members can break this chest."));
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`[Family] beforeEvents.playerBreakBlock unavailable: ${e}`);
|
||||
}
|
||||
|
||||
// ─── PvP off (heal damage between siblings) ────────────────────
|
||||
// Bedrock has no server.properties pvp toggle — we hook the after-event
|
||||
// and immediately re-heal. Net visible effect: zero damage between players.
|
||||
world.afterEvents.entityHurt.subscribe((event) => {
|
||||
const source = event.damageSource?.damagingEntity;
|
||||
const victim = event.hurtEntity;
|
||||
if (!source || !victim) return;
|
||||
if (source.typeId !== "minecraft:player" || victim.typeId !== "minecraft:player") return;
|
||||
try {
|
||||
const health = victim.getComponent("minecraft:health");
|
||||
if (!health) return;
|
||||
const restored = Math.min(health.effectiveMax, health.currentValue + event.damage);
|
||||
health.setCurrentValue(restored);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
// ─── Goal triggers ─────────────────────────────────────────────
|
||||
world.afterEvents.playerBreakBlock.subscribe((event) => {
|
||||
const player = event.player;
|
||||
if (!isFamily(player)) return;
|
||||
const id = event.brokenBlockPermutation?.type?.id;
|
||||
if (!id) return;
|
||||
|
||||
if (LOG_IDS.has(id)) {
|
||||
incrGoal("logs", player);
|
||||
return;
|
||||
}
|
||||
if (DIAMOND_IDS.has(id)) {
|
||||
incrGoal("diamonds", player);
|
||||
return;
|
||||
}
|
||||
if (id in CROP_AGES) {
|
||||
// Only fully grown crops count
|
||||
let states = null;
|
||||
try { states = event.brokenBlockPermutation.getAllStates(); } catch (_) {}
|
||||
const age = states && (states["growth"] ?? states["age"]);
|
||||
if (typeof age === "number" && age >= CROP_AGES[id]) {
|
||||
incrGoal("crops", player);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
world.afterEvents.entityDie.subscribe((event) => {
|
||||
const dead = event.deadEntity;
|
||||
if (!dead || dead.typeId === "minecraft:player") return;
|
||||
const source = event.damageSource?.damagingEntity;
|
||||
if (!source || source.typeId !== "minecraft:player") return;
|
||||
if (!isFamily(source)) return;
|
||||
|
||||
// Goal: monsters only
|
||||
let isMonster = false;
|
||||
try {
|
||||
const fam = dead.getComponent("minecraft:type_family");
|
||||
if (fam && typeof fam.hasTypeFamily === "function" && fam.hasTypeFamily("monster")) {
|
||||
isMonster = true;
|
||||
}
|
||||
} catch (_) {}
|
||||
if (isMonster) incrGoal("mobs", source);
|
||||
|
||||
// Loot share: every family member in 32 blocks (other than killer) gets cooked beef.
|
||||
// Sharing helpers, not real drop mirroring — keeps the drop pipeline untouched.
|
||||
try {
|
||||
const k = source.location;
|
||||
const r2 = LOOT_SHARE_RADIUS * LOOT_SHARE_RADIUS;
|
||||
for (const other of world.getAllPlayers()) {
|
||||
if (other.id === source.id) continue;
|
||||
if (!isFamily(other)) continue;
|
||||
if (other.dimension.id !== source.dimension.id) continue;
|
||||
const dx = other.location.x - k.x;
|
||||
const dy = other.location.y - k.y;
|
||||
const dz = other.location.z - k.z;
|
||||
if (dx*dx + dy*dy + dz*dz <= r2) {
|
||||
other.runCommand("give @s cooked_beef 2");
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
// ─── Nights survived ───────────────────────────────────────────
|
||||
system.runInterval(() => {
|
||||
let day;
|
||||
try { day = world.getDay(); } catch (_) { return; }
|
||||
if (typeof day !== "number") return;
|
||||
if (lastDay === -1) { lastDay = day; return; }
|
||||
if (day > lastDay) {
|
||||
const diff = day - lastDay;
|
||||
lastDay = day;
|
||||
for (const p of world.getAllPlayers()) {
|
||||
if (isFamily(p)) incrGoal("nights", p, diff);
|
||||
}
|
||||
}
|
||||
}, NIGHTS_TICK_INTERVAL);
|
||||
|
||||
// ─── Pity timer ────────────────────────────────────────────────
|
||||
system.runInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const p of world.getAllPlayers()) {
|
||||
if (!isFamily(p)) continue;
|
||||
|
||||
let last;
|
||||
try { last = p.getDynamicProperty(PROGRESS_PROP); } catch (_) { last = undefined; }
|
||||
if (typeof last !== "number") {
|
||||
// First time we've seen this family member contribute (or first session) — initialize, don't pity yet
|
||||
try { p.setDynamicProperty(PROGRESS_PROP, now); } catch (_) {}
|
||||
continue;
|
||||
}
|
||||
if (now - last < PITY_THRESHOLD_MS) continue;
|
||||
|
||||
// Drop encouragement bundle directly into inventory
|
||||
try {
|
||||
p.runCommand("give @s diamond 1");
|
||||
p.runCommand("give @s cooked_beef 16");
|
||||
p.runCommand("give @s iron_pickaxe 1");
|
||||
p.sendMessage("§6[Family] §fA little boost to keep you going! §7(family pity bundle)");
|
||||
p.setDynamicProperty(PROGRESS_PROP, now); // reset so we don't spam every 5 min
|
||||
} catch (_) {}
|
||||
}
|
||||
}, PITY_TICK_INTERVAL);
|
||||
|
||||
// ─── Goal display ──────────────────────────────────────────────
|
||||
function makeBar(pct) {
|
||||
const filled = Math.max(0, Math.min(10, Math.floor(pct / 10)));
|
||||
return "§a" + "▰".repeat(filled) + "§8" + "▰".repeat(10 - filled);
|
||||
}
|
||||
|
||||
async function showGoals(player, perPlayerMode) {
|
||||
const lines = [];
|
||||
for (const def of GOAL_DEFS) {
|
||||
const g = goals.goals[def.id];
|
||||
if (!g) continue;
|
||||
const cur = Math.min(g.current, g.target);
|
||||
const pct = Math.floor((cur / g.target) * 100);
|
||||
const bar = makeBar(pct);
|
||||
const status = g.completed ? " §a✔" : "";
|
||||
|
||||
if (perPlayerMode) {
|
||||
const my = g.contributions[player.id] || 0;
|
||||
const myPct = cur > 0 ? Math.floor((my / cur) * 100) : 0;
|
||||
lines.push(`§e${def.label}${status}\n§7 ${bar}§7 §f${cur}§7/§f${g.target}\n§7 You: §f${my}§7 (${myPct}% of progress)`);
|
||||
} else {
|
||||
lines.push(`§e${def.label}${status}\n§7 ${bar}§7 §f${cur}§7/§f${g.target}`);
|
||||
}
|
||||
}
|
||||
if (!lines.length) lines.push("§7No goals configured.");
|
||||
|
||||
const form = new ActionFormData()
|
||||
.title(perPlayerMode ? "My Goal Contributions" : "Family Goals")
|
||||
.body(lines.join("\n\n"))
|
||||
.button("§7OK");
|
||||
try { await form.show(player); } catch (_) {}
|
||||
}
|
||||
|
||||
// ─── Family menu (entered via scriptevent from hub-return compass) ─────
|
||||
function showRoster(player) {
|
||||
if (family.members.length === 0) {
|
||||
player.sendMessage("§7[Family] §fNo roster set yet — open §e🏆 Family§f from your compass and pick §eAdd me to the family§f.");
|
||||
return;
|
||||
}
|
||||
const names = world.getAllPlayers()
|
||||
.filter(p => family.members.includes(p.id))
|
||||
.map(p => p.name);
|
||||
player.sendMessage(`§a[Family] §fRoster: §e${family.members.length}§f members. Online: §e${names.join(", ") || "(none)"}§f.`);
|
||||
}
|
||||
|
||||
async function openFamilyMenu(player) {
|
||||
const inFamily = isFamily(player);
|
||||
const form = new ActionFormData()
|
||||
.title("🏆 Family")
|
||||
.body(`§7Roster: §f${family.members.length}§7 members${inFamily ? " §a(you're in)" : " §c(you're not)"}\n§7Bank: ${bank ? `§f${bank.x}, ${bank.y}, ${bank.z}` : "§cnot set"}`);
|
||||
|
||||
const actions = [];
|
||||
form.button("🎯 Family Goals"); actions.push("goals");
|
||||
form.button("📝 My Contributions"); actions.push("mygoals");
|
||||
form.button("👥 Show Roster"); actions.push("roster");
|
||||
form.button("➕ Add me / Add online players"); actions.push("setup");
|
||||
form.button("🏦 Register Family Bank (look at chest)"); actions.push("bank");
|
||||
form.button("♻ Reset Roster (danger)"); actions.push("reset");
|
||||
form.button("§7Cancel"); actions.push("cancel");
|
||||
|
||||
let res;
|
||||
try { res = await form.show(player); } catch (_) { return; }
|
||||
if (res.canceled || res.selection === undefined) return;
|
||||
const a = actions[res.selection];
|
||||
|
||||
if (a === "goals") return showGoals(player, false);
|
||||
if (a === "mygoals") return showGoals(player, true);
|
||||
if (a === "roster") return showRoster(player);
|
||||
if (a === "setup") return familySetup(player);
|
||||
if (a === "bank") return familyBank(player);
|
||||
if (a === "reset") return familyReset(player);
|
||||
}
|
||||
|
||||
// scriptEventReceive is the cross-pack dispatch surface. Hub-return's compass
|
||||
// menu fires `scriptevent family:menu` to enter our UI. Direct ids are also
|
||||
// handled so an opped player can run e.g. `/scriptevent family:setup`.
|
||||
try {
|
||||
system.afterEvents.scriptEventReceive.subscribe((event) => {
|
||||
const id = event.id;
|
||||
if (!id || !id.startsWith("family:")) return;
|
||||
const player = event.sourceEntity;
|
||||
if (!player || player.typeId !== "minecraft:player") return;
|
||||
switch (id) {
|
||||
case "family:menu": system.run(() => openFamilyMenu(player)); break;
|
||||
case "family:goals": system.run(() => showGoals(player, false)); break;
|
||||
case "family:mygoals": system.run(() => showGoals(player, true)); break;
|
||||
case "family:roster": showRoster(player); break;
|
||||
case "family:setup": familySetup(player); break;
|
||||
case "family:bank": familyBank(player); break;
|
||||
case "family:reset": familyReset(player); break;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`[Family] scriptEventReceive unavailable: ${e}`);
|
||||
}
|
||||
|
||||
// ─── Boot ──────────────────────────────────────────────────────
|
||||
system.run(() => {
|
||||
loadState();
|
||||
saveGoals(); // persist any backfilled goal defs from upgrade
|
||||
|
||||
world.sendMessage("§6[Family] §fLoaded. Right-click your §dHub Compass§f to find the §6🏆 Family§f menu.");
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:block": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:hemp_crop",
|
||||
"menu_category": { "category": "nature" },
|
||||
"states": {
|
||||
"silverlabs:hemp_age": { "values": { "min": 0, "max": 5 } },
|
||||
"silverlabs:hemp_top": { "values": [false, true] }
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"minecraft:destructible_by_mining": { "seconds_to_destroy": 0.1 },
|
||||
"minecraft:destructible_by_explosion": { "explosion_resistance": 0.1 },
|
||||
"minecraft:flammable": { "destroy_chance_modifier": 60, "catch_chance_modifier": 60 },
|
||||
"minecraft:light_dampening": 0,
|
||||
"minecraft:map_color": "#5d8a3c",
|
||||
"minecraft:collision_box": false,
|
||||
"minecraft:selection_box": { "origin": [-6, 0, -6], "size": [12, 14, 12] },
|
||||
"minecraft:geometry": "minecraft:geometry.cross",
|
||||
"minecraft:material_instances": {
|
||||
"*": {
|
||||
"texture": "hemp_crop_0",
|
||||
"render_method": "alpha_test",
|
||||
"ambient_occlusion": false,
|
||||
"face_dimming": false
|
||||
}
|
||||
},
|
||||
"minecraft:loot": "loot_tables/blocks/hemp_crop.json"
|
||||
},
|
||||
"permutations": [
|
||||
{
|
||||
"condition": "query.block_state('silverlabs:hemp_age') == 1",
|
||||
"components": {
|
||||
"minecraft:material_instances": {
|
||||
"*": { "texture": "hemp_crop_1", "render_method": "alpha_test", "ambient_occlusion": false, "face_dimming": false }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "query.block_state('silverlabs:hemp_age') == 2",
|
||||
"components": {
|
||||
"minecraft:material_instances": {
|
||||
"*": { "texture": "hemp_crop_2", "render_method": "alpha_test", "ambient_occlusion": false, "face_dimming": false }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "query.block_state('silverlabs:hemp_age') == 3",
|
||||
"components": {
|
||||
"minecraft:material_instances": {
|
||||
"*": { "texture": "hemp_crop_3", "render_method": "alpha_test", "ambient_occlusion": false, "face_dimming": false }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "query.block_state('silverlabs:hemp_age') == 4",
|
||||
"components": {
|
||||
"minecraft:material_instances": {
|
||||
"*": { "texture": "hemp_crop_4", "render_method": "alpha_test", "ambient_occlusion": false, "face_dimming": false }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "query.block_state('silverlabs:hemp_age') == 5",
|
||||
"components": {
|
||||
"minecraft:material_instances": {
|
||||
"*": { "texture": "hemp_crop_5", "render_method": "alpha_test", "ambient_occlusion": false, "face_dimming": false }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:block": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:sun_lamp",
|
||||
"menu_category": {
|
||||
"category": "items"
|
||||
},
|
||||
"states": {
|
||||
"silverlabs:powered": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"minecraft:destructible_by_mining": {
|
||||
"seconds_to_destroy": 0.3
|
||||
},
|
||||
"minecraft:destructible_by_explosion": {
|
||||
"explosion_resistance": 0.5
|
||||
},
|
||||
"minecraft:map_color": "#ffd24d",
|
||||
"minecraft:light_emission": 0,
|
||||
"minecraft:geometry": "minecraft:geometry.full_block",
|
||||
"minecraft:material_instances": {
|
||||
"*": {
|
||||
"texture": "sun_lamp_off",
|
||||
"render_method": "opaque"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permutations": [
|
||||
{
|
||||
"condition": "query.block_state('silverlabs:powered') == true",
|
||||
"components": {
|
||||
"minecraft:light_emission": 15,
|
||||
"minecraft:light_dampening": 0,
|
||||
"minecraft:geometry": "minecraft:geometry.full_block",
|
||||
"minecraft:material_instances": {
|
||||
"*": {
|
||||
"texture": "sun_lamp",
|
||||
"render_method": "opaque"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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] }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:item": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:hemp_brownie",
|
||||
"menu_category": {
|
||||
"category": "nature"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"minecraft:icon": "hemp_brownie",
|
||||
"minecraft:max_stack_size": 16,
|
||||
"minecraft:use_animation": "eat",
|
||||
"minecraft:use_modifiers": {
|
||||
"use_duration": 1.6,
|
||||
"movement_modifier": 0.35
|
||||
},
|
||||
"minecraft:food": {
|
||||
"nutrition": 4,
|
||||
"saturation_modifier": 0.6,
|
||||
"can_always_eat": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:item": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:hemp_bud",
|
||||
"menu_category": {
|
||||
"category": "nature"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"minecraft:icon": "hemp_bud",
|
||||
"minecraft:max_stack_size": 64
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:item": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:hemp_seeds",
|
||||
"menu_category": {
|
||||
"category": "nature",
|
||||
"group": "itemGroup.name.seed"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"minecraft:icon": "hemp_seeds",
|
||||
"minecraft:max_stack_size": 64,
|
||||
"minecraft:block_placer": {
|
||||
"block": "silverlabs:hemp_crop",
|
||||
"use_on": [
|
||||
"minecraft:dirt",
|
||||
"minecraft:grass_block",
|
||||
"minecraft:farmland",
|
||||
"minecraft:podzol",
|
||||
"minecraft:coarse_dirt",
|
||||
"minecraft:rooted_dirt",
|
||||
"minecraft:moss_block",
|
||||
"minecraft:flower_pot"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:item": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:hemp_tincture",
|
||||
"menu_category": {
|
||||
"category": "nature"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"minecraft:icon": "hemp_tincture",
|
||||
"minecraft:max_stack_size": 16,
|
||||
"minecraft:use_animation": "drink",
|
||||
"minecraft:use_modifiers": {
|
||||
"use_duration": 1.5,
|
||||
"movement_modifier": 0.35
|
||||
},
|
||||
"minecraft:food": {
|
||||
"nutrition": 1,
|
||||
"saturation_modifier": 0.1,
|
||||
"can_always_eat": true,
|
||||
"using_converts_to": "minecraft:glass_bottle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "item",
|
||||
"name": "silverlabs:hemp_seeds",
|
||||
"weight": 1,
|
||||
"functions": [
|
||||
{ "function": "set_count", "count": { "min": 0, "max": 1 } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"format_version": 2,
|
||||
"header": {
|
||||
"name": "Hemp Plant",
|
||||
"description": "Plantable hemp crop with sheers harvesting, cauldron tincture, brownie food, sun-lamp block",
|
||||
"uuid": "910fafaf-bcb0-4f1a-8a05-bd235a537c3b",
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min_engine_version": [
|
||||
1,
|
||||
21,
|
||||
0
|
||||
]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "data",
|
||||
"uuid": "a10b077f-703b-46b6-b45e-71ee2044c07c",
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "script",
|
||||
"language": "javascript",
|
||||
"uuid": "6e6ef947-60fc-49d2-97c1-b3cab6661ece",
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"entry": "scripts/main.js"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"module_name": "@minecraft/server",
|
||||
"version": "1.17.0"
|
||||
},
|
||||
{
|
||||
"uuid": "8e5c46ac-3b16-4f51-89f7-673bd06600f4",
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
5
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:recipe_shapeless": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:hemp_brownie_recipe"
|
||||
},
|
||||
"tags": ["crafting_table"],
|
||||
"unlock": [
|
||||
{ "item": "silverlabs:hemp_bud" },
|
||||
{ "item": "minecraft:cocoa_beans" }
|
||||
],
|
||||
"ingredients": [
|
||||
{ "item": "silverlabs:hemp_bud" },
|
||||
{ "item": "silverlabs:hemp_bud" },
|
||||
{ "item": "minecraft:cocoa_beans" },
|
||||
{ "item": "minecraft:wheat" },
|
||||
{ "item": "minecraft:sugar" }
|
||||
],
|
||||
"result": {
|
||||
"item": "silverlabs:hemp_brownie",
|
||||
"count": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"format_version": "1.21.0",
|
||||
"minecraft:recipe_shaped": {
|
||||
"description": {
|
||||
"identifier": "silverlabs:sun_lamp_recipe"
|
||||
},
|
||||
"tags": ["crafting_table"],
|
||||
"unlock": [
|
||||
{ "item": "minecraft:glowstone" }
|
||||
],
|
||||
"pattern": [
|
||||
"WIW",
|
||||
"IGI",
|
||||
"WIW"
|
||||
],
|
||||
"key": {
|
||||
"W": { "item": "minecraft:yellow_wool" },
|
||||
"I": { "item": "minecraft:iron_nugget" },
|
||||
"G": { "item": "minecraft:glowstone" }
|
||||
},
|
||||
"result": {
|
||||
"item": "silverlabs:sun_lamp",
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,812 @@
|
||||
import { world, system, ItemStack, BlockPermutation } from "@minecraft/server";
|
||||
|
||||
const CROP = "silverlabs:hemp_crop";
|
||||
const SUN_LAMP = "silverlabs:sun_lamp";
|
||||
const SEEDS = "silverlabs:hemp_seeds";
|
||||
const BUD = "silverlabs:hemp_bud";
|
||||
const TINCTURE = "silverlabs:hemp_tincture";
|
||||
const BROWNIE = "silverlabs:hemp_brownie";
|
||||
|
||||
const AGE = "silverlabs:hemp_age";
|
||||
const TOP = "silverlabs:hemp_top";
|
||||
|
||||
const GROWTH_INTERVAL_TICKS = 100; // 5s between growth ticks
|
||||
const RABBIT_INTERVAL_TICKS = 100; // 5s between rabbit checks
|
||||
const SCAN_RADIUS = 5; // ±5 horizontal — dense scan, every block looked at
|
||||
const SCAN_VERT = 2; // ±2 vertical
|
||||
const SUN_LAMP_RANGE = 4; // blocks searched for a lamp around indoor crop
|
||||
const GROWTH_CHANCE_OUTDOOR = 0.017; // per 5s tick — ~20 min for 0→4 (prime)
|
||||
const GROWTH_CHANCE_INDOOR = 0.011; // per 5s tick — ~30 min for 0→4 (sun lamp required)
|
||||
const OVERRIPE_CHANCE_MULT = 0.33; // age 4→5 transition is 3x slower so prime lingers
|
||||
|
||||
function rand(n) { return Math.floor(Math.random() * n); }
|
||||
function chance(p) { return Math.random() < p; }
|
||||
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
||||
|
||||
function getInv(player) {
|
||||
return player.getComponent("minecraft:inventory")?.container ?? null;
|
||||
}
|
||||
|
||||
function consumeOneOfType(player, typeId) {
|
||||
const inv = getInv(player);
|
||||
if (!inv) return false;
|
||||
const sel = player.selectedSlotIndex;
|
||||
const cur = inv.getItem(sel);
|
||||
if (cur && cur.typeId === typeId) {
|
||||
if (cur.amount > 1) { cur.amount -= 1; inv.setItem(sel, cur); }
|
||||
else inv.setItem(sel, undefined);
|
||||
return true;
|
||||
}
|
||||
for (let i = 0; i < inv.size; i++) {
|
||||
const it = inv.getItem(i);
|
||||
if (it && it.typeId === typeId) {
|
||||
if (it.amount > 1) { it.amount -= 1; inv.setItem(i, it); }
|
||||
else inv.setItem(i, undefined);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function giveItem(player, typeId, count = 1) {
|
||||
const inv = getInv(player);
|
||||
if (!inv) return false;
|
||||
try {
|
||||
inv.addItem(new ItemStack(typeId, count));
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Block ids that don't block sky access — the plant grows around these like
|
||||
// a vanilla tree pushes through leaves. Anything else above stops growth.
|
||||
function isSkyTransparent(typeId) {
|
||||
if (!typeId) return true;
|
||||
if (typeId === CROP) return true; // ignore our own top half
|
||||
if (typeId === "minecraft:air") return true;
|
||||
// Glass / panes / bars
|
||||
if (typeId.includes("glass")) return true;
|
||||
if (typeId === "minecraft:iron_bars") return true;
|
||||
// Leaves and saplings (canopy-style obstructions)
|
||||
if (typeId.includes("leaves")) return true;
|
||||
if (typeId.includes("sapling")) return true;
|
||||
// Other plants / vines / hanging stuff
|
||||
if (typeId === "minecraft:vine" || typeId === "minecraft:weeping_vines"
|
||||
|| typeId === "minecraft:twisting_vines") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isAirAbove(dim, loc) {
|
||||
// Walk up from y+1; sky-transparent blocks don't count as obstructions.
|
||||
for (let dy = 1; dy < 64; dy++) {
|
||||
let b;
|
||||
try { b = dim.getBlock({ x: loc.x, y: loc.y + dy, z: loc.z }); } catch (_) { return true; }
|
||||
if (!b) return true;
|
||||
if (b.isAir || b.isLiquid) continue;
|
||||
if (isSkyTransparent(b.typeId)) continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function findSunLampNear(dim, loc) {
|
||||
const r = SUN_LAMP_RANGE;
|
||||
for (let dy = -1; dy <= r; dy++) {
|
||||
for (let dx = -r; dx <= r; dx++) {
|
||||
for (let dz = -r; dz <= r; dz++) {
|
||||
let b;
|
||||
try { b = dim.getBlock({ x: loc.x + dx, y: loc.y + dy, z: loc.z + dz }); } catch (_) { continue; }
|
||||
if (b && b.typeId === SUN_LAMP) {
|
||||
try {
|
||||
if (b.permutation.getState("silverlabs:powered") === true) return true;
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setAge(block, newAge, outdoor) {
|
||||
try {
|
||||
const perm = BlockPermutation.resolve(CROP, {
|
||||
[AGE]: clamp(newAge, 0, 5),
|
||||
[TOP]: false,
|
||||
});
|
||||
block.setPermutation(perm);
|
||||
if (outdoor && newAge >= 3) {
|
||||
// Place top half above for the visual "tall outdoor crop"
|
||||
const above = block.dimension.getBlock({ x: block.location.x, y: block.location.y + 1, z: block.location.z });
|
||||
if (above && above.isAir) {
|
||||
try {
|
||||
const topPerm = BlockPermutation.resolve(CROP, { [AGE]: clamp(newAge, 0, 5), [TOP]: true });
|
||||
above.setPermutation(topPerm);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function clearTopAbove(block) {
|
||||
const above = block.dimension.getBlock({ x: block.location.x, y: block.location.y + 1, z: block.location.z });
|
||||
if (above && above.typeId === CROP) {
|
||||
try { above.setType("minecraft:air"); } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Growth: dense scan of an 11×11×5 box around each player ---
|
||||
// Every hemp_crop base in range gets a roll on every tick — no random sampling.
|
||||
system.runInterval(() => {
|
||||
for (const player of world.getAllPlayers()) {
|
||||
const dim = player.dimension;
|
||||
const px = Math.floor(player.location.x);
|
||||
const py = Math.floor(player.location.y);
|
||||
const pz = Math.floor(player.location.z);
|
||||
for (let dy = -SCAN_VERT; dy <= SCAN_VERT; dy++) {
|
||||
for (let dx = -SCAN_RADIUS; dx <= SCAN_RADIUS; dx++) {
|
||||
for (let dz = -SCAN_RADIUS; dz <= SCAN_RADIUS; dz++) {
|
||||
let b;
|
||||
try { b = dim.getBlock({ x: px + dx, y: py + dy, z: pz + dz }); } catch (_) { continue; }
|
||||
if (!b || b.typeId !== CROP) continue;
|
||||
if (b.permutation.getState(TOP) === true) continue; // only base ticks
|
||||
const age = b.permutation.getState(AGE) ?? 0;
|
||||
if (age >= 5) continue; // overripe stops
|
||||
const outdoor = isAirAbove(dim, b.location);
|
||||
let baseChance = 0;
|
||||
if (outdoor) baseChance = GROWTH_CHANCE_OUTDOOR;
|
||||
else if (findSunLampNear(dim, b.location)) baseChance = GROWTH_CHANCE_INDOOR;
|
||||
else continue;
|
||||
// Prime (age 4) lingers — slow the transition into overripe
|
||||
const c = age === 4 ? baseChance * OVERRIPE_CHANCE_MULT : baseChance;
|
||||
if (chance(c)) setAge(b, age + 1, outdoor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, GROWTH_INTERVAL_TICKS);
|
||||
|
||||
// --- Redstone power: scan known sources adjacent to a sun_lamp ---
|
||||
const REDSTONE_FACES = [
|
||||
[1, 0, 0], [-1, 0, 0],
|
||||
[0, 1, 0], [0, -1, 0],
|
||||
[0, 0, 1], [0, 0, -1],
|
||||
];
|
||||
function isPowerSource(b) {
|
||||
if (!b) return false;
|
||||
const t = b.typeId;
|
||||
if (t === "minecraft:redstone_block") return true;
|
||||
if (t === "minecraft:lit_redstone_torch" || t === "minecraft:redstone_torch") {
|
||||
// torch lit state
|
||||
try { return b.permutation.getState("toggle_bit") !== false; } catch (_) {}
|
||||
return t === "minecraft:lit_redstone_torch";
|
||||
}
|
||||
if (t === "minecraft:powered_repeater") return true;
|
||||
if (t === "minecraft:powered_comparator") return true;
|
||||
if (t === "minecraft:lever") {
|
||||
try { return b.permutation.getState("open_bit") === true; } catch (_) { return false; }
|
||||
}
|
||||
if (t.endsWith("_button") || t.includes(":wooden_button") || t.includes(":stone_button")) {
|
||||
try { return b.permutation.getState("button_pressed_bit") === true; } catch (_) { return false; }
|
||||
}
|
||||
if (t === "minecraft:redstone_wire") {
|
||||
try {
|
||||
const p = b.permutation.getState("redstone_signal") ?? 0;
|
||||
return p > 0;
|
||||
} catch (_) { return false; }
|
||||
}
|
||||
if (t === "minecraft:daylight_detector_inverted") return true;
|
||||
return false;
|
||||
}
|
||||
function lampShouldBePowered(lamp) {
|
||||
// Try the script API redstone power first (newer servers expose it)
|
||||
try {
|
||||
const p = lamp.getRedstonePower();
|
||||
if (typeof p === "number" && p > 0) return true;
|
||||
} catch (_) {}
|
||||
const dim = lamp.dimension;
|
||||
for (const [dx, dy, dz] of REDSTONE_FACES) {
|
||||
let n;
|
||||
try { n = dim.getBlock({ x: lamp.location.x + dx, y: lamp.location.y + dy, z: lamp.location.z + dz }); } catch (_) { continue; }
|
||||
if (isPowerSource(n)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const LAMP_SCAN_INTERVAL_TICKS = 20; // 1s
|
||||
system.runInterval(() => {
|
||||
for (const player of world.getAllPlayers()) {
|
||||
const dim = player.dimension;
|
||||
const px = Math.floor(player.location.x);
|
||||
const py = Math.floor(player.location.y);
|
||||
const pz = Math.floor(player.location.z);
|
||||
for (let dy = -SCAN_VERT; dy <= SCAN_VERT; dy++) {
|
||||
for (let dx = -SCAN_RADIUS; dx <= SCAN_RADIUS; dx++) {
|
||||
for (let dz = -SCAN_RADIUS; dz <= SCAN_RADIUS; dz++) {
|
||||
let b;
|
||||
try { b = dim.getBlock({ x: px + dx, y: py + dy, z: pz + dz }); } catch (_) { continue; }
|
||||
if (!b || b.typeId !== SUN_LAMP) continue;
|
||||
let cur = false;
|
||||
try { cur = b.permutation.getState("silverlabs:powered") === true; } catch (_) {}
|
||||
const want = lampShouldBePowered(b);
|
||||
if (cur !== want) {
|
||||
try {
|
||||
const perm = BlockPermutation.resolve(SUN_LAMP, { "silverlabs:powered": want });
|
||||
b.setPermutation(perm);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, LAMP_SCAN_INTERVAL_TICKS);
|
||||
|
||||
// --- Rabbit threat: damage outdoor hemp ---
|
||||
system.runInterval(() => {
|
||||
for (const player of world.getAllPlayers()) {
|
||||
const dim = player.dimension;
|
||||
let rabbits;
|
||||
try {
|
||||
rabbits = dim.getEntities({ type: "minecraft:rabbit", location: player.location, maxDistance: 48 });
|
||||
} catch (_) { continue; }
|
||||
for (const rabbit of rabbits) {
|
||||
// For each rabbit, look at a small box around its feet for hemp_crop.
|
||||
const rx = Math.floor(rabbit.location.x);
|
||||
const ry = Math.floor(rabbit.location.y);
|
||||
const rz = Math.floor(rabbit.location.z);
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
for (let dz = -1; dz <= 1; dz++) {
|
||||
let b;
|
||||
try { b = dim.getBlock({ x: rx + dx, y: ry, z: rz + dz }); } catch (_) { continue; }
|
||||
if (!b || b.typeId !== CROP) continue;
|
||||
const isTop = b.permutation.getState(TOP) === true;
|
||||
if (isTop) continue;
|
||||
if (chance(0.35)) {
|
||||
const age = b.permutation.getState(AGE) ?? 0;
|
||||
if (age <= 0) {
|
||||
clearTopAbove(b);
|
||||
try { b.setType("minecraft:air"); } catch (_) {}
|
||||
} else {
|
||||
clearTopAbove(b);
|
||||
setAge(b, age - 1, isAirAbove(dim, b.location));
|
||||
}
|
||||
try { dim.runCommand(`particle minecraft:crit_particle ${rx + dx + 0.5} ${ry + 0.5} ${rz + dz + 0.5}`); } catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, RABBIT_INTERVAL_TICKS);
|
||||
|
||||
// --- Pillager raid threat: illagers carrying a banner steal hemp during raids ---
|
||||
// Gate on either an active raid (detected via a nearby ominous_banner) or a
|
||||
// pillager_captain so isolated tower pillagers don't constantly cull farms
|
||||
// in render distance. When triggered, drops the crop's age by 1 (or removes
|
||||
// it if age 0) and spawns 1 hemp_bud at the illager's feet so the player
|
||||
// can recover the loot by killing the raider.
|
||||
const RAID_INTERVAL_TICKS = 100;
|
||||
const ILLAGER_TYPES = new Set([
|
||||
"minecraft:pillager",
|
||||
"minecraft:vindicator",
|
||||
"minecraft:evocation_illager",
|
||||
]);
|
||||
const ILLAGER_HEMP_STEAL_CHANCE = 0.30;
|
||||
|
||||
function illagerIsRaiding(dim, ill) {
|
||||
// Cheapest: check if the illager is wearing/carrying an ominous_banner
|
||||
// (raid captain marker). Also accept any nearby ominous_banner block within
|
||||
// 8 blocks as evidence of an ongoing raid the illager is participating in.
|
||||
const equip = (() => { try { return ill.getComponent("minecraft:equippable"); } catch (_) { return null; } })();
|
||||
if (equip) {
|
||||
try {
|
||||
const head = equip.getEquipmentSlot("Head")?.getItem();
|
||||
if (head && head.typeId === "minecraft:ominous_banner") return true;
|
||||
} catch (_) {}
|
||||
}
|
||||
const ix = Math.floor(ill.location.x);
|
||||
const iy = Math.floor(ill.location.y);
|
||||
const iz = Math.floor(ill.location.z);
|
||||
for (let dx = -8; dx <= 8; dx += 4) {
|
||||
for (let dz = -8; dz <= 8; dz += 4) {
|
||||
let b;
|
||||
try { b = dim.getBlock({ x: ix + dx, y: iy, z: iz + dz }); } catch (_) { continue; }
|
||||
if (b && (b.typeId === "minecraft:standing_banner" || b.typeId === "minecraft:wall_banner")) {
|
||||
// Banners can be ominous via their NBT; we can't read it, so any banner
|
||||
// within 8 blocks of an illager is treated as raid evidence. Still rare.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
system.runInterval(() => {
|
||||
for (const player of world.getAllPlayers()) {
|
||||
const dim = player.dimension;
|
||||
let illagers;
|
||||
try {
|
||||
illagers = dim.getEntities({ families: [], location: player.location, maxDistance: 48 });
|
||||
} catch (_) { continue; }
|
||||
for (const ill of illagers) {
|
||||
if (!ILLAGER_TYPES.has(ill.typeId)) continue;
|
||||
if (!illagerIsRaiding(dim, ill)) continue;
|
||||
const ix = Math.floor(ill.location.x);
|
||||
const iy = Math.floor(ill.location.y);
|
||||
const iz = Math.floor(ill.location.z);
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
for (let dz = -1; dz <= 1; dz++) {
|
||||
let b;
|
||||
try { b = dim.getBlock({ x: ix + dx, y: iy, z: iz + dz }); } catch (_) { continue; }
|
||||
if (!b || b.typeId !== CROP) continue;
|
||||
if (b.permutation.getState(TOP) === true) continue;
|
||||
if (!chance(ILLAGER_HEMP_STEAL_CHANCE)) continue;
|
||||
const age = b.permutation.getState(AGE) ?? 0;
|
||||
const outdoor = isAirAbove(dim, b.location);
|
||||
if (age <= 0) {
|
||||
clearTopAbove(b);
|
||||
try { b.setType("minecraft:air"); } catch (_) {}
|
||||
} else {
|
||||
clearTopAbove(b);
|
||||
setAge(b, age - 1, outdoor);
|
||||
}
|
||||
// Drop a hemp_bud at the illager's feet — recoverable when killed.
|
||||
spawnDrop(dim, ill.location, BUD, 1);
|
||||
try { dim.runCommand(`particle minecraft:villager_angry ${ix + 0.5} ${iy + 1.0} ${iz + 0.5}`); } catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, RAID_INTERVAL_TICKS);
|
||||
|
||||
// --- Wandering trader buyback: hand off hemp products for emeralds ---
|
||||
// Right-click a wandering_trader while holding hemp_bud / hemp_tincture /
|
||||
// hemp_seeds. Avoids overriding vanilla trade tables (which would pin us to
|
||||
// one Bedrock version) by acting as an off-menu interaction. Trader animates
|
||||
// a "happy villager" particle and the player's inventory swaps the items
|
||||
// for emeralds at the rates below.
|
||||
const TRADER_BUYBACK = {
|
||||
[BUD]: { perTrade: 2, emeralds: 1 },
|
||||
[TINCTURE]: { perTrade: 1, emeralds: 3 },
|
||||
[SEEDS]: { perTrade: 8, emeralds: 1 },
|
||||
};
|
||||
|
||||
world.beforeEvents.playerInteractWithEntity.subscribe((event) => {
|
||||
const target = event.target;
|
||||
if (!target || target.typeId !== "minecraft:wandering_trader") return;
|
||||
const stack = event.itemStack;
|
||||
if (!stack) return;
|
||||
const deal = TRADER_BUYBACK[stack.typeId];
|
||||
if (!deal) return;
|
||||
event.cancel = true; // suppress the vanilla trade window for this interaction
|
||||
const player = event.player;
|
||||
const held = stack.amount;
|
||||
system.run(() => {
|
||||
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 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 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${consumed}§f for §a${trades * deal.emeralds} emerald${trades * deal.emeralds === 1 ? "" : "s"}§f.`);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Outdoor detection on placement ---
|
||||
world.afterEvents.playerPlaceBlock.subscribe((event) => {
|
||||
const block = event.block;
|
||||
if (!block || block.typeId !== CROP) return;
|
||||
// newly-placed crop is age 0 by default — we don't need to set anything,
|
||||
// outdoor status is computed live during ticks.
|
||||
});
|
||||
|
||||
// --- Cleanup: breaking either half of a tall plant removes the other ---
|
||||
// Also: rare hemp_seeds drop from breaking vanilla grass / tall_grass /
|
||||
// short_grass — gives players a way to bootstrap into hemp without
|
||||
// already having seeds. Mirrors how vanilla wheat seeds work.
|
||||
const GRASS_BLOCK_IDS = new Set([
|
||||
"minecraft:short_grass",
|
||||
"minecraft:tall_grass",
|
||||
"minecraft:fern",
|
||||
"minecraft:large_fern",
|
||||
// Pre-1.21 alias kept for safety on mixed-version worlds
|
||||
"minecraft:grass",
|
||||
]);
|
||||
const GRASS_HEMP_SEED_CHANCE = 0.04; // ~1 in 25 grass tufts
|
||||
|
||||
world.afterEvents.playerBreakBlock.subscribe((event) => {
|
||||
const brokenType = event.brokenBlockPermutation?.type?.id;
|
||||
|
||||
// Hemp_crop tall-plant cleanup
|
||||
if (brokenType === CROP) {
|
||||
const block = event.block;
|
||||
const dim = block.dimension;
|
||||
const wasTop = event.brokenBlockPermutation.getState(TOP) === true;
|
||||
const dy = wasTop ? -1 : 1;
|
||||
let neighbor;
|
||||
try { neighbor = dim.getBlock({ x: block.location.x, y: block.location.y + dy, z: block.location.z }); } catch (_) { return; }
|
||||
if (!neighbor || neighbor.typeId !== CROP) return;
|
||||
const neighborIsTop = neighbor.permutation.getState(TOP) === true;
|
||||
if (wasTop === neighborIsTop) return;
|
||||
try { neighbor.setType("minecraft:air"); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
|
||||
// Hemp seed bootstrap from grass blocks
|
||||
if (brokenType && GRASS_BLOCK_IDS.has(brokenType)) {
|
||||
if (!chance(GRASS_HEMP_SEED_CHANCE)) return;
|
||||
const block = event.block;
|
||||
const loc = block.location;
|
||||
spawnDrop(block.dimension, loc, SEEDS, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Per-player debounce so a single click that fires the event twice
|
||||
// (a known Bedrock quirk for some interact paths) only does work once.
|
||||
const recentInteract = new Map(); // key: playerId|x|y|z -> system.currentTick
|
||||
function consumeInteractToken(player, block) {
|
||||
const key = `${player.id}|${block.location.x}|${block.location.y}|${block.location.z}`;
|
||||
const last = recentInteract.get(key) ?? -999;
|
||||
const now = system.currentTick;
|
||||
if (now - last < 4) return false; // <200ms — treat as duplicate
|
||||
recentInteract.set(key, now);
|
||||
// Lazy cleanup: prune any entries older than ~4s on each successful claim
|
||||
if (recentInteract.size > 64) {
|
||||
for (const [k, t] of recentInteract) {
|
||||
if (now - t > 80) recentInteract.delete(k);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Bonemeal on hemp_crop bumps growth ---
|
||||
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
|
||||
const block = event.block;
|
||||
const stack = event.itemStack;
|
||||
const player = event.player;
|
||||
if (!block || block.typeId !== CROP) return;
|
||||
|
||||
// Sheers harvest path
|
||||
if (stack && stack.typeId === "minecraft:shears") {
|
||||
event.cancel = true;
|
||||
if (!consumeInteractToken(player, block)) return;
|
||||
system.run(() => harvestWithShears(player, block, stack));
|
||||
return;
|
||||
}
|
||||
|
||||
// Bone meal path
|
||||
if (stack && stack.typeId === "minecraft:bone_meal") {
|
||||
event.cancel = true;
|
||||
if (!consumeInteractToken(player, block)) return;
|
||||
system.run(() => {
|
||||
// If the player clicked the top half, redirect to the base
|
||||
let target = block;
|
||||
if (block.permutation.getState(TOP) === true) {
|
||||
const below = block.dimension.getBlock({ x: block.location.x, y: block.location.y - 1, z: block.location.z });
|
||||
if (below && below.typeId === CROP) target = below;
|
||||
else return;
|
||||
}
|
||||
const age = target.permutation.getState(AGE) ?? 0;
|
||||
// Cap bone meal at age 4 (prime) — overripe (5) only via neglect
|
||||
if (age >= 4) {
|
||||
player.sendMessage("§e[Hemp] Already mature — harvest with shears.");
|
||||
return;
|
||||
}
|
||||
// Always consume the bone meal, but only 50% chance to advance
|
||||
consumeOneOfType(player, "minecraft:bone_meal");
|
||||
const loc = target.location;
|
||||
try { target.dimension.runCommand(`particle minecraft:crop_growth_emitter ${loc.x + 0.5} ${loc.y + 0.5} ${loc.z + 0.5}`); } catch (_) {}
|
||||
if (!chance(0.5)) return;
|
||||
const outdoor = isAirAbove(target.dimension, loc);
|
||||
setAge(target, age + 1, outdoor);
|
||||
try { target.dimension.runCommand(`playsound random.fizz @a ${loc.x + 0.5} ${loc.y + 0.5} ${loc.z + 0.5} 0.6 1.6`); } catch (_) {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
function harvestWithShears(player, block, shearsStack) {
|
||||
if (!block || block.typeId !== CROP) return; // already harvested / replaced
|
||||
// Read state on the BASE block (not top)
|
||||
let base = block;
|
||||
if (block.permutation.getState(TOP) === true) {
|
||||
const below = block.dimension.getBlock({ x: block.location.x, y: block.location.y - 1, z: block.location.z });
|
||||
if (below && below.typeId === CROP) base = below;
|
||||
}
|
||||
const age = base.permutation.getState(AGE) ?? 0;
|
||||
const dim = base.dimension;
|
||||
const loc = base.location;
|
||||
const outdoor = isAirAbove(dim, loc);
|
||||
|
||||
// Too early — leave the plant alone, don't damage shears
|
||||
if (age <= 1) {
|
||||
player.sendMessage("§7[Hemp] Too early — leave it to grow.");
|
||||
return;
|
||||
}
|
||||
|
||||
const yields = computeYield(age, outdoor);
|
||||
if (yields.msg) player.sendMessage(yields.msg);
|
||||
|
||||
if (yields.bud > 0) spawnDrop(dim, loc, BUD, yields.bud);
|
||||
if (yields.seeds > 0) spawnDrop(dim, loc, SEEDS, yields.seeds);
|
||||
|
||||
// Fully remove the plant — player replants from seeds
|
||||
clearTopAbove(base);
|
||||
try { base.setType("minecraft:air"); } catch (_) {}
|
||||
|
||||
damageShears(player, shearsStack);
|
||||
try { dim.runCommand(`playsound mob.sheep.shear @a ${loc.x + 0.5} ${loc.y + 0.5} ${loc.z + 0.5}`); } catch (_) {}
|
||||
}
|
||||
|
||||
function computeYield(age, outdoor) {
|
||||
// Outdoor gets a bud bonus; indoor (sun-lamp grown) is baseline.
|
||||
const out = outdoor ? 1 : 0;
|
||||
switch (age) {
|
||||
case 2: return { bud: 1, seeds: 1 + out };
|
||||
case 3: return { bud: 2 + out, seeds: 1 + out };
|
||||
case 4: return { bud: 3 + out * 2, seeds: 1 + out, msg: "§a[Hemp] Prime harvest." }; // peak
|
||||
case 5: return { bud: rand(3), seeds: 3 + rand(3), msg: "§7[Hemp] Overripe — mostly seeds now." };
|
||||
default: return { bud: 0, seeds: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
function spawnDrop(dim, loc, typeId, count) {
|
||||
try {
|
||||
const it = new ItemStack(typeId, count);
|
||||
dim.spawnItem(it, { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 });
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function damageShears(player, shearsStack) {
|
||||
try {
|
||||
const equippable = player.getComponent("minecraft:equippable");
|
||||
if (!equippable) return;
|
||||
const slot = equippable.getEquipmentSlot("Mainhand");
|
||||
if (!slot) return;
|
||||
const item = slot.getItem();
|
||||
if (!item || item.typeId !== "minecraft:shears") return;
|
||||
const dur = item.getComponent("minecraft:durability");
|
||||
if (!dur) return;
|
||||
const damage = dur.damage + 1;
|
||||
if (damage >= dur.maxDurability) {
|
||||
slot.setItem(undefined);
|
||||
try { player.dimension.runCommand(`playsound random.break @a ${player.location.x} ${player.location.y} ${player.location.z}`); } catch (_) {}
|
||||
} else {
|
||||
dur.damage = damage;
|
||||
slot.setItem(item);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// --- Cauldron tincture brewing: hemp_bud + water cauldron + glass bottle => tincture ---
|
||||
world.afterEvents.itemUse.subscribe((event) => {
|
||||
const player = event.source;
|
||||
const stack = event.itemStack;
|
||||
if (!stack || stack.typeId !== BUD) return;
|
||||
system.run(() => {
|
||||
const target = player.getBlockFromViewDirection({ maxDistance: 6 });
|
||||
const block = target?.block;
|
||||
if (!block) return;
|
||||
const isCauldron = block.typeId === "minecraft:cauldron" || block.typeId === "minecraft:water_cauldron";
|
||||
if (!isCauldron) return;
|
||||
|
||||
// Need at least 3 buds total in inventory and 1 glass bottle
|
||||
const inv = getInv(player);
|
||||
if (!inv) return;
|
||||
let budCount = 0, bottleCount = 0;
|
||||
for (let i = 0; i < inv.size; i++) {
|
||||
const it = inv.getItem(i);
|
||||
if (!it) continue;
|
||||
if (it.typeId === BUD) budCount += it.amount;
|
||||
else if (it.typeId === "minecraft:glass_bottle") bottleCount += it.amount;
|
||||
}
|
||||
if (budCount < 3) {
|
||||
player.sendMessage("§c[Hemp] Need 3 hemp buds to brew tincture.");
|
||||
return;
|
||||
}
|
||||
if (bottleCount < 1) {
|
||||
player.sendMessage("§c[Hemp] Need an empty glass bottle.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check water level (water cauldron has fill_level state 1-3; vanilla cauldron = empty)
|
||||
let level = 0;
|
||||
try { level = block.permutation.getState("fill_level") ?? 0; } catch (_) {}
|
||||
if (block.typeId === "minecraft:cauldron") {
|
||||
player.sendMessage("§c[Hemp] Cauldron must contain water.");
|
||||
return;
|
||||
}
|
||||
if (level <= 0) {
|
||||
player.sendMessage("§c[Hemp] Cauldron is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Consume 3 buds, 1 bottle
|
||||
for (let i = 0; i < 3; i++) consumeOneOfType(player, BUD);
|
||||
consumeOneOfType(player, "minecraft:glass_bottle");
|
||||
|
||||
// Decrement water level
|
||||
try {
|
||||
const newLevel = level - 1;
|
||||
if (newLevel <= 0) {
|
||||
block.setType("minecraft:cauldron");
|
||||
} else {
|
||||
const perm = BlockPermutation.resolve("minecraft:water_cauldron", { fill_level: newLevel });
|
||||
block.setPermutation(perm);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Give tincture
|
||||
giveItem(player, TINCTURE, 1);
|
||||
const loc = block.location;
|
||||
try { block.dimension.runCommand(`particle minecraft:water_splash_particle_manual ${loc.x + 0.5} ${loc.y + 1.0} ${loc.z + 0.5}`); } catch (_) {}
|
||||
try { block.dimension.runCommand(`playsound bucket.fill_water @a ${loc.x + 0.5} ${loc.y + 0.5} ${loc.z + 0.5}`); } catch (_) {}
|
||||
player.sendMessage("§a[Hemp] Hemp tincture brewed.");
|
||||
});
|
||||
});
|
||||
|
||||
// --- Consumption effects ---
|
||||
world.afterEvents.itemCompleteUse.subscribe((event) => {
|
||||
const player = event.source;
|
||||
const stack = event.itemStack;
|
||||
if (!stack) return;
|
||||
if (stack.typeId === TINCTURE) {
|
||||
try {
|
||||
player.addEffect("regeneration", 100, { amplifier: 1, showParticles: true });
|
||||
player.addEffect("slowness", 200, { amplifier: 0, showParticles: true });
|
||||
} catch (_) {}
|
||||
player.sendMessage("§a[Hemp] You feel a warm, calming wave.");
|
||||
} else if (stack.typeId === BROWNIE) {
|
||||
try {
|
||||
player.addEffect("regeneration", 200, { amplifier: 0, showParticles: true });
|
||||
player.addEffect("slowness", 400, { amplifier: 0, showParticles: true });
|
||||
} catch (_) {}
|
||||
player.sendMessage("§a[Hemp] Mmm. You feel relaxed.");
|
||||
}
|
||||
});
|
||||
|
||||
// --- 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.");
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"format_version": [1, 1, 0],
|
||||
"silverlabs:hemp_crop": { "sound": "grass" },
|
||||
"silverlabs:sun_lamp": { "sound": "glass" }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"format_version": 2,
|
||||
"header": {
|
||||
"name": "Hemp Plant Resources",
|
||||
"description": "Textures, models, lang for the Hemp Plant addon",
|
||||
"uuid": "8e5c46ac-3b16-4f51-89f7-673bd06600f4",
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
5
|
||||
],
|
||||
"min_engine_version": [
|
||||
1,
|
||||
21,
|
||||
0
|
||||
]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "resources",
|
||||
"uuid": "5fcc737f-d224-409d-85e9-99274afd72a9",
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
5
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
item.silverlabs:hemp_seeds.name=Hemp Seeds
|
||||
item.silverlabs:hemp_bud.name=Hemp Bud
|
||||
item.silverlabs:hemp_tincture.name=Hemp Tincture
|
||||
item.silverlabs:hemp_brownie.name=Hemp Brownie
|
||||
tile.silverlabs:hemp_crop.name=Hemp Plant
|
||||
tile.silverlabs:sun_lamp.name=Sun Lamp
|
||||
@@ -0,0 +1 @@
|
||||
["en_US"]
|
||||
|
After Width: | Height: | Size: 108 B |
|
After Width: | Height: | Size: 125 B |
|
After Width: | Height: | Size: 156 B |
|
After Width: | Height: | Size: 174 B |
|
After Width: | Height: | Size: 316 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 482 B |
|
After Width: | Height: | Size: 298 B |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"resource_pack_name": "hemp_RP",
|
||||
"texture_name": "atlas.items",
|
||||
"texture_data": {
|
||||
"hemp_seeds": { "textures": "textures/items/hemp_seeds" },
|
||||
"hemp_bud": { "textures": "textures/items/hemp_bud" },
|
||||
"hemp_tincture": { "textures": "textures/items/hemp_tincture" },
|
||||
"hemp_brownie": { "textures": "textures/items/hemp_brownie" }
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 177 B |
|
After Width: | Height: | Size: 198 B |
|
After Width: | Height: | Size: 139 B |
|
After Width: | Height: | Size: 152 B |
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"resource_pack_name": "hemp_RP",
|
||||
"texture_name": "atlas.terrain",
|
||||
"padding": 8,
|
||||
"num_mip_levels": 4,
|
||||
"texture_data": {
|
||||
"hemp_crop_0": { "textures": "textures/blocks/hemp_crop_0" },
|
||||
"hemp_crop_1": { "textures": "textures/blocks/hemp_crop_1" },
|
||||
"hemp_crop_2": { "textures": "textures/blocks/hemp_crop_2" },
|
||||
"hemp_crop_3": { "textures": "textures/blocks/hemp_crop_3" },
|
||||
"hemp_crop_4": { "textures": "textures/blocks/hemp_crop_4" },
|
||||
"hemp_crop_5": { "textures": "textures/blocks/hemp_crop_5" },
|
||||
"sun_lamp": { "textures": "textures/blocks/sun_lamp" },
|
||||
"sun_lamp_off": { "textures": "textures/blocks/sun_lamp_off" }
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
"explosion_resistance": 200.0
|
||||
},
|
||||
"minecraft:map_color": "#C0703A",
|
||||
"minecraft:geometry": "minecraft:geometry.full_block",
|
||||
"minecraft:material_instances": {
|
||||
"*": {
|
||||
"texture": "home_sign",
|
||||
|
||||
@@ -4,20 +4,36 @@
|
||||
"name": "Home Sweet Home",
|
||||
"description": "Defines a 32-block home zone for tamed cats; quieter cat meows",
|
||||
"uuid": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b80",
|
||||
"version": [1, 0, 0],
|
||||
"min_engine_version": [1, 21, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min_engine_version": [
|
||||
1,
|
||||
21,
|
||||
0
|
||||
]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "data",
|
||||
"uuid": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b81",
|
||||
"version": [1, 0, 0]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "script",
|
||||
"language": "javascript",
|
||||
"uuid": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b82",
|
||||
"version": [1, 0, 0],
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"entry": "scripts/main.js"
|
||||
}
|
||||
],
|
||||
@@ -28,7 +44,11 @@
|
||||
},
|
||||
{
|
||||
"uuid": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b83",
|
||||
"version": [1, 0, 1]
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,6 +17,33 @@ const ARRIVAL_RADIUS = 3;
|
||||
const HUD_TICK_INTERVAL = 5;
|
||||
const ARROWS = ["\u2191", "\u2197", "\u2192", "\u2198", "\u2193", "\u2199", "\u2190", "\u2196"];
|
||||
|
||||
// Navigation HUD lives in the title/subtitle slot so it doesn't fight the
|
||||
// vanilla mount UI for the action bar. The directional content goes in the
|
||||
// TITLE position (large, central) with the waypoint label in the subtitle as
|
||||
// a secondary line. Empty-string title gets suppressed by some Bedrock client
|
||||
// versions, so we always pass a non-empty title — that's what causes the
|
||||
// arrival message to render but a setActionBar-replacement subtitle not to.
|
||||
function showNav(player, mainLine, subLine) {
|
||||
try {
|
||||
player.onScreenDisplay.setTitle(mainLine, {
|
||||
subtitle: subLine ?? "",
|
||||
fadeInDuration: 0,
|
||||
stayDuration: HUD_TICK_INTERVAL * 4,
|
||||
fadeOutDuration: 0,
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
function clearNav(player) {
|
||||
try {
|
||||
player.onScreenDisplay.setTitle(" ", {
|
||||
subtitle: "",
|
||||
fadeInDuration: 0,
|
||||
stayDuration: 1,
|
||||
fadeOutDuration: 0,
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const recentTransfers = new Map();
|
||||
const spawnTimes = new Map();
|
||||
|
||||
@@ -24,8 +51,22 @@ const spawnTimes = new Map();
|
||||
|
||||
// Persisted: markers per player { [ownerId]: [ {x,y,z,dim,label,key}, ... ] }
|
||||
let markers = {};
|
||||
// Session-only: which marker each player is actively guiding toward
|
||||
const active = new Map();
|
||||
// Per-player active waypoint index, persisted via player dynamic property so
|
||||
// it survives container restarts and reconnects.
|
||||
const ACTIVE_PROP = "hub_active_waypoint_v1";
|
||||
function getActiveIndex(player) {
|
||||
try {
|
||||
const v = player.getDynamicProperty(ACTIVE_PROP);
|
||||
return typeof v === "number" ? v : null;
|
||||
} catch (_) { return null; }
|
||||
}
|
||||
function setActiveIndex(player, idx) {
|
||||
try {
|
||||
if (idx == null) player.setDynamicProperty(ACTIVE_PROP, undefined);
|
||||
else player.setDynamicProperty(ACTIVE_PROP, idx);
|
||||
} catch (_) {}
|
||||
}
|
||||
function clearActiveIndex(player) { setActiveIndex(player, null); }
|
||||
|
||||
function keyOf(loc, dimensionId) {
|
||||
return `${Math.floor(loc.x)},${Math.floor(loc.y)},${Math.floor(loc.z)},${dimensionId}`;
|
||||
@@ -67,10 +108,16 @@ function findMarkerByKey(key) {
|
||||
function removeMarker(ownerId, index) {
|
||||
const list = getMarkers(ownerId);
|
||||
list.splice(index, 1);
|
||||
const activeIdx = active.get(ownerId);
|
||||
if (activeIdx != null) {
|
||||
if (activeIdx === index) active.delete(ownerId);
|
||||
else if (activeIdx > index) active.set(ownerId, activeIdx - 1);
|
||||
// Active index is stored on the owner's player dynamic property; update only
|
||||
// if they're online (offline owner will revalidate their own index next tick).
|
||||
for (const p of world.getAllPlayers()) {
|
||||
if (p.id !== ownerId) continue;
|
||||
const activeIdx = getActiveIndex(p);
|
||||
if (activeIdx != null) {
|
||||
if (activeIdx === index) clearActiveIndex(p);
|
||||
else if (activeIdx > index) setActiveIndex(p, activeIdx - 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
saveWaypoints();
|
||||
}
|
||||
@@ -149,11 +196,8 @@ world.afterEvents.playerSpawn.subscribe((event) => {
|
||||
}, 20);
|
||||
});
|
||||
|
||||
world.afterEvents.playerLeave.subscribe((event) => {
|
||||
// Clear session HUD state for whoever left (keyed by id)
|
||||
// event.playerId exists; event.playerName for compat
|
||||
if (event.playerId) active.delete(event.playerId);
|
||||
});
|
||||
// Active waypoint is persisted as a player dynamic property, so playerLeave
|
||||
// does not need to clear any in-memory session state.
|
||||
|
||||
// ─── Compass Menu ───────────────────────────────────────────────
|
||||
|
||||
@@ -183,7 +227,7 @@ async function openCompassMenu(player) {
|
||||
actions.push({ kind: "select", index: i });
|
||||
}
|
||||
|
||||
const activeIdx = active.get(player.id);
|
||||
const activeIdx = getActiveIndex(player);
|
||||
if (activeIdx != null && list[activeIdx]) {
|
||||
form.button(`§c\u274C Clear active waypoint`);
|
||||
actions.push({ kind: "clear" });
|
||||
@@ -192,6 +236,9 @@ async function openCompassMenu(player) {
|
||||
if (list.length > 0) {
|
||||
form.button("\uD83D\uDDD1 Delete a waypoint…");
|
||||
actions.push({ kind: "delete_menu" });
|
||||
|
||||
form.button("\uD83D\uDCE4 Share with another player…");
|
||||
actions.push({ kind: "share" });
|
||||
}
|
||||
|
||||
form.button("\uD83E\uDDED Get a marker block");
|
||||
@@ -212,14 +259,14 @@ async function openCompassMenu(player) {
|
||||
return;
|
||||
}
|
||||
if (a.kind === "select") {
|
||||
active.set(player.id, a.index);
|
||||
setActiveIndex(player, a.index);
|
||||
const m = list[a.index];
|
||||
player.sendMessage(`§b[Waypoints] §fGuiding you to §d${m.label}§f.`);
|
||||
player.sendMessage(`§b[Waypoints] §fGuiding you to §d${m.label}§f. §8(arrow appears at the top of your screen)`);
|
||||
return;
|
||||
}
|
||||
if (a.kind === "clear") {
|
||||
active.delete(player.id);
|
||||
player.onScreenDisplay.setActionBar("");
|
||||
clearActiveIndex(player);
|
||||
clearNav(player);
|
||||
player.sendMessage("§b[Waypoints] §fActive waypoint cleared.");
|
||||
return;
|
||||
}
|
||||
@@ -227,6 +274,10 @@ async function openCompassMenu(player) {
|
||||
await openDeleteMenu(player);
|
||||
return;
|
||||
}
|
||||
if (a.kind === "share") {
|
||||
await openShareMenu(player);
|
||||
return;
|
||||
}
|
||||
if (a.kind === "give_marker") {
|
||||
giveMarkerBlock(player);
|
||||
return;
|
||||
@@ -286,6 +337,102 @@ async function openDeleteMenu(player) {
|
||||
player.sendMessage(`§b[Waypoints] §fDeleted §d${chosen.label}§f.`);
|
||||
}
|
||||
|
||||
// ─── Share Waypoint ─────────────────────────────────────────────
|
||||
// Pick one of your waypoints, pick an online player in this world, send
|
||||
// them a confirm prompt. On accept, the waypoint is copied into the
|
||||
// recipient's list with a `(from <sender>)` suffix so it's clearly
|
||||
// attributed. Cross-world shares are blocked here — a player on the lyla
|
||||
// server can't be reached from jamie. Same dimension is preferred but
|
||||
// cross-dim is allowed; the HUD already labels "different dimension".
|
||||
|
||||
async function openShareMenu(sender) {
|
||||
const list = getMarkers(sender.id);
|
||||
if (list.length === 0) {
|
||||
sender.sendMessage("§7[Share] You have no waypoints to share.");
|
||||
return;
|
||||
}
|
||||
const others = world.getAllPlayers().filter((p) => p.id !== sender.id);
|
||||
if (others.length === 0) {
|
||||
sender.sendMessage("§7[Share] No other players are online in this world.");
|
||||
return;
|
||||
}
|
||||
|
||||
const wpForm = new ActionFormData()
|
||||
.title("Share a Waypoint")
|
||||
.body("§7Pick which waypoint to share:");
|
||||
for (const m of list) {
|
||||
wpForm.button(`§f${m.label}\n§7${m.x}, ${m.y}, ${m.z}`);
|
||||
}
|
||||
wpForm.button("§7Cancel");
|
||||
|
||||
let wpRes;
|
||||
try { wpRes = await wpForm.show(sender); } catch (_) { return; }
|
||||
if (wpRes.canceled || wpRes.selection === undefined) return;
|
||||
if (wpRes.selection >= list.length) return;
|
||||
const marker = list[wpRes.selection];
|
||||
|
||||
const recipForm = new ActionFormData()
|
||||
.title(`Share "${marker.label}"`)
|
||||
.body("§7Who should receive this waypoint?");
|
||||
for (const p of others) recipForm.button(`§f${p.name}`);
|
||||
recipForm.button("§7Cancel");
|
||||
|
||||
let rRes;
|
||||
try { rRes = await recipForm.show(sender); } catch (_) { return; }
|
||||
if (rRes.canceled || rRes.selection === undefined) return;
|
||||
if (rRes.selection >= others.length) return;
|
||||
const recipient = others[rRes.selection];
|
||||
|
||||
// Receiver capacity check before bothering them with a prompt
|
||||
const recList = getMarkers(recipient.id);
|
||||
if (recList.length >= MAX_MARKERS_PER_PLAYER) {
|
||||
sender.sendMessage(`§c[Share] §f${recipient.name}'s waypoint slots are full.`);
|
||||
return;
|
||||
}
|
||||
|
||||
sender.sendMessage(`§b[Share] §fSent §d${marker.label}§f to §e${recipient.name}§f — waiting for them to accept…`);
|
||||
|
||||
const offer = new MessageFormData()
|
||||
.title("Waypoint Shared")
|
||||
.body(`§e${sender.name}§r wants to share a waypoint with you:\n\n§f${marker.label}\n§7${marker.x}, ${marker.y}, ${marker.z}\n\nAdd to your compass?`)
|
||||
.button1("§aAccept")
|
||||
.button2("§cDecline");
|
||||
|
||||
let oRes;
|
||||
try { oRes = await offer.show(recipient); } catch (_) {
|
||||
sender.sendMessage(`§c[Share] §f${recipient.name} couldn't be reached.`);
|
||||
return;
|
||||
}
|
||||
if (oRes.canceled || oRes.selection !== 0) {
|
||||
sender.sendMessage(`§7[Share] §f${recipient.name} declined.`);
|
||||
recipient.sendMessage(`§7[Share] §fDeclined waypoint from §e${sender.name}§f.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-check capacity after the await — markers may have changed
|
||||
if (recList.length >= MAX_MARKERS_PER_PLAYER) {
|
||||
sender.sendMessage(`§c[Share] §f${recipient.name}'s waypoint slots are full.`);
|
||||
recipient.sendMessage(`§c[Share] §fYour waypoint slots are full — couldn't accept §d${marker.label}§f.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const sharedLabel = `${marker.label} (from ${sender.name})`.slice(0, 40);
|
||||
// Use a key that won't collide with the receiver's own lodestones
|
||||
const sharedKey = `shared:${sender.id}:${marker.key}`;
|
||||
recList.push({
|
||||
x: marker.x,
|
||||
y: marker.y,
|
||||
z: marker.z,
|
||||
dim: marker.dim,
|
||||
label: sharedLabel,
|
||||
key: sharedKey,
|
||||
});
|
||||
saveWaypoints();
|
||||
|
||||
sender.sendMessage(`§a[Share] §f${recipient.name} accepted §d${marker.label}§f.`);
|
||||
recipient.sendMessage(`§a[Share] §fAdded §d${sharedLabel}§f. Type §e!nav§f to find it.`);
|
||||
}
|
||||
|
||||
// ─── Placement → Label Prompt ───────────────────────────────────
|
||||
|
||||
world.afterEvents.playerPlaceBlock.subscribe((event) => {
|
||||
@@ -402,17 +549,17 @@ function distanceXZ(a, b) {
|
||||
|
||||
system.runInterval(() => {
|
||||
for (const player of world.getAllPlayers()) {
|
||||
const idx = active.get(player.id);
|
||||
const idx = getActiveIndex(player);
|
||||
if (idx == null) continue;
|
||||
|
||||
const list = markers[player.id];
|
||||
const m = list && list[idx];
|
||||
if (!m) {
|
||||
active.delete(player.id);
|
||||
clearActiveIndex(player);
|
||||
continue;
|
||||
}
|
||||
if (m.dim !== player.dimension.id) {
|
||||
player.onScreenDisplay.setActionBar(`§b\u27A4 §f${m.label} §7• §cdifferent dimension`);
|
||||
showNav(player, `§c⚠ Different Dimension`, `§7${m.label}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -422,10 +569,9 @@ system.runInterval(() => {
|
||||
const distXZ = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
if (distXZ <= ARRIVAL_RADIUS) {
|
||||
active.delete(player.id);
|
||||
clearActiveIndex(player);
|
||||
try {
|
||||
player.onScreenDisplay.setTitle(`§aArrived`, { subtitle: `§f${m.label}`, fadeInDuration: 5, stayDuration: 30, fadeOutDuration: 10 });
|
||||
player.onScreenDisplay.setActionBar("");
|
||||
} catch (_) {}
|
||||
continue;
|
||||
}
|
||||
@@ -439,11 +585,7 @@ system.runInterval(() => {
|
||||
const bucket = ((Math.round(relative / 45) % 8) + 8) % 8;
|
||||
const arrow = ARROWS[bucket];
|
||||
|
||||
try {
|
||||
player.onScreenDisplay.setActionBar(
|
||||
`§b\u27A4 §f${m.label} §7• §f${Math.round(distXZ)}m §8[§a${arrow}§8]`
|
||||
);
|
||||
} catch (_) {}
|
||||
showNav(player, `§a${arrow} §f${Math.round(distXZ)}m`, `§7${m.label}`);
|
||||
}
|
||||
}, HUD_TICK_INTERVAL);
|
||||
|
||||
@@ -515,6 +657,52 @@ function handleChatCommand(player, msg) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg === "!share") {
|
||||
system.run(() => openShareMenu(player));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg === "!nav" || msg.startsWith("!nav ")) {
|
||||
const list = getMarkers(player.id);
|
||||
const arg = msg.slice(4).trim();
|
||||
if (arg === "off" || arg === "clear") {
|
||||
clearActiveIndex(player);
|
||||
clearNav(player);
|
||||
player.sendMessage("§b[Waypoints] §fNavigation cleared.");
|
||||
return true;
|
||||
}
|
||||
if (arg) {
|
||||
const n = Number.parseInt(arg, 10);
|
||||
if (Number.isFinite(n) && n >= 1 && n <= list.length) {
|
||||
setActiveIndex(player, n - 1);
|
||||
const m = list[n - 1];
|
||||
player.sendMessage(`§b[Waypoints] §fGuiding you to §d${m.label}§f. §8(arrow at top of screen)`);
|
||||
return true;
|
||||
}
|
||||
player.sendMessage(`§c[Waypoints] §fNo waypoint #${arg} — type §e!nav§f to list yours.`);
|
||||
return true;
|
||||
}
|
||||
if (list.length === 0) {
|
||||
player.sendMessage("§7[Waypoints] No waypoints yet — place a §dlodestone§7 to set one.");
|
||||
return true;
|
||||
}
|
||||
const idx = getActiveIndex(player);
|
||||
const cur = idx != null ? list[idx] : null;
|
||||
if (cur) {
|
||||
const dist = Math.round(distanceXZ(player.location, cur));
|
||||
player.sendMessage(`§b[Waypoints] §fActive: §d${cur.label}§f (${dist}m). Type §e!nav off§f to clear, or §e!nav <number>§f to switch.`);
|
||||
} else {
|
||||
player.sendMessage("§b[Waypoints] §fNo active waypoint. Pick one:");
|
||||
}
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const m = list[i];
|
||||
const here = m.dim === player.dimension.id;
|
||||
const tag = here ? `§7${Math.round(distanceXZ(player.location, m))}m` : "§8(other dim)";
|
||||
player.sendMessage(` §e${i + 1}§7. §f${m.label} ${tag}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg === "!clearwaypoints") {
|
||||
system.run(async () => {
|
||||
const confirm = new MessageFormData()
|
||||
@@ -526,7 +714,7 @@ function handleChatCommand(player, msg) {
|
||||
try { res = await confirm.show(player); } catch (_) { return; }
|
||||
if (res.canceled || res.selection !== 0) return;
|
||||
markers[player.id] = [];
|
||||
active.delete(player.id);
|
||||
clearActiveIndex(player);
|
||||
saveWaypoints();
|
||||
player.sendMessage("§b[Waypoints] §fAll your waypoints were cleared.");
|
||||
});
|
||||
@@ -606,5 +794,5 @@ system.runTimeout(() => {
|
||||
|
||||
system.run(() => {
|
||||
loadWaypoints();
|
||||
world.sendMessage("§b[World] §fHub return system loaded! Place a §dlodestone§f to set a waypoint; right-click your compass to navigate.");
|
||||
world.sendMessage("§b[World] §fHub return system loaded! Place a §dlodestone§f to set a waypoint, then right-click your compass and pick one — the §anavigation arrow§f shows at the §atop§f of your screen. (Type §e!nav§f for chat fallback.)");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"format_version": 2,
|
||||
"header": {
|
||||
"name": "Hub Return Resources",
|
||||
"description": "Recovery compass icon override + future hub-return assets",
|
||||
"uuid": "b1f7e2a4-3c5d-49b8-9d22-6a4f0c87e511",
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
5
|
||||
],
|
||||
"min_engine_version": [
|
||||
1,
|
||||
21,
|
||||
0
|
||||
]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "resources",
|
||||
"uuid": "c4a13e85-2f96-4d1a-b772-9e0f3b4d6c21",
|
||||
"version": [
|
||||
1,
|
||||
0,
|
||||
5
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
},
|
||||
"minecraft:light_emission": 10,
|
||||
"minecraft:map_color": "#5C6D74",
|
||||
"minecraft:geometry": "minecraft:geometry.full_block",
|
||||
"minecraft:material_instances": {
|
||||
"*": {
|
||||
"texture": "portal_frame",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
},
|
||||
"minecraft:light_emission": 10,
|
||||
"minecraft:map_color": "#2D9C2D",
|
||||
"minecraft:geometry": "minecraft:geometry.full_block",
|
||||
"minecraft:material_instances": {
|
||||
"*": {
|
||||
"texture": "portal_jamie",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
},
|
||||
"minecraft:light_emission": 10,
|
||||
"minecraft:map_color": "#9B59B6",
|
||||
"minecraft:geometry": "minecraft:geometry.full_block",
|
||||
"minecraft:material_instances": {
|
||||
"*": {
|
||||
"texture": "portal_lyla",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
},
|
||||
"minecraft:light_emission": 10,
|
||||
"minecraft:map_color": "#4FC1E9",
|
||||
"minecraft:geometry": "minecraft:geometry.full_block",
|
||||
"minecraft:material_instances": {
|
||||
"*": {
|
||||
"texture": "portal_mya",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.alligator.evict_riders": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"eject_rider": "query.has_target"
|
||||
}
|
||||
]
|
||||
},
|
||||
"eject_rider": {
|
||||
"transitions": [
|
||||
{
|
||||
"default": "(1.0)"
|
||||
}
|
||||
],
|
||||
"on_entry": [
|
||||
"/ride @s evict_riders"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.anteater": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"on_entry": ["/effect @s clear slowness"],
|
||||
"transitions": [
|
||||
{
|
||||
"not_move": "q.is_interested || q.timer_flag_1 || query.property('silverlabs_nat:on_defensive_mode') == 'enabled'"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"not_move": {
|
||||
"on_entry": ["/effect @s slowness infinite 255 true"],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "!q.is_interested && !q.timer_flag_1 && query.property('silverlabs_nat:on_defensive_mode') != 'enabled'"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.badger.daylight_controller": {
|
||||
"initial_state": "init",
|
||||
"states": {
|
||||
"init": {
|
||||
"transitions": [
|
||||
{
|
||||
"daylight_tamed": "((q.time_of_day - 0.25) <= 0.5) && q.is_tamed"
|
||||
},
|
||||
{
|
||||
"daylight_untamed": "((q.time_of_day - 0.25) <= 0.5)"
|
||||
},
|
||||
{
|
||||
"night": "((q.time_of_day - 0.25) > 0.5)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"daylight_tamed": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:badger_daytime_tamed"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"daylight_tamed": "((q.time_of_day - 0.25) <= 0.5) && q.is_tamed"
|
||||
},
|
||||
{
|
||||
"daylight_untamed": "((q.time_of_day - 0.25) <= 0.5)"
|
||||
},
|
||||
{
|
||||
"night": "((q.time_of_day - 0.25) > 0.5)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"daylight_untamed": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:badger_daytime_wild"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"daylight_tamed": "((q.time_of_day - 0.25) <= 0.5) && q.is_tamed"
|
||||
},
|
||||
{
|
||||
"daylight_untamed": "((q.time_of_day - 0.25) <= 0.5)"
|
||||
},
|
||||
{
|
||||
"night": "((q.time_of_day - 0.25) > 0.5)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"night": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:badger_nighttime"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"daylight_tamed": "((q.time_of_day - 0.25) <= 0.5) && q.is_tamed"
|
||||
},
|
||||
{
|
||||
"daylight_untamed": "((q.time_of_day - 0.25) <= 0.5)"
|
||||
},
|
||||
{
|
||||
"night": "((q.time_of_day - 0.25) > 0.5)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.bear.eat": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"can_use": "query.property('silverlabs_nat:is_sitting')"
|
||||
}
|
||||
]
|
||||
},
|
||||
"can_use": {
|
||||
"transitions": [
|
||||
{
|
||||
"default": "!query.property('silverlabs_nat:is_sitting')"
|
||||
},
|
||||
{
|
||||
"configure_eating": "!math.random_integer(0, 79)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configure_eating": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:configure_eating"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "!query.property('silverlabs_nat:is_sitting')"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.beaver.chew_watcher": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{ "chewing": "query.property('silverlabs_nat:is_chewing')" }
|
||||
]
|
||||
},
|
||||
"chewing": {
|
||||
"on_exit": [
|
||||
"/scriptevent silverlabs_nat:beaver_chew"
|
||||
],
|
||||
"transitions": [
|
||||
{ "default": "!query.property('silverlabs_nat:is_chewing')" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.bird.landing_check": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"on_entry": [
|
||||
"v.switch_chance = 200;"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"walking": "query.property('silverlabs_nat:movement_mode') == 'walk'"
|
||||
},
|
||||
{
|
||||
"flying": "query.property('silverlabs_nat:movement_mode') == 'fly'"
|
||||
}
|
||||
]
|
||||
},
|
||||
"walking": {
|
||||
"on_entry": [
|
||||
"v.switch_delay = query.life_time + Math.random_integer(5, 10);"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"start_flying": "!q.is_baby && (Math.random_integer(0, (v.switch_chance ?? 1)) == (v.switch_chance ?? 1) && (query.life_time > (v.switch_delay ?? 0))) && !query.property('silverlabs_nat:is_tempted') && !q.is_sitting && !q.is_in_love"
|
||||
},
|
||||
{
|
||||
"flying": "query.property('silverlabs_nat:movement_mode') == 'fly'"
|
||||
}
|
||||
]
|
||||
},
|
||||
"start_flying": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:bird_set_flying_mode"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"flying": "query.property('silverlabs_nat:movement_mode') == 'fly'"
|
||||
}
|
||||
]
|
||||
},
|
||||
"flying": {
|
||||
"on_entry": [
|
||||
"v.switch_delay = query.life_time + Math.random_integer(5, 10);"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"landing_check": "q.is_baby || (Math.random_integer(0, (v.switch_chance ?? 1)) == (v.switch_chance ?? 1) && (query.life_time > (v.switch_delay ?? 0)) && !q.has_target) || query.property('silverlabs_nat:is_tempted') || q.is_sitting || q.is_in_love"
|
||||
},
|
||||
{
|
||||
"walking": "query.property('silverlabs_nat:movement_mode') == 'walk'"
|
||||
}
|
||||
]
|
||||
},
|
||||
"landing_check": {
|
||||
"on_entry": [
|
||||
"/scriptevent silverlabs_nat:bird_landing_check"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.bird.tempt_check": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:not_tempted"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"tempted": "q.is_interested"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tempted": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:is_tempted"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "!q.is_interested"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.bird.smooth_landing": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"tempted": "!q.is_on_ground"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tempted": {
|
||||
"on_entry": [
|
||||
"/effect @s slow_falling 2 1 true"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.blobfish": {
|
||||
"initial_state": "init",
|
||||
"states": {
|
||||
"init": {
|
||||
"transitions": [
|
||||
{
|
||||
"normal_blobfish": "query.is_in_water && q.position(1) < 30"
|
||||
},
|
||||
{
|
||||
"land_blobfish": "q.position(1) > 30"
|
||||
}
|
||||
]
|
||||
},
|
||||
"normal_blobfish": {
|
||||
"animations": [
|
||||
"blobfish.duration"
|
||||
],
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:blobfish_normal"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"land_blobfish": "q.anim_time > 3.0 && q.position(1) > 30"
|
||||
}
|
||||
]
|
||||
},
|
||||
"land_blobfish": {
|
||||
"animations": [
|
||||
"blobfish.duration"
|
||||
],
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:blobfish_land"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"normal_blobfish": "q.anim_time > 3.0 && query.is_in_water && q.position(1) < 30"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.bucketable_entity": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"on_bucket": "q.has_any_family('silverlabs_nat:bucketable_entity') && query.property('silverlabs_nat:on_bucket')"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"on_bucket": {
|
||||
"on_entry": [
|
||||
"/scriptevent silverlabs_nat:bucketable_entity_interaction @initiator"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.clam.launch": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"before_open": "query.property('silverlabs_nat:before_open')"
|
||||
}
|
||||
]
|
||||
},
|
||||
"before_open": {
|
||||
"on_entry": [
|
||||
"/scriptevent silverlabs_nat:clam_launch"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "!query.property('silverlabs_nat:before_open')"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.clam.took_item": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"has_item": "query.property('silverlabs_nat:has_item')"
|
||||
}
|
||||
]
|
||||
},
|
||||
"has_item": {
|
||||
"transitions": [
|
||||
{
|
||||
"took_item": "query.property('silverlabs_nat:took_item')"
|
||||
}
|
||||
]
|
||||
},
|
||||
"took_item": {
|
||||
"on_entry": [
|
||||
"/replaceitem entity @s slot.weapon.mainhand 0 minecraft:air"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.crab": {
|
||||
"initial_state": "on_load",
|
||||
"states": {
|
||||
"on_load": {
|
||||
"on_entry": ["@s silverlabs_nat:crab_is_not_dancing"],
|
||||
"transitions": [
|
||||
{
|
||||
"try_collect_sand": "query.property('silverlabs_nat:sand_block_interaction_state') == 'try_collect_sand'"
|
||||
},
|
||||
{
|
||||
"try_place_sand": "query.property('silverlabs_nat:sand_block_interaction_state') == 'try_place_sand'"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"try_collect_sand": {
|
||||
"on_entry": ["/scriptevent silverlabs_nat:crab_try_collect_sand"],
|
||||
"transitions": [
|
||||
{
|
||||
"on_load": "query.property('silverlabs_nat:sand_block_interaction_state') != 'try_collect_sand'"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"try_place_sand": {
|
||||
"on_entry": ["/scriptevent silverlabs_nat:crab_try_place_sand"],
|
||||
"transitions": [
|
||||
{
|
||||
"on_load": "query.property('silverlabs_nat:sand_block_interaction_state') != 'try_place_sand'"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.eel_water_land": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"land": "!q.is_in_water"
|
||||
},
|
||||
{
|
||||
"water": "q.is_in_water"
|
||||
}
|
||||
]
|
||||
},
|
||||
"land": {
|
||||
"on_entry": ["@s silverlabs_nat:eel_on_land"],
|
||||
"transitions": [
|
||||
{
|
||||
"water": "q.is_in_water"
|
||||
}
|
||||
]
|
||||
},
|
||||
"water": {
|
||||
"on_entry": ["@s silverlabs_nat:eel_in_water"],
|
||||
"transitions": [
|
||||
{
|
||||
"land": "!q.is_in_water"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.emperor_penguin.fall": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"fall": "query.property('silverlabs_nat:slip_anim') == 'none' && q.is_on_ground && !q.is_in_water && q.modified_move_speed > 0.05 && !query.property('silverlabs_nat:egg_protector') && math.random_integer(0, 333) == 0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fall": {
|
||||
"on_entry": [
|
||||
"/scriptevent silverlabs_nat:emperor_penguin_fall",
|
||||
"v.cooldown_timer = query.life_time;"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "query.life_time >= (v.cooldown_timer + 16.0)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.emperor_penguin.egg_check": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"on_entry": [
|
||||
"v.cooldown_timer = 0;"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"remove": "query.property('silverlabs_nat:egg_protector') && q.has_rider"
|
||||
},
|
||||
{
|
||||
"cooldown": "query.property('silverlabs_nat:egg_protector') && !q.has_target"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cooldown": {
|
||||
"on_entry": [
|
||||
"v.cooldown_timer = query.life_time;"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"remove": "q.has_rider"
|
||||
},
|
||||
{
|
||||
"default": "!query.property('silverlabs_nat:egg_protector') || q.has_target"
|
||||
},
|
||||
{
|
||||
"egg_check": "query.life_time >= (v.cooldown_timer + 6.0)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"egg_check": {
|
||||
"on_entry": [
|
||||
"/execute unless entity @e[type=silverlabs_nat:emperor_penguin_egg,c=1,r=24] run event entity @s silverlabs_nat:remove_egg_protector"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"remove": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:remove_egg_protector"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.light": {
|
||||
"states": {
|
||||
"default": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:reset_all"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"reset": "q.is_shaking"
|
||||
},
|
||||
{
|
||||
"on": "!q.is_shaking"
|
||||
}
|
||||
]
|
||||
},
|
||||
"on": {
|
||||
"on_entry": [
|
||||
"/tag @s add silverlabs_nat:light_source",
|
||||
"/function sf/nba/lighting/setup"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "query.property('silverlabs_nat:furniture_variant') == 1"
|
||||
},
|
||||
{
|
||||
"reset": "q.is_shaking"
|
||||
}
|
||||
]
|
||||
},
|
||||
"reset": {
|
||||
"on_entry": [
|
||||
"/function sf/nba/lighting/remove",
|
||||
"/tag @s remove silverlabs_nat:light_source"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.furniture_moved": {
|
||||
"initial_state": "not_moving",
|
||||
"states": {
|
||||
"not_moving": {
|
||||
"transitions": [
|
||||
{
|
||||
"moving": "math.abs(q.position_delta(0)) > 0.01 || math.abs(q.position_delta(1)) > 0.01 || math.abs(q.position_delta(2)) > 0.01 || q.is_in_lava"
|
||||
}
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:drop_egg"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.furniture_hit": {
|
||||
"initial_state": "not_hit",
|
||||
"states": {
|
||||
"not_hit": {
|
||||
"transitions": [
|
||||
{
|
||||
"hit": "query.property('silverlabs_nat:furniture_hit')"
|
||||
},
|
||||
{
|
||||
"destroy": "query.is_shaking"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hit": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:furniture_not_hit",
|
||||
"/playsound dig.stone @a[r=16] ~ ~ ~ 0.75 1.45"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"destroy": "query.is_shaking"
|
||||
},
|
||||
{
|
||||
"not_hit": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"destroy": {}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.furniture_spawn": {
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"just_spawned": "query.property('silverlabs_nat:just_spawned')"
|
||||
}
|
||||
]
|
||||
},
|
||||
"just_spawned": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:finish_spawned",
|
||||
"/playsound dig.stone @a[r=16] ~ ~ ~ 0.75 1.45"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.firefly_jar.furniture_remove": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"egg_drop": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"egg_drop": {
|
||||
"on_entry": [
|
||||
"/loot spawn ~0.5 ~0.5 ~0.5 loot \"sf/nba/items/firefly_jar.loot\""
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"dropped_egg": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dropped_egg": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:remove"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.giraffe.head_collision": {
|
||||
"initial_state": "head_normal",
|
||||
"states": {
|
||||
"head_normal": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:set_head_normal"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"head_low": "q.has_rider && q.rider_head_x_rotation(0) >= 45"
|
||||
}
|
||||
]
|
||||
},
|
||||
"head_low": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:set_head_low"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"head_normal": "!q.has_rider || q.rider_head_x_rotation(0) < 45"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.hamster.riding_check": {
|
||||
"states": {
|
||||
"default": {
|
||||
"on_entry": ["@s silverlabs_nat:remove_move_away"],
|
||||
"transitions": [
|
||||
{ "riding": "q.is_riding" }
|
||||
]
|
||||
},
|
||||
"riding": {
|
||||
"transitions": [
|
||||
{ "unriding": "!q.is_riding" }
|
||||
],
|
||||
"on_exit": ["@s silverlabs_nat:move_away"]
|
||||
},
|
||||
"unriding": {
|
||||
"transitions": [
|
||||
{ "default": "q.state_time >= 3.0" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.hamster.variant_fixer": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{ "variant_0": "q.had_component_group('silverlabs_nat:variant_0')" },
|
||||
{ "variant_1": "q.had_component_group('silverlabs_nat:variant_1')" },
|
||||
{ "variant_2": "q.had_component_group('silverlabs_nat:variant_2')" },
|
||||
{ "variant_3": "q.had_component_group('silverlabs_nat:variant_3')" },
|
||||
{ "variant_4": "q.had_component_group('silverlabs_nat:variant_4')" },
|
||||
{ "variant_5": "q.had_component_group('silverlabs_nat:variant_5')" },
|
||||
{ "variant_6": "q.had_component_group('silverlabs_nat:variant_6')" }
|
||||
]
|
||||
},
|
||||
"variant_0": {
|
||||
"on_entry": ["@s silverlabs_nat:set_variant_black"]
|
||||
},
|
||||
"variant_1": {
|
||||
"on_entry": ["@s silverlabs_nat:set_variant_blackwhite"]
|
||||
},
|
||||
"variant_2": {
|
||||
"on_entry": ["@s silverlabs_nat:set_variant_brown"]
|
||||
},
|
||||
"variant_3": {
|
||||
"on_entry": ["@s silverlabs_nat:set_variant_grey"]
|
||||
},
|
||||
"variant_4": {
|
||||
"on_entry": ["@s silverlabs_nat:set_variant_orange"]
|
||||
},
|
||||
"variant_5": {
|
||||
"on_entry": ["@s silverlabs_nat:set_variant_peach"]
|
||||
},
|
||||
"variant_6": {
|
||||
"on_entry": ["@s silverlabs_nat:set_variant_white"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.hamster_wheel.initialize": {
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"initialize": "!query.property('silverlabs_nat:initialized')"
|
||||
}
|
||||
]
|
||||
},
|
||||
"initialize": {
|
||||
"on_entry": [
|
||||
"/function sf/nba/cardinal_orientation",
|
||||
"@s silverlabs_nat:initialized"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.hedgehog.hit": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"hit": "query.property('silverlabs_nat:hit')"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hit": {
|
||||
"on_entry": [
|
||||
"/scriptevent silverlabs_nat:hedgehog_hit",
|
||||
"@s silverlabs_nat:remove_projectile"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"fallback": "q.state_time > 3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fallback": {
|
||||
"on_entry": [
|
||||
"/scriptevent silverlabs_nat:hedgehog_hit",
|
||||
"@s silverlabs_nat:remove_projectile"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.block_animals.hippo.ram_watcher": {
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{ "ram_attack": "q.is_casting" }
|
||||
]
|
||||
},
|
||||
"ram_attack": {
|
||||
"on_entry": [
|
||||
"/scriptevent silverlabs_nat:hippo_ram_attack",
|
||||
"v.ram_attack_delay = query.life_time + 1.5;"
|
||||
],
|
||||
"transitions": [
|
||||
{ "default": "query.life_time > (v.ram_attack_delay ?? 0)" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.info_book.orient": {
|
||||
"initial_state": "not_oriented",
|
||||
"states": {
|
||||
"not_oriented": {
|
||||
"on_entry": [
|
||||
"/function sf/nba/cardinal_orientation"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"oriented": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"oriented": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:oriented"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.info_book.hit": {
|
||||
"initial_state": "not_hit",
|
||||
"states": {
|
||||
"not_hit": {
|
||||
"transitions": [
|
||||
{
|
||||
"hit": "query.property('silverlabs_nat:hit')"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hit": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:not_hit"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"not_hit": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.kiwi.digging_control": {
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"init": "!q.is_tamed"
|
||||
}
|
||||
]
|
||||
},
|
||||
"init": {
|
||||
"on_entry": [
|
||||
"/event entity @s silverlabs_nat:end_feeling_happy",
|
||||
"/event entity @s silverlabs_nat:scenting"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"test_digging_success": "query.property('silverlabs_nat:digging_states') == 'test_digging_success'"
|
||||
}
|
||||
]
|
||||
},
|
||||
"test_digging_success": {
|
||||
"on_entry": [
|
||||
"/function sf/nba/gameplay/digging/init_digging"
|
||||
],
|
||||
"on_exit": [
|
||||
"v.stand_delay_trigger = query.life_time + 2;"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"digging_successful": "query.property('silverlabs_nat:digging_states') == 'successful'"
|
||||
},
|
||||
{
|
||||
"digging_unsuccessful": "query.property('silverlabs_nat:digging_states') == 'unsuccessful'"
|
||||
}
|
||||
]
|
||||
},
|
||||
"digging_successful": {
|
||||
"on_entry": [
|
||||
"/function sf/nba/gameplay/digging/digging_loot"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"feeling_happy": "query.life_time >= v.stand_delay_trigger"
|
||||
}
|
||||
]
|
||||
},
|
||||
"digging_unsuccessful": {
|
||||
"transitions": [
|
||||
{
|
||||
"default": "query.life_time >= v.stand_delay_trigger"
|
||||
}
|
||||
]
|
||||
},
|
||||
"feeling_happy": {
|
||||
"on_entry": [
|
||||
"/event entity @s silverlabs_nat:feeling_happy"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.kiwi.sniffing_control": {
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"start_sniffing": "!q.is_tamed && (query.property('silverlabs_nat:digging_states') == 'trigger_digging')"
|
||||
}
|
||||
]
|
||||
},
|
||||
"start_sniffing": {
|
||||
"on_entry": [
|
||||
"/function sf/nba/gameplay/digging/init_sniffing"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "query.property('silverlabs_nat:digging_states') != 'trigger_digging'"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.kiwi.pickup_control": {
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"start_sniffing": "q.is_tamed && !q.is_item_name_any('slot.weapon.mainhand', 'minecraft:wheat')"
|
||||
}
|
||||
]
|
||||
},
|
||||
"start_sniffing": {
|
||||
"on_entry": [
|
||||
"/replaceitem entity @s slot.weapon.mainhand 0 wheat"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"start_sniffing": "!q.is_tamed || q.is_item_name_any('slot.weapon.mainhand', 'minecraft:wheat')"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.kiwi.following_owner_check": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:not_following_owner"
|
||||
],
|
||||
"transitions": [
|
||||
{ "following": "!q.is_sitting && q.is_tamed" }
|
||||
]
|
||||
},
|
||||
"following": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:is_following_owner"
|
||||
],
|
||||
"transitions": [
|
||||
{ "default": "q.is_sitting || !q.is_tamed" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.reptile_tail.flop": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"flop": "q.is_on_ground && !q.is_in_water"
|
||||
}
|
||||
]
|
||||
},
|
||||
"flop": {
|
||||
"on_entry": [
|
||||
"/scriptevent silverlabs_nat:reptile_tail_flop"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "!q.is_on_ground || q.is_in_water"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.mole.trail": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"state_check": "(math.abs(q.position_delta(0)) > 0.01|| math.abs(q.position_delta(2)) > 0.01) && query.property('silverlabs_nat:mole_state') != 'unrolled'"
|
||||
},
|
||||
{
|
||||
"state_check_baby": "(math.abs(q.position_delta(0)) > 0.01|| math.abs(q.position_delta(2)) > 0.01) && query.property('silverlabs_nat:mole_state') != 'unrolled' && q.is_baby"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state_check": {
|
||||
"on_entry": [
|
||||
"/execute positioned ~~~ unless entity @e[type=silverlabs_nat:dirt_trail,r=0.5] run summon silverlabs_nat:dirt_trail ~ ~ ~"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "(1.0)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state_check_baby": {
|
||||
"on_entry": [
|
||||
"/execute positioned ~~~ unless entity @e[type=silverlabs_nat:dirt_trail,r=0.5] run summon silverlabs_nat:dirt_trail ~ ~ ~ ~ ~ silverlabs_nat:baby"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "(1.0)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.ostrich.egg_check": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"on_entry": [
|
||||
"v.cooldown_timer = 0;"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"remove": "query.property('silverlabs_nat:egg_protector') && q.has_rider"
|
||||
},
|
||||
{
|
||||
"cooldown": "query.property('silverlabs_nat:egg_protector') && !q.has_target"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cooldown": {
|
||||
"on_entry": [
|
||||
"v.cooldown_timer = query.life_time;"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"remove": "q.has_rider"
|
||||
},
|
||||
{
|
||||
"default": "!query.property('silverlabs_nat:egg_protector') || q.has_target"
|
||||
},
|
||||
{
|
||||
"egg_check": "query.life_time >= (v.cooldown_timer + 6.0)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"egg_check": {
|
||||
"on_entry": [
|
||||
"/execute unless entity @e[type=silverlabs_nat:ostrich_egg,c=1,r=24] run event entity @s silverlabs_nat:remove_egg_protector"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"remove": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:remove_egg_protector"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.ostrich.flap": {
|
||||
"initial_state": "grounded",
|
||||
"states": {
|
||||
"grounded": {
|
||||
"transitions": [
|
||||
{
|
||||
"flapping": "query.property('silverlabs_nat:has_rider') && !q.is_on_ground"
|
||||
}
|
||||
]
|
||||
},
|
||||
"flapping": {
|
||||
"on_entry": [
|
||||
"/effect @s slow_falling 2 0 true"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"grounded": "q.is_on_ground || !query.property('silverlabs_nat:has_rider')"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.ostrich_egg.on_hatch": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"on_hatch": "q.is_transforming"
|
||||
}
|
||||
]
|
||||
},
|
||||
"on_hatch": {
|
||||
"on_entry": [
|
||||
"/event entity @e[c=1,type=silverlabs_nat:ostrich,r=12] silverlabs_nat:remove_egg_protector"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.otter.eat": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"on_entry": [
|
||||
"v.eat_step = 0;",
|
||||
"v.float_delay = query.life_time + 1;",
|
||||
"@s silverlabs_nat:not_floating"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"float": "(query.life_time > (v.float_delay ?? 0)) && q.is_item_equipped(0) && q.is_in_water && (math.random_integer(1,100) == 100)"
|
||||
},
|
||||
{
|
||||
"give_to_player": "(query.life_time > (v.float_delay ?? 0)) && q.is_item_equipped(0) && (math.random_integer(1,100) == 100)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"float": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:is_floating",
|
||||
"v.eat_delay = query.life_time + math.random(2,5);"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "!q.is_item_equipped(0) || !q.is_in_water || (q.modified_move_speed >= 0.075)"
|
||||
},
|
||||
{
|
||||
"eat": "(query.life_time > (v.eat_delay ?? 0))"
|
||||
}
|
||||
]
|
||||
},
|
||||
"eat": {
|
||||
"on_entry": [
|
||||
"v.eat_step = (v.eat_step ?? 0) + 1;",
|
||||
"v.eat_delay = query.life_time + 0.15;"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"munch": "((v.eat_step ?? 0) < 3) && (query.life_time > (v.eat_delay ?? 0))"
|
||||
},
|
||||
{
|
||||
"digest": "(query.life_time > (v.eat_delay ?? 0))"
|
||||
}
|
||||
]
|
||||
},
|
||||
"munch": {
|
||||
"on_entry": ["/playsound mob.fox.eat @a ~ ~ ~"],
|
||||
"transitions": [
|
||||
{
|
||||
"eat": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"digest": {
|
||||
"on_entry": [
|
||||
"/playsound mob.fox.eat @a ~ ~ ~ 1 1.2",
|
||||
"/replaceitem entity @s slot.inventory 0 air",
|
||||
"/replaceitem entity @s slot.weapon.mainhand 0 air",
|
||||
"@s silverlabs_nat:otter_on_finish_eat",
|
||||
"@s silverlabs_nat:not_floating"
|
||||
],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"give_to_player": {
|
||||
"on_entry": ["@s silverlabs_nat:give_item_to_player"],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "!q.is_item_equipped(0)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"controller.animation.silverlabs_nat.otter.item_check": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"transitions": [
|
||||
{
|
||||
"test": "q.is_item_equipped(0)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"on_entry": ["/scriptevent silverlabs_nat:otter_check"],
|
||||
"transitions": [
|
||||
{
|
||||
"default": "!q.is_item_equipped(0)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"animation_controllers": {
|
||||
"controller.animation.silverlabs_nat.piranha.attack": {
|
||||
"initial_state": "default",
|
||||
"states": {
|
||||
"default": {
|
||||
"on_entry": [
|
||||
"v.attack_start = 0.0;"
|
||||
],
|
||||
"transitions": [
|
||||
{ "attack": "q.is_delayed_attacking" }
|
||||
]
|
||||
},
|
||||
"attack": {
|
||||
"on_entry": [
|
||||
"v.attack_start = q.life_time;"
|
||||
],
|
||||
"transitions": [
|
||||
{ "apply_damage": "q.life_time >= v.attack_start + 0.3" }
|
||||
]
|
||||
},
|
||||
"apply_damage": {
|
||||
"on_entry": [
|
||||
"/damage @e[family=!silverlabs_nat:piranha, r=2, c=1] 1 none",
|
||||
"v.despawn_time = q.life_time;"
|
||||
],
|
||||
"transitions": [
|
||||
{ "despawn": "q.life_time >= v.despawn_time + 0.25" }
|
||||
]
|
||||
},
|
||||
"despawn": {
|
||||
"on_entry": [
|
||||
"@s silverlabs_nat:despawn"
|
||||
],
|
||||
"transitions": [
|
||||
{ "default": "1" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||