diff --git a/_maps/RandomRuins/SpaceRuins/abandonedzoo.dmm b/_maps/RandomRuins/SpaceRuins/abandonedzoo.dmm index ef7740fcc1e..ed62249b2c1 100644 --- a/_maps/RandomRuins/SpaceRuins/abandonedzoo.dmm +++ b/_maps/RandomRuins/SpaceRuins/abandonedzoo.dmm @@ -516,7 +516,7 @@ /turf/open/floor/iron/dark/side, /area/ruin/space/has_grav/abandonedzoo) "WV" = ( -/mob/living/simple_animal/hostile/asteroid/basilisk, +/mob/living/basic/mining/basilisk, /turf/open/misc/asteroid, /area/ruin/space/has_grav/abandonedzoo) "Xl" = ( diff --git a/_maps/RandomRuins/SpaceRuins/prison_shuttle.dmm b/_maps/RandomRuins/SpaceRuins/prison_shuttle.dmm index fe71fb86582..5aeba75b3bd 100644 --- a/_maps/RandomRuins/SpaceRuins/prison_shuttle.dmm +++ b/_maps/RandomRuins/SpaceRuins/prison_shuttle.dmm @@ -19,7 +19,7 @@ /turf/open/floor/carpet/red/airless, /area/ruin/space/prison_shuttle) "g" = ( -/mob/living/simple_animal/hostile/asteroid/basilisk{ +/mob/living/basic/mining/basilisk{ environment_smash = 0 }, /turf/open/misc/asteroid/airless, diff --git a/_maps/RandomZLevels/snowdin.dmm b/_maps/RandomZLevels/snowdin.dmm index 027c9813f86..f497a816b9e 100644 --- a/_maps/RandomZLevels/snowdin.dmm +++ b/_maps/RandomZLevels/snowdin.dmm @@ -10357,7 +10357,7 @@ /turf/open/floor/iron, /area/awaymission/snowdin/post/engineering) "Mo" = ( -/mob/living/simple_animal/hostile/asteroid/basilisk, +/mob/living/basic/mining/basilisk, /turf/open/misc/asteroid/snow{ floor_variance = 0; icon_state = "snow_dug"; @@ -11218,7 +11218,7 @@ /turf/open/floor/iron/dark, /area/awaymission/snowdin/cave) "QP" = ( -/mob/living/simple_animal/hostile/asteroid/basilisk, +/mob/living/basic/mining/basilisk, /turf/open/misc/asteroid/snow/ice, /area/awaymission/snowdin/cave/cavern) "QQ" = ( diff --git a/_maps/map_files/Birdshot/birdshot.dmm b/_maps/map_files/Birdshot/birdshot.dmm index f4ff9260955..aab6e18eb21 100644 --- a/_maps/map_files/Birdshot/birdshot.dmm +++ b/_maps/map_files/Birdshot/birdshot.dmm @@ -26881,7 +26881,7 @@ /turf/open/floor/iron/smooth, /area/station/engineering/supermatter/room) "jOc" = ( -/mob/living/simple_animal/hostile/asteroid/basilisk, +/mob/living/basic/mining/basilisk, /obj/effect/decal/cleanable/dirt, /turf/open/misc/asteroid, /area/station/maintenance/fore/greater) diff --git a/code/__DEFINES/ai/ai.dm b/code/__DEFINES/ai/ai.dm index 3cf1d721891..1a6829946a1 100644 --- a/code/__DEFINES/ai/ai.dm +++ b/code/__DEFINES/ai/ai.dm @@ -22,10 +22,8 @@ #define AI_BEHAVIOR_MOVE_AND_PERFORM (1<<2) ///Does finishing this task not null the current movement target? #define AI_BEHAVIOR_KEEP_MOVE_TARGET_ON_FINISH (1<<3) -///Does finishing this task make the AI stop moving towards the target? -#define AI_BEHAVIOR_KEEP_MOVING_TOWARDS_TARGET_ON_FINISH (1<<4) ///Does this behavior NOT block planning? -#define AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION (1<<5) +#define AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION (1<<4) ///AI flags /// Don't move if being pulled diff --git a/code/__DEFINES/ai/monsters.dm b/code/__DEFINES/ai/monsters.dm index 4ad06155f6c..9af21345ed2 100644 --- a/code/__DEFINES/ai/monsters.dm +++ b/code/__DEFINES/ai/monsters.dm @@ -59,14 +59,20 @@ /// We increment this counter every time we try to move while dragging an arm and if we go too long we'll give up trying to get out of line of sight and just eat the fingers #define BB_LOBSTROSITY_FINGER_LUST "BB_lobstrosity_finger_lust" -///eyeball keys +/// Key containing overwatch ability information +#define BB_WATCHER_OVERWATCH "BB_watcher_overwatch" +/// Key containing gazae ability information +#define BB_WATCHER_GAZE "BB_watcher_gaze" + +// eyeball keys ///the death glare ability #define BB_GLARE_ABILITY "BB_glare_ability" ///the blind target we must protect #define BB_BLIND_TARGET "BB_blind_target" ///value to store the minimum eye damage to prevent us from attacking a human #define BB_EYE_DAMAGE_THRESHOLD "BB_eye_damage_threshold" -///hivebot keys + +// hivebot keys ///the machine we must go to repair #define BB_MACHINE_TARGET "BB_machine_target" ///the hivebot partner we will go communicate with diff --git a/code/__DEFINES/basic_mobs.dm b/code/__DEFINES/basic_mobs.dm index 06a5c4443f9..5a4aebaee23 100644 --- a/code/__DEFINES/basic_mobs.dm +++ b/code/__DEFINES/basic_mobs.dm @@ -16,3 +16,6 @@ /// Temporary trait applied when an attack forecast animation has completed #define TRAIT_BASIC_ATTACK_FORECAST "trait_basic_attack_forecast" #define INTERACTION_BASIC_ATTACK_FORCEAST "interaction_basic_attack_forecast" + +/// Above this speed we stop gliding because it looks silly +#define END_GLIDE_SPEED 10 diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_basic.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_basic.dm index 738f033656a..18c6c651435 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_basic.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_basic.dm @@ -2,3 +2,6 @@ #define COMSIG_BASICMOB_LOOK_DEAD "basicmob_look_dead" /// Sent from /mob/living/basic/proc/look_alive() : () #define COMSIG_BASICMOB_LOOK_ALIVE "basicmob_look_alive" + +///from the ranged_attacks component for basic mobs: (mob/living/basic/firer, atom/target, modifiers) +#define COMSIG_BASICMOB_POST_ATTACK_RANGED "basicmob_post_attack_ranged" diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm index 2c80f3f1985..bb7d2de4989 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm @@ -182,6 +182,12 @@ #define WAIVE_AUTOMUTE_CHECK (1<<0) ///From base of /turf/closed/mineral/proc/gets_drilled(): (turf/closed/mineral/rock, give_exp) #define COMSIG_MOB_MINED "mob_mined" +///Sent by pilot of mech in base of /obj/vehicle/sealed/mecha/relaymove(): (/obj/vehicle/sealed/mecha/mech) +#define COMSIG_MOB_DROVE_MECH "mob_drove_mech" +///Sent by pilot of mech in /obj/vehicle/sealed/mecha/on_mouseclick when using mech equipment : (/obj/vehicle/sealed/mecha/mech) +#define COMSIG_MOB_USED_MECH_EQUIPMENT "mob_used_mech_equipment" +///Sent by pilot of mech in /obj/vehicle/sealed/mecha/on_mouseclick when triggering mech punch : (/obj/vehicle/sealed/mecha/mech) +#define COMSIG_MOB_USED_MECH_MELEE "mob_used_mech_melee" ///from living/flash_act(), when a mob is successfully flashed. #define COMSIG_MOB_FLASHED "mob_flashed" diff --git a/code/__DEFINES/mobfactions.dm b/code/__DEFINES/mobfactions.dm index 82146abc970..a7dcbee90e8 100644 --- a/code/__DEFINES/mobfactions.dm +++ b/code/__DEFINES/mobfactions.dm @@ -43,6 +43,8 @@ #define FACTION_MIMIC "mimic" /// Beasts found on the various mining environments #define FACTION_MINING "mining" +/// Watchers don't like any creatures other than each other +#define FACTION_WATCHER "watcher" /// Monkeys and gorillas #define FACTION_MONKEY "monkey" /// Mushrooms and mushroompeople diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index efbbf17e9ac..70b7e66671e 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -644,6 +644,10 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Cannot be grabbed by goliath tentacles #define TRAIT_TENTACLE_IMMUNE "tentacle_immune" +/// Currently under the effect of overwatch +#define TRAIT_OVERWATCHED "watcher_overwatched" +/// Cannot be targetted by watcher overwatch +#define TRAIT_OVERWATCH_IMMUNE "overwatch_immune" //non-mob traits /// Used for limb-based paralysis, where replacing the limb will fix it. @@ -1124,6 +1128,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define HALLUCINATION_TRAIT "hallucination_trait" /// Trait given by simple/basic mob death #define BASIC_MOB_DEATH_TRAIT "basic_mob_death" +/// Trait given by your current speed +#define SPEED_TRAIT "speed_trait" /// Trait given to mobs that have been autopsied #define AUTOPSY_TRAIT "autopsy_trait" diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm index 9515da7dca9..c566ccaf266 100644 --- a/code/_globalvars/traits.dm +++ b/code/_globalvars/traits.dm @@ -209,6 +209,8 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_EXPANDED_FOV" = TRAIT_EXPANDED_FOV, "TRAIT_GOOD_HEARING" = TRAIT_GOOD_HEARING, "TRAIT_XRAY_HEARING" = TRAIT_XRAY_HEARING, + "TRAIT_TENTACLE_IMMUNE" = TRAIT_TENTACLE_IMMUNE, + "TRAIT_OVERWATCH_IMMUNE" = TRAIT_OVERWATCH_IMMUNE, ), /obj/item/bodypart = list( "TRAIT_PARALYSIS" = TRAIT_PARALYSIS, diff --git a/code/datums/actions/mobs/small_sprite.dm b/code/datums/actions/mobs/small_sprite.dm index b53b46fe43a..335d6c9aff3 100644 --- a/code/datums/actions/mobs/small_sprite.dm +++ b/code/datums/actions/mobs/small_sprite.dm @@ -22,7 +22,7 @@ small_icon_state = "ash_whelp" /datum/action/small_sprite/megafauna/colossus - small_icon_state = "Basilisk" + small_icon_state = "basilisk" /datum/action/small_sprite/megafauna/bubblegum small_icon_state = "goliath2" diff --git a/code/datums/ai/_ai_behavior.dm b/code/datums/ai/_ai_behavior.dm index 9089bb5cf6b..98ab8f2fc7e 100644 --- a/code/datums/ai/_ai_behavior.dm +++ b/code/datums/ai/_ai_behavior.dm @@ -21,11 +21,12 @@ /datum/ai_behavior/proc/finish_action(datum/ai_controller/controller, succeeded, ...) LAZYREMOVE(controller.current_behaviors, src) controller.behavior_args -= type - if(behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT) //If this was a movement task, reset our movement target if necessary - if(!(behavior_flags & AI_BEHAVIOR_KEEP_MOVE_TARGET_ON_FINISH)) - clear_movement_target(controller) - if(!(behavior_flags & AI_BEHAVIOR_KEEP_MOVING_TOWARDS_TARGET_ON_FINISH)) - controller.ai_movement.stop_moving_towards(controller) + if(!(behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT)) //If this was a movement task, reset our movement target if necessary + return + if(behavior_flags & AI_BEHAVIOR_KEEP_MOVE_TARGET_ON_FINISH) + return + clear_movement_target(controller) + controller.ai_movement.stop_moving_towards(controller) /// Helper proc to ensure consistency in setting the source of the movement target /datum/ai_behavior/proc/set_movement_target(datum/ai_controller/controller, atom/target, datum/ai_movement/new_movement) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm b/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm new file mode 100644 index 00000000000..b80a28836a0 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm @@ -0,0 +1,82 @@ +/// Step away if too close, or towards if too far +/datum/ai_planning_subtree/maintain_distance + /// Blackboard key holding atom we want to stay away from + var/target_key = BB_BASIC_MOB_CURRENT_TARGET + /// How close will we allow our target to get? + var/minimum_distance = 3 + /// How far away will we allow our target to get? + var/maximum_distance = 6 + /// How far do we look for our target? + var/view_distance = 10 + +/datum/ai_planning_subtree/maintain_distance/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + var/atom/target = controller.blackboard[target_key] + if (!isliving(target) || !can_see(controller.pawn, target, view_distance)) + return // Don't run away from cucumbers, they're not snakes + var/range = get_dist(controller.pawn, target) + if (range < minimum_distance) + controller.queue_behavior(/datum/ai_behavior/step_away, target_key) + return + if (range > maximum_distance) + controller.queue_behavior(/datum/ai_behavior/pursue_to_range, target_key, maximum_distance) + return + +/// Take one step away +/datum/ai_behavior/step_away + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + required_distance = 0 + action_cooldown = 0.2 SECONDS + +/datum/ai_behavior/step_away/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/current_target = controller.blackboard[target_key] + if (QDELETED(current_target)) + return FALSE + + var/mob/living/our_pawn = controller.pawn + our_pawn.face_atom(current_target) + + var/turf/next_step = get_step_away(controller.pawn, current_target) + if (!isnull(next_step) && !next_step.is_blocked_turf(exclude_mobs = TRUE)) + set_movement_target(controller, target = next_step, new_movement = /datum/ai_movement/basic_avoidance/backstep) + return TRUE + + var/list/all_dirs = GLOB.alldirs.Copy() + all_dirs -= get_dir(controller.pawn, next_step) + all_dirs -= get_dir(controller.pawn, current_target) + shuffle_inplace(all_dirs) + + for (var/dir in all_dirs) + next_step = get_step(controller.pawn, dir) + if (!isnull(next_step) && !next_step.is_blocked_turf(exclude_mobs = TRUE)) + set_movement_target(controller, target = next_step, new_movement = /datum/ai_movement/basic_avoidance/backstep) + return TRUE + return FALSE + +/datum/ai_behavior/step_away/perform(seconds_per_tick, datum/ai_controller/controller) + . = ..() + finish_action(controller, succeeded = TRUE) + +/datum/ai_behavior/step_away/finish_action(datum/ai_controller/controller, succeeded) + . = ..() + controller.change_ai_movement_type(initial(controller.ai_movement)) + +/// Pursue a target until we are within a provided range +/datum/ai_behavior/pursue_to_range + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_MOVE_AND_PERFORM + +/datum/ai_behavior/pursue_to_range/setup(datum/ai_controller/controller, target_key, range) + . = ..() + var/atom/current_target = controller.blackboard[target_key] + if (QDELETED(current_target)) + return FALSE + if (get_dist(controller.pawn, current_target) <= range) + return FALSE + set_movement_target(controller, current_target) + +/datum/ai_behavior/pursue_to_range/perform(seconds_per_tick, datum/ai_controller/controller, target_key, range) + var/atom/current_target = controller.blackboard[target_key] + if (!QDELETED(current_target) && get_dist(controller.pawn, current_target) > range) + return + finish_action(controller, succeeded = TRUE) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm b/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm new file mode 100644 index 00000000000..07fb6f8b0dc --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm @@ -0,0 +1,53 @@ +/// Fire a ranged attack without interrupting movement. +/datum/ai_planning_subtree/ranged_skirmish + operational_datums = list(/datum/component/ranged_attacks) + /// Blackboard key holding target atom + var/target_key = BB_BASIC_MOB_CURRENT_TARGET + /// What AI behaviour do we actually run? + var/datum/ai_behavior/ranged_skirmish/attack_behavior = /datum/ai_behavior/ranged_skirmish + +/datum/ai_planning_subtree/ranged_skirmish/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return + controller.queue_behavior(attack_behavior, target_key, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + +/// How often will we try to perform our ranged attack? +/datum/ai_behavior/ranged_skirmish + action_cooldown = 1 SECONDS + /// If target is further away than this we don't fire + var/max_range = 9 + /// If target is closer than this we don't fire + var/min_range = 2 + +/datum/ai_behavior/ranged_skirmish/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) + . = ..() + var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key] + return !QDELETED(target) + +/datum/ai_behavior/ranged_skirmish/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if (QDELETED(target)) + finish_action(controller, succeeded = FALSE) + return + + var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key] + if(!targetting_datum.can_attack(controller.pawn, target)) + finish_action(controller, succeeded = FALSE) + return + + var/hiding_target = targetting_datum.find_hidden_mobs(controller.pawn, target) + controller.set_blackboard_key(hiding_location_key, hiding_target) + + target = hiding_target || target + + var/distance = get_dist(controller.pawn, target) + if (distance > max_range || distance < min_range) + finish_action(controller, succeeded = FALSE) + return + + var/mob/living/basic/gunman = controller.pawn + gunman.RangedAttack(target) + finish_action(controller, succeeded = TRUE) diff --git a/code/datums/ai/movement/ai_movement_basic_avoidance.dm b/code/datums/ai/movement/ai_movement_basic_avoidance.dm index 8a610a5aabc..371fb9dbc4b 100644 --- a/code/datums/ai/movement/ai_movement_basic_avoidance.dm +++ b/code/datums/ai/movement/ai_movement_basic_avoidance.dm @@ -1,13 +1,15 @@ ///Uses Byond's basic obstacle avoidance movement /datum/ai_movement/basic_avoidance max_pathing_attempts = 10 + /// Movement flags to pass to the loop + var/move_flags = NONE /datum/ai_movement/basic_avoidance/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance) . = ..() var/atom/movable/moving = controller.pawn var/min_dist = controller.blackboard[BB_CURRENT_MIN_MOVE_DISTANCE] var/delay = controller.movement_delay - var/datum/move_loop/loop = SSmove_manager.move_to(moving, current_movement_target, min_dist, delay, subsystem = SSai_movement, extra_info = controller) + var/datum/move_loop/loop = SSmove_manager.move_to(moving, current_movement_target, min_dist, delay, flags = move_flags, subsystem = SSai_movement, extra_info = controller) RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move)) RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move)) @@ -18,3 +20,7 @@ if(is_type_in_typecache(target_turf, GLOB.dangerous_turfs)) . = FALSE return . + +/// Move immediately and don't update our facing +/datum/ai_movement/basic_avoidance/backstep + move_flags = MOVEMENT_LOOP_START_FAST | MOVEMENT_LOOP_NO_DIR_UPDATE diff --git a/code/datums/components/ai_retaliate_advanced.dm b/code/datums/components/ai_retaliate_advanced.dm index 46557b0ef2f..1744394e8cb 100644 --- a/code/datums/components/ai_retaliate_advanced.dm +++ b/code/datums/components/ai_retaliate_advanced.dm @@ -1,6 +1,7 @@ /** * Attached to a mob with an AI controller, passes things which have damaged it to a blackboard. * The AI controller is responsible for doing anything with that information. + * Differs from the element as it passes new entries through a callback. */ /datum/component/ai_retaliate_advanced /// Callback to a mob for custom behaviour diff --git a/code/datums/components/basic_mob_attack_telegraph.dm b/code/datums/components/basic_mob_attack_telegraph.dm index 48359609bbb..96b417ea1d8 100644 --- a/code/datums/components/basic_mob_attack_telegraph.dm +++ b/code/datums/components/basic_mob_attack_telegraph.dm @@ -14,7 +14,7 @@ /datum/component/basic_mob_attack_telegraph/Initialize( telegraph_icon = 'icons/mob/telegraphing/telegraph.dmi', telegraph_state = ATTACK_EFFECT_BITE, - telegraph_duration = 0.3 SECONDS, + telegraph_duration = 0.4 SECONDS, datum/callback/on_began_forecast, ) . = ..() @@ -40,7 +40,7 @@ /// When we attempt to attack, check if it is allowed /datum/component/basic_mob_attack_telegraph/proc/on_attack(mob/living/basic/source, atom/target) SIGNAL_HANDLER - if (!isliving(target)) + if (!(isliving(target) || ismecha(target))) // Curse you CLARKE return if (HAS_TRAIT_FROM(source, TRAIT_BASIC_ATTACK_FORECAST, REF(src))) REMOVE_TRAIT(source, TRAIT_BASIC_ATTACK_FORECAST, REF(src)) @@ -51,7 +51,7 @@ return COMPONENT_HOSTILE_NO_ATTACK /// Perform an attack after a delay -/datum/component/basic_mob_attack_telegraph/proc/delayed_attack(mob/living/basic/source, mob/living/target) +/datum/component/basic_mob_attack_telegraph/proc/delayed_attack(mob/living/basic/source, atom/target) current_target = target target.add_overlay(target_overlay) RegisterSignal(target, COMSIG_QDELETING, PROC_REF(forget_target)) @@ -69,7 +69,7 @@ source.melee_attack(target) /// The guy we're trying to attack moved, is he still in range? -/datum/component/basic_mob_attack_telegraph/proc/target_moved(mob/living/target) +/datum/component/basic_mob_attack_telegraph/proc/target_moved(atom/target) SIGNAL_HANDLER if (in_range(parent, target)) return diff --git a/code/datums/components/basic_ranged_ready_overlay.dm b/code/datums/components/basic_ranged_ready_overlay.dm new file mode 100644 index 00000000000..434a64dd6ff --- /dev/null +++ b/code/datums/components/basic_ranged_ready_overlay.dm @@ -0,0 +1,56 @@ +/** + * Fade in an overlay x seconds after a basic mob makes a ranged attack + * Indicates that it will be ready to fire again + */ +/datum/component/basic_ranged_ready_overlay + /// Icon state for the overlay to display + var/overlay_state + /// Time after which to redisplay the overlay + var/display_after + /// Timer tracking when we can next fire + var/waiting_timer + +/datum/component/basic_ranged_ready_overlay/Initialize(overlay_state = "", display_after = 2.5 SECONDS) + . = ..() + if (!isbasicmob(parent)) + return COMPONENT_INCOMPATIBLE + if (!overlay_state) + CRASH("Attempted to assign basic ranged ready overlay with a null or empty overlay state") + src.overlay_state = overlay_state + src.display_after = display_after + restore_overlay(parent) + +/datum/component/basic_ranged_ready_overlay/RegisterWithParent() + . = ..() + RegisterSignal(parent, COMSIG_BASICMOB_POST_ATTACK_RANGED, PROC_REF(on_ranged_attack)) + RegisterSignal(parent, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_changed)) + +/datum/component/basic_ranged_ready_overlay/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_BASICMOB_POST_ATTACK_RANGED, COMSIG_LIVING_REVIVE)) + return ..() + +/datum/component/basic_ranged_ready_overlay/Destroy(force, silent) + deltimer(waiting_timer) + return ..() + +/// When we shoot, get rid of our overlay and queue its return +/datum/component/basic_ranged_ready_overlay/proc/on_ranged_attack(mob/living/basic/firer, atom/target, modifiers) + SIGNAL_HANDLER + firer.cut_overlay(overlay_state) + waiting_timer = addtimer(CALLBACK(src, PROC_REF(restore_overlay), firer), display_after, TIMER_UNIQUE | TIMER_STOPPABLE | TIMER_DELETE_ME) + +/// Don't show overlay on a dead man +/datum/component/basic_ranged_ready_overlay/proc/on_stat_changed(mob/living/basic/gunman) + SIGNAL_HANDLER + if (gunman.stat == DEAD) + gunman.cut_overlay(overlay_state) + return + if (timeleft(waiting_timer) <= 0) + restore_overlay(parent) + +/// Try putting our overlay back +/datum/component/basic_ranged_ready_overlay/proc/restore_overlay(mob/living/basic/gunman) + if (QDELETED(gunman) || gunman.stat == DEAD) + return + gunman.cut_overlay(overlay_state) + gunman.add_overlay(overlay_state) diff --git a/code/datums/components/magnet.dm b/code/datums/components/magnet.dm new file mode 100644 index 00000000000..5c78b8665ce --- /dev/null +++ b/code/datums/components/magnet.dm @@ -0,0 +1,70 @@ +/// Attracts items of a certain typepath +/datum/component/magnet + /// Range at which to pull items + var/pull_range + /// List of things we attract + var/list/attracted_typecache + /// What to do when we pull something + var/datum/callback/on_pulled + /// What to do when something reaches us + var/datum/callback/on_contact + /// Are we currently working? + var/active = TRUE + +/datum/component/magnet/Initialize( + pull_range = 5, + attracted_typecache = list(/obj/item/kitchen/spoon, /obj/item/kitchen/fork, /obj/item/knife), + on_pulled, + on_contact, +) + . = ..() + if (!length(attracted_typecache)) + CRASH("Attempted to instantiate a [src] on [parent] which does not do anything.") + if (!isatom(parent)) + return COMPONENT_INCOMPATIBLE + + src.pull_range = pull_range + src.attracted_typecache = typecacheof(attracted_typecache) + src.on_pulled = on_pulled + src.on_contact = on_contact + +/datum/component/magnet/RegisterWithParent() + . = ..() + START_PROCESSING(SSdcs, src) + if (!isliving(parent)) + return + RegisterSignal(parent, COMSIG_MOB_STATCHANGE, PROC_REF(toggle_on_stat_change)) + +/datum/component/magnet/UnregisterFromParent() + . = ..() + STOP_PROCESSING(SSdcs, src) + UnregisterSignal(parent, COMSIG_MOB_STATCHANGE) + +/datum/component/magnet/Destroy(force, silent) + STOP_PROCESSING(SSdcs, src) + on_pulled = null + on_contact = null + return ..() + +/// If a mob dies we stop attracting stuff +/datum/component/magnet/proc/toggle_on_stat_change(mob/living/source) + SIGNAL_HANDLER + if (source.stat == DEAD) + STOP_PROCESSING(SSdcs, src) + else + START_PROCESSING(SSdcs, src) + +/datum/component/magnet/process(seconds_per_tick) + for (var/atom/movable/thing in orange(pull_range, parent)) + if (!is_type_in_typecache(thing, attracted_typecache)) + continue + var/range = get_dist(thing, parent) + if (range == 0) + continue + if (range == 1 && !isnull(on_contact)) + on_contact.Invoke(thing) + continue + var/moved = thing.Move(get_step_towards(thing, parent)) + if (moved && !isnull(on_pulled)) + on_pulled.Invoke(thing) + CHECK_TICK diff --git a/code/datums/components/ranged_attacks.dm b/code/datums/components/ranged_attacks.dm index a61eff468ec..1a3bcacd5d0 100644 --- a/code/datums/components/ranged_attacks.dm +++ b/code/datums/components/ranged_attacks.dm @@ -49,30 +49,23 @@ return COOLDOWN_START(src, fire_cooldown, cooldown_time) INVOKE_ASYNC(src, PROC_REF(async_fire_ranged_attack), firer, target, modifiers) + SEND_SIGNAL(parent, COMSIG_BASICMOB_POST_ATTACK_RANGED, target, modifiers) /// Actually fire the damn thing /datum/component/ranged_attacks/proc/async_fire_ranged_attack(mob/living/basic/firer, atom/target, modifiers) + if(projectile_type) + firer.fire_projectile(projectile_type, target, projectile_sound) + return playsound(firer, projectile_sound, 100, TRUE) var/turf/startloc = get_turf(firer) + var/obj/item/ammo_casing/casing = new casing_type(startloc) + var/target_zone + if(ismob(target)) + var/mob/target_mob = target + target_zone = target_mob.get_random_valid_zone() + else + target_zone = ran_zone() + casing.fire_casing(target, firer, null, null, null, target_zone, 0, firer) + casing.AddElement(/datum/element/temporary_atom, 30 SECONDS) + return - if(casing_type) - var/obj/item/ammo_casing/casing = new casing_type(startloc) - var/target_zone - if(ismob(target)) - var/mob/target_mob = target - target_zone = target_mob.get_random_valid_zone() - else - target_zone = ran_zone() - casing.fire_casing(target, firer, null, null, null, target_zone, 0, firer) - casing.AddElement(/datum/element/temporary_atom, 30 SECONDS) - return - - var/obj/projectile/bullet = new projectile_type(startloc) - bullet.starting = startloc - bullet.firer = firer - bullet.fired_from = firer - bullet.yo = target.y - startloc.y - bullet.xo = target.x - startloc.x - bullet.original = target - bullet.preparePixelProjectile(target, firer) - bullet.fire() diff --git a/code/datums/components/style/style.dm b/code/datums/components/style/style.dm index 0f7999e7fd4..3894f77311e 100644 --- a/code/datums/components/style/style.dm +++ b/code/datums/components/style/style.dm @@ -117,7 +117,7 @@ /datum/component/projectile_parry,\ list(\ /obj/projectile/colossus,\ - /obj/projectile/temp/basilisk,\ + /obj/projectile/temp/watcher,\ /obj/projectile/kinetic,\ /obj/projectile/bileworm_acid,\ /obj/projectile/herald,\ diff --git a/code/datums/mapgen/CaveGenerator.dm b/code/datums/mapgen/CaveGenerator.dm index a1457ac02d8..6734fc06eb3 100644 --- a/code/datums/mapgen/CaveGenerator.dm +++ b/code/datums/mapgen/CaveGenerator.dm @@ -52,8 +52,8 @@ . = ..() if(!weighted_mob_spawn_list) weighted_mob_spawn_list = list( + /mob/living/basic/mining/basilisk = 4, /mob/living/basic/mining/goliath/ancient = 5, - /mob/living/simple_animal/hostile/asteroid/basilisk = 4, /mob/living/simple_animal/hostile/asteroid/hivelord = 3, /mob/living/simple_animal/hostile/asteroid/goldgrub = 1, ) diff --git a/code/datums/mapgen/Cavegens/LavalandGenerator.dm b/code/datums/mapgen/Cavegens/LavalandGenerator.dm index ea440d2cf21..78411008f69 100644 --- a/code/datums/mapgen/Cavegens/LavalandGenerator.dm +++ b/code/datums/mapgen/Cavegens/LavalandGenerator.dm @@ -5,12 +5,12 @@ weighted_mob_spawn_list = list( SPAWN_MEGAFAUNA = 2, /mob/living/basic/mining/bileworm = 20, - /mob/living/basic/mining/goliath/random = 50, + /obj/effect/spawner/random/goliath = 50, /mob/living/basic/mining/lobstrosity/lava = 20, + /obj/effect/spawner/random/watcher = 40, /mob/living/simple_animal/hostile/asteroid/brimdemon = 20, /mob/living/simple_animal/hostile/asteroid/goldgrub = 10, /mob/living/simple_animal/hostile/asteroid/hivelord/legion/random = 30, - /mob/living/simple_animal/hostile/asteroid/basilisk/watcher/random = 40, /obj/structure/spawner/lavaland = 2, /obj/structure/spawner/lavaland/goliath = 3, /obj/structure/spawner/lavaland/legion = 3, diff --git a/code/datums/memory/_memory.dm b/code/datums/memory/_memory.dm index a6379bc82c4..35d1a6683f3 100644 --- a/code/datums/memory/_memory.dm +++ b/code/datums/memory/_memory.dm @@ -259,6 +259,7 @@ /mob/living/basic/killer_tomato, /mob/living/basic/lizard, /mob/living/basic/mining/goliath, + /mob/living/basic/mining/watcher, /mob/living/basic/morph, /mob/living/basic/mouse, /mob/living/basic/mushroom, @@ -271,7 +272,6 @@ /mob/living/basic/statue, /mob/living/basic/stickman, /mob/living/basic/stickman/dog, - /mob/living/simple_animal/hostile/asteroid/basilisk/watcher, /mob/living/simple_animal/hostile/blob/blobbernaut/independent, /mob/living/simple_animal/hostile/gorilla, /mob/living/simple_animal/hostile/megafauna/dragon/lesser, diff --git a/code/datums/status_effects/gas.dm b/code/datums/status_effects/gas.dm index 3daec2dd4b9..bcc0a50e707 100644 --- a/code/datums/status_effects/gas.dm +++ b/code/datums/status_effects/gas.dm @@ -52,6 +52,9 @@ duration = 8 can_melt = FALSE +/datum/status_effect/freon/watcher/extended + duration = 5 SECONDS + /datum/status_effect/freon/lasting id = "lasting_frozen" duration = -1 diff --git a/code/game/objects/structures/lavaland/necropolis_tendril.dm b/code/game/objects/structures/lavaland/necropolis_tendril.dm index b8463921b0a..542df563a1f 100644 --- a/code/game/objects/structures/lavaland/necropolis_tendril.dm +++ b/code/game/objects/structures/lavaland/necropolis_tendril.dm @@ -9,7 +9,7 @@ faction = list(FACTION_MINING) max_mobs = 3 max_integrity = 250 - mob_types = list(/mob/living/simple_animal/hostile/asteroid/basilisk/watcher/tendril) + mob_types = list(/mob/living/basic/mining/watcher) move_resist=INFINITY // just killing it tears a massive hole in the ground, let's not move it anchored = TRUE @@ -26,7 +26,7 @@ mob_types = list(/mob/living/simple_animal/hostile/asteroid/hivelord/legion/tendril) /obj/structure/spawner/lavaland/icewatcher - mob_types = list(/mob/living/simple_animal/hostile/asteroid/basilisk/watcher/icewing) + mob_types = list(/mob/living/basic/mining/watcher/icewing) GLOBAL_LIST_INIT(tendrils, list()) /obj/structure/spawner/lavaland/Initialize(mapload) diff --git a/code/game/objects/structures/spawner.dm b/code/game/objects/structures/spawner.dm index 90a3366260f..31b6f76bbc2 100644 --- a/code/game/objects/structures/spawner.dm +++ b/code/game/objects/structures/spawner.dm @@ -77,9 +77,9 @@ icon = 'icons/mob/simple/lavaland/nest.dmi' spawn_text = "crawls out of" mob_types = list( + /mob/living/basic/mining/basilisk, /mob/living/basic/mining/goliath/ancient, /mob/living/basic/wumborian_fugu, - /mob/living/simple_animal/hostile/asteroid/basilisk, /mob/living/simple_animal/hostile/asteroid/goldgrub, /mob/living/simple_animal/hostile/asteroid/hivelord, ) @@ -103,7 +103,7 @@ /obj/structure/spawner/mining/basilisk name = "basilisk den" desc = "A den housing a nest of basilisks, bring a coat." - mob_types = list(/mob/living/simple_animal/hostile/asteroid/basilisk) + mob_types = list(/mob/living/basic/mining/basilisk) /obj/structure/spawner/mining/wumborian name = "wumborian fugu den" diff --git a/code/modules/mob/living/basic/basic.dm b/code/modules/mob/living/basic/basic.dm index 2f1c89fdb1d..27ad903367b 100644 --- a/code/modules/mob/living/basic/basic.dm +++ b/code/modules/mob/living/basic/basic.dm @@ -205,6 +205,13 @@ add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/simplemob_varspeed, multiplicative_slowdown = speed) SEND_SIGNAL(src, POST_BASIC_MOB_UPDATE_VARSPEED) +/mob/living/basic/update_movespeed() + . = ..() + if (cached_multiplicative_slowdown > END_GLIDE_SPEED) + ADD_TRAIT(src, TRAIT_NO_GLIDE, SPEED_TRAIT) + else + REMOVE_TRAIT(src, TRAIT_NO_GLIDE, SPEED_TRAIT) + /mob/living/basic/relaymove(mob/living/user, direction) if(user.incapacitated()) return diff --git a/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm b/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm new file mode 100644 index 00000000000..c662870393c --- /dev/null +++ b/code/modules/mob/living/basic/lavaland/basilisk/basilisk.dm @@ -0,0 +1,87 @@ +/// Watchers' ground-dwelling cousins, they shoot at you until they get into melee and absorb laser fire to power up. +/mob/living/basic/mining/basilisk + name = "basilisk" + desc = "A territorial beast, covered in a diamond shell which absorbs heat. Its stare causes victims to freeze from the inside." + icon_state = "basilisk" + icon_living = "basilisk" + icon_dead = "basilisk_dead" + speak_emote = list("chimes") + damage_coeff = list(BRUTE = 1, BURN = 0.1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1) + speed = 20 + maxHealth = 200 + health = 200 + obj_damage = 60 + melee_damage_lower = 12 + melee_damage_upper = 12 + attack_verb_continuous = "bites into" + attack_verb_simple = "bite into" + throw_blocked_message = "bounces off the shell of" + attack_sound = 'sound/weapons/bladeslice.ogg' + attack_vis_effect = ATTACK_EFFECT_BITE + ai_controller = /datum/ai_controller/basic_controller/basilisk + butcher_results = list( + /obj/item/stack/sheet/bone = 1, + /obj/item/stack/ore/diamond = 2, + /obj/item/stack/sheet/sinew = 2, + ) + /// The component we use for making ranged attacks + var/datum/component/ranged_attacks/ranged_attacks + +/mob/living/basic/mining/basilisk/Initialize(mapload) + . = ..() + AddComponent(/datum/component/basic_mob_attack_telegraph) + ranged_attacks = AddComponent(/datum/component/ranged_attacks, projectile_type = /obj/projectile/temp/watcher, projectile_sound = 'sound/weapons/pierce.ogg') + RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(check_lava)) + +/mob/living/basic/mining/basilisk/Destroy() + QDEL_NULL(ranged_attacks) + return ..() + +/mob/living/basic/mining/basilisk/welder_act(mob/living/user, obj/item/tool) + . = ..() + heat_up() // Who would do this? + +/mob/living/basic/mining/basilisk/bullet_act(obj/projectile/bullet, def_zone, piercing_hit) + . = ..() + if (istype(bullet, /obj/projectile/temp)) + var/obj/projectile/temp/heat_bullet = bullet + if (heat_bullet.temperature < 0) + return + heat_up() + return + + if (bullet.damage == 0 || bullet.damage_type != BURN) + return + heat_up() + +/// Are we standing in lava? +/mob/living/basic/mining/basilisk/proc/check_lava() + SIGNAL_HANDLER + var/turf/open/lava/entered_lava = loc + if (!islava(entered_lava) || entered_lava.immunity_trait != TRAIT_LAVA_IMMUNE) + return + heat_up() + +/// We got hit by something hot, go into heat mode +/mob/living/basic/mining/basilisk/proc/heat_up() + if (stat != CONSCIOUS || has_status_effect(/datum/status_effect/basilisk_overheat)) + return + apply_status_effect(/datum/status_effect/basilisk_overheat) + +/// Change what kind of beam we fire +/mob/living/basic/mining/basilisk/proc/set_projectile_type(projectile_type) + ranged_attacks.projectile_type = projectile_type + +/datum/ai_controller/basic_controller/basilisk + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic, + ) + + ai_movement = /datum/ai_movement/basic_avoidance + idle_behavior = /datum/idle_behavior/idle_random_walk + planning_subtrees = list( + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/ranged_skirmish, + /datum/ai_planning_subtree/attack_obstacle_in_path, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + ) diff --git a/code/modules/mob/living/basic/lavaland/basilisk/basilisk_overheat.dm b/code/modules/mob/living/basic/lavaland/basilisk/basilisk_overheat.dm new file mode 100644 index 00000000000..c0b49fbdc61 --- /dev/null +++ b/code/modules/mob/living/basic/lavaland/basilisk/basilisk_overheat.dm @@ -0,0 +1,76 @@ +/// Status effect gained by basilisks when they touch something hot +/datum/status_effect/basilisk_overheat + id = "basilisk_overheat" + duration = 3 MINUTES + /// Things which will chill us out if we get hit by them + var/static/list/chilling_reagents = list( + /datum/reagent/medicine/cryoxadone, + /datum/reagent/firefighting_foam, + /datum/reagent/consumable/frostoil, + /datum/reagent/consumable/ice, + /datum/reagent/water, + ) + +/datum/status_effect/basilisk_overheat/on_apply() + . = ..() + if (!. || !istype(owner, /mob/living/basic/mining/basilisk) || owner.stat != CONSCIOUS) + return FALSE + var/mob/living/basic/mining/basilisk/hot_stuff = owner + hot_stuff.visible_message(span_warning("[hot_stuff] is getting fired up!")) + hot_stuff.fully_heal() + hot_stuff.icon_living = "basilisk_alert" + hot_stuff.icon_state = "basilisk_alert" + hot_stuff.update_appearance(UPDATE_ICON_STATE) + hot_stuff.add_movespeed_modifier(/datum/movespeed_modifier/basilisk_overheat) + hot_stuff.set_projectile_type(/obj/projectile/basilisk_hot) + + RegisterSignal(hot_stuff, COMSIG_LIVING_DEATH, PROC_REF(on_death)) + RegisterSignal(hot_stuff, COMSIG_ATOM_EXPOSE_REAGENTS, PROC_REF(on_splashed)) + RegisterSignal(hot_stuff, COMSIG_ATOM_BULLET_ACT, PROC_REF(on_shot)) + +/datum/status_effect/basilisk_overheat/on_remove() + . = ..() + var/mob/living/basic/mining/basilisk/hot_stuff = owner + hot_stuff.icon_living = "basilisk" + hot_stuff.icon_state = "basilisk" + hot_stuff.set_projectile_type(/obj/projectile/temp/watcher) + + hot_stuff.update_appearance(UPDATE_ICON_STATE) + hot_stuff.remove_movespeed_modifier(/datum/movespeed_modifier/basilisk_overheat) + UnregisterSignal(hot_stuff, list(COMSIG_LIVING_DEATH, COMSIG_ATOM_EXPOSE_REAGENTS, COMSIG_ATOM_BULLET_ACT)) + + if (hot_stuff.stat != CONSCIOUS) + return + hot_stuff.visible_message(span_notice("[hot_stuff] seems to have cooled down.")) + var/obj/effect/particle_effect/fluid/smoke/poof = new(get_turf(hot_stuff)) + poof.lifetime = 2 SECONDS + +/// Cool down if we die +/datum/status_effect/basilisk_overheat/proc/on_death() + SIGNAL_HANDLER + qdel(src) + +/// Cool down if splashed with water +/datum/status_effect/basilisk_overheat/proc/on_splashed(atom/source, list/reagents, datum/reagents/source_reagents, methods, volume_modifier, show_message) + SIGNAL_HANDLER + if(!(methods & (TOUCH|VAPOR))) + return + for (var/datum/reagent in reagents) + if (!is_type_in_list(reagent, chilling_reagents)) + continue + qdel(src) + return + +/// Cool down if shot with a cryo beam +/datum/status_effect/basilisk_overheat/proc/on_shot(datum/source, obj/projectile/temp/cryo_shot) + SIGNAL_HANDLER + if (!istype(cryo_shot) || cryo_shot.temperature > 0) + return + qdel(src) + +/// Projectile basilisks use when hot +/obj/projectile/basilisk_hot + name = "energy blast" + icon_state = "chronobolt" + damage = 40 + damage_type = BRUTE diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm index db7b4785413..016fc3a5a87 100644 --- a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm +++ b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm @@ -11,7 +11,6 @@ gender = MALE // Female ones are the bipedal elites speed = 30 basic_mob_flags = IMMUNE_TO_FISTS - mob_biotypes = MOB_ORGANIC|MOB_BEAST maxHealth = 300 health = 300 friendly_verb_continuous = "wails at" @@ -50,7 +49,6 @@ /mob/living/basic/mining/goliath/Initialize(mapload) . = ..() - ADD_TRAIT(src, TRAIT_NO_GLIDE, INNATE_TRAIT) ADD_TRAIT(src, TRAIT_TENTACLE_IMMUNE, INNATE_TRAIT) AddElement(/datum/element/ai_retaliate) AddElement(/datum/element/footstep, FOOTSTEP_MOB_HEAVY) @@ -168,16 +166,6 @@ . = ..() faction = new_friend.faction.Copy() -/// Goliath which sometimes replaces itself with a rare variant -/mob/living/basic/mining/goliath/random - -/mob/living/basic/mining/goliath/random/Initialize(mapload) - . = ..() - if(!prob(1)) - return - new /mob/living/basic/mining/goliath/ancient/immortal(loc) - return INITIALIZE_HINT_QDEL - /// Legacy Goliath mob with different sprites, largely the same behaviour /mob/living/basic/mining/goliath/ancient name = "ancient goliath" @@ -234,3 +222,15 @@ desc = "This rough saddle will give you a serviceable seat upon a goliath! Provided you can get one to stand still." icon = 'icons/obj/mining.dmi' icon_state = "goliath_saddle" + +/// For map generation, has a chance to instantiate as a special subtype +/obj/effect/spawner/random/goliath + name = "random goliath" + desc = "Chance to spawn a rare shiny version." + icon = 'icons/mob/simple/lavaland/lavaland_monsters_wide.dmi' + icon_state = "goliath" + pixel_x = -12 + loot = list( + /mob/living/basic/mining/goliath = 99, + /mob/living/basic/mining/goliath/ancient/immortal = 1, + ) diff --git a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm index b0dfa60e49c..bc60ec88aa7 100644 --- a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm +++ b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm @@ -6,7 +6,6 @@ icon_state = "arctic_lobstrosity" icon_living = "arctic_lobstrosity" icon_dead = "arctic_lobstrosity_dead" - mob_biotypes = MOB_ORGANIC|MOB_BEAST friendly_verb_continuous = "chitters at" friendly_verb_simple = "chitters at" speak_emote = list("chitters") diff --git a/code/modules/mob/living/basic/lavaland/mining.dm b/code/modules/mob/living/basic/lavaland/mining.dm index cc4ca16b15b..6b1d0de5739 100644 --- a/code/modules/mob/living/basic/lavaland/mining.dm +++ b/code/modules/mob/living/basic/lavaland/mining.dm @@ -1,7 +1,9 @@ ///prototype for mining mobs /mob/living/basic/mining + icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi' combat_mode = TRUE mob_size = MOB_SIZE_LARGE + mob_biotypes = MOB_ORGANIC|MOB_BEAST faction = list(FACTION_MINING) unsuitable_atmos_damage = 0 minimum_survivable_temperature = 0 diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher.dm new file mode 100644 index 00000000000..fc7c527459f --- /dev/null +++ b/code/modules/mob/living/basic/lavaland/watcher/watcher.dm @@ -0,0 +1,120 @@ +/// A floating eyeball which keeps its distance and plays red light/green light with you. +/mob/living/basic/mining/watcher + name = "watcher" + desc = "A levitating, monocular creature held aloft by wing-like veins. A sharp spine of crystal protrudes from its body." + icon = 'icons/mob/simple/lavaland/lavaland_monsters_wide.dmi' + icon_state = "watcher" + icon_living = "watcher" + icon_dead = "watcher_dead" + health_doll_icon = "watcher" + pixel_x = -12 + base_pixel_x = -12 + speak_emote = list("chimes") + speed = 3 + maxHealth = 200 + health = 200 + attack_verb_continuous = "buffets" + attack_verb_simple = "buffet" + crusher_loot = /obj/item/crusher_trophy/watcher_wing + ai_controller = /datum/ai_controller/basic_controller/watcher + butcher_results = list( + /obj/item/stack/sheet/bone = 1, + /obj/item/stack/ore/diamond = 2, + /obj/item/stack/sheet/sinew = 2, + ) + /// How often can we shoot? + var/ranged_cooldown = 3 SECONDS + /// What kind of beams we got? + var/projectile_type = /obj/projectile/temp/watcher + /// Icon state for our eye overlay + var/eye_glow = "ice_glow" + /// Sound to play when we shoot + var/shoot_sound = 'sound/weapons/pierce.ogg' + /// Typepath of our gaze ability + var/gaze_attack = /datum/action/cooldown/mob_cooldown/watcher_gaze + // We attract and eat these things for some reason + var/list/wanted_objects = list( + /obj/item/stack/sheet/mineral/diamond, + /obj/item/stack/ore/diamond, + /obj/item/pen/survival, + ) + +/mob/living/basic/mining/watcher/Initialize(mapload) + . = ..() + AddElement(/datum/element/ai_retaliate) + AddElement(/datum/element/simple_flying) + AddElement(/datum/element/content_barfer) + AddComponent(/datum/component/basic_ranged_ready_overlay, overlay_state = eye_glow) + AddComponent(\ + /datum/component/ranged_attacks,\ + cooldown_time = ranged_cooldown,\ + projectile_type = projectile_type,\ + projectile_sound = shoot_sound,\ + ) + AddComponent(\ + /datum/component/magnet,\ + attracted_typecache = wanted_objects,\ + on_contact = CALLBACK(src, PROC_REF(consume)),\ + ) + update_appearance(UPDATE_OVERLAYS) + + var/datum/action/cooldown/mob_cooldown/watcher_overwatch/overwatch = new(src) + overwatch.Grant(src) + overwatch.projectile_type = projectile_type + ai_controller.set_blackboard_key(BB_WATCHER_OVERWATCH, overwatch) + + var/datum/action/cooldown/mob_cooldown/watcher_gaze/gaze = new gaze_attack(src) + gaze.Grant(src) + ai_controller.set_blackboard_key(BB_WATCHER_GAZE, gaze) + +/mob/living/basic/mining/watcher/update_overlays() + . = ..() + . += emissive_appearance(icon, "watcher_emissive", src) + +/// I love eating diamonds yum +/mob/living/basic/mining/watcher/proc/consume(atom/movable/thing) + visible_message(span_warning("[thing] seems to vanish into [src]'s body!")) + thing.forceMove(src) + +/// More durable, burning projectiles +/mob/living/basic/mining/watcher/magmawing + name = "magmawing watcher" + desc = "Presented with extreme temperatures, adaptive watchers absorb heat through their circulatory wings and repurpose it as a weapon." + icon_state = "watcher_magmawing" + icon_living = "watcher_magmawing" + icon_dead = "watcher_magmawing_dead" + eye_glow = "fire_glow" + maxHealth = 215 //Compensate for the lack of slowdown on projectiles with a bit of extra health + health = 215 + projectile_type = /obj/projectile/temp/watcher/magma_wing + gaze_attack = /datum/action/cooldown/mob_cooldown/watcher_gaze/fire + crusher_loot = /obj/item/crusher_trophy/blaster_tubes/magma_wing + crusher_drop_chance = 100 // There's only going to be one of these per round throw them a bone + +/// Less durable, freezing projectiles +/mob/living/basic/mining/watcher/icewing + name = "icewing watcher" + desc = "Watchers which fail to absorb enough heat during their development become fragile, but share their internal chill with their enemies." + icon_state = "watcher_icewing" + icon_living = "watcher_icewing" + icon_dead = "watcher_icewing_dead" + maxHealth = 170 + health = 170 + projectile_type = /obj/projectile/temp/watcher/ice_wing + gaze_attack = /datum/action/cooldown/mob_cooldown/watcher_gaze/ice + butcher_results = list(/obj/item/stack/ore/diamond = 5, /obj/item/stack/sheet/bone = 1) + crusher_loot = /obj/item/crusher_trophy/watcher_wing/ice_wing + crusher_drop_chance = 100 + +/// For map generation, has a chance to instantiate as a special subtype +/obj/effect/spawner/random/watcher + name = "random watcher" + desc = "Chance to spawn a rare shiny version." + icon = 'icons/mob/simple/lavaland/lavaland_monsters_wide.dmi' + icon_state = "watcher" + pixel_x = -12 + loot = list( + /mob/living/basic/mining/watcher = 80, + /mob/living/basic/mining/watcher/magmawing = 15, + /mob/living/basic/mining/watcher/icewing = 5, + ) diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm new file mode 100644 index 00000000000..119f4e6f233 --- /dev/null +++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm @@ -0,0 +1,49 @@ +/datum/ai_controller/basic_controller/watcher + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic, + ) + + ai_movement = /datum/ai_movement/basic_avoidance + idle_behavior = /datum/idle_behavior/idle_random_walk + planning_subtrees = list( + /datum/ai_planning_subtree/target_retaliate, + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/use_mob_ability/gaze, + /datum/ai_planning_subtree/targeted_mob_ability/overwatch, + /datum/ai_planning_subtree/ranged_skirmish/watcher, + /datum/ai_planning_subtree/maintain_distance, + ) + +/datum/ai_planning_subtree/targeted_mob_ability/overwatch + ability_key = BB_WATCHER_OVERWATCH + +/datum/ai_planning_subtree/targeted_mob_ability/overwatch/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/atom/target = controller.blackboard[target_key] + if (QDELETED(target) || HAS_TRAIT(target, TRAIT_OVERWATCH_IMMUNE)) + return // We should probably let miners move sometimes + return ..() + +/datum/ai_planning_subtree/use_mob_ability/gaze + ability_key = BB_WATCHER_GAZE + finish_planning = TRUE + +/datum/ai_planning_subtree/use_mob_ability/gaze/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/watcher = controller.pawn + if (watcher.health > watcher.maxHealth * 0.66) // When we're a little hurt + return + var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if (!isliving(target)) + return // Don't do this if there's nothing hostile around or if our target is a mech + return ..() + +/datum/ai_planning_subtree/ranged_skirmish/watcher + attack_behavior = /datum/ai_behavior/ranged_skirmish/watcher + +/datum/ai_planning_subtree/ranged_skirmish/watcher/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if (QDELETED(target) || HAS_TRAIT(target, TRAIT_OVERWATCHED)) + return // Don't bully people who are playing red light green light + return ..() + +/datum/ai_behavior/ranged_skirmish/watcher + min_range = 0 diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm new file mode 100644 index 00000000000..85d2e384ae3 --- /dev/null +++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm @@ -0,0 +1,119 @@ +/** + * Do something nasty to everyone nearby if they're looking at us. + */ +/datum/action/cooldown/mob_cooldown/watcher_gaze + name = "Disorienting Gaze" + desc = "After a delay, flash everyone looking at you." + button_icon = 'icons/mob/actions/actions_animal.dmi' + button_icon_state = "gaze" + background_icon_state = "bg_demon" + overlay_icon_state = "bg_demon_border" + cooldown_time = 30 SECONDS + check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED + click_to_activate = FALSE + shared_cooldown = null + /// At what range do we check for vision? + var/effect_radius = 7 + /// How long does it take to play our various animation stages + var/animation_time = 0.8 SECONDS + /// How long after pressing the button do we give people to turn around? + var/wait_delay = 1.6 SECONDS + /// What are we currently displaying? + var/image/current_overlay + /// Timer until we go to the next stage + var/stage_timer + +/datum/action/cooldown/mob_cooldown/watcher_gaze/Activate(mob/living/target) + show_indicator_overlay("eye_open") + stage_timer = addtimer(CALLBACK(src, PROC_REF(show_indicator_overlay), "eye_pulse"), animation_time, TIMER_STOPPABLE) + StartCooldown(360 SECONDS, 360 SECONDS) + if (do_after(owner, delay = wait_delay, target = owner)) + trigger_effect() + else + deltimer(stage_timer) + clear_current_overlay() + StartCooldown() + return TRUE + +/datum/action/cooldown/mob_cooldown/watcher_gaze/Destroy() + deltimer(stage_timer) + clear_current_overlay() + return ..() + +/datum/action/cooldown/mob_cooldown/watcher_gaze/Remove(mob/removed_from) + deltimer(stage_timer) + clear_current_overlay() + return ..() + +/// Do some effects to whoever is looking at us +/datum/action/cooldown/mob_cooldown/watcher_gaze/proc/trigger_effect() + deltimer(stage_timer) + show_indicator_overlay("eye_flash") + for (var/mob/living/viewer in viewers(effect_radius, owner)) + var/view_dir = get_dir(viewer, owner) + if (!(viewer.dir & view_dir) || viewer.stat != CONSCIOUS) + continue + if (!apply_effect(viewer)) + continue + var/image/flashed_overlay = image( + icon = 'icons/effects/eldritch.dmi', + loc = viewer, + icon_state = "eye_flash", + pixel_x = -viewer.pixel_x, + pixel_y = -viewer.pixel_y, + ) + flick_overlay_global(flashed_overlay, show_to = GLOB.clients, duration = animation_time) + stage_timer = addtimer(CALLBACK(src, PROC_REF(hide_eye)), animation_time, TIMER_STOPPABLE) + +/// Do something bad to someone who was looking at us +/datum/action/cooldown/mob_cooldown/watcher_gaze/proc/apply_effect(mob/living/viewer) + if (!viewer.flash_act(intensity = 4, affect_silicon = TRUE, visual = TRUE, length = 3 SECONDS)) + return FALSE + viewer.set_confusion_if_lower(12 SECONDS) + return TRUE + +/// Animate our effect out +/datum/action/cooldown/mob_cooldown/watcher_gaze/proc/hide_eye() + show_indicator_overlay("eye_close") + stage_timer = addtimer(CALLBACK(src, PROC_REF(clear_current_overlay)), animation_time, TIMER_STOPPABLE) + +/// Display an animated overlay over our head to indicate what's going on +/datum/action/cooldown/mob_cooldown/watcher_gaze/proc/show_indicator_overlay(overlay_state) + clear_current_overlay() + current_overlay = image(icon = 'icons/effects/eldritch.dmi', loc = owner, icon_state = overlay_state, pixel_x = -owner.pixel_x, pixel_y = 28) + for(var/client/add_to in GLOB.clients) + add_to.images += current_overlay + +/// Hide whatever overlay we are showing +/datum/action/cooldown/mob_cooldown/watcher_gaze/proc/clear_current_overlay() + if (!isnull(current_overlay)) + remove_image_from_clients(current_overlay, GLOB.clients) + current_overlay = null + +/// Magmawing glare burns you +/datum/action/cooldown/mob_cooldown/watcher_gaze/fire + name = "Searing Glare" + desc = "After a delay, burn and stun everyone looking at you." + +/datum/action/cooldown/mob_cooldown/watcher_gaze/fire/apply_effect(mob/living/viewer) + viewer.Paralyze(3 SECONDS) + viewer.adjust_fire_stacks(10) + viewer.ignite_mob() + return TRUE + +/// Icewing glare freezes you +/datum/action/cooldown/mob_cooldown/watcher_gaze/ice + name = "Cold Stare" + desc = "After a delay, freeze and repulse everyone looking at you." + /// Max distance to throw people looking at us + var/max_throw = 3 + +/datum/action/cooldown/mob_cooldown/watcher_gaze/ice/apply_effect(mob/living/viewer) + viewer.apply_status_effect(/datum/status_effect/freon/watcher/extended) + viewer.safe_throw_at( + target = get_edge_target_turf(owner, get_dir(owner, get_step_away(viewer, owner))), + range = max_throw, + speed = 1, + thrower = owner, + force = MOVE_FORCE_EXTREMELY_STRONG, + ) diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_overwatch.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_overwatch.dm new file mode 100644 index 00000000000..59fe0e0903b --- /dev/null +++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_overwatch.dm @@ -0,0 +1,161 @@ +/** + * Automatically shoot at a target if they do anything while this is active on them. + */ +/datum/action/cooldown/mob_cooldown/watcher_overwatch + name = "Overwatch" + desc = "Keep a close eye on the target's actions, automatically firing upon them if they act." + button_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "eye" + background_icon_state = "bg_demon" + overlay_icon_state = "bg_demon_border" + cooldown_time = 20 SECONDS + check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED + click_to_activate = TRUE + shared_cooldown = null + /// Furthest range we can activate ability at + var/max_range = 7 + /// Type of projectile to fire + var/projectile_type = /obj/projectile/temp/watcher + /// Sound the projectile we fire makes + var/projectile_sound = 'sound/weapons/pierce.ogg' + /// Time to watch for + var/overwatch_duration = 3 SECONDS + +/datum/action/cooldown/mob_cooldown/watcher_overwatch/New(Target, original) + . = ..() + melee_cooldown_time = overwatch_duration + +/datum/action/cooldown/mob_cooldown/watcher_overwatch/PreActivate(atom/target) + if (target == owner) + return + if (ismecha(target)) + var/obj/vehicle/sealed/mecha/mech = target + var/list/drivers = mech.return_drivers() + if (!length(drivers)) + return + target = drivers[1] + if (!isliving(target)) + return + if (get_dist(owner, target) > max_range) + return + return ..() + +/datum/action/cooldown/mob_cooldown/watcher_overwatch/Activate(mob/living/target) + var/mob/living/living_owner = owner + living_owner.face_atom(target) + living_owner.Stun(overwatch_duration, ignore_canstun = TRUE) + target.apply_status_effect(/datum/status_effect/overwatch, overwatch_duration, owner, projectile_type, projectile_sound) + StartCooldown() + return TRUE + +/// Status effect which tracks whether our overwatched mob moves or acts +/datum/status_effect/overwatch + id = "watcher_overwatch" + duration = 5 SECONDS + status_type = STATUS_EFFECT_MULTIPLE + alert_type = /atom/movable/screen/alert/status_effect/overwatch + /// Distance at which we break off the ability + var/watch_range = 9 + /// Visual effect to make the status obvious + var/datum/beam/link + /// Which watcher is watching? + var/mob/living/watcher + /// Type of projectile to fire + var/projectile_type + /// Noise to make when we shoot beam + var/projectile_sound + /// Did the overwatch ever trigger during our run? + var/overwatch_triggered = FALSE + /// Signals which trigger a hostile response + var/static/list/forbidden_actions = list( + COMSIG_MOB_ABILITY_FINISHED, + COMSIG_MOB_ATTACK_HAND, + COMSIG_MOB_DROVE_MECH, + COMSIG_MOB_FIRED_GUN, + COMSIG_MOB_ITEM_ATTACK, + COMSIG_MOB_THROW, + COMSIG_MOB_USED_MECH_EQUIPMENT, + COMSIG_MOB_USED_MECH_MELEE, + COMSIG_MOVABLE_MOVED, + ) + +/datum/status_effect/overwatch/on_creation(mob/living/new_owner, set_duration, mob/living/watcher, projectile_type, projectile_sound) + if (isnull(watcher) || isnull(projectile_type)) + return FALSE + if (HAS_TRAIT(new_owner, TRAIT_OVERWATCH_IMMUNE)) + return FALSE + src.watcher = watcher + src.projectile_type = projectile_type + src.projectile_sound = projectile_sound + if (!isnull(set_duration)) + duration = set_duration + return ..() + +/datum/status_effect/overwatch/on_apply() + . = ..() + if (!.) + return FALSE + ADD_TRAIT(owner, TRAIT_OVERWATCHED, TRAIT_STATUS_EFFECT(id)) + owner.do_alert_animation() + owner.Immobilize(0.25 SECONDS) // Just long enough that they don't trigger it by mistake + owner.playsound_local(owner, 'sound/machines/chime.ogg', 50, TRUE) + var/atom/beam_origin = ismecha(owner.loc) ? owner.loc : owner + link = beam_origin.Beam(watcher, icon_state = "r_beam", override_target_pixel_x = 0) + RegisterSignals(owner, forbidden_actions, PROC_REF(opportunity_attack)) + RegisterSignals(owner, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(on_participant_died)) + RegisterSignals(watcher, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(on_participant_died)) + +/datum/status_effect/overwatch/on_remove() + UnregisterSignal(owner, forbidden_actions + list(COMSIG_QDELETING, COMSIG_LIVING_DEATH)) + QDEL_NULL(link) + REMOVE_TRAIT(owner, TRAIT_OVERWATCHED, TRAIT_STATUS_EFFECT(id)) + if (!QDELETED(owner)) + owner.apply_status_effect(/datum/status_effect/overwatch_immune) + return ..() + +/datum/status_effect/overwatch/Destroy() + QDEL_NULL(link) + if (!isnull(watcher)) // Side effects in Destroy? Well it turns out `on_remove` is also just called on Destroy. But only if the owner isn't deleting. + INVOKE_ASYNC(src, PROC_REF(unregister_watcher), watcher) + watcher = null + + return ..() + +/// Clean up our association with the caster of this ability. +/datum/status_effect/overwatch/proc/unregister_watcher(mob/living/former_overwatcher) + if (!overwatch_triggered) + former_overwatcher.Stun(2 SECONDS, ignore_canstun = TRUE) + UnregisterSignal(former_overwatcher, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH)) + +/// Uh oh, you did something within my threat radius, now we're going to shoot you +/datum/status_effect/overwatch/proc/opportunity_attack() + SIGNAL_HANDLER + if (!can_see(watcher, owner, length = watch_range)) + qdel(src) + return + overwatch_triggered = TRUE + INVOKE_ASYNC(watcher, TYPE_PROC_REF(/atom/, fire_projectile), projectile_type, owner, projectile_sound) + +/// Can't overwatch you if I don't exist +/datum/status_effect/overwatch/proc/on_participant_died() + SIGNAL_HANDLER + qdel(src) + +/atom/movable/screen/alert/status_effect/overwatch + name = "Overwatched" + desc = "Freeze! You are being watched!" + icon_state = "aimed" + +/// Blocks further applications of the ability for a little while +/datum/status_effect/overwatch_immune + id = "watcher_overwatch_immunity" + duration = 10 SECONDS // To stop watcher tendrils spamming the shit out of you + alert_type = null + +/datum/status_effect/overwatch_immune/on_apply() + . = ..() + ADD_TRAIT(owner, TRAIT_OVERWATCH_IMMUNE, TRAIT_STATUS_EFFECT(id)) + +/datum/status_effect/overwatch_immune/on_remove() + REMOVE_TRAIT(owner, TRAIT_OVERWATCH_IMMUNE, TRAIT_STATUS_EFFECT(id)) + return ..() diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_projectiles.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_projectiles.dm new file mode 100644 index 00000000000..2680e9aa914 --- /dev/null +++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_projectiles.dm @@ -0,0 +1,37 @@ +/// Chilling projectile, hurts and slows you down +/obj/projectile/temp/watcher + name = "chilling blast" + icon_state = "ice_2" + damage = 10 + damage_type = BURN + armor_flag = ENERGY + temperature = -50 + +/obj/projectile/temp/watcher/on_hit(mob/living/target, blocked = 0) + . = ..() + if (!isliving(target)) + return + apply_status(target) + +/// Apply an additional on-hit effect +/obj/projectile/temp/watcher/proc/apply_status(mob/living/target) + target.apply_status_effect(/datum/status_effect/freezing_blast) + +/// Lava projectile, ignites you +/obj/projectile/temp/watcher/magma_wing + name = "scorching blast" + icon_state = "lava" + damage = 5 + temperature = 200 + +/obj/projectile/temp/watcher/magma_wing/apply_status(mob/living/target) + target.adjust_fire_stacks(0.1) + target.ignite_mob() + +/// Freezing projectile, freezes you +/obj/projectile/temp/watcher/ice_wing + name = "freezing blast" + damage = 5 + +/obj/projectile/temp/watcher/ice_wing/apply_status(mob/living/target) + target.apply_status_effect(/datum/status_effect/freon/watcher) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 6d4f1da8eb2..3efbeed0ce4 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1421,6 +1421,7 @@ /mob/living/basic/killer_tomato, /mob/living/basic/lizard, /mob/living/basic/mining/goliath, + /mob/living/basic/mining/watcher, /mob/living/basic/morph, /mob/living/basic/mouse, /mob/living/basic/mushroom, @@ -1433,7 +1434,6 @@ /mob/living/basic/statue, /mob/living/basic/stickman, /mob/living/basic/stickman/dog, - /mob/living/simple_animal/hostile/asteroid/basilisk/watcher, /mob/living/simple_animal/hostile/blob/blobbernaut/independent, /mob/living/simple_animal/hostile/gorilla, /mob/living/simple_animal/hostile/megafauna/dragon/lesser, diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm index 50e581844de..3248fd9c2aa 100644 --- a/code/modules/mob/living/simple_animal/hostile/hostile.dm +++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm @@ -479,20 +479,11 @@ else targeted_zone = ran_zone() casing.fire_casing(targeted_atom, src, null, null, null, targeted_zone, 0, src) - else if(projectiletype) - var/obj/projectile/P = new projectiletype(startloc) - playsound(src, projectilesound, 100, TRUE) - P.starting = startloc - P.firer = src - P.fired_from = src - P.yo = targeted_atom.y - startloc.y - P.xo = targeted_atom.x - startloc.x + return + if(projectiletype) + fire_projectile(projectiletype, targeted_atom, projectilesound) if(AIStatus != AI_ON)//Don't want mindless mobs to have their movement screwed up firing in space newtonian_move(get_dir(targeted_atom, target_from)) - P.original = targeted_atom - P.preparePixelProjectile(targeted_atom, src) - P.fire() - return P /mob/living/simple_animal/hostile/proc/CanSmashTurfs(turf/T) diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/basilisk.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/basilisk.dm deleted file mode 100644 index 97a144f0477..00000000000 --- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/basilisk.dm +++ /dev/null @@ -1,220 +0,0 @@ -//A beast that fire freezing blasts. -/mob/living/simple_animal/hostile/asteroid/basilisk - name = "basilisk" - desc = "A territorial beast, covered in a thick shell that absorbs energy. Its stare causes victims to freeze from the inside." - icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi' - icon_state = "Basilisk" - icon_living = "Basilisk" - icon_aggro = "Basilisk_alert" - icon_dead = "Basilisk_dead" - icon_gib = "syndicate_gib" - mob_biotypes = MOB_ORGANIC|MOB_BEAST - move_to_delay = 20 - projectiletype = /obj/projectile/temp/basilisk - projectilesound = 'sound/weapons/pierce.ogg' - ranged = 1 - ranged_message = "stares" - ranged_cooldown_time = 30 - throw_message = "does nothing against the hard shell of" - vision_range = 2 - speed = 3 - maxHealth = 200 - health = 200 - harm_intent_damage = 5 - obj_damage = 60 - melee_damage_lower = 12 - melee_damage_upper = 12 - attack_verb_continuous = "bites into" - attack_verb_simple = "bite into" - speak_emote = list("chitters") - attack_sound = 'sound/weapons/bladeslice.ogg' - attack_vis_effect = ATTACK_EFFECT_BITE - aggro_vision_range = 9 - turns_per_move = 5 - gold_core_spawnable = HOSTILE_SPAWN - loot = list(/obj/item/stack/ore/diamond{layer = ABOVE_MOB_LAYER}, - /obj/item/stack/ore/diamond{layer = ABOVE_MOB_LAYER}) - var/lava_drinker = TRUE - var/warmed_up = FALSE - -/obj/projectile/temp/basilisk - name = "freezing blast" - icon_state = "ice_2" - damage = 10 - damage_type = BURN - armor_flag = ENERGY - temperature = -50 // Cools you down! per hit! - var/slowdown = TRUE //Determines if the projectile applies a slowdown status effect on carbons or not - -/obj/projectile/temp/basilisk/on_hit(atom/target, blocked = 0) - . = ..() - if(iscarbon(target) && slowdown) - var/mob/living/carbon/carbon_target = target - carbon_target.apply_status_effect(/datum/status_effect/freezing_blast) - -/obj/projectile/temp/basilisk/heated - name = "energy blast" - icon_state= "chronobolt" - damage = 40 - damage_type = BRUTE - temperature = 0 - slowdown = FALSE - - -/mob/living/simple_animal/hostile/asteroid/basilisk/GiveTarget(new_target) - if(..()) //we have a target - var/atom/target_from = GET_TARGETS_FROM(src) - if(isliving(target) && !target.Adjacent(target_from) && ranged_cooldown <= world.time)//No more being shot at point blank or spammed with RNG beams - INVOKE_ASYNC(src, PROC_REF(OpenFire), target) - -/mob/living/simple_animal/hostile/asteroid/basilisk/ex_act(severity, target) - switch(severity) - if(EXPLODE_DEVASTATE) - investigate_log("has been gibbed by an explosion.", INVESTIGATE_DEATHS) - gib() - if(EXPLODE_HEAVY) - adjustBruteLoss(140) - if(EXPLODE_LIGHT) - adjustBruteLoss(110) - - return TRUE - -/mob/living/simple_animal/hostile/asteroid/basilisk/AttackingTarget() - . = ..() - if(lava_drinker && !warmed_up && islava(target)) - visible_message(span_warning("[src] begins to drink from [target]...")) - if(do_after(src, 70, target = target)) - visible_message(span_warning("[src] begins to fire up!")) - fully_heal() - icon_state = "Basilisk_alert" - set_varspeed(0) - warmed_up = TRUE - projectiletype = /obj/projectile/temp/basilisk/heated - addtimer(CALLBACK(src, PROC_REF(cool_down)), 3000) - -/mob/living/simple_animal/hostile/asteroid/basilisk/proc/cool_down() - visible_message(span_warning("[src] appears to be cooling down...")) - if(stat != DEAD) - icon_state = "Basilisk" - set_varspeed(3) - warmed_up = FALSE - projectiletype = /obj/projectile/temp/basilisk - -//Watcher -/mob/living/simple_animal/hostile/asteroid/basilisk/watcher - name = "watcher" - desc = "A levitating, eye-like creature held aloft by winglike formations of sinew. A sharp spine of crystal protrudes from its body." - icon = 'icons/mob/simple/lavaland/lavaland_monsters_wide.dmi' - icon_state = "watcher" - icon_living = "watcher" - icon_aggro = "watcher" - icon_dead = "watcher_dead" - health_doll_icon = "watcher" - pixel_x = -12 - base_pixel_x = -12 - throw_message = "bounces harmlessly off of" - melee_damage_lower = 15 - melee_damage_upper = 15 - attack_verb_continuous = "impales" - attack_verb_simple = "impale" - combat_mode = TRUE - speak_emote = list("telepathically cries") - attack_sound = 'sound/weapons/bladeslice.ogg' - attack_vis_effect = null // doesn't bite unlike the parent type. - stat_attack = HARD_CRIT - robust_searching = 1 - crusher_loot = /obj/item/crusher_trophy/watcher_wing - gold_core_spawnable = NO_SPAWN - loot = list() - butcher_results = list(/obj/item/stack/ore/diamond = 2, /obj/item/stack/sheet/sinew = 2, /obj/item/stack/sheet/bone = 1) - lava_drinker = FALSE - search_objects = 1 - wanted_objects = list(/obj/item/pen/survival, /obj/item/stack/ore/diamond) - -/mob/living/simple_animal/hostile/asteroid/basilisk/watcher/Initialize(mapload) - . = ..() - AddElement(/datum/element/simple_flying) - -/mob/living/simple_animal/hostile/asteroid/basilisk/watcher/Life(seconds_per_tick = SSMOBS_DT, times_fired) - . = ..() - if(stat == CONSCIOUS) - consume_bait() - -/mob/living/simple_animal/hostile/asteroid/basilisk/watcher/proc/consume_bait() - for(var/obj/potential_consumption in view(1, src)) - if(istype(potential_consumption, /obj/item/stack/ore/diamond)) - qdel(potential_consumption) - visible_message(span_notice("[src] consumes [potential_consumption], and it disappears! ...At least, you think.")) - else if(istype(potential_consumption, /obj/item/pen/survival)) - qdel(potential_consumption) - visible_message(span_notice("[src] examines [potential_consumption] closer, and telekinetically shatters the pen.")) - -/mob/living/simple_animal/hostile/asteroid/basilisk/watcher/random/Initialize(mapload) - . = ..() - if(prob(1)) - if(prob(75)) - new /mob/living/simple_animal/hostile/asteroid/basilisk/watcher/magmawing(loc) - else - new /mob/living/simple_animal/hostile/asteroid/basilisk/watcher/icewing(loc) - return INITIALIZE_HINT_QDEL - -/mob/living/simple_animal/hostile/asteroid/basilisk/watcher/magmawing - name = "magmawing watcher" - desc = "When raised very close to lava, some watchers adapt to the extreme heat and use lava as both a weapon and wings." - icon_state = "watcher_magmawing" - icon_living = "watcher_magmawing" - icon_aggro = "watcher_magmawing" - icon_dead = "watcher_magmawing_dead" - maxHealth = 215 //Compensate for the lack of slowdown on projectiles with a bit of extra health - health = 215 - light_system = MOVABLE_LIGHT - light_range = 3 - light_power = 2.5 - light_color = LIGHT_COLOR_LAVA - projectiletype = /obj/projectile/temp/basilisk/magmawing - crusher_loot = /obj/item/crusher_trophy/blaster_tubes/magma_wing - crusher_drop_mod = 60 - -/mob/living/simple_animal/hostile/asteroid/basilisk/watcher/icewing - name = "icewing watcher" - desc = "Very rarely, some watchers will eke out an existence far from heat sources. In the absence of warmth, they become icy and fragile but fire much stronger freezing blasts." - icon_state = "watcher_icewing" - icon_living = "watcher_icewing" - icon_aggro = "watcher_icewing" - icon_dead = "watcher_icewing_dead" - maxHealth = 170 - health = 170 - projectiletype = /obj/projectile/temp/basilisk/icewing - butcher_results = list(/obj/item/stack/ore/diamond = 5, /obj/item/stack/sheet/bone = 1) //No sinew; the wings are too fragile to be usable - crusher_loot = /obj/item/crusher_trophy/watcher_wing/ice_wing - crusher_drop_mod = 30 - -/obj/projectile/temp/basilisk/magmawing - name = "scorching blast" - icon_state = "lava" - damage = 5 - damage_type = BURN - temperature = 200 // Heats you up! per hit! - slowdown = FALSE - -/obj/projectile/temp/basilisk/magmawing/on_hit(atom/target, blocked = FALSE) - . = ..() - if(.) - var/mob/living/L = target - if (istype(L)) - L.adjust_fire_stacks(0.1) - L.ignite_mob() - -/obj/projectile/temp/basilisk/icewing - damage = 5 - damage_type = BURN - -/obj/projectile/temp/basilisk/icewing/on_hit(atom/target, blocked = FALSE) - . = ..() - if(.) - var/mob/living/L = target - if(istype(L)) - L.apply_status_effect(/datum/status_effect/freon/watcher) - -/mob/living/simple_animal/hostile/asteroid/basilisk/watcher/tendril - from_spawner = TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/ice_demon.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/ice_demon.dm index b70ab7a3a22..9d84fe2e1fe 100644 --- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/ice_demon.dm +++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/ice_demon.dm @@ -11,7 +11,7 @@ speak_emote = list("telepathically cries") speed = 2 move_to_delay = 2 - projectiletype = /obj/projectile/temp/basilisk/ice + projectiletype = /obj/projectile/temp/ice_demon projectilesound = 'sound/weapons/pierce.ogg' ranged = TRUE ranged_message = "manifests ice" @@ -47,14 +47,16 @@ . = ..() AddElement(/datum/element/simple_flying) -/obj/projectile/temp/basilisk/ice +/obj/projectile/temp/ice_demon name = "ice blast" + icon_state = "ice_2" damage = 5 + damage_type = BURN + armor_flag = ENERGY speed = 1 pixel_speed_multiplier = 0.25 range = 200 temperature = -75 - slowdown = FALSE /mob/living/simple_animal/hostile/asteroid/ice_demon/OpenFire() ranged_cooldown = world.time + ranged_cooldown_time diff --git a/code/modules/movespeed/modifiers/mobs.dm b/code/modules/movespeed/modifiers/mobs.dm index 52c79945021..89ab5cd5c17 100644 --- a/code/modules/movespeed/modifiers/mobs.dm +++ b/code/modules/movespeed/modifiers/mobs.dm @@ -167,3 +167,6 @@ /datum/movespeed_modifier/goliath_mount multiplicative_slowdown = -26 + +/datum/movespeed_modifier/basilisk_overheat + multiplicative_slowdown = -18 diff --git a/code/modules/procedural_mapping/mapGenerators/asteroid.dm b/code/modules/procedural_mapping/mapGenerators/asteroid.dm index 3da5c0d6182..ab2bc6f2ca4 100644 --- a/code/modules/procedural_mapping/mapGenerators/asteroid.dm +++ b/code/modules/procedural_mapping/mapGenerators/asteroid.dm @@ -20,8 +20,8 @@ /datum/map_generator_module/splatter_layer/asteroid_monsters spawnableTurfs = list() spawnableAtoms = list( + /mob/living/basic/mining/basilisk = 10, /mob/living/basic/mining/goliath/ancient = 10, - /mob/living/simple_animal/hostile/asteroid/basilisk = 10, /mob/living/simple_animal/hostile/asteroid/hivelord = 10, ) diff --git a/code/modules/procedural_mapping/mapGenerators/lavaland.dm b/code/modules/procedural_mapping/mapGenerators/lavaland.dm index d262a317a13..e1a6505a5e8 100644 --- a/code/modules/procedural_mapping/mapGenerators/lavaland.dm +++ b/code/modules/procedural_mapping/mapGenerators/lavaland.dm @@ -10,9 +10,11 @@ /datum/map_generator_module/splatter_layer/lavaland_monsters spawnableTurfs = list() - spawnableAtoms = list(/mob/living/basic/mining/goliath = 10, - /mob/living/simple_animal/hostile/asteroid/hivelord/legion = 10, - /mob/living/simple_animal/hostile/asteroid/basilisk/watcher = 10) + spawnableAtoms = list( + /mob/living/basic/mining/goliath = 10, + /obj/effect/spawner/random/watcher = 10, + /mob/living/simple_animal/hostile/asteroid/hivelord/legion = 10, + ) /datum/map_generator_module/splatter_layer/lavaland_tendrils spawnableTurfs = list() diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index b20eb63520f..ec4fc9227e9 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -1131,3 +1131,19 @@ #undef MOVES_HITSCAN #undef MUZZLE_EFFECT_PIXEL_INCREMENT + +/// Fire a projectile from this atom at another atom +/atom/proc/fire_projectile(projectile_type, atom/target, sound, firer) + if (!isnull(sound)) + playsound(src, sound, vol = 100, vary = TRUE) + + var/turf/startloc = get_turf(src) + var/obj/projectile/bullet = new projectile_type(startloc) + bullet.starting = startloc + bullet.firer = firer || src + bullet.fired_from = src + bullet.yo = target.y - startloc.y + bullet.xo = target.x - startloc.x + bullet.original = target + bullet.preparePixelProjectile(target, src) + bullet.fire() diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm index ddfe58e2666..5d145907589 100644 --- a/code/modules/unit_tests/simple_animal_freeze.dm +++ b/code/modules/unit_tests/simple_animal_freeze.dm @@ -51,12 +51,6 @@ /mob/living/simple_animal/hostile/alien/queen/large, /mob/living/simple_animal/hostile/alien/sentinel, /mob/living/simple_animal/hostile/asteroid, - /mob/living/simple_animal/hostile/asteroid/basilisk, - /mob/living/simple_animal/hostile/asteroid/basilisk/watcher, - /mob/living/simple_animal/hostile/asteroid/basilisk/watcher/icewing, - /mob/living/simple_animal/hostile/asteroid/basilisk/watcher/magmawing, - /mob/living/simple_animal/hostile/asteroid/basilisk/watcher/random, - /mob/living/simple_animal/hostile/asteroid/basilisk/watcher/tendril, /mob/living/simple_animal/hostile/asteroid/brimdemon, /mob/living/simple_animal/hostile/asteroid/curseblob, /mob/living/simple_animal/hostile/asteroid/elite, diff --git a/code/modules/vehicles/mecha/_mecha.dm b/code/modules/vehicles/mecha/_mecha.dm index 23084173caa..fdb53c47572 100644 --- a/code/modules/vehicles/mecha/_mecha.dm +++ b/code/modules/vehicles/mecha/_mecha.dm @@ -688,6 +688,7 @@ return use_power(melee_energy_drain) + SEND_SIGNAL(user, COMSIG_MOB_USED_MECH_MELEE, src) target.mech_melee_attack(src, user) TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MELEE_ATTACK, melee_cooldown) diff --git a/code/modules/vehicles/mecha/equipment/mecha_equipment.dm b/code/modules/vehicles/mecha/equipment/mecha_equipment.dm index c88b8a62085..7bdd5cab006 100644 --- a/code/modules/vehicles/mecha/equipment/mecha_equipment.dm +++ b/code/modules/vehicles/mecha/equipment/mecha_equipment.dm @@ -106,6 +106,7 @@ /obj/item/mecha_parts/mecha_equipment/proc/action(mob/source, atom/target, list/modifiers) TIMER_COOLDOWN_START(chassis, COOLDOWN_MECHA_EQUIPMENT(type), equip_cooldown)//Cooldown is on the MECH so people dont bypass it by switching equipment + SEND_SIGNAL(source, COMSIG_MOB_USED_MECH_EQUIPMENT, chassis) chassis.use_power(energy_drain) return TRUE diff --git a/code/modules/vehicles/mecha/mecha_movement.dm b/code/modules/vehicles/mecha/mecha_movement.dm index 48d370d6172..d6c8feed4b3 100644 --- a/code/modules/vehicles/mecha/mecha_movement.dm +++ b/code/modules/vehicles/mecha/mecha_movement.dm @@ -53,7 +53,9 @@ . = TRUE if(!canmove || !(user in return_drivers())) return - vehicle_move(direction) + if (!vehicle_move(direction)) + return + SEND_SIGNAL(user, COMSIG_MOB_DROVE_MECH, src) /obj/vehicle/sealed/mecha/vehicle_move(direction, forcerotate = FALSE) if(!COOLDOWN_FINISHED(src, cooldown_vehicle_move)) diff --git a/icons/effects/eldritch.dmi b/icons/effects/eldritch.dmi index ebcbb397af0..40d4ea80df2 100644 Binary files a/icons/effects/eldritch.dmi and b/icons/effects/eldritch.dmi differ diff --git a/icons/mob/actions/actions_animal.dmi b/icons/mob/actions/actions_animal.dmi index 9caf5cdf2f7..f3f9a667f22 100644 Binary files a/icons/mob/actions/actions_animal.dmi and b/icons/mob/actions/actions_animal.dmi differ diff --git a/icons/mob/simple/lavaland/lavaland_monsters.dmi b/icons/mob/simple/lavaland/lavaland_monsters.dmi index c167b45e629..a2d840c4ced 100644 Binary files a/icons/mob/simple/lavaland/lavaland_monsters.dmi and b/icons/mob/simple/lavaland/lavaland_monsters.dmi differ diff --git a/icons/mob/simple/lavaland/lavaland_monsters_wide.dmi b/icons/mob/simple/lavaland/lavaland_monsters_wide.dmi index 60d8cc16cae..b6a47908b44 100644 Binary files a/icons/mob/simple/lavaland/lavaland_monsters_wide.dmi and b/icons/mob/simple/lavaland/lavaland_monsters_wide.dmi differ diff --git a/tgstation.dme b/tgstation.dme index fdeb3903df5..f62fe7cc76c 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -807,7 +807,9 @@ #include "code\datums\ai\basic_mobs\basic_subtrees\find_paper_and_write.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\find_parent.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\flee_target.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\maintain_distance.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\opportunistic_ventcrawler.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\ranged_skirmish.dm" #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" @@ -909,6 +911,7 @@ #include "code\datums\components\aura_healing.dm" #include "code\datums\components\bakeable.dm" #include "code\datums\components\basic_mob_attack_telegraph.dm" +#include "code\datums\components\basic_ranged_ready_overlay.dm" #include "code\datums\components\beetlejuice.dm" #include "code\datums\components\blood_walk.dm" #include "code\datums\components\bloodysoles.dm" @@ -992,6 +995,7 @@ #include "code\datums\components\leash.dm" #include "code\datums\components\light_eater.dm" #include "code\datums\components\lock_on_cursor.dm" +#include "code\datums\components\magnet.dm" #include "code\datums\components\manual_blinking.dm" #include "code\datums\components\manual_breathing.dm" #include "code\datums\components\mind_linker.dm" @@ -4142,6 +4146,8 @@ #include "code\modules\mob\living\basic\icemoon\ice_whelp\ice_whelp_abilities.dm" #include "code\modules\mob\living\basic\icemoon\ice_whelp\ice_whelp_ai.dm" #include "code\modules\mob\living\basic\lavaland\mining.dm" +#include "code\modules\mob\living\basic\lavaland\basilisk\basilisk.dm" +#include "code\modules\mob\living\basic\lavaland\basilisk\basilisk_overheat.dm" #include "code\modules\mob\living\basic\lavaland\bileworm\_bileworm.dm" #include "code\modules\mob\living\basic\lavaland\bileworm\bileworm_actions.dm" #include "code\modules\mob\living\basic\lavaland\bileworm\bileworm_ai.dm" @@ -4156,6 +4162,11 @@ #include "code\modules\mob\living\basic\lavaland\lobstrosity\lobstrosity.dm" #include "code\modules\mob\living\basic\lavaland\lobstrosity\lobstrosity_ai.dm" #include "code\modules\mob\living\basic\lavaland\lobstrosity\lobstrosity_trophy.dm" +#include "code\modules\mob\living\basic\lavaland\watcher\watcher.dm" +#include "code\modules\mob\living\basic\lavaland\watcher\watcher_ai.dm" +#include "code\modules\mob\living\basic\lavaland\watcher\watcher_gaze.dm" +#include "code\modules\mob\living\basic\lavaland\watcher\watcher_overwatch.dm" +#include "code\modules\mob\living\basic\lavaland\watcher\watcher_projectiles.dm" #include "code\modules\mob\living\basic\pets\fox.dm" #include "code\modules\mob\living\basic\pets\penguin.dm" #include "code\modules\mob\living\basic\pets\pet.dm" @@ -4471,7 +4482,6 @@ #include "code\modules\mob\living\simple_animal\hostile\megafauna\hierophant.dm" #include "code\modules\mob\living\simple_animal\hostile\megafauna\legion.dm" #include "code\modules\mob\living\simple_animal\hostile\megafauna\wendigo.dm" -#include "code\modules\mob\living\simple_animal\hostile\mining_mobs\basilisk.dm" #include "code\modules\mob\living\simple_animal\hostile\mining_mobs\brimdemon.dm" #include "code\modules\mob\living\simple_animal\hostile\mining_mobs\curse_blob.dm" #include "code\modules\mob\living\simple_animal\hostile\mining_mobs\goldgrub.dm" diff --git a/tools/UpdatePaths/Scripts/77630_simple_to_basic_watcher.txt b/tools/UpdatePaths/Scripts/77630_simple_to_basic_watcher.txt new file mode 100644 index 00000000000..9d3679be4d4 --- /dev/null +++ b/tools/UpdatePaths/Scripts/77630_simple_to_basic_watcher.txt @@ -0,0 +1,5 @@ +/mob/living/simple_animal/hostile/asteroid/basilisk/watcher/random : /obj/effect/spawner/random/watcher +/mob/living/simple_animal/hostile/asteroid/basilisk/watcher/@SUBTYPES : /mob/living/basic/mining/watcher/@SUBTYPES{@OLD} +/mob/living/simple_animal/hostile/asteroid/basilisk : /mob/living/basic/mining/basilisk{@OLD} + +/mob/living/basic/mining/goliath/random : /obj/effect/spawner/random/goliath