488 lines
16 KiB
Lua
488 lines
16 KiB
Lua
--[[
|
|
X Tumbleweed. Adds tumbleweeds.
|
|
Copyright (C) 2023 SaKeL <juraj.vajda@gmail.com>
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2.1 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library; if not, write to juraj.vajda@gmail.com
|
|
--]]
|
|
|
|
local S = minetest.get_translator(minetest.get_current_modname())
|
|
|
|
---@type XTumbleweed
|
|
XTumbleweed = {
|
|
settings = {
|
|
abr = minetest.get_mapgen_setting('active_block_range') or 4 --[[@as number]]
|
|
},
|
|
spawn_reduction = 0.5,
|
|
spawn_rate = 0.5,
|
|
allowed_biomes = {
|
|
'desert',
|
|
'sandstone_desert',
|
|
'cold_desert'
|
|
},
|
|
sounds = {
|
|
neutral = 'x_tumbleweed_tumbleweed'
|
|
}
|
|
}
|
|
|
|
---Check if indexed table contains a value
|
|
---@param table table
|
|
---@param value string|number|integer
|
|
---@return boolean
|
|
local function tableContains(table, value)
|
|
local found = false
|
|
|
|
if not table or type(table) ~= 'table' then
|
|
return found
|
|
end
|
|
|
|
for _, v in ipairs(table) do
|
|
if v == value then
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
|
|
return found
|
|
end
|
|
|
|
---@diagnostic disable-next-line: unused-local
|
|
function XTumbleweed.register_entity(self, name, def)
|
|
local _def = def or {}
|
|
|
|
-- common props
|
|
_def.initial_properties = {
|
|
physical = true,
|
|
collide_with_objects = true,
|
|
collisionbox = { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 },
|
|
selectionbox = { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5, rotate = true },
|
|
visual = 'mesh',
|
|
mesh = 'x_tumbleweed_tumbleweed.b3d',
|
|
textures = { 'x_tumbleweed_tumbleweed_1.png' },
|
|
backface_culling = false,
|
|
visual_size = { x = 1, y = 1, z = 1 },
|
|
hp_max = 10,
|
|
infotext = S('Tumbleweed')
|
|
}
|
|
_def.static_save = true
|
|
_def.makes_footstep_sound = true
|
|
_def.on_step = mobkit.stepfunc
|
|
_def.on_activate = mobkit.actfunc
|
|
_def.get_staticdata = mobkit.statfunc
|
|
|
|
-- api props
|
|
_def.springiness = 0
|
|
_def.buoyancy = 0.5
|
|
_def.max_speed = 2
|
|
_def.jump_height = 1.3
|
|
_def.view_range = 5
|
|
_def.lung_capacity = 1
|
|
_def.max_hp = 10
|
|
_def.timeout = 600
|
|
_def.armor_groups = { fleshy = 10 }
|
|
_def.animation = {
|
|
walk = { range = { x = 1, y = 30 }, speed = 30, loop = true },
|
|
stand = { range = { x = 31, y = 32 }, speed = 0, loop = false }
|
|
}
|
|
_def.brainfunc = function(...)
|
|
XTumbleweed:brainfunc(...)
|
|
end
|
|
_def.on_punch = function(...)
|
|
XTumbleweed:on_punch(...)
|
|
end
|
|
|
|
minetest.register_entity(name, _def)
|
|
end
|
|
|
|
---@diagnostic disable-next-line: unused-local
|
|
function XTumbleweed.brainfunc(self, selfObj)
|
|
local pos = selfObj.object:get_pos()
|
|
mobkit.vitals(selfObj)
|
|
|
|
-- dead
|
|
if selfObj.hp <= 0 or selfObj.isinliquid then
|
|
mobkit.clear_queue_high(selfObj)
|
|
mobkit.hq_die(selfObj)
|
|
|
|
-- multiply
|
|
if math.random(1, 3) == 3 and not selfObj.isinliquid and not selfObj.is_child then
|
|
local angle, posadd
|
|
angle = math.random(0, math.pi * 2)
|
|
|
|
for _ = 1, 4 do
|
|
posadd = { x = math.cos(angle), y = 0, z = math.sin(angle) }
|
|
posadd = vector.normalize(posadd)
|
|
local mobname = 'x_tumbleweed:tumbleweed'
|
|
local obj = minetest.add_entity(vector.add(pos, posadd), mobname)
|
|
|
|
if obj then
|
|
local lua_ent = obj:get_luaentity()
|
|
lua_ent.is_child = true
|
|
|
|
obj:set_properties({
|
|
visual_size = {
|
|
x = 1 * 0.5,
|
|
y = 1 * 0.5,
|
|
z = 1 * 0.5
|
|
},
|
|
collisionbox = {
|
|
-0.5 * 0.5,
|
|
-0.5 * 0.5,
|
|
-0.5 * 0.5,
|
|
0.5 * 0.5,
|
|
0.5 * 0.5,
|
|
0.5 * 0.5
|
|
},
|
|
selectionbox = {
|
|
-0.5 * 0.5,
|
|
-0.5 * 0.5,
|
|
-0.5 * 0.5,
|
|
0.5 * 0.5,
|
|
0.5 * 0.5,
|
|
0.5 * 0.5,
|
|
rotate = true
|
|
}
|
|
})
|
|
obj:set_yaw(angle - math.pi / 2)
|
|
angle = angle + math.pi / 2
|
|
end
|
|
end
|
|
end
|
|
|
|
minetest.after(4.5, function()
|
|
local pos_after = selfObj.object:get_pos()
|
|
|
|
minetest.add_particlespawner({
|
|
amount = 9,
|
|
time = 1,
|
|
minpos = pos_after,
|
|
maxpos = pos_after,
|
|
minvel = { x = -3, y = 2, z = -3 },
|
|
maxvel = { x = 3, y = 3, z = 3 },
|
|
minacc = vector.new({ x = -3, y = 2, z = -3 }),
|
|
maxacc = vector.new({ x = 3, y = 3, z = 3 }),
|
|
minexptime = 0.5,
|
|
maxexptime = 1,
|
|
minsize = 16,
|
|
maxsize = 24,
|
|
texture = 'x_tumbleweed_death_particle_animated.png',
|
|
animation = {
|
|
type = 'vertical_frames',
|
|
aspect_w = 16,
|
|
aspect_h = 16,
|
|
length = 1,
|
|
},
|
|
})
|
|
|
|
-- drop items
|
|
local puncher = selfObj._puncher
|
|
local death_by_player = puncher and puncher:is_player()
|
|
local biome_data = minetest.get_biome_data(pos)
|
|
local biome_name = ''
|
|
|
|
if biome_data then
|
|
biome_name = minetest.get_biome_name(biome_data.biome) or ''
|
|
end
|
|
|
|
if death_by_player and pos_after then
|
|
-- find suitable drop items
|
|
local registered_decorations_filtered = { 'default:stick' }
|
|
|
|
for _, v in pairs(minetest.registered_decorations) do
|
|
local registered_biome_names = v.biomes
|
|
|
|
if type(registered_biome_names) == 'string' then
|
|
registered_biome_names = { registered_biome_names }
|
|
end
|
|
|
|
---only for 'simple' decoration types
|
|
if v.deco_type == 'simple' then
|
|
---filter based on biome name in `biomes` table and node name in `place_on` table
|
|
if tableContains(registered_biome_names, biome_name) then
|
|
local item_names = v.decoration
|
|
|
|
if type(item_names) == 'string' then
|
|
item_names = { item_names }
|
|
end
|
|
|
|
for _, item_name in ipairs(item_names) do
|
|
local item_name_result = item_name
|
|
|
|
if minetest.registered_nodes[item_name_result] and minetest.registered_nodes[item_name_result].drop then
|
|
item_name_result = minetest.registered_nodes[item_name_result].drop
|
|
elseif minetest.registered_items[item_name_result] and minetest.registered_items[item_name_result].drop then
|
|
item_name_result = minetest.registered_items[item_name_result].drop
|
|
end
|
|
|
|
if type(item_name_result) == 'table' then
|
|
if item_name_result.items then
|
|
local random_item = item_name_result.items[math.random(1, #item_name_result.items)]
|
|
|
|
item_name_result = random_item.items[math.random(1, #random_item.items)]
|
|
end
|
|
end
|
|
|
|
if not tableContains(registered_decorations_filtered, item_name_result) then
|
|
table.insert(registered_decorations_filtered, item_name_result)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local wield_stack = puncher:get_wielded_item()
|
|
local tool_capabilities = wield_stack:get_tool_capabilities()
|
|
local chance = math.random(1, tool_capabilities.max_drop_level)
|
|
|
|
for _ = 1, math.random(1, 3) do
|
|
local stack_name = registered_decorations_filtered[1]
|
|
|
|
if #registered_decorations_filtered > 1 then
|
|
local rand_num = math.random(1, #registered_decorations_filtered)
|
|
stack_name = registered_decorations_filtered[rand_num]
|
|
end
|
|
|
|
if type(stack_name) == 'string' then
|
|
local stack = ItemStack({
|
|
name = stack_name,
|
|
count = math.random(3) * chance
|
|
})
|
|
local stack_obj = minetest.add_item(
|
|
{ x = pos_after.x, y = pos_after.y + 1, z = pos_after.z },
|
|
stack
|
|
)
|
|
|
|
if stack_obj then
|
|
stack_obj:set_velocity({
|
|
x = math.random(-1, 1),
|
|
y = 2,
|
|
z = math.random(-1, 1),
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
return
|
|
end
|
|
|
|
-- alive
|
|
if mobkit.is_queue_empty_low(selfObj) then
|
|
local yaw = selfObj.object:get_yaw()
|
|
|
|
if not yaw then
|
|
return
|
|
end
|
|
|
|
local dir_x = -math.sin(yaw) * (0.5 + 0.5)
|
|
local dir_z = math.cos(yaw) * (0.5 + 0.5)
|
|
local ypos = pos.y - 0.5
|
|
local will_fall = false
|
|
|
|
local not_blocking_sight = minetest.line_of_sight(
|
|
{ x = pos.x + dir_x, y = ypos, z = pos.z + dir_z },
|
|
{ x = pos.x + dir_x, y = ypos - 1, z = pos.z + dir_z })
|
|
|
|
if not_blocking_sight then
|
|
will_fall = true
|
|
end
|
|
|
|
if will_fall then
|
|
mobkit.animate(selfObj, 'walk')
|
|
mobkit.go_forward_horizontal(selfObj, 2)
|
|
else
|
|
mobkit.lq_dumbjump(selfObj, 1.3, 'walk')
|
|
|
|
minetest.add_particlespawner({
|
|
amount = 6,
|
|
time = 0.5,
|
|
minpos = {
|
|
x = pos.x - 0.5,
|
|
y = pos.y + 0.1,
|
|
z = pos.z - 0.5
|
|
},
|
|
maxpos = {
|
|
x = pos.x + 0.5,
|
|
y = pos.y + 0.1,
|
|
z = pos.z + 0.5
|
|
},
|
|
minvel = { x = 0, y = 5, z = 0 },
|
|
maxvel = { x = 0, y = 5, z = 0 },
|
|
minacc = { x = 0, y = -13, z = 0 },
|
|
maxacc = { x = 0, y = -13, z = 0 },
|
|
minexptime = 0.5,
|
|
maxexptime = 1,
|
|
minsize = 0.5,
|
|
maxsize = 1.5,
|
|
vertical = false,
|
|
texture = selfObj.object:get_properties().textures[1],
|
|
collisiondetection = true
|
|
})
|
|
end
|
|
end
|
|
|
|
if mobkit.timer(selfObj, 1) then
|
|
if (#selfObj.colinfo.collisions == 1 and selfObj.colinfo.collisions[1].axis ~= 'y')
|
|
or #selfObj.colinfo.collisions > 1
|
|
then
|
|
-- turn around randomly when stuck
|
|
selfObj.object:set_yaw(math.rad(math.random(1, 360)))
|
|
end
|
|
end
|
|
end
|
|
|
|
---@diagnostic disable-next-line: unused-local
|
|
function XTumbleweed.on_punch(self, selfObj, puncher, time_from_last_punch, tool_capabilities, dir)
|
|
if mobkit.is_alive(selfObj) then
|
|
minetest.sound_play({
|
|
name = self.sounds.neutral
|
|
}, {
|
|
object = selfObj.object
|
|
})
|
|
|
|
local hvel = vector.multiply(vector.normalize({ x = dir.x, y = 0, z = dir.z }), 4)
|
|
selfObj._puncher = puncher
|
|
selfObj.object:set_velocity({ x = hvel.x, y = 2, z = hvel.z })
|
|
|
|
local damage = tool_capabilities and tool_capabilities.damage_groups.fleshy or 1
|
|
|
|
mobkit.hurt(selfObj, damage)
|
|
|
|
selfObj.object:set_yaw(minetest.dir_to_yaw(dir))
|
|
end
|
|
end
|
|
|
|
function XTumbleweed.globalstep(self, dtime)
|
|
local abr = self.settings.abr
|
|
local spawn_reduction = self.spawn_reduction
|
|
local spawn_rate = self.spawn_rate
|
|
local spawnpos, liquidflag = mobkit.get_spawn_pos_abr(dtime, 3, abr * 16, spawn_rate, spawn_reduction)
|
|
|
|
if spawnpos and not liquidflag then
|
|
local biome_data = minetest.get_biome_data(spawnpos)
|
|
|
|
if not biome_data then
|
|
return
|
|
end
|
|
|
|
local biome_name = minetest.get_biome_name(biome_data.biome)
|
|
|
|
if not biome_name then
|
|
return
|
|
end
|
|
|
|
local objs = minetest.get_objects_inside_radius(spawnpos, abr * 16 * 1.1)
|
|
local tumbleweed_counter = 0
|
|
|
|
if not tableContains(self.allowed_biomes, biome_name) then
|
|
return
|
|
end
|
|
|
|
-- count mobs in abrange
|
|
for _, obj in ipairs(objs) do
|
|
if not obj:is_player() then
|
|
local luaent = obj:get_luaentity()
|
|
|
|
if luaent and luaent.name:find('x_tumbleweed:') then
|
|
if luaent.name == 'x_tumbleweed:tumbleweed' then
|
|
tumbleweed_counter = tumbleweed_counter + 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if tumbleweed_counter > 1 then
|
|
return
|
|
end
|
|
|
|
local mobname = 'x_tumbleweed:tumbleweed'
|
|
local obj = minetest.add_entity({ x = spawnpos.x, y = spawnpos.y + 0.5, z = spawnpos.z }, mobname)
|
|
|
|
-- camouflage texture
|
|
local spawn_node = minetest.get_node(vector.new(spawnpos.x, spawnpos.y - 1, spawnpos.z))
|
|
local spawn_node_def = minetest.registered_nodes[spawn_node.name]
|
|
local tex = spawn_node_def.tiles
|
|
|
|
if spawn_node_def and spawn_node_def.walkable then
|
|
if type(tex) == 'table' then
|
|
tex = spawn_node_def.tiles[1]
|
|
end
|
|
|
|
if type(tex) == 'table' and (tex[1] or tex.name) then
|
|
tex = tex[1] and tex[1].name or tex.name
|
|
end
|
|
|
|
if tex then
|
|
tex = tex .. '^[mask:x_tumbleweed_tumbleweed_mask_' .. math.random(1, 3) .. '.png'
|
|
end
|
|
end
|
|
|
|
|
|
if obj then
|
|
local yaw = math.rad(math.random(1, 360))
|
|
local rand_num = math.random(50, 150) / 100
|
|
local rand = PcgRandom(spawnpos.x * spawnpos.y * spawnpos.z)
|
|
local obj_properties = {
|
|
visual_size = {
|
|
x = 1 * rand_num,
|
|
y = 1 * rand_num,
|
|
z = 1 * rand_num
|
|
},
|
|
collisionbox = {
|
|
-0.5 * rand_num,
|
|
-0.5 * rand_num,
|
|
-0.5 * rand_num,
|
|
0.5 * rand_num,
|
|
0.5 * rand_num,
|
|
0.5 * rand_num
|
|
},
|
|
selectionbox = {
|
|
-0.5 * rand_num,
|
|
-0.5 * rand_num,
|
|
-0.5 * rand_num,
|
|
0.5 * rand_num,
|
|
0.5 * rand_num,
|
|
0.5 * rand_num,
|
|
rotate = true
|
|
}
|
|
}
|
|
|
|
-- camouflage texture
|
|
if tex and rand:next(0, 100) < 25 then
|
|
obj_properties.textures = { tex }
|
|
end
|
|
|
|
obj:set_yaw(yaw)
|
|
obj:set_properties(obj_properties)
|
|
end
|
|
|
|
minetest.log(
|
|
'action',
|
|
'[x_tumbleweed] Spawned ' .. mobname .. ' at ' .. minetest.pos_to_string(spawnpos)
|
|
)
|
|
end
|
|
end
|
|
|
|
function XTumbleweed.add_allowed_biomes(self, biomes)
|
|
if not biomes or type(biomes) ~= 'table' then
|
|
return
|
|
end
|
|
|
|
for _, biome_name in ipairs(biomes) do
|
|
if not tableContains(self.allowed_biomes, biome_name) then
|
|
table.insert(self.allowed_biomes, biome_name)
|
|
end
|
|
end
|
|
end
|