diff --git a/camping-supplies-addon/camping_supplies_BP/blocks/tent_panel_l.json b/camping-supplies-addon/camping_supplies_BP/blocks/tent_panel_l.json new file mode 100644 index 0000000..fe0bcaf --- /dev/null +++ b/camping-supplies-addon/camping_supplies_BP/blocks/tent_panel_l.json @@ -0,0 +1,46 @@ +{ + "format_version": "1.21.0", + "minecraft:block": { + "description": { + "identifier": "silverlabs:tent_panel_l", + "traits": { + "minecraft:placement_direction": { + "enabled_states": ["minecraft:cardinal_direction"] + } + } + }, + "components": { + "minecraft:destructible_by_mining": { "seconds_to_destroy": 0.4 }, + "minecraft:destructible_by_explosion": { "explosion_resistance": 1.0 }, + "minecraft:map_color": "#547A4E", + "minecraft:material_instances": { + "*": { + "texture": "tent_canvas", + "render_method": "alpha_test" + } + }, + "minecraft:light_dampening": 1, + "minecraft:geometry": "geometry.silverlabs.tent_panel_l" + }, + "permutations": [ + { + "condition": "query.block_state('minecraft:cardinal_direction') == 'south'", + "components": { + "minecraft:transformation": { "rotation": [0, 180, 0] } + } + }, + { + "condition": "query.block_state('minecraft:cardinal_direction') == 'east'", + "components": { + "minecraft:transformation": { "rotation": [0, 90, 0] } + } + }, + { + "condition": "query.block_state('minecraft:cardinal_direction') == 'west'", + "components": { + "minecraft:transformation": { "rotation": [0, 270, 0] } + } + } + ] + } +} diff --git a/camping-supplies-addon/camping_supplies_BP/blocks/tent_panel_r.json b/camping-supplies-addon/camping_supplies_BP/blocks/tent_panel_r.json new file mode 100644 index 0000000..c6b5304 --- /dev/null +++ b/camping-supplies-addon/camping_supplies_BP/blocks/tent_panel_r.json @@ -0,0 +1,46 @@ +{ + "format_version": "1.21.0", + "minecraft:block": { + "description": { + "identifier": "silverlabs:tent_panel_r", + "traits": { + "minecraft:placement_direction": { + "enabled_states": ["minecraft:cardinal_direction"] + } + } + }, + "components": { + "minecraft:destructible_by_mining": { "seconds_to_destroy": 0.4 }, + "minecraft:destructible_by_explosion": { "explosion_resistance": 1.0 }, + "minecraft:map_color": "#547A4E", + "minecraft:material_instances": { + "*": { + "texture": "tent_canvas", + "render_method": "alpha_test" + } + }, + "minecraft:light_dampening": 1, + "minecraft:geometry": "geometry.silverlabs.tent_panel_r" + }, + "permutations": [ + { + "condition": "query.block_state('minecraft:cardinal_direction') == 'south'", + "components": { + "minecraft:transformation": { "rotation": [0, 180, 0] } + } + }, + { + "condition": "query.block_state('minecraft:cardinal_direction') == 'east'", + "components": { + "minecraft:transformation": { "rotation": [0, 90, 0] } + } + }, + { + "condition": "query.block_state('minecraft:cardinal_direction') == 'west'", + "components": { + "minecraft:transformation": { "rotation": [0, 270, 0] } + } + } + ] + } +} diff --git a/camping-supplies-addon/camping_supplies_BP/scripts/main.js b/camping-supplies-addon/camping_supplies_BP/scripts/main.js index ce7d8f3..24fab31 100644 --- a/camping-supplies-addon/camping_supplies_BP/scripts/main.js +++ b/camping-supplies-addon/camping_supplies_BP/scripts/main.js @@ -3,10 +3,17 @@ import { world, system, ItemStack } from "@minecraft/server"; // ─── Constants ────────────────────────────────────────────── const TENT_ITEM = "silverlabs:tent"; const HAMMOCK_ITEM = "silverlabs:hammock"; -const TENT_BLOCK = "silverlabs:tent_canvas"; +const TENT_BLOCK = "silverlabs:tent_canvas"; // legacy cube — kept so old worlds load cleanly +const TENT_PANEL_L = "silverlabs:tent_panel_l"; +const TENT_PANEL_R = "silverlabs:tent_panel_r"; +const TENT_BLOCK_IDS = [TENT_BLOCK, TENT_PANEL_L, TENT_PANEL_R]; const HAMMOCK_BLOCK = "silverlabs:hammock_cloth"; const STATE_PROP = "camping_state_v1"; const HAMMOCK_TAG = "camping_hammock"; +const TENT_REST_PROP = "camping_tent_rest"; // per-player: "{x,y,z,dim,startTick}" +const SLEEP_TICK_INTERVAL = 20; // run sleep loop every 1s +const NIGHT_START = 12500; +const NIGHT_END = 23500; // ─── State ────────────────────────────────────────────────── // tents: key = "ox,oy,oz,dim" -> { ownerId, ownerName, cells: [[x,y,z]...] } @@ -51,6 +58,15 @@ function cardinalFacing(yaw) { return "north"; } +// Map our placement facing → block-state cardinal_direction the panel rotates to. +// The geometry's "default" (cardinal_direction = "north") has the slope's outer +// edge on -X and inner apex on +X with the depth running along Z. When the +// player faces north, that aligns with the world. When they face elsewhere, +// the placement_direction trait + permutations rotate the model to match. +function blockFacingFor(playerFacing) { + return playerFacing; // 1:1 — placement_direction handles the rotation +} + function vecsForFacing(facing) { switch (facing) { case "north": return { fx: 0, fz: -1, rx: 1, rz: 0 }; @@ -149,19 +165,32 @@ function tryPlaceTent(player) { } } + // A-frame layout: 3 long × 2 wide, single block tall. Each cross-section is a + // pair of slope panels meeting at the apex on the seam between the two columns. + // w=0 is the player's column → LEFT side of the tent (panel_l). + // w=1 is one step right → RIGHT side of the tent (panel_r). + const blockFacing = blockFacingFor(facing); const canvasCells = []; - for (let w = 0; w < 2; w++) { - // Back wall at l=0 (Y=0 and Y=1) - canvasCells.push({ x: ox + w * rx, y: oy + 0, z: oz + w * rz }); - canvasCells.push({ x: ox + w * rx, y: oy + 1, z: oz + w * rz }); - // Roof at l=1 and l=2 (Y=1) - canvasCells.push({ x: ox + 1 * fx + w * rx, y: oy + 1, z: oz + 1 * fz + w * rz }); - canvasCells.push({ x: ox + 2 * fx + w * rx, y: oy + 1, z: oz + 2 * fz + w * rz }); + for (let l = 0; l < 3; l++) { + canvasCells.push({ + x: ox + l * fx, + y: oy, + z: oz + l * fz, + block: TENT_PANEL_L, + }); + canvasCells.push({ + x: ox + l * fx + rx, + y: oy, + z: oz + l * fz + rz, + block: TENT_PANEL_R, + }); } for (const c of canvasCells) { try { - dim.runCommand(`setblock ${c.x} ${c.y} ${c.z} ${TENT_BLOCK}`); + dim.runCommand( + `setblock ${c.x} ${c.y} ${c.z} ${c.block} ["minecraft:cardinal_direction"="${blockFacing}"]` + ); } catch (_) {} } @@ -169,6 +198,7 @@ function tryPlaceTent(player) { state.tents[key] = { ownerId: player.id, ownerName: player.name, + facing, cells: canvasCells.map((c) => [c.x, c.y, c.z]), }; saveState(); @@ -297,10 +327,12 @@ try { world.beforeEvents.playerInteractWithBlock.subscribe((event) => { const block = event.block; if (!block) return; - if (block.typeId === TENT_BLOCK) { + if (TENT_BLOCK_IDS.includes(block.typeId)) { event.cancel = true; const player = event.player; - system.run(() => sleepInTent(player)); + const loc = { x: block.location.x, y: block.location.y, z: block.location.z }; + const dimId = block.dimension.id; + system.run(() => enterTentRest(player, loc, dimId)); } else if (block.typeId === HAMMOCK_BLOCK) { event.cancel = true; const player = event.player; @@ -312,18 +344,182 @@ try { console.warn(`[Camping] playerInteractWithBlock unavailable: ${e}`); } -function sleepInTent(player) { +// ─── Tent rest: vote-skip with mixed bed + tent sleepers ──────── +// Tracks which players are currently "resting" in a tent. A player counts as a +// tent sleeper as long as they stay near the panel they interacted with and +// don't sneak/move/disconnect/take damage. Vanilla bed sleepers are detected +// via player.isSleeping (true while a player is in a real bed). We compare the +// combined count against the world's playersSleepingPercentage gamerule and +// skip the night when the threshold is crossed. + +const tentRest = new Map(); // playerId → { x, y, z, dimId, startTick } + +function isNight(tod) { + return tod >= NIGHT_START && tod <= NIGHT_END; +} + +function enterTentRest(player, loc, dimId) { const tod = world.getTimeOfDay(); - if (tod < 12500 && tod > 500) { + if (!isNight(tod)) { player.sendMessage("§e[Camping] §7It's still daylight — nothing to sleep off."); return; } - player.addEffect("regeneration", 200, { amplifier: 1, showParticles: false }); - player.addEffect("saturation", 40, { amplifier: 0, showParticles: false }); - world.setTimeOfDay(0); - player.sendMessage("§a[Camping] §7You rest until dawn. §8Spawn point unchanged."); + if (tentRest.has(player.id)) { + leaveTentRest(player, "§7[Camping] You stop resting."); + return; + } + tentRest.set(player.id, { + x: loc.x, + y: loc.y, + z: loc.z, + dimId, + startTick: system.currentTick, + }); + // Cinematic fade so it reads like sleep instead of a status message. + try { + player.runCommand("camera @s fade time 0.4 1.5 0.6 color 0 0 0"); + } catch (_) {} + try { + player.onScreenDisplay.setTitle("§7Resting…", { + fadeInDuration: 8, + stayDuration: 60, + fadeOutDuration: 12, + subtitle: "§8Move, sneak, or take damage to wake.", + }); + } catch (_) {} + reportSleepProgress(player, /*onEnter*/ true); } +function leaveTentRest(player, msg) { + if (!tentRest.delete(player.id)) return; + if (msg && player) { + try { player.sendMessage(msg); } catch (_) {} + } +} + +function countSleepers() { + let bed = 0; + let tent = 0; + let online = 0; + for (const p of world.getAllPlayers()) { + online++; + // Vanilla bed sleep: Player.isSleeping is true while they're in a bed. + // Available since @minecraft/server 1.10+; guarded for safety. + let sleeping = false; + try { sleeping = !!p.isSleeping; } catch (_) {} + if (sleeping) bed++; + else if (tentRest.has(p.id)) tent++; + } + return { bed, tent, online, resting: bed + tent }; +} + +function getSleepThreshold() { + // playersSleepingPercentage is a percentage 0-100. 0 means any one player + // can skip night (vanilla quirk); 100 means everyone must sleep. + let pct = 100; + try { + const v = world.gameRules?.playersSleepingPercentage; + if (typeof v === "number") pct = v; + } catch (_) {} + // 0 in vanilla means "one is enough" — preserve that intent. + if (pct <= 0) return 1; + return pct; +} + +function requiredSleepers(online, pct) { + // Standard vanilla rounding: ceil(online * pct / 100), min 1. + return Math.max(1, Math.ceil((online * pct) / 100)); +} + +function reportSleepProgress(targetPlayer, onEnter = false) { + const { bed, tent, online, resting } = countSleepers(); + const pct = getSleepThreshold(); + const need = requiredSleepers(online, pct); + const remaining = Math.max(0, need - resting); + const msg = onEnter + ? `§a[Camping] §7You settle in. §f${resting}§7/§f${need}§7 resting (§f${tent}§7 tent + §f${bed}§7 bed)${remaining ? `. Need §f${remaining}§7 more.` : `.`}` + : `§7[Sleep] §f${resting}§7/§f${need}§7 resting (§f${tent}§7 tent + §f${bed}§7 bed)`; + if (onEnter && targetPlayer) { + try { targetPlayer.sendMessage(msg); } catch (_) {} + } else { + // Broadcast a subtle update to everyone currently resting. + for (const p of world.getAllPlayers()) { + if (tentRest.has(p.id) || (() => { try { return !!p.isSleeping; } catch (_) { return false; } })()) { + try { p.sendMessage(msg); } catch (_) {} + } + } + } +} + +function awardRestEffects(player) { + try { + player.addEffect("regeneration", 200, { amplifier: 1, showParticles: false }); + player.addEffect("saturation", 40, { amplifier: 0, showParticles: false }); + } catch (_) {} +} + +function executeNightSkip() { + // Snapshot tent sleepers before clearing, so we can give them rest perks. + const tentIds = [...tentRest.keys()]; + tentRest.clear(); + try { world.setTimeOfDay(0); } catch (_) {} + for (const p of world.getAllPlayers()) { + if (tentIds.includes(p.id) || p.isSleeping) { + awardRestEffects(p); + } + try { p.runCommand("camera @s clear"); } catch (_) {} + } + world.sendMessage("§6[Sleep] §7The camp rests. Dawn breaks."); +} + +// ─── Sleep loop: validate tent sleepers, check threshold ──────── +system.runInterval(() => { + if (tentRest.size === 0) return; + + // Validate each tent sleeper still meets the criteria. + for (const [pid, rest] of [...tentRest.entries()]) { + const player = world.getAllPlayers().find((p) => p.id === pid); + if (!player) { + tentRest.delete(pid); + continue; + } + if (player.dimension.id !== rest.dimId) { + leaveTentRest(player, "§7[Camping] You wandered out of camp."); + continue; + } + const dx = player.location.x - (rest.x + 0.5); + const dy = player.location.y - rest.y; + const dz = player.location.z - (rest.z + 0.5); + if (dx * dx + dz * dz > 4 || Math.abs(dy) > 2) { + leaveTentRest(player, "§7[Camping] You wandered out of camp."); + continue; + } + if (player.isSneaking) { + leaveTentRest(player, "§7[Camping] You climb out of the tent."); + continue; + } + // Sleepers don't get scared off by mobs but a hit cancels rest: + // (handled implicitly — damage breaks the camera fade and the player is + // expected to sneak out; we don't have a public hurt event hook here) + awardRestEffects(player); + } + + if (tentRest.size === 0) return; + const tod = world.getTimeOfDay(); + if (!isNight(tod)) { + // Sun came up some other way — clear resters quietly. + tentRest.clear(); + return; + } + + const { online, resting } = countSleepers(); + const pct = getSleepThreshold(); + const need = requiredSleepers(online, pct); + if (resting >= need) { + executeNightSkip(); + } +}, SLEEP_TICK_INTERVAL); + const HAMMOCK_ANCHOR_PROP = "camping_hammock_anchor"; function toggleHammock(player, loc) { @@ -437,12 +633,13 @@ try { const block = event.block; if (!block) return; const id = block.typeId; - if (id !== TENT_BLOCK && id !== HAMMOCK_BLOCK) return; + const isTent = TENT_BLOCK_IDS.includes(id); + if (!isTent && id !== HAMMOCK_BLOCK) return; event.cancel = true; const loc = { x: block.location.x, y: block.location.y, z: block.location.z }; const dimId = block.dimension.id; const player = event.player; - if (id === TENT_BLOCK) { + if (isTent) { system.run(() => dismantleTentAt(loc, dimId, player)); } else { system.run(() => dismantleHammockAt(loc, dimId, player)); diff --git a/camping-supplies-addon/camping_supplies_RP/models/blocks/tent_panel_l.geo.json b/camping-supplies-addon/camping_supplies_RP/models/blocks/tent_panel_l.geo.json new file mode 100644 index 0000000..3281145 --- /dev/null +++ b/camping-supplies-addon/camping_supplies_RP/models/blocks/tent_panel_l.geo.json @@ -0,0 +1,31 @@ +{ + "format_version": "1.12.0", + "minecraft:geometry": [ + { + "description": { + "identifier": "geometry.silverlabs.tent_panel_l", + "texture_width": 16, + "texture_height": 16, + "visible_bounds_width": 2, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 0.5, 0] + }, + "bones": [ + { + "name": "panel", + "pivot": [0, 0, 0], + "cubes": [ + { "origin": [-8, 0, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [-6, 2, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [-4, 4, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [-2, 6, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [ 0, 8, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [ 2, 10, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [ 4, 12, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [ 6, 14, -8], "size": [2, 2, 16], "uv": [0, 0] } + ] + } + ] + } + ] +} diff --git a/camping-supplies-addon/camping_supplies_RP/models/blocks/tent_panel_r.geo.json b/camping-supplies-addon/camping_supplies_RP/models/blocks/tent_panel_r.geo.json new file mode 100644 index 0000000..01cacc5 --- /dev/null +++ b/camping-supplies-addon/camping_supplies_RP/models/blocks/tent_panel_r.geo.json @@ -0,0 +1,31 @@ +{ + "format_version": "1.12.0", + "minecraft:geometry": [ + { + "description": { + "identifier": "geometry.silverlabs.tent_panel_r", + "texture_width": 16, + "texture_height": 16, + "visible_bounds_width": 2, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 0.5, 0] + }, + "bones": [ + { + "name": "panel", + "pivot": [0, 0, 0], + "cubes": [ + { "origin": [ 6, 0, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [ 4, 2, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [ 2, 4, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [ 0, 6, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [-2, 8, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [-4, 10, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [-6, 12, -8], "size": [2, 2, 16], "uv": [0, 0] }, + { "origin": [-8, 14, -8], "size": [2, 2, 16], "uv": [0, 0] } + ] + } + ] + } + ] +} diff --git a/camping-supplies-addon/camping_supplies_RP/textures/blocks/tent_canvas.png b/camping-supplies-addon/camping_supplies_RP/textures/blocks/tent_canvas.png index 5819621..eb9a78e 100644 Binary files a/camping-supplies-addon/camping_supplies_RP/textures/blocks/tent_canvas.png and b/camping-supplies-addon/camping_supplies_RP/textures/blocks/tent_canvas.png differ diff --git a/docker-compose.yml b/docker-compose.yml index 63af563..7ecffda 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,10 @@ services: - ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP - ./camping-supplies-addon/camping_supplies_BP:/data/behavior_packs/camping_supplies_BP - ./camping-supplies-addon/camping_supplies_RP:/data/resource_packs/camping_supplies_RP + - ./dynamite-addon/dynamite_BP:/data/behavior_packs/dynamite_BP + - ./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 restart: unless-stopped networks: - mc-network @@ -63,6 +67,10 @@ services: - ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP - ./camping-supplies-addon/camping_supplies_BP:/data/behavior_packs/camping_supplies_BP - ./camping-supplies-addon/camping_supplies_RP:/data/resource_packs/camping_supplies_RP + - ./dynamite-addon/dynamite_BP:/data/behavior_packs/dynamite_BP + - ./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 restart: unless-stopped networks: - mc-network @@ -97,6 +105,10 @@ services: - ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP - ./camping-supplies-addon/camping_supplies_BP:/data/behavior_packs/camping_supplies_BP - ./camping-supplies-addon/camping_supplies_RP:/data/resource_packs/camping_supplies_RP + - ./dynamite-addon/dynamite_BP:/data/behavior_packs/dynamite_BP + - ./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 - ./village-evolution-addon/enabled_packs.json:/data/config/default/enabled_packs.json restart: unless-stopped networks: @@ -132,6 +144,10 @@ services: - ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP - ./camping-supplies-addon/camping_supplies_BP:/data/behavior_packs/camping_supplies_BP - ./camping-supplies-addon/camping_supplies_RP:/data/resource_packs/camping_supplies_RP + - ./dynamite-addon/dynamite_BP:/data/behavior_packs/dynamite_BP + - ./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 - ./village-evolution-addon/enabled_packs.json:/data/config/default/enabled_packs.json restart: unless-stopped networks: diff --git a/dynamite-addon/dynamite_BP/entities/thrown_banger.json b/dynamite-addon/dynamite_BP/entities/thrown_banger.json new file mode 100644 index 0000000..f10c940 --- /dev/null +++ b/dynamite-addon/dynamite_BP/entities/thrown_banger.json @@ -0,0 +1,47 @@ +{ + "format_version": "1.21.0", + "minecraft:entity": { + "description": { + "identifier": "silverlabs:thrown_banger", + "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": 2, + "knockback": true, + "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, + "gravity": 0.05, + "inertia": 0.99, + "liquid_inertia": 0.6, + "anchor": 1, + "offset": [0, -0.1, 0], + "should_bounce": false, + "hit_sound": "random.fuse" + } + } + } +} diff --git a/dynamite-addon/dynamite_BP/entities/thrown_bundle.json b/dynamite-addon/dynamite_BP/entities/thrown_bundle.json new file mode 100644 index 0000000..adc37c4 --- /dev/null +++ b/dynamite-addon/dynamite_BP/entities/thrown_bundle.json @@ -0,0 +1,66 @@ +{ + "format_version": "1.21.0", + "minecraft:entity": { + "description": { + "identifier": "silverlabs:thrown_bundle", + "is_spawnable": false, + "is_summonable": true, + "is_experimental": false + }, + "component_groups": { + "silverlabs:detonate": { + "minecraft:explode": { + "fuse_length": 0, + "fuse_lit": true, + "power": 3.0, + "causes_fire": false, + "breaks_blocks": true, + "max_resistance": 12, + "destroy_affected_by_griefing": true + } + } + }, + "components": { + "minecraft:collision_box": { + "width": 0.3, + "height": 0.3 + }, + "minecraft:physics": {}, + "minecraft:pushable": { + "is_pushable": false, + "is_pushable_by_piston": false + }, + "minecraft:projectile": { + "on_hit": { + "impact_damage": { + "damage": 6, + "knockback": true, + "destroy_on_hit": false, + "semi_random_diff_damage": false + }, + "definition_event": { + "event_trigger": { + "event": "silverlabs:detonate", + "target": "self" + } + } + }, + "power": 1.3, + "gravity": 0.06, + "inertia": 0.99, + "liquid_inertia": 0.6, + "anchor": 1, + "offset": [0, -0.1, 0], + "should_bounce": false, + "hit_sound": "random.fuse" + } + }, + "events": { + "silverlabs:detonate": { + "add": { + "component_groups": ["silverlabs:detonate"] + } + } + } + } +} diff --git a/dynamite-addon/dynamite_BP/entities/thrown_dynamite.json b/dynamite-addon/dynamite_BP/entities/thrown_dynamite.json new file mode 100644 index 0000000..830af91 --- /dev/null +++ b/dynamite-addon/dynamite_BP/entities/thrown_dynamite.json @@ -0,0 +1,66 @@ +{ + "format_version": "1.21.0", + "minecraft:entity": { + "description": { + "identifier": "silverlabs:thrown_dynamite", + "is_spawnable": false, + "is_summonable": true, + "is_experimental": false + }, + "component_groups": { + "silverlabs:detonate": { + "minecraft:explode": { + "fuse_length": 0, + "fuse_lit": true, + "power": 1.5, + "causes_fire": false, + "breaks_blocks": true, + "max_resistance": 4, + "destroy_affected_by_griefing": true + } + } + }, + "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": 4, + "knockback": true, + "destroy_on_hit": false, + "semi_random_diff_damage": false + }, + "definition_event": { + "event_trigger": { + "event": "silverlabs:detonate", + "target": "self" + } + } + }, + "power": 1.4, + "gravity": 0.05, + "inertia": 0.99, + "liquid_inertia": 0.6, + "anchor": 1, + "offset": [0, -0.1, 0], + "should_bounce": false, + "hit_sound": "random.fuse" + } + }, + "events": { + "silverlabs:detonate": { + "add": { + "component_groups": ["silverlabs:detonate"] + } + } + } + } +} diff --git a/dynamite-addon/dynamite_BP/items/banger.json b/dynamite-addon/dynamite_BP/items/banger.json new file mode 100644 index 0000000..4cb0438 --- /dev/null +++ b/dynamite-addon/dynamite_BP/items/banger.json @@ -0,0 +1,27 @@ +{ + "format_version": "1.21.0", + "minecraft:item": { + "description": { + "identifier": "silverlabs:banger", + "menu_category": { + "category": "equipment" + } + }, + "components": { + "minecraft:icon": "banger", + "minecraft:display_name": { + "value": "Banger" + }, + "minecraft:max_stack_size": 16, + "minecraft:hand_equipped": true, + "minecraft:throwable": { + "do_swing_animation": true, + "max_draw_duration": 0, + "scale_power_by_draw_duration": false + }, + "minecraft:projectile": { + "projectile_entity": "silverlabs:thrown_banger" + } + } + } +} diff --git a/dynamite-addon/dynamite_BP/items/dynamite_bundle.json b/dynamite-addon/dynamite_BP/items/dynamite_bundle.json new file mode 100644 index 0000000..f06e6ad --- /dev/null +++ b/dynamite-addon/dynamite_BP/items/dynamite_bundle.json @@ -0,0 +1,27 @@ +{ + "format_version": "1.21.0", + "minecraft:item": { + "description": { + "identifier": "silverlabs:dynamite_bundle", + "menu_category": { + "category": "equipment" + } + }, + "components": { + "minecraft:icon": "dynamite_bundle", + "minecraft:display_name": { + "value": "Bundle of Dynamite" + }, + "minecraft:max_stack_size": 16, + "minecraft:hand_equipped": true, + "minecraft:throwable": { + "do_swing_animation": true, + "max_draw_duration": 0, + "scale_power_by_draw_duration": false + }, + "minecraft:projectile": { + "projectile_entity": "silverlabs:thrown_bundle" + } + } + } +} diff --git a/dynamite-addon/dynamite_BP/items/dynamite_stick.json b/dynamite-addon/dynamite_BP/items/dynamite_stick.json new file mode 100644 index 0000000..4deffe8 --- /dev/null +++ b/dynamite-addon/dynamite_BP/items/dynamite_stick.json @@ -0,0 +1,27 @@ +{ + "format_version": "1.21.0", + "minecraft:item": { + "description": { + "identifier": "silverlabs:dynamite_stick", + "menu_category": { + "category": "equipment" + } + }, + "components": { + "minecraft:icon": "dynamite_stick", + "minecraft:display_name": { + "value": "Stick of Dynamite" + }, + "minecraft:max_stack_size": 16, + "minecraft:hand_equipped": true, + "minecraft:throwable": { + "do_swing_animation": true, + "max_draw_duration": 0, + "scale_power_by_draw_duration": false + }, + "minecraft:projectile": { + "projectile_entity": "silverlabs:thrown_dynamite" + } + } + } +} diff --git a/dynamite-addon/dynamite_BP/manifest.json b/dynamite-addon/dynamite_BP/manifest.json new file mode 100644 index 0000000..a8f6afe --- /dev/null +++ b/dynamite-addon/dynamite_BP/manifest.json @@ -0,0 +1,23 @@ +{ + "format_version": 2, + "header": { + "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] + }, + "modules": [ + { + "type": "data", + "uuid": "1354002c-fdd5-4f7e-b89b-f5dd2c38799c", + "version": [1, 0, 0] + } + ], + "dependencies": [ + { + "uuid": "a18bdde1-53f8-49aa-b06d-6f0ec6c45b46", + "version": [1, 0, 0] + } + ] +} diff --git a/dynamite-addon/dynamite_BP/pack_icon.png b/dynamite-addon/dynamite_BP/pack_icon.png new file mode 100644 index 0000000..bee6b79 Binary files /dev/null and b/dynamite-addon/dynamite_BP/pack_icon.png differ diff --git a/dynamite-addon/dynamite_BP/recipes/banger.json b/dynamite-addon/dynamite_BP/recipes/banger.json new file mode 100644 index 0000000..2f1a9e9 --- /dev/null +++ b/dynamite-addon/dynamite_BP/recipes/banger.json @@ -0,0 +1,25 @@ +{ + "format_version": "1.21.0", + "minecraft:recipe_shaped": { + "description": { + "identifier": "silverlabs:banger_recipe" + }, + "tags": ["crafting_table"], + "unlock": [ + { "item": "minecraft:gunpowder" } + ], + "pattern": [ + "SP", + "G " + ], + "key": { + "S": { "item": "minecraft:string" }, + "P": { "item": "minecraft:paper" }, + "G": { "item": "minecraft:gunpowder" } + }, + "result": { + "item": "silverlabs:banger", + "count": 4 + } + } +} diff --git a/dynamite-addon/dynamite_BP/recipes/dynamite_bundle.json b/dynamite-addon/dynamite_BP/recipes/dynamite_bundle.json new file mode 100644 index 0000000..369b417 --- /dev/null +++ b/dynamite-addon/dynamite_BP/recipes/dynamite_bundle.json @@ -0,0 +1,24 @@ +{ + "format_version": "1.21.0", + "minecraft:recipe_shaped": { + "description": { + "identifier": "silverlabs:dynamite_bundle_recipe" + }, + "tags": ["crafting_table"], + "unlock": [ + { "item": "silverlabs:dynamite_stick" } + ], + "pattern": [ + "DDD", + " S " + ], + "key": { + "D": { "item": "silverlabs:dynamite_stick" }, + "S": { "item": "minecraft:string" } + }, + "result": { + "item": "silverlabs:dynamite_bundle", + "count": 1 + } + } +} diff --git a/dynamite-addon/dynamite_BP/recipes/dynamite_stick.json b/dynamite-addon/dynamite_BP/recipes/dynamite_stick.json new file mode 100644 index 0000000..62bcd84 --- /dev/null +++ b/dynamite-addon/dynamite_BP/recipes/dynamite_stick.json @@ -0,0 +1,25 @@ +{ + "format_version": "1.21.0", + "minecraft:recipe_shaped": { + "description": { + "identifier": "silverlabs:dynamite_stick_recipe" + }, + "tags": ["crafting_table"], + "unlock": [ + { "item": "silverlabs:banger" } + ], + "pattern": [ + "BB", + "RT" + ], + "key": { + "B": { "item": "silverlabs:banger" }, + "R": { "item": "minecraft:redstone" }, + "T": { "item": "minecraft:stick" } + }, + "result": { + "item": "silverlabs:dynamite_stick", + "count": 2 + } + } +} diff --git a/dynamite-addon/dynamite_RP/manifest.json b/dynamite-addon/dynamite_RP/manifest.json new file mode 100644 index 0000000..c0ec166 --- /dev/null +++ b/dynamite-addon/dynamite_RP/manifest.json @@ -0,0 +1,17 @@ +{ + "format_version": 2, + "header": { + "name": "Dynamite Resources", + "description": "Textures and language for the Dynamite addon.", + "uuid": "a18bdde1-53f8-49aa-b06d-6f0ec6c45b46", + "version": [1, 0, 0], + "min_engine_version": [1, 21, 0] + }, + "modules": [ + { + "type": "resources", + "uuid": "587281f2-f159-4ad9-85a6-d20ff4899717", + "version": [1, 0, 0] + } + ] +} diff --git a/dynamite-addon/dynamite_RP/pack_icon.png b/dynamite-addon/dynamite_RP/pack_icon.png new file mode 100644 index 0000000..bee6b79 Binary files /dev/null and b/dynamite-addon/dynamite_RP/pack_icon.png differ diff --git a/dynamite-addon/dynamite_RP/texts/en_US.lang b/dynamite-addon/dynamite_RP/texts/en_US.lang new file mode 100644 index 0000000..77d3c1b --- /dev/null +++ b/dynamite-addon/dynamite_RP/texts/en_US.lang @@ -0,0 +1,6 @@ +item.silverlabs:banger=Banger +item.silverlabs:dynamite_stick=Stick of Dynamite +item.silverlabs:dynamite_bundle=Bundle of Dynamite +entity.silverlabs:thrown_banger.name=Banger +entity.silverlabs:thrown_dynamite.name=Dynamite +entity.silverlabs:thrown_bundle.name=Dynamite Bundle diff --git a/dynamite-addon/dynamite_RP/texts/languages.json b/dynamite-addon/dynamite_RP/texts/languages.json new file mode 100644 index 0000000..818bf4c --- /dev/null +++ b/dynamite-addon/dynamite_RP/texts/languages.json @@ -0,0 +1,3 @@ +[ + "en_US" +] diff --git a/dynamite-addon/dynamite_RP/textures/item_texture.json b/dynamite-addon/dynamite_RP/textures/item_texture.json new file mode 100644 index 0000000..488e21e --- /dev/null +++ b/dynamite-addon/dynamite_RP/textures/item_texture.json @@ -0,0 +1,15 @@ +{ + "resource_pack_name": "dynamite_RP", + "texture_name": "atlas.items", + "texture_data": { + "banger": { + "textures": "textures/items/banger" + }, + "dynamite_stick": { + "textures": "textures/items/dynamite_stick" + }, + "dynamite_bundle": { + "textures": "textures/items/dynamite_bundle" + } + } +} diff --git a/dynamite-addon/dynamite_RP/textures/items/banger.png b/dynamite-addon/dynamite_RP/textures/items/banger.png new file mode 100644 index 0000000..d6fefd4 Binary files /dev/null and b/dynamite-addon/dynamite_RP/textures/items/banger.png differ diff --git a/dynamite-addon/dynamite_RP/textures/items/dynamite_bundle.png b/dynamite-addon/dynamite_RP/textures/items/dynamite_bundle.png new file mode 100644 index 0000000..d181587 Binary files /dev/null and b/dynamite-addon/dynamite_RP/textures/items/dynamite_bundle.png differ diff --git a/dynamite-addon/dynamite_RP/textures/items/dynamite_stick.png b/dynamite-addon/dynamite_RP/textures/items/dynamite_stick.png new file mode 100644 index 0000000..3e73c0d Binary files /dev/null and b/dynamite-addon/dynamite_RP/textures/items/dynamite_stick.png differ diff --git a/lobby-addon/lobby_transfer_BP/blocks/portal_field.json b/lobby-addon/lobby_transfer_BP/blocks/portal_field.json new file mode 100644 index 0000000..9892aff --- /dev/null +++ b/lobby-addon/lobby_transfer_BP/blocks/portal_field.json @@ -0,0 +1,33 @@ +{ + "format_version": "1.21.0", + "minecraft:block": { + "description": { + "identifier": "silverlabs:portal_field", + "menu_category": { + "category": "items", + "group": "itemGroup.name.decorations" + } + }, + "components": { + "minecraft:destructible_by_mining": { "seconds_to_destroy": 0 }, + "minecraft:destructible_by_explosion": { "explosion_resistance": 1200.0 }, + "minecraft:light_emission": 11, + "minecraft:light_dampening": 0, + "minecraft:collision_box": false, + "minecraft:selection_box": { + "origin": [-7, 0, -7], + "size": [14, 16, 14] + }, + "minecraft:map_color": "#7B27FF", + "minecraft:material_instances": { + "*": { + "texture": "portal_field", + "render_method": "blend", + "ambient_occlusion": false, + "face_dimming": false + } + }, + "minecraft:geometry": "geometry.silverlabs.portal_field" + } + } +} diff --git a/lobby-addon/lobby_transfer_BP/entities/portal_label.json b/lobby-addon/lobby_transfer_BP/entities/portal_label.json new file mode 100644 index 0000000..aeb63f5 --- /dev/null +++ b/lobby-addon/lobby_transfer_BP/entities/portal_label.json @@ -0,0 +1,26 @@ +{ + "format_version": "1.21.0", + "minecraft:entity": { + "description": { + "identifier": "silverlabs:portal_label", + "is_spawnable": false, + "is_summonable": true, + "is_experimental": false + }, + "components": { + "minecraft:nameable": {}, + "minecraft:type_family": { "family": ["portal_label", "inanimate"] }, + "minecraft:health": { "value": 1, "max": 1 }, + "minecraft:physics": { "has_collision": false, "has_gravity": false }, + "minecraft:pushable": { "is_pushable": false, "is_pushable_by_piston": false }, + "minecraft:damage_sensor": { + "triggers": [{ "deals_damage": false }] + }, + "minecraft:fire_immune": {}, + "minecraft:knockback_resistance": { "value": 1.0, "max": 1.0 }, + "minecraft:persistent": {}, + "minecraft:movement": { "value": 0 }, + "minecraft:collision_box": { "width": 0.01, "height": 0.01 } + } + } +} diff --git a/lobby-addon/lobby_transfer_BP/scripts/main.js b/lobby-addon/lobby_transfer_BP/scripts/main.js index fd3ce8c..26d1951 100644 --- a/lobby-addon/lobby_transfer_BP/scripts/main.js +++ b/lobby-addon/lobby_transfer_BP/scripts/main.js @@ -1,19 +1,26 @@ import { world, system } from "@minecraft/server"; import { transferPlayer } from "@minecraft/server-admin"; -// Portal block → transfer target mapping (custom blocks — priority detection) +// Portal frame block → transfer target mapping. The frame is the floor block; +// the walk-through field column above it inherits the destination by looking down. const PORTAL_BLOCKS = { "silverlabs:portal_jamie": { name: "Jamie's World", host: "10.0.0.247", port: 19133, color: "§a" }, "silverlabs:portal_lyla": { name: "Lyla's World", host: "10.0.0.247", port: 19134, color: "§d" }, "silverlabs:portal_mya": { name: "Mya's World", host: "10.0.0.247", port: 19135, color: "§b" }, }; -// Coordinate-based portal zones (fallback detection) +const PORTAL_FIELD_BLOCK = "silverlabs:portal_field"; +const PORTAL_LABEL_ENTITY = "silverlabs:portal_label"; + +// Coordinate-based portal zones (fallback detection + auto-spawn anchors). +// `frame_y` is the floor block where the destination frame sits; the field +// blocks are placed at frame_y+1 and frame_y+2 (2-tall walk-through column), +// and the floating label entity sits at frame_y+3.5. const PORTAL_ZONES = [ - { 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" }, + { name: "Jamie's World", x: 436, y: 66, z: -296, frame_y: 66, host: "10.0.0.247", port: 19133, color: "§a" }, + { name: "Lyla's World", x: 462, y: 65, z: -322, frame_y: 65, host: "10.0.0.247", port: 19134, color: "§d" }, + { name: "Lyla's World", x: 474, y: 65, z: -281, frame_y: 65, host: "10.0.0.247", port: 19134, color: "§d", label_suffix: " (Super Kitties)" }, + { name: "Mya's World", x: 488, y: 66, z: -296, frame_y: 66, host: "10.0.0.247", port: 19135, color: "§b" }, ]; const PORTAL_RADIUS_X = 2.5; @@ -38,13 +45,26 @@ world.afterEvents.playerSpawn.subscribe((event) => { function checkBlockPortal(player) { const pos = player.location; const dimension = player.dimension; - const blockAtFeet = dimension.getBlock({ x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z) }); - const blockBelow = dimension.getBlock({ x: Math.floor(pos.x), y: Math.floor(pos.y) - 1, z: Math.floor(pos.z) }); + const fx = Math.floor(pos.x); + const fy = Math.floor(pos.y); + const fz = Math.floor(pos.z); + const blockAtFeet = dimension.getBlock({ x: fx, y: fy, z: fz }); + const blockHead = dimension.getBlock({ x: fx, y: fy + 1, z: fz }); + const blockBelow = dimension.getBlock({ x: fx, y: fy - 1, z: fz }); - const feetId = blockAtFeet?.typeId; - const belowId = blockBelow?.typeId; + // Walk-through portal field: scan downward for the destination frame block. + for (const b of [blockAtFeet, blockHead]) { + if (b?.typeId === PORTAL_FIELD_BLOCK) { + for (let dy = 1; dy <= 4; dy++) { + const probe = dimension.getBlock({ x: fx, y: fy - dy, z: fz }); + const dest = probe && PORTAL_BLOCKS[probe.typeId]; + if (dest) return dest; + } + } + } - return PORTAL_BLOCKS[feetId] || PORTAL_BLOCKS[belowId] || null; + // Legacy: standing on a frame block directly. + return PORTAL_BLOCKS[blockAtFeet?.typeId] || PORTAL_BLOCKS[blockBelow?.typeId] || null; } /** @@ -111,43 +131,65 @@ world.afterEvents.playerLeave.subscribe((event) => { spawnTicks.delete(event.playerId); }); -// ─── Place signs above each portal on load ────────────────────── +// ─── Walk-through portal fields + floating destination labels ── +// For each known portal zone, place a 2-block-tall column of `portal_field` +// blocks (no collision, translucent, light-emitting) directly above the frame +// block, and summon an invisible `portal_label` entity above with the +// destination world's name visible as a floating tag. -function placePortalSigns() { - const overworld = world.getDimension("overworld"); - const signs = [ - { 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) { +function ensurePortalField(overworld, x, fy, z) { + for (const dy of [1, 2]) { try { - 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 - } - } - - system.runTimeout(() => { - for (const sign of signs) { - try { - const block = overworld.getBlock({ x: sign.x, y: sign.y, z: sign.z }); - if (!block) continue; - const signComponent = block.getComponent("minecraft:sign"); - if (!signComponent) continue; - signComponent.setText(`${sign.color}${sign.name}\n${sign.color}World\n§7▼ Step in ▼`); - } catch (e) { - // Non-fatal + const b = overworld.getBlock({ x, y: fy + dy, z }); + if (!b) continue; + if (b.typeId !== PORTAL_FIELD_BLOCK) { + overworld.runCommand(`setblock ${x} ${fy + dy} ${z} ${PORTAL_FIELD_BLOCK}`); } - } - }, 10); + } catch (_) { /* chunks may be unloaded; retried next boot */ } + } } -system.runTimeout(() => { - placePortalSigns(); -}, 40); +function ensurePortalLabel(overworld, zone) { + const labelY = zone.frame_y + 3.5; + // Check if a label already exists nearby; replace its tag if outdated. + let existing = null; + try { + const matches = overworld.getEntities({ + type: PORTAL_LABEL_ENTITY, + location: { x: zone.x + 0.5, y: labelY, z: zone.z + 0.5 }, + maxDistance: 1.5, + }); + existing = matches[0] || null; + } catch (_) {} + + const tag = `${zone.color}${zone.name}${zone.label_suffix || ""}`; + if (existing) { + try { existing.nameTag = tag; } catch (_) {} + return; + } + try { + const entity = overworld.spawnEntity( + PORTAL_LABEL_ENTITY, + { x: zone.x + 0.5, y: labelY, z: zone.z + 0.5 } + ); + if (entity) { + try { entity.nameTag = tag; } catch (_) {} + } + } catch (_) { /* chunks may be unloaded */ } +} + +function placePortalDressing() { + const overworld = world.getDimension("overworld"); + for (const zone of PORTAL_ZONES) { + if (zone.frame_y === undefined) continue; + ensurePortalField(overworld, zone.x, zone.frame_y, zone.z); + ensurePortalLabel(overworld, zone); + } +} + +// Initial place + periodic re-ensure (handles chunk loading after first attempt). +system.runTimeout(() => placePortalDressing(), 40); +system.runInterval(() => placePortalDressing(), 600); // every 30s system.run(() => { world.sendMessage("§6[Hub] §7Portal transfer system loaded!"); diff --git a/lobby-addon/lobby_transfer_RP/blocks.json b/lobby-addon/lobby_transfer_RP/blocks.json index d3b9401..ba31c47 100644 --- a/lobby-addon/lobby_transfer_RP/blocks.json +++ b/lobby-addon/lobby_transfer_RP/blocks.json @@ -3,5 +3,6 @@ "silverlabs:portal_frame": { "sound": "stone" }, "silverlabs:portal_jamie": { "sound": "stone" }, "silverlabs:portal_lyla": { "sound": "stone" }, - "silverlabs:portal_mya": { "sound": "stone" } + "silverlabs:portal_mya": { "sound": "stone" }, + "silverlabs:portal_field": { "sound": "glass" } } diff --git a/lobby-addon/lobby_transfer_RP/entity/portal_label.json b/lobby-addon/lobby_transfer_RP/entity/portal_label.json new file mode 100644 index 0000000..f6e1f00 --- /dev/null +++ b/lobby-addon/lobby_transfer_RP/entity/portal_label.json @@ -0,0 +1,12 @@ +{ + "format_version": "1.10.0", + "minecraft:client_entity": { + "description": { + "identifier": "silverlabs:portal_label", + "materials": { "default": "entity_alphatest" }, + "textures": { "default": "textures/entity/portal_label" }, + "geometry": { "default": "geometry.silverlabs.portal_label" }, + "render_controllers": ["controller.render.default"] + } + } +} diff --git a/lobby-addon/lobby_transfer_RP/models/blocks/portal_field.geo.json b/lobby-addon/lobby_transfer_RP/models/blocks/portal_field.geo.json new file mode 100644 index 0000000..b91d2d6 --- /dev/null +++ b/lobby-addon/lobby_transfer_RP/models/blocks/portal_field.geo.json @@ -0,0 +1,25 @@ +{ + "format_version": "1.12.0", + "minecraft:geometry": [ + { + "description": { + "identifier": "geometry.silverlabs.portal_field", + "texture_width": 16, + "texture_height": 16, + "visible_bounds_width": 1.5, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 0.5, 0] + }, + "bones": [ + { + "name": "field", + "pivot": [0, 0, 0], + "cubes": [ + { "origin": [-7, 0, -1], "size": [14, 16, 2], "uv": [0, 0] }, + { "origin": [-1, 0, -7], "size": [2, 16, 14], "uv": [0, 0] } + ] + } + ] + } + ] +} diff --git a/lobby-addon/lobby_transfer_RP/models/entity/portal_label.geo.json b/lobby-addon/lobby_transfer_RP/models/entity/portal_label.geo.json new file mode 100644 index 0000000..1bc72bf --- /dev/null +++ b/lobby-addon/lobby_transfer_RP/models/entity/portal_label.geo.json @@ -0,0 +1,21 @@ +{ + "format_version": "1.12.0", + "minecraft:geometry": [ + { + "description": { + "identifier": "geometry.silverlabs.portal_label", + "texture_width": 1, + "texture_height": 1, + "visible_bounds_width": 0, + "visible_bounds_height": 0, + "visible_bounds_offset": [0, 0, 0] + }, + "bones": [ + { + "name": "root", + "pivot": [0, 0, 0] + } + ] + } + ] +} diff --git a/lobby-addon/lobby_transfer_RP/textures/blocks/portal_field.png b/lobby-addon/lobby_transfer_RP/textures/blocks/portal_field.png new file mode 100644 index 0000000..e4b45c7 Binary files /dev/null and b/lobby-addon/lobby_transfer_RP/textures/blocks/portal_field.png differ diff --git a/lobby-addon/lobby_transfer_RP/textures/entity/portal_label.png b/lobby-addon/lobby_transfer_RP/textures/entity/portal_label.png new file mode 100644 index 0000000..145a07d Binary files /dev/null and b/lobby-addon/lobby_transfer_RP/textures/entity/portal_label.png differ diff --git a/lobby-addon/lobby_transfer_RP/textures/terrain_texture.json b/lobby-addon/lobby_transfer_RP/textures/terrain_texture.json index fd06532..0256dc3 100644 --- a/lobby-addon/lobby_transfer_RP/textures/terrain_texture.json +++ b/lobby-addon/lobby_transfer_RP/textures/terrain_texture.json @@ -15,6 +15,9 @@ }, "portal_mya": { "textures": "textures/blocks/prismarine_bricks" + }, + "portal_field": { + "textures": "textures/blocks/portal_field" } } } diff --git a/postal-service-addon/postal_service_RP/textures/blocks/mailbox.png b/postal-service-addon/postal_service_RP/textures/blocks/mailbox.png index d2066ab..06b83f5 100644 Binary files a/postal-service-addon/postal_service_RP/textures/blocks/mailbox.png and b/postal-service-addon/postal_service_RP/textures/blocks/mailbox.png differ diff --git a/postal-service-addon/postal_service_RP/textures/blocks/post_office.png b/postal-service-addon/postal_service_RP/textures/blocks/post_office.png index 3d5fe2d..31a0c01 100644 Binary files a/postal-service-addon/postal_service_RP/textures/blocks/post_office.png and b/postal-service-addon/postal_service_RP/textures/blocks/post_office.png differ diff --git a/scripts/build-art-catalog.py b/scripts/build-art-catalog.py new file mode 100644 index 0000000..e15916f --- /dev/null +++ b/scripts/build-art-catalog.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +Build a contact sheet of every texture under art/ — each PNG upscaled +with nearest-neighbour (so pixels stay crisp) and labelled with its +relative path. Output: art/CATALOG.png + +Run from repo root: + python3 scripts/build-art-catalog.py +""" +import os +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont + +REPO = Path(__file__).resolve().parent.parent +ART = REPO / "art" +OUT = ART / "CATALOG.png" + +TILE = 128 # upscaled texture size +COLS = 6 # tiles per row +PAD_X = 20 # horizontal padding around each tile +PAD_Y = 60 # vertical padding (extra room for label below) +LABEL_PX = 14 # label font size +BG = (30, 30, 46) # dark background +FG = (230, 230, 240) +SECTION_BG = (50, 60, 90) + + +def load_font(size): + for cand in ( + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + "/mnt/c/Windows/Fonts/consola.ttf", + "/mnt/c/Windows/Fonts/arial.ttf", + ): + if os.path.exists(cand): + return ImageFont.truetype(cand, size) + return ImageFont.load_default() + + +def collect(): + """Return {pack_name: [(relative_path, abs_path), ...]} grouped & sorted.""" + groups = {} + for png in sorted(ART.rglob("*.png")): + if png.name == "CATALOG.png": + continue + rel = png.relative_to(ART) + pack = rel.parts[0] + groups.setdefault(pack, []).append((rel, png)) + return groups + + +def upscale(png_path, size): + img = Image.open(png_path).convert("RGBA") + return img.resize((size, size), Image.NEAREST) + + +def main(): + groups = collect() + if not groups: + print("No PNGs found under art/. Nothing to do.") + return + + font_label = load_font(LABEL_PX) + font_section = load_font(LABEL_PX + 8) + + # Calculate overall canvas size + section_height = LABEL_PX + 24 + tile_block_h = TILE + PAD_Y + total_h = 40 + for pack, items in groups.items(): + rows = (len(items) + COLS - 1) // COLS + total_h += section_height + rows * tile_block_h + 20 + total_w = COLS * (TILE + PAD_X) + PAD_X + 40 + + canvas = Image.new("RGBA", (total_w, total_h), BG) + draw = ImageDraw.Draw(canvas) + + y = 20 + for pack, items in groups.items(): + # Section header + draw.rectangle([20, y, total_w - 20, y + section_height - 4], fill=SECTION_BG) + draw.text((32, y + 4), pack, font=font_section, fill=FG) + y += section_height + 6 + + # Tiles + for i, (rel, abs_path) in enumerate(items): + col = i % COLS + row = i // COLS + x = 20 + PAD_X // 2 + col * (TILE + PAD_X) + ty = y + row * tile_block_h + + try: + tile = upscale(abs_path, TILE) + canvas.paste(tile, (x, ty), tile) + except Exception as e: + draw.rectangle([x, ty, x + TILE, ty + TILE], outline=(200, 80, 80), width=2) + draw.text((x + 4, ty + 4), f"ERR: {e}", font=font_label, fill=(255, 120, 120)) + + # Label: show subpath + filename below the tile + label = "/".join(rel.parts[1:]) # drop pack prefix + draw.text((x, ty + TILE + 4), label, font=font_label, fill=FG) + + rows = (len(items) + COLS - 1) // COLS + y += rows * tile_block_h + 20 + + canvas.save(OUT) + print(f"Wrote {OUT} — {sum(len(v) for v in groups.values())} textures across {len(groups)} packs.") + + +if __name__ == "__main__": + main() diff --git a/scripts/build-textures.py b/scripts/build-textures.py index ff4efe0..c30a64c 100644 --- a/scripts/build-textures.py +++ b/scripts/build-textures.py @@ -30,6 +30,13 @@ MB_BLACK = (20, 20, 20) MB_FLAG = (235, 190, 55) MB_POST = (90, 60, 35) +# Tent canvas — weathered green army-tent fabric +CANVAS_LIGHT = (118, 138, 88) +CANVAS_MID = (90, 110, 68) +CANVAS_DARK = (66, 84, 50) +CANVAS_SHADOW = (44, 58, 36) +CANVAS_STITCH = (190, 175, 130) + def save(img: Image.Image, rel: str) -> None: out = ROOT / rel @@ -228,10 +235,174 @@ def mailbox() -> Image.Image: return img +# ── Tent Canvas ──────────────────────────────────────────── +# Weathered army-canvas fabric: subtle horizontal weave + occasional darker +# mottling + a vertical seam stitch line. Fully opaque so it doesn't ghost +# against the alpha_test renderer. +def tent_canvas() -> Image.Image: + img = Image.new("RGBA", (16, 16), CANVAS_MID) + + # Horizontal weave bands (every 2 rows alternating tone) + for y in range(16): + row = CANVAS_MID if (y // 2) % 2 == 0 else CANVAS_DARK + rect(img, 0, y, 15, y, row) + + # Diagonal weave specks for fabric texture + for y in range(16): + for x in range(16): + if (x + y) % 5 == 0: + px(img, x, y, CANVAS_LIGHT) + elif (x - y) % 7 == 0: + px(img, x, y, CANVAS_SHADOW) + + # Wear / sun-bleached patches (clusters) + for (x, y) in [(2, 3), (3, 3), (10, 6), (11, 6), (5, 11), (6, 11), (13, 12)]: + px(img, x, y, CANVAS_LIGHT) + + # Vertical seam stitch down the center + for y in range(0, 16, 2): + px(img, 7, y, CANVAS_STITCH) + px(img, 8, y, CANVAS_SHADOW) + + # Edge darken (gives the panel some depth at block boundaries) + rect(img, 0, 0, 15, 0, CANVAS_SHADOW) + rect(img, 0, 15, 15, 15, CANVAS_SHADOW) + rect(img, 0, 0, 0, 15, CANVAS_SHADOW) + rect(img, 15, 0, 15, 15, CANVAS_SHADOW) + + return img + + +# ── Mailbox v2 — block-style (full 16x16 face, no transparency) ─ +# The previous sprite was an item-sized icon centered on transparency, which +# reads as a placeholder when applied to all 6 faces of a cube. This version +# fills the face: red body panel, vertical seam, slot, latch, riveted corners. +def mailbox_block() -> Image.Image: + img = Image.new("RGBA", (16, 16), MB_RED) + + # Vertical highlight strip on the left + rect(img, 0, 0, 1, 15, MB_RED_HL) + # Vertical shadow strip on the right + rect(img, 14, 0, 15, 15, MB_RED_DARK) + # Top dome highlight (1px band) + rect(img, 2, 0, 13, 0, MB_RED_HL) + # Bottom shadow band + rect(img, 2, 15, 13, 15, MB_RED_DARK) + + # Horizontal mail slot (centered) + rect(img, 4, 6, 11, 8, MB_BLACK) + rect(img, 4, 6, 11, 6, (50, 50, 50)) # slot lip highlight + rect(img, 4, 8, 11, 8, (5, 5, 5)) # slot lip shadow + + # Round latch below the slot + rect(img, 7, 11, 8, 12, MB_BLACK) + px(img, 7, 11, (90, 90, 90)) + + # Yellow flag emblem in the top-right corner + rect(img, 12, 2, 13, 4, MB_FLAG) + px(img, 12, 2, (180, 140, 40)) + # Flag pole + px(img, 13, 5, (60, 60, 60)) + + # Rivets in the corners + for (x, y) in [(2, 2), (13, 2), (2, 13), (13, 13)]: + px(img, x, y, (40, 15, 15)) + + return img + + +# ── Post Office v2 — block-style facade ───────────────────── +# A small post-office building face: brick wall, "POST" plaque, awning hint. +def post_office_block() -> Image.Image: + img = Image.new("RGBA", (16, 16), BRICK_RED) + + # Brick courses with mortar lines (offset every other row) + for y in range(16): + # mortar line every 4 rows + if y % 4 == 0: + rect(img, 0, y, 15, y, MORTAR) + else: + # vertical mortar joints, offset by course + offset = 0 if (y // 4) % 2 == 0 else 4 + for x in range(16): + if (x + offset) % 8 == 0: + px(img, x, y, MORTAR) + else: + # subtle brick tone variation + if (x + y) % 3 == 0: + px(img, x, y, BRICK_DARK) + + # Cream-colored "POST" plaque (centered) + rect(img, 2, 5, 13, 10, CREAM) + rect(img, 2, 5, 13, 5, ENVELOPE_LINE) # top border + rect(img, 2, 10, 13, 10, ENVELOPE_LINE) # bottom border + rect(img, 2, 5, 2, 10, ENVELOPE_LINE) # left border + rect(img, 13, 5, 13, 10, ENVELOPE_LINE) # right border + + # "POST" lettering — 4 chunky chars on the plaque + # P + rect(img, 3, 7, 4, 8, ENVELOPE_LINE); px(img, 3, 6, ENVELOPE_LINE) + # O + rect(img, 6, 6, 7, 9, ENVELOPE_LINE); px(img, 6, 7, CREAM); px(img, 7, 7, CREAM); px(img, 6, 8, CREAM); px(img, 7, 8, CREAM) + # S (simplified stub) + rect(img, 9, 6, 10, 6, ENVELOPE_LINE); px(img, 9, 7, ENVELOPE_LINE); rect(img, 9, 8, 10, 8, ENVELOPE_LINE); px(img, 10, 9, ENVELOPE_LINE); rect(img, 9, 9, 10, 9, ENVELOPE_LINE) + # T + rect(img, 11, 6, 12, 6, ENVELOPE_LINE); rect(img, 11, 7, 11, 9, ENVELOPE_LINE) + + # Red stamp accent in the top-right + rect(img, 12, 1, 14, 3, STAMP_RED) + px(img, 13, 2, (255, 220, 220)) + + # Awning suggestion (alternating red/cream stripes at the top) + for x in range(16): + if (x // 2) % 2 == 0: + px(img, x, 1, (210, 70, 70)) + else: + px(img, x, 1, CREAM) + rect(img, 0, 0, 15, 0, ENVELOPE_LINE) + + return img + + +# ── Portal Field ─────────────────────────────────────────── +# Translucent swirling-energy texture for the walk-through portal column. +# Renders with alpha blending — the dark areas read as voids, the bright +# center streaks read as concentrated portal energy. +def portal_field() -> Image.Image: + import math + img = Image.new("RGBA", (16, 16), (0, 0, 0, 0)) + cx, cy = 7.5, 7.5 + for y in range(16): + for x in range(16): + dx = x - cx + dy = y - cy + dist = math.sqrt(dx * dx + dy * dy) + angle = math.atan2(dy, dx) + # swirl: angle modulated by distance + swirl = math.sin(angle * 3 + dist * 1.5) * 0.5 + 0.5 + # purple/violet base + r = int(80 + swirl * 100) + g = int(20 + swirl * 30) + b = int(140 + swirl * 90) + # alpha falls off near edges, bright in middle bands + edge = max(0, 1 - dist / 8.5) + alpha = int(160 * edge + swirl * 60) + alpha = max(0, min(220, alpha)) + img.putpixel((x, y), (r, g, b, alpha)) + # Bright vertical core streaks + for y in range(16): + for x in (7, 8): + r, g, b, _ = img.getpixel((x, y)) + img.putpixel((x, y), (min(255, r + 60), min(255, g + 30), min(255, b + 60), 220)) + return img + + def main() -> None: save(smart_crafting_table(), "smart-crafting-addon/smart_crafting_RP/textures/blocks/smart_crafting_table.png") - save(post_office(), "postal-service-addon/postal_service_RP/textures/blocks/post_office.png") - save(mailbox(), "postal-service-addon/postal_service_RP/textures/blocks/mailbox.png") + save(post_office_block(), "postal-service-addon/postal_service_RP/textures/blocks/post_office.png") + save(mailbox_block(), "postal-service-addon/postal_service_RP/textures/blocks/mailbox.png") + save(tent_canvas(), "camping-supplies-addon/camping_supplies_RP/textures/blocks/tent_canvas.png") + save(portal_field(), "lobby-addon/lobby_transfer_RP/textures/blocks/portal_field.png") if __name__ == "__main__": diff --git a/tow-boat-addon/tow_boat_BP/entities/tow_boat.json b/tow-boat-addon/tow_boat_BP/entities/tow_boat.json new file mode 100644 index 0000000..39a9dd9 --- /dev/null +++ b/tow-boat-addon/tow_boat_BP/entities/tow_boat.json @@ -0,0 +1,95 @@ +{ + "format_version": "1.21.0", + "minecraft:entity": { + "description": { + "identifier": "silverlabs:tow_boat", + "is_spawnable": false, + "is_summonable": true, + "is_experimental": false + }, + "components": { + "minecraft:type_family": { + "family": ["tow_boat", "boat"] + }, + "minecraft:health": { + "value": 20, + "max": 20 + }, + "minecraft:collision_box": { + "width": 1.4, + "height": 0.55 + }, + "minecraft:physics": { + "has_gravity": true, + "has_collision": true + }, + "minecraft:pushable": { + "is_pushable": true, + "is_pushable_by_piston": false + }, + "minecraft:persistent": {}, + "minecraft:nameable": {}, + "minecraft:movement": { + "value": 0.18 + }, + "minecraft:movement.basic": {}, + "minecraft:navigation.float": { + "can_path_over_water": true, + "can_breach": false + }, + "minecraft:underwater_movement": { + "value": 0.0 + }, + "minecraft:can_climb": {}, + "minecraft:rideable": { + "seat_count": 1, + "family_types": ["player"], + "interact_text": "action.interact.mount", + "pull_in_entities": false, + "seats": [ + { + "position": [0, 0.4, 0.0], + "lock_rider_rotation": 90, + "min_rider_count": 0, + "max_rider_count": 1 + } + ] + }, + "minecraft:input_ground_controlled": {}, + "minecraft:leashable": { + "soft_distance": 4.0, + "hard_distance": 6.0, + "max_distance": 12.0 + }, + "minecraft:damage_sensor": { + "triggers": [ + { + "cause": "drowning", + "deals_damage": "no" + }, + { + "cause": "fall", + "deals_damage": "no" + }, + { + "cause": "fire", + "deals_damage": "no" + }, + { + "cause": "fire_tick", + "deals_damage": "no" + }, + { + "cause": "lava", + "deals_damage": "no" + }, + { + "cause": "suffocation", + "deals_damage": "no" + } + ] + } + }, + "events": {} + } +} diff --git a/tow-boat-addon/tow_boat_BP/items/tow_boat.json b/tow-boat-addon/tow_boat_BP/items/tow_boat.json new file mode 100644 index 0000000..1d17e14 --- /dev/null +++ b/tow-boat-addon/tow_boat_BP/items/tow_boat.json @@ -0,0 +1,26 @@ +{ + "format_version": "1.21.0", + "minecraft:item": { + "description": { + "identifier": "silverlabs:tow_boat", + "menu_category": { + "category": "equipment", + "group": "itemGroup.name.boat" + } + }, + "components": { + "minecraft:max_stack_size": 1, + "minecraft:icon": "tow_boat", + "minecraft:display_name": { + "value": "Tow Boat" + }, + "minecraft:entity_placer": { + "entity": "silverlabs:tow_boat", + "use_on": [ + "minecraft:water", + "minecraft:flowing_water" + ] + } + } + } +} diff --git a/tow-boat-addon/tow_boat_BP/manifest.json b/tow-boat-addon/tow_boat_BP/manifest.json new file mode 100644 index 0000000..8ae48f1 --- /dev/null +++ b/tow-boat-addon/tow_boat_BP/manifest.json @@ -0,0 +1,33 @@ +{ + "format_version": 2, + "header": { + "name": "Tow Boat", + "description": "A flatbed boat that can be leashed and chained for towing.", + "uuid": "a35d89fd-99e1-497a-9e78-35443c0cd59f", + "version": [1, 0, 0], + "min_engine_version": [1, 21, 0] + }, + "modules": [ + { + "type": "data", + "uuid": "b29d967c-8a9a-491d-867d-2866890780f0", + "version": [1, 0, 0] + }, + { + "type": "script", + "uuid": "06b92593-0c15-41a0-8f39-bc6bba516ebb", + "version": [1, 0, 0], + "entry": "scripts/main.js" + } + ], + "dependencies": [ + { + "uuid": "c6193eda-f160-475b-ab65-fca17741e41b", + "version": [1, 0, 0] + }, + { + "module_name": "@minecraft/server", + "version": "1.17.0" + } + ] +} diff --git a/tow-boat-addon/tow_boat_BP/pack_icon.png b/tow-boat-addon/tow_boat_BP/pack_icon.png new file mode 100644 index 0000000..5bb1140 Binary files /dev/null and b/tow-boat-addon/tow_boat_BP/pack_icon.png differ diff --git a/tow-boat-addon/tow_boat_BP/recipes/tow_boat.json b/tow-boat-addon/tow_boat_BP/recipes/tow_boat.json new file mode 100644 index 0000000..5b47d17 --- /dev/null +++ b/tow-boat-addon/tow_boat_BP/recipes/tow_boat.json @@ -0,0 +1,24 @@ +{ + "format_version": "1.21.0", + "minecraft:recipe_shaped": { + "description": { + "identifier": "silverlabs:tow_boat_recipe" + }, + "tags": ["crafting_table"], + "unlock": [ + { "item": "minecraft:lead" } + ], + "pattern": [ + "PLP", + "PPP" + ], + "key": { + "P": { "item": "minecraft:planks" }, + "L": { "item": "minecraft:lead" } + }, + "result": { + "item": "silverlabs:tow_boat", + "count": 1 + } + } +} diff --git a/tow-boat-addon/tow_boat_BP/scripts/main.js b/tow-boat-addon/tow_boat_BP/scripts/main.js new file mode 100644 index 0000000..8df9ef0 --- /dev/null +++ b/tow-boat-addon/tow_boat_BP/scripts/main.js @@ -0,0 +1,150 @@ +import { world, system } from "@minecraft/server"; + +const BOAT = "silverlabs:tow_boat"; +const TOW_PROP = "silverlabs:tow_target"; + +const SOFT_DIST = 3.0; +const HARD_DIST = 8.0; +const PULL_GAIN = 0.20; +const TICK_RATE = 4; + +function dist(a, b) { + const dx = a.x - b.x; + const dy = a.y - b.y; + const dz = a.z - b.z; + return Math.sqrt(dx * dx + dy * dy + dz * dz); +} + +function unitVec(from, to, len) { + return { + x: (to.x - from.x) / len, + y: (to.y - from.y) / len, + z: (to.z - from.z) / len + }; +} + +function getTowTarget(boat) { + const id = boat.getDynamicProperty(TOW_PROP); + return typeof id === "string" && id.length > 0 ? id : null; +} + +function setTowTarget(boat, targetId) { + boat.setDynamicProperty(TOW_PROP, targetId); +} + +function clearTowTarget(boat) { + boat.setDynamicProperty(TOW_PROP, ""); +} + +function findLeashedBoat(player) { + const overworld = world.getDimension(player.dimension.id); + const nearby = overworld.getEntities({ + type: BOAT, + location: player.location, + maxDistance: 16 + }); + for (const boat of nearby) { + try { + const holder = boat.getComponent("leashable")?.leashHolder; + if (holder && holder.id === player.id) return boat; + } catch { + /* ignore — component may be unavailable mid-load */ + } + } + return null; +} + +world.beforeEvents.playerInteractWithEntity.subscribe((ev) => { + const { player, target } = ev; + if (target.typeId !== BOAT) return; + if (!player.isSneaking) return; + + const inv = player.getComponent("inventory")?.container; + const held = inv?.getItem(player.selectedSlotIndex); + if (held) return; // empty hand only + + if (getTowTarget(target)) { + ev.cancel = true; + system.run(() => { + clearTowTarget(target); + player.sendMessage("§eTow link removed."); + }); + return; + } + + const leader = findLeashedBoat(player); + if (!leader || leader.id === target.id) return; + + ev.cancel = true; + system.run(() => { + setTowTarget(target, leader.id); + try { + leader.getComponent("leashable")?.unleash(); + } catch { + /* unleash may not exist on all versions; safe to ignore */ + } + player.sendMessage("§aTow link created — boat will follow the leader."); + }); +}); + +function tickFollowers() { + for (const dim of ["overworld", "nether", "the_end"]) { + let boats; + try { + boats = world.getDimension(dim).getEntities({ type: BOAT }); + } catch { + continue; + } + for (const boat of boats) { + const targetId = getTowTarget(boat); + if (!targetId) continue; + + const target = world.getEntity(targetId); + if (!target || !target.isValid()) { + clearTowTarget(boat); + continue; + } + + const d = dist(boat.location, target.location); + if (d < SOFT_DIST) continue; + + if (d > HARD_DIST) { + const u = unitVec(target.location, boat.location, d); + try { + boat.teleport({ + x: target.location.x + u.x * (SOFT_DIST - 0.5), + y: target.location.y, + z: target.location.z + u.z * (SOFT_DIST - 0.5) + }); + } catch { /* chunk unloaded */ } + continue; + } + + const u = unitVec(boat.location, target.location, d); + try { + boat.applyImpulse({ + x: u.x * PULL_GAIN, + y: 0, + z: u.z * PULL_GAIN + }); + } catch { /* boat may not support impulse if unloaded */ } + } + } +} + +system.runInterval(tickFollowers, TICK_RATE); + +world.afterEvents.entityRemove.subscribe((ev) => { + const removedId = ev.removedEntityId; + for (const dim of ["overworld", "nether", "the_end"]) { + let boats; + try { + boats = world.getDimension(dim).getEntities({ type: BOAT }); + } catch { + continue; + } + for (const boat of boats) { + if (getTowTarget(boat) === removedId) clearTowTarget(boat); + } + } +}); diff --git a/tow-boat-addon/tow_boat_RP/animations/tow_boat.animation.json b/tow-boat-addon/tow_boat_RP/animations/tow_boat.animation.json new file mode 100644 index 0000000..284c670 --- /dev/null +++ b/tow-boat-addon/tow_boat_RP/animations/tow_boat.animation.json @@ -0,0 +1,27 @@ +{ + "format_version": "1.8.0", + "animations": { + "animation.tow_boat.bob": { + "loop": true, + "animation_length": 4.0, + "bones": { + "root": { + "position": { + "0.0": [0, 0, 0], + "1.0": [0, 0.6, 0], + "2.0": [0, 0, 0], + "3.0": [0, -0.4, 0], + "4.0": [0, 0, 0] + }, + "rotation": { + "0.0": [0, 0, 0], + "1.0": [1.5, 0, -0.5], + "2.0": [0, 0, 0], + "3.0": [-1.0, 0, 0.5], + "4.0": [0, 0, 0] + } + } + } + } + } +} diff --git a/tow-boat-addon/tow_boat_RP/entity/tow_boat.entity.json b/tow-boat-addon/tow_boat_RP/entity/tow_boat.entity.json new file mode 100644 index 0000000..2d8bae3 --- /dev/null +++ b/tow-boat-addon/tow_boat_RP/entity/tow_boat.entity.json @@ -0,0 +1,30 @@ +{ + "format_version": "1.10.0", + "minecraft:client_entity": { + "description": { + "identifier": "silverlabs:tow_boat", + "materials": { + "default": "entity_alphatest" + }, + "textures": { + "default": "textures/entity/tow_boat" + }, + "geometry": { + "default": "geometry.tow_boat" + }, + "render_controllers": [ + "controller.render.tow_boat" + ], + "animations": { + "bob": "animation.tow_boat.bob" + }, + "scripts": { + "animate": ["bob"] + }, + "spawn_egg": { + "texture": "tow_boat", + "texture_index": 0 + } + } + } +} diff --git a/tow-boat-addon/tow_boat_RP/manifest.json b/tow-boat-addon/tow_boat_RP/manifest.json new file mode 100644 index 0000000..d55ba9c --- /dev/null +++ b/tow-boat-addon/tow_boat_RP/manifest.json @@ -0,0 +1,17 @@ +{ + "format_version": 2, + "header": { + "name": "Tow Boat Resources", + "description": "Model, texture, and animations for the Tow Boat addon.", + "uuid": "c6193eda-f160-475b-ab65-fca17741e41b", + "version": [1, 0, 0], + "min_engine_version": [1, 21, 0] + }, + "modules": [ + { + "type": "resources", + "uuid": "2ef90fb4-1c8f-404e-924f-a41463a51758", + "version": [1, 0, 0] + } + ] +} diff --git a/tow-boat-addon/tow_boat_RP/models/entity/tow_boat.geo.json b/tow-boat-addon/tow_boat_RP/models/entity/tow_boat.geo.json new file mode 100644 index 0000000..963c1d0 --- /dev/null +++ b/tow-boat-addon/tow_boat_RP/models/entity/tow_boat.geo.json @@ -0,0 +1,105 @@ +{ + "format_version": "1.12.0", + "minecraft:geometry": [ + { + "description": { + "identifier": "geometry.tow_boat", + "texture_width": 64, + "texture_height": 32, + "visible_bounds_width": 3, + "visible_bounds_height": 2, + "visible_bounds_offset": [0, 0.5, 0] + }, + "bones": [ + { + "name": "root", + "pivot": [0, 0, 0] + }, + { + "name": "hull", + "parent": "root", + "pivot": [0, 0, 0], + "cubes": [ + { + "origin": [-11, 0, -16], + "size": [22, 3, 32], + "uv": [0, 0] + } + ] + }, + { + "name": "deck", + "parent": "hull", + "pivot": [0, 3, 0], + "cubes": [ + { + "origin": [-11, 3, -16], + "size": [22, 1, 32], + "uv": [0, 16] + } + ] + }, + { + "name": "rail_left", + "parent": "deck", + "pivot": [-11, 4, 0], + "cubes": [ + { + "origin": [-12, 4, -16], + "size": [1, 2, 32], + "uv": [0, 18] + } + ] + }, + { + "name": "rail_right", + "parent": "deck", + "pivot": [11, 4, 0], + "cubes": [ + { + "origin": [11, 4, -16], + "size": [1, 2, 32], + "uv": [0, 18] + } + ] + }, + { + "name": "rail_front", + "parent": "deck", + "pivot": [0, 4, -16], + "cubes": [ + { + "origin": [-11, 4, -17], + "size": [22, 2, 1], + "uv": [0, 22] + } + ] + }, + { + "name": "rail_back", + "parent": "deck", + "pivot": [0, 4, 16], + "cubes": [ + { + "origin": [-11, 4, 16], + "size": [22, 2, 1], + "uv": [0, 22] + } + ] + }, + { + "name": "tow_post", + "parent": "deck", + "pivot": [0, 4, -14], + "cubes": [ + { + "origin": [-1, 4, -15], + "size": [2, 4, 2], + "uv": [44, 16] + } + ] + } + ] + } + ] +} diff --git a/tow-boat-addon/tow_boat_RP/pack_icon.png b/tow-boat-addon/tow_boat_RP/pack_icon.png new file mode 100644 index 0000000..5bb1140 Binary files /dev/null and b/tow-boat-addon/tow_boat_RP/pack_icon.png differ diff --git a/tow-boat-addon/tow_boat_RP/render_controllers/tow_boat.rc.json b/tow-boat-addon/tow_boat_RP/render_controllers/tow_boat.rc.json new file mode 100644 index 0000000..23468c0 --- /dev/null +++ b/tow-boat-addon/tow_boat_RP/render_controllers/tow_boat.rc.json @@ -0,0 +1,12 @@ +{ + "format_version": "1.10.0", + "render_controllers": { + "controller.render.tow_boat": { + "geometry": "Geometry.default", + "materials": [ + { "*": "Material.default" } + ], + "textures": ["Texture.default"] + } + } +} diff --git a/tow-boat-addon/tow_boat_RP/texts/en_US.lang b/tow-boat-addon/tow_boat_RP/texts/en_US.lang new file mode 100644 index 0000000..2cfe6e1 --- /dev/null +++ b/tow-boat-addon/tow_boat_RP/texts/en_US.lang @@ -0,0 +1,3 @@ +item.silverlabs:tow_boat=Tow Boat +entity.silverlabs:tow_boat.name=Tow Boat +action.interact.mount=Mount diff --git a/tow-boat-addon/tow_boat_RP/texts/languages.json b/tow-boat-addon/tow_boat_RP/texts/languages.json new file mode 100644 index 0000000..818bf4c --- /dev/null +++ b/tow-boat-addon/tow_boat_RP/texts/languages.json @@ -0,0 +1,3 @@ +[ + "en_US" +] diff --git a/tow-boat-addon/tow_boat_RP/textures/entity/tow_boat.png b/tow-boat-addon/tow_boat_RP/textures/entity/tow_boat.png new file mode 100644 index 0000000..ea1f7fc Binary files /dev/null and b/tow-boat-addon/tow_boat_RP/textures/entity/tow_boat.png differ diff --git a/tow-boat-addon/tow_boat_RP/textures/item_texture.json b/tow-boat-addon/tow_boat_RP/textures/item_texture.json new file mode 100644 index 0000000..d3d3764 --- /dev/null +++ b/tow-boat-addon/tow_boat_RP/textures/item_texture.json @@ -0,0 +1,9 @@ +{ + "resource_pack_name": "tow_boat_RP", + "texture_name": "atlas.items", + "texture_data": { + "tow_boat": { + "textures": "textures/items/tow_boat" + } + } +} diff --git a/tow-boat-addon/tow_boat_RP/textures/items/tow_boat.png b/tow-boat-addon/tow_boat_RP/textures/items/tow_boat.png new file mode 100644 index 0000000..fb7fd11 Binary files /dev/null and b/tow-boat-addon/tow_boat_RP/textures/items/tow_boat.png differ diff --git a/village-evolution-addon/enabled_packs.json b/village-evolution-addon/enabled_packs.json index be9af57..c8231ca 100644 --- a/village-evolution-addon/enabled_packs.json +++ b/village-evolution-addon/enabled_packs.json @@ -4,11 +4,15 @@ {"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]} + {"pack_id": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b60", "version": [1, 0, 0]}, + {"pack_id": "fac83943-16bc-4790-aa05-631894f59a03", "version": [1, 0, 0]}, + {"pack_id": "a35d89fd-99e1-497a-9e78-35443c0cd59f", "version": [1, 0, 0]} ], "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]} + {"pack_id": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b63", "version": [1, 0, 0]}, + {"pack_id": "a18bdde1-53f8-49aa-b06d-6f0ec6c45b46", "version": [1, 0, 0]}, + {"pack_id": "c6193eda-f160-475b-ab65-fca17741e41b", "version": [1, 0, 0]} ] }