-- Configurable damage (set in settingtypes.txt) -- Load AI system and control panel local modpath = minetest.get_modpath("assorted_mecha") dofile(modpath .. "/ai.lua") dofile(modpath .. "/mecha_control_panel.lua") dofile(modpath .. "/mecha_controller.lua") local MECH_EXPLOSION_DAMAGE = tonumber(minetest.settings:get("assorted_mecha_explosion_damage")) or 100 local MECH_BULLET_DAMAGE = tonumber(minetest.settings:get("assorted_mecha_bullet_damage")) or 10 local function apply_mech_scope(player, mech) if not player or not mech then return end -- Apply FULLSCREEN HUD overlay (scope effect) local hud_id = player:hud_add({ type = "image", position = {x = 0.4974, y = 0.5025}, offset = {x = 0, y = 0}, text = mech.scope_texture, alignment = {x = 0, y = 0}, scale = {x = -100, y = -100}, }) player:get_meta():set_int("mech_hud_id", hud_id) end local function remove_mech_scope(player) if not player then return end local hud_id = player:get_meta():get_int("mech_hud_id") if hud_id and hud_id > 0 then player:hud_remove(hud_id) end end local function play_step_sound(self, mech) if self.driver then minetest.sound_play(mech.step_sound, {pos = self.object:get_pos(), gain = 0.5}) local step_interval = self.driver:get_player_control().sneak and (mech.step_interval * 0.5) or mech.step_interval minetest.after(step_interval, function() if self.driver and (self.object:get_velocity().x ~= 0 or self.object:get_velocity().z ~= 0) then play_step_sound(self, mech) end end) end end -- the word "assorted" implies three or more of something. local mechs = { { name = "mech1", collisionbox = {-1.5, -0.1, -1.5, 1.5, 4.8, 1.5}, speed = 4, driver_visible = false, scope_texture = "mecha_static.png^scope_1.png", inventory_image = "spider_spawn.png", stepheight = 3.2, player_attach_offset = {x=0, y=1, z=0}, player_eye_offset = {x=0, y=20, z=16.2}, visual_size = {x=17.5, y=17.5, z=17.5}, description = "(Walking Cannon)\nCategory: (4 Legged Stomper)\n\nControls:\nW-A-S-D - Walk\nShift - Sprint\nE - Fire Cannon", mesh = "mecha_spider_legs.x", step_sound = "mech1_step", turn_speed = 2, walk_animation = {x=25, y=60}, reverse_walk_animation = {x=61, y=97}, stand_animation = {x=1, y=1}, walk_frame_interval = 0.1, texture = "mecha_head_gun.png", jump_height = 10, spawn_height = 0.5, player_attach_sound = "mech_attach", player_detach_sound = "mech_detach", jump_sound = "mech_jump", head_name = "assorted_mecha:head_spider", head_offset = {x=0, y=4.75, z=0}, has_craftitem = true, step_interval = 0.5, }, { name = "mech2", collisionbox = {-1.4, -0.1, -1.4, 1.4, 4.8, 1.4}, speed = 3, driver_visible = false, scope_texture = "mecha_static.png^scope_1.png", inventory_image = "fang_spawn.png", stepheight = 1.2, player_attach_offset = {x=0, y=1, z=0}, player_eye_offset = {x=0, y=23, z=12}, visual_size = {x=30, y=30, z=30}, description = "(Fang)\nCategory: (Bipedal Walker)\n\nControls:\nW-A-S-D - Walk\nShift - Sprint\nE - Rapidfire", mesh = "mecha1_legs.x", step_sound = "mech1_step", turn_speed = 1, walk_animation = {x=0, y=45}, reverse_walk_animation = {x=45, y=90}, stand_animation = {x=92, y=92}, walk_frame_interval = 0.1, texture = "fang_mecha.png", jump_height = 10, air_animation = {x=1, y=5}, spawn_height = 0.5, player_attach_sound = "mech_attach", player_detach_sound = "mech_detach", jump_sound = "mech_jump", head_name = "assorted_mecha:head_2", head_offset = {x=0, y=4.75, z=0}, has_craftitem = true, step_interval = 0.75, }, { name = "mech3", collisionbox = {-1.5, -0.1, -1.5, 1.5, 4.8, 1.5}, speed = 3, driver_visible = false, scope_texture = "mecha_static.png^scope_2.png", inventory_image = "mecha_icon_default.png", stepheight = 1.2, player_attach_offset = {x=0, y=1, z=0}, player_eye_offset = {x=0, y=28, z=25}, visual_size = {x=14.5, y=14.5, z=14.5}, description = "(new mecha)\nCategory: ( category )\n\nControls:\n WASD or something.\nShift - Sprint\nE - activate head function", mesh = "mecha_spider_legs.x", step_sound = "mech1_step", turn_speed = 1, walk_animation = {x=25, y=60}, reverse_walk_animation = {x=61, y=97}, stand_animation = {x=1, y=1}, walk_frame_interval = 0.1, texture = "mecha_head_gun.png", jump_height = 10, air_animation = {x=1, y=5}, spawn_height = 0.5, player_attach_sound = "mech_attach", player_detach_sound = "mech_detach", jump_sound = "mech_jump", head_name = "assorted_mecha:head_2", head_offset = {x=0, y=3.5, z=0}, has_craftitem = true, step_interval = 0.5, }, } local function register_mech(mech) local props = { physical = true, collisionbox = mech.collisionbox, visual = "mesh", mesh = mech.mesh, textures = {mech.texture}, visual_size = mech.visual_size, stepheight = mech.stepheight, } minetest.register_entity("assorted_mecha:" .. mech.name, { initial_properties = props, driver = nil, head_entity = nil, jump_allowed = true, animation_timer = 0, on_activate = function(self) self.object:set_armor_groups({immortal=1}) -- Store mech config on self so AI can access it outside the closure self._mech_speed = mech.speed self._mech_walk_anim = mech.walk_animation self._mech_stand_anim = mech.stand_animation self._mech_head_name = mech.head_name self._mech_name = mech.name -- Initialize AI state (restore from staticdata if available) assorted_mecha_ai_init(self) if staticdata and staticdata ~= "" then local ok, data = pcall(minetest.parse_json, staticdata) if ok and data and data.ai then assorted_mecha_ai_deserialize(self, minetest.write_json(data.ai)) end end if self._ai_enabled then assorted_mecha_ai_start(self) end if not self.head_entity then local pos = self.object:get_pos() pos.y = pos.y + mech.head_offset.y self.head_entity = minetest.add_entity(pos, mech.head_name) end end, get_staticdata = function(self) return minetest.write_json({ai = minetest.parse_json(assorted_mecha_ai_serialize(self))}) end, on_step = function(self, dtime) if not self.initial_animation_set then self.object:set_animation(mech.stand_animation, 30, 0) self.initial_animation_set = true end if not self.initial_yaw_set then self.object:set_yaw(0) self.initial_yaw_set = true end if not self.initial_step_sound_played then minetest.after(0.5, function() minetest.sound_play(mech.step_sound, {pos = self.object:get_pos(), gain = 0.5}) end) self.initial_step_sound_played = true end local vel = self.object:get_velocity() or {x=0, y=0, z=0} vel.y = vel.y - (dtime * 9.8) self.object:set_velocity(vel) if self.driver then local control = self.driver:get_player_control() local yaw = self.object:get_yaw() local speed = (control.up and mech.speed or (control.down and -mech.speed or 0)) local animation_speed = 30 -- Default animation speed -- Apply sprint boost when Shift (sneak) is pressed if control.sneak then speed = speed * 2 -- Double the normal speed animation_speed = animation_speed * 2 -- Double animation speed end local vel = self.object:get_velocity() or {x=0, y=0, z=0} vel.x = math.sin(yaw) * -speed vel.z = math.cos(yaw) * speed self.object:set_velocity(vel) if control.left then self.object:set_yaw(yaw + mech.turn_speed * dtime) elseif control.right then self.object:set_yaw(yaw - mech.turn_speed * dtime) end if speed ~= 0 and not self.is_playing_step_sound then self.is_playing_step_sound = true play_step_sound(self, mech) elseif speed == 0 then self.is_playing_step_sound = false end -- Sneak+Jump = toggle 3rd-person overview / 1st-person scope -- (V/zoom key is client-side only and never reaches the server) local view_combo = control.sneak and control.jump if view_combo then if not self.view_combo_pressed then self.view_combo_pressed = true local meta = self.driver:get_meta() local third = meta:get_int("mech_third_person") if third == 0 then meta:set_int("mech_third_person", 1) remove_mech_scope(self.driver) self.driver:hud_set_flags({crosshair = false, wielditem = false}) self.driver:set_eye_offset( {x = 0, y = 14, z = -28}, {x = 0, y = 5, z = -5} ) else meta:set_int("mech_third_person", 0) self.driver:hud_set_flags({crosshair = false, wielditem = false}) self.driver:set_eye_offset(mech.player_eye_offset, {x=0, y=0, z=0}) apply_mech_scope(self.driver, mech) end end else self.view_combo_pressed = false end if control.jump and self.jump_allowed and vel.y >= -1 then self.object:set_velocity({x=vel.x, y=mech.jump_height, z=vel.z}) self.jump_allowed = false minetest.after(2, function() self.jump_allowed = true end) minetest.sound_play(mech.jump_sound, {pos = self.object:get_pos(), gain = 0.5}) end if self.driver then local control = self.driver:get_player_control() local speed = control.up and mech.speed or (control.down and -mech.speed or 0) self.animation_timer = self.animation_timer + dtime local is_moving = speed ~= 0 local moving_backward = control.down local anim_range = moving_backward and mech.reverse_walk_animation or mech.walk_animation if self.animation_timer >= mech.walk_frame_interval then self.animation_timer = 0 if is_moving then self.object:set_animation(anim_range, animation_speed, 0) else self.object:set_animation(mech.stand_animation, 30, 0) end end end end if self.head_entity then local mech_pos = self.object:get_pos() local head_pos = self.head_entity:get_pos() head_pos.x = mech_pos.x head_pos.y = mech_pos.y + mech.head_offset.y head_pos.z = mech_pos.z self.head_entity:set_pos(head_pos) self.head_entity:set_yaw(self.head_entity:get_yaw()) end end, on_rightclick = function(self, clicker) -- If AI is active: open control panel, boarding not allowed if self._ai_enabled then assorted_mecha_open_panel(self, clicker) return end -- Shift+rightclick opens panel in manual mode local ctrl = clicker:get_player_control() if not self.driver and ctrl.sneak then assorted_mecha_open_panel(self, clicker) return end if self.driver and clicker == self.driver then clicker:set_detach() clicker:set_eye_offset({x=0, y=0, z=0}) minetest.sound_play(mech.player_detach_sound, {pos = self.object:get_pos(), gain = 0.5}) self.driver = nil clicker:set_properties({visual_size = mech.driver_visible and {x=1, y=1, z=1} or {x=0, y=0, z=0}}) if self.saved_hp then clicker:set_hp(self.saved_hp) self.saved_hp = nil end clicker:set_properties({visual_size = {x=1, y=1, z=1}}) remove_mech_scope(clicker) clicker:hud_set_flags({crosshair = true, wielditem = true}) clicker:set_eye_offset({x=0, y=0, z=0}, {x=0, y=0, z=0}) clicker:get_meta():set_int("mech_third_person", 0) else clicker:set_attach(self.object, "", mech.player_attach_offset, {x=0, y=0, z=0}) clicker:set_eye_offset(mech.player_eye_offset) minetest.sound_play(mech.player_attach_sound, {pos = self.object:get_pos(), gain = 0.5}) self.driver = clicker self._driver_name = clicker:get_player_name() self.saved_hp = clicker:get_hp() minetest.register_on_player_hpchange(function(player, hp_change) if player == clicker and player:get_attach() == self.object then player:set_hp(self.saved_hp) return 0 end return hp_change end, true) clicker:set_properties({visual_size = mech.driver_visible and {x=1, y=1, z=1} or {x=0, y=0, z=0}}) apply_mech_scope(clicker, mech) -- Scope overlay replaces crosshair; hide ingame crosshair & wielditem clicker:hud_set_flags({crosshair = false, wielditem = false}) clicker:get_meta():set_int("mech_third_person", 0) end end, }) end for _, mech in ipairs(mechs) do if mech.has_craftitem then minetest.register_craftitem("assorted_mecha:spawn_" .. mech.name, { description = "Name: " .. mech.description, inventory_image = mech.inventory_image, range = 9, on_place = function(itemstack, placer, pointed_thing) if pointed_thing.type ~= "node" then return itemstack end local pos = pointed_thing.above pos.y = pos.y + mech.spawn_height local mech_entity = minetest.add_entity(pos, "assorted_mecha:" .. mech.name) if mech_entity and placer then mech_entity:set_yaw(placer:get_look_horizontal()) end if not minetest.is_creative_enabled(placer:get_player_name()) then itemstack:take_item() end return itemstack end, }) end register_mech(mech) end -----------------------------------------------------------------------------------------------------mecha1 local function rotate_offset(offset, yaw) local rotated_x = offset.x * math.cos(yaw) - offset.z * math.sin(yaw) local rotated_z = offset.x * math.sin(yaw) + offset.z * math.cos(yaw) return {x = rotated_x, y = offset.y, z = rotated_z} end local bullet_groups = { A = { {x = -1.35, y = 3.772, z = 0.85}, {x = 1.35, y = 3.772, z = 0.85} }, B = { {x = -1.35, y = 3.92, z = 0.85}, {x = 1.35, y = 3.92, z = 0.85} }, C = { {x = -1.35, y = 4.067, z = 0.85}, {x = 1.35, y = 4.067, z = 0.85} } } local function spawn_bullets(player) if not player then return end local player_pos = player:get_pos() local player_yaw = player:get_look_horizontal() local player_dir = player:get_look_dir() -- Correct movement direction -- Randomly select one bullet group (A, B, or C) local group_key = math.random(1, 3) == 1 and "A" or (math.random(1, 2) == 1 and "B" or "C") local selected_positions = bullet_groups[group_key] -- Play sound when a bullet group is picked minetest.sound_play("mech_fire2", {pos = player_pos, gain = 1.0}) -- Spawn bullets from the selected group for _, offset in ipairs(selected_positions) do local rotated_offset = rotate_offset(offset, player_yaw) local bullet_pos = vector.add(player_pos, rotated_offset) local bullet = minetest.add_entity(bullet_pos, "assorted_mecha:mecha_bullet") if bullet then local _bent = bullet:get_luaentity() if _bent then _bent.shooter_name = player:get_player_name() end local random_x = (math.random() * 0.5) - 0.25 local random_z = (math.random() * 0.5) - 0.25 local bullet_velocity = vector.multiply(player_dir, 30) bullet_velocity.x = bullet_velocity.x + random_x bullet_velocity.z = bullet_velocity.z + random_z bullet:set_velocity(bullet_velocity) bullet:set_yaw(player:get_look_horizontal()) local muzzle_textures = {"muzzle_flash.png", "muzzle_flash2.png"} local selected_texture = muzzle_textures[math.random(1, #muzzle_textures)] minetest.add_particlespawner({ amount = 1, time = 0.1, minpos = bullet_pos, maxpos = bullet_pos, minvel = {x=0, y=0, z=0}, maxvel = {x=0, y=0, z=0}, minexptime = 0.1, maxexptime = 0.1, minsize = 6, maxsize = 6, texture = selected_texture, glow = 15, }) end end end minetest.register_entity("assorted_mecha:head_2", { initial_properties = { physical = false, pointable = false, visual = "mesh", mesh = "mecha_1.x", glow = 0, textures = {"fang_mecha_antiglow.png"}, visual_size = {x=11.4, y=11.4, z=11.4}, static_save = false, }, on_activate = function(self, staticdata) self.object:set_armor_groups({immortal=1}) if not self.glow_entity then self.glow_entity = minetest.add_entity(self.object:get_pos(), "assorted_mecha:glow_effect") if self.glow_entity then self.glow_entity:set_attach(self.object, "", {x=0, y=0, z=0}, {x=0, y=0, z=0}) end end end, on_step = function(self, dtime) local head_pos = self.object:get_pos() local target_player = nil for _, player in ipairs(minetest.get_connected_players()) do local player_pos = player:get_pos() local height_difference = head_pos.y - player_pos.y local horizontal_distance = vector.distance(vector.new(head_pos.x, 0, head_pos.z), vector.new(player_pos.x, 0, player_pos.z)) if math.abs(height_difference - 4.75) < 0.1 and horizontal_distance <= 1 then target_player = player break end end if target_player then self.object:set_yaw(target_player:get_look_horizontal()) local control = target_player:get_player_control() if control.aux1 and not self.is_firing then self.is_firing = true local function fire_loop() if self.is_firing and target_player then spawn_bullets(target_player) minetest.after(0.1, fire_loop) end end fire_loop() elseif not control.aux1 then self.is_firing = false end end end, }) minetest.register_entity("assorted_mecha:glow_effect", { initial_properties = { physical = false, pointable = false, visual = "mesh", mesh = "mecha_1.x", glow = 30, textures = {"fang_glow.png"}, visual_size = {x=1, y=1, z=1}, static_save = false, }, on_activate = function(self) self.object:set_armor_groups({immortal=1}) end, }) --------------------------------------------------------------------------------------------------mecha2 local function rotate_offset(offset, yaw) local rotated_x = offset.x * math.cos(yaw) - offset.z * math.sin(yaw) local rotated_z = offset.x * math.sin(yaw) + offset.z * math.cos(yaw) return {x = rotated_x, y = offset.y, z = rotated_z} end minetest.register_entity("assorted_mecha:head_spider", { initial_properties = { physical = false, pointable = false, visual = "mesh", mesh = "mecha_big_gun_head.x", glow = 0, textures = {"mecha_head_gun_anti.png"}, visual_size = {x=14, y=14, z=14}, static_save = false, }, on_activate = function(self, staticdata) self.object:set_armor_groups({immortal=1}) if not self.glow_entity then self.glow_entity = minetest.add_entity(self.object:get_pos(), "assorted_mecha:spider_glow_effect") if self.glow_entity then self.glow_entity:set_attach(self.object, "", {x=0, y=0, z=0}, {x=0, y=0, z=0}) end end end, on_step = function(self, dtime) local head_pos = self.object:get_pos() local target_player = nil for _, player in ipairs(minetest.get_connected_players()) do local player_pos = player:get_pos() local height_difference = head_pos.y - player_pos.y local horizontal_distance = vector.distance(vector.new(head_pos.x, 0, head_pos.z), vector.new(player_pos.x, 0, player_pos.z)) if math.abs(height_difference - 4.75) < 0.1 and horizontal_distance <= 1 then target_player = player break end end if target_player then self.object:set_yaw(target_player:get_look_horizontal()) local control = target_player:get_player_control() if control.aux1 and not self.is_firing then self.is_firing = true local shoot_speed = 15 self.object:set_animation({x=24, y=38}, shoot_speed, 0) if self.glow_entity then self.glow_entity:set_animation({x=24, y=38}, shoot_speed, 0) -- Sync glow animation end local yaw = target_player:get_look_horizontal() local pitch = target_player:get_look_vertical() local dir = vector.new(-math.sin(yaw) * math.cos(pitch), -math.sin(pitch), math.cos(yaw) * math.cos(pitch)) -- **Bullet offset relative to the mech** local bullet_offset = {x=0, y=-0.75, z=2.75} -- Adjust positioning local rotated_offset = rotate_offset(bullet_offset, yaw) local bullet_pos = vector.add(head_pos, rotated_offset) local bullet = minetest.add_entity(bullet_pos, "assorted_mecha:plasma_bolt") if bullet then local _pent = bullet:get_luaentity() if _pent then _pent.shooter_name = target_player:get_player_name() end bullet:set_velocity(vector.multiply(dir, 40)) minetest.sound_play("plasma_rifle", {pos = bullet_pos, gain = 3.0}) bullet:set_yaw(target_player:get_look_horizontal()) -- **Spawn muzzle flash particles at bullet position** local muzzle_textures = {"plasma_pop.png"} local selected_texture = muzzle_textures[math.random(1, #muzzle_textures)] minetest.add_particlespawner({ amount = 1, time = 0.1, minpos = bullet_pos, maxpos = bullet_pos, minvel = {x=0, y=0, z=0}, maxvel = {x=0, y=0, z=0}, minexptime = 0.1, maxexptime = 0.1, minsize = 12, maxsize = 12, texture = selected_texture, glow = 15, }) end minetest.after(0.6, function() self.object:set_animation({x=0.5, y=0.9}, 1, 0) if self.glow_entity then self.glow_entity:set_animation({x=0.5, y=0.9}, 1, 0) end end) -- Cooldown of 2 seconds before firing again minetest.after(2, function() self.is_firing = false end) end end end, }) minetest.register_entity("assorted_mecha:spider_glow_effect", { initial_properties = { physical = false, pointable = false, visual = "mesh", mesh = "mecha_big_gun_head.x", glow = 30, textures = {"mecha_head_gun_glow.png"}, visual_size = {x=1, y=1, z=1}, static_save = false, }, on_activate = function(self) self.object:set_armor_groups({immortal=1}) end, }) --------------------------------------------------------------------------------------------------stuff minetest.register_entity("assorted_mecha:mecha_bullet", { initial_properties = { physical = true, visual_size = {x=0.1, y=0.1, z=0.1}, visual = "sprite", textures = {"mecha_bullet.png"}, glow = 30, collisionbox = {0, 0, 0, 0, 0, 0}, }, on_activate = function(self) self.shooter_name = nil minetest.after(3, function() self.object:remove() end) end, on_step = function(self, dtime) local pos = self.object:get_pos() if not pos then return end local function is_liquid(name) local def = minetest.registered_nodes[name] return def and def.liquidtype and def.liquidtype ~= "none" end local blood = {"mech_blood_1.png","mech_blood_2.png","mech_blood_3.png","mech_blood_4.png"} -- Entity check FIRST for _, obj in ipairs(minetest.get_objects_inside_radius(pos, 1.0)) do if obj ~= self.object then local skip = false if obj:is_player() then if obj:get_player_name() == self.shooter_name then skip = true end else local ent = obj:get_luaentity() if ent then local n = ent.name or "" if (self.shooter_name and ent._driver_name == self.shooter_name) or string.find(n, "plasma_bolt") or string.find(n, "mecha_bullet") or string.find(n, "node_shard") or string.find(n, "head_spider") or string.find(n, "head_2") or string.find(n, "glow_effect") then skip = true end else skip = true end end if not skip then minetest.add_particlespawner({ amount = 15, time = 0.3, minpos = pos, maxpos = pos, minvel = {x=-2,y=3,z=-2}, maxvel = {x=2,y=5,z=2}, minacc = {x=0,y=-15,z=0}, maxacc = {x=0,y=-20,z=0}, minexptime = 1, maxexptime = 1, minsize = 1, maxsize = 1.5, glow = 0, texture = blood[math.random(1,#blood)], }) if obj:is_player() then obj:set_hp(math.max(0, obj:get_hp() - MECH_BULLET_DAMAGE)) else obj:punch(obj, 1.0, {full_punch_interval=1.0, damage_groups={fleshy=MECH_BULLET_DAMAGE}}) end self.object:remove() return end end end -- Node check local node = minetest.get_node(pos) if node.name ~= "air" and not is_liquid(node.name) and node.name ~= "ignore" then minetest.add_particlespawner({ amount = 25, time = 0.3, minpos = pos, maxpos = pos, minvel = {x=-2,y=1,z=-2}, maxvel = {x=2,y=3,z=2}, minexptime = 0.4, maxexptime = 0.8, minsize = 4, maxsize = 6, texture = "smoke_puff.png", glow = 0, }) minetest.add_particlespawner({ amount = 25, time = 0.3, minpos = pos, maxpos = pos, minvel = {x=-2,y=3,z=-2}, maxvel = {x=2,y=5,z=2}, minacc = {x=0,y=-15,z=0}, maxacc = {x=0,y=-20,z=0}, minexptime = 0.3, maxexptime = 0.6, minsize = 4, maxsize = 4, texture = "spark.png", glow = 30, }) minetest.sound_play("bullet", {pos = pos, gain = 1.5, max_hear_distance = 32}) minetest.set_node(pos, {name = "air"}) local shard_obj = minetest.add_entity(pos, "assorted_mecha:node_shard") if shard_obj then local shard = shard_obj:get_luaentity() if shard then shard.original_node = node.name shard_obj:set_properties({wield_item = node.name}) shard_obj:set_velocity({ x = (math.random()-0.5)*8, y = 4 + math.random()*4, z = (math.random()-0.5)*8, }) end end self.object:remove() return end end, }) minetest.register_entity("assorted_mecha:node_shard", { initial_properties = { physical = true, collide_with_objects = false, collisionbox = {-0.3, -0.3, -0.3, 0.3, 0.3, 0.3}, visual = "wielditem", visual_size = {x = 0.5, y = 0.5}, textures = {""}, }, on_activate = function(self) self.timer = 0 self.rotation = {x = 0, y = 0, z = 0} self.spin_rate = { x = math.rad(math.random(-180, 180)), y = math.rad(math.random(-180, 180)), z = math.rad(math.random(-180, 180)), } -- gravity self.object:set_acceleration({x = 0, y = -3.5, z = 0}) end, on_step = function(self, dtime) self.timer = self.timer + dtime -- Visual spin self.rotation.x = self.rotation.x + self.spin_rate.x * dtime self.rotation.y = self.rotation.y + self.spin_rate.y * dtime self.rotation.z = self.rotation.z + self.spin_rate.z * dtime self.object:set_rotation(self.rotation) local pos = self.object:get_pos() local radius = 1.2 local nearby_node_found = false for dx = -1, 1 do for dy = -1, 0 do for dz = -1, 1 do local check_pos = vector.add(pos, {x = dx, y = dy, z = dz}) local dist = vector.distance(pos, check_pos) local node = minetest.get_node_or_nil(check_pos) if node and node.name ~= "air" and dist <= radius then nearby_node_found = true break end end if nearby_node_found then break end end if nearby_node_found then break end end if nearby_node_found or self.timer >= 8 then minetest.add_item(pos, self.original_node or "default:dirt") -- **Play original node's place sound before removal** if self.original_node then local node_def = minetest.registered_nodes[self.original_node] if node_def and node_def.sounds and node_def.sounds.place then minetest.sound_play(node_def.sounds.place.name, {pos = pos, gain = 0.5}) end end self.object:remove() end end, }) minetest.register_entity("assorted_mecha:plasma_bolt", { initial_properties = { physical = true, visual_size = {x = 0.2, y = 0.2, z = 0.2}, visual = "sprite", textures = {"plasma_bolt.png"}, glow = 50, collisionbox = {0, 0, 0, 0, 0, 0}, }, on_activate = function(self) self.shooter_name = nil minetest.after(3, function() if self.object then self.object:remove() end end) end, on_step = function(self, dtime) local pos = self.object:get_pos() if not pos then return end local function is_liquid(name) local def = minetest.registered_nodes[name] return def and def.liquidtype and def.liquidtype ~= "none" end local function do_explosion(epos) minetest.add_particlespawner({ amount = 45, time = 0.3, minpos = epos, maxpos = epos, minvel = {x=-4,y=2,z=-4}, maxvel = {x=4,y=6,z=4}, minexptime = 0.8, maxexptime = 1.6, minsize = 8, maxsize = 12, texture = "smoke_puff.png", glow = 0, }) minetest.add_particlespawner({ amount = 45, time = 0.3, minpos = epos, maxpos = epos, minvel = {x=-4,y=6,z=-4}, maxvel = {x=4,y=10,z=4}, minacc = {x=0,y=-15,z=0}, maxacc = {x=0,y=-20,z=0}, minexptime = 0.6, maxexptime = 1.2, minsize = 8, maxsize = 8, texture = "spark.png", glow = 30, }) minetest.sound_play("bomber_ex", {pos = epos, gain = 2.0, max_hear_distance = 64}) local radius = 6 for dx = -radius, radius do for dy = -radius, radius do for dz = -radius, radius do if dx*dx + dy*dy + dz*dz <= radius*radius then local tpos = vector.add(epos, {x=dx, y=dy, z=dz}) local tnode = minetest.get_node(tpos) if tnode.name ~= "air" and not is_liquid(tnode.name) and tnode.name ~= "ignore" then minetest.set_node(tpos, {name = "air"}) local so = minetest.add_entity(tpos, "assorted_mecha:node_shard") if so then local se = so:get_luaentity() if se then se.original_node = tnode.name so:set_properties({wield_item = tnode.name}) so:set_velocity({ x = (math.random()-0.5)*10, y = 5 + math.random()*6, z = (math.random()-0.5)*10, }) end end end end end end end for _, obj in ipairs(minetest.get_objects_inside_radius(epos, radius)) do if obj:is_player() then if obj:get_player_name() ~= self.shooter_name then obj:set_hp(math.max(0, obj:get_hp() - MECH_EXPLOSION_DAMAGE)) end else local ent = obj:get_luaentity() if ent then local n = ent.name or "" local own = self.shooter_name and ent._driver_name == self.shooter_name local proj = string.find(n,"plasma_bolt") or string.find(n,"mecha_bullet") or string.find(n,"node_shard") if not own and not proj then obj:punch(obj, 1.0, {full_punch_interval=1.0, damage_groups={fleshy=MECH_EXPLOSION_DAMAGE}}) end end end end self.object:remove() end -- Entity check FIRST (prevents tunneling) for _, obj in ipairs(minetest.get_objects_inside_radius(pos, 1.5)) do if obj ~= self.object then local skip = false if obj:is_player() then if obj:get_player_name() == self.shooter_name then skip = true end else local ent = obj:get_luaentity() if ent then local n = ent.name or "" if (self.shooter_name and ent._driver_name == self.shooter_name) or string.find(n,"plasma_bolt") or string.find(n,"mecha_bullet") or string.find(n,"node_shard") or string.find(n,"head_spider") or string.find(n,"head_2") or string.find(n,"glow_effect") then skip = true end else skip = true end end if not skip then do_explosion(pos) return end end end -- Node impact local node = minetest.get_node(pos) if node.name ~= "air" and not is_liquid(node.name) and node.name ~= "ignore" then do_explosion(pos) return end end, }) -- Chat command to open the control panel of the nearest mech -- Usage: /mecha_panel (finds nearest mech within 8 nodes) minetest.register_chatcommand("mecha_panel", { params = "", description = "Opens the control panel of the nearest mech", func = function(name) local player = minetest.get_player_by_name(name) if not player then return false, "Player not found" end local pos = player:get_pos() local closest = nil local closest_dist = math.huge for _, obj in ipairs(minetest.get_objects_inside_radius(pos, 8)) do if not obj:is_player() then local ent = obj:get_luaentity() if ent and ent.name and string.find(ent.name, "assorted_mecha:mech") then local d = vector.distance(pos, obj:get_pos()) if d < closest_dist then closest = ent closest_dist = d end end end end if closest then assorted_mecha_open_panel(closest, player) return true, "" else return false, "No mech within range (8 nodes)" end end, }) minetest.register_chatcommand("clear_foliage", { params = "", description = "Removes all trees, logs, leaves, or saplings within a 40-block radius.", privs = {server = true}, func = function(name) local player = minetest.get_player_by_name(name) if not player then return end local pos = player:get_pos() local radius = 40 for x = -radius, radius do for y = -radius, radius do for z = -radius, radius do local check_pos = vector.add(pos, {x=x, y=y, z=z}) local node = minetest.get_node(check_pos) local node_name = node.name -- Remove nodes containing keywords if string.find(node_name, "tree") or string.find(node_name, "log") or string.find(node_name, "leaves") or string.find(node_name, "sapling") then minetest.set_node(check_pos, {name = "air"}) end end end end minetest.chat_send_player(name, "Cleared all trees, logs, leaves, and saplings within 40 blocks!") end })