Introduce classes and reusable OOP

This commit is contained in:
Juraj Vajda 2022-10-20 09:13:35 -04:00
parent a1ab39bfb8
commit bf833c4575
13 changed files with 2289 additions and 1864 deletions

View File

@ -3,59 +3,62 @@ allow_defined_top = true
max_line_length = false
globals = {
'x_bows'
'XBows',
'XBowsQuiver',
'XBowsEntityDefBase',
'XBowsEntityDefCustom'
}
read_globals = {
"DIR_DELIM", "INIT",
"DIR_DELIM", "INIT",
"minetest", "core",
"dump", "dump2",
"minetest", "core",
"dump", "dump2",
"Raycast",
"Settings",
"PseudoRandom",
"PerlinNoise",
"VoxelManip",
"SecureRandom",
"VoxelArea",
"PerlinNoiseMap",
"PcgRandom",
"ItemStack",
"AreaStore",
"unpack",
"Raycast",
"Settings",
"PseudoRandom",
"PerlinNoise",
"VoxelManip",
"SecureRandom",
"VoxelArea",
"PerlinNoiseMap",
"PcgRandom",
"ItemStack",
"AreaStore",
"unpack",
"vector",
"vector",
table = {
fields = {
"copy",
"indexof",
"insert_all",
"key_value_swap",
"shuffle",
}
},
table = {
fields = {
"copy",
"indexof",
"insert_all",
"key_value_swap",
"shuffle",
}
},
string = {
fields = {
"split",
"trim",
}
},
string = {
fields = {
"split",
"trim",
}
},
math = {
fields = {
"hypot",
"sign",
"factorial"
}
},
math = {
fields = {
"hypot",
"sign",
"factorial"
}
},
"player_monoids",
"playerphysics",
"hb",
"mesecon",
"armor",
"default"
"player_monoids",
"playerphysics",
"hb",
"mesecon",
"armor",
"default"
}

1863
api.lua Normal file

File diff suppressed because it is too large Load Diff

598
arrow.lua
View File

@ -1,597 +1 @@
---Gets total armor level from 3d armor
---@param player ObjectRef
---@return integer
local function get_3d_armor_armor(player)
local armor_total = 0
if not player:is_player() or not minetest.get_modpath('3d_armor') or not armor.def[player:get_player_name()] then
return armor_total
end
armor_total = armor.def[player:get_player_name()].level
return armor_total
end
---Limits number `x` between `min` and `max` values
---@param x integer
---@param min integer
---@param max integer
---@return integer
local function limit(x, min, max)
return math.min(math.max(x, min), max)
end
---Gets collision box
---@param obj ObjectRef
---@return number[]
local function get_obj_box(obj)
local box
if obj:is_player() then
box = obj:get_properties().collisionbox or {-0.5, 0.0, -0.5, 0.5, 1.0, 0.5}
else
box = obj:get_luaentity().collisionbox or {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5}
end
return box
end
---Poison Arrow Effects
---@param tick integer|number
---@param time integer|number
---@param time_left integer|number
---@param arrow_obj ObjectRef
---@param target_obj ObjectRef
---@param old_damage_texture_modifier string
---@param punch_def table
function x_bows.poison_effect(tick, time, time_left, arrow_obj, target_obj, old_damage_texture_modifier, punch_def)
if not arrow_obj or target_obj:get_hp() <= 0 then
return
end
target_obj:set_properties({damage_texture_modifier = '^[colorize:#00FF0050'})
time_left = time_left + tick
if time_left <= time then
minetest.after(
tick,
x_bows.poison_effect,
tick,
time,
time_left,
arrow_obj,
target_obj,
old_damage_texture_modifier,
punch_def
)
elseif target_obj:is_player() then
if x_bows.hbhunger then
-- Reset HUD bar color
hb.change_hudbar(target_obj, 'health', nil, nil, 'hudbars_icon_health.png', nil, 'hudbars_bar_health.png')
end
if old_damage_texture_modifier then
target_obj:set_properties({damage_texture_modifier = old_damage_texture_modifier})
end
-- return
else
-- local lua_ent = target_obj:get_luaentity()
-- if not lua_ent then
-- return
-- end
-- lua_ent[arrow_obj.arrow .. '_active'] = false
if old_damage_texture_modifier then
target_obj:set_properties({damage_texture_modifier = old_damage_texture_modifier})
end
-- return
end
local _damage = punch_def.tool_capabilities.damage_groups.fleshy
if target_obj:get_hp() - _damage > 0 then
target_obj:punch(
punch_def.puncher,
punch_def.time_from_last_punch,
punch_def.tool_capabilities
)
local target_obj_pos = target_obj:get_pos()
if target_obj_pos then
x_bows.particle_effect(target_obj_pos, 'arrow_tipped')
end
end
end
-- Main Arrow Entity
minetest.register_entity('x_bows:arrow_entity', {
initial_properties = {
visual = 'wielditem',
collisionbox = {0, 0, 0, 0, 0, 0},
selectionbox = {0, 0, 0, 0, 0, 0},
physical = false,
textures = {'air'},
hp_max = 1
},
---@param self table
---@param staticdata string
on_activate = function(self, staticdata)
if not self or not staticdata or staticdata == '' then
self.object:remove()
return
end
local _staticdata = minetest.deserialize(staticdata)
-- set/reset - do not inherit from previous entity table
self._velocity = {x = 0, y = 0, z = 0}
self._old_pos = nil
self._attached = false
self._attached_to = {
type = '',
pos = nil
}
self._has_particles = false
self._lifetimer = 60
self._nodechecktimer = 0.5
self._is_drowning = false
self._in_liquid = false
self._poison_arrow = false
self._shot_from_pos = self.object:get_pos()
self.arrow = _staticdata.arrow
self.user = minetest.get_player_by_name(_staticdata.user_name)
self._tflp = _staticdata._tflp
self._tool_capabilities = _staticdata._tool_capabilities
self._is_critical_hit = _staticdata.is_critical_hit
self._faster_arrows_multiplier = _staticdata.faster_arrows_multiplier
self._add_damage = _staticdata.add_damage
if self.arrow == 'x_bows:arrow_diamond_tipped_poison' then
self._poison_arrow = true
end
self.object:set_properties({
textures = {'x_bows:arrow_node'},
infotext = self.arrow
})
end,
---@param self table
---@param killer ObjectRef|nil
on_death = function(self, killer)
if not self._old_pos then
self.object:remove()
return
end
minetest.item_drop(ItemStack(self.arrow), nil, vector.round(self._old_pos))
end,
---@param self table
---@param dtime integer|number
on_step = function(self, dtime)
local pos = self.object:get_pos()
self._old_pos = self._old_pos or pos
local ray = minetest.raycast(self._old_pos, pos, true, true)
local pointed_thing = ray:next()
self._lifetimer = self._lifetimer - dtime
self._nodechecktimer = self._nodechecktimer - dtime
-- adjust pitch when flying
if not self._attached then
local velocity = self.object:get_velocity()
local v_rotation = self.object:get_rotation()
local pitch = math.atan2(velocity.y, math.sqrt(velocity.x^2 + velocity.z^2))
self.object:set_rotation({
x = pitch,
y = v_rotation.y,
z = v_rotation.z
})
end
-- remove attached arrows after lifetime
if self._lifetimer <= 0 then
self.object:remove()
return
end
-- add particles only when not attached
if not self._attached and not self._in_liquid then
self._has_particles = true
if self._tflp >= self._tool_capabilities.full_punch_interval then
if self._is_critical_hit then
x_bows.particle_effect(self._old_pos, 'arrow_crit')
elseif self._faster_arrows_multiplier then
x_bows.particle_effect(self._old_pos, 'arrow_fast')
else
x_bows.particle_effect(self._old_pos, 'arrow')
end
end
end
-- remove attached arrows after object dies
if not self.object:get_attach() and self._attached_to.type == 'object' then
self.object:remove()
return
end
-- arrow falls down when not attached to node any more
if self._attached_to.type == 'node' and self._attached and self._nodechecktimer <= 0 then
local node = minetest.get_node(self._attached_to.pos)
self._nodechecktimer = 0.5
if not node then
return
end
if node.name == 'air' then
self.object:set_velocity({x = 0, y = -3, z = 0})
self.object:set_acceleration({x = 0, y = -3, z = 0})
-- reset values
self._attached = false
self._attached_to.type = ''
self._attached_to.pos = nil
self.object:set_properties({collisionbox = {0, 0, 0, 0, 0, 0}})
return
end
end
while pointed_thing do
local ip_pos = pointed_thing.intersection_point
local in_pos = pointed_thing.intersection_normal
self.pointed_thing = pointed_thing
if pointed_thing.type == 'object'
and pointed_thing.ref ~= self.object
and pointed_thing.ref:get_hp() > 0
and (
(pointed_thing.ref:is_player() and pointed_thing.ref:get_player_name() ~= self.user:get_player_name())
or (
pointed_thing.ref:get_luaentity()
and pointed_thing.ref:get_luaentity().physical
and pointed_thing.ref:get_luaentity().name ~= '__builtin:item'
)
)
and self.object:get_attach() == nil
then
if pointed_thing.ref:is_player() then
minetest.sound_play('x_bows_arrow_successful_hit', {
to_player = self.user:get_player_name(),
gain = 0.3
})
else
minetest.sound_play('x_bows_arrow_hit', {
to_player = self.user:get_player_name(),
gain = 0.6
})
end
-- store these here before punching in case pointed_thing.ref dies
local collisionbox = get_obj_box(pointed_thing.ref)
local xmin = collisionbox[1] * 100
local ymin = collisionbox[2] * 100
local zmin = collisionbox[3] * 100
local xmax = collisionbox[4] * 100
local ymax = collisionbox[5] * 100
local zmax = collisionbox[6] * 100
self.object:set_velocity({x = 0, y = 0, z = 0})
self.object:set_acceleration({x = 0, y = 0, z = 0})
-- calculate damage
local target_armor_groups = pointed_thing.ref:get_armor_groups()
local _damage = 0
if self._add_damage then
_damage = _damage + self._add_damage
end
for group, base_damage in pairs(self._tool_capabilities.damage_groups) do
_damage = _damage
+ base_damage
* limit(self._tflp / self._tool_capabilities.full_punch_interval, 0.0, 1.0)
* ((target_armor_groups[group] or 0) + get_3d_armor_armor(pointed_thing.ref)) / 100.0
end
-- crits
if self._is_critical_hit then
_damage = _damage * 2
end
-- knockback
local dir = vector.normalize(vector.subtract(self._shot_from_pos, ip_pos))
local distance = vector.distance(self._shot_from_pos, ip_pos)
local knockback = minetest.calculate_knockback(
pointed_thing.ref,
self.object,
self._tflp,
{
full_punch_interval = self._tool_capabilities.full_punch_interval,
damage_groups = {fleshy = _damage},
},
dir,
distance,
_damage
)
pointed_thing.ref:add_velocity({
x = dir.x * knockback * -1,
y = 7,
z = dir.z * knockback * -1
})
pointed_thing.ref:punch(
self.object,
self._tflp,
{
full_punch_interval = self._tool_capabilities.full_punch_interval,
damage_groups = {fleshy = _damage, knockback = knockback}
},
{
x = dir.x * -1,
y = 7,
z = dir.z * -1
}
)
-- already dead (entity)
if not pointed_thing.ref:get_luaentity() and not pointed_thing.ref:is_player() then
self.object:remove()
return
end
-- already dead (player)
if pointed_thing.ref:get_hp() <= 0 then
if x_bows.hbhunger then
-- Reset HUD bar color
hb.change_hudbar(pointed_thing.ref, 'health', nil, nil, 'hudbars_icon_health.png', nil, 'hudbars_bar_health.png')
end
self.object:remove()
return
end
-- attach arrow prepare
local rotation = {x = 0, y = 0, z = 0}
local position = {x = 0, y = 0, z = 0}
if in_pos.x == 1 then
-- x = 0
-- y = -90
-- z = 0
rotation.x = math.random(-10, 10)
rotation.y = math.random(-100, -80)
rotation.z = math.random(-10, 10)
position.x = xmax / 10
position.y = math.random(ymin, ymax) / 10
position.z = math.random(zmin, zmax) / 10
elseif in_pos.x == -1 then
-- x = 0
-- y = 90
-- z = 0
rotation.x = math.random(-10, 10)
rotation.y = math.random(80, 100)
rotation.z = math.random(-10, 10)
position.x = xmin / 10
position.y = math.random(ymin, ymax) / 10
position.z = math.random(zmin, zmax) / 10
elseif in_pos.y == 1 then
-- x = -90
-- y = 0
-- z = -180
rotation.x = math.random(-100, -80)
rotation.y = math.random(-10, 10)
rotation.z = math.random(-190, -170)
position.x = math.random(xmin, xmax) / 10
position.y = ymax / 10
position.z = math.random(zmin, zmax) / 10
elseif in_pos.y == -1 then
-- x = 90
-- y = 0
-- z = 180
rotation.x = math.random(80, 100)
rotation.y = math.random(-10, 10)
rotation.z = math.random(170, 190)
position.x = math.random(xmin, xmax) / 10
position.y = ymin / 10
position.z = math.random(zmin, zmax) / 10
elseif in_pos.z == 1 then
-- x = 180
-- y = 0
-- z = 180
rotation.x = math.random(170, 190)
rotation.y = math.random(-10, 10)
rotation.z = math.random(170, 190)
position.x = math.random(xmin, xmax) / 10
position.y = math.random(ymin, ymax) / 10
position.z = zmax / 10
elseif in_pos.z == -1 then
-- x = -180
-- y = 180
-- z = -180
rotation.x = math.random(-190, -170)
rotation.y = math.random(170, 190)
rotation.z = math.random(-190, -170)
position.x = math.random(xmin, xmax) / 10
position.y = math.random(ymin, ymax) / 10
position.z = zmin / 10
end
-- poison arrow
if self._poison_arrow then
local old_damage_texture_modifier = pointed_thing.ref:get_properties().damage_texture_modifier
local punch_def = {}
punch_def.puncher = self.object
punch_def.time_from_last_punch = self._tflp
punch_def.tool_capabilities = {
full_punch_interval = self._tool_capabilities.full_punch_interval,
damage_groups = {fleshy = _damage, knockback = knockback}
}
if pointed_thing.ref:is_player() then
-- @TODO missing `active` posion arrow check for player (see lua_ent below)
if x_bows.hbhunger then
-- Set poison bar
hb.change_hudbar(
pointed_thing.ref,
'health',
nil,
nil,
'hbhunger_icon_health_poison.png',
nil,
'hbhunger_bar_health_poison.png'
)
end
x_bows.poison_effect(1, 5, 0, self, pointed_thing.ref, old_damage_texture_modifier, punch_def)
else
-- local lua_ent = pointed_thing.ref:get_luaentity()
-- if not lua_ent[self.arrow .. '_active'] or lua_ent[self.arrow .. '_active'] == 'false' then
-- lua_ent[self.arrow .. '_active'] = true
x_bows.poison_effect(1, 5, 0, self, pointed_thing.ref, old_damage_texture_modifier, punch_def)
-- end
end
end
if not x_bows.settings.x_bows_attach_arrows_to_entities and not pointed_thing.ref:is_player() then
self.object:remove()
return
end
-- attach arrow
self.object:set_attach(
pointed_thing.ref,
'',
position,
rotation,
true
)
self._attached = true
self._attached_to.type = pointed_thing.type
self._attached_to.pos = position
-- remove last arrow when too many already attached
local children = {}
for _, object in ipairs(pointed_thing.ref:get_children()) do
if object:get_luaentity() and object:get_luaentity().name == 'x_bows:arrow_entity' then
table.insert(children, object)
end
end
if #children >= 5 then
children[1]:remove()
end
return
elseif pointed_thing.type == 'node' and not self._attached then
local node = minetest.get_node(pointed_thing.under)
local node_def = minetest.registered_nodes[node.name]
if not node_def then
return
end
self._velocity = self.object:get_velocity()
if node_def.drawtype == 'liquid' and not self._is_drowning then
self._is_drowning = true
self._in_liquid = true
local drag = 1 / (node_def.liquid_viscosity * 6)
self.object:set_velocity(vector.multiply(self._velocity, drag))
self.object:set_acceleration({x = 0, y = -1.0, z = 0})
x_bows.particle_effect(self._old_pos, 'bubble')
elseif self._is_drowning then
self._is_drowning = false
if self._velocity then
self.object:set_velocity(self._velocity)
end
self.object:set_acceleration({x = 0, y = -9.81, z = 0})
end
if x_bows.mesecons and node.name == 'x_bows:target' then
local distance = vector.distance(pointed_thing.under, ip_pos)
distance = math.floor(distance * 100) / 100
-- only close to the center of the target will trigger signal
if distance < 0.54 then
mesecon.receptor_on(pointed_thing.under)
minetest.get_node_timer(pointed_thing.under):start(2)
end
end
if node_def.walkable then
self.object:set_velocity({x=0, y=0, z=0})
self.object:set_acceleration({x=0, y=0, z=0})
self.object:set_pos(ip_pos)
self.object:set_rotation(self.object:get_rotation())
self._attached = true
self._attached_to.type = pointed_thing.type
self._attached_to.pos = pointed_thing.under
self.object:set_properties({collisionbox = {-0.2, -0.2, -0.2, 0.2, 0.2, 0.2}})
-- remove last arrow when too many already attached
local children = {}
for _, object in ipairs(minetest.get_objects_inside_radius(pointed_thing.under, 1)) do
if not object:is_player() and object:get_luaentity() and object:get_luaentity().name == 'x_bows:arrow_entity' then
table.insert(children, object)
end
end
if #children >= 5 then
children[#children]:remove()
end
minetest.sound_play('x_bows_arrow_hit', {
pos = pointed_thing.under,
gain = 0.6,
max_hear_distance = 16
})
return
end
end
pointed_thing = ray:next()
end
self._old_pos = pos
end,
---@param self table
---@param puncher ObjectRef|nil
---@param time_from_last_punch number|integer|nil
---@param tool_capabilities ToolCapabilitiesDef|nil
---@param dir Vector
---@param damage number|integer
on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
local wood_sound_def = default.node_sound_wood_defaults()
minetest.sound_play(wood_sound_def.dig.name, {
pos = self.object:get_pos(),
gain = wood_sound_def.dig.gain
})
return false
end,
})
XBows:register_entity('arrow_entity', {})

625
init.lua
View File

@ -8,636 +8,67 @@ default = default--[[@as MtgDefault]]
math.randomseed(tonumber(tostring(os.time()):reverse():sub(1, 9))--[[@as number]])
local path = minetest.get_modpath('x_bows')
local mod_start_time = minetest.get_us_time()
local bow_charged_timer = 0
---x_bows main class
---@class XBows
x_bows = {
pvp = minetest.settings:get_bool('enable_pvp') or false,
creative = minetest.settings:get_bool('creative_mode') or false,
mesecons = minetest.get_modpath('mesecons'),
hbhunger = minetest.get_modpath('hbhunger'),
playerphysics = minetest.get_modpath('playerphysics'),
player_monoids = minetest.get_modpath('player_monoids'),
registered_arrows = {},
registered_bows = {},
registered_quivers = {},
player_bow_sneak = {},
settings = {
x_bows_attach_arrows_to_entities = minetest.settings:get_bool('x_bows_attach_arrows_to_entities', false)
},
quiver = {
hud_item_ids = {},
after_job = {}
},
charge_sound_after_job = {}
}
---Shorthand for checking creative priv
---@param name string
---@return boolean
function x_bows.is_creative(name)
return x_bows.creative or minetest.check_player_privs(name, {creative = true})
end
---Reset charged bow to uncharged bow, this will return the arrow item to the inventory also
---@param player ObjectRef Player Ref
---@param includeWielded? boolean Will include reset for wielded bow also. default: `false`
---@return nil
local function reset_charged_bow(player, includeWielded)
local _includeWielded = includeWielded or false
local inv = player:get_inventory()
if inv and inv:contains_item('main', 'x_bows:bow_wood_charged') then
local inv_list = inv:get_list('main')
for i, st in ipairs(inv_list) do
local reset = _includeWielded or player:get_wield_index() ~= i
if not st:is_empty() and x_bows.registered_bows[st:get_name()] and reset then
local item_meta = st:get_meta()
local arrow_itemstack = ItemStack(minetest.deserialize(item_meta:get_string('arrow_itemstack_string')))
-- return arrow
if arrow_itemstack and not x_bows.is_creative(player:get_player_name()) then
if inv:room_for_item('main', {name=arrow_itemstack:get_name()}) then
inv:add_item('main', arrow_itemstack:get_name())
else
minetest.item_drop(ItemStack({name=arrow_itemstack:get_name(), count=1}), player, player:get_pos())
end
end
-- reset bow to uncharged bow
inv:set_stack('main', i, ItemStack({
name=x_bows.registered_bows[st:get_name()].name,
count=st:get_count(),
wear=st:get_wear()
}))
end
end
end
end
dofile(path .. '/api.lua')
dofile(path .. '/particle_effects.lua')
dofile(path .. '/nodes.lua')
dofile(path .. '/arrow.lua')
dofile(path .. '/items.lua')
dofile(path .. '/quiver.lua')
minetest.register_on_joinplayer(function(player)
reset_charged_bow(player, true)
x_bows.quiver.close_quiver(player)
XBows:reset_charged_bow(player, true)
XBowsQuiver:close_quiver(player)
end)
---Register bows
---@param name string
---@param def table
---@return boolean|nil
function x_bows.register_bow(name, def)
if name == nil or name == '' then
return false
end
def.name = 'x_bows:' .. name
def.name_charged = 'x_bows:' .. name .. '_charged'
def.description = def.description or name
def.uses = def.uses or 150
x_bows.registered_bows[def.name_charged] = def
-- not charged bow
minetest.register_tool(def.name, {
description = def.description .. '\n' .. minetest.colorize('#00FF00', 'Critical Arrow Chance: '
.. (1 / def.crit_chance) * 100 .. '%'),
inventory_image = def.inventory_image or 'x_bows_bow_wood.png',
on_place = x_bows.load,
on_secondary_use = x_bows.load,
groups = {bow = 1, flammable = 1}
})
-- charged bow
minetest.register_tool(def.name_charged, {
description = def.description .. '\n' .. minetest.colorize('#00FF00', 'Critical Arrow Chance: '
.. (1 / def.crit_chance) * 100 .. '%'),
inventory_image = def.inventory_image_charged or 'x_bows_bow_wood_charged.png',
on_use = x_bows.shoot,
groups = {bow = 1, flammable = 1, not_in_creative_inventory = 1},
on_drop = function(itemstack, dropper, pos)
local item_meta = itemstack:get_meta()
local arrow_itemstack = ItemStack(minetest.deserialize(item_meta:get_string('arrow_itemstack_string')))
-- return arrow
if arrow_itemstack and not x_bows.is_creative(dropper:get_player_name()) then
minetest.item_drop(ItemStack({name=arrow_itemstack:get_name(), count=1}), dropper, {x=pos.x + 0.5, y=pos.y + 0.5, z=pos.z + 0.5})
end
itemstack:set_name(def.name)
-- returns leftover itemstack
return minetest.item_drop(itemstack, dropper, pos)
end
})
-- recipes
if def.recipe then
minetest.register_craft({
output = def.name,
recipe = def.recipe
})
end
end
---Register arrows
---@param name string
---@param def table
---@return boolean|nil
function x_bows.register_arrow(name, def)
if name == nil or name == '' then
return false
end
def.name = 'x_bows:' .. name
def.description = def.description or name
x_bows.registered_arrows[def.name] = def
minetest.register_craftitem('x_bows:' .. name, {
description = def.description .. '\n' .. minetest.colorize('#00FF00', 'Damage: '
.. def.tool_capabilities.damage_groups.fleshy) .. '\n' .. minetest.colorize('#00BFFF', 'Charge Time: '
.. def.tool_capabilities.full_punch_interval .. 's'),
short_description = def.description,
inventory_image = def.inventory_image,
groups = {arrow = 1, flammable = 1}
})
-- recipes
if def.craft then
minetest.register_craft({
output = def.name ..' ' .. (def.craft_count or 4),
recipe = def.craft
})
end
end
---Register quivers
---@param name string
---@param def table
---@return boolean|nil
function x_bows.register_quiver(name, def)
if name == nil or name == '' then
return false
end
def.name = 'x_bows:' .. name
def.name_open = 'x_bows:' .. name .. '_open'
def.description = def.description or name
def.uses = def.uses or 150
x_bows.registered_quivers[def.name] = def
---closed quiver
minetest.register_tool(def.name, {
description = def.description
.. '\n' .. minetest.colorize('#00FF00', 'Faster Arrows: ' .. (1 / def.faster_arrows) * 100 .. '%')
.. '\n' .. minetest.colorize('#FF8080', 'Arrow Damage: +' .. def.add_damage),
short_description = def.short_description
.. '\n' .. minetest.colorize('#00FF00', 'Faster Arrows: ' .. (1 / def.faster_arrows) * 100 .. '%')
.. '\n' .. minetest.colorize('#FF8080', 'Arrow Damage: +' .. def.add_damage),
inventory_image = def.inventory_image or 'x_bows_quiver.png',
wield_image = def.wield_image or 'x_bows_quiver.png',
groups = {quiver = 1, flammable = 1},
on_secondary_use = function(itemstack, user, pointed_thing)
return x_bows.open_quiver(itemstack, user)
end,
---@param itemstack ItemStack
---@param placer ObjectRef
---@param pointed_thing PointedThingDef
---@return ItemStack|nil
on_place = function(itemstack, placer, pointed_thing)
if pointed_thing.under then
local node = minetest.get_node(pointed_thing.under)
local node_def = minetest.registered_nodes[node.name]
if node_def and node_def.on_rightclick then
return node_def.on_rightclick(pointed_thing.under, node, placer, itemstack, pointed_thing)
end
end
return x_bows.open_quiver(itemstack, placer)
end
})
---open quiver
minetest.register_tool(def.name_open, {
description = def.description
.. '\n' .. minetest.colorize('#00FF00', 'Faster Arrows: ' .. (1 / def.faster_arrows) * 100 .. '%')
.. '\n' .. minetest.colorize('#FF8080', 'Arrow Damage: +' .. def.add_damage),
short_description = def.short_description
.. '\n' .. minetest.colorize('#00FF00', 'Faster Arrows: ' .. (1 / def.faster_arrows) * 100 .. '%')
.. '\n' .. minetest.colorize('#FF8080', 'Arrow Damage: +' .. def.add_damage),
inventory_image = def.inventory_image_open or 'x_bows_quiver_open.png',
wield_image = def.wield_image_open or 'x_bows_quiver_open.png',
groups = {quiver = 1, flammable = 1, not_in_creative_inventory = 1},
---@param itemstack ItemStack
---@param dropper ObjectRef|nil
---@param pos Vector
---@return ItemStack
on_drop = function (itemstack, dropper, pos)
local replace_item = x_bows.quiver.get_replacement_item(itemstack, 'x_bows:quiver')
return minetest.item_drop(replace_item, dropper, pos)
end
})
-- recipes
if def.recipe then
minetest.register_craft({
output = def.name,
recipe = def.recipe
})
end
end
---Loads bow
---@param itemstack ItemStack
---@param user ObjectRef
---@param pointed_thing PointedThingDef
---@return ItemStack
function x_bows.load(itemstack, user, pointed_thing)
local player_name = user:get_player_name()
local inv = user:get_inventory()--[[@as InvRef]]
local inv_list = inv:get_list('main')
local bow_name = itemstack:get_name()
local bow_def = x_bows.registered_bows[bow_name .. '_charged']
---@alias ItemStackArrows {["stack"]: ItemStack, ["idx"]: number|integer}[]
---@type ItemStackArrows
local itemstack_arrows = {}
---trigger right click event if pointed item has one
if pointed_thing.under then
local node = minetest.get_node(pointed_thing.under)
local node_def = minetest.registered_nodes[node.name]
if node_def and node_def.on_rightclick then
return node_def.on_rightclick(pointed_thing.under, node, user, itemstack, pointed_thing)
end
end
---find itemstack arrow in quiver
local quiver_result = x_bows.quiver.get_itemstack_arrow_from_quiver(user)
local itemstack_arrow = quiver_result.found_arrow_stack
if itemstack_arrow then
local itemstack_arrow_meta = itemstack_arrow:get_meta()
itemstack_arrow_meta:set_int('is_arrow_from_quiver', 1)
itemstack_arrow_meta:set_string('quiver_name', quiver_result.quiver_name)
itemstack_arrow_meta:set_string('quiver_id', quiver_result.quiver_id)
else
x_bows.quiver.remove_hud(user)
---find itemstack arrow in players inventory
for i, st in ipairs(inv_list) do
if not st:is_empty() and x_bows.registered_arrows[st:get_name()] then
table.insert(itemstack_arrows, {stack = st, idx = i})
end
end
-- take 1st found arrow in the list
itemstack_arrow = #itemstack_arrows > 0 and itemstack_arrows[1].stack or nil
end
if itemstack_arrow and bow_def then
local _tool_capabilities = x_bows.registered_arrows[itemstack_arrow:get_name()].tool_capabilities
---@param v_user ObjectRef
---@param v_bow_name string
---@param v_itemstack_arrow ItemStack
---@param v_inv InvRef
---@param v_itemstack_arrows ItemStackArrows
minetest.after(0, function(v_user, v_bow_name, v_itemstack_arrow, v_inv, v_itemstack_arrows)
local wielded_item = v_user:get_wielded_item()
if wielded_item:get_name() == v_bow_name then
local wielded_item_meta = wielded_item:get_meta()
local v_itemstack_arrow_meta = v_itemstack_arrow:get_meta()
wielded_item_meta:set_string('arrow_itemstack_string', minetest.serialize(v_itemstack_arrow:to_table()))
wielded_item_meta:set_string('time_load', tostring(minetest.get_us_time()))
wielded_item:set_name(v_bow_name .. '_charged')
v_user:set_wielded_item(wielded_item)
if not x_bows.is_creative(v_user:get_player_name()) and v_itemstack_arrow_meta:get_int('is_arrow_from_quiver') ~= 1 then
v_itemstack_arrow:take_item()
v_inv:set_stack('main', v_itemstack_arrows[1].idx, v_itemstack_arrow)
end
end
end, user, bow_name, itemstack_arrow, inv, itemstack_arrows)
---stop previous charged sound after job
if x_bows.charge_sound_after_job[player_name] then
for _, v in pairs(x_bows.charge_sound_after_job[player_name]) do
v:cancel()
end
x_bows.charge_sound_after_job[player_name] = {}
else
x_bows.charge_sound_after_job[player_name] = {}
end
---sound plays when charge time reaches full punch interval time
table.insert(x_bows.charge_sound_after_job[player_name], minetest.after(_tool_capabilities.full_punch_interval, function(v_user, v_bow_name)
local wielded_item = v_user:get_wielded_item()
local wielded_item_name = wielded_item:get_name()
if wielded_item_name == v_bow_name .. '_charged' then
minetest.sound_play('x_bows_bow_loaded', {
to_player = v_user:get_player_name(),
gain = 0.6
})
end
end, user, bow_name))
minetest.sound_play('x_bows_bow_load', {
to_player = player_name,
gain = 0.6
})
return itemstack
end
return itemstack
end
---Shoots the bow
---@param itemstack ItemStack
---@param user ObjectRef
---@param pointed_thing? PointedThingDef
---@return ItemStack
function x_bows.shoot(itemstack, user, pointed_thing)
local time_shoot = minetest.get_us_time();
local meta = itemstack:get_meta()
local time_load = tonumber(meta:get_string('time_load'))
local tflp = (time_shoot - time_load) / 1000000
---@type ItemStack
local arrow_itemstack = ItemStack(minetest.deserialize(meta:get_string('arrow_itemstack_string')))
local arrow_itemstack_meta = arrow_itemstack:get_meta()
local arrow_name = arrow_itemstack:get_name()
local is_arrow_from_quiver = arrow_itemstack_meta:get_int('is_arrow_from_quiver')
local quiver_name = arrow_itemstack_meta:get_string('quiver_name')
local quiver_id = arrow_itemstack_meta:get_string('quiver_id')
local detached_inv = x_bows.quiver.get_or_create_detached_inv(
quiver_id,
user:get_player_name()
)
if is_arrow_from_quiver == 1 then
x_bows.quiver.udate_or_create_hud(user, detached_inv:get_list('main'))
else
x_bows.quiver.remove_hud(user)
end
if not x_bows.registered_arrows[arrow_name] then
return itemstack
end
local bow_name_charged = itemstack:get_name()
local bow_name = x_bows.registered_bows[bow_name_charged].name
local uses = x_bows.registered_bows[bow_name_charged].uses
local crit_chance = x_bows.registered_bows[bow_name_charged].crit_chance
local _tool_capabilities = x_bows.registered_arrows[arrow_name].tool_capabilities
local quiver_xbows_def = x_bows.registered_quivers[quiver_name]
local staticdata = {
arrow = arrow_name,
user_name = user:get_player_name(),
is_critical_hit = false,
_tool_capabilities = _tool_capabilities,
_tflp = tflp,
}
---crits, only on full punch interval
if crit_chance and crit_chance > 1 and tflp >= _tool_capabilities.full_punch_interval then
if math.random(1, crit_chance) == 1 then
staticdata.is_critical_hit = true
end
end
---speed multiply
if quiver_xbows_def and quiver_xbows_def.faster_arrows and quiver_xbows_def.faster_arrows > 1 then
staticdata.faster_arrows_multiplier = quiver_xbows_def.faster_arrows
end
---add damage
if quiver_xbows_def and quiver_xbows_def.add_damage and quiver_xbows_def.add_damage > 1 then
staticdata.add_damage = quiver_xbows_def.add_damage
end
---sound
local sound_name = 'x_bows_bow_shoot'
if staticdata.is_critical_hit then
sound_name = 'x_bows_bow_shoot_crit'
end
meta:set_string('arrow_itemstack_string', '')
itemstack:set_name(bow_name)
local pos = user:get_pos()
local dir = user:get_look_dir()
local obj = minetest.add_entity(
{
x = pos.x,
y = pos.y + 1.5,
z = pos.z
},
'x_bows:arrow_entity',
minetest.serialize(staticdata)
)
if not obj then
return itemstack
end
local strength_multiplier = tflp
if strength_multiplier > _tool_capabilities.full_punch_interval then
strength_multiplier = 1
---faster arrow, only on full punch interval
if staticdata.faster_arrows_multiplier then
strength_multiplier = strength_multiplier + (strength_multiplier / staticdata.faster_arrows_multiplier)
end
end
local strength = 30 * strength_multiplier
obj:set_velocity(vector.multiply(dir, strength))
obj:set_acceleration({x = dir.x * -3, y = -10, z = dir.z * -3})
obj:set_yaw(minetest.dir_to_yaw(dir))
if not x_bows.is_creative(user:get_player_name()) then
itemstack:add_wear(65535 / uses)
end
minetest.sound_play(sound_name, {
gain = 0.3,
pos = user:get_pos(),
max_hear_distance = 10
})
return itemstack
end
---Arrow particle
---@param pos Vector
---@param type 'arrow' | 'arrow_crit' | 'bubble' | 'arrow_tipped'
---@return number|nil
function x_bows.particle_effect(pos, type)
if type == 'arrow' then
return minetest.add_particlespawner({
amount = 1,
time = 0.1,
minpos = pos,
maxpos = pos,
minexptime = 1,
maxexptime = 1,
minsize = 2,
maxsize = 2,
texture = 'x_bows_arrow_particle.png',
animation = {
type = 'vertical_frames',
aspect_w = 8,
aspect_h = 8,
length = 1,
},
glow = 1
})
elseif type == 'arrow_crit' then
return minetest.add_particlespawner({
amount = 3,
time = 0.1,
minpos = pos,
maxpos = pos,
minexptime = 0.5,
maxexptime = 0.5,
minsize = 2,
maxsize = 2,
texture = 'x_bows_arrow_particle.png^[colorize:#B22222:127',
animation = {
type = 'vertical_frames',
aspect_w = 8,
aspect_h = 8,
length = 1,
},
glow = 1
})
elseif type == 'arrow_fast' then
return minetest.add_particlespawner({
amount = 3,
time = 0.1,
minpos = pos,
maxpos = pos,
minexptime = 0.5,
maxexptime = 0.5,
minsize = 2,
maxsize = 2,
texture = 'x_bows_arrow_particle.png^[colorize:#0000FF:64',
animation = {
type = 'vertical_frames',
aspect_w = 8,
aspect_h = 8,
length = 1,
},
glow = 1
})
elseif type == 'bubble' then
return minetest.add_particlespawner({
amount = 1,
time = 1,
minpos = pos,
maxpos = pos,
minvel = {x=1, y=1, z=0},
maxvel = {x=1, y=1, z=0},
minacc = {x=1, y=1, z=1},
maxacc = {x=1, y=1, z=1},
minexptime = 0.2,
maxexptime = 0.5,
minsize = 0.5,
maxsize = 1,
texture = 'x_bows_bubble.png'
})
elseif type == 'arrow_tipped' then
return minetest.add_particlespawner({
amount = 5,
time = 1,
minpos = vector.subtract(pos, 0.5),
maxpos = vector.add(pos, 0.5),
minexptime = 0.4,
maxexptime = 0.8,
minvel = {x=-0.4, y=0.4, z=-0.4},
maxvel = {x=0.4, y=0.6, z=0.4},
minacc = {x=0.2, y=0.4, z=0.2},
maxacc = {x=0.4, y=0.6, z=0.4},
minsize = 4,
maxsize = 6,
texture = 'x_bows_arrow_tipped_particle.png^[colorize:#008000:127',
animation = {
type = 'vertical_frames',
aspect_w = 8,
aspect_h = 8,
length = 1,
},
glow = 1
})
end
end
-- sneak, fov adjustments when bow is charged
minetest.register_globalstep(function(dtime)
bow_charged_timer = bow_charged_timer + dtime
if bow_charged_timer > 0.5 then
for _, player in ipairs(minetest.get_connected_players()) do
local name = player:get_player_name()
local stack = player:get_wielded_item()
local item = stack:get_name()
local player_name = player:get_player_name()
local wielded_stack = player:get_wielded_item()
local wielded_stack_name = wielded_stack:get_name()
if not item then
if not wielded_stack_name then
return
end
if not x_bows.player_bow_sneak[name] then
x_bows.player_bow_sneak[name] = {}
if not XBows.player_bow_sneak[player_name] then
XBows.player_bow_sneak[player_name] = {}
end
if item == 'x_bows:bow_wood_charged' and not x_bows.player_bow_sneak[name].sneak then
if x_bows.playerphysics then
playerphysics.add_physics_factor(player, 'speed', 'x_bows:bow_wood_charged', 0.25)
elseif x_bows.player_monoids then
player_monoids.speed:add_change(player, 0.25, 'x_bows:bow_wood_charged')
if minetest.get_item_group(wielded_stack_name, 'bow_charged') ~= 0 and not XBows.player_bow_sneak[player_name].sneak then
if XBows.playerphysics then
playerphysics.add_physics_factor(player, 'speed', 'x_bows:bow_charged_speed', 0.25)
elseif XBows.player_monoids then
player_monoids.speed:add_change(player, 0.25, 'x_bows:bow_charged_speed')
end
x_bows.player_bow_sneak[name].sneak = true
XBows.player_bow_sneak[player_name].sneak = true
player:set_fov(0.9, true, 0.4)
elseif item ~= 'x_bows:bow_wood_charged' and x_bows.player_bow_sneak[name].sneak then
if x_bows.playerphysics then
playerphysics.remove_physics_factor(player, 'speed', 'x_bows:bow_wood_charged')
elseif x_bows.player_monoids then
player_monoids.speed:del_change(player, 'x_bows:bow_wood_charged')
elseif minetest.get_item_group(wielded_stack_name, 'bow_charged') == 0 and XBows.player_bow_sneak[player_name].sneak then
if XBows.playerphysics then
playerphysics.remove_physics_factor(player, 'speed', 'x_bows:bow_charged_speed')
elseif XBows.player_monoids then
player_monoids.speed:del_change(player, 'x_bows:bow_charged_speed')
end
x_bows.player_bow_sneak[name].sneak = false
XBows.player_bow_sneak[player_name].sneak = false
player:set_fov(0, true, 0.4)
end
reset_charged_bow(player)
XBows:reset_charged_bow(player)
end
bow_charged_timer = 0
end
end)
local path = minetest.get_modpath('x_bows')
dofile(path .. '/nodes.lua')
dofile(path .. '/arrow.lua')
dofile(path .. '/items.lua')
dofile(path .. '/quiver.lua')
local mod_end_time = (minetest.get_us_time() - mod_start_time) / 1000000
print('[Mod] x_bows loaded.. ['.. mod_end_time ..'s]')

235
items.lua
View File

@ -1,149 +1,168 @@
x_bows.register_bow('bow_wood', {
XBows:register_bow('bow_wood', {
description = 'Wooden Bow',
uses = 385,
-- `crit_chance` 10% chance, 5 is 20% chance
-- (1 / crit_chance) * 100 = % chance
crit_chance = 10,
recipe = {
{'', 'default:stick', 'farming:string'},
{'default:stick', '', 'farming:string'},
{'', 'default:stick', 'farming:string'},
custom = {
uses = 385,
crit_chance = 10,
recipe = {
{'', 'default:stick', 'farming:string'},
{'default:stick', '', 'farming:string'},
{'', 'default:stick', 'farming:string'}
},
fuel_burntime = 3,
allowed_ammunition = {
'x_bows:arrow_wood',
'x_bows:arrow_stone',
'x_bows:arrow_bronze',
'x_bows:arrow_steel',
'x_bows:arrow_mese',
'x_bows:arrow_diamond',
'x_bows:arrow_diamond_tipped_poison'
}
}
})
x_bows.register_arrow('arrow_wood', {
XBows:register_arrow('arrow_wood', {
description = 'Arrow Wood',
inventory_image = 'x_bows_arrow_wood.png',
craft = {
{'default:flint'},
{'group:stick'},
{'group:wool'}
},
tool_capabilities = {
full_punch_interval = 1,
max_drop_level = 0,
damage_groups = {fleshy=2}
custom = {
recipe = {
{'default:flint'},
{'group:stick'},
{'group:wool'}
},
tool_capabilities = {
full_punch_interval = 1,
max_drop_level = 0,
damage_groups = {fleshy=2}
},
fuel_burntime = 1
}
})
x_bows.register_arrow('arrow_stone', {
XBows:register_arrow('arrow_stone', {
description = 'Arrow Stone',
inventory_image = 'x_bows_arrow_stone.png',
craft = {
{'default:flint'},
{'group:stone'},
{'group:wool'}
},
tool_capabilities = {
full_punch_interval = 1.2,
max_drop_level = 0,
damage_groups = {fleshy=4}
custom = {
recipe = {
{'default:flint'},
{'group:stone'},
{'group:wool'}
},
tool_capabilities = {
full_punch_interval = 1.2,
max_drop_level = 0,
damage_groups = {fleshy=4}
}
}
})
x_bows.register_arrow('arrow_bronze', {
XBows:register_arrow('arrow_bronze', {
description = 'Arrow Bronze',
inventory_image = 'x_bows_arrow_bronze.png',
craft = {
{'default:flint'},
{'default:bronze_ingot'},
{'group:wool'}
},
tool_capabilities = {
full_punch_interval = 0.8,
max_drop_level = 1,
damage_groups = {fleshy=6}
custom = {
recipe = {
{'default:flint'},
{'default:bronze_ingot'},
{'group:wool'}
},
tool_capabilities = {
full_punch_interval = 0.8,
max_drop_level = 1,
damage_groups = {fleshy=6}
}
}
})
x_bows.register_arrow('arrow_steel', {
XBows:register_arrow('arrow_steel', {
description = 'Arrow Steel',
inventory_image = 'x_bows_arrow_steel.png',
craft = {
{'default:flint'},
{'default:steel_ingot'},
{'group:wool'}
},
tool_capabilities = {
full_punch_interval = 0.7,
max_drop_level = 1,
damage_groups = {fleshy=6}
custom = {
recipe = {
{'default:flint'},
{'default:steel_ingot'},
{'group:wool'}
},
tool_capabilities = {
full_punch_interval = 0.7,
max_drop_level = 1,
damage_groups = {fleshy=6}
}
}
})
x_bows.register_arrow('arrow_mese', {
XBows:register_arrow('arrow_mese', {
description = 'Arrow Mese',
inventory_image = 'x_bows_arrow_mese.png',
craft = {
{'default:flint'},
{'default:mese_crystal'},
{'group:wool'}
},
tool_capabilities = {
full_punch_interval = 0.7,
max_drop_level = 1,
damage_groups = {fleshy=7}
custom = {
recipe = {
{'default:flint'},
{'default:mese_crystal'},
{'group:wool'}
},
tool_capabilities = {
full_punch_interval = 0.7,
max_drop_level = 1,
damage_groups = {fleshy=7}
}
}
})
x_bows.register_arrow('arrow_diamond', {
XBows:register_arrow('arrow_diamond', {
description = 'Arrow Diamond',
inventory_image = 'x_bows_arrow_diamond.png',
craft = {
{'default:flint'},
{'default:diamond'},
{'group:wool'}
},
tool_capabilities = {
full_punch_interval = 0.7,
max_drop_level = 1,
damage_groups = {fleshy=8}
custom = {
recipe = {
{'default:flint'},
{'default:diamond'},
{'group:wool'}
},
tool_capabilities = {
full_punch_interval = 0.7,
max_drop_level = 1,
damage_groups = {fleshy=8}
}
}
})
x_bows.register_arrow('arrow_diamond_tipped_poison', {
XBows:register_arrow('arrow_diamond_tipped_poison', {
description = 'Arrow Diamond Tipped Poison (0:05)',
inventory_image = 'x_bows_arrow_diamond_poison.png',
craft = {
{'', '', ''},
{'', 'default:marram_grass_1', ''},
{'', 'x_bows:arrow_diamond', ''}
},
tool_capabilities = {
full_punch_interval = 0.7,
max_drop_level = 1,
damage_groups = {fleshy=8}
},
craft_count = 1
custom = {
recipe = {
{'', '', ''},
{'', 'default:marram_grass_1', ''},
{'', 'x_bows:arrow_diamond', ''}
},
tool_capabilities = {
full_punch_interval = 0.7,
max_drop_level = 1,
damage_groups = {fleshy=8}
},
recipe_count = 1
}
})
x_bows.register_quiver('quiver', {
XBows:register_quiver('quiver', {
description = 'Quiver \n\n Empty\n',
short_description = 'Quiver',
recipe = {
{'group:arrow', 'group:arrow', 'group:arrow'},
{'group:arrow', 'wool:brown', 'group:arrow'},
{'group:arrow', 'group:arrow', 'group:arrow'}
},
craft_count = 1,
faster_arrows = 5,
add_damage = 2
})
minetest.register_craft({
type = 'fuel',
recipe = 'x_bows:bow_wood',
burntime = 3,
})
minetest.register_craft({
type = 'fuel',
recipe = 'x_bows:arrow_wood',
burntime = 1,
})
minetest.register_craft({
type = 'fuel',
recipe = 'x_bows:quiver',
burntime = 3,
custom = {
recipe = {
{'group:arrow', 'group:arrow', 'group:arrow'},
{'group:arrow', 'wool:brown', 'group:arrow'},
{'group:arrow', 'group:arrow', 'group:arrow'}
},
recipe_count = 1,
faster_arrows = 5,
add_damage = 2,
fuel_burntime = 3,
allowed_ammunition = {
'x_bows:arrow_wood',
'x_bows:arrow_stone',
'x_bows:arrow_bronze',
'x_bows:arrow_steel',
'x_bows:arrow_mese',
'x_bows:arrow_diamond',
'x_bows:arrow_diamond_tipped_poison'
}
}
})

View File

@ -10,22 +10,28 @@ minetest.register_node('x_bows:arrow_node', {
})
minetest.register_node('x_bows:target', {
description = 'Straw',
description = 'Target',
tiles = {'x_bows_target.png'},
is_ground_content = false,
groups = {snappy=3, flammable=4, fall_damage_add_percent=-30},
groups = {snappy=3, flammable=4, fall_damage_add_percent = -30},
sounds = default.node_sound_leaves_defaults(),
mesecons = {receptor = {state = 'off'}},
---@param pos Vector
---@param elapsed number
---@return boolean
on_timer = function (pos, elapsed)
mesecon.receptor_off(pos)
if XBows.mesecons then
mesecon.receptor_off(pos)
end
return false
end,
end
})
minetest.register_craft({
type = 'fuel',
recipe = 'x_bows:target',
burntime = 3,
burntime = 3
})
minetest.register_craft({

89
particle_effects.lua Normal file
View File

@ -0,0 +1,89 @@
XBows:register_particle_effect('arrow', {
amount = 1,
time = 0.1,
minexptime = 1,
maxexptime = 1,
minsize = 2,
maxsize = 2,
texture = 'x_bows_arrow_particle.png',
animation = {
type = 'vertical_frames',
aspect_w = 8,
aspect_h = 8,
length = 1,
},
glow = 1
})
XBows:register_particle_effect('arrow_crit', {
amount = 3,
time = 0.1,
minexptime = 0.5,
maxexptime = 0.5,
minsize = 2,
maxsize = 2,
texture = 'x_bows_arrow_particle.png^[colorize:#B22222:127',
animation = {
type = 'vertical_frames',
aspect_w = 8,
aspect_h = 8,
length = 1,
},
glow = 1
})
XBows:register_particle_effect('arrow_fast', {
amount = 3,
time = 0.1,
minexptime = 0.5,
maxexptime = 0.5,
minsize = 2,
maxsize = 2,
texture = 'x_bows_arrow_particle.png^[colorize:#0000FF:64',
animation = {
type = 'vertical_frames',
aspect_w = 8,
aspect_h = 8,
length = 1,
},
glow = 1
})
XBows:register_particle_effect('bubble', {
amount = 1,
time = 1,
minvel = {x=1, y=1, z=0},
maxvel = {x=1, y=1, z=0},
minacc = {x=1, y=1, z=1},
maxacc = {x=1, y=1, z=1},
minexptime = 0.2,
maxexptime = 0.5,
minsize = 0.5,
maxsize = 1,
texture = 'x_bows_bubble.png'
})
XBows:register_particle_effect('arrow_tipped', {
amount = 5,
time = 1,
minexptime = 0.4,
maxexptime = 0.8,
minvel = {x=-0.4, y=0.4, z=-0.4},
maxvel = {x=0.4, y=0.6, z=0.4},
minacc = {x=0.2, y=0.4, z=0.2},
maxacc = {x=0.4, y=0.6, z=0.4},
minsize = 4,
maxsize = 6,
texture = 'x_bows_arrow_tipped_particle.png^[colorize:#008000:127',
animation = {
type = 'vertical_frames',
aspect_w = 8,
aspect_h = 8,
length = 1,
},
glow = 1,
custom = {
minpos = {x = -0.5, y = -0.5, z = -0.5},
maxpos = {x = 0.5, y = 0.5, z = 0.5}
}
})

View File

@ -1,515 +1,5 @@
---Close one or all open quivers in players inventory
---@param player ObjectRef
---@param quiver_id? string If `nil` then all open quivers will be closed
---@returns nil
function x_bows.quiver.close_quiver(player, quiver_id)
local player_inv = player:get_inventory()
---find matching quiver item in players inventory with the open formspec name
if player_inv and player_inv:contains_item('main', 'x_bows:quiver_open') then
local inv_list = player_inv:get_list('main')
for i, st in ipairs(inv_list) do
local st_meta = st:get_meta()
if not st:is_empty() and st:get_name() == 'x_bows:quiver_open' then
if quiver_id and st_meta:get_string('quiver_id') == quiver_id then
local replace_item = x_bows.quiver.get_replacement_item(st, 'x_bows:quiver')
player_inv:set_stack('main', i, replace_item)
break
else
local replace_item = x_bows.quiver.get_replacement_item(st, 'x_bows:quiver')
player_inv:set_stack('main', i, replace_item)
end
end
end
end
end
---Swap item in player inventory indicating open quiver. Preserve all ItemStack definition and meta.
---@param from_stack ItemStack transfer data from this item
---@param to_item_name string transfer data to this item
---@return ItemStack ItemStack replacement item
function x_bows.quiver.get_replacement_item(from_stack, to_item_name)
---@type ItemStack
local replace_item = ItemStack({
name = to_item_name,
count = from_stack:get_count(),
wear = from_stack:get_wear()
})
local replace_item_meta = replace_item:get_meta()
local from_stack_meta = from_stack:get_meta()
replace_item_meta:set_string('quiver_items', from_stack_meta:get_string('quiver_items'))
replace_item_meta:set_string('quiver_id', from_stack_meta:get_string('quiver_id'))
replace_item_meta:set_string('description', from_stack_meta:get_string('description'))
return replace_item
end
---Gets arrow from quiver
---@param player ObjectRef
---@return {["found_arrow_stack"]: ItemStack|nil, ["quiver_id"]: string|nil, ["quiver_name"]: string|nil}
function x_bows.quiver.get_itemstack_arrow_from_quiver(player)
local player_inv = player:get_inventory()
---@type ItemStack|nil
local found_arrow_stack
local prev_detached_inv_list = {}
local quiver_id
local quiver_name
---find matching quiver item in players inventory with the open formspec name
if player_inv and player_inv:contains_item('main', 'x_bows:quiver') then
local inv_list = player_inv:get_list('main')
for i, st in ipairs(inv_list) do
if not st:is_empty() and st:get_name() == 'x_bows:quiver' then
local st_meta = st:get_meta()
local player_name = player:get_player_name()
quiver_id = st_meta:get_string('quiver_id')
local detached_inv = x_bows.quiver.get_or_create_detached_inv(
quiver_id,
player_name,
st_meta:get_string('quiver_items')
)
if not detached_inv:is_empty('main') then
local detached_inv_list = detached_inv:get_list('main')
---find arrows inside quiver inventory
for j, qst in ipairs(detached_inv_list) do
---save copy of inv list before we take the item
table.insert(prev_detached_inv_list, detached_inv:get_stack('main', j))
if not qst:is_empty() and not found_arrow_stack then
quiver_name = st:get_name()
found_arrow_stack = qst:take_item()
if not x_bows.is_creative(player_name) then
detached_inv:set_list('main', detached_inv_list)
x_bows.quiver.save(detached_inv, player, true)
end
end
end
end
end
if found_arrow_stack then
---show HUD - quiver inventory
x_bows.quiver.udate_or_create_hud(player, prev_detached_inv_list)
break
end
end
end
return {
found_arrow_stack = found_arrow_stack,
quiver_id = quiver_id,
quiver_name = quiver_name
}
end
---Remove all added HUDs
---@param player ObjectRef
function x_bows.quiver.remove_hud(player)
local player_name = player:get_player_name()
if x_bows.quiver.hud_item_ids[player_name] then
for _, v in pairs(x_bows.quiver.hud_item_ids[player_name]) do
if type(v) == 'table' then
for _, v2 in pairs(v) do
player:hud_remove(v2)
end
else
player:hud_remove(v)
end
end
x_bows.quiver.hud_item_ids[player_name] = {
arrow_inv_img = {},
stack_count = {}
}
else
x_bows.quiver.hud_item_ids[player_name] = {
arrow_inv_img = {},
stack_count = {}
}
end
end
---Update or create HUD
---@todo implement hud_change?
---@param player ObjectRef
---@param inv_list ItemStack[]
---@return nil
function x_bows.quiver.udate_or_create_hud(player, inv_list)
local player_name = player:get_player_name()
local selected_bg_added = false
if x_bows.quiver.after_job[player_name] then
for _, v in pairs(x_bows.quiver.after_job[player_name]) do
v:cancel()
end
x_bows.quiver.after_job[player_name] = {}
else
x_bows.quiver.after_job[player_name] = {}
end
x_bows.quiver.remove_hud(player)
---title image
x_bows.quiver.hud_item_ids[player_name].title_image = player:hud_add({
hud_elem_type = 'image',
position = {x = 1, y = 0.5},
offset = {x = -120, y = -140},
text = 'x_bows_quiver.png',
scale = {x = 4, y = 4},
alignment = 0,
})
---title copy
local quiver_def = minetest.registered_items['x_bows:quiver']
x_bows.quiver.hud_item_ids[player_name].title_copy = player:hud_add({
hud_elem_type = 'text',
position = {x = 1, y = 0.5},
offset = {x = -120, y = -75},
text = quiver_def.short_description,
alignment = 0,
scale = {x = 100, y = 30},
number = 0xFFFFFF,
})
---hotbar bg
x_bows.quiver.hud_item_ids[player_name].hotbar_bg = player:hud_add({
hud_elem_type = 'image',
position = {x = 1, y = 0.5},
offset = {x = -238, y = 0},
text = 'x_bows_quiver_hotbar.png',
scale = {x = 1, y = 1},
alignment = {x = 1, y = 0 },
})
for j, qst in ipairs(inv_list) do
if not qst:is_empty() then
local found_arrow_stack_def = minetest.registered_items[qst:get_name()]
if not selected_bg_added then
selected_bg_added = true
---ui selected bg
x_bows.quiver.hud_item_ids[player_name].hotbar_selected = player:hud_add({
hud_elem_type = 'image',
position = {x = 1, y = 0.5},
offset = {x = -308 + (j * 74), y = 2},
text = 'x_bows_quiver_hotbar_selected.png',
scale = {x = 1, y = 1},
alignment = {x = 1, y = 0 },
})
end
if found_arrow_stack_def then
---arrow inventory image
table.insert(x_bows.quiver.hud_item_ids[player_name].arrow_inv_img, player:hud_add({
hud_elem_type = 'image',
position = {x = 1, y = 0.5},
offset = {x = -300 + (j * 74), y = 0},
text = found_arrow_stack_def.inventory_image,
scale = {x = 4, y = 4},
alignment = {x = 1, y = 0 },
}))
---stack count
table.insert(x_bows.quiver.hud_item_ids[player_name].stack_count, player:hud_add({
hud_elem_type = 'text',
position = {x = 1, y = 0.5},
offset = {x = -244 + (j * 74), y = 23},
text = qst:get_count(),
alignment = -1,
scale = {x = 50, y = 10},
number = 0xFFFFFF,
}))
end
end
end
---@param v_player ObjectRef
table.insert(x_bows.quiver.after_job[player_name], minetest.after(10, function(v_player)
x_bows.quiver.remove_hud(v_player)
end, player))
end
---Get existing detached inventory or create new one
---@param quiver_id string
---@param player_name string
---@param quiver_items? string
---@return InvRef
function x_bows.quiver.get_or_create_detached_inv(quiver_id, player_name, quiver_items)
local detached_inv
if quiver_id ~= '' then
detached_inv = minetest.get_inventory({type='detached', name=quiver_id})
end
if not detached_inv then
detached_inv = minetest.create_detached_inventory(quiver_id, {
---@param inv InvRef detached inventory
---@param from_list string
---@param from_index number
---@param to_list string
---@param to_index number
---@param count number
---@param player ObjectRef
allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
if x_bows.quiver.quiver_can_allow(inv, player) then
return count
else
return 0
end
end,
---@param inv InvRef detached inventory
---@param listname string listname of the inventory, e.g. `'main'`
---@param index number
---@param stack ItemStack
---@param player ObjectRef
allow_put = function(inv, listname, index, stack, player)
if minetest.get_item_group(stack:get_name(), 'arrow') ~= 0 and x_bows.quiver.quiver_can_allow(inv, player) then
return stack:get_count()
else
return 0
end
end,
---@param inv InvRef detached inventory
---@param listname string listname of the inventory, e.g. `'main'`
---@param index number
---@param stack ItemStack
---@param player ObjectRef
allow_take = function(inv, listname, index, stack, player)
if minetest.get_item_group(stack:get_name(), 'arrow') ~= 0 and x_bows.quiver.quiver_can_allow(inv, player) then
return stack:get_count()
else
return 0
end
end,
---@param inv InvRef detached inventory
---@param from_list string
---@param from_index number
---@param to_list string
---@param to_index number
---@param count number
---@param player ObjectRef
on_move = function(inv, from_list, from_index, to_list, to_index, count, player)
x_bows.quiver.save(inv, player)
end,
---@param inv InvRef detached inventory
---@param listname string listname of the inventory, e.g. `'main'`
---@param index number index where was item put
---@param stack ItemStack stack of item what was put
---@param player ObjectRef
on_put = function(inv, listname, index, stack, player)
x_bows.quiver.save(inv, player)
end,
---@param inv InvRef detached inventory
---@param listname string listname of the inventory, e.g. `'main'`
---@param index number
---@param stack ItemStack
---@param player ObjectRef
on_take = function(inv, listname, index, stack, player)
x_bows.quiver.save(inv, player)
end,
}, player_name)
detached_inv:set_size('main', 3 * 1)
end
---populate items in inventory
if quiver_items and quiver_items ~= '' then
x_bows.quiver.set_string_to_inv(detached_inv, quiver_items)
end
return detached_inv
end
---create UUID
---@return string
local function uuid()
local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
---@diagnostic disable-next-line: redundant-return-value
return string.gsub(template, '[xy]', function (c)
local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb)
return string.format('%x', v)
end)
end
---create formspec
---@param name string name of the form
---@return string
local function get_formspec(name)
local width = 3
local height = 1
local list_w = 8
local list_pos_x = (list_w - width) / 2
local formspec =
'size['..list_w..',6]' ..
'list[detached:'..name..';main;'..list_pos_x..',0.3;'..width..',1;]'..
'list[current_player;main;0,'..(height + 0.85)..';'..list_w..',1;]'..
'list[current_player;main;0,'..(height + 2.08)..';'..list_w..',3;8]'..
'listring[detached:'..name..';main]'..
'listring[current_player;main]'..
default.get_hotbar_bg(0, height + 0.85)
--update formspec
local inv = minetest.get_inventory({type='detached', name=name})
local invlist = inv:get_list(name)
---inventory slots overlay
local px, py = list_pos_x, 0.3
for i = 1, 3 do
if not invlist or invlist[i]:is_empty() then
formspec = formspec ..
'image[' .. px .. ',' .. py .. ';1,1;x_bows_arrow_slot.png]'
end
px = px + 1
end
return formspec
end
---convert inventory of itemstacks to serialized string
---@param inv InvRef
---@return {['inv_string']: string, ['content_description']: string}
local function get_string_from_inv(inv)
local inv_list = inv:get_list('main')
local t = {}
local content_description = ''
for i, st in ipairs(inv_list) do
if not st:is_empty() then
table.insert(t, st:to_table())
content_description = content_description .. '\n' ..st:get_short_description()..' '..st:get_count()
else
table.insert(t, {is_empty = true})
end
end
return {
inv_string = minetest.serialize(t),
content_description = content_description == '' and '\nEmpty' or content_description
}
end
---set items from serialized string to inventory
---@param inv InvRef inventory to add items to
---@param str string previously stringified inventory of itemstacks
function x_bows.quiver.set_string_to_inv(inv, str)
local t = minetest.deserialize(str)
for i, item in ipairs(t) do
if not item.is_empty then
inv:set_stack('main', i, ItemStack(item))
end
end
end
---save quiver inventory to itemstack meta
---@param inv InvRef
---@param player ObjectRef
---@param quiver_is_closed? boolean default `false`
function x_bows.quiver.save(inv, player, quiver_is_closed)
local player_inv = player:get_inventory()
local inv_loc = inv:get_location()
local quiver_item_name = quiver_is_closed and 'x_bows:quiver' or 'x_bows:quiver_open'
---find matching quiver item in players inventory with the open formspec name
if player_inv and player_inv:contains_item('main', quiver_item_name) then
local inv_list = player_inv:get_list('main')
for i, st in ipairs(inv_list) do
local st_meta = st:get_meta()
if not st:is_empty() and st:get_name() == quiver_item_name and st_meta:get_string('quiver_id') == inv_loc.name then
---save inventory items in quiver item meta
local string_from_inventory_result = get_string_from_inv(inv)
st_meta:set_string('quiver_items', string_from_inventory_result.inv_string)
---update description
local new_description = st:get_short_description()..'\n'..string_from_inventory_result.content_description..'\n'
st_meta:set_string('description', new_description)
player_inv:set_stack('main', i, st)
break
end
end
end
end
---check if we are allowing actions in the correct quiver inventory
---@param inv InvRef
---@param player ObjectRef
---@return boolean
function x_bows.quiver.quiver_can_allow(inv, player)
local player_inv = player:get_inventory()
local inv_loc = inv:get_location()
---find matching quiver item in players inventory with the open formspec name
if player_inv and player_inv:contains_item('main', 'x_bows:quiver_open') then
local inv_list = player_inv:get_list('main')
for i, st in ipairs(inv_list) do
local st_meta = st:get_meta()
if not st:is_empty() and st:get_name() == 'x_bows:quiver_open' and st_meta:get_string('quiver_id') == inv_loc.name then
return true
end
end
end
return false
end
---Open quiver
---@param itemstack ItemStack
---@param user ObjectRef
---@return ItemStack
function x_bows.open_quiver(itemstack, user)
local itemstack_meta = itemstack:get_meta()
local pname = user:get_player_name()
local quiver_id = itemstack_meta:get_string('quiver_id')
---create inventory id and save it
if quiver_id == '' then
quiver_id = itemstack:get_name()..'_'..uuid()
itemstack_meta:set_string('quiver_id', quiver_id)
end
local quiver_items = itemstack_meta:get_string('quiver_items')
x_bows.quiver.get_or_create_detached_inv(quiver_id, pname, quiver_items)
---show open variation of quiver
local replace_item = x_bows.quiver.get_replacement_item(itemstack, 'x_bows:quiver_open')
itemstack:replace(replace_item)
minetest.sound_play('x_bows_quiver', {
to_player = user:get_player_name(),
gain = 0.1
})
minetest.show_formspec(pname, quiver_id, get_formspec(quiver_id))
return itemstack
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if player and fields.quit then
x_bows.quiver.close_quiver(player, formname)
XBowsQuiver:close_quiver(player, formname)
end
end)

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

View File

@ -23,7 +23,7 @@
---@field sound ItemSoundDef
---@field on_place fun(itemstack: ItemStack, placer: ObjectRef|nil, pointed_thing: PointedThingDef): ItemStack|nil When the 'place' key was pressed with the item in hand and a node was pointed at. Shall place item and return the leftover itemstack or nil to not modify the inventory. The placer may be any ObjectRef or nil. default: minetest.item_place
---@field on_secondary_use fun(itemstack: ItemStack, user: ObjectRef|nil, pointed_thing: PointedThingDef): ItemStack|nil Same as on_place but called when not pointing at a node. Function must return either nil if inventory shall not be modified, or an itemstack to replace the original itemstack. The user may be any ObjectRef or nil. default: nil
---@field on_drop fun(itemstack: ItemStack, dropper: ObjectRef|nil, pos: Vector): ItemStack Shall drop item and return the leftover itemstack. The dropper may be any ObjectRef or nil. default: minetest.item_drop
---@field on_drop fun(itemstack: ItemStack, dropper: ObjectRef|nil, pos: Vector): ItemStack|nil Shall drop item and return the leftover itemstack. The dropper may be any ObjectRef or nil. default: minetest.item_drop
---@field on_pickup fun(itemstack: ItemStack, picker: ObjectRef|nil, pointed_thing?: PointedThingDef, time_from_last_punch?: number|integer, rest?: any): ItemStack|nil Called when a dropped item is punched by a player. Shall pick-up the item and return the leftover itemstack or nil to not modify the dropped item. `rest` are other parameters from `luaentity:on_punch`. default: `minetest.item_pickup`
---@field on_use fun(itemstack: ItemStack, user: ObjectRef|nil, pointed_thing: PointedThingDef): ItemStack|nil default: nil. When user pressed the 'punch/mine' key with the item in hand. Function must return either nil if inventory shall not be modified, or an itemstack to replace the original itemstack. e.g. itemstack:take_item(); return itemstack. Otherwise, the function is free to do what it wants. The user may be any ObjectRef or nil. The default functions handle regular use cases.
---@field after_use fun(itemstack: ItemStack, user: ObjectRef|nil, node: NodeDef, digparams: DigParamsDef): ItemStack|nil default: nil. If defined, should return an itemstack and will be called instead of wearing out the item (if tool). If returns nil, does nothing.

View File

@ -8,3 +8,4 @@
---@field steps number How many steps the plant has to grow, until it can be harvested
---@field minlight number Minimum light to grow
---@field maxlight number Maximum light to grow
---@field on_timer fun(pos: Vector, elapsed: number): boolean default: nil, called by NodeTimers, see minetest.get_node_timer and NodeTimerRef. elapsed is the total time passed since the timer was started. return true to run the timer for another cycle with the same timeout value.

119
types/xbows.type.lua Normal file
View File

@ -0,0 +1,119 @@
---Base class XBows
---@class XBows
---@field pvp boolean
---@field creative boolean
---@field mesecons string|nil
---@field hbhunger string|nil
---@field playerphysics string|nil
---@field player_monoids string|nil
---@field registered_bows table<string, ItemDef|BowItemDefCustom>
---@field registered_arrows table<string, ItemDef|ArrowItemDefCustom>
---@field registered_quivers table<string, ItemDef|QuiverItemDefCustom>
---@field registered_particle_spawners table<string, ParticlespawnerDef|ParticlespawnerDefCustom>
---@field player_bow_sneak table<string, table<string, boolean>>
---@field settings table
---@field quiver table Quiver class
---@field charge_sound_after_job table<string, JobTable>
---@field is_allowed_ammunition fun(self: XBows, weapon_name: string, ammo_name: string): boolean ---Check if ammunition is allowed to charge this weapon
---@field is_creative fun(self: XBows, name: string): boolean Check if creative is enabled or if player has creative priv
---@field register_particle_effect fun(self: XBows, name: string, def: ParticlespawnerDef|ParticlespawnerDefCustom): nil
---@field get_particle_effect_for_arrow fun(self: XBows, name: string, pos: Vector): number
---@field register_entity fun(self: EntityDef|XBows, name: string, def: XBowsEntityDef): nil Register new projectile entity
---XBowsQuiver class extended from XBows
---@alias XBowsQuiver XBowsQuiverBase|XBows
---@class XBowsQuiverBase
---@field hud_item_ids table
---@field after_job table<string, JobTable>
---@field udate_or_create_hud fun(self: XBowsQuiver, player: ObjectRef, inv_list: ItemStack[], idx?: number): nil Update or create HUD
---@field get_or_create_detached_inv fun(self: XBowsQuiver, quiver_id: string, player_name: string, quiver_items?: string): InvRef Get existing detached inventory or create new one
---@field save fun(self: XBowsQuiver, inv: InvRef, player: ObjectRef, quiver_is_closed?: boolean): nil Save quiver inventory to itemstack meta
---Custom field in ParticlespawnerDef
---@class ParticlespawnerDefCustom
---@field custom ParticlespawnerDefCustomAttr
---Custom field attributes in ParticlespawnerDef
---@class ParticlespawnerDefCustomAttr
---@field minpos Vector
---@field maxpos Vector
---Custom field in ItemDef
---@class BowItemDefCustom
---@field custom BowItemDefCustomAttr
---Custom field attributes in ItemDef
---@class BowItemDefCustomAttr
---@field crit_chance number `crit_chance` 10% chance, 5 is 20% chance, (1 / crit_chance) * 100 = % chance
---@field inventory_image_charged string
---@field recipe table
---@field fuel_burntime number
---@field name_charged string
---@field name string
---@field mod_name string
---@field uses number
---@field strength number How strong is the bow. Defines how far the arrow will fly.
---@field strength_min number|nil How strong is the bow. Defines how far the arrow will fly.
---@field strength_max number|nil How strong is the bow. Defines how far the arrow will fly.
---@field allowed_ammunition string[]|nil
---@field wield_image_charged string|nil
---@field acc_x_min number|nil
---@field acc_y_min number|nil
---@field acc_z_min number|nil
---@field acc_x_max number|nil
---@field acc_y_max number|nil
---@field acc_z_max number|nil
---@field sound_load string
---@field sound_hit string
---@field sound_shoot string
---@field sound_shoot_crit string
---Custom field in ItemDef
---@class ArrowItemDefCustom
---@field custom ArrowItemDefCustomAttr
---Custom field attributes in ItemDef
---@class ArrowItemDefCustomAttr
---@field tool_capabilities ToolCapabilitiesDef
---@field craft_count number
---@field recipe table
---@field fuel_burntime number
---@field name string
---@field mod_name string
---@field particle_effect string|nil
---@field particle_effect_crit string|nil
---@field particle_effect_fast string|nil
---@field projectile_textures table|nil
---@field projectile_visual_size table
---@field projectile_entity string
---Custom field in ItemDef
---@class QuiverItemDefCustom
---@field custom QuiverItemDefCustomAttr
---Custom field attributes in ItemDef
---@class QuiverItemDefCustomAttr
---@field recipe table
---@field recipe_count number
---@field faster_arrows number
---@field add_damage number
---@field fuel_burntime number
---@field inventory_image_open string
---@field wield_image_open string
---@field name string
---Custom field in EntityDef
---@alias XBowsEntityDef EntityDef|EntityDefCustom|XBows
---@class EntityDefCustom
---@field on_death fun(self: XBowsEntityDef, selfObj: table, killer: ObjectRef|nil): nil Function receive a "luaentity" table as `self`. Called when the object dies.
---@field on_punch fun(self: XBowsEntityDef, selfObj: table, puncher: ObjectRef|nil, time_from_last_punch: number|integer|nil, tool_capabilities: ToolCapabilitiesDef|nil, dir: Vector, damage: number|integer): boolean|nil Function receive a "luaentity" table as `self`. Called when somebody punches the object. Note that you probably want to handle most punches using the automatic armor group system. Can return `true` to prevent the default damage mechanism.
---@field _custom EntityDefCustomAttr
---@alias EntityDefCustomAttr EntityDefCustomAttrDef|EntityDef
---@class EntityDefCustomAttrDef
---@field name string
---@field mod_name string