Files
assorted-mecha-fork/init.lua
2026-03-06 10:50:54 +00:00

1255 lines
39 KiB
Lua

-- 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
})