minetest_x_bows/init.lua
2022-10-16 16:02:04 -04:00

644 lines
22 KiB
Lua

-- X Bows
-- by SaKeL
minetest = minetest--[[@as Minetest]]
ItemStack = ItemStack--[[@as ItemStack]]
vector = vector--[[@as Vector]]
default = default--[[@as MtgDefault]]
math.randomseed(tonumber(tostring(os.time()):reverse():sub(1, 9))--[[@as number]])
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
minetest.register_on_joinplayer(function(player)
reset_charged_bow(player, true)
x_bows.quiver.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()
if not item then
return
end
if not x_bows.player_bow_sneak[name] then
x_bows.player_bow_sneak[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')
end
x_bows.player_bow_sneak[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')
end
x_bows.player_bow_sneak[name].sneak = false
player:set_fov(0, true, 0.4)
end
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]')