206 lines
9.3 KiB
Lua
206 lines
9.3 KiB
Lua
--[[
|
|
Implementation of the Minecraft 1.16 Anger System (copied from https://www.minecraft.net/ru-ru/article/nether-update-java, with modifications):
|
|
|
|
Forgive dead players
|
|
|
|
If this gamerule is disabled, then angered mobs will stay angry even if the targeted player dies
|
|
If both forgiveDeadPlayers and universalAnger are enabled, an angered neutral mob will stop being angry when their target dies. They won't seek any new targets after that
|
|
|
|
Neutral mob anger
|
|
|
|
When hurt by a player, the neutral mob will target that player and try to kill it
|
|
The mob will stay angry until the player is dead or out of sight for a while
|
|
Anger is persistent, so a player can't escape by temporarily logging out or switching dimension
|
|
If a targeted player dies near the angered mob, it will stop being angry (unless forgiveDeadPlayers is disabled)
|
|
Neutral mobs also get angry at other mobs who hurt them. However, that anger is not persistent
|
|
Angered neutral mobs will only attack the offending player, not innocent bystanders
|
|
Some mobs spread anger (wolf, Zombie Pigman). If a player attacks one, all nearby mobs of the same type will get angry at that player
|
|
|
|
Universal anger
|
|
|
|
Universal anger is basically guilt by association. A neutral mob attacked by players will be angry at players in general, regardless of who attacked them. More specifically:
|
|
|
|
A neutral mob attacked by a player will target the nearest player, even if that player wasn't the attacker
|
|
Every time the neutral mob is hit by a player it will update its attack target to the nearest player
|
|
Players can use this to make neutral mobs attack other players. Who would ever do something that devious?
|
|
Universal anger does not apply when a neutral mob is attacked by another mob - only when it is attacked by a player
|
|
Universal anger is persistent. The angered mob will stay angry even if the player logs out and logs in, or jumps through a portal and back
|
|
mcl_mobs.mobs that spread anger will also spread universal anger. So if a player attacks a Zombie Pigman, all other Zombie Pigmen within sight will be universally angry and attack their nearest player
|
|
An angered neutral mob will stop being angry if it can't see any eligible target for a while
|
|
--]]
|
|
|
|
function mcl_mobs.mob:anger_on_staticdata()
|
|
if self.anger_persistent then
|
|
self.data.anger_target_name = self.anger_target_name
|
|
self.data.anger_hurt_timestamp = self.anger_hurt_timestamp
|
|
end
|
|
end
|
|
|
|
function mcl_mobs.mob:anger_on_activate()
|
|
if self.data.anger_target_name then
|
|
self.anger = true
|
|
self.anger_persistent = true
|
|
self.anger_target_name = self.data.anger_target_name
|
|
self.anger_hurt_timestamp = self.data.anger_hurt_timestamp
|
|
|
|
self.data.anger_target_name = nil
|
|
self.data.anger_hurt_timestamp = nil
|
|
end
|
|
end
|
|
|
|
function mcl_mobs.mob:get_anger_attack_target()
|
|
if not self.anger then
|
|
return
|
|
end
|
|
|
|
-- if the mob is universally angry and the current target is unreachable, search a new one
|
|
local search_new_target = self.anger_universal and (
|
|
not self.anger_current_target -- does a current target even exist?
|
|
or not self.anger_current_target:is_player() -- universal anger only applies to players, so this is just a check whether the ObjectRef is still valid
|
|
or not self:can_see(self.anger_current_target) -- dimension check is not done since it is covered by the view distance check
|
|
)
|
|
|
|
if search_new_target then
|
|
self.anger_current_target = self:get_player_in_sight()
|
|
if self.anger_current_target then
|
|
self:debug("found new universal anger target: " .. self.anger_current_target:get_player_name())
|
|
end
|
|
end
|
|
|
|
-- if the anger is not persistant (e.g. enderman provocation, angry at mobs)
|
|
if not self.anger_persistent then
|
|
-- calm down if either the target ObjectRef is invalid or changed its dimension
|
|
if not self.anger_target:is_player() and not self.anger_target:get_luaentity() or not self:same_dimension_as(self.anger_target) then
|
|
self:debug("non persistent anger target unreachable, calming down" .. (self.anger_target_name and "[anger_target_name = " .. self.anger_target_name .. "]" or ""))
|
|
return nil, true
|
|
end
|
|
end
|
|
|
|
-- if this is a player, special rules apply (don't use anger_target:is_player() since the player may have logged out so it's not a valid check)
|
|
if self.anger_target_name then
|
|
-- check if player logged out (if the player had already logged out in the last step anger_target will be nil, else it will be a dangling ObjectRef that can be validated by calling is_player())
|
|
if not self.anger_target or not self.anger_target:is_player() then
|
|
if self.anger_target then
|
|
self:debug("anger target logged out: " .. self.anger_target_name)
|
|
end
|
|
-- in case the player relogged (if the player did not relog anger_target becomes nil and this is run in the next step as well)
|
|
self.anger_target = minetest.get_player_by_name(self.anger_target_name)
|
|
if self.anger_target then
|
|
self:debug("anger target relogged: " .. self.anger_target_name)
|
|
end
|
|
end
|
|
-- if forgiveDeadPlayers is true (it is by default)
|
|
if self.anger_target and minetest.settings:get_bool("mclForgiveDeadPlayers", true) then
|
|
-- check death timestamp of player and forget about the player in case it was killed
|
|
if self.anger_target:get_meta():get_int("mcl_mobs:last_death") >= self.hurt_timestamp then
|
|
self:debug("forgave " .. self.anger_target_name .. " since they died")
|
|
return nil, true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- the actual target we want to attack
|
|
local target
|
|
|
|
-- note: dont use a selfmade ternary expression (v = x and a or b, in other languages that have real ternary expressions this would be v = x ? a : b) here
|
|
-- because anger_current_target might be nil and we don't care about the original player if they are not in the area (anger_current_target is only nil if there is absolutely no player in the area)
|
|
if self.anger_universal then
|
|
target = self.anger_current_target
|
|
else
|
|
target = self.anger_target
|
|
-- if the target is out of reach, it counts as not existant in terms of the reset timer
|
|
if target and not self:can_see(target) then
|
|
target = nil
|
|
if not self.anger_calm_timer then
|
|
self:debug("cannot see anger target " .. self.anger_target_name .. " anymore")
|
|
end
|
|
end
|
|
end
|
|
|
|
if not target and not self.anger_calm_timer then
|
|
-- start to calm down if noone to attack in sight
|
|
self:debug("anger target " .. (self.anger_universal and self.anger_target_name .. " " or "") .. "is not reachable anymore, starting calm timer")
|
|
self.anger_calm_timer = mcl_mobs.const.calm_down_timer
|
|
elseif target and self.anger_calm_timer then
|
|
-- stop calming down if there is someone in sight again
|
|
self:debug("anger target " .. (self.anger_universal and self.anger_target_name .. " " or "") .. "is reachable again, resetting calm timer")
|
|
self.anger_calm_timer = nil
|
|
end
|
|
|
|
if target then
|
|
return target
|
|
end
|
|
|
|
-- wait for the mob to calm down if there is no target, then clear variables
|
|
-- do_timer returns true if the timer has not elapsed yet
|
|
if not self:do_timer("anger_calm") then
|
|
self:debug("calmed down")
|
|
self.anger = nil
|
|
self.anger_target = nil
|
|
self.anger_target_name = nil
|
|
self.anger_universal = nil
|
|
self.anger_current_target = nil
|
|
self.anger_persistent = nil
|
|
self.anger_hurt_timestamp = nil
|
|
end
|
|
end
|
|
|
|
function mcl_mobs.mob:get_angry_raw(target, target_name, timestamp, universal, persistent)
|
|
if self.owner == target_name then
|
|
return false
|
|
end
|
|
|
|
self:debug("getting angry at " .. (target_name or tostring(target))
|
|
.. " persistent: " .. (persistent and "yes" or "no")
|
|
.. " universal: " .. (universal and "yes" or "no")
|
|
)
|
|
|
|
self.anger = true
|
|
self.anger_target = target -- even if universally angry, still remember the actual cause to apply forgiveDeadPlayers properly. anger_current_target is used to get the actual attack target
|
|
self.anger_target_name = target_name -- remember player name separately to work around the ObjectRef becoming invalid when the player logs out
|
|
-- the persistent field is used to optionally forget about the player when they log out or change dimension (e.g. provoking endermen by looking at them) or for when attacked by another mob
|
|
self.anger_persistent = persistent
|
|
self.anger_hurt_timestamp = timestamp
|
|
|
|
if universal then
|
|
self.anger_universal = true
|
|
-- set this to nil because then universal anger is enabled every mob will look for a new target everytime a provocation happens
|
|
self.anger_current_target = nil
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
function mcl_mobs.mob:get_angry(target)
|
|
local timestamp = os.time()
|
|
local is_player = target:is_player()
|
|
|
|
local universal = is_player and minetest.settings:get_bool("mclUniversalAnger")
|
|
local target_name = is_player and target:get_player_name() or ""
|
|
local persistent = not is_player
|
|
|
|
self:debug("provoked by " .. (target_name or tostring(target))
|
|
.. " persistent: " .. (persistent and "yes" or "no")
|
|
.. " universal: " .. (universal and "yes" or "no")
|
|
)
|
|
|
|
if not self:get_angry_raw(target, target_name, timestamp, universal, persistent) then
|
|
return false
|
|
end
|
|
|
|
if self.def.group_attack then
|
|
for _, obj in pairs(minetest.get_objects_inside_radius(self.object:get_pos(), self.def.view_range)) do
|
|
local luaentity = obj:get_luaentity()
|
|
if luaentity and self.def.group_attack[luaentity.name] then
|
|
luaentity:get_angry_raw(target, target_name, timestamp, universal_anger, persistent)
|
|
end
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
minetest.register_on_dieplayer(function(player)
|
|
player:get_meta():set_int("mcl_mobs:last_death", os.time())
|
|
end)
|