Compare commits

...

3 Commits

Author SHA1 Message Date
f7aa71e9eb feat(postal): add postal service addon and bundle pending addon work
All checks were successful
Deploy Addons / deploy (push) Successful in 16s
- New postal-service-addon: per-player mailboxes + post-office send block
  (ActionForm recipient picker, offline notification queue, chunk-load
  retry via tickingarea)
- Commit previously untracked private-chest, home-sign, keep-inventory
  addons and their docker-compose mounts
- Deploy workflow: add postal + previously unwired addons to path filter
  and checkout list; drop easter-egg from deployment
- enabled_packs.json: register postal UUIDs for Lyla + Mya

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:07:39 +01:00
cc18066c17 fix(lobby,easter-egg): update portal coordinates and egg-check ring
Lobby portal zones and signs moved to the rebuilt portal area at
~(436..488, 65..74, -322..-281), including a second Lyla's-World
"Super Kitties" portal. Signs now use per-portal facing_direction
so they face the hub walkway correctly.

Easter-egg placeAllEggs validation now samples the far-ring eggs
(indices 45-49) before falling back to the near ring — avoids false
"blocks missing" reports when only close-ring eggs have been picked up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:19:33 +01:00
c176263ec5 feat(monkey): add cheeky banana-throwing monkey addon
New silverlabs:cheeky_monkey mob that wanders the lobby watching players
and throwing harmless (knockback-only) silverlabs:banana_projectile at
them. Drops 1-2 silverlabs:banana (food) on death. Spawn rules target
jungle biomes for future deployment to survival worlds; for now the pack
is bind-mounted into the lobby service only.

Also bundles a stray docker-compose tidy from earlier local work
(Jamie's world seed pinned, pet addons dropped from Jamie).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:19:18 +01:00
64 changed files with 2629 additions and 34 deletions

View File

@@ -7,8 +7,12 @@ on:
- 'addon/**'
- 'lobby-addon/**'
- 'hub-return-addon/**'
- 'easter-egg-addon/**'
- 'village-evolution-addon/**'
- 'monkey-addon/**'
- 'private-chest-addon/**'
- 'home-sign-addon/**'
- 'keep-inventory-addon/**'
- 'postal-service-addon/**'
- 'docker-compose.yml'
- 'scripts/**'
@@ -26,6 +30,7 @@ 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/ docker-compose.yml"
# First run: clone. Subsequent: pull.
if [ ! -d "$APP_DIR/.git" ]; then
@@ -33,11 +38,11 @@ jobs:
git init
git remote add origin https://git.silverlabs.uk/SilverLABS/minecraft-aiworld.git
git fetch origin main
git checkout -f origin/main -- addon/ lobby-addon/ hub-return-addon/ easter-egg-addon/ village-evolution-addon/ docker-compose.yml
git checkout -f origin/main -- $PATHS
else
cd "$APP_DIR"
git fetch origin main
git checkout -f origin/main -- addon/ lobby-addon/ hub-return-addon/ easter-egg-addon/ village-evolution-addon/ docker-compose.yml
git checkout -f origin/main -- $PATHS
fi
# Recreate containers so any new docker-compose volume mounts are applied,

View File

@@ -3,7 +3,7 @@
"minecraft:entity": {
"description": {
"identifier": "silverlabs:anthrax_cat",
"is_spawnable": false,
"is_spawnable": true,
"is_summonable": true,
"is_experimental": false
},
@@ -39,18 +39,6 @@
"minecraft:knockback_resistance": {
"value": 1.0
},
"minecraft:damage_sensor": {
"triggers": [
{
"cause": "all",
"deals_damage": false
},
{
"cause": "override",
"deals_damage": true
}
]
},
"minecraft:health": {
"value": 20,
"max": 20

View File

@@ -26,6 +26,15 @@ services:
- ./addon/heyhe_pet_RP:/data/resource_packs/heyhe_pet_RP
- ./addon/anthrax_cat_BP:/data/behavior_packs/anthrax_cat_BP
- ./addon/anthrax_cat_RP:/data/resource_packs/anthrax_cat_RP
- ./monkey-addon/monkey_BP:/data/behavior_packs/monkey_BP
- ./monkey-addon/monkey_RP:/data/resource_packs/monkey_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
- ./home-sign-addon/home_sign_BP:/data/behavior_packs/home_sign_BP
- ./home-sign-addon/home_sign_RP:/data/resource_packs/home_sign_RP
- ./keep-inventory-addon/keep_inventory_BP:/data/behavior_packs/keep_inventory_BP
- ./postal-service-addon/postal_service_BP:/data/behavior_packs/postal_service_BP
- ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP
restart: unless-stopped
networks:
- mc-network
@@ -42,6 +51,7 @@ services:
ONLINE_MODE: "false"
SERVER_PORT: "19132"
LEVEL_NAME: "Jamie World"
LEVEL_SEED: "-6717666844935858147"
MAX_PLAYERS: "10"
DEFAULT_PLAYER_PERMISSION_LEVEL: operator
OP_PERMISSION_LEVEL: "4"
@@ -50,14 +60,13 @@ services:
volumes:
- jamie-data:/data
- ./hub-return-addon/hub_return_transfer_BP:/data/behavior_packs/hub_return_transfer_BP
- ./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
- ./addon/heyhe_pet_RP:/data/resource_packs/heyhe_pet_RP
- ./addon/anthrax_cat_BP:/data/behavior_packs/anthrax_cat_BP
- ./addon/anthrax_cat_RP:/data/resource_packs/anthrax_cat_RP
- ./village-evolution-addon/village_evolution_BP:/data/behavior_packs/village_evolution_BP
- ./village-evolution-addon/enabled_packs.json:/data/config/default/enabled_packs.json
- ./private-chest-addon/private_chest_BP:/data/behavior_packs/private_chest_BP
- ./private-chest-addon/private_chest_RP:/data/resource_packs/private_chest_RP
- ./home-sign-addon/home_sign_BP:/data/behavior_packs/home_sign_BP
- ./home-sign-addon/home_sign_RP:/data/resource_packs/home_sign_RP
- ./keep-inventory-addon/keep_inventory_BP:/data/behavior_packs/keep_inventory_BP
- ./postal-service-addon/postal_service_BP:/data/behavior_packs/postal_service_BP
- ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP
restart: unless-stopped
networks:
- mc-network
@@ -89,6 +98,13 @@ services:
- ./addon/anthrax_cat_BP:/data/behavior_packs/anthrax_cat_BP
- ./addon/anthrax_cat_RP:/data/resource_packs/anthrax_cat_RP
- ./village-evolution-addon/village_evolution_BP:/data/behavior_packs/village_evolution_BP
- ./private-chest-addon/private_chest_BP:/data/behavior_packs/private_chest_BP
- ./private-chest-addon/private_chest_RP:/data/resource_packs/private_chest_RP
- ./home-sign-addon/home_sign_BP:/data/behavior_packs/home_sign_BP
- ./home-sign-addon/home_sign_RP:/data/resource_packs/home_sign_RP
- ./keep-inventory-addon/keep_inventory_BP:/data/behavior_packs/keep_inventory_BP
- ./postal-service-addon/postal_service_BP:/data/behavior_packs/postal_service_BP
- ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP
- ./village-evolution-addon/enabled_packs.json:/data/config/default/enabled_packs.json
restart: unless-stopped
networks:
@@ -121,6 +137,13 @@ services:
- ./addon/anthrax_cat_BP:/data/behavior_packs/anthrax_cat_BP
- ./addon/anthrax_cat_RP:/data/resource_packs/anthrax_cat_RP
- ./village-evolution-addon/village_evolution_BP:/data/behavior_packs/village_evolution_BP
- ./private-chest-addon/private_chest_BP:/data/behavior_packs/private_chest_BP
- ./private-chest-addon/private_chest_RP:/data/resource_packs/private_chest_RP
- ./home-sign-addon/home_sign_BP:/data/behavior_packs/home_sign_BP
- ./home-sign-addon/home_sign_RP:/data/resource_packs/home_sign_RP
- ./keep-inventory-addon/keep_inventory_BP:/data/behavior_packs/keep_inventory_BP
- ./postal-service-addon/postal_service_BP:/data/behavior_packs/postal_service_BP
- ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP
- ./village-evolution-addon/enabled_packs.json:/data/config/default/enabled_packs.json
restart: unless-stopped
networks:

View File

@@ -240,8 +240,14 @@ function placeAllEggs(playerPos) {
// Validate at least one stored egg block actually exists
const stored = getEggPositions();
if (stored && stored.length > 0) {
// Check far-ring eggs (last 5) — they're hardest to reach and least likely
// to have been collected yet. Avoids false "blocks missing" detection when
// only the close-ring eggs (1-5) have been collected.
let found = 0;
for (let i = 0; i < Math.min(5, stored.length); i++) {
const checkAt = [45, 46, 47, 48, 49].filter(i => i < stored.length);
const fallback = checkAt.length === 0;
const indices = fallback ? [0, 1, 2, 3, 4].filter(i => i < stored.length) : checkAt;
for (const i of indices) {
try {
const block = overworld.getBlock({ x: stored[i].x, y: stored[i].y, z: stored[i].z });
if (block && block.typeId.includes("glazed_terracotta")) found++;

View File

@@ -0,0 +1,27 @@
{
"format_version": "1.21.0",
"minecraft:block": {
"description": {
"identifier": "silverlabs:home_sign",
"menu_category": {
"category": "items",
"group": "itemGroup.name.decorations"
}
},
"components": {
"minecraft:destructible_by_mining": {
"seconds_to_destroy": 1.5
},
"minecraft:destructible_by_explosion": {
"explosion_resistance": 200.0
},
"minecraft:map_color": "#C0703A",
"minecraft:material_instances": {
"*": {
"texture": "home_sign",
"render_method": "opaque"
}
}
}
}
}

View File

@@ -0,0 +1,701 @@
{
"format_version": "1.26.10",
"minecraft:entity": {
"description": {
"identifier": "minecraft:cat",
"spawn_category": "creature",
"is_spawnable": true,
"is_summonable": true,
"properties": {
"minecraft:sound_variant": {
"type": "enum",
"values": ["default", "royal"],
"default": "default",
"client_sync": true
}
}
},
"component_groups": {
"minecraft:cat_baby": {
"minecraft:is_baby": {},
"minecraft:scale": {
"value": 0.4
},
"minecraft:ageable": {
"duration": 1200,
"feed_items": ["fish", "salmon"],
"pause_growth_items": [ "golden_dandelion" ],
"reset_growth_items": [ "golden_dandelion" ],
"grow_up": {
"event": "minecraft:ageable_grow_up",
"target": "self"
}
}
},
"minecraft:cat_adult": {
"minecraft:experience_reward": {
"on_bred": "Math.Random(1,7)",
"on_death": "query.last_hit_by_player ? Math.Random(1,3) : 0"
},
"minecraft:loot": {
"table": "loot_tables/entities/cat.json"
},
"minecraft:scale": {
"value": 0.8
},
"minecraft:leashable_to": {},
"minecraft:breedable": {
"require_tame": true,
"require_full_health": true,
"allow_sitting": true,
"breeds_with": {
"minecraft:cat": {}
},
"breed_items": ["fish", "salmon"]
},
"minecraft:behavior.breed": {
"priority": 3,
"speed_multiplier": 1.0
}
},
"minecraft:cat_wild": {
"minecraft:health": {
"value": 10,
"max": 10
},
"minecraft:tameable": {
"probability": 0.33,
"tame_items": ["fish", "salmon"],
"tame_event": {
"event": "minecraft:on_tame",
"target": "self"
}
},
"minecraft:rideable": {
"seat_count": 1,
"family_types": ["baby_undead"],
"seats": {
// This value results in zombies floating when riding baby cats,
// but switching to a different setup would break pre-existing mobs.
"position": [0.0, 0.35, 0.0]
}
},
"minecraft:behavior.nearest_attackable_target": {
"priority": 1,
"reselect_targets": true,
"within_radius": 16.0,
"entity_types": [
{
"filters": {
"test": "is_family",
"subject": "other",
"value": "rabbit"
},
"max_dist": 8
},
{
"filters": {
"all_of": [
{
"test": "is_family",
"subject": "other",
"value": "baby_turtle"
},
{
"test": "in_water",
"subject": "other",
"operator": "!=",
"value": true
}
]
},
"max_dist": 8
}
]
},
"minecraft:behavior.tempt": {
"priority": 5,
"speed_multiplier": 0.5,
"within_radius": 16,
"can_get_scared": true,
"tempt_sound": "tempt",
"sound_interval": [0, 100],
"items": ["fish", "salmon"]
},
"minecraft:behavior.avoid_mob_type": {
"priority": 6,
"entity_types": [
{
"filters": {
"test": "is_family",
"subject": "other",
"value": "player"
},
"max_dist": 10,
"walk_speed_multiplier": 0.8,
"sprint_speed_multiplier": 1.33
}
]
},
"minecraft:behavior.move_towards_dwelling_restriction": {
"priority": 7
}
},
"minecraft:cat_tame": {
"minecraft:is_tamed": {},
"minecraft:health": {
"value": 20,
"max": 20
},
"minecraft:color": {
"value": 14
},
"minecraft:sittable": {},
"minecraft:is_dyeable": {
"interact_text": "action.interact.dye"
},
"minecraft:on_wake_with_owner": {
"event": "minecraft:pet_slept_with_owner",
"target": "self"
},
"minecraft:behavior.teleport_to_owner": {
"priority": 0,
"filters": {
"all_of": [
{ "test": "owner_distance", "operator": ">", "value": 12 },
{ "test": "is_panicking" }
]
}
},
"minecraft:behavior.pet_sleep_with_owner": {
"priority": 2,
"speed_multiplier": 1.2,
"search_radius": 10,
"search_height": 10,
"goal_radius": 1.0
},
"minecraft:behavior.stay_while_sitting": {
"priority": 3
},
"minecraft:behavior.tempt": {
"priority": 5,
"speed_multiplier": 0.5,
"within_radius": 16,
"items": ["fish", "salmon"]
},
"minecraft:behavior.ocelot_sit_on_block": {
"priority": 7,
"speed_multiplier": 1.0
}
},
"minecraft:cat_gift_for_owner": {
"minecraft:behavior.drop_item_for": {
"priority": 1,
"seconds_before_pickup": 0.0,
"cooldown": 0.25,
"drop_item_chance": 0.7,
"offering_distance": 5.0,
"minimum_teleport_distance": 2.0,
"max_head_look_at_height": 10.0,
"target_range": [5.0, 5.0, 5.0],
"teleport_offset": [0.0, 1.0, 0.0],
"time_of_day_range": {
"min": 0.74999,
"max": 0.8
},
"speed_multiplier": 1.0,
"search_range": 5,
"search_height": 2,
"search_count": 0,
"goal_radius": 1.0,
"entity_types": [
{
"filters": {
"test": "is_family",
"subject": "other",
"value": "player"
},
"max_dist": 6
}
],
"loot_table": "loot_tables/entities/cat_gift.json",
"on_drop_attempt": {
"event": "minecraft:cat_gifted_owner",
"target": "self"
}
}
},
"minecraft:cat_white": {
"minecraft:variant": {
"value": 0
}
},
"minecraft:cat_tuxedo": {
"minecraft:variant": {
"value": 1
}
},
"minecraft:cat_red": {
"minecraft:variant": {
"value": 2
}
},
"minecraft:cat_siamese": {
"minecraft:variant": {
"value": 3
}
},
"minecraft:cat_british": {
"minecraft:variant": {
"value": 4
}
},
"minecraft:cat_calico": {
"minecraft:variant": {
"value": 5
}
},
"minecraft:cat_persian": {
"minecraft:variant": {
"value": 6
}
},
"minecraft:cat_ragdoll": {
"minecraft:variant": {
"value": 7
}
},
"minecraft:cat_tabby": {
"minecraft:variant": {
"value": 8
}
},
"minecraft:cat_black": {
"minecraft:variant": {
"value": 9
}
},
"minecraft:cat_jellie": {
"minecraft:variant": {
"value": 10
}
}
},
"components": {
"minecraft:ambient_sound_interval": {
"value": 120,
"range": 60,
"event_name": "ambient"
},
"minecraft:offspring": {
"offspring_pairs": {
"minecraft:cat": "minecraft:cat"
},
"combine_parent_colors": true
},
"minecraft:spawn_egg_interaction": {},
"minecraft:leashable": {},
"minecraft:balloonable": {
"mass": 0.6
},
"minecraft:is_hidden_when_invisible": {},
"minecraft:attack_damage": {
"value": 4
},
"minecraft:nameable": {},
"minecraft:type_family": {
"family": ["cat", "mob"]
},
"minecraft:breathable": {
"total_supply": 15,
"suffocate_time": 0
},
"minecraft:collision_box": {
"width": 0.6,
"height": 0.7
},
"minecraft:healable": {
"items": [
{
"item": "fish",
"heal_amount": 2
},
{
"item": "salmon",
"heal_amount": 2
}
]
},
"minecraft:hurt_on_condition": {
"damage_conditions": [
{
"filters": {
"test": "in_lava",
"subject": "self",
"operator": "==",
"value": true
},
"cause": "lava",
"damage_per_tick": 4
}
]
},
"minecraft:movement": {
"value": 0.3
},
"minecraft:navigation.walk": {
"can_float": true,
"avoid_water": true,
"avoid_damage_blocks": true
},
"minecraft:movement.basic": {},
"minecraft:jump.static": {},
"minecraft:can_climb": {},
"minecraft:damage_sensor": {
"triggers": {
"cause": "fall",
"deals_damage": "no"
}
},
"minecraft:dweller": {
"dwelling_type": "village",
"dweller_role": "passive",
"update_interval_base": 60,
"update_interval_variant": 40,
"can_find_poi": false,
"can_migrate": true,
"first_founding_reward": 0
},
"minecraft:despawn": {
"despawn_from_distance": {}
},
"minecraft:physics": {},
"minecraft:pushable_by_entity": {
},
"minecraft:pushable_by_block": {
},
"minecraft:conditional_bandwidth_optimization": {},
"minecraft:behavior.float": {
"priority": 0
},
"minecraft:behavior.panic": {
"priority": 1,
"speed_multiplier": 1.25
},
"minecraft:behavior.mount_pathing": {
"priority": 1,
"speed_multiplier": 1.25,
"target_dist": 0,
"track_target": true
},
"minecraft:behavior.leap_at_target": {
"priority": 3,
"target_dist": 0.3
},
"minecraft:behavior.ocelotattack": {
"priority": 4,
"cooldown_time": 1.0,
"x_max_rotation": 30.0,
"y_max_head_rotation": 30.0,
"max_distance": 15.0,
"max_sneak_range": 15.0,
"max_sprint_range": 4.0,
"reach_multiplier": 2.0,
"sneak_speed_multiplier": 0.6,
"sprint_speed_multiplier": 1.33,
"walk_speed_multiplier": 0.8
},
"minecraft:behavior.random_stroll": {
"priority": 8,
"speed_multiplier": 0.8
},
"minecraft:behavior.look_at_player": {
"priority": 9
}
},
"events": {
"minecraft:entity_spawned": {
"sequence": [
{
"randomize": [
{
"weight": 3,
"remove": {},
"add": {
"component_groups": [
"minecraft:cat_adult",
"minecraft:cat_wild"
]
}
},
{
"weight": 1,
"remove": {},
"add": {
"component_groups": [
"minecraft:cat_baby",
"minecraft:cat_wild"
]
}
}
]
},
{
"randomize": [
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_white"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_tuxedo"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_red"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_siamese"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_british"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_calico"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_persian"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_ragdoll"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_tabby"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_black"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_jellie"]
}
}
]
},
{
"trigger": "minecraft:randomize_sound_variant"
}
]
},
"minecraft:spawn_from_village": {
"sequence": [
{
"randomize": [
{
"weight": 3,
"trigger": "minecraft:spawn_wild_adult"
},
{
"weight": 1,
"trigger": "minecraft:spawn_wild_baby"
}
]
},
{
"randomize": [
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_tuxedo"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_red"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_siamese"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_white"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_british"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_calico"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_persian"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_ragdoll"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_tabby"]
}
},
{
"weight": 15,
"add": {
"component_groups": ["minecraft:cat_jellie"]
}
}
]
}
]
},
"minecraft:spawn_midnight_cat": {
"sequence": [
{
"trigger": "minecraft:spawn_wild_adult",
"add": {
"component_groups": ["minecraft:cat_black"]
}
}
]
},
"minecraft:entity_born": {
"sequence": [
{
"filters": {
"test": "is_tamed"
},
"trigger": "minecraft:spawn_tame_baby"
},
{
"filters": {
"test": "is_tamed",
"value": false
},
"trigger": "minecraft:spawn_wild_baby"
}
]
},
"minecraft:spawn_wild_baby": {
"add": {
"component_groups": ["minecraft:cat_baby", "minecraft:cat_wild"]
}
},
"minecraft:spawn_wild_adult": {
"add": {
"component_groups": ["minecraft:cat_adult", "minecraft:cat_wild"]
},
"trigger": "minecraft:randomize_sound_variant"
},
"minecraft:spawn_tame_baby": {
"add": {
"component_groups": ["minecraft:cat_baby", "minecraft:cat_tame"]
}
},
"minecraft:spawn_tame_adult": {
"add": {
"component_groups": ["minecraft:cat_adult", "minecraft:cat_tame"]
},
"trigger": "minecraft:randomize_sound_variant"
},
"minecraft:ageable_grow_up": {
"remove": {
"component_groups": ["minecraft:cat_baby"]
},
"add": {
"component_groups": ["minecraft:cat_adult"]
},
"trigger": "minecraft:randomize_sound_variant"
},
"minecraft:on_tame": {
"sequence": [
{
"remove": {
"component_groups": ["minecraft:cat_wild"]
}
},
{
"add": {
"component_groups": ["minecraft:cat_tame"]
}
}
]
},
"minecraft:pet_slept_with_owner": {
"add": {
"component_groups": ["minecraft:cat_gift_for_owner"]
}
},
"minecraft:cat_gifted_owner": {
"remove": {
"component_groups": ["minecraft:cat_gift_for_owner"]
}
},
"minecraft:randomize_sound_variant": {
"randomize": [
{
"weight": 1,
"set_property": {
"minecraft:sound_variant": "default"
}
},
{
"weight": 1,
"set_property": {
"minecraft:sound_variant": "royal"
}
}
]
}
}
}
}

View File

@@ -0,0 +1,34 @@
{
"format_version": 2,
"header": {
"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]
},
"modules": [
{
"type": "data",
"uuid": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b81",
"version": [1, 0, 0]
},
{
"type": "script",
"language": "javascript",
"uuid": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b82",
"version": [1, 0, 0],
"entry": "scripts/main.js"
}
],
"dependencies": [
{
"module_name": "@minecraft/server",
"version": "1.17.0"
},
{
"uuid": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b83",
"version": [1, 0, 1]
}
]
}

View File

@@ -0,0 +1,18 @@
{
"format_version": "1.21.0",
"minecraft:recipe_shapeless": {
"description": {
"identifier": "silverlabs:home_sign_recipe"
},
"tags": ["crafting_table"],
"unlock": { "context": "AlwaysUnlocked" },
"ingredients": [
{ "item": "minecraft:oak_sign" },
{ "item": "minecraft:red_dye" }
],
"result": {
"item": "silverlabs:home_sign",
"count": 1
}
}
}

View File

@@ -0,0 +1,191 @@
import { world, system, ItemStack } from "@minecraft/server";
// ─── Constants ──────────────────────────────────────────────
const HOME_BLOCK = "silverlabs:home_sign";
const PROP_KEY = "home_signs_v1";
const HOME_RADIUS = 32; // user-facing "home area" size
const STRAY_RADIUS = 16; // cat is nudged back beyond this — keeps cats actually at home, not just "within shouting distance"
const RETURN_RADIUS_MIN = 2;
const RETURN_RADIUS_MAX = 10;
const TICK_INTERVAL = 40; // 2 s
// ─── State ──────────────────────────────────────────────────
// In-memory mirror: { "x,y,z,dim": { ownerId, ownerName } }
let homes = {};
function loadState() {
try {
const raw = world.getDynamicProperty(PROP_KEY);
if (raw && typeof raw === "string") {
homes = JSON.parse(raw);
}
} catch (e) {
world.sendMessage(`§c[Home Sweet Home] Failed to load state: ${e.message}`);
homes = {};
}
}
function saveState() {
try {
world.setDynamicProperty(PROP_KEY, JSON.stringify(homes));
} catch (e) {
world.sendMessage(`§c[Home Sweet Home] Failed to save state: ${e.message}`);
}
}
function keyOf(loc, dimensionId) {
return `${Math.floor(loc.x)},${Math.floor(loc.y)},${Math.floor(loc.z)},${dimensionId}`;
}
function claim(loc, dimensionId, player) {
homes[keyOf(loc, dimensionId)] = { ownerId: player.id, ownerName: player.name };
saveState();
}
function release(loc, dimensionId) {
delete homes[keyOf(loc, dimensionId)];
saveState();
}
function getOwner(loc, dimensionId) {
return homes[keyOf(loc, dimensionId)] || null;
}
// ─── Placement: claim home zone ────────────────────────────
world.afterEvents.playerPlaceBlock.subscribe((event) => {
const block = event.block;
if (block.typeId !== HOME_BLOCK) return;
const player = event.player;
claim(block.location, block.dimension.id, player);
player.sendMessage(
`§a[Home Sweet Home] §7Home zone set — tamed cats will stay within ${HOME_RADIUS} blocks.`
);
});
// ─── Break: owner-only ─────────────────────────────────────
try {
world.beforeEvents.playerBreakBlock.subscribe((event) => {
const block = event.block;
if (block.typeId !== HOME_BLOCK) return;
const owner = getOwner(block.location, block.dimension.id);
if (!owner) return; // unclaimed — let it break
const player = event.player;
if (owner.ownerId !== player.id) {
event.cancel = true;
system.run(() =>
player.sendMessage(
`§c[Home Sweet Home] §7This sign belongs to §f${owner.ownerName}§7. You can't break it.`
)
);
return;
}
// Owner break — drop the item back, clear the registry, remove the block.
event.cancel = true;
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
const dim = block.dimension;
system.run(() => {
try {
const dropPos = { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 };
dim.spawnItem(new ItemStack(HOME_BLOCK, 1), dropPos);
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
} catch (e) {
player.sendMessage(`§c[Home Sweet Home] Error during break: ${e.message}`);
}
release(loc, dim.id);
});
});
} catch (e) {
console.warn(`[Home Sweet Home] beforeEvents.playerBreakBlock unavailable: ${e}`);
}
// ─── Cat homing: nudge stray tamed cats back to the nearest home ──
function homesByDimension() {
const out = {};
for (const key in homes) {
const [xs, ys, zs, dim] = key.split(",");
if (!out[dim]) out[dim] = [];
out[dim].push({
x: parseInt(xs, 10) + 0.5,
y: parseInt(ys, 10) + 0.5,
z: parseInt(zs, 10) + 0.5,
ownerName: homes[key].ownerName,
});
}
return out;
}
function nearestHome(catLoc, homeList) {
let best = null;
let bestD2 = Infinity;
for (const h of homeList) {
const dx = catLoc.x - h.x;
const dy = catLoc.y - h.y;
const dz = catLoc.z - h.z;
const d2 = dx * dx + dy * dy + dz * dz;
if (d2 < bestD2) {
bestD2 = d2;
best = h;
}
}
return { home: best, distance: Math.sqrt(bestD2) };
}
function pickReturnSpot(home) {
const angle = Math.random() * Math.PI * 2;
const r = RETURN_RADIUS_MIN + Math.random() * (RETURN_RADIUS_MAX - RETURN_RADIUS_MIN);
return {
x: home.x + Math.cos(angle) * r,
y: home.y,
z: home.z + Math.sin(angle) * r,
};
}
system.runInterval(() => {
const byDim = homesByDimension();
const dimIds = Object.keys(byDim);
if (dimIds.length === 0) return;
for (const dimId of dimIds) {
let dim;
try {
dim = world.getDimension(dimId);
} catch (e) {
continue;
}
let cats;
try {
// Tamed cats only — vanilla cats gain the "tamed" family on tame.
cats = dim.getEntities({ type: "minecraft:cat", families: ["tamed"] });
} catch (e) {
continue;
}
for (const cat of cats) {
try {
const { home, distance } = nearestHome(cat.location, byDim[dimId]);
if (!home || distance <= STRAY_RADIUS) continue;
const target = pickReturnSpot(home);
const ok = cat.tryTeleport(target, { checkForBlocks: true });
if (!ok) {
// Retry tighter
cat.tryTeleport(
{ x: home.x, y: home.y, z: home.z },
{ checkForBlocks: false }
);
}
} catch (e) {
// Skip bad cat, continue with next
}
}
}
}, TICK_INTERVAL);
// ─── Boot ──────────────────────────────────────────────────
system.run(() => {
loadState();
world.sendMessage("§a[Home Sweet Home] §7Loaded — place a sign to claim a 32-block home zone.");
});

View File

@@ -0,0 +1,4 @@
{
"format_version": [1, 1, 0],
"silverlabs:home_sign": { "sound": "wood" }
}

View File

@@ -0,0 +1,17 @@
{
"format_version": 2,
"header": {
"name": "Home Sweet Home Resources",
"description": "Texture and lang for the silverlabs:home_sign block",
"uuid": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b83",
"version": [1, 0, 0],
"min_engine_version": [1, 21, 0]
},
"modules": [
{
"type": "resources",
"uuid": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b84",
"version": [1, 0, 1]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

View File

@@ -0,0 +1 @@
tile.silverlabs:home_sign.name=Home Sweet Home

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

View File

@@ -0,0 +1,11 @@
{
"resource_pack_name": "home_sign_RP",
"texture_name": "atlas.terrain",
"padding": 8,
"num_mip_levels": 4,
"texture_data": {
"home_sign": {
"textures": "textures/blocks/home_sign"
}
}
}

View File

@@ -0,0 +1,30 @@
{
"format_version": 2,
"header": {
"name": "Keep Inventory",
"description": "Asserts gamerule keepInventory true on world load (Lyla's world)",
"uuid": "b4d72e91-8c43-4f1a-95d2-7e3b6c8a0d10",
"version": [1, 0, 0],
"min_engine_version": [1, 21, 0]
},
"modules": [
{
"type": "data",
"uuid": "b4d72e91-8c43-4f1a-95d2-7e3b6c8a0d11",
"version": [1, 0, 0]
},
{
"type": "script",
"language": "javascript",
"uuid": "b4d72e91-8c43-4f1a-95d2-7e3b6c8a0d12",
"version": [1, 0, 0],
"entry": "scripts/main.js"
}
],
"dependencies": [
{
"module_name": "@minecraft/server",
"version": "1.17.0"
}
]
}

View File

@@ -0,0 +1,17 @@
import { world, system } from "@minecraft/server";
function assertKeepInventory() {
try {
world.getDimension("overworld").runCommand("gamerule keepInventory true");
} catch (e) {
// Non-fatal — chunk-load races, etc. The next interval will retry.
}
}
system.run(() => {
assertKeepInventory();
world.sendMessage("§a[Keep Inventory] §7keepInventory enforced — items stay on death.");
});
// Re-assert every 10 minutes so anything that toggles it off (manual op, world reset, etc.) gets corrected.
system.runInterval(assertKeepInventory, 12000);

View File

@@ -10,9 +10,10 @@ const PORTAL_BLOCKS = {
// Coordinate-based portal zones (fallback detection)
const PORTAL_ZONES = [
{ name: "Jamie's World", x: -15, y: 65, z: -24, host: "10.0.0.247", port: 19133, color: "§a" },
{ name: "Lyla's World", x: 0, y: 65, z: -24, host: "10.0.0.247", port: 19134, color: "§d" },
{ name: "Mya's World", x: 15, y: 65, z: -24, host: "10.0.0.247", port: 19135, color: b" },
{ name: "Jamie's World", x: 436, y: 66, z: -296, host: "10.0.0.247", port: 19133, color: "§a" },
{ name: "Lyla's World", x: 462, y: 65, z: -322, host: "10.0.0.247", port: 19134, color: "§d" },
{ name: "Lyla's World", x: 474, y: 65, z: -281, host: "10.0.0.247", port: 19134, color: d" }, // Super Kitties portal
{ name: "Mya's World", x: 488, y: 66, z: -296, host: "10.0.0.247", port: 19135, color: "§b" },
];
const PORTAL_RADIUS_X = 2.5;
@@ -114,14 +115,15 @@ world.afterEvents.playerLeave.subscribe((event) => {
function placePortalSigns() {
const overworld = world.getDimension("overworld");
const signs = [
{ x: -15, y: 70, z: -23, name: "Jamie's", color: "§a" },
{ x: 0, y: 70, z: -23, name: "Lyla's", color: "§d" },
{ x: 15, y: 70, z: -23, name: "Mya's", color: "§b" },
{ x: 438, y: 71, z: -296, name: "Jamie's", color: "§a", facing: 5 }, // east-facing
{ x: 462, y: 71, z: -320, name: "Lyla's", color: "§d", facing: 3 }, // south-facing
{ x: 486, y: 71, z: -296, name: "Mya's", color: "§b", facing: 4 }, // west-facing
{ x: 474, y: 74, z: -281, name: "Super Kitties\n§fLyla's", color: "§d", facing: 2 }, // north-facing (Super Kitties portal)
];
for (const sign of signs) {
try {
overworld.runCommand(`setblock ${sign.x} ${sign.y} ${sign.z} oak_wall_sign ["facing_direction":3]`);
overworld.runCommand(`setblock ${sign.x} ${sign.y} ${sign.z} oak_wall_sign ["facing_direction":${sign.facing}]`);
} catch (e) {
// Non-fatal — chunks may not be loaded
}

View File

@@ -0,0 +1,41 @@
{
"format_version": "1.21.0",
"minecraft:entity": {
"description": {
"identifier": "silverlabs:banana_projectile",
"is_spawnable": false,
"is_summonable": true,
"is_experimental": false
},
"components": {
"minecraft:collision_box": {
"width": 0.25,
"height": 0.25
},
"minecraft:physics": {},
"minecraft:pushable": {
"is_pushable": false,
"is_pushable_by_piston": false
},
"minecraft:projectile": {
"on_hit": {
"impact_damage": {
"damage": 0,
"knockback": true,
"semi_random_diff_damage": false
},
"remove_on_hit": {}
},
"power": 1.3,
"gravity": 0.05,
"inertia": 0.99,
"liquid_inertia": 0.6,
"anchor": 1,
"offset": [0, -0.1, 0],
"should_bounce": false,
"hit_sound": "mob.slime.small"
}
}
}
}

View File

@@ -0,0 +1,118 @@
{
"format_version": "1.21.0",
"minecraft:entity": {
"description": {
"identifier": "silverlabs:cheeky_monkey",
"is_spawnable": true,
"is_summonable": true,
"is_experimental": false
},
"components": {
"minecraft:type_family": {
"family": ["cheeky_monkey", "monkey", "animal", "mob"]
},
"minecraft:physics": {},
"minecraft:pushable": {
"is_pushable": true,
"is_pushable_by_piston": true
},
"minecraft:collision_box": {
"width": 0.5,
"height": 0.9
},
"minecraft:health": {
"value": 8,
"max": 8
},
"minecraft:movement": {
"value": 0.3
},
"minecraft:navigation.walk": {
"can_path_over_water": false,
"avoid_water": true,
"avoid_damage_blocks": true
},
"minecraft:movement.basic": {},
"minecraft:jump.static": {},
"minecraft:can_climb": {},
"minecraft:breathable": {
"total_supply": 15,
"suffocate_time": -1
},
"minecraft:nameable": {},
"minecraft:hurt_on_condition": {
"damage_conditions": [
{
"filters": { "test": "in_lava", "subject": "self", "operator": "==", "value": true },
"cause": "lava",
"damage_per_tick": 4
}
]
},
"minecraft:loot": {
"table": "loot_tables/entities/cheeky_monkey.json"
},
"minecraft:shooter": {
"def": "silverlabs:banana_projectile"
},
"minecraft:behavior.float": { "priority": 0 },
"minecraft:behavior.panic": {
"priority": 1,
"speed_multiplier": 1.6
},
"minecraft:behavior.ranged_attack": {
"priority": 3,
"attack_interval_min": 3.0,
"attack_interval_max": 6.0,
"attack_radius": 10.0,
"speed_multiplier": 0.9
},
"minecraft:behavior.nearest_attackable_target": {
"priority": 4,
"within_radius": 16,
"entity_types": [
{
"filters": { "test": "is_family", "subject": "other", "value": "player" },
"max_dist": 16
}
],
"must_see": false,
"reselect_targets": true,
"must_reach": false
},
"minecraft:behavior.move_towards_target": {
"priority": 5,
"speed_multiplier": 0.8,
"within_radius": 10.0
},
"minecraft:behavior.look_at_player": {
"priority": 7,
"target_distance": 14,
"look_time": [3, 7],
"probability": 0.9
},
"minecraft:behavior.random_stroll": {
"priority": 6,
"speed_multiplier": 0.7,
"interval": 60,
"xz_dist": 8,
"y_dist": 4
},
"minecraft:behavior.random_look_around": {
"priority": 9,
"look_time": [2, 5]
}
}
}
}

View File

@@ -0,0 +1,32 @@
{
"format_version": "1.21.0",
"minecraft:item": {
"description": {
"identifier": "silverlabs:banana",
"menu_category": {
"category": "nature"
}
},
"components": {
"minecraft:icon": {
"textures": {
"default": "banana"
}
},
"minecraft:display_name": {
"value": "Banana"
},
"minecraft:max_stack_size": 64,
"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
}
}
}
}

View File

@@ -0,0 +1,20 @@
{
"pools": [
{
"rolls": 1,
"entries": [
{
"type": "item",
"name": "silverlabs:banana",
"weight": 1,
"functions": [
{
"function": "set_count",
"count": { "min": 1, "max": 2 }
}
]
}
]
}
]
}

View File

@@ -0,0 +1,23 @@
{
"format_version": 2,
"header": {
"name": "Cheeky Monkey",
"description": "Curious banana-throwing monkeys that spawn naturally in jungles.",
"uuid": "c9d1f11c-0eef-4374-81ee-ebde67d7a0a3",
"version": [1, 0, 0],
"min_engine_version": [1, 21, 0]
},
"modules": [
{
"type": "data",
"uuid": "48d1d747-0765-49ab-8f50-e9b0236c8e32",
"version": [1, 0, 0]
}
],
"dependencies": [
{
"uuid": "7195b65a-171d-4324-a2df-e36ba75ec48a",
"version": [1, 0, 0]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,41 @@
{
"format_version": "1.8.0",
"minecraft:spawn_rules": {
"description": {
"identifier": "silverlabs:cheeky_monkey",
"population_control": "animal"
},
"conditions": [
{
"minecraft:spawns_on_surface": {},
"minecraft:brightness_filter": {
"min": 7,
"max": 15,
"adjust_for_weather": false
},
"minecraft:weight": {
"default": 8
},
"minecraft:herd": {
"min_size": 2,
"max_size": 4
},
"minecraft:density_limit": {
"surface": 4
},
"minecraft:biome_filter": {
"any_of": [
{ "test": "has_biome_tag", "value": "jungle" },
{ "test": "has_biome_tag", "value": "bamboo" }
]
},
"minecraft:spawns_on_block_filter": [
"minecraft:grass_block",
"minecraft:dirt",
"minecraft:podzol",
"minecraft:moss_block"
]
}
]
}
}

View File

@@ -0,0 +1,22 @@
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.cheeky_monkey": {
"initial_state": "idle",
"states": {
"idle": {
"animations": ["idle"],
"transitions": [
{ "walk": "query.modified_move_speed > 0.1" }
]
},
"walk": {
"animations": ["walk", "idle"],
"transitions": [
{ "idle": "query.modified_move_speed <= 0.1" }
]
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
{
"format_version": "1.8.0",
"animations": {
"animation.banana_projectile.spin": {
"loop": true,
"bones": {
"banana": {
"rotation": ["query.anim_time * 720", "0", "query.anim_time * 360"]
}
}
}
}
}

View File

@@ -0,0 +1,70 @@
{
"format_version": "1.8.0",
"animations": {
"animation.cheeky_monkey.idle": {
"loop": true,
"bones": {
"body": {
"rotation": ["0", "math.sin(query.anim_time * 120) * 1.5", "0"]
},
"head": {
"rotation": ["math.sin(query.anim_time * 90) * 3", "math.sin(query.anim_time * 60) * 5", "0"]
},
"tail": {
"rotation": ["25 + math.sin(query.anim_time * 180) * 8", "math.sin(query.anim_time * 140) * 10", "0"]
},
"arm_left": {
"rotation": ["math.sin(query.anim_time * 100) * 3", "0", "0"]
},
"arm_right": {
"rotation": ["-math.sin(query.anim_time * 100) * 3", "0", "0"]
}
}
},
"animation.cheeky_monkey.walk": {
"loop": true,
"anim_time_update": "query.modified_distance_moved * 1.5",
"bones": {
"leg_left": {
"rotation": ["math.sin(query.anim_time * 240) * 30", "0", "0"]
},
"leg_right": {
"rotation": ["-math.sin(query.anim_time * 240) * 30", "0", "0"]
},
"arm_left": {
"rotation": ["-math.sin(query.anim_time * 240) * 25", "0", "0"]
},
"arm_right": {
"rotation": ["math.sin(query.anim_time * 240) * 25", "0", "0"]
},
"tail": {
"rotation": ["25 + math.sin(query.anim_time * 240) * 15", "0", "0"]
}
}
},
"animation.cheeky_monkey.throw": {
"loop": false,
"animation_length": 0.6,
"bones": {
"arm_right": {
"rotation": {
"0.0": [0, 0, 0],
"0.15": [-110, 0, 15],
"0.35": [60, 0, -10],
"0.6": [0, 0, 0]
}
},
"body": {
"rotation": {
"0.0": [0, 0, 0],
"0.15": [0, -10, 0],
"0.35": [0, 15, 0],
"0.6": [0, 0, 0]
}
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
{
"format_version": "1.10.0",
"minecraft:client_entity": {
"description": {
"identifier": "silverlabs:banana_projectile",
"materials": {
"default": "entity_alphatest"
},
"textures": {
"default": "textures/entity/banana_projectile"
},
"geometry": {
"default": "geometry.banana_projectile"
},
"render_controllers": [
"controller.render.banana_projectile"
],
"animations": {
"spin": "animation.banana_projectile.spin"
},
"scripts": {
"animate": ["spin"]
}
}
}
}

View File

@@ -0,0 +1,33 @@
{
"format_version": "1.10.0",
"minecraft:client_entity": {
"description": {
"identifier": "silverlabs:cheeky_monkey",
"materials": {
"default": "entity_alphatest"
},
"textures": {
"default": "textures/entity/cheeky_monkey"
},
"geometry": {
"default": "geometry.cheeky_monkey"
},
"render_controllers": [
"controller.render.cheeky_monkey"
],
"animations": {
"idle": "animation.cheeky_monkey.idle",
"walk": "animation.cheeky_monkey.walk",
"throw": "animation.cheeky_monkey.throw",
"controller": "controller.animation.cheeky_monkey"
},
"scripts": {
"animate": ["controller"]
},
"spawn_egg": {
"base_color": "#6B4423",
"overlay_color": "#F4D03F"
}
}
}
}

View File

@@ -0,0 +1,17 @@
{
"format_version": 2,
"header": {
"name": "Cheeky Monkey Resources",
"description": "Textures, models, and animations for the cheeky monkey.",
"uuid": "7195b65a-171d-4324-a2df-e36ba75ec48a",
"version": [1, 0, 0],
"min_engine_version": [1, 21, 0]
},
"modules": [
{
"type": "resources",
"uuid": "52b854fe-8b36-4a0d-a94a-d20b8fbca74a",
"version": [1, 0, 0]
}
]
}

View File

@@ -0,0 +1,41 @@
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.banana_projectile",
"texture_width": 16,
"texture_height": 16,
"visible_bounds_width": 1,
"visible_bounds_height": 1,
"visible_bounds_offset": [0, 0.25, 0]
},
"bones": [
{ "name": "root", "pivot": [0, 0, 0] },
{
"name": "banana",
"parent": "root",
"pivot": [0, 0, 0],
"rotation": [0, 0, 25],
"cubes": [
{
"origin": [-2, 0, -0.5],
"size": [4, 1, 1],
"uv": [0, 0]
},
{
"origin": [-3, 0.5, -0.5],
"size": [1, 1, 1],
"uv": [0, 2]
},
{
"origin": [2, 0.5, -0.5],
"size": [1, 1, 1],
"uv": [0, 2]
}
]
}
]
}
]
}

View File

@@ -0,0 +1,152 @@
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.cheeky_monkey",
"texture_width": 64,
"texture_height": 32,
"visible_bounds_width": 2,
"visible_bounds_height": 2.5,
"visible_bounds_offset": [0, 1, 0]
},
"bones": [
{ "name": "root", "pivot": [0, 0, 0] },
{
"name": "body",
"parent": "root",
"pivot": [0, 8, 0],
"cubes": [
{
"origin": [-3, 4, -2],
"size": [6, 8, 4],
"uv": [0, 0]
}
]
},
{
"name": "head",
"parent": "body",
"pivot": [0, 12, 0],
"cubes": [
{
"origin": [-3, 12, -3],
"size": [6, 6, 6],
"uv": [20, 0]
}
]
},
{
"name": "muzzle",
"parent": "head",
"pivot": [0, 14, -3],
"cubes": [
{
"origin": [-1.5, 13, -5],
"size": [3, 2, 2],
"uv": [44, 0]
}
]
},
{
"name": "ear_left",
"parent": "head",
"pivot": [-3, 17, 0],
"cubes": [
{
"origin": [-4, 16, -1],
"size": [1, 2, 2],
"uv": [50, 0]
}
]
},
{
"name": "ear_right",
"parent": "head",
"pivot": [3, 17, 0],
"mirror": true,
"cubes": [
{
"origin": [3, 16, -1],
"size": [1, 2, 2],
"uv": [50, 0]
}
]
},
{
"name": "arm_left",
"parent": "body",
"pivot": [-3, 11, 0],
"cubes": [
{
"origin": [-5, 5, -1],
"size": [2, 6, 2],
"uv": [0, 16]
}
]
},
{
"name": "arm_right",
"parent": "body",
"pivot": [3, 11, 0],
"mirror": true,
"cubes": [
{
"origin": [3, 5, -1],
"size": [2, 6, 2],
"uv": [0, 16]
}
]
},
{
"name": "leg_left",
"parent": "root",
"pivot": [-1.5, 4, 0],
"cubes": [
{
"origin": [-2.5, 0, -1],
"size": [2, 4, 2],
"uv": [16, 16]
}
]
},
{
"name": "leg_right",
"parent": "root",
"pivot": [1.5, 4, 0],
"mirror": true,
"cubes": [
{
"origin": [0.5, 0, -1],
"size": [2, 4, 2],
"uv": [16, 16]
}
]
},
{
"name": "tail",
"parent": "body",
"pivot": [0, 5, 2],
"rotation": [-25, 0, 0],
"cubes": [
{
"origin": [-0.5, 4.5, 1],
"size": [1, 1, 5],
"uv": [32, 16]
}
]
}
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,19 @@
{
"format_version": "1.10.0",
"render_controllers": {
"controller.render.cheeky_monkey": {
"geometry": "geometry.default",
"materials": [
{ "*": "material.default" }
],
"textures": ["texture.default"]
},
"controller.render.banana_projectile": {
"geometry": "geometry.default",
"materials": [
{ "*": "material.default" }
],
"textures": ["texture.default"]
}
}
}

View File

@@ -0,0 +1,3 @@
entity.silverlabs:cheeky_monkey.name=Cheeky Monkey
item.spawn_egg.entity.silverlabs:cheeky_monkey.name=Spawn Cheeky Monkey
item.silverlabs:banana=Banana

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

View File

@@ -0,0 +1,9 @@
{
"resource_pack_name": "monkey_RP",
"texture_name": "atlas.items",
"texture_data": {
"banana": {
"textures": "textures/items/banana"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

View File

@@ -0,0 +1,27 @@
{
"format_version": "1.21.0",
"minecraft:block": {
"description": {
"identifier": "silverlabs:mailbox",
"menu_category": {
"category": "items",
"group": "itemGroup.name.chest"
}
},
"components": {
"minecraft:destructible_by_mining": {
"seconds_to_destroy": 2.0
},
"minecraft:destructible_by_explosion": {
"explosion_resistance": 1200.0
},
"minecraft:map_color": "#C83232",
"minecraft:material_instances": {
"*": {
"texture": "mailbox",
"render_method": "opaque"
}
}
}
}
}

View File

@@ -0,0 +1,27 @@
{
"format_version": "1.21.0",
"minecraft:block": {
"description": {
"identifier": "silverlabs:post_office",
"menu_category": {
"category": "items",
"group": "itemGroup.name.chest"
}
},
"components": {
"minecraft:destructible_by_mining": {
"seconds_to_destroy": 2.5
},
"minecraft:destructible_by_explosion": {
"explosion_resistance": 1200.0
},
"minecraft:map_color": "#8B5A2B",
"minecraft:material_instances": {
"*": {
"texture": "post_office",
"render_method": "opaque"
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
{
"format_version": 2,
"header": {
"name": "Postal Service",
"description": "Player-to-player mail: personal mailboxes and a post office send block",
"uuid": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b60",
"version": [1, 0, 0],
"min_engine_version": [1, 21, 0]
},
"modules": [
{
"type": "data",
"uuid": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b61",
"version": [1, 0, 0]
},
{
"type": "script",
"language": "javascript",
"uuid": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b62",
"version": [1, 0, 0],
"entry": "scripts/main.js"
}
],
"dependencies": [
{
"module_name": "@minecraft/server",
"version": "1.17.0"
},
{
"module_name": "@minecraft/server-ui",
"version": "1.3.0"
},
{
"uuid": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b63",
"version": [1, 0, 0]
}
]
}

View File

@@ -0,0 +1,18 @@
{
"format_version": "1.21.0",
"minecraft:recipe_shapeless": {
"description": {
"identifier": "silverlabs:mailbox_recipe"
},
"tags": ["crafting_table"],
"unlock": { "context": "AlwaysUnlocked" },
"ingredients": [
{ "item": "minecraft:chest" },
{ "item": "minecraft:gold_ingot" }
],
"result": {
"item": "silverlabs:mailbox",
"count": 1
}
}
}

View File

@@ -0,0 +1,24 @@
{
"format_version": "1.21.0",
"minecraft:recipe_shaped": {
"description": {
"identifier": "silverlabs:post_office_recipe"
},
"tags": ["crafting_table"],
"unlock": { "context": "AlwaysUnlocked" },
"pattern": [
"III",
"PCP",
"PPP"
],
"key": {
"I": { "item": "minecraft:iron_ingot" },
"P": { "item": "minecraft:oak_planks" },
"C": { "item": "minecraft:chest" }
},
"result": {
"item": "silverlabs:post_office",
"count": 1
}
}
}

View File

@@ -0,0 +1,390 @@
import { world, system, ItemStack } from "@minecraft/server";
import { ActionFormData, MessageFormData } from "@minecraft/server-ui";
// ─── Constants ──────────────────────────────────────────────
const MAILBOX_BLOCK = "silverlabs:mailbox";
const POST_OFFICE_BLOCK = "silverlabs:post_office";
const VANILLA_CHEST = "minecraft:chest";
const PROP_KEY = "postal_state_v1";
// ─── State ──────────────────────────────────────────────────
// mailboxes: { "x,y,z,dim": { ownerId, ownerName } }
// registry: { [ownerId]: { name, x, y, z, dim } } — reverse lookup for recipient picker
// pending: { [ownerId]: [{ from, itemSummary, ts }] } — queued offline notifications
let state = { mailboxes: {}, registry: {}, pending: {} };
function loadState() {
try {
const raw = world.getDynamicProperty(PROP_KEY);
if (raw && typeof raw === "string") {
const parsed = JSON.parse(raw);
state = {
mailboxes: parsed.mailboxes || {},
registry: parsed.registry || {},
pending: parsed.pending || {},
};
}
} catch (e) {
world.sendMessage(`§c[Postal] Failed to load state: ${e.message}`);
state = { mailboxes: {}, registry: {}, pending: {} };
}
}
function saveState() {
try {
world.setDynamicProperty(PROP_KEY, JSON.stringify(state));
} catch (e) {
world.sendMessage(`§c[Postal] Failed to save state: ${e.message}`);
}
}
function keyOf(loc, dimensionId) {
return `${Math.floor(loc.x)},${Math.floor(loc.y)},${Math.floor(loc.z)},${dimensionId}`;
}
function getMailboxOwner(loc, dimensionId) {
return state.mailboxes[keyOf(loc, dimensionId)] || null;
}
function claimMailbox(loc, dimensionId, player) {
const k = keyOf(loc, dimensionId);
state.mailboxes[k] = { ownerId: player.id, ownerName: player.name };
state.registry[player.id] = {
name: player.name,
x: Math.floor(loc.x),
y: Math.floor(loc.y),
z: Math.floor(loc.z),
dim: dimensionId,
};
saveState();
}
function releaseMailbox(loc, dimensionId) {
const k = keyOf(loc, dimensionId);
const entry = state.mailboxes[k];
delete state.mailboxes[k];
if (entry && state.registry[entry.ownerId]) {
const reg = state.registry[entry.ownerId];
if (reg.x === Math.floor(loc.x) && reg.y === Math.floor(loc.y) && reg.z === Math.floor(loc.z) && reg.dim === dimensionId) {
delete state.registry[entry.ownerId];
}
}
saveState();
}
function hasClaim(playerId) {
return !!state.registry[playerId];
}
// ─── Chest facing from player rotation ──────────────────────
function chestFacing(yaw) {
let y = yaw;
while (y > 180) y -= 360;
while (y < -180) y += 360;
if (y >= -45 && y < 45) return "north";
if (y >= 45 && y < 135) return "east";
if (y >= -135 && y < -45) return "west";
return "south";
}
// ─── Mailbox placement ──────────────────────────────────────
world.afterEvents.playerPlaceBlock.subscribe((event) => {
const block = event.block;
if (block.typeId !== MAILBOX_BLOCK) return;
const player = event.player;
const loc = block.location;
const dim = block.dimension;
if (hasClaim(player.id)) {
// Revert placement and refund the item
try {
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
dim.spawnItem(new ItemStack(MAILBOX_BLOCK, 1), {
x: loc.x + 0.5,
y: loc.y + 0.5,
z: loc.z + 0.5,
});
} catch (_) {}
player.sendMessage(`§c[Postal] §7You already have a mailbox in this world.`);
return;
}
const facing = chestFacing(player.getRotation().y);
try {
dim.runCommand(
`setblock ${loc.x} ${loc.y} ${loc.z} chest ["minecraft:cardinal_direction":"${facing}"]`
);
} catch (_) {
try {
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} chest`);
} catch (_) {}
}
claimMailbox(loc, dim.id, player);
player.sendMessage(`§6[Postal] §7Mailbox locked to you. Only you can open or break it.`);
});
// ─── Interact: gate mailbox opening for non-owners ──────────
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
const block = event.block;
if (!block) return;
// Post-office block: open send UI (cancel default interact)
if (block.typeId === POST_OFFICE_BLOCK) {
event.cancel = true;
const player = event.player;
system.run(() => openSendForm(player));
return;
}
// Vanilla chest: gate if it's a claimed mailbox
if (block.typeId !== VANILLA_CHEST) return;
const owner = getMailboxOwner(block.location, block.dimension.id);
if (!owner) return;
if (owner.ownerId === event.player.id) return;
event.cancel = true;
const playerRef = event.player;
system.run(() =>
playerRef.sendMessage(`§c[Postal] §7This mailbox belongs to §f${owner.ownerName}§7.`)
);
});
// ─── Break: protect mailboxes; owner break returns custom item ──
world.beforeEvents.playerBreakBlock.subscribe((event) => {
const block = event.block;
if (block.typeId !== VANILLA_CHEST) return;
const owner = getMailboxOwner(block.location, block.dimension.id);
if (!owner) return;
const player = event.player;
if (owner.ownerId !== player.id) {
event.cancel = true;
system.run(() =>
player.sendMessage(`§c[Postal] §7This mailbox belongs to §f${owner.ownerName}§7. You can't break it.`)
);
return;
}
event.cancel = true;
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
const dim = block.dimension;
system.run(() => {
try {
const inv = dim.getBlock(loc)?.getComponent("inventory");
const container = inv?.container;
const dropPos = { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 };
if (container) {
for (let i = 0; i < container.size; i++) {
const item = container.getItem(i);
if (item) {
dim.spawnItem(item, dropPos);
container.setItem(i, undefined);
}
}
}
dim.spawnItem(new ItemStack(MAILBOX_BLOCK, 1), dropPos);
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
} catch (e) {
player.sendMessage(`§c[Postal] Error during break: ${e.message}`);
}
releaseMailbox(loc, dim.id);
});
});
// ─── Post Office: send form ─────────────────────────────────
function getHeldItem(player) {
try {
const inv = player.getComponent("inventory")?.container;
if (!inv) return { item: undefined, slot: -1 };
const slot = player.selectedSlotIndex;
const item = inv.getItem(slot);
return { item, slot, inv };
} catch (_) {
return { item: undefined, slot: -1 };
}
}
function itemSummary(item) {
const niceName = item.typeId.replace(/^minecraft:/, "").replace(/_/g, " ");
return `${item.amount} × ${niceName}`;
}
async function openSendForm(player) {
const { item, slot, inv } = getHeldItem(player);
if (!item) {
player.sendMessage(`§c[Postal] §7Hold the item you want to send in your hotbar first.`);
return;
}
const candidates = [];
for (const [ownerId, reg] of Object.entries(state.registry)) {
if (ownerId === player.id) continue;
candidates.push({ ownerId, reg });
}
if (candidates.length === 0) {
player.sendMessage(`§c[Postal] §7No other players have claimed a mailbox yet.`);
return;
}
const form = new ActionFormData()
.title("Post Office")
.body(`Sending §f${itemSummary(item)}§r\n\nChoose a recipient:`);
for (const c of candidates) form.button(c.reg.name);
form.button("§cCancel");
let response;
try {
response = await form.show(player);
} catch (_) {
return;
}
if (response.canceled || response.selection === undefined) return;
if (response.selection >= candidates.length) return; // cancel button
const chosen = candidates[response.selection];
const confirm = new MessageFormData()
.title("Confirm Send")
.body(`Send §f${itemSummary(item)}§r\nto §f${chosen.reg.name}§r?`)
.button1("Send")
.button2("Cancel");
let conf;
try {
conf = await confirm.show(player);
} catch (_) {
return;
}
if (conf.canceled || conf.selection !== 0) return;
// Re-fetch held item in case it changed while form was open
const fresh = getHeldItem(player);
if (!fresh.item || fresh.item.typeId !== item.typeId || fresh.item.amount !== item.amount) {
player.sendMessage(`§c[Postal] §7Held item changed — send cancelled.`);
return;
}
deliver(player, chosen.ownerId, chosen.reg, fresh.item, fresh.slot, fresh.inv);
}
// ─── Delivery ───────────────────────────────────────────────
function deliver(senderPlayer, recipientId, reg, itemStack, slot, inv, retry = 0) {
const dim = world.getDimension(reg.dim);
const loc = { x: reg.x, y: reg.y, z: reg.z };
let block;
try {
block = dim.getBlock(loc);
} catch (_) {
block = undefined;
}
if (!block) {
if (retry >= 1) {
senderPlayer.sendMessage(
`§c[Postal] §7Couldn't reach §f${reg.name}§7's mailbox (chunk not loaded). Try again later.`
);
return;
}
// Force chunk load briefly, then retry
const taName = `postal_tmp_${recipientId}`.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 20);
try {
dim.runCommand(
`tickingarea add ${reg.x} ${reg.y} ${reg.z} ${reg.x} ${reg.y} ${reg.z} ${taName}`
);
} catch (_) {}
system.runTimeout(() => {
deliver(senderPlayer, recipientId, reg, itemStack, slot, inv, retry + 1);
try {
dim.runCommand(`tickingarea remove ${taName}`);
} catch (_) {}
}, 20);
return;
}
if (block.typeId !== VANILLA_CHEST) {
// Mailbox was destroyed externally — clean up registry
releaseMailbox(loc, reg.dim);
senderPlayer.sendMessage(
`§c[Postal] §f${reg.name}§7's mailbox is missing. Send cancelled.`
);
return;
}
const owner = getMailboxOwner(loc, reg.dim);
if (!owner || owner.ownerId !== recipientId) {
releaseMailbox(loc, reg.dim);
senderPlayer.sendMessage(
`§c[Postal] §f${reg.name}§7's mailbox is no longer claimed. Send cancelled.`
);
return;
}
const container = block.getComponent("inventory")?.container;
if (!container) {
senderPlayer.sendMessage(`§c[Postal] §7Couldn't access the mailbox contents.`);
return;
}
const leftover = container.addItem(itemStack);
// Remove the full stack from sender's inventory first
try {
inv.setItem(slot, undefined);
} catch (_) {}
// Refund leftover (mailbox full) back to sender
if (leftover) {
const p = senderPlayer.location;
try {
senderPlayer.dimension.spawnItem(leftover, p);
} catch (_) {}
senderPlayer.sendMessage(
`§c[Postal] §f${reg.name}§7's mailbox is full — partial delivery. Leftover dropped at your feet.`
);
}
const summary = itemSummary(itemStack);
senderPlayer.sendMessage(`§6[Postal] §7Sent §f${summary}§7 to §f${reg.name}§7.`);
// Notify recipient
const onlineRecipient = world.getAllPlayers().find((p) => p.id === recipientId);
if (onlineRecipient) {
onlineRecipient.sendMessage(`§6[Postal] §7You've got mail from §f${senderPlayer.name}§7! (${summary})`);
} else {
if (!state.pending[recipientId]) state.pending[recipientId] = [];
state.pending[recipientId].push({
from: senderPlayer.name,
itemSummary: summary,
ts: Date.now(),
});
saveState();
}
}
// ─── Flush pending notifications on login ───────────────────
world.afterEvents.playerSpawn.subscribe((event) => {
if (!event.initialSpawn) return;
const player = event.player;
const queue = state.pending[player.id];
if (!queue || queue.length === 0) return;
system.runTimeout(() => {
player.sendMessage(`§6[Postal] §7You have §f${queue.length}§7 new mail notification(s):`);
for (const n of queue) {
const when = new Date(n.ts).toISOString().replace("T", " ").slice(0, 16);
player.sendMessage(`§7[${when}] §7from §f${n.from}§7 — ${n.itemSummary}`);
}
delete state.pending[player.id];
saveState();
}, 40);
});
// ─── Boot ──────────────────────────────────────────────────
system.run(() => {
loadState();
world.sendMessage("§6[Postal] §7Postal service loaded.");
});

View File

@@ -0,0 +1,5 @@
{
"format_version": [1, 1, 0],
"silverlabs:mailbox": { "sound": "stone" },
"silverlabs:post_office": { "sound": "wood" }
}

View File

@@ -0,0 +1,17 @@
{
"format_version": 2,
"header": {
"name": "Postal Service Resources",
"description": "Textures and lang for silverlabs:mailbox and silverlabs:post_office",
"uuid": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b63",
"version": [1, 0, 0],
"min_engine_version": [1, 21, 0]
},
"modules": [
{
"type": "resources",
"uuid": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b64",
"version": [1, 0, 0]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

View File

@@ -0,0 +1,2 @@
tile.silverlabs:mailbox.name=Mailbox
tile.silverlabs:post_office.name=Post Office

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

View File

@@ -0,0 +1,14 @@
{
"resource_pack_name": "postal_service_RP",
"texture_name": "atlas.terrain",
"padding": 8,
"num_mip_levels": 4,
"texture_data": {
"mailbox": {
"textures": "textures/blocks/mailbox"
},
"post_office": {
"textures": "textures/blocks/post_office"
}
}
}

View File

@@ -0,0 +1,27 @@
{
"format_version": "1.21.0",
"minecraft:block": {
"description": {
"identifier": "silverlabs:private_chest",
"menu_category": {
"category": "items",
"group": "itemGroup.name.chest"
}
},
"components": {
"minecraft:destructible_by_mining": {
"seconds_to_destroy": 2.5
},
"minecraft:destructible_by_explosion": {
"explosion_resistance": 1200.0
},
"minecraft:map_color": "#B0B0B0",
"minecraft:material_instances": {
"*": {
"texture": "private_chest",
"render_method": "opaque"
}
}
}
}
}

View File

@@ -0,0 +1,34 @@
{
"format_version": 2,
"header": {
"name": "Private Chests",
"description": "Owner-locked chests — only the placer can open or break them",
"uuid": "9a3f8d2e-7c5b-4e1a-b9d2-1f6e3c4a8b50",
"version": [1, 0, 0],
"min_engine_version": [1, 21, 0]
},
"modules": [
{
"type": "data",
"uuid": "9a3f8d2e-7c5b-4e1a-b9d2-1f6e3c4a8b51",
"version": [1, 0, 0]
},
{
"type": "script",
"language": "javascript",
"uuid": "9a3f8d2e-7c5b-4e1a-b9d2-1f6e3c4a8b52",
"version": [1, 0, 0],
"entry": "scripts/main.js"
}
],
"dependencies": [
{
"module_name": "@minecraft/server",
"version": "1.17.0"
},
{
"uuid": "9a3f8d2e-7c5b-4e1a-b9d2-1f6e3c4a8b53",
"version": [1, 0, 1]
}
]
}

View File

@@ -0,0 +1,18 @@
{
"format_version": "1.21.0",
"minecraft:recipe_shapeless": {
"description": {
"identifier": "silverlabs:private_chest_recipe"
},
"tags": ["crafting_table"],
"unlock": { "context": "AlwaysUnlocked" },
"ingredients": [
{ "item": "minecraft:chest" },
{ "item": "minecraft:iron_ingot" }
],
"result": {
"item": "silverlabs:private_chest",
"count": 1
}
}
}

View File

@@ -0,0 +1,158 @@
import { world, system, ItemStack } from "@minecraft/server";
// ─── Constants ──────────────────────────────────────────────
const PRIVATE_BLOCK = "silverlabs:private_chest";
const VANILLA_CHEST = "minecraft:chest";
const PROP_KEY = "private_chests_v1";
// ─── State ──────────────────────────────────────────────────
// In-memory mirror of dynamic property: { "x,y,z,dim": { ownerId, ownerName } }
let chests = {};
function loadState() {
try {
const raw = world.getDynamicProperty(PROP_KEY);
if (raw && typeof raw === "string") {
chests = JSON.parse(raw);
}
} catch (e) {
world.sendMessage(`§c[Private Chest] Failed to load state: ${e.message}`);
chests = {};
}
}
function saveState() {
try {
world.setDynamicProperty(PROP_KEY, JSON.stringify(chests));
} catch (e) {
world.sendMessage(`§c[Private Chest] Failed to save state: ${e.message}`);
}
}
function keyOf(loc, dimensionId) {
return `${Math.floor(loc.x)},${Math.floor(loc.y)},${Math.floor(loc.z)},${dimensionId}`;
}
function claim(loc, dimensionId, player) {
chests[keyOf(loc, dimensionId)] = { ownerId: player.id, ownerName: player.name };
saveState();
}
function release(loc, dimensionId) {
delete chests[keyOf(loc, dimensionId)];
saveState();
}
function getOwner(loc, dimensionId) {
return chests[keyOf(loc, dimensionId)] || null;
}
// ─── Chest facing from player rotation ──────────────────────
// Bedrock yaw: 0=south, 90=west, 180=north, -90=east. Chest front faces the player.
function chestFacing(yaw) {
let y = yaw;
while (y > 180) y -= 360;
while (y < -180) y += 360;
if (y >= -45 && y < 45) return "north"; // player facing south
if (y >= 45 && y < 135) return "east"; // player facing west
if (y >= -135 && y < -45) return "west"; // player facing east
return "south"; // player facing north
}
// ─── Placement: swap custom block → vanilla chest + claim ──
world.afterEvents.playerPlaceBlock.subscribe((event) => {
const block = event.block;
if (block.typeId !== PRIVATE_BLOCK) return;
const player = event.player;
const loc = block.location;
const dim = block.dimension;
const facing = chestFacing(player.getRotation().y);
try {
dim.runCommand(
`setblock ${loc.x} ${loc.y} ${loc.z} chest ["minecraft:cardinal_direction":"${facing}"]`
);
} catch (e) {
// Older block-state syntax fallback (rare)
try {
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} chest`);
} catch (_) {}
}
claim(loc, dim.id, player);
player.sendMessage(`§6[Private Chest] §7Locked to you. Only you can open or break it.`);
});
// ─── Interact: block opening for non-owners ─────────────────
try {
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
const block = event.block;
if (block.typeId !== VANILLA_CHEST) return;
const owner = getOwner(block.location, block.dimension.id);
if (!owner) return;
if (owner.ownerId === event.player.id) return;
event.cancel = true;
const playerRef = event.player;
system.run(() =>
playerRef.sendMessage(`§c[Private Chest] §7This chest belongs to §f${owner.ownerName}§7.`)
);
});
} catch (e) {
console.warn(`[Private Chest] beforeEvents.playerInteractWithBlock unavailable: ${e}`);
}
// ─── Break: protect for non-owners; owner break drops the custom item back ──
try {
world.beforeEvents.playerBreakBlock.subscribe((event) => {
const block = event.block;
if (block.typeId !== VANILLA_CHEST) return;
const owner = getOwner(block.location, block.dimension.id);
if (!owner) return;
const player = event.player;
if (owner.ownerId !== player.id) {
event.cancel = true;
system.run(() =>
player.sendMessage(`§c[Private Chest] §7This chest belongs to §f${owner.ownerName}§7. You can't break it.`)
);
return;
}
// Owner breaking — cancel default drop, manually eject contents + return the custom item.
event.cancel = true;
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
const dim = block.dimension;
system.run(() => {
try {
const inv = dim.getBlock(loc)?.getComponent("inventory");
const container = inv?.container;
const dropPos = { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 };
if (container) {
for (let i = 0; i < container.size; i++) {
const item = container.getItem(i);
if (item) {
dim.spawnItem(item, dropPos);
container.setItem(i, undefined);
}
}
}
dim.spawnItem(new ItemStack(PRIVATE_BLOCK, 1), dropPos);
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
} catch (e) {
player.sendMessage(`§c[Private Chest] Error during break: ${e.message}`);
}
release(loc, dim.id);
});
});
} catch (e) {
console.warn(`[Private Chest] beforeEvents.playerBreakBlock unavailable: ${e}`);
}
// ─── Boot ──────────────────────────────────────────────────
system.run(() => {
loadState();
world.sendMessage("§6[Private Chest] §7Owner-locked chest system loaded.");
});

View File

@@ -0,0 +1,4 @@
{
"format_version": [1, 1, 0],
"silverlabs:private_chest": { "sound": "stone" }
}

View File

@@ -0,0 +1,17 @@
{
"format_version": 2,
"header": {
"name": "Private Chests Resources",
"description": "Textures and lang for the silverlabs:private_chest block",
"uuid": "9a3f8d2e-7c5b-4e1a-b9d2-1f6e3c4a8b53",
"version": [1, 0, 0],
"min_engine_version": [1, 21, 0]
},
"modules": [
{
"type": "resources",
"uuid": "9a3f8d2e-7c5b-4e1a-b9d2-1f6e3c4a8b54",
"version": [1, 0, 1]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

View File

@@ -0,0 +1 @@
tile.silverlabs:private_chest.name=Private Chest

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

View File

@@ -0,0 +1,11 @@
{
"resource_pack_name": "private_chest_RP",
"texture_name": "atlas.terrain",
"padding": 8,
"num_mip_levels": 4,
"texture_data": {
"private_chest": {
"textures": "textures/blocks/private_chest"
}
}
}

View File

@@ -1,6 +1,14 @@
{
"behavior_packs": [
{"pack_id": "c7e91f32-4a8b-4d6e-b21c-9f3a05d8e047", "version": [1, 0, 0]}
{"pack_id": "c7e91f32-4a8b-4d6e-b21c-9f3a05d8e047", "version": [1, 0, 0]},
{"pack_id": "9a3f8d2e-7c5b-4e1a-b9d2-1f6e3c4a8b50", "version": [1, 0, 0]},
{"pack_id": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b80", "version": [1, 0, 0]},
{"pack_id": "b4d72e91-8c43-4f1a-95d2-7e3b6c8a0d10", "version": [1, 0, 0]},
{"pack_id": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b60", "version": [1, 0, 0]}
],
"resource_packs": []
"resource_packs": [
{"pack_id": "9a3f8d2e-7c5b-4e1a-b9d2-1f6e3c4a8b53", "version": [1, 0, 1]},
{"pack_id": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b83", "version": [1, 0, 1]},
{"pack_id": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b63", "version": [1, 0, 0]}
]
}