1
0
Fork 0
MineClone2/mods/ENTITIES/mcl_mobs/api/anger.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)