1255 lines
39 KiB
Lua
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
|
|
}) |