From e472c31976ffc54c576a3f903913aefe1475eb9e Mon Sep 17 00:00:00 2001 From: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:17:55 -0400 Subject: [PATCH] Moves possessed objects to basic mobs (#29851) * Moves possessed objects to basic mobs * Linters * Merge conflict resolution --------- Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> --- .../basic_ai_behaviors/swirl_around.dm | 40 ++++++++++ .../basic_subtrees/swirl_around_target.dm | 7 ++ .../gamemodes/miniantags/revenant/revenant.dm | 1 + .../miniantags/revenant/revenant_abilities.dm | 49 +++++------- code/modules/admin/misc_admin_procs.dm | 2 +- .../posessed_object.dm | 74 ++++++++++++------- paradise.dme | 4 +- 7 files changed, 120 insertions(+), 57 deletions(-) create mode 100644 code/datums/ai/basic_mobs/basic_ai_behaviors/swirl_around.dm create mode 100644 code/datums/ai/basic_mobs/basic_subtrees/swirl_around_target.dm rename code/modules/mob/living/{simple_animal => basic}/posessed_object.dm (70%) diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/swirl_around.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/swirl_around.dm new file mode 100644 index 00000000000..b609c1d6bb0 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/swirl_around.dm @@ -0,0 +1,40 @@ +/datum/ai_behavior/swirl_around_target + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM + required_distance = 0 + /// chance to swirl + var/swirl_chance = 60 + +/datum/ai_behavior/swirl_around_target/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/swirl_around_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + var/atom/target = controller.blackboard[target_key] + var/mob/living/living_pawn = controller.pawn + + if(QDELETED(target)) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED + + if(get_dist(target, living_pawn) > 1) + set_movement_target(controller, target) + return AI_BEHAVIOR_DELAY + + if(!SPT_PROB(swirl_chance, seconds_per_tick)) + return AI_BEHAVIOR_DELAY + + var/list/possible_turfs = list() + + for(var/turf/possible_turf in oview(2, target)) + if(possible_turf.is_blocked_turf(source_atom = living_pawn)) + continue + possible_turfs += possible_turf + + if(!length(possible_turfs)) + return AI_BEHAVIOR_DELAY + + if(isnull(controller.movement_target_source) || controller.movement_target_source == type) + set_movement_target(controller, pick(possible_turfs)) + return AI_BEHAVIOR_DELAY diff --git a/code/datums/ai/basic_mobs/basic_subtrees/swirl_around_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/swirl_around_target.dm new file mode 100644 index 00000000000..0ba020c7460 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/swirl_around_target.dm @@ -0,0 +1,7 @@ +/datum/ai_planning_subtree/swirl_around_target + /// Variable to store target in + var/target_key = BB_BASIC_MOB_CURRENT_TARGET + +/datum/ai_planning_subtree/swirl_around_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + controller.queue_behavior(/datum/ai_behavior/swirl_around_target, target_key) diff --git a/code/game/gamemodes/miniantags/revenant/revenant.dm b/code/game/gamemodes/miniantags/revenant/revenant.dm index 4d457535414..2b024671e89 100644 --- a/code/game/gamemodes/miniantags/revenant/revenant.dm +++ b/code/game/gamemodes/miniantags/revenant/revenant.dm @@ -40,6 +40,7 @@ contains_xeno_organ = TRUE ignore_generic_organs = TRUE surgery_container = /datum/xenobiology_surgery_container/revenant + faction = list("revenant") /// The revenant's idle icon var/icon_idle = "revenant_idle" diff --git a/code/game/gamemodes/miniantags/revenant/revenant_abilities.dm b/code/game/gamemodes/miniantags/revenant/revenant_abilities.dm index d70d689f6ff..860c720e40a 100644 --- a/code/game/gamemodes/miniantags/revenant/revenant_abilities.dm +++ b/code/game/gamemodes/miniantags/revenant/revenant_abilities.dm @@ -369,43 +369,32 @@ /// Handles making an object haunted and setting it up to attack /datum/spell/aoe/revenant/haunt_object/proc/make_spooky(obj/item/item_to_possess, mob/living/simple_animal/revenant/user) new /obj/effect/temp_visual/revenant(get_turf(item_to_possess)) // Thematic spooky visuals - var/mob/living/simple_animal/possessed_object/possessed_object = new(item_to_possess) // Begin haunting object - item_to_possess.throwforce = min(item_to_possess.throwforce + 5, 15) // Damage it should do? throwforce+5 or 15, whichever is lower + var/mob/living/basic/possessed_object/revenant/possessed_object = new(item_to_possess) // Begin haunting object set_outline(possessed_object) - possessed_object.maxHealth = 100 // Double the regular HP of possessed objects - possessed_object.health = 100 - possessed_object.escape_chance = 100 // We cannot be contained ADD_TRAIT(possessed_object, TRAIT_DODGE_ALL_OBJECTS, "Revenant") + addtimer(CALLBACK(src, PROC_REF(begin_poltergheist), possessed_object, user), 1 SECONDS, TIMER_UNIQUE) // Short warm-up for floaty ambience + addtimer(CALLBACK(possessed_object, TYPE_PROC_REF(/mob/living/basic/possessed_object, death)), 70 SECONDS, TIMER_UNIQUE) // De-haunt the object - addtimer(CALLBACK(src, PROC_REF(attack__legacy__attackchain), possessed_object, user), 1 SECONDS, TIMER_UNIQUE) // Short warm-up for floaty ambience - attack_timers.Add(addtimer(CALLBACK(src, PROC_REF(attack__legacy__attackchain), possessed_object, user), 4 SECONDS, TIMER_UNIQUE|TIMER_LOOP|TIMER_STOPPABLE)) // 5 second looping attacks - addtimer(CALLBACK(possessed_object, TYPE_PROC_REF(/mob/living/simple_animal/possessed_object, death)), 70 SECONDS, TIMER_UNIQUE) // De-haunt the object +/// Gives it AI +/datum/spell/aoe/revenant/haunt_object/proc/begin_poltergheist(mob/living/basic/possessed_object/possessed_object, mob/living/simple_animal/revenant/user) + possessed_object.ai_controller = new /datum/ai_controller/basic_controller/revenant(possessed_object) -/// Handles finding a valid target and throwing us at it -/datum/spell/aoe/revenant/haunt_object/proc/attack__legacy__attackchain(mob/living/simple_animal/possessed_object/possessed_object, mob/living/simple_animal/revenant/user) - var/list/potential_victims = list() - for(var/turf/turf_to_search in spiral_range_turfs(aoe_range, get_turf(possessed_object))) - for(var/mob/living/potential_victim in turf_to_search) - if(QDELETED(possessed_object) || !can_see(possessed_object, potential_victim, aoe_range)) // You can't see me - continue - if(potential_victim.stat != CONSCIOUS) // Don't kill our precious essence-filled sleepy mobs - continue - if(istype(potential_victim, user) || istype(potential_victim, possessed_object)) - continue - potential_victims.Add(potential_victim) +/datum/ai_controller/basic_controller/revenant + blackboard = list( + BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + ) - potential_victims.Cut((length(potential_victims) * 0.8) + 1.2) // Only consider the people near us - - if(!length(potential_victims)) - possessed_object.possessed_item.throwforce = min(possessed_object.possessed_item.throwforce + 5, 15) // If an item is stood still for a while it can gather power - set_outline(possessed_object) - return - - var/mob/living/victim = pick(potential_victims) - possessed_object.throw_at(victim, aoe_range, 2, user, dodgeable = FALSE) + ai_movement = /datum/ai_movement/basic_avoidance + idle_behavior = null + planning_subtrees = list( + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/attack_obstacle_in_path, + /datum/ai_planning_subtree/swirl_around_target, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + ) /// Sets the glow on the haunted object, scales up based on throwforce -/datum/spell/aoe/revenant/haunt_object/proc/set_outline(mob/living/simple_animal/possessed_object/possessed_object) +/datum/spell/aoe/revenant/haunt_object/proc/set_outline(mob/living/basic/possessed_object/possessed_object) possessed_object.remove_filter("haunt_glow") var/outline_size = min((possessed_object.possessed_item.throwforce / 15) * 3, 3) possessed_object.add_filter("haunt_glow", 2, list("type" = "outline", "color" = "#7A4FA9", "size" = outline_size)) // Give it spooky purple outline diff --git a/code/modules/admin/misc_admin_procs.dm b/code/modules/admin/misc_admin_procs.dm index 31bea5bda82..46d03f32d04 100644 --- a/code/modules/admin/misc_admin_procs.dm +++ b/code/modules/admin/misc_admin_procs.dm @@ -933,7 +933,7 @@ GLOBAL_VAR_INIT(gamma_ship_location, 1) // 0 = station , 1 = space if(!frommob || !toitem) //make sure the mobs don't go away while we waited for a response return TRUE - var/mob/living/simple_animal/possessed_object/tomob = new(toitem) + var/mob/living/basic/possessed_object/tomob = new(toitem) message_admins("[key_name_admin(usr)] has put [frommob.ckey] in control of [tomob.name].") log_admin("[key_name(usr)] stuffed [frommob.ckey] into [tomob.name].") diff --git a/code/modules/mob/living/simple_animal/posessed_object.dm b/code/modules/mob/living/basic/posessed_object.dm similarity index 70% rename from code/modules/mob/living/simple_animal/posessed_object.dm rename to code/modules/mob/living/basic/posessed_object.dm index 0807eb841b9..75002d7383a 100644 --- a/code/modules/mob/living/simple_animal/posessed_object.dm +++ b/code/modules/mob/living/basic/posessed_object.dm @@ -1,29 +1,27 @@ -/mob/living/simple_animal/possessed_object +/mob/living/basic/possessed_object name = "possessed object" var/spirit_name = "mysterious force" // What we call ourselves in attack messages. mob_biotypes = MOB_SPIRIT health = 50 maxHealth = 50 - pass_flags = PASSTABLE // Floating past tables is pretty damn spooky. status_flags = null // No canpush to prevent grabs ... density = FALSE // ... But a density of 0 means we won't be blocking anyone's way. healable = FALSE // Animated with SPACE NECROMANCY, mere mortal medicines cannot heal such an object. - wander = FALSE // These things probably ought to never be AI controlled, but in the event they are probably shouldn't wander. - universal_speak = TRUE // Tell the humans spooky things about the afterlife + ai_controller = null // We don't give this mob a controller, as usually they're player controlled. Rev versions get a special controller. speak_emote = list("mumbles", "moans", "whispers", "laments", "screeches") - no_spin_thrown = TRUE - del_on_death = TRUE + basic_mob_flags = DEL_ON_DEATH weather_immunities = list("ash") - /// The probability % of us escaping if stuffed into a bag/toolbox/etc var/escape_chance = 10 /// What is the actual item we are "possessing" var/obj/item/possessed_item + /// Is this item possessed by a revenant? + var/revenant_possessed = FALSE -/mob/living/simple_animal/possessed_object/examine(mob/user) +/mob/living/basic/possessed_object/examine(mob/user) . = possessed_item.examine(user) if(health > (maxHealth / 30)) . += "[src] appears to be floating without any support!" @@ -31,17 +29,17 @@ . += "[src] appears to be having trouble staying afloat!" -/mob/living/simple_animal/possessed_object/do_attack_animation(atom/A, visual_effect_icon, used_item, no_effect) +/mob/living/basic/possessed_object/do_attack_animation(atom/A, visual_effect_icon, used_item, no_effect) ..() animate_ghostly_presence(src, -1, 20, 1) // Restart the floating animation after the attack animation, as it will be cancelled. -/mob/living/simple_animal/possessed_object/start_pulling(atom/movable/AM, state, force = pull_force, show_message = FALSE) // Silly motherfuckers think they can pull things. +/mob/living/basic/possessed_object/start_pulling(atom/movable/AM, state, force = pull_force, show_message = FALSE) // Silly motherfuckers think they can pull things. if(show_message) to_chat(src, "You are unable to pull [AM]!") -/mob/living/simple_animal/possessed_object/ghost() // Ghosting will return the object to normal, and will not disqualify the ghoster from various mid-round antag positions. +/mob/living/basic/possessed_object/ghost() // Ghosting will return the object to normal, and will not disqualify the ghoster from various mid-round antag positions. var/response = tgui_alert(src, "End your possession of this object? (It will not stop you from respawning later)", "Are you sure you want to ghost?", list("Ghost", "Stay in body")) if(response != "Ghost") return @@ -50,7 +48,7 @@ ghost.timeofdeath = world.time death(0) // Turn back into a regular object. -/mob/living/simple_animal/possessed_object/death(gibbed) +/mob/living/basic/possessed_object/death(gibbed) if(can_die()) ghostize(TRUE) // if gibbed, the item goes with the ghost @@ -62,7 +60,7 @@ return ..() -/mob/living/simple_animal/possessed_object/Life(seconds, times_fired) +/mob/living/basic/possessed_object/Life(seconds, times_fired) ..() if(!possessed_item) // If we're a donut and someone's eaten us, for instance. @@ -86,12 +84,12 @@ if(possessed_item.loc != src) //safety so the item doesn't somehow become detatched from us while doing this possessed_item.forceMove(src) -/mob/living/simple_animal/possessed_object/Login() +/mob/living/basic/possessed_object/Login() ..() to_chat(src, "Your spirit has entered [src] and possessed it.
You are able to do most things a humanoid would be able to do with a [src] in their hands.
If you want to end your ghostly possession, use the 'ghost' verb, it won't penalize your ability to respawn.
") -/mob/living/simple_animal/possessed_object/Initialize(mapload) +/mob/living/basic/possessed_object/Initialize(mapload) . = ..() if(!isitem(loc)) // Some silly motherfucker spawned us directly via the game panel. @@ -114,27 +112,31 @@ throwforce = possessed_item.throwforce armour_penetration_flat = possessed_item.armour_penetration_flat armour_penetration_percentage = possessed_item.armour_penetration_percentage + melee_damage_lower = max(0, possessed_item.force - 5) + melee_damage_upper = possessed_item.force + 5 visible_message("[src] rises into the air and begins to float!") // Inform those around us that shit's gettin' spooky. animate_ghostly_presence(src, -1, 20, 1) -/mob/living/simple_animal/possessed_object/get_active_hand() // So that our attacks count as attacking with the item we've possessed. +/mob/living/basic/possessed_object/get_active_hand() // So that our attacks count as attacking with the item we've possessed. + if(revenant_possessed) + return ..() return possessed_item -/mob/living/simple_animal/possessed_object/IsAdvancedToolUser() // So we can shoot guns (Mostly ourselves), among other things. +/mob/living/basic/possessed_object/IsAdvancedToolUser() // So we can shoot guns (Mostly ourselves), among other things. return TRUE -/mob/living/simple_animal/possessed_object/get_access() // If we've possessed an ID card we've got access to lots of fun things! +/mob/living/basic/possessed_object/get_access() // If we've possessed an ID card we've got access to lots of fun things! if(istype(possessed_item, /obj/item/card/id)) var/obj/item/card/id/possessed_id = possessed_item . = possessed_id.access -/mob/living/simple_animal/possessed_object/ClickOn(atom/A, params) - if(client.click_intercept) +/mob/living/basic/possessed_object/ClickOn(atom/A, params) + if(client && client.click_intercept) client.click_intercept.InterceptClickOn(src, params, A) return @@ -153,11 +155,15 @@ forceMove(possessed_item.loc) possessed_item.forceMove(src) else // If we're inside a toolbox or something, we are inside the item rather than the item inside us. This is so people can see the item in the toolbox. - forceMove( possessed_item ) + forceMove(possessed_item) update_icon() -/mob/living/simple_animal/possessed_object/update_icon(update_pixel_xy = 0) +/mob/living/basic/possessed_object/melee_attack(atom/target, list/modifiers, ignore_cooldown) + . = ..() + animate_ghostly_presence(src, -1, 20, 1) + +/mob/living/basic/possessed_object/update_icon(update_pixel_xy = 0) name = possessed_item.name // Take on all the attributes of the item we've possessed. real_name = name desc = possessed_item.desc @@ -173,13 +179,31 @@ set_opacity(possessed_item.opacity) return ..(NONE) -/mob/living/simple_animal/possessed_object/item_interaction(mob/living/user, obj/item/O, list/modifiers) +/mob/living/basic/possessed_object/item_interaction(mob/living/user, obj/item/O, list/modifiers) if(istype(O, /obj/item/nullrod)) visible_message("[O] dispels the spooky aura!") death() return ITEM_INTERACT_COMPLETE -/mob/living/simple_animal/possessed_object/throw_impact(atom/hit_atom, throwingdatum) - //Don't call parent here as the mob isn't doing the hitting, technically +/mob/living/basic/possessed_object/throw_impact(atom/hit_atom, throwingdatum) + // Don't call parent here as the mob isn't doing the hitting, technically return possessed_item.throw_impact(hit_atom, throwingdatum) + +/mob/living/basic/possessed_object/revenant + maxHealth = 100 + health = 100 + melee_attack_cooldown_min = 4 SECONDS + melee_attack_cooldown_max = 5 SECONDS + faction = list("revenant") + speed = -1 + a_intent = INTENT_HARM + escape_chance = 100 + revenant_possessed = TRUE + +/mob/living/basic/possessed_object/revenant/Initialize(mapload) + . = ..() + AddComponent(/datum/component/swarming) + throwforce = min(possessed_item.throwforce + 5, 15) // Damage it should do? throwforce+5 or 15, whichever is lower + melee_damage_lower = min(possessed_item.throwforce + 5, 15) + melee_damage_upper = melee_damage_lower diff --git a/paradise.dme b/paradise.dme index 086cbc82429..ad8654c4737 100644 --- a/paradise.dme +++ b/paradise.dme @@ -462,6 +462,7 @@ #include "code\datums\ai\basic_mobs\basic_ai_behaviors\nearest_targeting.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\run_away_from_target.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\stop_and_stare.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\swirl_around.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\targeting.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\tipped_reaction.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\travel_towards.dm" @@ -475,6 +476,7 @@ #include "code\datums\ai\basic_mobs\basic_subtrees\simple_attack_target.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\simple_find_nearest_target_to_flee.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\simple_find_target.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\swirl_around_target.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\stare_at_thing.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\target_retaliate.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\tip_reaction.dm" @@ -2408,6 +2410,7 @@ #include "code\modules\mob\living\stat_states.dm" #include "code\modules\mob\living\taste.dm" #include "code\modules\mob\living\basic\basic_mob.dm" +#include "code\modules\mob\living\basic\posessed_object.dm" #include "code\modules\mob\living\basic\farm_animals\cow.dm" #include "code\modules\mob\living\basic\farm_animals\deer.dm" #include "code\modules\mob\living\basic\farm_animals\deer_ai.dm" @@ -2597,7 +2600,6 @@ #include "code\modules\mob\living\simple_animal\hide_action.dm" #include "code\modules\mob\living\simple_animal\parrot.dm" #include "code\modules\mob\living\simple_animal\parrot_stripping.dm" -#include "code\modules\mob\living\simple_animal\posessed_object.dm" #include "code\modules\mob\living\simple_animal\shade.dm" #include "code\modules\mob\living\simple_animal\simple_animal.dm" #include "code\modules\mob\living\simple_animal\simple_animal_damage.dm"