diff --git a/code/__DEFINES/ai/monsters.dm b/code/__DEFINES/ai/monsters.dm index 7fd7556125b..750d65e6eb3 100644 --- a/code/__DEFINES/ai/monsters.dm +++ b/code/__DEFINES/ai/monsters.dm @@ -190,6 +190,23 @@ /// the bonfire we will light up #define BB_MOOK_BONFIRE_TARGET "bonfire_target" +//leaper keys +///key holds our volley ability +#define BB_LEAPER_VOLLEY "leaper_volley" +///key holds our flop ability +#define BB_LEAPER_FLOP "leaper_flop" +///key holds our bubble ability +#define BB_LEAPER_BUBBLE "leaper_bubble" +///key holds our summon ability +#define BB_LEAPER_SUMMON "leaper_summon" +///key holds the world timer for swimming +#define BB_KEY_SWIM_TIME "key_swim_time" +///key holds the water or land target turf +#define BB_SWIM_ALTERNATE_TURF "swim_alternate_turf" +///key holds our state of swimming +#define BB_CURRENTLY_SWIMMING "currently_swimming" +///key holds how long we will be swimming for +#define BB_KEY_SWIMMER_COOLDOWN "key_swimmer_cooldown" //Wizard AI keys /// Key where we store our main targeted spell #define BB_WIZARD_TARGETED_SPELL "BB_wizard_targeted_spell" diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index 2d8a96213c4..5676a6aaa8e 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -73,6 +73,8 @@ GLOBAL_LIST_INIT(turfs_openspace, typecacheof(list( #define iscliffturf(A) (istype(A, /turf/open/cliff)) +#define iswaterturf(A) (istype(A, /turf/open/water)) + GLOBAL_LIST_INIT(turfs_pass_meteor, typecacheof(list( /turf/closed/mineral, /turf/open/misc/asteroid, diff --git a/code/__DEFINES/research.dm b/code/__DEFINES/research.dm index 52ee2c87f4e..c1427fcb67a 100644 --- a/code/__DEFINES/research.dm +++ b/code/__DEFINES/research.dm @@ -55,7 +55,6 @@ #define CELL_LINE_TABLE_WALKING_MUSHROOM "cell_line_walking_mushroom_table" #define CELL_LINE_TABLE_QUEEN_BEE "cell_line_bee_queen_table" #define CELL_LINE_TABLE_BUTTERFLY "cell_line_butterfly_table" -#define CELL_LINE_TABLE_LEAPER "cell_line_leaper_table" #define CELL_LINE_TABLE_MEGA_ARACHNID "cell_line_table_mega_arachnid" //! All cell virus types diff --git a/code/__DEFINES/vehicles.dm b/code/__DEFINES/vehicles.dm index 9aac19be3c1..d210a07dde4 100644 --- a/code/__DEFINES/vehicles.dm +++ b/code/__DEFINES/vehicles.dm @@ -26,6 +26,8 @@ #define UNBUCKLE_DISABLED_RIDER (1<<3) // For fireman carries, the carrying human needs an arm #define CARRIER_NEEDS_ARM (1<<4) +// This rider must be our friend +#define JUST_FRIEND_RIDERS (1<<5) //car_traits flags ///Will this car kidnap people by ramming into them? diff --git a/code/_globalvars/lists/xenobiology.dm b/code/_globalvars/lists/xenobiology.dm index 1e247a6279d..f641e9d5ecc 100644 --- a/code/_globalvars/lists/xenobiology.dm +++ b/code/_globalvars/lists/xenobiology.dm @@ -73,11 +73,9 @@ GLOBAL_LIST_INIT_TYPED(cell_line_tables, /list, list( CELL_LINE_TABLE_WALKING_MUSHROOM = list(/datum/micro_organism/cell_line/walking_mushroom = 1), CELL_LINE_TABLE_QUEEN_BEE = list(/datum/micro_organism/cell_line/queen_bee = 1), CELL_LINE_TABLE_BUTTERFLY = list(/datum/micro_organism/cell_line/butterfly = 1), - CELL_LINE_TABLE_LEAPER = list(/datum/micro_organism/cell_line/leaper = 1), CELL_LINE_TABLE_MEGA_ARACHNID = list(/datum/micro_organism/cell_line/mega_arachnid = 1), CELL_LINE_TABLE_ALGAE = list( /datum/micro_organism/cell_line/frog = 2, - /datum/micro_organism/cell_line/leaper = 2, /datum/micro_organism/cell_line/mega_arachnid = 1, /datum/micro_organism/cell_line/queen_bee = 1, /datum/micro_organism/cell_line/butterfly = 1, diff --git a/code/datums/ai/basic_mobs/basic_subtrees/go_for_swim.dm b/code/datums/ai/basic_mobs/basic_subtrees/go_for_swim.dm new file mode 100644 index 00000000000..12c77119f3e --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/go_for_swim.dm @@ -0,0 +1,59 @@ +#define DEFAULT_TIME_SWIMMER 30 SECONDS + +///subtree to go and swim! +/datum/ai_planning_subtree/go_for_swim + +/datum/ai_planning_subtree/go_for_swim/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + if(controller.blackboard_key_exists(BB_SWIM_ALTERNATE_TURF)) + controller.queue_behavior(/datum/ai_behavior/travel_towards/swimming, BB_SWIM_ALTERNATE_TURF) + + if(isnull(controller.blackboard[BB_KEY_SWIM_TIME])) + controller.set_blackboard_key(BB_KEY_SWIM_TIME, DEFAULT_TIME_SWIMMER) + + var/mob/living/living_pawn = controller.pawn + var/turf/our_turf = get_turf(living_pawn) + + // we have been taken out of water! + controller.set_blackboard_key(BB_CURRENTLY_SWIMMING, iswaterturf(our_turf)) + + if(controller.blackboard[BB_KEY_SWIM_TIME] < world.time) + controller.queue_behavior(/datum/ai_behavior/find_and_set/swim_alternate, BB_SWIM_ALTERNATE_TURF, /turf/open) + return + + // have some fun in the water + if(controller.blackboard[BB_CURRENTLY_SWIMMING] && SPT_PROB(5, seconds_per_tick)) + controller.queue_behavior(/datum/ai_behavior/perform_emote, "splashes water all around!") + + +///find land if its time to get out of water, otherwise find water +/datum/ai_behavior/find_and_set/swim_alternate + +/datum/ai_behavior/find_and_set/swim_alternate/search_tactic(datum/ai_controller/controller, locate_path, search_range) + var/mob/living/living_pawn = controller.pawn + if(QDELETED(living_pawn)) + return null + var/look_for_land = controller.blackboard[BB_CURRENTLY_SWIMMING] + var/list/possible_turfs = list() + for(var/turf/possible_turf in oview(search_range, living_pawn)) + if(isclosedturf(possible_turf) || isspaceturf(possible_turf) || isopenspaceturf(possible_turf)) + continue + if(possible_turf.is_blocked_turf()) + continue + if(look_for_land == iswaterturf(possible_turf)) + continue + possible_turfs += possible_turf + + if(!length(possible_turfs)) + return null + + return(pick(possible_turfs)) + +/datum/ai_behavior/travel_towards/swimming + clear_target = TRUE + +/datum/ai_behavior/travel_towards/swimming/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + var/time_to_add = controller.blackboard[BB_KEY_SWIMMER_COOLDOWN] ? controller.blackboard[BB_KEY_SWIMMER_COOLDOWN] : DEFAULT_TIME_SWIMMER + controller.set_blackboard_key(BB_KEY_SWIM_TIME, world.time + time_to_add ) + +#undef DEFAULT_TIME_SWIMMER diff --git a/code/datums/components/explode_on_attack.dm b/code/datums/components/explode_on_attack.dm new file mode 100644 index 00000000000..0560184f2ba --- /dev/null +++ b/code/datums/components/explode_on_attack.dm @@ -0,0 +1,40 @@ +/** + * Bombs the user after an attack + */ +/datum/component/explode_on_attack + /// range of bomb impact + var/impact_range + /// should we be destroyed after the explosion? + var/destroy_on_explode + /// list of mobs we wont bomb on attack + var/list/mob_type_dont_bomb + +/datum/component/explode_on_attack/Initialize(impact_range = 1, destroy_on_explode = TRUE, list/mob_type_dont_bomb = list()) + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + src.impact_range = impact_range + src.destroy_on_explode = destroy_on_explode + src.mob_type_dont_bomb = mob_type_dont_bomb + +/datum/component/explode_on_attack/RegisterWithParent() + RegisterSignal(parent, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(bomb_target)) + +/datum/component/explode_on_attack/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_HOSTILE_PRE_ATTACKINGTARGET) + + +/datum/component/explode_on_attack/proc/bomb_target(mob/living/owner, atom/victim) + SIGNAL_HANDLER + + if(!isliving(victim)) + return + + if(is_type_in_typecache(victim, mob_type_dont_bomb)) + return + + explosion(owner, light_impact_range = impact_range, explosion_cause = src) + + if(destroy_on_explode && owner) + qdel(owner) + return COMPONENT_HOSTILE_NO_ATTACK + diff --git a/code/datums/components/pet_commands/pet_commands_basic.dm b/code/datums/components/pet_commands/pet_commands_basic.dm index 68a20fa77f2..263f9b17f5b 100644 --- a/code/datums/components/pet_commands/pet_commands_basic.dm +++ b/code/datums/components/pet_commands/pet_commands_basic.dm @@ -98,6 +98,22 @@ parent.emote("spin") return SUBTREE_RETURN_FINISH_PLANNING +/** + * # Pet Command: Use ability + * Use an an ability that does not require any targets + */ +/datum/pet_command/untargetted_ability + ///untargetted ability we will use + var/ability_key + +/datum/pet_command/untargetted_ability/execute_action(datum/ai_controller/controller) + var/datum/action/cooldown/ability = controller.blackboard[ability_key] + if(!ability?.IsAvailable()) + return + controller.queue_behavior(/datum/ai_behavior/use_mob_ability, ability_key) + controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND) + return SUBTREE_RETURN_FINISH_PLANNING + /** * # Pet Command: Attack * Tells a pet to chase and bite the next thing you point at diff --git a/code/datums/components/riding/riding.dm b/code/datums/components/riding/riding.dm index 14358a5c5a3..d1a8f827caf 100644 --- a/code/datums/components/riding/riding.dm +++ b/code/datums/components/riding/riding.dm @@ -26,12 +26,16 @@ var/list/directional_vehicle_layers = list() /// same as above but instead of layer you have a list(px, py) var/list/directional_vehicle_offsets = list() + /// planes of the rider + var/list/directional_rider_planes = list() /// allow typecache for only certain turfs, forbid to allow all but those. allow only certain turfs will take precedence. var/list/allowed_turf_typecache /// allow typecache for only certain turfs, forbid to allow all but those. allow only certain turfs will take precedence. var/list/forbid_turf_typecache /// We don't need roads where we're going if this is TRUE, allow normal movement in space tiles var/override_allow_spacemove = FALSE + /// can anyone other than the rider unbuckle the rider? + var/can_force_unbuckle = TRUE /** * Ride check flags defined for the specific riding component types, so we know if we need arms, legs, or whatever. @@ -55,7 +59,6 @@ if(potion_boost) vehicle_move_delay = round(CONFIG_GET(number/movedelay/run_delay) * 0.85, 0.01) - /datum/component/riding/RegisterWithParent() . = ..() RegisterSignal(parent, COMSIG_ATOM_DIR_CHANGE, PROC_REF(vehicle_turned)) @@ -64,7 +67,8 @@ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(vehicle_moved)) RegisterSignal(parent, COMSIG_MOVABLE_BUMP, PROC_REF(vehicle_bump)) RegisterSignal(parent, COMSIG_BUCKLED_CAN_Z_MOVE, PROC_REF(riding_can_z_move)) - + if(!can_force_unbuckle) + RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(force_unbuckle)) /** * This proc handles all of the proc calls to things like set_vehicle_dir_layer() that a type of riding datum needs to call on creation * @@ -79,6 +83,9 @@ /datum/component/riding/proc/vehicle_mob_unbuckle(datum/source, mob/living/rider, force = FALSE) SIGNAL_HANDLER + handle_unbuckle(rider) + +/datum/component/riding/proc/handle_unbuckle(mob/living/rider) var/atom/movable/movable_parent = parent restore_position(rider) unequip_buckle_inhands(rider) @@ -94,6 +101,7 @@ var/atom/movable/movable_parent = parent handle_vehicle_layer(movable_parent.dir) handle_vehicle_offsets(movable_parent.dir) + handle_rider_plane(movable_parent.dir) if(rider.pulling == source) rider.stop_pulling() @@ -118,9 +126,20 @@ . = AM.layer AM.layer = . +/datum/component/riding/proc/handle_rider_plane(dir) + var/atom/movable/movable_parent = parent + var/target_plane = directional_rider_planes["[dir]"] + if(isnull(target_plane)) + return + for(var/mob/buckled_mob in movable_parent.buckled_mobs) + SET_PLANE_EXPLICIT(buckled_mob, target_plane, movable_parent) + /datum/component/riding/proc/set_vehicle_dir_layer(dir, layer) directional_vehicle_layers["[dir]"] = layer +/datum/component/riding/proc/set_rider_dir_plane(dir, plane) + directional_rider_planes["[dir]"] = plane + /// This is called after the ridden atom is successfully moved and is used to handle icon stuff /datum/component/riding/proc/vehicle_moved(datum/source, oldloc, dir, forced) SIGNAL_HANDLER @@ -135,6 +154,7 @@ return // runtimed with piggy's without this, look into this more handle_vehicle_offsets(dir) handle_vehicle_layer(dir) + handle_rider_plane(dir) /// Turning is like moving /datum/component/riding/proc/vehicle_turned(datum/source, _old_dir, new_dir) @@ -222,11 +242,14 @@ //BUCKLE HOOKS /datum/component/riding/proc/restore_position(mob/living/buckled_mob) - if(buckled_mob) - buckled_mob.pixel_x = buckled_mob.base_pixel_x - buckled_mob.pixel_y = buckled_mob.base_pixel_y - if(buckled_mob.client) - buckled_mob.client.view_size.resetToDefault() + if(isnull(buckled_mob)) + return + buckled_mob.pixel_x = buckled_mob.base_pixel_x + buckled_mob.pixel_y = buckled_mob.base_pixel_y + var/atom/source = parent + SET_PLANE_EXPLICIT(buckled_mob, initial(buckled_mob.plane), source) + if(buckled_mob.client) + buckled_mob.client.view_size.resetToDefault() //MOVEMENT /datum/component/riding/proc/turf_check(turf/next, turf/current) @@ -273,3 +296,10 @@ /datum/component/riding/proc/riding_can_z_move(atom/movable/movable_parent, direction, turf/start, turf/destination, z_move_flags, mob/living/rider) SIGNAL_HANDLER return COMPONENT_RIDDEN_ALLOW_Z_MOVE + +/datum/component/riding/proc/force_unbuckle(atom/movable/source, mob/living/living_hitter) + SIGNAL_HANDLER + + if((living_hitter in source.buckled_mobs)) + return + return COMPONENT_CANCEL_ATTACK_CHAIN diff --git a/code/datums/components/riding/riding_mob.dm b/code/datums/components/riding/riding_mob.dm index cf4dad61b5e..900a1648314 100644 --- a/code/datums/components/riding/riding_mob.dm +++ b/code/datums/components/riding/riding_mob.dm @@ -5,7 +5,8 @@ var/can_be_driven = TRUE /// If TRUE, this creature's abilities can be triggered by the rider while mounted var/can_use_abilities = FALSE - var/list/shared_action_buttons = list() + /// list of blacklisted abilities that cant be shared + var/list/blacklist_abilities = list() /datum/component/riding/creature/Initialize(mob/living/riding_mob, force = FALSE, ride_check_flags = NONE, potion_boost = FALSE) if(!isliving(parent)) @@ -60,6 +61,8 @@ // for fireman carries, check if the ridden is stunned/restrained else if((ride_check_flags & CARRIER_NEEDS_ARM) && (HAS_TRAIT(living_parent, TRAIT_RESTRAINED) || living_parent.incapacitated(IGNORE_RESTRAINTS|IGNORE_GRAB))) . = FALSE + else if((ride_check_flags & JUST_FRIEND_RIDERS) && !(living_parent.faction.Find(REF(rider)))) + . = FALSE if(. || !consequences) return @@ -156,6 +159,7 @@ for(var/mob/yeet_mob in user.buckled_mobs) force_dismount(yeet_mob, (!user.combat_mode)) // gentle on help, byeeee if not + /// If the ridden creature has abilities, and some var yet to be made is set to TRUE, the rider will be able to control those abilities /datum/component/riding/creature/proc/setup_abilities(mob/living/rider) if(!isliving(parent)) @@ -164,6 +168,8 @@ var/mob/living/ridden_creature = parent for(var/datum/action/action as anything in ridden_creature.actions) + if(is_type_in_list(action, blacklist_abilities)) + continue action.GiveAction(rider) /// Takes away the riding parent's abilities from the rider @@ -484,3 +490,35 @@ set_vehicle_dir_layer(NORTH, OBJ_LAYER) set_vehicle_dir_layer(EAST, OBJ_LAYER) set_vehicle_dir_layer(WEST, OBJ_LAYER) + +/datum/component/riding/creature/leaper + can_force_unbuckle = FALSE + can_use_abilities = TRUE + blacklist_abilities = list(/datum/action/cooldown/toggle_seethrough) + ride_check_flags = JUST_FRIEND_RIDERS + +/datum/component/riding/creature/leaper/handle_specials() + . = ..() + set_riding_offsets(RIDING_OFFSET_ALL, list(TEXT_NORTH = list(17, 46), TEXT_SOUTH = list(17,51), TEXT_EAST = list(27, 46), TEXT_WEST = list(6, 46))) + set_rider_dir_plane(SOUTH, GAME_PLANE_UPPER) + set_rider_dir_plane(NORTH, GAME_PLANE) + set_rider_dir_plane(EAST, GAME_PLANE_UPPER) + set_rider_dir_plane(WEST, GAME_PLANE_UPPER) + +/datum/component/riding/creature/leaper/Initialize(mob/living/riding_mob, force = FALSE, ride_check_flags = NONE, potion_boost = FALSE) + . = ..() + RegisterSignal(riding_mob, COMSIG_MOB_POINTED, PROC_REF(attack_pointed)) + +/datum/component/riding/creature/leaper/proc/attack_pointed(mob/living/rider, atom/pointed) + SIGNAL_HANDLER + if(!isclosedturf(pointed)) + return + var/mob/living/basic/basic_parent = parent + if(!basic_parent.CanReach(pointed)) + return + basic_parent.melee_attack(pointed) + + +/datum/component/riding/leaper/handle_unbuckle(mob/living/rider) + . = ..() + UnregisterSignal(rider, COMSIG_MOB_POINTED) diff --git a/code/game/objects/items/frog_statue.dm b/code/game/objects/items/frog_statue.dm new file mode 100644 index 00000000000..4d1bf9b6aa5 --- /dev/null +++ b/code/game/objects/items/frog_statue.dm @@ -0,0 +1,165 @@ +#define STATUE_FILTER "statue_filter" +#define FILTER_COLOR "#34b347" +#define RECALL_DURATION 3 SECONDS +#define MINIMUM_COLOR_VALUE 60 + +/obj/item/frog_statue + name = "frog statue" + desc = "Are they really comfortable living in this thing?" + icon = 'icons/obj/weapons/guns/magic.dmi' + icon_state = "frog_statue" + item_flags = NOBLUDGEON + ///our pet frog + var/mob/living/contained_frog + ///the summon cooldown + COOLDOWN_DECLARE(summon_cooldown) + +/obj/item/frog_statue/attack_self(mob/user) + . = ..() + + if(.) + return TRUE + + if(!COOLDOWN_FINISHED(src, summon_cooldown)) + user.balloon_alert(user, "recharging!") + return TRUE + + COOLDOWN_START(src, summon_cooldown, 30 SECONDS) + if(isnull(contained_frog)) + user.balloon_alert(user, "no frog linked!") + return TRUE + if(contained_frog.loc == src) + release_frog(user) + return TRUE + recall_frog(user) + return TRUE + +/obj/item/frog_statue/examine(mob/user) + . = ..() + if(!IS_WIZARD(user)) + return + if(isnull(contained_frog)) + . += span_notice("There are currently no frogs linked to this statue!") + else + . += span_notice("Using it will [contained_frog in src ? "release" : "recall"] the beast!") + +///resummon the frog into its home +/obj/item/frog_statue/proc/recall_frog(mob/user) + playsound(src, 'sound/items/frog_statue_release.ogg', 20) + user.Beam(contained_frog, icon_state = "lichbeam", time = RECALL_DURATION) + animate(contained_frog, transform = matrix().Scale(0.3, 0.3), time = RECALL_DURATION) + addtimer(CALLBACK(contained_frog, TYPE_PROC_REF(/atom/movable, forceMove), src), RECALL_DURATION) + +///release the frog to wreak havoc +/obj/item/frog_statue/proc/release_frog(mob/user) + var/list/possible_turfs = list() + for(var/turf/possible_turf in oview(2, user)) + if(possible_turf.is_blocked_turf() || isopenspaceturf(possible_turf)) + continue + possible_turfs += possible_turf + playsound(src, 'sound/items/frog_statue_release.ogg', 50, TRUE) + var/turf/final_turf = length(possible_turfs) ? pick(possible_turfs) : get_turf(src) + user.Beam(final_turf, icon_state = "lichbeam", time = RECALL_DURATION) + contained_frog.forceMove(final_turf) + animate(contained_frog, transform = matrix(), time = RECALL_DURATION) + REMOVE_TRAIT(contained_frog, TRAIT_AI_PAUSED, MAGIC_TRAIT) + +///set this frog as our inhabitor +/obj/item/frog_statue/proc/set_new_frog(mob/living/frog) + frog.transform = frog.transform.Scale(0.3, 0.3) + contained_frog = frog + animate_filter() + RegisterSignal(frog, COMSIG_QDELETING, PROC_REF(render_obsolete)) + +/// we have lost our frog, let out a scream! +/obj/item/frog_statue/proc/render_obsolete(datum/source) + SIGNAL_HANDLER + + contained_frog = null + playsound(src, 'sound/magic/demon_dies.ogg', 50, TRUE) + UnregisterSignal(source, COMSIG_QDELETING) + +/obj/item/frog_statue/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs) + . = ..() + if(arrived != contained_frog) + return + animate_filter() + ADD_TRAIT(contained_frog, TRAIT_AI_PAUSED, MAGIC_TRAIT) + if(contained_frog.health < contained_frog.maxHealth) + START_PROCESSING(SSobj, src) + +/obj/item/frog_statue/process(seconds_per_tick) + if(isnull(contained_frog)) + return + if(contained_frog.health == contained_frog.maxHealth) + STOP_PROCESSING(SSobj, src) + return + if(contained_frog.stat == DEAD) + contained_frog.revive() + contained_frog.adjustBruteLoss(-5) + +/obj/item/frog_statue/proc/animate_filter(mob/living/frog) + add_filter(STATUE_FILTER, 2, list("type" = "outline", "color" = FILTER_COLOR, "size" = 1)) + var/filter = get_filter(STATUE_FILTER) + animate(filter, alpha = 230, time = 2 SECONDS, loop = -1) + animate(alpha = 30, time = 0.5 SECONDS) + +/obj/item/frog_statue/Exited(atom/movable/gone, direction) + . = ..() + if(gone != contained_frog) + return + clear_filters() + +/obj/item/frog_contract + name = "frog contract" + desc = "Create a pact with an elder frog! This great beast will be your mount, protector, but most importantly your friend." + icon = 'icons/obj/scrolls.dmi' + icon_state = "scroll" + +/obj/item/frog_contract/attack_self(mob/user) + . = ..() + if(.) + return TRUE + create_frog(user) + return TRUE + +///customize our own frog and trap it into the statue +/obj/item/frog_contract/proc/create_frog(mob/user) + var/obj/item/frog_statue/statue = new(null) + var/mob/living/basic/leaper/new_frog = new(statue) + statue.set_new_frog(new_frog) + new_frog.befriend(user) + ADD_TRAIT(new_frog, TRAIT_AI_PAUSED, MAGIC_TRAIT) + select_frog_name(user, new_frog) + select_frog_color(user, new_frog) + user.put_in_hands(statue) + qdel(src) + + + +/obj/item/frog_contract/proc/select_frog_name(mob/user, mob/new_frog) + var/frog_name = sanitize_name(tgui_input_text(user, "Choose your frog's name!", "Name pet toad", "leaper", MAX_NAME_LEN), allow_numbers = TRUE) + if(!frog_name) + to_chat(user, span_warning("Please enter a valid name.")) + select_frog_name(user, new_frog) + return + new_frog.name = frog_name + +/obj/item/frog_contract/proc/select_frog_color(mob/user, mob/living/basic/leaper/new_frog) + var/frog_color = input(user, "Select your frog's color!" , "Pet toad color", COLOR_GREEN) as color|null + if(isnull(frog_color)) + to_chat(user, span_warning("Please choose a valid color.")) + select_frog_color(user, new_frog) + return + var/temp_hsv = RGBtoHSV(frog_color) + if(ReadHSV(temp_hsv)[3] < MINIMUM_COLOR_VALUE) + to_chat(user, span_danger("This color is too dark!")) + select_frog_color(user, new_frog) + return + new_frog.set_color_overlay(frog_color) + + +#undef STATUE_FILTER +#undef FILTER_COLOR +#undef RECALL_DURATION +#undef MINIMUM_COLOR_VALUE diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm index 546ae4da36a..b23de0aa6b0 100644 --- a/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm @@ -142,3 +142,9 @@ item_path = /obj/item/highfrequencyblade/wizard category = "Offensive" cost = 3 + +/datum/spellbook_entry/item/frog_contract + name = "Frog Contract" + desc = "Sign a pact with the frogs to have your own destructive pet guardian!" + item_path = /obj/item/frog_contract + category = "Offensive" diff --git a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm index 63c2992b513..a83953ae1c9 100644 --- a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm +++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm @@ -31,7 +31,7 @@ var/static/list/innate_actions = list( /datum/action/cooldown/mob_cooldown/slippery_ice_floors = BB_DEMON_SLIP_ABILITY, /datum/action/cooldown/mob_cooldown/ice_demon_teleport = BB_DEMON_TELEPORT_ABILITY, - /datum/action/cooldown/spell/conjure/create_afterimages = BB_DEMON_CLONE_ABILITY, + /datum/action/cooldown/spell/conjure/limit_summons/create_afterimages = BB_DEMON_CLONE_ABILITY, ) grant_actions_by_list(innate_actions) diff --git a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm index 79c9ee9bd58..b143f471138 100644 --- a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm +++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_abilities.dm @@ -82,7 +82,7 @@ /obj/effect/temp_visual/slippery_ice/proc/add_slippery_component() AddComponent(/datum/component/slippery, 2 SECONDS) -/datum/action/cooldown/spell/conjure/create_afterimages +/datum/action/cooldown/spell/conjure/limit_summons/create_afterimages name = "Create After Images" button_icon = 'icons/mob/simple/icemoon/icemoon_monsters.dmi' button_icon_state = "ice_demon" @@ -91,27 +91,8 @@ summon_type = list(/mob/living/basic/mining/demon_afterimage) summon_radius = 1 summon_amount = 2 - ///max number of after images - var/max_afterimages = 2 - ///How many clones do we have summoned - var/number_of_afterimages = 0 + max_summons = 2 -/datum/action/cooldown/spell/conjure/create_afterimages/can_cast_spell(feedback = TRUE) +/datum/action/cooldown/spell/conjure/limit_summons/create_afterimages/post_summon(atom/summoned_object, atom/cast_on) . = ..() - if(!.) - return FALSE - if(number_of_afterimages >= max_afterimages) - return FALSE - return TRUE - -/datum/action/cooldown/spell/conjure/create_afterimages/post_summon(atom/summoned_object, atom/cast_on) - var/mob/living/basic/created_copy = summoned_object - created_copy.AddComponent(/datum/component/joint_damage, overlord_mob = owner) - RegisterSignals(created_copy, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(delete_copy)) - number_of_afterimages++ - -/datum/action/cooldown/spell/conjure/create_afterimages/proc/delete_copy(mob/source) - SIGNAL_HANDLER - - UnregisterSignal(source, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH)) - number_of_afterimages-- + summoned_object.AddComponent(/datum/component/joint_damage, overlord_mob = owner) diff --git a/code/modules/mob/living/basic/jungle/leaper/leaper.dm b/code/modules/mob/living/basic/jungle/leaper/leaper.dm new file mode 100644 index 00000000000..543e21ef802 --- /dev/null +++ b/code/modules/mob/living/basic/jungle/leaper/leaper.dm @@ -0,0 +1,93 @@ +/mob/living/basic/leaper + name = "leaper" + desc = "Commonly referred to as 'leapers', the Geron Toad is a massive beast that spits out highly pressurized bubbles containing a unique toxin, knocking down its prey and then crushing it with its girth." + icon = 'icons/mob/simple/jungle/leaper.dmi' + icon_state = "leaper" + icon_living = "leaper" + icon_dead = "leaper_dead" + mob_biotypes = MOB_ORGANIC|MOB_BEAST + + melee_damage_lower = 15 + melee_damage_upper = 20 + maxHealth = 350 + health = 350 + speed = 10 + + pixel_x = -16 + base_pixel_x = -16 + + faction = list(FACTION_JUNGLE) + obj_damage = 30 + + unsuitable_atmos_damage = 0 + minimum_survivable_temperature = 0 + maximum_survivable_temperature = INFINITY + + attack_sound = 'sound/weapons/bladeslice.ogg' + attack_vis_effect = ATTACK_EFFECT_SLASH + status_flags = NONE + lighting_cutoff_red = 5 + lighting_cutoff_green = 20 + lighting_cutoff_blue = 25 + mob_size = MOB_SIZE_LARGE + ai_controller = /datum/ai_controller/basic_controller/leaper + ///appearance when we dead + var/mutable_appearance/dead_overlay + ///appearance when we are alive + var/mutable_appearance/living_overlay + ///list of pet commands we can issue + var/list/pet_commands = list( + /datum/pet_command/idle, + /datum/pet_command/free, + /datum/pet_command/follow, + /datum/pet_command/untargetted_ability/blood_rain, + /datum/pet_command/untargetted_ability/summon_toad, + /datum/pet_command/point_targetting/attack, + /datum/pet_command/point_targetting/use_ability/flop, + /datum/pet_command/point_targetting/use_ability/bubble, + ) + +/mob/living/basic/leaper/Initialize(mapload) + . = ..() + AddComponent(/datum/component/obeys_commands, pet_commands) + AddComponent(/datum/component/seethrough_mob) + AddElement(/datum/element/wall_smasher) + AddElement(/datum/element/ridable, component_type = /datum/component/riding/creature/leaper) + AddElement(/datum/element/footstep, footstep_type = FOOTSTEP_MOB_HEAVY) + var/datum/action/cooldown/mob_cooldown/blood_rain/volley = new(src) + volley.Grant(src) + ai_controller.set_blackboard_key(BB_LEAPER_VOLLEY, volley) + var/datum/action/cooldown/mob_cooldown/belly_flop/flop = new(src) + flop.Grant(src) + ai_controller.set_blackboard_key(BB_LEAPER_FLOP, flop) + var/datum/action/cooldown/mob_cooldown/projectile_attack/leaper_bubble/bubble = new(src) + bubble.Grant(src) + ai_controller.set_blackboard_key(BB_LEAPER_BUBBLE, bubble) + var/datum/action/cooldown/spell/conjure/limit_summons/create_suicide_toads/toads = new(src) + toads.Grant(src) + ai_controller.set_blackboard_key(BB_LEAPER_SUMMON, toads) + +/mob/living/basic/leaper/proc/set_color_overlay(toad_color) + dead_overlay = mutable_appearance(icon, "[icon_state]_dead_overlay") + dead_overlay.color = toad_color + + living_overlay = mutable_appearance(icon, "[icon_state]_overlay") + living_overlay.color = toad_color + update_appearance(UPDATE_OVERLAYS) + +/mob/living/basic/leaper/update_overlays() + . = ..() + if(stat == DEAD && dead_overlay) + . += dead_overlay + return + + if(living_overlay) + . += living_overlay + +/mob/living/basic/leaper/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, force, gentle = FALSE, quickstart = TRUE) + ADD_TRAIT(src, TRAIT_IMMOBILIZED, LEAPING_TRAIT) + return ..() + +/mob/living/basic/leaper/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum) + . = ..() + REMOVE_TRAIT(src, TRAIT_IMMOBILIZED, LEAPING_TRAIT) diff --git a/code/modules/mob/living/basic/jungle/leaper/leaper_abilities.dm b/code/modules/mob/living/basic/jungle/leaper/leaper_abilities.dm new file mode 100644 index 00000000000..8ff9edd1476 --- /dev/null +++ b/code/modules/mob/living/basic/jungle/leaper/leaper_abilities.dm @@ -0,0 +1,256 @@ +// fire leaper bubble ability +/datum/action/cooldown/mob_cooldown/projectile_attack/leaper_bubble + name = "Fire Leaper Bubble" + button_icon = 'icons/obj/weapons/guns/projectiles.dmi' + button_icon_state = "leaper" + desc = "Fires a poisonous leaper bubble towards the victim!" + background_icon_state = "bg_revenant" + overlay_icon_state = "bg_revenant_border" + cooldown_time = 7 SECONDS + projectile_type = /obj/projectile/leaper + projectile_sound = 'sound/effects/snap.ogg' + shared_cooldown = NONE + melee_cooldown_time = 0 SECONDS + +// bubble ability objects and effects +/obj/projectile/leaper + name = "leaper bubble" + icon_state = "leaper" + paralyze = 5 SECONDS + damage = 0 + range = 7 + hitsound = 'sound/effects/snap.ogg' + nondirectional_sprite = TRUE + impact_effect_type = /obj/effect/temp_visual/leaper_projectile_impact + +/obj/projectile/leaper/on_hit(atom/target, blocked = 0, pierce_hit) + . = ..() + if (!isliving(target)) + return + var/mob/living/bubbled = target + if(iscarbon(target)) + bubbled.reagents.add_reagent(/datum/reagent/toxin/leaper_venom, 5) + return + bubbled.apply_damage(30) + +/obj/projectile/leaper/on_range() + new /obj/structure/leaper_bubble(get_turf(src)) + return ..() + +/obj/effect/temp_visual/leaper_projectile_impact + name = "leaper bubble" + icon = 'icons/obj/weapons/guns/projectiles.dmi' + icon_state = "leaper_bubble_pop" + layer = ABOVE_ALL_MOB_LAYER + plane = GAME_PLANE_UPPER_FOV_HIDDEN + duration = 3 SECONDS + +/obj/effect/temp_visual/leaper_projectile_impact/Initialize(mapload) + . = ..() + new /obj/effect/decal/cleanable/leaper_sludge(get_turf(src)) + +/obj/effect/decal/cleanable/leaper_sludge + name = "leaper sludge" + desc = "A small pool of sludge, containing trace amounts of leaper venom." + icon = 'icons/effects/tomatodecal.dmi' + icon_state = "tomato_floor1" + +// bubble ability reagent +/datum/reagent/toxin/leaper_venom + name = "Leaper venom" + description = "A toxin spat out by leapers that, while harmless in small doses, quickly creates a toxic reaction if too much is in the body." + color = "#801E28" // rgb: 128, 30, 40 + toxpwr = 0 + taste_description = "french cuisine" + taste_mult = 1.3 + +/datum/reagent/toxin/leaper_venom/on_mob_life(mob/living/carbon/poisoned_mob, seconds_per_tick, times_fired) + . = ..() + if(volume <= 5) + return + if(poisoned_mob.adjustToxLoss(2.5 * REM * seconds_per_tick, updating_health = FALSE)) + return UPDATE_MOB_HEALTH + +// bubble ability structure +/obj/structure/leaper_bubble + name = "leaper bubble" + desc = "A floating bubble containing leaper venom. The contents are under a surprising amount of pressure." + icon = 'icons/obj/weapons/guns/projectiles.dmi' + icon_state = "leaper" + max_integrity = 10 + density = FALSE + +/obj/structure/leaper_bubble/Initialize(mapload) + . = ..() + AddElement(/datum/element/movetype_handler) + ADD_TRAIT(src, TRAIT_MOVE_FLOATING, LEAPER_BUBBLE_TRAIT) + QDEL_IN(src, 10 SECONDS) + var/static/list/loc_connections = list( + COMSIG_ATOM_ENTERED = PROC_REF(on_entered), + ) + AddElement(/datum/element/connect_loc, loc_connections) + +/obj/structure/leaper_bubble/Destroy() + new /obj/effect/temp_visual/leaper_projectile_impact(get_turf(src)) + playsound(src,'sound/effects/snap.ogg', 50, TRUE) + return ..() + +/obj/structure/leaper_bubble/proc/on_entered(datum/source, atom/movable/bubbled) + SIGNAL_HANDLER + if(!isliving(bubbled) || istype(bubbled, /mob/living/basic/leaper)) + return + var/mob/living/bubbled_mob = bubbled + + playsound(src, 'sound/effects/snap.ogg',50, TRUE) + bubbled_mob.Paralyze(5 SECONDS) + if(iscarbon(bubbled_mob)) + bubbled_mob.reagents.add_reagent(/datum/reagent/toxin/leaper_venom, 5) + else + bubbled_mob.apply_damage(30) + qdel(src) + +// blood rain ability +/datum/action/cooldown/mob_cooldown/blood_rain + name = "Blood Rain" + button_icon = 'icons/effects/effects.dmi' + button_icon_state = "blood_effect_falling" + background_icon_state = "bg_revenant" + overlay_icon_state = "bg_revenant_border" + desc = "Rain down poisonous dropplets of blood!" + cooldown_time = 10 SECONDS + click_to_activate = FALSE + shared_cooldown = NONE + melee_cooldown_time = 0 SECONDS + /// how many droplets we will fire + var/volley_count = 8 + /// time between each droplet launched + var/fire_interval = 0.1 SECONDS + +/datum/action/cooldown/mob_cooldown/blood_rain/Activate(mob/living/firer, atom/target) + var/list/possible_turfs = list() + for(var/turf/possible_turf in oview(5, owner)) + if(possible_turf.is_blocked_turf() || isopenspaceturf(possible_turf) || isspaceturf(possible_turf)) + continue + if(locate(/obj/structure/leaper_bubble) in possible_turf) + continue + possible_turfs += possible_turf + + if(!length(possible_turfs)) + return FALSE + + playsound(owner, 'sound/magic/fireball.ogg', 70, TRUE) + new /obj/effect/temp_visual/blood_drop_rising(get_turf(owner)) + addtimer(CALLBACK(src, PROC_REF(fire_droplets), possible_turfs), 1.5 SECONDS) + StartCooldown() + return TRUE + +/datum/action/cooldown/mob_cooldown/blood_rain/proc/fire_droplets(list/possible_turfs) + var/fire_count = min(volley_count, possible_turfs.len) + for(var/i in 1 to fire_count) + addtimer(CALLBACK(src, PROC_REF(fall_effect), pick_n_take(possible_turfs)), i * fire_interval) + +/datum/action/cooldown/mob_cooldown/blood_rain/proc/fall_effect(turf/selected_turf) + new /obj/effect/temp_visual/blood_drop_falling(selected_turf) + var/obj/effect/temp_visual/falling_shadow = new /obj/effect/temp_visual/shadow_telegraph(selected_turf) + animate(falling_shadow, transform = matrix().Scale(0.1, 0.1), time = falling_shadow.duration) + +// blood rain effects +/obj/effect/temp_visual/blood_drop_rising + name = "leaper bubble" + icon = 'icons/obj/weapons/guns/projectiles.dmi' + icon_state = "leaper" + layer = ABOVE_ALL_MOB_LAYER + plane = GAME_PLANE_UPPER_FOV_HIDDEN + duration = 1 SECONDS + +/obj/effect/temp_visual/blood_drop_rising/Initialize(mapload) + . = ..() + animate(src, pixel_y = base_pixel_y + 150, time = duration) + +/obj/effect/temp_visual/blood_drop_falling + name = "leaper bubble" + icon = 'icons/effects/effects.dmi' + icon_state = "blood_effect_falling" + layer = ABOVE_ALL_MOB_LAYER + plane = GAME_PLANE_UPPER_FOV_HIDDEN + duration = 0.7 SECONDS + pixel_y = 60 + +/obj/effect/temp_visual/blood_drop_falling/Initialize(mapload) + . = ..() + addtimer(CALLBACK(src, PROC_REF(create_blood_structure)), duration) + animate(src, pixel_y = 0, time = duration) + +/obj/effect/temp_visual/blood_drop_falling/proc/create_blood_structure() + playsound(src, 'sound/effects/snap.ogg', 50, TRUE) + new /obj/structure/leaper_bubble(get_turf(src)) + +/obj/effect/temp_visual/shadow_telegraph + name = "shadow" + icon = 'icons/effects/effects.dmi' + icon_state = "shadow_telegraph" + duration = 1.5 SECONDS + + +// flop ability +/datum/action/cooldown/mob_cooldown/belly_flop + name = "Belly Flop" + desc = "Belly flop your enemy!" + cooldown_time = 14 SECONDS + background_icon_state = "bg_revenant" + overlay_icon_state = "bg_revenant_border" + shared_cooldown = NONE + melee_cooldown_time = 0 SECONDS + +/datum/action/cooldown/mob_cooldown/belly_flop/Activate(atom/target) + var/turf/target_turf = get_turf(target) + if(isclosedturf(target_turf) || isspaceturf(target_turf)) + owner.balloon_alert(owner, "base not suitable!") + return FALSE + new /obj/effect/temp_visual/leaper_crush(target_turf) + owner.throw_at(target = target_turf, range = 7, speed = 1, spin = FALSE, callback = CALLBACK(src, PROC_REF(flop_on_turf), target_turf)) + StartCooldown() + return TRUE + +/datum/action/cooldown/mob_cooldown/belly_flop/proc/flop_on_turf(turf/target, original_pixel_y) + playsound(get_turf(owner), 'sound/effects/meteorimpact.ogg', 200, TRUE) + for(var/mob/living/victim in oview(1, owner)) + if(victim in owner.buckled_mobs) + continue + victim.apply_damage(35) + if(QDELETED(victim)) // Some mobs are deleted on death + continue + var/throw_dir = victim.loc == owner.loc ? get_dir(owner, victim) : pick(GLOB.alldirs) + var/throwtarget = get_edge_target_turf(victim, throw_dir) + victim.throw_at(target = throwtarget, range = 3, speed = 1) + victim.visible_message(span_warning("[victim] is thrown clear of [owner]!")) + +// flop ability effects +/obj/effect/temp_visual/leaper_crush + name = "grim tidings" + desc = "Incoming leaper!" + icon = 'icons/effects/96x96.dmi' + icon_state = "lily_pad" + layer = BELOW_MOB_LAYER + plane = GAME_PLANE + SET_BASE_PIXEL(-32, -32) + duration = 3 SECONDS + +// summon toads ability +/datum/action/cooldown/spell/conjure/limit_summons/create_suicide_toads + name = "Summon Suicide Toads" + button_icon = 'icons/mob/simple/animal.dmi' + button_icon_state = "frog_trash" + background_icon_state = "bg_revenant" + overlay_icon_state = "bg_revenant_border" + spell_requirements = NONE + cooldown_time = 30 SECONDS + summon_type = list(/mob/living/basic/frog/frog_suicide) + summon_radius = 2 + summon_amount = 2 + max_summons = 2 + +/datum/action/cooldown/spell/conjure/limit_summons/create_suicide_toads/post_summon(atom/summoned_object, atom/cast_on) + . = ..() + var/mob/living/summoned_toad = summoned_object + summoned_toad.faction = owner.faction ///so they dont attack the leaper or the wizard master diff --git a/code/modules/mob/living/basic/jungle/leaper/leaper_ai.dm b/code/modules/mob/living/basic/jungle/leaper/leaper_ai.dm new file mode 100644 index 00000000000..63e32f069ed --- /dev/null +++ b/code/modules/mob/living/basic/jungle/leaper/leaper_ai.dm @@ -0,0 +1,73 @@ +/datum/ai_controller/basic_controller/leaper + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic, + BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/basic/not_friends, + ) + + ai_movement = /datum/ai_movement/basic_avoidance + idle_behavior = /datum/idle_behavior/idle_random_walk/less_walking + planning_subtrees = list( + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/pet_planning, + /datum/ai_planning_subtree/targeted_mob_ability/pointed_bubble, + /datum/ai_planning_subtree/targeted_mob_ability/flop, + /datum/ai_planning_subtree/targeted_mob_ability/volley, + /datum/ai_planning_subtree/targeted_mob_ability/summon, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/go_for_swim, + ) + +/datum/ai_planning_subtree/targeted_mob_ability/pointed_bubble + ability_key = BB_LEAPER_BUBBLE + finish_planning = FALSE + +/datum/ai_planning_subtree/targeted_mob_ability/flop + ability_key = BB_LEAPER_FLOP + finish_planning = FALSE + +/datum/ai_planning_subtree/targeted_mob_ability/flop/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/atom/current_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(isclosedturf(current_target) || isspaceturf(current_target)) + return + return ..() + +/datum/ai_planning_subtree/targeted_mob_ability/volley + ability_key = BB_LEAPER_VOLLEY + finish_planning = FALSE + +/datum/ai_planning_subtree/targeted_mob_ability/summon + ability_key = BB_LEAPER_SUMMON + finish_planning = FALSE + +/datum/pet_command/point_targetting/use_ability/flop + command_name = "Flop" + command_desc = "Command your pet to belly flop your target!" + radial_icon = 'icons/mob/actions/actions_items.dmi' + radial_icon_state = "sniper_zoom" + speech_commands = list("flop", "crush") + pet_ability_key = BB_LEAPER_FLOP + +/datum/pet_command/point_targetting/use_ability/bubble + command_name = "Poison Bubble" + command_desc = "Launch poisonous bubbles at your target!" + radial_icon = 'icons/obj/weapons/guns/projectiles.dmi' + radial_icon_state = "leaper" + speech_commands = list("bubble", "shoot") + pet_ability_key = BB_LEAPER_BUBBLE + +/datum/pet_command/untargetted_ability/blood_rain + command_name = "Blood Rain" + command_desc = "Let it rain poisonous blood!" + radial_icon = 'icons/effects/effects.dmi' + radial_icon_state = "blood_effect_falling" + speech_commands = list("blood", "rain", "volley") + ability_key = BB_LEAPER_VOLLEY + + +/datum/pet_command/untargetted_ability/summon_toad + command_name = "Summon Toads" + command_desc = "Summon crazy suicide frogs!" + radial_icon = 'icons/mob/simple/animal.dmi' + radial_icon_state = "frog_trash" + speech_commands = list("frogs", "bombers") + ability_key = BB_LEAPER_SUMMON diff --git a/code/modules/mob/living/basic/vermin/crab.dm b/code/modules/mob/living/basic/vermin/crab.dm index bb81fd29c4d..276ed86d4de 100644 --- a/code/modules/mob/living/basic/vermin/crab.dm +++ b/code/modules/mob/living/basic/vermin/crab.dm @@ -92,4 +92,5 @@ /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/basic_melee_attack_subtree, /datum/ai_planning_subtree/random_speech/crab, + /datum/ai_planning_subtree/go_for_swim, ) diff --git a/code/modules/mob/living/basic/vermin/frog.dm b/code/modules/mob/living/basic/vermin/frog.dm index 7f46812d1da..1ffe12e063b 100644 --- a/code/modules/mob/living/basic/vermin/frog.dm +++ b/code/modules/mob/living/basic/vermin/frog.dm @@ -75,6 +75,23 @@ if(L.mob_size > MOB_SIZE_TINY) playsound(src, stepped_sound, 50, TRUE) +/mob/living/basic/frog/frog_suicide + name = "suicide frog" + desc = "Driven by sheer will." + icon_state = "frog_trash" + icon_living = "frog_trash" + icon_dead = "frog_trash_dead" + maxHealth = 5 + health = 5 + ai_controller = /datum/ai_controller/basic_controller/frog/suicide_frog + ///how long do we exist for + var/existence_period = 15 SECONDS + +/mob/living/basic/frog/frog_suicide/Initialize(mapload) + . = ..() + AddComponent(/datum/component/explode_on_attack, mob_type_dont_bomb = typecacheof(list(/mob/living/basic/frog, /mob/living/basic/leaper))) + addtimer(CALLBACK(src, PROC_REF(death)), existence_period) + /datum/ai_controller/basic_controller/frog blackboard = list( BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(), @@ -87,6 +104,7 @@ /datum/ai_planning_subtree/target_retaliate, /datum/ai_planning_subtree/random_speech/frog, /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/go_for_swim, ) /datum/ai_controller/basic_controller/frog/trash @@ -96,3 +114,9 @@ /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/basic_melee_attack_subtree, ) + +/datum/ai_controller/basic_controller/frog/suicide_frog + planning_subtrees = list( + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + ) diff --git a/code/modules/mob/living/simple_animal/hostile/jungle/_jungle_mobs.dm b/code/modules/mob/living/simple_animal/hostile/jungle/_jungle_mobs.dm deleted file mode 100644 index 8cccf16b850..00000000000 --- a/code/modules/mob/living/simple_animal/hostile/jungle/_jungle_mobs.dm +++ /dev/null @@ -1,17 +0,0 @@ -/mob/living/simple_animal/hostile/jungle - vision_range = 5 - atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) - faction = list(FACTION_JUNGLE) - obj_damage = 30 - environment_smash = ENVIRONMENT_SMASH_WALLS - minbodytemp = 0 - maxbodytemp = 450 - response_harm_continuous = "strikes" - response_harm_simple = "strike" - status_flags = NONE - combat_mode = TRUE - // Let's do a blue, since they'll be on green turfs if this shit is ever finished - lighting_cutoff_red = 5 - lighting_cutoff_green = 20 - lighting_cutoff_blue = 25 - mob_size = MOB_SIZE_LARGE diff --git a/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm b/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm deleted file mode 100644 index 3330b682a01..00000000000 --- a/code/modules/mob/living/simple_animal/hostile/jungle/leaper.dm +++ /dev/null @@ -1,289 +0,0 @@ -#define PLAYER_HOP_DELAY 25 - -//Huge, carnivorous toads that spit an immobilizing toxin at its victims before leaping onto them. -//It has no melee attack, and its damage comes from the toxin in its bubbles and its crushing leap. -//Its eyes will turn red to signal an imminent attack! -/mob/living/simple_animal/hostile/jungle/leaper - name = "leaper" - desc = "Commonly referred to as 'leapers', the Geron Toad is a massive beast that spits out highly pressurized bubbles containing a unique toxin, knocking down its prey and then crushing it with its girth." - icon = 'icons/mob/simple/jungle/leaper.dmi' - icon_state = "leaper" - icon_living = "leaper" - icon_dead = "leaper_dead" - mob_biotypes = MOB_ORGANIC|MOB_BEAST - maxHealth = 300 - health = 300 - ranged = TRUE - projectiletype = /obj/projectile/leaper - projectilesound = 'sound/weapons/pierce.ogg' - ranged_cooldown_time = 30 - pixel_x = -16 - base_pixel_x = -16 - layer = LARGE_MOB_LAYER - plane = GAME_PLANE_UPPER_FOV_HIDDEN - speed = 10 - stat_attack = HARD_CRIT - robust_searching = 1 - var/hopping = FALSE - var/hop_cooldown = 0 //Strictly for player controlled leapers - var/projectile_ready = FALSE //Stopping AI leapers from firing whenever they want, and only doing it after a hop has finished instead - - footstep_type = FOOTSTEP_MOB_HEAVY - -/obj/projectile/leaper - name = "leaper bubble" - icon_state = "leaper" - paralyze = 50 - damage = 0 - range = 7 - hitsound = 'sound/effects/snap.ogg' - nondirectional_sprite = TRUE - impact_effect_type = /obj/effect/temp_visual/leaper_projectile_impact - -/obj/projectile/leaper/on_hit(atom/target, blocked = 0, pierce_hit) - ..() - if (!isliving(target)) - return - var/mob/living/bubbled = target - if(iscarbon(target)) - bubbled.reagents.add_reagent(/datum/reagent/toxin/leaper_venom, 5) - return - if(isanimal(target)) - var/mob/living/simple_animal/bubbled_animal = bubbled - bubbled_animal.adjustHealth(25) - return - if (isbasicmob(target)) - bubbled.adjustBruteLoss(25) - -/obj/projectile/leaper/on_range() - var/turf/T = get_turf(src) - ..() - new /obj/structure/leaper_bubble(T) - -/obj/effect/temp_visual/leaper_projectile_impact - name = "leaper bubble" - icon = 'icons/obj/weapons/guns/projectiles.dmi' - icon_state = "leaper_bubble_pop" - layer = ABOVE_ALL_MOB_LAYER - plane = GAME_PLANE_UPPER_FOV_HIDDEN - duration = 3 - -/obj/effect/temp_visual/leaper_projectile_impact/Initialize(mapload) - . = ..() - new /obj/effect/decal/cleanable/leaper_sludge(get_turf(src)) - -/obj/effect/decal/cleanable/leaper_sludge - name = "leaper sludge" - desc = "A small pool of sludge, containing trace amounts of leaper venom." - icon = 'icons/effects/tomatodecal.dmi' - icon_state = "tomato_floor1" - -/obj/effect/decal/cleanable/leaper_sludge/Initialize(mapload, list/datum/disease/diseases) - . = ..() - AddElement(/datum/element/swabable, CELL_LINE_TABLE_LEAPER, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5) - -/obj/structure/leaper_bubble - name = "leaper bubble" - desc = "A floating bubble containing leaper venom. The contents are under a surprising amount of pressure." - icon = 'icons/obj/weapons/guns/projectiles.dmi' - icon_state = "leaper" - max_integrity = 10 - density = FALSE - -/obj/structure/leaper_bubble/Initialize(mapload) - . = ..() - AddElement(/datum/element/movetype_handler) - ADD_TRAIT(src, TRAIT_MOVE_FLOATING, LEAPER_BUBBLE_TRAIT) - QDEL_IN(src, 100) - var/static/list/loc_connections = list( - COMSIG_ATOM_ENTERED = PROC_REF(on_entered), - ) - AddElement(/datum/element/connect_loc, loc_connections) - AddElement(/datum/element/swabable, CELL_LINE_TABLE_LEAPER, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5) - -/obj/structure/leaper_bubble/Destroy() - new /obj/effect/temp_visual/leaper_projectile_impact(get_turf(src)) - playsound(src,'sound/effects/snap.ogg',50, TRUE, -1) - return ..() - -/obj/structure/leaper_bubble/proc/on_entered(datum/source, atom/movable/bubbled) - SIGNAL_HANDLER - if(!isliving(bubbled) || istype(bubbled, /mob/living/simple_animal/hostile/jungle/leaper)) - return - var/mob/living/bubbled_mob = bubbled - - playsound(src,'sound/effects/snap.ogg',50, TRUE, -1) - bubbled_mob.Paralyze(50) - if(iscarbon(bubbled_mob)) - bubbled_mob.reagents.add_reagent(/datum/reagent/toxin/leaper_venom, 5) - else if(isanimal(bubbled_mob)) - var/mob/living/simple_animal/bubbled_animal = bubbled_mob - bubbled_animal.adjustHealth(25) - else if(isbasicmob(bubbled_mob)) - bubbled_mob.adjustBruteLoss(25) - qdel(src) - -/datum/reagent/toxin/leaper_venom - name = "Leaper venom" - description = "A toxin spat out by leapers that, while harmless in small doses, quickly creates a toxic reaction if too much is in the body." - color = "#801E28" // rgb: 128, 30, 40 - toxpwr = 0 - taste_description = "french cuisine" - taste_mult = 1.3 - -/datum/reagent/toxin/leaper_venom/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired) - . = ..() - if(volume >= 10) - if(M.adjustToxLoss(5 * REM * seconds_per_tick, updating_health = FALSE)) - . = UPDATE_MOB_HEALTH - -/obj/effect/temp_visual/leaper_crush - name = "grim tidings" - desc = "Incoming leaper!" - icon = 'icons/effects/96x96.dmi' - icon_state = "lily_pad" - layer = BELOW_MOB_LAYER - plane = GAME_PLANE - SET_BASE_PIXEL(-32, -32) - duration = 30 - -/mob/living/simple_animal/hostile/jungle/leaper/Initialize(mapload) - . = ..() - AddComponent(/datum/component/seethrough_mob) - remove_verb(src, /mob/living/verb/pulled) - add_cell_sample() - -/mob/living/simple_animal/hostile/jungle/leaper/CtrlClickOn(atom/A) - face_atom(A) - GiveTarget(A) - if(!isturf(loc)) - return - if(next_move > world.time) - return - if(hopping) - return - if(isliving(A)) - var/mob/living/L = A - if(L.incapacitated()) - BellyFlop() - return - if(hop_cooldown <= world.time) - Hop(player_hop = TRUE) - -/mob/living/simple_animal/hostile/jungle/leaper/AttackingTarget(atom/attacked_target) - if(isliving(target)) - return - return ..() - -/mob/living/simple_animal/hostile/jungle/leaper/handle_automated_action() - if(hopping || projectile_ready) - return - . = ..() - if(target) - if(isliving(target)) - var/mob/living/L = target - if(L.incapacitated()) - BellyFlop() - return - if(!hopping) - Hop() - -/mob/living/simple_animal/hostile/jungle/leaper/Life(seconds_per_tick = SSMOBS_DT, times_fired) - . = ..() - update_icons() - -/mob/living/simple_animal/hostile/jungle/leaper/adjustHealth(amount, updating_health = TRUE, forced = FALSE) - if(prob(33) && !ckey) - ranged_cooldown = 0 //Keeps em on their toes instead of a constant rotation - ..() - -/mob/living/simple_animal/hostile/jungle/leaper/OpenFire() - face_atom(target) - if(ranged_cooldown <= world.time) - if(ckey) - if(hopping) - return - if(isliving(target)) - var/mob/living/L = target - if(L.incapacitated()) - return //No stunlocking. Hop on them after you stun them, you donk. - if(AIStatus == AI_ON && !projectile_ready && !ckey) - return - . = ..(target) - projectile_ready = FALSE - update_icons() - -/mob/living/simple_animal/hostile/jungle/leaper/proc/Hop(player_hop = FALSE) - if(z != target.z) - return - hopping = TRUE - add_traits(list(TRAIT_UNDENSE, TRAIT_NO_TRANSFORM), LEAPING_TRAIT) - pass_flags |= PASSMOB - var/turf/new_turf = locate((target.x + rand(-3,3)),(target.y + rand(-3,3)),target.z) - if(player_hop) - new_turf = get_turf(target) - hop_cooldown = world.time + PLAYER_HOP_DELAY - if(AIStatus == AI_ON && ranged_cooldown <= world.time) - projectile_ready = TRUE - update_icons() - throw_at(new_turf, max(3,get_dist(src,new_turf)), 1, src, FALSE, callback = CALLBACK(src, PROC_REF(FinishHop))) - -/mob/living/simple_animal/hostile/jungle/leaper/proc/FinishHop() - remove_traits(list(TRAIT_UNDENSE, TRAIT_NO_TRANSFORM), LEAPING_TRAIT) - pass_flags &= ~PASSMOB - hopping = FALSE - playsound(src.loc, 'sound/effects/meteorimpact.ogg', 100, TRUE) - if(target && AIStatus == AI_ON && projectile_ready && !ckey) - face_atom(target) - addtimer(CALLBACK(src, PROC_REF(OpenFire), target), 5) - -/mob/living/simple_animal/hostile/jungle/leaper/proc/BellyFlop() - var/turf/new_turf = get_turf(target) - hopping = TRUE - ADD_TRAIT(src, TRAIT_NO_TRANSFORM, LEAPING_TRAIT) - new /obj/effect/temp_visual/leaper_crush(new_turf) - addtimer(CALLBACK(src, PROC_REF(BellyFlopHop), new_turf), 3 SECONDS) - -/mob/living/simple_animal/hostile/jungle/leaper/proc/BellyFlopHop(turf/T) - ADD_TRAIT(src, TRAIT_UNDENSE, LEAPING_TRAIT) - throw_at(T, get_dist(src,T),1,src, FALSE, callback = CALLBACK(src, PROC_REF(Crush))) - -/mob/living/simple_animal/hostile/jungle/leaper/proc/Crush() - hopping = FALSE - remove_traits(list(TRAIT_UNDENSE, TRAIT_NO_TRANSFORM), LEAPING_TRAIT) - playsound(src, 'sound/effects/meteorimpact.ogg', 200, TRUE) - for(var/mob/living/L in orange(1, src)) - L.adjustBruteLoss(35) - if(!QDELETED(L)) // Some mobs are deleted on death - var/throw_dir = get_dir(src, L) - if(L.loc == loc) - throw_dir = pick(GLOB.alldirs) - var/throwtarget = get_edge_target_turf(src, throw_dir) - L.throw_at(throwtarget, 3, 1) - visible_message(span_warning("[L] is thrown clear of [src]!")) - if(ckey)//Lessens ability to chain stun as a player - ranged_cooldown = ranged_cooldown_time + world.time - update_icons() - -/mob/living/simple_animal/hostile/jungle/leaper/Goto() - return - -/mob/living/simple_animal/hostile/jungle/leaper/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum) - return - -/mob/living/simple_animal/hostile/jungle/leaper/update_icons() - . = ..() - if(stat) - icon_state = "leaper_dead" - return - if(ranged_cooldown <= world.time) - if(AIStatus == AI_ON && projectile_ready || ckey) - icon_state = "leaper_alert" - return - icon_state = "leaper" - -/mob/living/simple_animal/hostile/jungle/leaper/add_cell_sample() - . = ..() - AddElement(/datum/element/swabable, CELL_LINE_TABLE_LEAPER, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5) - -#undef PLAYER_HOP_DELAY diff --git a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm index 0152b343c45..5e9045e751f 100644 --- a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm +++ b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm @@ -684,30 +684,6 @@ virus_suspectibility = 0 resulting_atoms = list(/mob/living/basic/butterfly = 3) -/datum/micro_organism/cell_line/leaper - desc = "atypical amphibian cells" - required_reagents = list( - /datum/reagent/consumable/nutriment/protein, - /datum/reagent/ants, - /datum/reagent/consumable/eggyolk, - /datum/reagent/medicine/c2/synthflesh) - - supplementary_reagents = list( - /datum/reagent/growthserum = 4, - /datum/reagent/drug/blastoff = 3, - /datum/reagent/drug/space_drugs = 2, - /datum/reagent/consumable/ethanol/eggnog = 2, - /datum/reagent/consumable/vanilla = 2, - /datum/reagent/consumable/banana = 1, - /datum/reagent/consumable/nutriment/vitamin = 1) - - suppressive_reagents = list( - /datum/reagent/toxin/cyanide = -5, - /datum/reagent/consumable/mold = -2, - /datum/reagent/toxin/spore = -1) - - resulting_atoms = list(/mob/living/simple_animal/hostile/jungle/leaper = 1) - /datum/micro_organism/cell_line/mega_arachnid desc = "pseudoarachnoid cells" required_reagents = list( diff --git a/code/modules/spells/spell_types/conjure/_conjure.dm b/code/modules/spells/spell_types/conjure/_conjure.dm index 3afe7c52557..5fcff10ee18 100644 --- a/code/modules/spells/spell_types/conjure/_conjure.dm +++ b/code/modules/spells/spell_types/conjure/_conjure.dm @@ -61,3 +61,28 @@ /// Called on atoms summoned after they are created, allows extra variable editing and such of created objects /datum/action/cooldown/spell/conjure/proc/post_summon(atom/summoned_object, atom/cast_on) return + +///limits the amount of summons +/datum/action/cooldown/spell/conjure/limit_summons + ///max number of after images + var/max_summons + ///How many clones do we have summoned + var/number_of_summons = 0 + +/datum/action/cooldown/spell/conjure/limit_summons/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + if(number_of_summons >= max_summons) + return FALSE + return TRUE + +/datum/action/cooldown/spell/conjure/limit_summons/post_summon(atom/summoned_object, atom/cast_on) + RegisterSignals(summoned_object, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(delete_copy)) + number_of_summons++ + +/datum/action/cooldown/spell/conjure/limit_summons/proc/delete_copy(datum/source) + SIGNAL_HANDLER + + UnregisterSignal(source, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH)) + number_of_summons-- diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm index 3d0f60ae072..d5e1e773c92 100644 --- a/code/modules/unit_tests/simple_animal_freeze.dm +++ b/code/modules/unit_tests/simple_animal_freeze.dm @@ -74,8 +74,6 @@ /mob/living/simple_animal/hostile/illusion, /mob/living/simple_animal/hostile/illusion/escape, /mob/living/simple_animal/hostile/illusion/mirage, - /mob/living/simple_animal/hostile/jungle, - /mob/living/simple_animal/hostile/jungle/leaper, /mob/living/simple_animal/hostile/megafauna, /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner, /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/doom, diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi index bbde393b5ef..0dd945ff37e 100644 Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ diff --git a/icons/mob/simple/jungle/leaper.dmi b/icons/mob/simple/jungle/leaper.dmi index 39f807a191c..2150c7b1ac2 100644 Binary files a/icons/mob/simple/jungle/leaper.dmi and b/icons/mob/simple/jungle/leaper.dmi differ diff --git a/icons/obj/weapons/guns/magic.dmi b/icons/obj/weapons/guns/magic.dmi index 7cab0cdfc25..53217ef461b 100644 Binary files a/icons/obj/weapons/guns/magic.dmi and b/icons/obj/weapons/guns/magic.dmi differ diff --git a/sound/items/frog_statue_release.ogg b/sound/items/frog_statue_release.ogg new file mode 100644 index 00000000000..de7d3547778 Binary files /dev/null and b/sound/items/frog_statue_release.ogg differ diff --git a/tgstation.dme b/tgstation.dme index 0224a392d3d..bb2b7f4a3b0 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -852,6 +852,7 @@ #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\go_for_swim.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\maintain_distance.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\mine_walls.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\move_to_cardinal.dm" @@ -1022,6 +1023,7 @@ #include "code\datums\components\engraved.dm" #include "code\datums\components\evolutionary_leap.dm" #include "code\datums\components\explodable.dm" +#include "code\datums\components\explode_on_attack.dm" #include "code\datums\components\faction_granter.dm" #include "code\datums\components\fertile_egg.dm" #include "code\datums\components\fishing_spot.dm" @@ -2168,6 +2170,7 @@ #include "code\game\objects\items\extinguisher.dm" #include "code\game\objects\items\fireaxe.dm" #include "code\game\objects\items\flamethrower.dm" +#include "code\game\objects\items\frog_statue.dm" #include "code\game\objects\items\gift.dm" #include "code\game\objects\items\gun_maintenance.dm" #include "code\game\objects\items\hand_items.dm" @@ -4455,6 +4458,9 @@ #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\jungle\venus_human_trap.dm" +#include "code\modules\mob\living\basic\jungle\leaper\leaper.dm" +#include "code\modules\mob\living\basic\jungle\leaper\leaper_abilities.dm" +#include "code\modules\mob\living\basic\jungle\leaper\leaper_ai.dm" #include "code\modules\mob\living\basic\jungle\mega_arachnid\mega_arachnid.dm" #include "code\modules\mob\living\basic\jungle\mega_arachnid\mega_arachnid_abilities.dm" #include "code\modules\mob\living\basic\jungle\mega_arachnid\mega_arachnid_ai.dm" @@ -4802,8 +4808,6 @@ #include "code\modules\mob\living\simple_animal\hostile\ooze.dm" #include "code\modules\mob\living\simple_animal\hostile\vatbeast.dm" #include "code\modules\mob\living\simple_animal\hostile\zombie.dm" -#include "code\modules\mob\living\simple_animal\hostile\jungle\_jungle_mobs.dm" -#include "code\modules\mob\living\simple_animal\hostile\jungle\leaper.dm" #include "code\modules\mob\living\simple_animal\hostile\megafauna\_megafauna.dm" #include "code\modules\mob\living\simple_animal\hostile\megafauna\blood_drunk_miner.dm" #include "code\modules\mob\living\simple_animal\hostile\megafauna\bubblegum.dm"