EinsDreiDreiSieben/mods/leads/leads.lua

536 lines
19 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--[[
Leads — Adds leads for transporting animals to Minetest.
Copyright © 2023, Silver Sandstone <@SilverSandstone@craftodon.social>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
]]
--- Lead entity definition.
-- @module leads
local S = leads.S;
leads.SLACK_MODELS =
{
[0] = 'leads_lead.obj',
'leads_lead_slack1.obj',
'leads_lead_slack2.obj',
'leads_lead_slack3.obj',
'leads_lead_slack4.obj',
'leads_lead_slack5.obj',
'leads_lead_slack6.obj',
'leads_lead_slack7.obj',
'leads_lead_slack8.obj',
'leads_lead_slack9.obj',
'leads_lead_slack10.obj',
'leads_lead_slack11.obj',
'leads_lead_slack12.obj',
};
leads.STRETCH_SOUND_INTERVAL = 2.0;
if leads.settings.drop_mode == 'drop' then
leads.DROP_ITEM = true;
elseif leads.settings.drop_mode == 'give' then
leads.DROP_ITEM = false;
else
leads.DROP_ITEM = (minetest.get_modpath('mcl_core') or minetest.get_modpath('rp_default') or minetest.get_modpath('item_drop')) ~= nil;
end;
--- The main lead entity.
-- @type LeadEntity
leads.LeadEntity = {};
leads.LeadEntity.description = S'Lead';
leads.LeadEntity._leads_immobile = true;
leads.LeadEntity.initial_properties =
{
visual = 'mesh';
mesh = 'leads_lead.obj';
textures = {leads.DEFAULT_LEAD_TEXTURE};
physical = false;
selectionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5};
};
--- Spawns or unloads a lead.
function leads.LeadEntity:on_activate(staticdata, dtime_s)
self.current_length = 0.24;
self.max_length = leads.settings.lead_length;
self.rotation = vector.zero();
self.leader_attach_offset = vector.zero();
self.follower_attach_offset = vector.zero();
self.sound_timer = 0.0;
self.item = ItemStack();
self.texture = leads.DEFAULT_LEAD_TEXTURE;
self.strength = leads.settings.lead_strength;
self.breaking = 0.0;
local data = minetest.deserialize(staticdata);
if data then
self:load_from_data(data);
end;
self.object:set_armor_groups{fleshy = 0};
end;
--- Initialises the lead's state from a table.
function leads.LeadEntity:load_from_data(data)
self.max_length = data.max_length or self.max_length;
self.leader_id = data.leader_id or {};
self.follower_id = data.follower_id or {};
self.leader_attach_offset = data.leader_attach_offset or self.leader_attach_offset;
self.follower_attach_offset = data.follower_attach_offset or self.follower_attach_offset;
self.leader_id.pos = vector.new(self.leader_id.pos);
self.follower_id.pos = vector.new(self.follower_id.pos);
if data.item then
self:set_item(data.item);
end;
self:update_visuals();
end;
--- Sets the lead's item, updating relevant properties.
function leads.LeadEntity:set_item(item)
item = ItemStack(item);
self.item = item;
local def = item:get_definition();
self.strength = def._leads_strength or leads.settings.lead_strength;
self.max_length = def._leads_length or leads.settings.lead_length;
self.texture = def._leads_texture or leads.DEFAULT_LEAD_TEXTURE;
if not leads.settings.dynamic_textures then
self.object:set_properties{textures = {self.texture}};
end;
end;
--- Steps the knot.
function leads.LeadEntity:on_step(dtime)
self:_update_connectors();
local success, pos, offset = self:step_physics(dtime);
if success then
self.current_length = leads.util.clamp(offset:length(), 0.25, 256);
self.rotation = offset:dir_to_rotation();
self.object:move_to(pos, true);
self:update_visuals();
end;
end;
--- Simulates the lead's physics.
-- @param dtime [number] The time elapsed since the last tick, in seconds.
-- @return [boolean] true if the lead is functioning correctly, or false if it should break.
-- @return [vector|nil] The centre position of the lead, or nil on failure.
-- @return [vector|nil] The offset between the leader and the follower, or nil on failure.
function leads.LeadEntity:step_physics(dtime)
dtime = math.min(dtime, 0.125);
local l_pos = self.leader_pos;
local f_pos = self.follower_pos;
if not (l_pos and f_pos) then
self:break_lead();
return false, nil, nil;
end;
l_pos = l_pos + self.leader_attach_offset;
f_pos = f_pos + self.follower_attach_offset;
local pull_distance = self.max_length;
local break_distance = pull_distance * 2;
local distance = l_pos:distance(f_pos);
if distance > break_distance then
-- Lead is too long, break:
local overextension = distance - break_distance;
self.breaking = self.breaking + overextension * dtime;
if self.breaking > self.strength then
self:break_lead(nil, true);
return false, nil, nil;
end;
else
self.breaking = 0.0;
end;
local pos = (f_pos + l_pos) / 2;
if self.leader and self.follower and distance > pull_distance then
local base_force;
local function _pull_connector(connector, this_pos, other_pos)
if leads.is_immobile(connector) then
return;
end;
if not base_force then
base_force = (distance - pull_distance) * leads.settings.pull_force / pull_distance;
end;
local force = base_force / math.sqrt(leads.util.get_object_mass(connector));
local pull_direction = (other_pos - this_pos):normalize();
connector:add_velocity(pull_direction * dtime * force ^ 1.5);
end;
-- Pull follower:
_pull_connector(self.follower, f_pos, l_pos);
-- Pull leader if symmetrical mode is enabled:
if leads.settings.symmetrical then
_pull_connector(self.leader, l_pos, f_pos);
end;
-- Play stretching sound:
self.sound_timer = self.sound_timer + dtime;
if self.sound_timer >= leads.STRETCH_SOUND_INTERVAL then
self.sound_timer = self.sound_timer - leads.STRETCH_SOUND_INTERVAL;
if leads.util.rng:next(0, 8) == 0 then
minetest.sound_play(leads.sounds.stretch, {pos = pos}, true);
end;
end;
end;
return true, pos, f_pos - l_pos;
end;
--- Updates the connector references and stored positions.
-- @local
function leads.LeadEntity:_update_connectors()
local function _get_pos(key)
local object = self[key];
local pos = object and object:get_pos();
local id = self[key .. '_id'];
if not pos then
pos = id.pos;
if not pos then
return nil;
end;
local object = leads.util.deserialise_objref(id);
if object then
pos = object:get_pos();
self[key] = object;
self[key .. '_attach_offset'] = leads.util.get_attach_offset(object);
leads.leads_by_connector[object][self.object] = true;
else
-- The object reference is invalid, and deserialising the
-- object failed. This could mean that the object has been
-- removed and the lead should break, or it could mean that
-- the object's mapblock has been unloaded, and the lead
-- should just wait until it gets loaded again. We can figure
-- out which one by checking if the mapblock is active.
if minetest.compare_block_status(pos, 'active') then
return nil;
end;
end;
end;
id.pos = pos or id.pos;
return pos;
end;
self.leader_pos = _get_pos('leader');
self.follower_pos = _get_pos('follower');
end;
--- Handles the lead being punched.
function leads.LeadEntity:on_punch(puncher, time_from_last_punch, tool_capabilities, dir, damage)
local name = puncher and puncher:get_player_name() or '';
-- Check protection:
local is_protected, protected_pos = self:is_protected(name);
if is_protected then
minetest.record_protection_violation(protected_pos, name);
return true;
end;
-- Break the lead:
self:break_lead(puncher);
-- Block the player's interaction for a moment to prevent accidentally breaking the node behind the lead:
if name ~= '' then
leads.util.block_player_interaction(name, 0.25);
end;
return true;
end;
--- Handles the lead being interacted with while holding a lead item.
function leads.LeadEntity:_leads_on_interact(itemstack, user, pointed_thing, is_punch)
if is_punch then
self:on_punch(user);
return true, nil;
end;
return false, nil;
end;
--- Handles the lead being killed.
function leads.LeadEntity:on_death(killer)
self:break_lead(killer);
end;
--- Returns the lead's state as a table.
function leads.LeadEntity:get_staticdata()
local data = {};
data.item = self.item:to_string();
data.max_length = self.max_length;
data.leader_id = self.leader_id;
data.follower_id = self.follower_id;
data.leader_attach_offset = self.leader_attach_offset;
data.follower_attach_offset = self.follower_attach_offset;
return minetest.serialize(data);
end;
--- Breaks the lead, possibly giving/dropping an item.
-- @param breaker [ObjectRef|nil] The object breaking the lead.
-- @param snap [boolean|nil] true if the lead is breaking due to tension.
function leads.LeadEntity:break_lead(breaker, snap)
if leads.settings.debug then
minetest.log(debug.traceback(('[Leads] Breaking lead %s at %s.'):format(self, self.object:get_pos())));
end;
-- Notify leader and follower:
self:notify_connector_removed(self.leader, true);
self:notify_connector_removed(self.follower, false);
-- Give or drop item:
if not self.item:is_empty() then
local owner = breaker;
if not minetest.is_player(owner) then
owner = self.leader;
end;
local pos = self.object:get_pos();
local item = self.item;
if not leads.DROP_ITEM then
local inventory = minetest.is_player(owner) and owner:get_inventory();
if inventory then
if minetest.is_creative_enabled(owner) and inventory:contains_item('main', item, true) then
item = ItemStack();
else
item = inventory:add_item('main', item);
end;
end;
end;
minetest.add_item(pos, item);
end;
-- Play sound:
if snap then
minetest.sound_play(leads.sounds.snap, {pos = self.object:get_pos()}, true);
else
minetest.sound_play(leads.sounds.remove, {pos = self.object:get_pos()}, true);
end;
-- Remove lead:
self.object:remove();
self.item = ItemStack();
end;
--- Checks if either end of the lead is in an area protected from the specified player.
-- If protection support is disabled, this always returns false.
-- @param player [string|ObjectRef] A player object or username.
-- @return [boolean] true if the player is not allowed to break the lead due to protection.
-- @return [vector|nil] The protected position, if any.
function leads.LeadEntity:is_protected(player)
if not leads.settings.respect_protection then
return false, nil; -- Protection support is disabled.
end;
local name;
if type(player) == 'string' then
name = player;
else
name = player:get_player_name();
end;
name = name or '';
if minetest.check_player_privs(name, 'protection_bypass') then
return false, nil; -- The player is exempt from protection.
end;
if name == self.leader_id.player_name then
return false, nil; -- The player is holding the lead.
end;
for __, connector_id in ipairs{self.leader_id, self.follower_id} do
if connector_id and connector_id.pos then
local pos = vector.round(connector_id.pos);
if minetest.is_protected(pos, name) then
return true, pos; -- An end of the lead is in a protected area.
end;
end;
end;
return false, nil;
end;
--- Updates the visual properties of the lead to show its current state.
function leads.LeadEntity:update_visuals()
local SCALE = 8;
if self.current_length == self.old_length then
return;
end;
self.old_length = self.current_length;
local properties = {visual_size = vector.new(1, 1, self.current_length)};
local selbox_offset = 0;
-- Dynamic textures:
if leads.settings.dynamic_textures then
local texture = leads.util.tile_texture(self.texture, 96 * SCALE, 2 * SCALE, math.floor(self.current_length * 16 * SCALE), 2 * SCALE);
properties.textures = {texture};
elseif self.texture ~= self.old_texture then
self.old_texture = self.texture;
properties.textures = {self.texture};
end;
-- Slack model:
if leads.settings.enable_slack then
if self.leader_pos and self.follower_pos then
local slack, mesh = self:get_slack();
properties.mesh = mesh;
selbox_offset = selbox_offset - slack / 12;
end;
end;
-- Selection box:
if leads.settings.rotate_selection_box then
properties.selectionbox = {-0.0625, -0.0625 + selbox_offset, -self.current_length / 2,
0.0625, 0.0625 + selbox_offset, self.current_length / 2, rotate = true};
end;
self.object:set_properties(properties);
self.object:set_rotation(self.rotation);
end;
--- Calculates the slack value and chooses a model to represent it.
-- @return [number] The current slack value.
-- @return [string] A model name.
function leads.LeadEntity:get_slack()
local span = self.follower_pos - self.leader_pos;
local slack = 0.5 + 1 - span:length() / self.max_length;
-- Scale the slack by how horizontal the lead is, otherwise it would droop sideways when vertical.
slack = slack * (1 - math.abs(span:normalize().y));
slack = leads.util.clamp(slack, 0.0, 1.0)
local model_index = math.floor(slack * #leads.SLACK_MODELS);
return slack, leads.SLACK_MODELS[model_index];
end;
--- Updates the connector IDs to reflect the current connectors.
function leads.LeadEntity:update_objref_ids()
self.leader_id = leads.util.serialise_objref(self.leader) or self.leader_id;
self.follower_id = leads.util.serialise_objref(self.follower) or self.follower_id;
self:update_attach_offsets();
end;
--- Updates the attachment offsets to reflect the current connectors' properties.
function leads.LeadEntity:update_attach_offsets()
self.leader_attach_offset = leads.util.get_attach_offset(self.leader) or self.leader_attach_offset;
self.follower_attach_offset = leads.util.get_attach_offset(self.follower) or self.follower_attach_offset;
end;
--- Transfers this lead to a new leader.
-- @param leader [ObjectRef] The new leader object.
-- @return [boolean] true on success.
function leads.LeadEntity:set_leader(leader)
return self:set_connector(leader, true);
end;
--- Transfers this lead to a new follower.
-- @param follower [ObjectRef] The new follower object.
-- @return [boolean] true on success.
function leads.LeadEntity:set_follower(follower)
return self:set_connector(follower, false);
end;
--- Transfers this lead to a new leader or follower.
-- @param object [ObjectRef] The new connector.
-- @param is_leader [boolean] true to set the leader, false to set the follower.
-- @return [boolean] true on success.
function leads.LeadEntity:set_connector(object, is_leader)
if (self.leader and leads.util.is_same_object(object, self.leader)) or
(self.follower and leads.util.is_same_object(object, self.follower)) then
return false;
end;
local key = is_leader and 'leader' or 'follower';
local old_object = self[key];
self:notify_connector_removed(old_object, is_leader);
self[key] = object;
self:notify_connector_added(object, is_leader);
self:update_objref_ids();
return true;
end;
--- Reverses the direction of the lead, swapping the leader and follower.
function leads.LeadEntity:reverse()
self.leader, self.follower = self.follower, self.leader;
self.leader_id, self.follower_id = self.follower_id, self.leader_id;
end;
--- Notifies the connector that this lead has been added.
-- @param object [ObjectRef|nil] The connector to notify.
-- @param is_leader [boolean] true if the connector is the leader.
function leads.LeadEntity:notify_connector_added(object, is_leader)
if not object then
return;
end;
leads.leads_by_connector[object][self.object] = true;
local entity = object:get_luaentity();
if entity and entity._leads_lead_add then
entity:_leads_lead_add(self, is_leader or false);
end;
end;
--- Notifies the connector that this lead has been removed.
-- @param object [ObjectRef|nil] The connector to notify.
-- @param is_leader [boolean] true if the connector was the leader.
function leads.LeadEntity:notify_connector_removed(object, is_leader)
if not object then
return;
end;
leads.leads_by_connector[object][self.object] = nil;
local entity = object:get_luaentity();
if entity and entity._leads_lead_remove then
entity:_leads_lead_remove(self, is_leader or false);
end;
end;
--- Gets the lead's leader if it's loaded.
-- @return [ObjectRef|nil] The leader object.
function leads.LeadEntity:get_leader()
if self.leader and self.leader:get_pos() then
return self.leader;
end;
return nil;
end;
--- Gets the lead's follower if it's loaded.
-- @return [ObjectRef|nil] The follower object.
function leads.LeadEntity:get_follower()
if self.leader and self.follower:get_pos() then
return self.follower;
end;
return nil;
end;
minetest.register_entity('leads:lead', leads.LeadEntity);