Compare commits

...

12 Commits

10 changed files with 228 additions and 124 deletions

View File

@ -128,7 +128,7 @@ functions needed for the mob to work properly which contains the following:
arrow/fireball appears on mob. arrow/fireball appears on mob.
'specific_attack' has a table of entity names that mob can also attack 'specific_attack' has a table of entity names that mob can also attack
e.g. {"player", "mobs_animal:chicken"}. e.g. {"player", "mobs_animal:chicken"}.
'runaway_from' contains a table with mob names to run away from, add 'runaway_from' contains a table with mob/node names to run away from, add
"player" to list to runaway from player also. "player" to list to runaway from player also.
'pathfinding' set to 1 for mobs to use pathfinder feature to locate 'pathfinding' set to 1 for mobs to use pathfinder feature to locate
player, set to 2 so they can build/break also (only player, set to 2 so they can build/break also (only
@ -263,6 +263,9 @@ functions needed for the mob to work properly which contains the following:
'noyaw' If true this mob will not automatically change yaw 'noyaw' If true this mob will not automatically change yaw
'particlespawners' Table of particlespawners attached to the mob. This is implemented in a coord safe manner i.e. spawners are only sent to players within the player_transfer_distance (and automatically removed). This enables infinitely lived particlespawners. 'particlespawners' Table of particlespawners attached to the mob. This is implemented in a coord safe manner i.e. spawners are only sent to players within the player_transfer_distance (and automatically removed). This enables infinitely lived particlespawners.
'attack_frequency' Attack frequency in seconds. If unset, this defaults to 1. Implemented for melee only atm. 'attack_frequency' Attack frequency in seconds. If unset, this defaults to 1. Implemented for melee only atm.
'avoid_from' contains a table with mob/node names to avoid from, add
"player" to list to avoid from player also.
mobs:gopath(self,target,callback_arrived) pathfind a way to target and run callback on arrival mobs:gopath(self,target,callback_arrived) pathfind a way to target and run callback on arrival
@ -449,6 +452,16 @@ Create death particles at pos with the given collisionbox.
mcl_mobs.spawn(pos,name/entity name) mcl_mobs.spawn(pos,name/entity name)
mcl_mobs:is_object_in_view(object_list, object_range, node_range, turn_around)
Returns 'true' if an object (mob or node) is in the field of view.
'object_list' list of mob and/or node names
'object_range' maximum distance to a mob from object_list
'node_range' maximum distance to a node from object_list
'turn_around' true or false
Making Arrows Making Arrows
------------- -------------

View File

@ -35,6 +35,34 @@ function mob_class:day_docile()
end end
end end
local function name(obj)
if obj:is_player() then
return "player:" .. obj:get_player_name()
else
local e = obj:get_luaentity()
return e.name
end
end
function mob_class:log2(msg, object)
--# local o = object:get_luaentity()
--# local o2 = object.objectget_luaentity()
--minetest.log("do_attack SRC " .. dump(self) ) -- { object = userdata: 0x49d9a1a0, is_mob = true, health = 20
-- minetest.log("do_attack SRC " .. dump(self.object) ) -- { object = userdata: 0x49d9a1a0, ...
-- minetest.log("do_attack DST " .. dump(object) ) -- userdata metatable: { is_player(), get_player_name()
-- minetest.log("do_attack SRC " .. name(self.object) )
-- minetest.log("do_attack DST " .. name(object) )
local src = self.object
if object then
local dst = object
minetest.log("# " .. msg .. " " .. name(src) .. " > " .. name(dst) .. " dist:" .. vector.distance(src:get_pos(), dst:get_pos()) .. " self:" .. dump(src == dst))
else
minetest.log("# " .. msg .. " " .. name(src))
end
end
-- get this mob to attack the object -- get this mob to attack the object
function mob_class:do_attack(object) function mob_class:do_attack(object)
@ -50,6 +78,7 @@ function mob_class:do_attack(object)
self.attack = object self.attack = object
self.state = "attack" self.state = "attack"
self:log2("do_attack", object)
-- TODO: Implement war_cry sound without being annoying -- TODO: Implement war_cry sound without being annoying
--if random(0, 100) < 90 then --if random(0, 100) < 90 then
--self:mob_sound("war_cry", true) --self:mob_sound("war_cry", true)
@ -204,6 +233,7 @@ function mob_class:smart_mobs(s, p, dist, dtime)
self.path.way = minetest.find_path(s, p1, 16, jumpheight, dropheight, "A*_noprefetch") self.path.way = minetest.find_path(s, p1, 16, jumpheight, dropheight, "A*_noprefetch")
self.state = "" self.state = ""
self:log2("smart_mobs -> do_attack", self.attack)
self:do_attack(self.attack) self:do_attack(self.attack)
-- no path found, try something else -- no path found, try something else
@ -360,7 +390,8 @@ function mob_class:monster_attack()
player = obj.object player = obj.object
name = obj.name or "" name = obj.name or ""
end end
if obj and obj.type == self.type and obj.passive == false and obj.state == "attack" and obj.attack then if obj and obj.type == self.type and obj.passive == false and obj.state == "attack" and obj.attack and self.object ~= obj.attack then
self:log2("add blacklist",obj.attack)
table.insert(blacklist_attack, obj.attack) table.insert(blacklist_attack, obj.attack)
end end
end end
@ -386,8 +417,10 @@ function mob_class:monster_attack()
end end
end end
-- find specific mob to attack, failing that attack player/npc/animal -- find specific mob to attack, failing that attack player/npc/animal
if specific_attack(self.specific_attack, name) if specific_attack(self.specific_attack, name)
and self.object ~= player
and (type == "player" or ( type == "npc" and self.attack_npcs ) and (type == "player" or ( type == "npc" and self.attack_npcs )
or (type == "animal" and self.attack_animals == true) or (type == "animal" and self.attack_animals == true)
or (self.extra_hostile and not self.attack_exception(player))) then or (self.extra_hostile and not self.attack_exception(player))) then
@ -416,13 +449,19 @@ function mob_class:monster_attack()
end end
end end
if not min_player and #blacklist_attack > 0 then if not min_player and #blacklist_attack > 0 then
self:log2("pre OOPS monster_attack -> do_attack", min_player)
min_player=blacklist_attack[math.random(#blacklist_attack)] min_player=blacklist_attack[math.random(#blacklist_attack)]
end end
-- attack player -- attack player
if min_player then if min_player then
if self.object == min_player then
self:log2("OOPS monster_attack -> do_attack", min_player)
else
self:log2("monster_attack -> do_attack", min_player)
self:do_attack(min_player) self:do_attack(min_player)
end end
end end
end
-- npc, find closest monster to attack -- npc, find closest monster to attack
@ -460,6 +499,7 @@ function mob_class:npc_attack()
end end
if min_player then if min_player then
self:log2("npc_attack -> do_attack", min_player)
self:do_attack(min_player) self:do_attack(min_player)
end end
end end
@ -544,6 +584,7 @@ function mob_class:on_punch(hitter, tflp, tool_capabilities, dir)
return return
end end
self:log2("on_punch", hitter)
-- custom punch function -- custom punch function
if self.do_punch then if self.do_punch then
@ -788,6 +829,7 @@ function mob_class:on_punch(hitter, tflp, tool_capabilities, dir)
if not die then if not die then
-- attack whoever punched mob -- attack whoever punched mob
self.state = "" self.state = ""
self:log2("on_punch[-1] -> do_attack", hitter)
self:do_attack(hitter) self:do_attack(hitter)
self._aggro= true self._aggro= true
end end
@ -807,11 +849,15 @@ function mob_class:on_punch(hitter, tflp, tool_capabilities, dir)
and obj.state ~= "attack" and obj.state ~= "attack"
and obj.owner ~= name then and obj.owner ~= name then
if obj.name == self.name then if obj.name == self.name then
--minetest.log("on_punch0 -> do_attack")
obj:log2("group on_punch[0] -> do_attack", hitter)
obj:do_attack(hitter) obj:do_attack(hitter)
elseif type(obj.group_attack) == "table" then elseif type(obj.group_attack) == "table" then
for i=1, #obj.group_attack do for i=1, #obj.group_attack do
if obj.group_attack[i] == self.name then if obj.group_attack[i] == self.name then
obj._aggro = true obj._aggro = true
--minetest.log("on_punch1 -> do_attack")
obj:log2("group on_punch[1] -> do_attack", hitter)
obj:do_attack(hitter) obj:do_attack(hitter)
break break
end end
@ -821,6 +867,8 @@ function mob_class:on_punch(hitter, tflp, tool_capabilities, dir)
-- have owned mobs attack player threat -- have owned mobs attack player threat
if obj.owner == name and obj.owner_loyal then if obj.owner == name and obj.owner_loyal then
--minetest.log("on_punch2 -> do_attack")
obj:log2("on_punch[2] -> do_attack", self.object)
obj:do_attack(self.object) obj:do_attack(self.object)
end end
end end
@ -990,7 +1038,7 @@ function mob_class:do_states_attack (dtime)
end end
elseif self.attack_type == "dogfight" elseif self.attack_type == "dogfight"
or (self.attack_type == "dogshoot" and self:dogswitch(dtime) == 2) and (dist >= self.avoid_distance or not self.shooter_avoid_enemy) or (self.attack_type == "dogshoot" and self:dogswitch(dtime) == 2 and (dist >= self.avoid_distance or not self.shooter_avoid_enemy))
or (self.attack_type == "dogshoot" and dist <= self.reach and self:dogswitch() == 0) then or (self.attack_type == "dogshoot" and dist <= self.reach and self:dogswitch() == 0) then
if self.fly if self.fly
@ -1130,6 +1178,11 @@ function mob_class:do_states_attack (dtime)
s2.y = s2.y + .5 s2.y = s2.y + .5
if self:line_of_sight( p2, s2) == true then if self:line_of_sight( p2, s2) == true then
minetest.log("dogfight2 " .. dist .. "<" .. self.reach
.. " sae:".. dump(self.shooter_avoid_enemy)
.. " sa:" .. dump(self.shoot_arrow and true or false))
self:mob_sound("attack") self:mob_sound("attack")
-- punch player (or what player is attached to) -- punch player (or what player is attached to)
@ -1137,6 +1190,7 @@ function mob_class:do_states_attack (dtime)
if attached then if attached then
self.attack = attached self.attack = attached
end end
self:log2("do_state_attack", self.attack)
self.attack:punch(self.object, 1.0, { self.attack:punch(self.object, 1.0, {
full_punch_interval = 1.0, full_punch_interval = 1.0,
damage_groups = {fleshy = self.damage} damage_groups = {fleshy = self.damage}
@ -1152,14 +1206,17 @@ function mob_class:do_states_attack (dtime)
end end
end end
end end
elseif dist <= self.shoot_reach and (self.attack_type == "shoot"
elseif self.attack_type == "shoot"
or (self.attack_type == "dogshoot" and self:dogswitch(dtime) == 1) or (self.attack_type == "dogshoot" and self:dogswitch(dtime) == 1)
or (self.attack_type == "dogshoot" and (dist > self.reach or dist < self.avoid_distance and self.shooter_avoid_enemy) and self:dogswitch() == 0) then or (self.attack_type == "dogshoot" and ((dist > self.reach or dist < self.avoid_distance) and self.shooter_avoid_enemy) and self:dogswitch() == 0)) then
p.y = p.y - .5 p.y = p.y - .5
s.y = s.y + .5 s.y = s.y + .5
--#self:log2("shoot 3", object)
minetest.log("shoot3 " .. dist .. "<=" .. self.shoot_reach
.. " sae:".. dump(self.shooter_avoid_enemy)
.. " sa:" .. dump(self.shoot_arrow and true or false))
local dist = vector.distance(p, s) local dist = vector.distance(p, s)
local vec = { local vec = {
x = p.x - s.x, x = p.x - s.x,

View File

@ -229,6 +229,7 @@ function mcl_mobs.register_mob(name, def)
health = 0, health = 0,
frame_speed_multiplier = 1, frame_speed_multiplier = 1,
reach = def.reach or 3, reach = def.reach or 3,
shoot_reach = def.shoot_reach or def.view_range or 16,
htimer = 0, htimer = 0,
texture_list = def.textures, texture_list = def.textures,
child_texture = def.child_texture, child_texture = def.child_texture,
@ -289,6 +290,7 @@ function mcl_mobs.register_mob(name, def)
noyaw = def.noyaw or false, noyaw = def.noyaw or false,
particlespawners = def.particlespawners, particlespawners = def.particlespawners,
spawn_check = def.spawn_check, spawn_check = def.spawn_check,
avoid_from = def.avoid_from,
-- End of MCL2 extensions -- End of MCL2 extensions
on_spawn = def.on_spawn, on_spawn = def.on_spawn,
on_blast = def.on_blast or function(self,damage) on_blast = def.on_blast or function(self,damage)

View File

@ -509,6 +509,81 @@ function mob_class:do_jump()
return false return false
end end
local function in_list(list, what)
return type(list) == "table" and table.indexof(list, what) ~= -1
end
function mob_class:is_object_in_view(object_list, object_range, node_range, turn_around)
local s = self.object:get_pos()
local min_dist = object_range + 1
local objs = minetest.get_objects_inside_radius(s, object_range)
local object_pos = nil
for n = 1, #objs do
local name = ""
local object = objs[n]
if object:is_player() then
if not (mcl_mobs.invis[ object:get_player_name() ]
or self.owner == object:get_player_name()
or (not self:object_in_range(object))) then
name = "player"
if not (name ~= self.name
and in_list(object_list, name)) then
local item = object:get_wielded_item()
name = item:get_name() or ""
end
end
else
local obj = object:get_luaentity()
if obj then
object = obj.object
name = obj.name or ""
end
end
-- find specific mob to avoid or runaway from
if name ~= "" and name ~= self.name
and in_list(object_list, name) then
local p = object:get_pos()
local dist = vector.distance(p, s)
-- choose closest player/mob to avoid or runaway from
if dist < min_dist
-- aim higher to make looking up hills more realistic
and self:line_of_sight(vector.offset(s, 0,1,0), vector.offset(p, 0,1,0)) == true then
min_dist = dist
object_pos = p
end
end
end
if not object_pos then
-- find specific node to avoid or runaway from
local p = minetest.find_node_near(s, node_range, object_list, true)
local dist = p and vector.distance(p, s)
if dist and dist < min_dist
and self:line_of_sight(s, p) == true then
min_dist = dist
object_pos = p
end
end
if object_pos and turn_around then
local vec = vector.subtract(object_pos, s)
local yaw = (atan(vec.z / vec.x) + 3 *math.pi/ 2) - self.rotate
if object_pos.x > s.x then yaw = yaw + math.pi end
yaw = self:set_yaw(yaw, 4)
end
return object_pos ~= nil
end
-- should mob follow what I'm holding ? -- should mob follow what I'm holding ?
function mob_class:follow_holding(clicker) function mob_class:follow_holding(clicker)
if self.nofollow then return false end if self.nofollow then return false end
@ -526,15 +601,9 @@ function mob_class:follow_holding(clicker)
return true return true
-- multiple items -- multiple items
elseif t == "table" then elseif t == "table" and in_list(self.follow, item:get_name()) then
for no = 1, #self.follow do
if self.follow[no] == item:get_name() then
return true return true
end end
end
end
return false return false
end end
@ -593,104 +662,14 @@ function mob_class:replace_node(pos)
end end
end end
-- specific runaway
local specific_runaway = function(list, what)
if type(list) ~= "table" then
list = {}
end
-- no list so do not run
if list == nil then
return false
end
-- found entity on list to attack?
for no = 1, #list do
if list[no] == what then
return true
end
end
return false
end
-- find someone to runaway from -- find someone to runaway from
function mob_class:check_runaway_from() function mob_class:check_runaway_from()
if not self.runaway_from and self.state ~= "flop" then if not self.runaway_from and self.state ~= "flop" then
return return
end end
local s = self.object:get_pos() if self:is_object_in_view(self.runaway_from, self.view_range, self.view_range / 2, true) then
local p, sp, dist self.shaking = self.shaking or 5
local player, obj, min_player
local type, name = "", ""
local min_dist = self.view_range + 1
local objs = minetest.get_objects_inside_radius(s, self.view_range)
for n = 1, #objs do
if objs[n]:is_player() then
if mcl_mobs.invis[ objs[n]:get_player_name() ]
or self.owner == objs[n]:get_player_name()
or (not self:object_in_range(objs[n])) then
type = ""
else
player = objs[n]
type = "player"
name = "player"
end
else
obj = objs[n]:get_luaentity()
if obj then
player = obj.object
type = obj.type
name = obj.name or ""
end
end
-- find specific mob to runaway from
if name ~= "" and name ~= self.name
and specific_runaway(self.runaway_from, name) then
p = player:get_pos()
sp = s
-- aim higher to make looking up hills more realistic
p.y = p.y + 1
sp.y = sp.y + 1
dist = vector.distance(p, s)
-- choose closest player/mpb to runaway from
if dist < min_dist
and self:line_of_sight(sp, p, 2) == true then
min_dist = dist
min_player = player
end
end
end
if min_player then
local lp = player:get_pos()
local vec = {
x = lp.x - s.x,
y = lp.y - s.y,
z = lp.z - s.z
}
local yaw = (atan(vec.z / vec.x) + 3 *math.pi/ 2) - self.rotate
if lp.x > s.x then
yaw = yaw + math.pi
end
yaw = self:set_yaw( yaw, 4)
self.state = "runaway" self.state = "runaway"
self.runaway_timer = 3 self.runaway_timer = 3
self.following = nil self.following = nil
@ -914,30 +893,28 @@ function mob_class:do_states_walk()
end end
end end
-- A danger is near but mob is not inside
else
-- Randomly turn
if math.random(1, 100) <= 30 then
yaw = yaw + math.random(-0.5, 0.5)
yaw = self:set_yaw( yaw, 8)
end end
end end
yaw = self:set_yaw( yaw, 8) if not is_in_danger then
local distance = self.avoid_distance or self.view_range / 2
-- find specific node to avoid
if self:is_object_in_view(self.avoid_from, distance, distance, true) then
self.shaking = self.shaking or 2
self:set_velocity(self.walk_velocity)
-- otherwise randomly turn -- otherwise randomly turn
elseif math.random(1, 100) <= 30 then elseif math.random(1, 100) <= 30 then
yaw = yaw + math.random(-0.5, 0.5) yaw = yaw + math.random(-0.5, 0.5)
yaw = self:set_yaw(yaw, 8) yaw = self:set_yaw(yaw, 8)
end end
end
-- stand for great fall or danger or fence in front -- stand for great fall or danger or fence in front
local cliff_or_danger = false local cliff_or_danger = false
if is_in_danger then if is_in_danger then
cliff_or_danger = self:is_at_cliff_or_danger() cliff_or_danger = self:is_at_cliff_or_danger()
end end
if self.facing_fence == true if self.facing_fence == true
or cliff_or_danger or cliff_or_danger
or math.random(1, 100) <= 30 then or math.random(1, 100) <= 30 then
@ -1086,6 +1063,12 @@ function mob_class:check_smooth_rotation(dtime)
self.delay = self.delay - 1 self.delay = self.delay - 1
if self.shaking then if self.shaking then
yaw = yaw + (math.random() * 2 - 1) * 5 * dtime yaw = yaw + (math.random() * 2 - 1) * 5 * dtime
if type(self.shaking) == "number" then
self.shaking = self.shaking - dtime
if self.shaking <= 0 then
self.shaking = nil
end
end
end end
self.object:set_yaw(yaw) self.object:set_yaw(yaw)
--self:update_roll() --self:update_roll()

View File

@ -40,6 +40,17 @@ local hoglin = {
makes_footstep_sound = true, makes_footstep_sound = true,
walk_velocity = 1, walk_velocity = 1,
run_velocity = 2.8, run_velocity = 2.8,
avoid_from = {
"mcl_crimson:warped_fungus",
"mcl_flowerpots:flower_pot_warped_fungus",
"mcl_portals:portal",
"mcl_beds:respawn_anchor",
"mcl_beds:respawn_anchor_charged_1",
"mcl_beds:respawn_anchor_charged_2",
"mcl_beds:respawn_anchor_charged_3",
"mcl_beds:respawn_anchor_charged_4",
},
follow = {"mcl_crimson:crimson_fungus"},
drops = { drops = {
{name = "mobs_mcitems:leather", {name = "mobs_mcitems:leather",
chance = 1, chance = 1,
@ -87,9 +98,26 @@ local hoglin = {
attack_animals = true, attack_animals = true,
} }
mcl_mobs.register_mob("mobs_mc:hoglin", hoglin)
local zoglin = table.copy(hoglin) local zoglin = table.copy(hoglin)
hoglin.on_rightclick = function(self, clicker)
-- local item = clicker:get_wielded_item()
if self:feed_tame(clicker, 1, true, false) then return end
-- if mcl_mobs:protect(self, clicker) then return end
end
hoglin.on_breed = function(parent1, parent2)
local pos = parent1.object:get_pos()
local child = mcl_mobs.spawn_child(pos, parent1.name)
if child then
local ent_c = child:get_luaentity()
-- ent_c.tamed = true
ent_c.owner = parent1.owner
return false
end
end,
mcl_mobs.register_mob("mobs_mc:hoglin", hoglin)
zoglin.description = S("Zoglin") zoglin.description = S("Zoglin")
zoglin.fire_resistant = 1 zoglin.fire_resistant = 1
zoglin.textures = {"extra_mobs_zoglin.png"} zoglin.textures = {"extra_mobs_zoglin.png"}

View File

@ -53,6 +53,7 @@ mcl_mobs.register_mob("mobs_mc:llama", {
spawn_class = "passive", spawn_class = "passive",
passive = false, passive = false,
attack_type = "shoot", attack_type = "shoot",
--attack_type = "dogfight",
shoot_interval = 5.5, shoot_interval = 5.5,
arrow = "mobs_mc:llamaspit", arrow = "mobs_mc:llamaspit",
shoot_offset = 1, --3.5 *would* be a good value visually but it somehow messes with the projectiles trajectory shoot_offset = 1, --3.5 *would* be a good value visually but it somehow messes with the projectiles trajectory
@ -112,6 +113,12 @@ mcl_mobs.register_mob("mobs_mc:llama", {
}, },
follow = { "mcl_farming:wheat_item", "mcl_farming:hay_block" }, follow = { "mcl_farming:wheat_item", "mcl_farming:hay_block" },
view_range = 16, view_range = 16,
attack_animals = true,
damage = 1,
shoot_reach = 5,
--avoid_distance = 5, -- default 9
--shooter_avoid_enemy = true,
specific_attack = { "mobs_mc:wolf" },
do_custom = function(self, dtime) do_custom = function(self, dtime)
-- set needed values if not already present -- set needed values if not already present

View File

@ -42,6 +42,7 @@ local rabbit = {
makes_footstep_sound = false, makes_footstep_sound = false,
walk_velocity = 1, walk_velocity = 1,
run_velocity = 3.7, run_velocity = 3.7,
avoid_from = {"mobs_mc:wolf"},
follow_velocity = 1.1, follow_velocity = 1.1,
floats = 1, floats = 1,
runaway = true, runaway = true,

View File

@ -46,6 +46,7 @@ local skeleton = {
}, },
walk_velocity = 1.2, walk_velocity = 1.2,
run_velocity = 2.0, run_velocity = 2.0,
runaway_from = {"mobs_mc:wolf"},
damage = 2, damage = 2,
reach = 2, reach = 2,
drops = { drops = {

View File

@ -44,6 +44,7 @@ mcl_mobs.register_mob("mobs_mc:witherskeleton", {
}, },
walk_velocity = 1.2, walk_velocity = 1.2,
run_velocity = 2.0, run_velocity = 2.0,
runaway_from = {"mobs_mc:wolf"},
damage = 7, damage = 7,
reach = 2, reach = 2,
drops = { drops = {

View File

@ -46,6 +46,8 @@ local wolf = {
walk_chance = default_walk_chance, walk_chance = default_walk_chance,
walk_velocity = 2, walk_velocity = 2,
run_velocity = 3, run_velocity = 3,
runaway = true,
runaway_from = { "mobs_mc:llama" },
damage = 4, damage = 4,
reach = 2, reach = 2,
attack_type = "dogfight", attack_type = "dogfight",
@ -97,7 +99,16 @@ local wolf = {
jump = true, jump = true,
attacks_monsters = true, attacks_monsters = true,
attack_animals = true, attack_animals = true,
specific_attack = { "player", "mobs_mc:sheep" }, specific_attack = {
--FIXME: "player",
"mobs_mc:sheep",
"mobs_mc:rabbit",
-- TODO: "mobs_mc:fox",
"mobs_mc:skeleton",
"mobs_mc:stray",
"mobs_mc:witherskeleton",
},
avoid_from = { "mobs_mc:llama" },
} }
mcl_mobs.register_mob("mobs_mc:wolf", wolf) mcl_mobs.register_mob("mobs_mc:wolf", wolf)