diff --git a/code/__DEFINES/ai/pets.dm b/code/__DEFINES/ai/pets.dm index b3ad67ecc068d..3afaedfe0bcc6 100644 --- a/code/__DEFINES/ai/pets.dm +++ b/code/__DEFINES/ai/pets.dm @@ -27,3 +27,21 @@ #define BB_FIND_MOM_TYPES "BB_find_mom_types" ///list of types of mobs we must ignore #define BB_IGNORE_MOM_TYPES "BB_ignore_mom_types" + +/// The current string that this parrot will repeat back to someone +#define BB_PARROT_REPEAT_STRING "BB_parrot_repeat_string" +/// The odds that this parrot will repeat back a string +#define BB_PARROT_REPEAT_PROBABILITY "BB_parrot_repeat_probability" +/// The odds that this parrot will choose another string to repeat +#define BB_PARROT_PHRASE_CHANGE_PROBABILITY "BB_parrot_phrase_change_probability" +/// A copy of the string buffer that we end the shift with. DO NOT ACCESS THIS DIRECTLY - YOU SHOULD USE THE COMPONENT IN MOST CASES +#define BB_EXPORTABLE_STRING_BUFFER_LIST "BB_parrot_repeat_string_buffer" +/// The types of perches we desire to use +#define BB_PARROT_PERCH_TYPES "BB_parrot_perch_types" +#define BB_PERCH_TARGET "perch_target" +#define BB_HOARD_ITEM_TARGET "hoard_item_target" +#define BB_THEFT_VICTIM "theft_victim" +#define BB_HOARD_LOCATION "hoard_location" +#define BB_HOARD_LOCATION_RANGE "hoard_location_range" +#define BB_IGNORE_ITEMS "ignore_items" + diff --git a/code/__DEFINES/combat.dm b/code/__DEFINES/combat.dm index 116b933c6ae22..a8e0fef619f2f 100644 --- a/code/__DEFINES/combat.dm +++ b/code/__DEFINES/combat.dm @@ -277,6 +277,15 @@ GLOBAL_LIST_INIT(shove_disarming_types, typecacheof(list( #define BODY_ZONE_L_LEG "l_leg" #define BODY_ZONE_R_LEG "r_leg" +#define CARBON_GENERIC_BODY_ZONES list(\ + BODY_ZONE_HEAD,\ + BODY_ZONE_CHEST,\ + BODY_ZONE_L_ARM,\ + BODY_ZONE_R_ARM,\ + BODY_ZONE_L_LEG,\ + BODY_ZONE_R_LEG,\ +) + GLOBAL_LIST_INIT(arm_zones, list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM)) #define BODY_ZONE_PRECISE_EYES "eyes" diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm index d26dd4e8c8682..ed0e09f433137 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm @@ -47,13 +47,17 @@ #define COMSIG_MOVABLE_THROW_LANDED "movable_throw_landed" ///from base of atom/movable/on_changed_z_level(): (turf/old_turf, turf/new_turf, same_z_layer) #define COMSIG_MOVABLE_Z_CHANGED "movable_ztransit" +///called before hearing a message froma tom/movable/Hear(): +#define COMSIG_MOVABLE_PRE_HEAR "movable_pre_hear" + ///do not proceed to hear the message + #define COMSIG_MOVABLE_CANCEL_HEARING (1<<0) ///from base of atom/movable/Hear(): (proc args list(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, list/message_mods = list(), message_range)) #define COMSIG_MOVABLE_HEAR "movable_hear" - //#define HEARING_MESSAGE 1 - (I'm pretty sure this is never really used and can be gutted) + #define HEARING_MESSAGE 1 #define HEARING_SPEAKER 2 #define HEARING_LANGUAGE 3 #define HEARING_RAW_MESSAGE 4 - //#define HEARING_RADIO_FREQ 5 + #define HEARING_RADIO_FREQ 5 #define HEARING_SPANS 6 #define HEARING_MESSAGE_MODE 7 #define HEARING_RANGE 8 diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_ai.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_ai.dm index 85630c8e8f041..a04b8e751a0c4 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_ai.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_ai.dm @@ -3,3 +3,4 @@ /// Signal sent when a blackboard key is cleared #define COMSIG_AI_BLACKBOARD_KEY_CLEARED(blackboard_key) "ai_blackboard_key_clear_[blackboard_key]" + 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 18c6c651435ba..2c856e850c3ca 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_basic.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_basic.dm @@ -5,3 +5,7 @@ ///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" + +/// Sent from /datum/ai_planning_subtree/parrot_as_in_repeat() : () +#define COMSIG_NEEDS_NEW_PHRASE "parrot_needs_new_phrase" + #define NO_NEW_PHRASE_AVAILABLE (1<<0) //! Cancel to try again later for when we actually get a new phrase diff --git a/code/__DEFINES/research.dm b/code/__DEFINES/research.dm index c1427fcb67a06..005c67ae1a3ee 100644 --- a/code/__DEFINES/research.dm +++ b/code/__DEFINES/research.dm @@ -1,10 +1,6 @@ -#define RDSCREEN_NOBREAK "" - /// For instances where we don't want a design showing up due to it being for debug/sanity purposes #define DESIGN_ID_IGNORE "IGNORE_THIS_DESIGN" -#define RESEARCH_MATERIAL_DESTROY_ID "__destroy" - //! Techweb names for new point types. Can be used to define specific point values for specific types of research (science, security, engineering, etc.) #define TECHWEB_POINT_TYPE_GENERIC "General Research" diff --git a/code/__HELPERS/priority_announce.dm b/code/__HELPERS/priority_announce.dm index 7d716e5eaf6bb..f36ab36becbd5 100644 --- a/code/__HELPERS/priority_announce.dm +++ b/code/__HELPERS/priority_announce.dm @@ -37,7 +37,7 @@ * * encode_title - if TRUE, the title will be HTML encoded * * encode_text - if TRUE, the text will be HTML encoded */ -/proc/priority_announce(text, title = "", sound, type, sender_override, has_important_message = FALSE, list/mob/players, encode_title = TRUE, encode_text = TRUE, color_override) +/proc/priority_announce(text, title = "", sound, type, sender_override, has_important_message = FALSE, list/mob/players = GLOB.player_list, encode_title = TRUE, encode_text = TRUE, color_override) if(!text) return @@ -85,7 +85,7 @@ dispatch_announcement_to_players(finalized_announcement, players, sound) - if(isnull(sender_override)) + if(isnull(sender_override) && players == GLOB.player_list) if(length(title) > 0) GLOB.news_network.submit_article(title + "

" + text, "[command_name()]", "Station Announcements", null) else @@ -185,10 +185,7 @@ return jointext(returnable_strings, "") /// Proc that just dispatches the announcement to our applicable audience. Only the announcement is a mandatory arg. -/proc/dispatch_announcement_to_players(announcement, list/players, sound_override = null, should_play_sound = TRUE) - if(!players) - players = GLOB.player_list - +/proc/dispatch_announcement_to_players(announcement, list/players = GLOB.player_list, sound_override = null, should_play_sound = TRUE) var/sound_to_play = !isnull(sound_override) ? sound_override : 'sound/misc/notice2.ogg' for(var/mob/target in players) diff --git a/code/_globalvars/phobias.dm b/code/_globalvars/phobias.dm index 5bb8b4bf31498..3aa6c41de4f7f 100644 --- a/code/_globalvars/phobias.dm +++ b/code/_globalvars/phobias.dm @@ -61,8 +61,8 @@ GLOBAL_LIST_INIT(phobia_mobs, list( "birds" = typecacheof(list( /mob/living/basic/chick, /mob/living/basic/chicken, + /mob/living/basic/parrot, /mob/living/basic/pet/penguin, - /mob/living/simple_animal/parrot, )), "conspiracies" = typecacheof(list( /mob/living/basic/drone, diff --git a/code/controllers/subsystem/persistence/_persistence.dm b/code/controllers/subsystem/persistence/_persistence.dm index 586723c92e594..8d00c77812170 100644 --- a/code/controllers/subsystem/persistence/_persistence.dm +++ b/code/controllers/subsystem/persistence/_persistence.dm @@ -59,8 +59,9 @@ SUBSYSTEM_DEF(persistence) ///Loads up Poly's speech buffer. /datum/controller/subsystem/persistence/proc/load_poly() - for(var/mob/living/simple_animal/parrot/poly/P in GLOB.alive_mob_list) - twitterize(P.speech_buffer, "polytalk") + for(var/mob/living/basic/parrot/poly/bird in GLOB.alive_mob_list) + var/list/list_to_read = bird.get_static_list_of_phrases() + twitterize(list_to_read, "polytalk") break //Who's been duping the bird?! /// Loads up the amount of times maps appeared to alter their appearance in voting and rotation. diff --git a/code/controllers/subsystem/research.dm b/code/controllers/subsystem/research.dm index 1cc3468fb7db4..1d31582fc5fff 100644 --- a/code/controllers/subsystem/research.dm +++ b/code/controllers/subsystem/research.dm @@ -27,15 +27,15 @@ SUBSYSTEM_DEF(research) var/list/techweb_nodes_starting = list() ///category name = list(node.id = TRUE) var/list/techweb_categories = list() - ///associative double-layer path = list(id = list(point_type = point_discount)) - var/list/techweb_boost_items = list() + ///List of all items that can unlock a node. (node.id = list(items)) + var/list/techweb_unlock_items = list() ///Node ids that should be hidden by default. var/list/techweb_nodes_hidden = list() ///Node ids that are exclusive to the BEPIS. var/list/techweb_nodes_experimental = list() ///path = list(point type = value) var/list/techweb_point_items = list( - /obj/item/assembly/signaler/anomaly = list(TECHWEB_POINT_TYPE_GENERIC = 10000) + /obj/item/assembly/signaler/anomaly = list(TECHWEB_POINT_TYPE_GENERIC = 10000) ) var/list/errored_datums = list() var/list/point_types = list() //typecache style type = TRUE list @@ -64,7 +64,7 @@ SUBSYSTEM_DEF(research) /// Lookup list for ordnance briefers. var/list/ordnance_experiments = list() /// Lookup list for scipaper partners. - var/list/scientific_partners = list() + var/list/datum/scientific_partner/scientific_partners = list() /datum/controller/subsystem/research/Initialize() point_types = TECHWEB_POINT_TYPE_LIST_ASSOCIATIVE_NAMES @@ -153,7 +153,7 @@ SUBSYSTEM_DEF(research) if (!verify_techweb_nodes()) //Verify all nodes have ids and such. stack_trace("Invalid techweb nodes detected") calculate_techweb_nodes() - calculate_techweb_boost_list() + calculate_techweb_item_unlocking_requirements() if (!verify_techweb_nodes()) //Verify nodes and designs have been crosslinked properly. CRASH("Invalid techweb nodes detected") @@ -209,25 +209,15 @@ SUBSYSTEM_DEF(research) N.unlock_ids -= u research_node_id_error(u) . = FALSE - for(var/p in N.boost_item_paths) + for(var/p in N.required_items_to_unlock) if(!ispath(p)) - N.boost_item_paths -= p + N.required_items_to_unlock -= p WARNING("[p] is not a valid path.") node_boost_error(N.id, "[p] is not a valid path.") . = FALSE - var/list/points = N.boost_item_paths[p] - if(islist(points)) - for(var/i in points) - if(!isnum(points[i])) - WARNING("[points[i]] is not a valid number.") - node_boost_error(N.id, "[points[i]] is not a valid number.") - . = FALSE - else if(!point_types[i]) - WARNING("[i] is not a valid point type.") - node_boost_error(N.id, "[i] is not a valid point type.") - . = FALSE - else if(!isnull(points)) - N.boost_item_paths -= p + var/list/points = N.required_items_to_unlock[p] + if(!isnull(points)) + N.required_items_to_unlock -= p node_boost_error(N.id, "No valid list.") WARNING("No valid list.") . = FALSE @@ -281,18 +271,16 @@ SUBSYSTEM_DEF(research) var/datum/techweb_node/prereq_node = techweb_node_by_id(prereq_id) prereq_node.unlock_ids[node.id] = node -/datum/controller/subsystem/research/proc/calculate_techweb_boost_list(clearall = FALSE) - if(clearall) - techweb_boost_items = list() +/datum/controller/subsystem/research/proc/calculate_techweb_item_unlocking_requirements() for(var/node_id in techweb_nodes) var/datum/techweb_node/node = techweb_nodes[node_id] - for(var/path in node.boost_item_paths) + for(var/path in node.required_items_to_unlock) if(!ispath(path)) continue - if(length(techweb_boost_items[path])) - techweb_boost_items[path][node.id] = node.boost_item_paths[path] + if(length(techweb_unlock_items[path])) + techweb_unlock_items[path][node.id] = node.required_items_to_unlock[path] else - techweb_boost_items[path] = list(node.id = node.boost_item_paths[path]) + techweb_unlock_items[path] = list(node.id = node.required_items_to_unlock[path]) CHECK_TICK /datum/controller/subsystem/research/proc/populate_ordnance_experiments() diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm index 21b141feff801..0f7fe6ef142f2 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm @@ -1,6 +1,8 @@ /datum/ai_behavior/basic_melee_attack action_cooldown = 0.2 SECONDS // We gotta check unfortunately often because we're in a race condition with nextmove behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + ///do we finish this action after hitting once? + var/terminate_after_action = FALSE /datum/ai_behavior/basic_melee_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) . = ..() @@ -39,12 +41,21 @@ else basic_mob.melee_attack(target) + if(terminate_after_action) + finish_action(controller, TRUE, target_key) /datum/ai_behavior/basic_melee_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key) . = ..() if(!succeeded) controller.clear_blackboard_key(target_key) +/datum/ai_behavior/basic_melee_attack/interact_once + terminate_after_action = TRUE + +/datum/ai_behavior/basic_melee_attack/interact_once/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key) + . = ..() + controller.clear_blackboard_key(target_key) + /datum/ai_behavior/basic_ranged_attack action_cooldown = 0.6 SECONDS behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM diff --git a/code/datums/ai/generic/find_and_set.dm b/code/datums/ai/generic/find_and_set.dm index f54ba33e6e3a3..d368641ce0ca0 100644 --- a/code/datums/ai/generic/find_and_set.dm +++ b/code/datums/ai/generic/find_and_set.dm @@ -72,6 +72,16 @@ if(length(found)) return pick(found) +/// Like find_and_set/in_list, but we return the turf location of the item instead of the item itself. +/datum/ai_behavior/find_and_set/in_list/turf_location + +/datum/ai_behavior/find_and_set/in_list/turf_location/search_tactic(datum/ai_controller/controller, locate_paths, search_range) + . = ..() + if(isnull(.)) + return null + + return get_turf(.) + /** * Variant of find and set which returns an object which can be animated with a staff of change */ @@ -148,3 +158,16 @@ return pick(customers) return null + +/datum/ai_behavior/find_and_set/nearby_friends + action_cooldown = 2 SECONDS + +/datum/ai_behavior/find_and_set/nearby_friends/search_tactic(datum/ai_controller/controller, locate_path, search_range) + var/atom/friend = locate(/mob/living/carbon/human) in oview(search_range, controller.pawn) + + if(isnull(friend)) + return null + + var/mob/living/living_pawn = controller.pawn + var/potential_friend = living_pawn.faction.Find(REF(friend)) ? friend : null + return potential_friend diff --git a/code/datums/ai/generic/generic_behaviors.dm b/code/datums/ai/generic/generic_behaviors.dm index 4f816de4be30a..962c8d141cd8a 100644 --- a/code/datums/ai/generic/generic_behaviors.dm +++ b/code/datums/ai/generic/generic_behaviors.dm @@ -299,6 +299,8 @@ /datum/ai_behavior/perform_speech /datum/ai_behavior/perform_speech/perform(seconds_per_tick, datum/ai_controller/controller, speech, speech_sound) + . = ..() + var/mob/living/living_pawn = controller.pawn if(!istype(living_pawn)) return diff --git a/code/datums/components/energized.dm b/code/datums/components/energized.dm index 41262e23efc6a..f70e661336a11 100644 --- a/code/datums/components/energized.dm +++ b/code/datums/components/energized.dm @@ -1,3 +1,6 @@ +#define NORMAL_TOAST_PROB 3 +#define BROKEN_TOAST_PROB 33 + /datum/component/energized can_transfer = FALSE ///what we give to connect_loc by default, makes slippable mobs moving over us slip @@ -48,24 +51,14 @@ /datum/component/energized/proc/toast(turf/open/floor/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs) SIGNAL_HANDLER - if(!source.broken && !source.burnt) - return - if(!isliving(arrived)) return - if(prob(85)) - if(prob(25)) - do_sparks(1, FALSE, source) - playsound(src, SFX_SPARKS, 40, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) - source.audible_message(span_danger("[parent] makes an electric crackle...")) - return - var/mob/living/future_tram_victim = arrived var/datum/transport_controller/linear/tram/tram = transport_ref?.resolve() // Check for stopped states. - if(isnull(tram) || !tram.controller_operational || !inbound || !outbound) + if(isnull(tram) || !tram.controller_operational || !tram.controller_active || !inbound || !outbound) return FALSE var/obj/structure/transport/linear/tram/tram_part = tram.return_closest_platform_to(parent) @@ -76,6 +69,17 @@ if(isnull(source)) return FALSE + var/toast_prob = NORMAL_TOAST_PROB + if(source.broken || source.burnt || HAS_TRAIT(future_tram_victim, TRAIT_CURSED)) + toast_prob = BROKEN_TOAST_PROB + + if(prob(100 - toast_prob)) + if(prob(25)) + do_sparks(1, FALSE, source) + playsound(src, SFX_SPARKS, 40, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + source.audible_message(span_danger("[parent] makes an electric crackle...")) + return FALSE + // Everything will be based on position and travel direction var/plate_pos var/tram_pos @@ -102,7 +106,7 @@ return FALSE if((tram.travel_direction & EAST) && outbound > tram.destination_platform.platform_code) return FALSE - if(approach_distance >= AMBER_THRESHOLD_NORMAL) + if(approach_distance >= AMBER_THRESHOLD_DEGRADED) return FALSE // Finally the interesting part where they ACTUALLY get hit! @@ -112,8 +116,12 @@ action = NOTIFY_ORBIT, header = "Electrifying!", ) + do_sparks(4, FALSE, source) playsound(src, SFX_SPARKS, 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) source.audible_message(span_danger("[parent] makes a loud electric crackle!")) to_chat(future_tram_victim, span_userdanger("You hear a loud electric crackle!")) future_tram_victim.electrocute_act(15, src, 1) return TRUE + +#undef NORMAL_TOAST_PROB +#undef BROKEN_TOAST_PROB diff --git a/code/datums/components/listen_and_repeat.dm b/code/datums/components/listen_and_repeat.dm new file mode 100644 index 0000000000000..093557ec03b82 --- /dev/null +++ b/code/datums/components/listen_and_repeat.dm @@ -0,0 +1,74 @@ +/// The maximum number of phrases we can store in our speech buffer +#define MAX_SPEECH_BUFFER_SIZE 500 +/// Tendency we have to ignore radio chatter +#define RADIO_IGNORE_CHANCE 10 + +/// Simple element that will deterministically set a value based on stuff that the source has heard and will then compel the source to repeat it. +/// Requires a valid AI Blackboard. +/datum/component/listen_and_repeat + /// List of things that we start out having in our speech buffer + var/list/desired_phrases = null + /// The AI Blackboard Key we assign the value to. + var/blackboard_key = null + /// Probability we speak + var/speech_probability = null + /// Probabiliy we switch our phrase + var/switch_phrase_probability = null + /// List of things that we've heard and will repeat. + var/list/speech_buffer = null + +/datum/component/listen_and_repeat/Initialize(list/desired_phrases, blackboard_key) + . = ..() + if(!ismovable(parent)) + return COMPONENT_INCOMPATIBLE + + if(!isnull(desired_phrases)) + LAZYADD(speech_buffer, desired_phrases) + + src.blackboard_key = blackboard_key + + RegisterSignal(parent, COMSIG_MOVABLE_PRE_HEAR, PROC_REF(on_hear)) + RegisterSignal(parent, COMSIG_NEEDS_NEW_PHRASE, PROC_REF(set_new_blackboard_phrase)) + RegisterSignal(parent, COMSIG_LIVING_WRITE_MEMORY, PROC_REF(on_write_memory)) + +/// Called when we hear something. +/datum/component/listen_and_repeat/proc/on_hear(datum/source, list/passed_arguments) + SIGNAL_HANDLER + + var/message = passed_arguments[HEARING_RAW_MESSAGE] + var/speaker = passed_arguments[HEARING_SPEAKER] + var/over_radio = !!passed_arguments[HEARING_RADIO_FREQ] + if(speaker == source) // don't parrot ourselves + return + + if(over_radio && prob(RADIO_IGNORE_CHANCE)) + return + + var/number_of_excess_strings = LAZYLEN(speech_buffer) - MAX_SPEECH_BUFFER_SIZE + if(number_of_excess_strings > 0) // only remove if we're overfull + for(var/i in 1 to number_of_excess_strings) + LAZYREMOVE(speech_buffer, pick(speech_buffer)) + + LAZYOR(speech_buffer, html_decode(message)) + +/// Called to set a new value for the blackboard key. +/datum/component/listen_and_repeat/proc/set_new_blackboard_phrase(datum/source) + SIGNAL_HANDLER + var/atom/movable/atom_source = source + var/datum/ai_controller/controller = atom_source.ai_controller + if(!LAZYLEN(speech_buffer)) + controller.clear_blackboard_key(blackboard_key) + return NO_NEW_PHRASE_AVAILABLE + + var/selected_phrase = pick(speech_buffer) + controller.set_blackboard_key(blackboard_key, selected_phrase) + +/// Exports all the speech buffer data to a dedicated blackboard key on the source. +/datum/component/listen_and_repeat/proc/on_write_memory(datum/source, dead, gibbed) + SIGNAL_HANDLER + var/atom/movable/atom_source = source + var/datum/ai_controller/controller = atom_source.ai_controller + if(LAZYLEN(speech_buffer)) // what? well whatever let's just move on + return + + controller.set_blackboard_key(BB_EXPORTABLE_STRING_BUFFER_LIST, speech_buffer.Copy()) diff --git a/code/datums/components/omen.dm b/code/datums/components/omen.dm index 8b7a751ef2704..f03f28b8d6aff 100644 --- a/code/datums/components/omen.dm +++ b/code/datums/components/omen.dm @@ -31,6 +31,8 @@ if(!isnull(damage_mod)) src.damage_mod = damage_mod + ADD_TRAIT(parent, TRAIT_CURSED, SMITE_TRAIT) + /** * This is a omen eat omen world! The stronger omen survives. */ @@ -50,6 +52,7 @@ /datum/component/omen/Destroy(force) var/mob/living/person = parent + REMOVE_TRAIT(person, TRAIT_CURSED, SMITE_TRAIT) to_chat(person, span_nicegreen("You feel a horrible omen lifted off your shoulders!")) if(vessel) diff --git a/code/datums/components/orbit_poll.dm b/code/datums/components/orbit_poll.dm index 91b27d4b66797..dda7207ed4546 100644 --- a/code/datums/components/orbit_poll.dm +++ b/code/datums/components/orbit_poll.dm @@ -85,22 +85,47 @@ return for(var/mob/dead/observer/ghost as anything in orbiter_comp.orbiter_list) - if(QDELETED(ghost) || isnull(ghost.client)) + var/client/ghost_client = ghost.client + + if(QDELETED(ghost) || isnull(ghost_client)) continue + if(is_banned_from(ghost.ckey, job_bans)) continue + var/datum/preferences/ghost_prefs = ghost_client.prefs + if(isnull(ghost_prefs)) + candidates += ghost // we'll assume they wanted to be picked despite prefs being null for whatever fucked up reason + continue + + if(!ghost_prefs.read_preference(/datum/preference/toggle/ghost_roles)) + continue + if(!isnull(ghost_client.holder) && !ghost_prefs.read_preference(/datum/preference/toggle/ghost_roles_as_admin)) + continue + candidates += ghost - if(!length(candidates)) + pick_and_offer(candidates) + +/// Takes a list, picks a candidate, and offers the role to them. +/datum/component/orbit_poll/proc/pick_and_offer(list/volunteers) + if(length(volunteers) <= 0) phone_home() return - var/mob/dead/observer/chosen = pick(candidates) + var/mob/dead/observer/chosen = pick(volunteers) + + if(isnull(chosen)) + phone_home() + return - if(chosen) - deadchat_broadcast("[key_name(chosen, include_name = FALSE)] was selected for the role ([title]).", "Ghost Poll: ", parent) + SEND_SOUND(chosen, 'sound/misc/notice2.ogg') + var/response = tgui_alert(chosen, "Do you want to assume the role of [title]?", "Orbit Polling", list("Yes", "No"), 10 SECONDS) + if(response != "Yes") + var/reusable_list = volunteers - chosen + return pick_and_offer(reusable_list) + deadchat_broadcast("[key_name(chosen, include_name = FALSE)] was selected for the role ([title]).", "Ghost Poll: ", parent) phone_home(chosen) /// Make sure to call your parents my dude diff --git a/code/datums/components/supermatter_crystal.dm b/code/datums/components/supermatter_crystal.dm index 3c37bba33cb8d..14a0122dcf5cf 100644 --- a/code/datums/components/supermatter_crystal.dm +++ b/code/datums/components/supermatter_crystal.dm @@ -300,7 +300,7 @@ message_admins("[atom_source] has consumed [key_name_admin(consumed_mob)] [ADMIN_JMP(atom_source)].") atom_source.investigate_log("has consumed [key_name(consumed_mob)].", INVESTIGATE_ENGINE) consumed_mob.investigate_log("has been dusted by [atom_source].", INVESTIGATE_DEATHS) - if(istype(consumed_mob, /mob/living/simple_animal/parrot/poly)) // Dusting Poly creates a power surge + if(istype(consumed_mob, /mob/living/basic/parrot/poly)) // Dusting Poly creates a power surge force_event(/datum/round_event_control/supermatter_surge/poly, "Poly's revenge") notify_ghosts( "[consumed_mob] has been dusted by [atom_source]!", diff --git a/code/datums/diseases/parrotpossession.dm b/code/datums/diseases/parrotpossession.dm index 23f68e1a42ff6..2645ef8e19173 100644 --- a/code/datums/diseases/parrotpossession.dm +++ b/code/datums/diseases/parrotpossession.dm @@ -13,24 +13,41 @@ severity = DISEASE_SEVERITY_MEDIUM infectable_biotypes = MOB_ORGANIC|MOB_UNDEAD|MOB_ROBOTIC|MOB_MINERAL bypasses_immunity = TRUE //2spook - var/mob/living/simple_animal/parrot/poly/ghost/parrot + ///chance we speak + var/speak_chance = 5 + ///controller we speak from + var/datum/ai_controller/basic_controller/parrot_controller /datum/disease/parrot_possession/stage_act(seconds_per_tick, times_fired) . = ..() - if(!.) + + if(!. || isnull(parrot_controller)) return - if(QDELETED(parrot) || parrot.loc != affected_mob) - cure() - return FALSE + var/potential_phrase = parrot_controller.blackboard[BB_PARROT_REPEAT_STRING] - if(length(parrot.speech_buffer) && SPT_PROB(parrot.speak_chance, seconds_per_tick)) // I'm not going to dive into polycode trying to adjust that probability. Enjoy doubled ghost parrot speach - affected_mob.say(pick(parrot.speech_buffer), forced = "parrot possession") + if(SPT_PROB(speak_chance, seconds_per_tick) && !isnull(potential_phrase)) // I'm not going to dive into polycode trying to adjust that probability. Enjoy doubled ghost parrot speach + affected_mob.say(potential_phrase, forced = "parrot possession") /datum/disease/parrot_possession/cure() - if(parrot && parrot.loc == affected_mob) - parrot.forceMove(affected_mob.drop_location()) - affected_mob.visible_message(span_danger("[parrot] is violently driven out of [affected_mob]!"), span_userdanger("[parrot] bursts out of your chest!")) - ..() + var/atom/movable/inside_parrot = locate(/mob/living/basic/parrot/poly/ghost) in affected_mob + if(inside_parrot) + UnregisterSignal(inside_parrot, list(COMSIG_PREQDELETED, COMSIG_MOVABLE_MOVED)) + inside_parrot.forceMove(affected_mob.drop_location()) + affected_mob.visible_message( + span_danger("[inside_parrot] is violently driven out of [affected_mob]!"), + span_userdanger("[inside_parrot] bursts out of your chest!"), + ) + parrot_controller = null + return ..() + +/datum/disease/parrot_possession/proc/set_parrot(mob/living/parrot) + parrot_controller = parrot.ai_controller + RegisterSignals(parrot, list(COMSIG_PREQDELETED, COMSIG_MOVABLE_MOVED), PROC_REF(on_parrot_exit)) + +/datum/disease/parrot_possession/proc/on_parrot_exit(datum/source) + SIGNAL_HANDLER + UnregisterSignal(source, list(COMSIG_PREQDELETED, COMSIG_MOVABLE_MOVED)) + cure() diff --git a/code/datums/elements/loomable.dm b/code/datums/elements/loomable.dm index cfd0b4bc83480..76ee071a9a202 100644 --- a/code/datums/elements/loomable.dm +++ b/code/datums/elements/loomable.dm @@ -71,23 +71,33 @@ /// If a do_after of the specified loom_time passes, will create a new one of resulting_atom and either delete the item, or .use the required amount if its a stack /datum/element/loomable/proc/loom_me(obj/item/source, mob/living/user, atom/target) - if(!do_after(user, loom_time, target)) - user.balloon_alert(user, "interrupted!") - return - - ///we need to perform another check in case a stack somehow got diminished in the middle of the do_after - var/successful = TRUE + //this allows us to count the amount of times it has successfully used the stack's required amount + var/spawning_amount = 0 if(isstack(source)) var/obj/item/stack/stack_we_use = source - if(!stack_we_use.use(required_amount)) - successful = FALSE + while(stack_we_use.amount >= required_amount) + if(!do_after(user, loom_time, target)) + break + + if(!stack_we_use.use(required_amount)) + user.balloon_alert(user, "need [required_amount] of [source]!") + break + + spawning_amount++ + else + if(!do_after(user, loom_time, target)) + user.balloon_alert(user, "interrupted!") + return + qdel(source) + spawning_amount++ - //ripbozo - if(!successful) - user.balloon_alert(user, "need [required_amount] of [source]!") + if(spawning_amount == 0) return - var/new_thing = new resulting_atom(target.drop_location()) + var/new_thing + for(var/repeated in 1 to spawning_amount) + new_thing = new resulting_atom(target.drop_location()) + user.balloon_alert_to_viewers("[process_completion_verb] [new_thing]") diff --git a/code/datums/emotes.dm b/code/datums/emotes.dm index bdc644ea75179..3c0e2a903c23f 100644 --- a/code/datums/emotes.dm +++ b/code/datums/emotes.dm @@ -14,6 +14,8 @@ var/key = "" /// This will also call the emote. var/key_third_person = "" + /// Needed for more user-friendly emote names, so emotes with keys like "aflap" will show as "flap angry". Defaulted to key. + var/name = "" /// Message displayed when emote is used. var/message = "" /// Message displayed if the user is a mime. @@ -71,6 +73,9 @@ mob_type_blacklist_typecache = typecacheof(mob_type_blacklist_typecache) mob_type_ignore_stat_typecache = typecacheof(mob_type_ignore_stat_typecache) + if(!name) + name = key + /** * Handles the modifications and execution of emotes. * diff --git a/code/datums/memory/_memory.dm b/code/datums/memory/_memory.dm index 2b3e250a3fbf2..dd571c85746c8 100644 --- a/code/datums/memory/_memory.dm +++ b/code/datums/memory/_memory.dm @@ -266,6 +266,7 @@ /mob/living/basic/morph, /mob/living/basic/mouse, /mob/living/basic/mushroom, + /mob/living/basic/parrot, /mob/living/basic/pet/dog/breaddog, /mob/living/basic/pet/dog/corgi, /mob/living/basic/pet/dog/pug, @@ -276,7 +277,6 @@ /mob/living/basic/stickman, /mob/living/basic/stickman/dog, /mob/living/simple_animal/hostile/megafauna/dragon/lesser, - /mob/living/simple_animal/parrot, /mob/living/simple_animal/pet/cat, /mob/living/simple_animal/pet/cat/cak, /obj/item/food/sausage/american, diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm index 5101242f42831..fe2c281a3380f 100644 --- a/code/datums/status_effects/buffs.dm +++ b/code/datums/status_effects/buffs.dm @@ -557,7 +557,7 @@ owner.AddElement(/datum/element/forced_gravity, 0) owner.AddElement(/datum/element/simple_flying) owner.add_stun_absorption(source = id, priority = 4) - add_traits(list(TRAIT_IGNOREDAMAGESLOWDOWN, TRAIT_FREE_HYPERSPACE_MOVEMENT), MAD_WIZARD_TRAIT) + owner.add_traits(list(TRAIT_IGNOREDAMAGESLOWDOWN, TRAIT_FREE_HYPERSPACE_MOVEMENT), MAD_WIZARD_TRAIT) owner.playsound_local(get_turf(owner), 'sound/chemistry/ahaha.ogg', vol = 100, vary = TRUE, use_reverb = TRUE) return TRUE @@ -575,7 +575,7 @@ owner.RemoveElement(/datum/element/forced_gravity, 0) owner.RemoveElement(/datum/element/simple_flying) owner.remove_stun_absorption(id) - remove_traits(list(TRAIT_IGNOREDAMAGESLOWDOWN, TRAIT_FREE_HYPERSPACE_MOVEMENT), MAD_WIZARD_TRAIT) + owner.remove_traits(list(TRAIT_IGNOREDAMAGESLOWDOWN, TRAIT_FREE_HYPERSPACE_MOVEMENT), MAD_WIZARD_TRAIT) /// Gives you a brief period of anti-gravity /datum/status_effect/jump_jet diff --git a/code/game/machinery/dna_infuser/organ_sets/fly_organs.dm b/code/game/machinery/dna_infuser/organ_sets/fly_organs.dm index 257831472d601..47d65a9492f04 100644 --- a/code/game/machinery/dna_infuser/organ_sets/fly_organs.dm +++ b/code/game/machinery/dna_infuser/organ_sets/fly_organs.dm @@ -69,10 +69,7 @@ name = odd_organ_name() icon_state = FLY_INFUSED_ORGAN_ICON AddElement(/datum/element/organ_set_bonus, /datum/status_effect/organ_set_bonus/fly) - -/obj/item/organ/internal/heart/fly/update_icon_state() - SHOULD_CALL_PARENT(FALSE) - return //don't set icon thank you + AddElement(/datum/element/update_icon_blocker) /obj/item/organ/internal/lungs/fly desc = FLY_INFUSED_ORGAN_DESC diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 699cac8dccb1a..a93d2b4b56d29 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -421,7 +421,7 @@ ///Separator between the items on the list var/sep = "" ///Nodes that can be boosted - var/list/boostable_nodes = techweb_item_boost_check(src) + var/list/boostable_nodes = techweb_item_unlock_check(src) if (boostable_nodes) for(var/id in boostable_nodes) var/datum/techweb_node/node = SSresearch.techweb_node_by_id(id) @@ -596,9 +596,6 @@ R.activate_module(src) R.hud_used.update_robot_modules_display() -/obj/item/proc/GetDeconstructableContents() - return get_all_contents() - src - // afterattack() and attack() prototypes moved to _onclick/item_attack.dm for consistency /obj/item/proc/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE) diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm index 6f76861ae2174..c052a9c18e181 100644 --- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm +++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm @@ -551,7 +551,7 @@ /obj/item/circuitboard/machine/smartfridge/apply_default_parts(obj/machinery/smartfridge/smartfridge) build_path = smartfridge.base_build_path - if(!fridges_name_paths.Find(build_path, fridges_name_paths)) + if(!fridges_name_paths.Find(build_path)) name = "[initial(smartfridge.name)]" //if it's a unique type, give it a unique name. is_special_type = TRUE return ..() @@ -961,7 +961,8 @@ req_components = list( /datum/stock_part/scanning_module = 1, /datum/stock_part/servo = 1, - /datum/stock_part/micro_laser = 1) + /datum/stock_part/micro_laser = 1, + ) /obj/item/circuitboard/machine/experimentor name = "E.X.P.E.R.I-MENTOR" diff --git a/code/game/objects/items/singularityhammer.dm b/code/game/objects/items/wizard_weapons.dm similarity index 98% rename from code/game/objects/items/singularityhammer.dm rename to code/game/objects/items/wizard_weapons.dm index 50a97c215d850..b16f5be5bfd48 100644 --- a/code/game/objects/items/singularityhammer.dm +++ b/code/game/objects/items/wizard_weapons.dm @@ -77,12 +77,12 @@ return . /obj/item/mjollnir - name = "Mjolnir" + name = "Mjollnir" desc = "A weapon worthy of a god, able to strike with the force of a lightning bolt. It crackles with barely contained energy." icon = 'icons/obj/weapons/hammer.dmi' icon_state = "mjollnir0" base_icon_state = "mjollnir" - worn_icon_state = "mjolnir" + worn_icon_state = "mjollnir" lefthand_file = 'icons/mob/inhands/weapons/hammers_lefthand.dmi' righthand_file = 'icons/mob/inhands/weapons/hammers_righthand.dmi' flags_1 = CONDUCT_1 diff --git a/code/modules/antagonists/abductor/equipment/gland.dm b/code/modules/antagonists/abductor/equipment/gland.dm index c1dcf68b9dfa7..e29388c9fd628 100644 --- a/code/modules/antagonists/abductor/equipment/gland.dm +++ b/code/modules/antagonists/abductor/equipment/gland.dm @@ -4,7 +4,6 @@ icon = 'icons/obj/antags/abductor.dmi' icon_state = "gland" organ_flags = ORGAN_ROBOTIC // weird? - beating = TRUE /// Shows name of the gland as well as a description of what it does upon examination by abductor scientists and observers. var/abductor_hint = "baseline placebo referencer" @@ -26,12 +25,16 @@ /obj/item/organ/internal/heart/gland/Initialize(mapload) . = ..() icon_state = pick(list("health", "spider", "slime", "emp", "species", "egg", "vent", "mindshock", "viral")) + AddElement(/datum/element/update_icon_blocker) /obj/item/organ/internal/heart/gland/examine(mob/user) . = ..() if(HAS_MIND_TRAIT(user, TRAIT_ABDUCTOR_SCIENTIST_TRAINING) || isobserver(user)) . += span_notice("It is \a [abductor_hint]") +/obj/item/organ/internal/heart/gland/Stop() + return FALSE + /obj/item/organ/internal/heart/gland/proc/ownerCheck() if(ishuman(owner)) return TRUE @@ -102,9 +105,6 @@ update_gland_hud() /obj/item/organ/internal/heart/gland/on_life(seconds_per_tick, times_fired) - if(!beating) - // alien glands are immune to stopping. - beating = TRUE if(!active) return if(!ownerCheck()) diff --git a/code/modules/antagonists/nightmare/nightmare_organs.dm b/code/modules/antagonists/nightmare/nightmare_organs.dm index 1e07ddc69417f..ce5ff427a213f 100644 --- a/code/modules/antagonists/nightmare/nightmare_organs.dm +++ b/code/modules/antagonists/nightmare/nightmare_organs.dm @@ -50,8 +50,8 @@ /obj/item/organ/internal/heart/nightmare name = "heart of darkness" desc = "An alien organ that twists and writhes when exposed to light." - icon = 'icons/obj/medical/organs/organs.dmi' icon_state = "demon_heart-on" + base_icon_state = "demon_heart" visual = TRUE color = "#1C1C1C" decay_factor = 0 @@ -60,10 +60,6 @@ /// The armblade granted to the host of this heart. var/obj/item/light_eater/blade -/obj/item/organ/internal/heart/nightmare/Initialize(mapload) - AddElement(/datum/element/update_icon_blocker) - return ..() - /obj/item/organ/internal/heart/nightmare/attack(mob/M, mob/living/carbon/user, obj/target) if(M != user) return ..() @@ -94,7 +90,7 @@ QDEL_NULL(blade) /obj/item/organ/internal/heart/nightmare/Stop() - return 0 + return FALSE /obj/item/organ/internal/heart/nightmare/on_death(seconds_per_tick, times_fired) if(!owner) diff --git a/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm index 3ea6488b2d42d..7521ff155496e 100644 --- a/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm +++ b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm @@ -404,10 +404,10 @@ /datum/export/pirate/parrot cost = 2000 unit_name = "alive parrot" - export_types = list(/mob/living/simple_animal/parrot) + export_types = list(/mob/living/basic/parrot) /datum/export/pirate/parrot/find_loot() - for(var/mob/living/simple_animal/parrot/current_parrot in GLOB.alive_mob_list) + for(var/mob/living/basic/parrot/current_parrot in GLOB.alive_mob_list) var/turf/parrot_turf = get_turf(current_parrot) if(parrot_turf && is_station_level(parrot_turf.z)) return current_parrot diff --git a/code/modules/antagonists/traitor/objectives/kill_pet.dm b/code/modules/antagonists/traitor/objectives/kill_pet.dm index 8ea89cb44d033..01ab042f11b03 100644 --- a/code/modules/antagonists/traitor/objectives/kill_pet.dm +++ b/code/modules/antagonists/traitor/objectives/kill_pet.dm @@ -24,7 +24,7 @@ ), JOB_CAPTAIN = /mob/living/basic/pet/fox/renault, JOB_CHIEF_MEDICAL_OFFICER = /mob/living/simple_animal/pet/cat/runtime, - JOB_CHIEF_ENGINEER = /mob/living/simple_animal/parrot/poly, + JOB_CHIEF_ENGINEER = /mob/living/basic/parrot/poly, JOB_QUARTERMASTER = list( /mob/living/basic/gorilla/cargorilla, /mob/living/basic/sloth/citrus, diff --git a/code/modules/cargo/packs/livestock.dm b/code/modules/cargo/packs/livestock.dm index ef9fb96182302..942b1414cf9f5 100644 --- a/code/modules/cargo/packs/livestock.dm +++ b/code/modules/cargo/packs/livestock.dm @@ -6,13 +6,13 @@ name = "Bird Crate" desc = "Contains five expert telecommunication birds." cost = CARGO_CRATE_VALUE * 8 - contains = list(/mob/living/simple_animal/parrot) + contains = list(/mob/living/basic/parrot) crate_name = "parrot crate" /datum/supply_pack/critter/parrot/generate() . = ..() for(var/i in 1 to 4) - new /mob/living/simple_animal/parrot(.) + new /mob/living/basic/parrot(.) /datum/supply_pack/critter/butterfly name = "Butterflies Crate" diff --git a/code/modules/clothing/neck/_neck.dm b/code/modules/clothing/neck/_neck.dm index 947128213f261..2f9a7727d2fc0 100644 --- a/code/modules/clothing/neck/_neck.dm +++ b/code/modules/clothing/neck/_neck.dm @@ -237,7 +237,7 @@ //assess heart if(body_part == BODY_ZONE_CHEST)//if we're listening to the chest - if(isnull(heart) || !heart.beating || carbon_patient.stat == DEAD) + if(isnull(heart) || !heart.is_beating() || carbon_patient.stat == DEAD) render_list += "You don't hear a heartbeat!\n"//they're dead or their heart isn't beating else if(heart.damage > 10 || carbon_patient.blood_volume <= BLOOD_VOLUME_OKAY) render_list += "You hear a weak heartbeat.\n"//their heart is damaged, or they have critical blood @@ -289,7 +289,7 @@ user.visible_message(span_notice("[user] presses their fingers against [carbon_patient]'s [body_part]."), ignored_mobs = user) //assess pulse (heart & blood level) - if(isnull(heart) || !heart.beating || carbon_patient.blood_volume <= BLOOD_VOLUME_OKAY || carbon_patient.stat == DEAD) + if(isnull(heart) || !heart.is_beating() || carbon_patient.blood_volume <= BLOOD_VOLUME_OKAY || carbon_patient.stat == DEAD) render_list += "You can't find a pulse!\n"//they're dead, their heart isn't beating, or they have critical blood else if(heart.damage > 10) diff --git a/code/modules/emote_panel/emote_panel.dm b/code/modules/emote_panel/emote_panel.dm new file mode 100644 index 0000000000000..72caf05e92b27 --- /dev/null +++ b/code/modules/emote_panel/emote_panel.dm @@ -0,0 +1,64 @@ +/datum/emote_panel + var/list/blacklisted_emotes = list("me", "help") + +/datum/emote_panel/ui_static_data(mob/user) + var/list/data = list() + + var/list/emotes = list() + var/list/keys = list() + + for(var/key in GLOB.emote_list) + for(var/datum/emote/emote in GLOB.emote_list[key]) + if(emote.key in keys) + continue + if(emote.key in blacklisted_emotes) + continue + if(emote.can_run_emote(user, status_check = FALSE, intentional = FALSE)) + keys += emote.key + emotes += list(list( + "key" = emote.key, + "name" = emote.name, + "hands" = emote.hands_use_check, + "visible" = emote.emote_type & EMOTE_VISIBLE, + "audible" = emote.emote_type & EMOTE_AUDIBLE, + "sound" = !isnull(emote.get_sound(user)), + "use_params" = emote.message_param, + )) + + data["emotes"] = emotes + + return data + +/datum/emote_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + switch(action) + if("play_emote") + var/emote_key = params["emote_key"] + if(isnull(emote_key) || !GLOB.emote_list[emote_key]) + return + var/use_params = params["use_params"] + var/datum/emote/emote = GLOB.emote_list[emote_key][1] + var/emote_param + if(emote.message_param && use_params) + emote_param = tgui_input_text(ui.user, "Add params to the emote...", emote.message_param) + ui.user.emote(emote_key, message = emote_param, intentional = TRUE) + +/datum/emote_panel/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "EmotePanel") + ui.open() + +/datum/emote_panel/ui_state(mob/user) + return GLOB.always_state + +/mob/living/verb/emote_panel() + set name = "Emote Panel" + set category = "IC" + + var/static/datum/emote_panel/emote_panel + if(isnull(emote_panel)) + emote_panel = new + emote_panel.ui_interact(src) diff --git a/code/modules/events/ghost_role/sentience.dm b/code/modules/events/ghost_role/sentience.dm index abc57d33a0758..f33333ddb1bd0 100644 --- a/code/modules/events/ghost_role/sentience.dm +++ b/code/modules/events/ghost_role/sentience.dm @@ -7,6 +7,7 @@ GLOBAL_LIST_INIT(high_priority_sentience, typecacheof(list( /mob/living/basic/goat, /mob/living/basic/lizard, /mob/living/basic/mouse/brown/tom, + /mob/living/basic/parrot, /mob/living/basic/pet, /mob/living/basic/pig, /mob/living/basic/rabbit, @@ -16,7 +17,6 @@ GLOBAL_LIST_INIT(high_priority_sentience, typecacheof(list( /mob/living/basic/spider/giant/sgt_araneus, /mob/living/simple_animal/bot/secbot/beepsky, /mob/living/simple_animal/hostile/retaliate/goose/vomit, - /mob/living/simple_animal/parrot, /mob/living/simple_animal/pet, ))) diff --git a/code/modules/events/holiday/halloween.dm b/code/modules/events/holiday/halloween.dm index d52dd1b9c508a..8eb29fa706c45 100644 --- a/code/modules/events/holiday/halloween.dm +++ b/code/modules/events/holiday/halloween.dm @@ -18,9 +18,9 @@ for(var/mob/living/basic/pet/dog/corgi/ian/Ian in GLOB.mob_living_list) Ian.place_on_head(new /obj/item/bedsheet(Ian)) - for(var/mob/living/simple_animal/parrot/poly/Poly in GLOB.mob_living_list) - new /mob/living/simple_animal/parrot/poly/ghost(Poly.loc) - qdel(Poly) + for(var/mob/living/basic/parrot/poly/bird in GLOB.mob_living_list) + new /mob/living/basic/parrot/poly/ghost(bird.loc) + qdel(bird) /datum/round_event/spooky/announce(fake) priority_announce(pick("RATTLE ME BONES!","THE RIDE NEVER ENDS!", "A SKELETON POPS OUT!", "SPOOKY SCARY SKELETONS!", "CREWMEMBERS BEWARE, YOU'RE IN FOR A SCARE!") , "THE CALL IS COMING FROM INSIDE THE HOUSE") diff --git a/code/modules/events/wizard/petsplosion.dm b/code/modules/events/wizard/petsplosion.dm index e670fa910a1f0..9de7fffcc3aea 100644 --- a/code/modules/events/wizard/petsplosion.dm +++ b/code/modules/events/wizard/petsplosion.dm @@ -9,6 +9,7 @@ GLOBAL_LIST_INIT(petsplosion_candidates, typecacheof(list( /mob/living/basic/lizard, /mob/living/basic/mothroach, /mob/living/basic/mouse/brown/tom, + /mob/living/basic/parrot, /mob/living/basic/pet, /mob/living/basic/pig, /mob/living/basic/rabbit, @@ -17,7 +18,6 @@ GLOBAL_LIST_INIT(petsplosion_candidates, typecacheof(list( /mob/living/basic/snake, /mob/living/basic/spider/giant/sgt_araneus, /mob/living/simple_animal/hostile/retaliate/goose/vomit, - /mob/living/simple_animal/parrot, /mob/living/simple_animal/pet, ))) diff --git a/code/modules/food_and_drinks/machinery/smartfridge.dm b/code/modules/food_and_drinks/machinery/smartfridge.dm index 37018f7dd5b49..30b9cf84070d7 100644 --- a/code/modules/food_and_drinks/machinery/smartfridge.dm +++ b/code/modules/food_and_drinks/machinery/smartfridge.dm @@ -17,8 +17,8 @@ var/base_build_path = /obj/machinery/smartfridge /// Maximum number of items that can be loaded into the machine var/max_n_of_items = 1500 - /// If the AI is allowed to retrieve items within the machine - var/allow_ai_retrieve = FALSE + /// The overlay for this fridge when it is filled with stuff + var/contents_icon_state = "plant" /// List of items that the machine starts with upon spawn var/list/initial_contents /// If the machine shows an approximate number of its contents on its sprite @@ -33,7 +33,7 @@ create_reagents(100, NO_REACT) air_update_turf(TRUE, TRUE) register_context() - if(mapload && !istype(src, /obj/machinery/smartfridge/drying_rack)) + if(mapload) welded_down = TRUE if(islist(initial_contents)) @@ -49,83 +49,115 @@ . = ..() move_update_air(old_loc) -/obj/machinery/smartfridge/can_be_unfasten_wrench(mob/user, silent) - if(welded_down) - to_chat(user, span_warning("[src] is welded to the floor!")) - return FAILED_UNFASTEN - return ..() - -/obj/machinery/smartfridge/set_anchored(anchorvalue) - . = ..() - if(!anchored && welded_down) //make sure they're keep in sync in case it was forcibly unanchored by badmins or by a megafauna. - welded_down = FALSE - can_atmos_pass = anchorvalue ? ATMOS_PASS_NO : ATMOS_PASS_YES - air_update_turf(TRUE, anchorvalue) - /obj/machinery/smartfridge/welder_act(mob/living/user, obj/item/tool) - ..() - if(istype(src, /obj/machinery/smartfridge/drying_rack)) - return FALSE + . = TOOL_ACT_TOOLTYPE_SUCCESS + if(welded_down) if(!tool.tool_start_check(user, amount=2)) - return TRUE + return + user.visible_message( span_notice("[user.name] starts to cut the [name] free from the floor."), span_notice("You start to cut [src] free from the floor..."), span_hear("You hear welding."), ) + if(!tool.use_tool(src, user, delay=100, volume=100)) - return FALSE + return + welded_down = FALSE to_chat(user, span_notice("You cut [src] free from the floor.")) - return TRUE + return + if(!anchored) - to_chat(user, span_warning("[src] needs to be wrenched to the floor!")) - return TRUE + balloon_alert(user, "wrench it first!") + return + if(!tool.tool_start_check(user, amount=2)) - return TRUE + return + user.visible_message( span_notice("[user.name] starts to weld the [name] to the floor."), span_notice("You start to weld [src] to the floor..."), span_hear("You hear welding."), ) - if(!tool.use_tool(src, user, delay=100, volume=100)) - balloon_alert(user, "cancelled!") - return FALSE + + if(!tool.use_tool(src, user, delay = 100, volume = 100)) + return + welded_down = TRUE to_chat(user, span_notice("You weld [src] to the floor.")) - return TRUE /obj/machinery/smartfridge/welder_act_secondary(mob/living/user, obj/item/tool) + . = TOOL_ACT_TOOLTYPE_SUCCESS + + if(!(machine_stat & BROKEN)) + balloon_alert(user, "no repair needed!") + return + + if(!tool.tool_start_check(user, amount=1)) + return + + user.visible_message( + span_notice("[user] is repairing [src]."), + span_notice("You begin repairing [src]..."), + span_hear("You hear welding."), + ) + + if(tool.use_tool(src, user, delay = 40, volume = 50)) + if(!(machine_stat & BROKEN)) + return + to_chat(user, span_notice("You repair [src]")) + atom_integrity = max_integrity + set_machine_stat(machine_stat & ~BROKEN) + update_icon() + +/obj/machinery/smartfridge/screwdriver_act(mob/living/user, obj/item/tool) + . = TOOL_ACT_TOOLTYPE_SUCCESS + + if(default_deconstruction_screwdriver(user, icon_state, icon_state, tool)) + if(panel_open) + add_overlay("[initial(icon_state)]-panel") + else + cut_overlay("[initial(icon_state)]-panel") + SStgui.update_uis(src) + +/obj/machinery/smartfridge/can_be_unfasten_wrench(mob/user, silent) + if(welded_down) + balloon_alert(user, "unweld first!") + return FAILED_UNFASTEN + return ..() + +/obj/machinery/smartfridge/set_anchored(anchorvalue) . = ..() - if(istype(src, /obj/machinery/smartfridge/drying_rack)) - return FALSE - if(machine_stat & BROKEN) - if(!tool.tool_start_check(user, amount=1)) - return FALSE - user.visible_message( - span_notice("[user] is repairing [src]."), - span_notice("You begin repairing [src]..."), - span_hear("You hear welding."), - ) - if(tool.use_tool(src, user, delay=40, volume=50)) - if(!(machine_stat & BROKEN)) - return FALSE - balloon_alert(user, "repaired") - atom_integrity = max_integrity - set_machine_stat(machine_stat & ~BROKEN) - update_icon() - return TRUE + if(!anchored && welded_down) //make sure they're keep in sync in case it was forcibly unanchored by badmins or by a megafauna. + welded_down = FALSE + can_atmos_pass = anchorvalue ? ATMOS_PASS_NO : ATMOS_PASS_YES + air_update_turf(TRUE, anchorvalue) + +/obj/machinery/smartfridge/wrench_act(mob/living/user, obj/item/tool) + . = TOOL_ACT_TOOLTYPE_SUCCESS + + if(default_unfasten_wrench(user, tool) == SUCCESSFUL_UNFASTEN) + power_change() + +/obj/machinery/smartfridge/crowbar_act(mob/living/user, obj/item/tool) + . = TOOL_ACT_TOOLTYPE_SUCCESS + + if(default_pry_open(tool, close_after_pry = TRUE)) + return + + if(welded_down) + balloon_alert(user, "unweld first!") else - balloon_alert(user, "no repair needed!") - return FALSE + default_deconstruction_crowbar(tool) /obj/machinery/smartfridge/add_context(atom/source, list/context, obj/item/held_item, mob/living/user) if(isnull(held_item)) return NONE var/tool_tip_set = FALSE - if(held_item.tool_behaviour == TOOL_WELDER && !istype(src, /obj/machinery/smartfridge/drying_rack)) + if(held_item.tool_behaviour == TOOL_WELDER) if(welded_down) context[SCREENTIP_CONTEXT_LMB] = "Unweld" tool_tip_set = TRUE @@ -136,6 +168,19 @@ context[SCREENTIP_CONTEXT_RMB] = "Repair" tool_tip_set = TRUE + else if(held_item.tool_behaviour == TOOL_SCREWDRIVER) + context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "close" : "open"] panel" + tool_tip_set = TRUE + + else if(held_item.tool_behaviour == TOOL_CROWBAR) + if(panel_open) + context[SCREENTIP_CONTEXT_LMB] = "Deconstruct" + tool_tip_set = TRUE + + else if(held_item.tool_behaviour == TOOL_WRENCH) + context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Un" : ""]anchore" + tool_tip_set = TRUE + return tool_tip_set ? CONTEXTUAL_SCREENTIP_SET : NONE /obj/machinery/smartfridge/RefreshParts() @@ -149,19 +194,26 @@ if(in_range(user, src) || isobserver(user)) . += span_notice("The status display reads: This unit can hold a maximum of [max_n_of_items] items.") + . += structure_examine() + +/// Returns details related to the fridge structure +/obj/machinery/smartfridge/proc/structure_examine() + . = "" + if(welded_down) - . += span_info("It's moored firmly to the floor. You can unsecure its moorings with a welder.") - else if(anchored) - . += span_info("It's currently anchored to the floor. You can secure its moorings with a welder, or remove it with a wrench.") + . += span_info("It's moorings are firmly [EXAMINE_HINT("welded")] to the floor.") + else + . += span_info("It's moorings are loose and can be [EXAMINE_HINT("welded")] down.") + + if(anchored) + . += span_info("It is [EXAMINE_HINT("wrenched")] down on the floor.") else - . += span_info("It's not anchored to the floor. You can secure it in place with a wrench.") + . += span_info("It could be [EXAMINE_HINT("wrenched")] down.") /obj/machinery/smartfridge/update_appearance(updates=ALL) . = ..() - if(machine_stat & BROKEN) - set_light(0) - return - set_light(powered() ? MINIMUM_USEFUL_LIGHT_RANGE : 0) + + set_light((!(machine_stat & BROKEN) && powered()) ? MINIMUM_USEFUL_LIGHT_RANGE : 0) /obj/machinery/smartfridge/update_icon_state() icon_state = "[initial(icon_state)]" @@ -171,49 +223,33 @@ icon_state += "-off" return ..() +/// Returns the number of items visible in the fridge. Faster than subtracting 2 lists +/obj/machinery/smartfridge/proc/visible_items() + var/component_part_count = 0 + for(var/datum/stock_part/datum_part in component_parts) + component_part_count -= 1 + return contents.len - component_part_count + /obj/machinery/smartfridge/update_overlays() . = ..() - var/list/shown_contents = contents - component_parts - if(visible_contents && shown_contents.len > 0) - var/contents_icon_state = "[initial(icon_state)]" - switch(base_build_path) - if(/obj/machinery/smartfridge/extract) - contents_icon_state += "-slime" - if(/obj/machinery/smartfridge/food) - contents_icon_state += "-food" - if(/obj/machinery/smartfridge/drinks) - contents_icon_state += "-drink" - if(/obj/machinery/smartfridge/organ) - contents_icon_state += "-organ" - if(/obj/machinery/smartfridge/petri) - contents_icon_state += "-petri" - if(/obj/machinery/smartfridge/chemistry) - contents_icon_state += "-chem" - if(/obj/machinery/smartfridge/chemistry/virology) - contents_icon_state += "-viro" - else - contents_icon_state += "-plant" - switch(shown_contents.len) + var/shown_contents_length = visible_items() + if(visible_contents && shown_contents_length) + var/content_level = "[initial(icon_state)]-[contents_icon_state]" + switch(shown_contents_length) if(1 to 25) - contents_icon_state += "-1" + content_level += "-1" if(26 to 50) - contents_icon_state += "-2" + content_level += "-2" if(31 to INFINITY) - contents_icon_state += "-3" - . += mutable_appearance(icon, contents_icon_state) + content_level += "-3" + . += mutable_appearance(icon, content_level) . += mutable_appearance(icon, "[initial(icon_state)]-glass[(machine_stat & BROKEN) ? "-broken" : ""]") if(!machine_stat && has_emissive) . += emissive_appearance(icon, "[initial(icon_state)]-light-mask", src, alpha = src.alpha) -/obj/machinery/smartfridge/wrench_act(mob/living/user, obj/item/tool) - . = ..() - if(default_unfasten_wrench(user, tool)) - power_change() - return TOOL_ACT_TOOLTYPE_SUCCESS - /obj/machinery/smartfridge/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0) switch(damage_type) if(BRUTE) @@ -225,101 +261,94 @@ playsound(src, SFX_SHATTER, 50, TRUE) return ..() -/******************* -* Item Adding -********************/ - -/obj/machinery/smartfridge/attackby(obj/item/O, mob/living/user, params) - if(default_deconstruction_screwdriver(user, icon_state, icon_state, O)) - if(panel_open) - add_overlay("[initial(icon_state)]-panel") - else - cut_overlay("[initial(icon_state)]-panel") - SStgui.update_uis(src) - return - - if(default_pry_open(O, close_after_pry = TRUE)) - return - - if(!welded_down && default_deconstruction_crowbar(O)) - SStgui.update_uis(src) - return - +/obj/machinery/smartfridge/attackby(obj/item/weapon, mob/living/user, params) if(!machine_stat) - var/list/shown_contents = contents - component_parts - if(shown_contents.len >= max_n_of_items) - to_chat(user, span_warning("\The [src] is full!")) + var/shown_contents_length = visible_items() + if(shown_contents_length >= max_n_of_items) + balloon_alert(user, "no space!") return FALSE - if(accept_check(O)) - load(O) - user.visible_message(span_notice("[user] adds \the [O] to \the [src]."), span_notice("You add \the [O] to \the [src].")) + if(!(weapon.item_flags & ABSTRACT) && \ + !(weapon.flags_1 & HOLOGRAM_1) && \ + accept_check(weapon) \ + ) + load(weapon) + user.visible_message(span_notice("[user] adds \the [weapon] to \the [src]."), span_notice("You add \the [weapon] to \the [src].")) SStgui.update_uis(src) if(visible_contents) update_appearance() return TRUE - if(istype(O, /obj/item/storage/bag)) - var/obj/item/storage/P = O + if(istype(weapon, /obj/item/storage/bag)) + var/obj/item/storage/bag = weapon var/loaded = 0 - for(var/obj/G in P.contents) - if(shown_contents.len >= max_n_of_items) + for(var/obj/item/object in bag.contents) + if(shown_contents_length >= max_n_of_items) break - if(accept_check(G)) - load(G) + if(!(object.item_flags & ABSTRACT) && \ + !(object.flags_1 & HOLOGRAM_1) && \ + accept_check(object) \ + ) + load(object) loaded++ SStgui.update_uis(src) if(loaded) - if(shown_contents.len >= max_n_of_items) - user.visible_message(span_notice("[user] loads \the [src] with \the [O]."), \ - span_notice("You fill \the [src] with \the [O].")) + if(shown_contents_length >= max_n_of_items) + user.visible_message(span_notice("[user] loads \the [src] with \the [weapon]."), \ + span_notice("You fill \the [src] with \the [weapon].")) else - user.visible_message(span_notice("[user] loads \the [src] with \the [O]."), \ - span_notice("You load \the [src] with \the [O].")) - if(O.contents.len > 0) + user.visible_message(span_notice("[user] loads \the [src] with \the [weapon]."), \ + span_notice("You load \the [src] with \the [weapon].")) + if(weapon.contents.len) to_chat(user, span_warning("Some items are refused.")) if (visible_contents) update_appearance() return TRUE else - to_chat(user, span_warning("There is nothing in [O] to put in [src]!")) + to_chat(user, span_warning("There is nothing in [weapon] to put in [src]!")) return FALSE if(!user.combat_mode) - to_chat(user, span_warning("\The [src] smartly refuses [O].")) - SStgui.update_uis(src) + to_chat(user, span_warning("\The [src] smartly refuses [weapon].")) return FALSE + else return ..() -/obj/machinery/smartfridge/proc/accept_check(obj/item/O) - if(istype(O, /obj/item/food/grown/) || istype(O, /obj/item/seeds/) || istype(O, /obj/item/grown/) || istype(O, /obj/item/graft/)) - return TRUE - return FALSE - -/obj/machinery/smartfridge/proc/load(obj/item/O) - if(ismob(O.loc)) - var/mob/M = O.loc - if(!M.transferItemToLoc(O, src)) - to_chat(usr, span_warning("\the [O] is stuck to your hand, you cannot put it in \the [src]!")) +/** + * Can this item be accepted by the smart fridge + * Arguments + * * [weapon][obj/item] - the item to accept + */ +/obj/machinery/smartfridge/proc/accept_check(obj/item/weapon) + var/static/list/accepted_items = list( + /obj/item/food/grown, + /obj/item/seeds, + /obj/item/grown, + /obj/item/graft, + ) + return is_type_in_list(weapon, accepted_items) + +/** + * Loads the item into the smart fridge + * Arguments + * * [weapon][obj/item] - the item to load. If the item is being held by a mo it will transfer it from hand else directly force move + */ +/obj/machinery/smartfridge/proc/load(obj/item/weapon) + if(ismob(weapon.loc)) + var/mob/owner = weapon.loc + if(!owner.transferItemToLoc(weapon, src)) + to_chat(usr, span_warning("\the [weapon] is stuck to your hand, you cannot put it in \the [src]!")) return FALSE - else - return TRUE + return TRUE else - if(O.loc.atom_storage) - return O.loc.atom_storage.attempt_remove(O, src, silent = TRUE) + if(weapon.loc.atom_storage) + return weapon.loc.atom_storage.attempt_remove(weapon, src, silent = TRUE) else - O.forceMove(src) + weapon.forceMove(src) return TRUE -///Really simple proc, just moves the object "O" into the hands of mob "M" if able, done so I could modify the proc a little for the organ fridge -/obj/machinery/smartfridge/proc/dispense(obj/item/O, mob/M) - if(!M.put_in_hands(O)) - O.forceMove(drop_location()) - adjust_item_drop_location(O) - use_power(active_power_usage) - /obj/machinery/smartfridge/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) @@ -331,18 +360,18 @@ . = list() var/listofitems = list() - for (var/I in src) + for (var/item in src) // We do not vend our own components. - if(I in component_parts) + if(item in component_parts) continue - var/atom/movable/O = I - if (!QDELETED(O)) - var/md5name = md5(O.name) // This needs to happen because of a bug in a TGUI component, https://github.com/ractivejs/ractive/issues/744 + var/atom/movable/atom = item + if (!QDELETED(atom)) + var/md5name = md5(atom.name) // This needs to happen because of a bug in a TGUI component, https://github.com/ractivejs/ractive/issues/744 if (listofitems[md5name]) // which is fixed in a version we cannot use due to ie8 incompatibility listofitems[md5name]["amount"]++ // The good news is, #30519 made smartfridge UIs non-auto-updating else - listofitems[md5name] = list("name" = O.name, "type" = O.type, "amount" = 1) + listofitems[md5name] = list("name" = atom.name, "amount" = 1) sort_list(listofitems) .["contents"] = listofitems @@ -353,27 +382,28 @@ . = ..() SStgui.update_uis(src) -/obj/machinery/smartfridge/ui_act(action, params) +/obj/machinery/smartfridge/ui_act(action, params, datum/tgui/ui, datum/ui_state/state) . = ..() - if(.) + if(. || !ui.user.can_perform_action(src, FORBID_TELEKINESIS_REACH)) return + + . = TRUE + var/mob/living_mob = ui.user + switch(action) if("Release") var/desired = 0 - if(!allow_ai_retrieve && isAI(usr)) - to_chat(usr, span_warning("[src] does not seem to be configured to respect your authority!")) + if(isAI(living_mob)) + to_chat(living_mob, span_warning("[src] does not respect your authority!")) return if (params["amount"]) desired = text2num(params["amount"]) else - desired = tgui_input_number(usr, "How many items would you like to take out?", "Release", max_value = 50) + desired = tgui_input_number(living_mob, "How many items would you like to take out?", "Release", max_value = 50) if(!desired) - return FALSE - - if(QDELETED(src) || QDELETED(usr) || !usr.can_perform_action(src, FORBID_TELEKINESIS_REACH)) // Sanity checkin' in case stupid stuff happens while we wait for input() - return FALSE + return for(var/obj/item/dispensed_item in src) if(desired <= 0) @@ -384,12 +414,16 @@ if(format_text(dispensed_item.name) == format_text(params["name"])) if(dispensed_item in component_parts) CRASH("Attempted removal of [dispensed_item] component_part from smartfridge via smartfridge interface.") - dispense(dispensed_item, usr) + //dispense the item + if(!living_mob.put_in_hands(dispensed_item)) + dispensed_item.forceMove(drop_location()) + adjust_item_drop_location(dispensed_item) + use_power(active_power_usage) desired-- if (visible_contents) update_appearance() - return TRUE + return return FALSE @@ -408,48 +442,72 @@ idle_power_usage = 0 has_emissive = FALSE can_atmos_pass = ATMOS_PASS_YES + /// Is the rack currently drying stuff var/drying = FALSE -/obj/machinery/smartfridge/drying_rack/on_deconstruction() - new /obj/item/stack/sheet/mineral/wood(drop_location(), 10) +/obj/machinery/smartfridge/drying_rack/Initialize(mapload) + . = ..() - //remove all component parts inherited from smartfridge cause they were not required in crafting - var/obj/item/circuitboard/machine/smartfridge/board = locate() in component_parts - component_parts -= board - qdel(board) - component_parts.Cut() + //you can't weld down wood + welded_down = FALSE - return ..() + //so we don't drop any of the parent smart fridge parts upon deconstruction + clear_components() +/// We cleared out the components in initialize so we can optimize this +/obj/machinery/smartfridge/drying_rack/visible_items() + return contents.len + +/obj/machinery/smartfridge/drying_rack/add_context(atom/source, list/context, obj/item/held_item, mob/living/user) + if(isnull(held_item)) + return NONE + + var/tool_tip_set = FALSE + if(held_item.tool_behaviour == TOOL_CROWBAR) + context[SCREENTIP_CONTEXT_LMB] = "Deconstruct" + tool_tip_set = TRUE + else if(held_item.tool_behaviour == TOOL_WRENCH) + context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Un" : ""]anchore" + tool_tip_set = TRUE + + return tool_tip_set ? CONTEXTUAL_SCREENTIP_SET : NONE + +/obj/machinery/smartfridge/drying_rack/structure_examine() + . = "" + if(anchored) + . += span_info("It's currently anchored to the floor. It can be [EXAMINE_HINT("wrenched")] loose.") + else + . += span_info("It's not anchored to the floor. It can be [EXAMINE_HINT("wrenched")] down.") + . += span_info("The whole rack can be [EXAMINE_HINT("pried")] apart.") +/obj/machinery/smartfridge/drying_rack/welder_act(mob/living/user, obj/item/tool) +/obj/machinery/smartfridge/drying_rack/welder_act_secondary(mob/living/user, obj/item/tool) /obj/machinery/smartfridge/drying_rack/default_deconstruction_screwdriver() /obj/machinery/smartfridge/drying_rack/exchange_parts() -/obj/machinery/smartfridge/drying_rack/spawn_frame() +/obj/machinery/smartfridge/drying_rack/on_deconstruction() + new /obj/item/stack/sheet/mineral/wood(drop_location(), 10) +/obj/machinery/smartfridge/drying_rack/crowbar_act(mob/living/user, obj/item/tool) + . = TOOL_ACT_TOOLTYPE_SUCCESS -/obj/machinery/smartfridge/drying_rack/default_deconstruction_crowbar(obj/item/crowbar/C, ignore_panel = 1) - ..() + default_deconstruction_crowbar(tool, ignore_panel = TRUE) /obj/machinery/smartfridge/drying_rack/ui_data(mob/user) . = ..() .["isdryer"] = TRUE - .["verb"] = "Take" .["drying"] = drying - /obj/machinery/smartfridge/drying_rack/ui_act(action, params) . = ..() if(.) update_appearance() // This is to handle a case where the last item is taken out manually instead of through drying pop-out return + switch(action) if("Dry") toggle_drying(FALSE) return TRUE - return FALSE /obj/machinery/smartfridge/drying_rack/powered() - if(!anchored) - return FALSE - return ..() + return !anchored ? FALSE : ..() /obj/machinery/smartfridge/drying_rack/power_change() . = ..() @@ -464,12 +522,10 @@ . = ..() if(drying) . += "drying_rack_drying" - var/list/shown_contents = contents - component_parts - if(shown_contents.len) + if(contents.len) . += "drying_rack_filled" /obj/machinery/smartfridge/drying_rack/process() - ..() if(drying) for(var/obj/item/item_iterator in src) if(!accept_check(item_iterator)) @@ -481,10 +537,13 @@ use_power(active_power_usage) /obj/machinery/smartfridge/drying_rack/accept_check(obj/item/O) - if(HAS_TRAIT(O, TRAIT_DRYABLE)) //set on dryable element - return TRUE - return FALSE + return HAS_TRAIT(O, TRAIT_DRYABLE) +/** + * Toggles drying on or off + * Arguments + * * forceoff - if TRUE will force the dryer off always + */ /obj/machinery/smartfridge/drying_rack/proc/toggle_drying(forceoff) if(drying || forceoff) drying = FALSE @@ -503,7 +562,6 @@ return atmos_spawn_air("[TURF_TEMPERATURE(1000)]") - // ---------------------------- // Bar drink smartfridge // ---------------------------- @@ -511,12 +569,19 @@ name = "drink showcase" desc = "A refrigerated storage unit for tasty tasty alcohol." base_build_path = /obj/machinery/smartfridge/drinks + contents_icon_state = "drink" -/obj/machinery/smartfridge/drinks/accept_check(obj/item/O) - if(!is_reagent_container(O) || (O.item_flags & ABSTRACT) || istype(O,/obj/item/reagent_containers/cup/bowl) || !O.reagents || !O.reagents.reagent_list.len) +/obj/machinery/smartfridge/drinks/accept_check(obj/item/weapon) + //not an item or valid container + if(!is_reagent_container(weapon)) return FALSE - if(istype(O, /obj/item/reagent_containers/cup) || istype(O, /obj/item/reagent_containers/cup/glass) || istype(O, /obj/item/reagent_containers/condiment)) - return TRUE + + //an bowl or something that has no reagents + if(istype(weapon,/obj/item/reagent_containers/cup/bowl) || !length(weapon.reagents?.reagent_list)) + return FALSE + + //list of items acceptable + return (istype(weapon, /obj/item/reagent_containers/cup) || istype(weapon, /obj/item/reagent_containers/condiment)) // ---------------------------- // Food smartfridge @@ -524,11 +589,10 @@ /obj/machinery/smartfridge/food desc = "A refrigerated storage unit for food." base_build_path = /obj/machinery/smartfridge/food + contents_icon_state = "food" -/obj/machinery/smartfridge/food/accept_check(obj/item/O) - if(IS_EDIBLE(O) || (istype(O,/obj/item/reagent_containers/cup/bowl) && O.reagents && O.reagents.reagent_list.len)) - return TRUE - return FALSE +/obj/machinery/smartfridge/food/accept_check(obj/item/weapon) + return (IS_EDIBLE(weapon) || (istype(weapon,/obj/item/reagent_containers/cup/bowl) && length(weapon.reagents?.reagent_list))) // ------------------------------------- // Xenobiology Slime-Extract Smartfridge @@ -537,13 +601,10 @@ name = "smart slime extract storage" desc = "A refrigerated storage unit for slime extracts." base_build_path = /obj/machinery/smartfridge/extract + contents_icon_state = "slime" -/obj/machinery/smartfridge/extract/accept_check(obj/item/O) - if(istype(O, /obj/item/slime_extract)) - return TRUE - if(istype(O, /obj/item/slime_scanner)) - return TRUE - return FALSE +/obj/machinery/smartfridge/extract/accept_check(obj/item/weapon) + return (istype(weapon, /obj/item/slime_extract) || istype(weapon, /obj/item/slime_scanner)) /obj/machinery/smartfridge/extract/preloaded initial_contents = list(/obj/item/slime_scanner = 2) @@ -555,11 +616,10 @@ name = "smart petri dish storage" desc = "A refrigerated storage unit for petri dishes." base_build_path = /obj/machinery/smartfridge/petri + contents_icon_state = "petri" -/obj/machinery/smartfridge/petri/accept_check(obj/item/O) - if(istype(O, /obj/item/petri_dish)) - return TRUE - return FALSE +/obj/machinery/smartfridge/petri/accept_check(obj/item/weapon) + return istype(weapon, /obj/item/petri_dish) /obj/machinery/smartfridge/petri/preloaded initial_contents = list(/obj/item/petri_dish = 5) @@ -572,20 +632,22 @@ desc = "A refrigerated storage unit for organ storage." max_n_of_items = 20 //vastly lower to prevent processing too long base_build_path = /obj/machinery/smartfridge/organ + contents_icon_state = "organ" + /// The rate at which this fridge will repair damaged organs var/repair_rate = 0 /obj/machinery/smartfridge/organ/accept_check(obj/item/O) - if(isorgan(O) || isbodypart(O)) - return TRUE - return FALSE + return (isorgan(O) || isbodypart(O)) /obj/machinery/smartfridge/organ/load(obj/item/O) . = ..() if(!.) //if the item loads, clear can_decompose return + if(isorgan(O)) var/obj/item/organ/organ = O organ.organ_flags |= ORGAN_FROZEN + if(isbodypart(O)) var/obj/item/bodypart/bodypart = O for(var/obj/item/organ/stored in bodypart.contents) @@ -606,9 +668,11 @@ /obj/machinery/smartfridge/organ/Exited(atom/movable/gone, direction) . = ..() + if(isorgan(gone)) var/obj/item/organ/O = gone O.organ_flags &= ~ORGAN_FROZEN + if(isbodypart(gone)) var/obj/item/bodypart/bodypart = gone for(var/obj/item/organ/stored in bodypart.contents) @@ -621,8 +685,31 @@ name = "smart chemical storage" desc = "A refrigerated storage unit for medicine storage." base_build_path = /obj/machinery/smartfridge/chemistry + contents_icon_state = "chem" + +/obj/machinery/smartfridge/chemistry/accept_check(obj/item/weapon) + // not an item or reagent container + if(!is_reagent_container(weapon)) + return FALSE + + // empty pill prank ok + if(istype(weapon, /obj/item/reagent_containers/pill)) + return TRUE + + //check each pill in the pill bottle + if(istype(weapon, /obj/item/storage/pill_bottle)) + if(weapon.contents.len) + for(var/obj/item/target_item in weapon) + if(!accept_check(target_item)) + return FALSE + return TRUE + return FALSE -/obj/machinery/smartfridge/chemistry/accept_check(obj/item/O) + // other empty containers not accepted + if(!length(weapon.reagents?.reagent_list)) + return FALSE + + // the long list of other containers that can be accepted var/static/list/chemfridge_typecache = typecacheof(list( /obj/item/reagent_containers/syringe, /obj/item/reagent_containers/cup/tube, @@ -632,23 +719,7 @@ /obj/item/reagent_containers/medigel, /obj/item/reagent_containers/chem_pack )) - - if(istype(O, /obj/item/storage/pill_bottle)) - if(O.contents.len) - for(var/obj/item/I in O) - if(!accept_check(I)) - return FALSE - return TRUE - return FALSE - if(!is_reagent_container(O) || (O.item_flags & ABSTRACT)) - return FALSE - if(istype(O, /obj/item/reagent_containers/pill)) // empty pill prank ok - return TRUE - if(!O.reagents || !O.reagents.reagent_list.len) // other empty containers not accepted - return FALSE - if(is_type_in_typecache(O, chemfridge_typecache)) - return TRUE - return FALSE + return is_type_in_typecache(weapon, chemfridge_typecache) /obj/machinery/smartfridge/chemistry/preloaded initial_contents = list( @@ -664,6 +735,7 @@ name = "smart virus storage" desc = "A refrigerated storage unit for volatile sample storage." base_build_path = /obj/machinery/smartfridge/chemistry/virology + contents_icon_state = "viro" /obj/machinery/smartfridge/chemistry/virology/preloaded initial_contents = list( @@ -690,8 +762,5 @@ visible_contents = FALSE base_build_path = /obj/machinery/smartfridge/disks -/obj/machinery/smartfridge/disks/accept_check(obj/item/O) - if(istype(O, /obj/item/disk/)) - return TRUE - else - return FALSE +/obj/machinery/smartfridge/disks/accept_check(obj/item/weapon) + return istype(weapon, /obj/item/disk) diff --git a/code/modules/mob/living/basic/pets/parrot/parrot.dm b/code/modules/mob/living/basic/pets/parrot/parrot.dm new file mode 100644 index 0000000000000..fd3509a8122c7 --- /dev/null +++ b/code/modules/mob/living/basic/pets/parrot/parrot.dm @@ -0,0 +1,408 @@ +GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list( + /datum/strippable_item/parrot_headset, +))) + + +/// Parrots! Klepto bastards that imitate your speech and hoard your shit. +/mob/living/basic/parrot + name = "parrot" + desc = "The parrot squawks, \"They're a Parrot! BAWWK!\"" + icon = 'icons/mob/simple/animal.dmi' + icon_state = "parrot_fly" + icon_living = "parrot_fly" + icon_dead = "parrot_dead" + density = FALSE + health = 80 + maxHealth = 80 + pass_flags = PASSTABLE | PASSMOB + + butcher_results = list(/obj/item/food/cracker = 1) + melee_damage_upper = 10 + melee_damage_lower = 5 + + response_help_continuous = "pets" + response_help_simple = "pet" + response_disarm_continuous = "gently moves aside" + response_disarm_simple = "gently move aside" + response_harm_continuous = "swats" + response_harm_simple = "swat" + combat_mode = TRUE //parrots now start "aggressive" since only player parrots will nuzzle. + attack_verb_continuous = "chomps" + attack_verb_simple = "chomp" + attack_vis_effect = ATTACK_EFFECT_BITE + friendly_verb_continuous = "grooms" + friendly_verb_simple = "groom" + mob_size = MOB_SIZE_SMALL + gold_core_spawnable = FRIENDLY_SPAWN + + ai_controller = /datum/ai_controller/basic_controller/parrot + + /// Icon we use while sitting + var/icon_sit = "parrot_sit" + /// The number of damage we do when we decide to aggro for our lives + var/parrot_damage_upper = 10 + /// Potential bodyparts for us to attack + var/parrot_dam_zone = CARBON_GENERIC_BODY_ZONES + + ///Headset for Poly to yell at engineers :) + var/obj/item/radio/headset/ears = null + + ///Parrots are kleptomaniacs. This variable ... stores the item a parrot is holding. + var/obj/item/held_item = null + + /// The blackboard key we use to store the string we're repeating + var/speech_blackboard_key = BB_PARROT_REPEAT_STRING + /// The generic probability odds we have to do a speech-related action // FIXME might need to tone this down + var/speech_probability_rate = 5 + /// The generic probability odds we have to switch out our speech string + var/speech_shuffle_rate = 30 + + //Parrots will generally sit on their perch unless something catches their eye. + var/static/list/desired_perches = list( + /obj/machinery/computer, + /obj/machinery/dna_scannernew, + /obj/machinery/nuclearbomb, + /obj/machinery/recharge_station, + /obj/machinery/smartfridge, + /obj/machinery/suit_storage_unit, + /obj/machinery/telecomms, + /obj/machinery/teleport, + /obj/structure/displaycase, + /obj/structure/filingcabinet, + /obj/structure/frame/computer, + ) + ///items we wont pick up + var/static/list/ignore_items = typecacheof(list(/obj/item/radio)) + + +/mob/living/basic/parrot/Initialize(mapload) + . = ..() + setup_headset() + update_speech_blackboards() + ai_controller.set_blackboard_key(BB_PARROT_PERCH_TYPES, desired_perches) + ai_controller.set_blackboard_key(BB_IGNORE_ITEMS, ignore_items) + AddElement(/datum/element/ai_retaliate) + AddElement(/datum/element/strippable, GLOB.strippable_parrot_items) + AddElement(/datum/element/simple_flying) + AddElement(/datum/element/basic_eating, food_types = list(/obj/item/food/cracker)) + AddComponent(/datum/component/listen_and_repeat, desired_phrases = get_static_list_of_phrases(), blackboard_key = BB_PARROT_REPEAT_STRING) + AddComponent(\ + /datum/component/tameable,\ + food_types = list(/obj/item/food/cracker),\ + tame_chance = 100,\ + bonus_tame_chance = 0,\ + after_tame = CALLBACK(src, PROC_REF(tamed)),\ + ) + + RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attacking)) + RegisterSignal(src, COMSIG_MOB_CLICKON, PROC_REF(on_click)) + RegisterSignal(src, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attacked)) // this means we could have a peaceful interaction, like getting a cracker + RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_injured)) // this means we got hurt and it's go time + RegisterSignal(src, COMSIG_KB_MOB_DROPITEM_DOWN, PROC_REF(drop_item_on_signal)) + +/mob/living/basic/parrot/Destroy() + // should have cleaned these up on death, but let's be super safe in case that didn't happen + if(!QDELETED(ears)) + QDEL_NULL(ears) + if(!QDELETED(held_item)) + QDEL_NULL(held_item) + return ..() + +/mob/living/basic/parrot/death(gibbed) + if(held_item) + held_item.forceMove(drop_location()) + held_item = null + + if(ears) + ears.forceMove(drop_location()) + ears = null + + if(!isnull(buckled)) + buckled.unbuckle_mob(src, force = TRUE) + buckled = null + pixel_x = base_pixel_x + pixel_y = base_pixel_y + + return ..() + +/mob/living/basic/parrot/examine(mob/user) + . = ..() + if(stat != DEAD) + return + + if(HAS_MIND_TRAIT(user, TRAIT_NAIVE)) + . += pick( + "It seems tired and shagged out after a long squawk.", + "It seems to be pining for the fjords.", + "It's resting. It's a beautiful bird. Lovely plumage.", + ) + else + . += pick( + "This is a late parrot.", + "This is an ex-parrot.", + "This parrot is no more.", + ) + +/mob/living/basic/parrot/get_status_tab_items() + . = ..() + . += "Held Item: [held_item]" + +/mob/living/basic/parrot/Process_Spacemove(movement_dir = 0, continuous_move = FALSE) + if(stat != DEAD) // parrots have evolved to let them fly in space because fucking uhhhhhhhhhh + return TRUE + return ..() + +/mob/living/basic/parrot/radio(message, list/message_mods = list(), list/spans, language) //literally copied from human/radio(), but there's no other way to do this. at least it's better than it used to be. + . = ..() + if(. != NONE) + return + + if(message_mods[MODE_HEADSET]) + if(ears) + ears.talk_into(src, message, , spans, language, message_mods) + return ITALICS | REDUCE_RANGE + else if(message_mods[RADIO_EXTENSION] == MODE_DEPARTMENT) + if(ears) + ears.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods) + return ITALICS | REDUCE_RANGE + else if(message_mods[RADIO_EXTENSION] in GLOB.radiochannels) + if(ears) + ears.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods) + return ITALICS | REDUCE_RANGE + + return NONE + +#define PARROT_PERCHED "parrot_perched" // move this later + +/mob/living/basic/parrot/update_icon_state() + . = ..() + if(HAS_TRAIT(src, PARROT_PERCHED)) + icon_state = icon_sit + else + icon_state = icon_living + +/// Proc that we just use to see if we're rightclicking something for perch behavior or dropping the item we currently ahve +/mob/living/basic/parrot/proc/on_click(mob/living/basic/source, atom/target, params) + SIGNAL_HANDLER + if(!LAZYACCESS(params, RIGHT_CLICK) || !CanReach(target)) + return + if(start_perching(target) && !isnull(held_item)) + drop_held_item(gently = TRUE) + +/// Proc that ascertains the type of perch we're dealing with and starts the perching process. +/// Returns TRUE if we started perching, FALSE otherwise. +/mob/living/basic/parrot/proc/start_perching(atom/target) + if(HAS_TRAIT(src, PARROT_PERCHED)) + balloon_alert(src, "already perched!") + return FALSE + + if(ishuman(target)) + return perch_on_human(target) + + if(!is_type_in_list(target, desired_perches)) + return FALSE + + forceMove(get_turf(target)) + drop_held_item(gently = TRUE) // comfy :) + toggle_perched(perched = TRUE) + RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(after_move)) + return TRUE + +/mob/living/basic/parrot/proc/after_move(atom/source) + SIGNAL_HANDLER + + UnregisterSignal(src, COMSIG_MOVABLE_MOVED) + toggle_perched(perched = FALSE) + +/// Proc that will perch us on a human. Returns TRUE if we perched, FALSE otherwise. +/mob/living/basic/parrot/proc/perch_on_human(mob/living/carbon/human/target) + if(LAZYLEN(target.buckled_mobs) >= target.max_buckled_mobs) + balloon_alert(src, "can't perch on them!") + return FALSE + + forceMove(get_turf(target)) + if(!target.buckle_mob(src, TRUE)) + return FALSE + + to_chat(src, span_notice("You sit on [target]'s shoulder.")) + toggle_perched(perched = TRUE) + RegisterSignal(src, COMSIG_LIVING_SET_BUCKLED, PROC_REF(on_unbuckle)) + return TRUE + +/mob/living/basic/parrot/proc/on_unbuckle(mob/living/source, atom/movable/new_buckled) + SIGNAL_HANDLER + + if(new_buckled) + return + UnregisterSignal(src, COMSIG_LIVING_SET_BUCKLED) + toggle_perched(perched = FALSE) + +/mob/living/basic/parrot/proc/toggle_perched(perched) + if(!perched) + REMOVE_TRAIT(src, PARROT_PERCHED, TRAIT_GENERIC) + else + ADD_TRAIT(src, PARROT_PERCHED, TRAIT_GENERIC) + update_appearance(UPDATE_ICON_STATE) + +/// Master proc which will determine the intent of OUR attacks on an object and summon the relevant procs accordingly. +/// This is pretty much meant for players, AI will use the task-specific procs instead. +/mob/living/basic/parrot/proc/pre_attacking(mob/living/basic/source, atom/target) + SIGNAL_HANDLER + if(stat != CONSCIOUS) + return + + if(isitem(target) && steal_from_ground(target)) + return COMPONENT_HOSTILE_NO_ATTACK + + if(iscarbon(target) && steal_from_mob(target)) + return COMPONENT_HOSTILE_NO_ATTACK + +/// Picks up an item from the ground and puts it in our claws. Returns TRUE if we picked it up, FALSE otherwise. +/mob/living/basic/parrot/proc/steal_from_ground(obj/item/target) + if(!isnull(held_item)) + balloon_alert(src, "already holding something!") + return FALSE + + if(target.w_class > WEIGHT_CLASS_SMALL) + balloon_alert(src, "too big to pick up!") + return FALSE + + target.forceMove(src) + held_item = target + visible_message( + span_notice("[src] grabs [held_item]!"), + span_notice("You grab [held_item]!"), + span_hear("You hear the sounds of wings flapping furiously."), + ) + return TRUE + +/// Looks for an item that we can snatch and puts it in our claws. Returns TRUE if we picked it up, FALSE otherwise. +/mob/living/basic/parrot/proc/steal_from_mob(mob/living/carbon/victim) + if(!isnull(held_item)) + balloon_alert(src, "already holding something!") + return FALSE + + for(var/obj/item/stealable in victim.held_items) + if(stealable.w_class > WEIGHT_CLASS_SMALL) + continue + + if(!victim.temporarilyRemoveItemFromInventory(stealable)) + continue + + visible_message( + span_notice("[src] grabs [held_item] out of [victim]'s hand!"), + span_notice("You snag [held_item] out of [victim]'s hand!"), + span_hear("You hear the sounds of wings flapping furiously."), + ) + pick_up_item(stealable) + return TRUE + + return FALSE + +/// Handles special behavior whenever we're attacked with a special item. +/mob/living/basic/parrot/proc/on_attacked(mob/living/basic/source, obj/item/thing, mob/living/attacker, params) + SIGNAL_HANDLER + if(!istype(thing, /obj/item/food/cracker)) // Poly wants a cracker + return + + qdel(thing) + if(health < maxHealth) + adjustBruteLoss(-10) + speech_probability_rate *= 1.27 + speech_shuffle_rate += 10 + update_speech_blackboards() + to_chat(src, span_notice("[src] eagerly devours the cracker.")) + return COMPONENT_NO_AFTERATTACK + +/// Handles special behavior whenever we are injured. +/mob/living/basic/parrot/proc/on_injured(mob/living/basic/source, mob/living/attacker, attack_flags) + SIGNAL_HANDLER + if(!isnull(client) || stat == CONSCIOUS) + return + + drop_held_item(gently = FALSE) + var/return_value = SEND_SIGNAL(source, COMSIG_NEEDS_NEW_PHRASE) + if(return_value & NO_NEW_PHRASE_AVAILABLE) + return + + INVOKE_ASYNC(src, TYPE_PROC_REF(/atom/movable, say), message = ai_controller.blackboard[BB_PARROT_REPEAT_STRING], forced = "parrot oneliner on attack") + +/// Handles picking up the item we're holding, done in its own proc because of a snowflake edge case we need to account for. No additional logic beyond that. +/// Returns TRUE if we picked it up, FALSE otherwise. +/mob/living/basic/parrot/proc/pick_up_item(obj/item/target) + target.forceMove(src) + held_item = target + +/// Handles dropping items we're holding. Gently is a special modifier we can use for special interactions. +/mob/living/basic/parrot/proc/drop_held_item(gently = TRUE) + if(isnull(held_item)) + balloon_alert(src, "nothing to drop!") + return + + if(stat != CONSCIOUS) // don't gotta do shit + return + + if(!gently && isgrenade(held_item)) + var/obj/item/grenade/bomb = held_item + balloon_alert(src, "bombs away!") // you'll likely die too so we can get away with the `!` here + bomb.forceMove(drop_location()) + bomb.detonate() + return + + balloon_alert(src, "dropped item") + held_item.forceMove(drop_location()) + +/mob/living/basic/parrot/Exited(atom/movable/gone, direction) + . = ..() + if(gone != held_item) + return + held_item= null + +/mob/living/basic/parrot/vv_edit_var(var_name, vval) + . = ..() // give admins an easier time when it comes to fucking with poly + switch(var_name) + if(NAMEOF(src, speech_probability_rate)) + update_speech_blackboards() + if(NAMEOF(src, speech_shuffle_rate)) + update_speech_blackboards() + +/// Updates our speech blackboards mob-side to reflect the current speech on the controller to ensure everything is synchronized. +/mob/living/basic/parrot/proc/update_speech_blackboards() + ai_controller.set_blackboard_key(BB_PARROT_REPEAT_PROBABILITY, speech_probability_rate) + ai_controller.set_blackboard_key(BB_PARROT_PHRASE_CHANGE_PROBABILITY, speech_shuffle_rate) + +/// Will simply set up the headset for the parrot to use. Stub, implemented on subtypes. +/mob/living/basic/parrot/proc/setup_headset() + return + +/// Gets a static list of phrases we wish to pass to the element. +/mob/living/basic/parrot/proc/get_static_list_of_phrases() + var/static/list/default_phrases = list( + "BAWWWWK george mellons griffing me!", + "Cracker?", + "Hello!", + "Hi!", + ) + + return default_phrases + +/// Gets the available channels that this parrot has access to. Returns a list of the channels we can use. +/mob/living/basic/parrot/proc/get_available_channels() + var/list/returnable_list = list() + if(isnull(ears)) + return returnable_list + + var/list/headset_channels = ears.channels + for(var/channel in headset_channels) + returnable_list += GLOB.channel_tokens[channel] // will return something like ":e" or ":c" y'know + + return returnable_list + +/mob/living/basic/parrot/proc/tamed() + new /obj/effect/temp_visual/heart(drop_location()) + +/mob/living/basic/parrot/proc/drop_item_on_signal(mob/living/user) + SIGNAL_HANDLER + + drop_held_item() + return COMSIG_KB_ACTIVATED diff --git a/code/modules/mob/living/basic/pets/parrot/parrot_ai/parrot_controller.dm b/code/modules/mob/living/basic/pets/parrot/parrot_ai/parrot_controller.dm new file mode 100644 index 0000000000000..43f6de54ad8a7 --- /dev/null +++ b/code/modules/mob/living/basic/pets/parrot/parrot_ai/parrot_controller.dm @@ -0,0 +1,251 @@ +/datum/ai_controller/basic_controller/parrot + blackboard = list( + BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/allow_items, + BB_HOARD_LOCATION_RANGE = 9, + ) + + ai_traits = STOP_MOVING_WHEN_PULLED + ai_movement = /datum/ai_movement/basic_avoidance + idle_behavior = /datum/idle_behavior/idle_random_walk/parrot + + planning_subtrees = list( + /datum/ai_planning_subtree/target_retaliate, + /datum/ai_planning_subtree/perch_on_target, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/hoard_items, + /datum/ai_planning_subtree/parrot_as_in_repeat, // always get a witty oneliner in when you can + ) + +/datum/idle_behavior/idle_random_walk/parrot + ///chance of us moving while perched + var/walk_chance_when_perched = 5 + +/datum/idle_behavior/idle_random_walk/parrot/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller) + var/mob/living/living_pawn = controller.pawn + walk_chance = HAS_TRAIT(living_pawn, PARROT_PERCHED) ? walk_chance_when_perched : initial(walk_chance) + return ..() + +///subtree to steal items +/datum/ai_planning_subtree/hoard_items + var/theft_chance = 5 + +/datum/ai_planning_subtree/hoard_items/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/living_pawn = controller.pawn + + var/turf/myspace = controller.blackboard[BB_HOARD_LOCATION] + + if(isnull(myspace) || myspace.is_blocked_turf(source_atom = controller.pawn) || get_dist(myspace, controller.pawn) > controller.blackboard[BB_HOARD_LOCATION_RANGE]) + controller.queue_behavior(/datum/ai_behavior/find_and_set/hoard_location, BB_HOARD_LOCATION, /turf/open) + return + + //we have an item, go drop! + var/list/our_contents = living_pawn.contents - typecache_filter_list(living_pawn.contents, controller.blackboard[BB_IGNORE_ITEMS]) + if(length(our_contents)) + controller.queue_behavior(/datum/ai_behavior/travel_towards/and_drop, BB_HOARD_LOCATION) + return SUBTREE_RETURN_FINISH_PLANNING + + if(controller.blackboard_key_exists(BB_HOARD_ITEM_TARGET)) + controller.queue_behavior(/datum/ai_behavior/basic_melee_attack/interact_once, BB_HOARD_ITEM_TARGET, BB_TARGETING_STRATEGY) + return SUBTREE_RETURN_FINISH_PLANNING + + if(!SPT_PROB(theft_chance, seconds_per_tick)) + return + controller.queue_behavior(/datum/ai_behavior/find_and_set/hoard_item, BB_HOARD_ITEM_TARGET) + +/datum/ai_behavior/find_and_set/hoard_location + +/datum/ai_behavior/find_and_set/hoard_location/search_tactic(datum/ai_controller/controller, locate_path, search_range) + for(var/turf/open/candidate in oview(search_range, controller.pawn)) + if(isspaceturf(candidate) || isopenspaceturf(candidate)) + continue + if(candidate.is_blocked_turf(source_atom = controller.pawn)) + continue + return candidate + + return null + +/datum/ai_behavior/find_and_set/hoard_item + action_cooldown = 5 SECONDS + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/find_and_set/hoard_item/search_tactic(datum/ai_controller/controller, locate_path, search_range) + if(!controller.blackboard_key_exists(BB_HOARD_LOCATION)) + return null + var/turf/nest_turf = controller.blackboard[BB_HOARD_LOCATION] + var/mob/living/living_pawn = controller.pawn + for(var/atom/potential_victim in oview(search_range, controller.pawn)) + if(is_type_in_typecache(potential_victim, controller.blackboard[BB_IGNORE_ITEMS])) + continue + if(potential_victim.loc == nest_turf) + continue + if(isitem(potential_victim)) + var/obj/item/item_steal = potential_victim + if(item_steal.w_class <= WEIGHT_CLASS_SMALL) + return potential_victim + if(!ishuman(potential_victim)) + continue + if(living_pawn.faction.Find(REF(potential_victim))) + continue //dont steal from friends + if(holding_valuable(controller, potential_victim)) + controller.set_blackboard_key(BB_ALWAYS_IGNORE_FACTION, TRUE) + return potential_victim + + return null + +/datum/ai_behavior/find_and_set/hoard_item/proc/holding_valuable(datum/ai_controller/controller, mob/living/human_target) + for(var/obj/item/potential_item in human_target.held_items) + if(is_type_in_typecache(potential_item, controller.blackboard[BB_IGNORE_ITEMS])) + continue + if(potential_item.w_class <= WEIGHT_CLASS_SMALL) + return TRUE + return FALSE + +/datum/ai_behavior/travel_towards/and_drop + +/datum/ai_behavior/travel_towards/and_drop/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + var/mob/living/living_mob = controller.pawn + var/obj/drop_item = locate(/obj/item) in (living_mob.contents - typecache_filter_list(living_mob.contents, controller.blackboard[BB_IGNORE_ITEMS])) + drop_item?.forceMove(get_turf(living_mob)) + +/datum/ai_behavior/basic_melee_attack/interact_once/parrot + +/datum/ai_behavior/basic_melee_attack/interact_once/parrot/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + controller.set_blackboard_key(BB_ALWAYS_IGNORE_FACTION, FALSE) + +///subtree to perch on targets +/datum/ai_planning_subtree/perch_on_target + ///perchance... + var/perch_chance = 5 + ///chance we unbuckle + var/unperch_chance = 15 + + +/datum/ai_planning_subtree/perch_on_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/living_pawn = controller.pawn + var/atom/buckled_too = living_pawn.buckled + + //do we have a current target or is chance to unbuckle has passed? then unbuckle! + if(buckled_too) + if((SPT_PROB(unperch_chance, seconds_per_tick) || controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))) + controller.queue_behavior(/datum/ai_behavior/unbuckle_mob) + return + return SUBTREE_RETURN_FINISH_PLANNING + + //if we are perched, we can go find something else to perch too + var/final_chance = HAS_TRAIT(living_pawn, PARROT_PERCHED) ? unperch_chance : perch_chance + + if(!SPT_PROB(final_chance, seconds_per_tick) || controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET)) + return + + if(controller.blackboard_key_exists(BB_PERCH_TARGET)) + controller.queue_behavior(/datum/ai_behavior/perch_on_target, BB_PERCH_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + //50 50 chance to look for an object, or a friend + if(prob(50)) + controller.queue_behavior(/datum/ai_behavior/find_and_set/nearby_friends, BB_PERCH_TARGET) + return + + controller.queue_behavior(/datum/ai_behavior/find_and_set/in_list, BB_PERCH_TARGET, controller.blackboard[BB_PARROT_PERCH_TYPES]) + + +/datum/ai_behavior/perch_on_target + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/perch_on_target/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + + set_movement_target(controller, target) + +/datum/ai_behavior/perch_on_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + finish_action(controller, FALSE, target_key) + return + + var/mob/living/basic/parrot/living_pawn = controller.pawn + + if(!ishuman(target)) + living_pawn.start_perching(target) + finish_action(controller, TRUE, target_key) + return + + if(!check_human_conditions(target)) + finish_action(controller, FALSE, target_key) + return + + living_pawn.start_perching(target) + finish_action(controller, TRUE, target_key) + +/datum/ai_behavior/perch_on_target/proc/check_human_conditions(mob/living/living_human) + if(living_human.stat == DEAD || LAZYLEN(living_human.buckled_mobs) >= living_human.max_buckled_mobs) + return FALSE + + return TRUE + +/datum/ai_behavior/perch_on_target/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + controller.clear_blackboard_key(target_key) + +/datum/ai_behavior/unbuckle_mob + +/datum/ai_behavior/unbuckle_mob/perform(seconds_per_tick, datum/ai_controller/controller) + . = ..() + + var/mob/living/living_pawn = controller.pawn + var/atom/movable/buckled_too = living_pawn.buckled + + if(isnull(buckled_too)) + finish_action(controller, FALSE) + return + + buckled_too.unbuckle_mob(living_pawn) + finish_action(controller, TRUE) + + +//ghost poly + +/datum/ai_controller/basic_controller/parrot/ghost + planning_subtrees = list( + /datum/ai_planning_subtree/parrot_as_in_repeat, + /datum/ai_planning_subtree/possess_humans, + /datum/ai_planning_subtree/hoard_items, + ) + +///subtree to possess humans +/datum/ai_planning_subtree/possess_humans + ///chance we go possess humans + var/possess_chance = 80 + +/datum/ai_planning_subtree/possess_humans/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/living_pawn = controller.pawn + + if(controller.blackboard_key_exists(BB_PERCH_TARGET)) + controller.queue_behavior(/datum/ai_behavior/perch_on_target/haunt, BB_PERCH_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + + if(!SPT_PROB(possess_chance, seconds_per_tick)) + if(ishuman(living_pawn.loc)) + return SUBTREE_RETURN_FINISH_PLANNING + return + + if(ishuman(living_pawn.loc)) + controller.set_blackboard_key(living_pawn.loc) + return + + controller.queue_behavior(/datum/ai_behavior/find_and_set/conscious_person, BB_PERCH_TARGET) + + +/datum/ai_behavior/perch_on_target/haunt + +/datum/ai_behavior/perch_on_target/haunt/check_human_conditions(mob/living/living_human) + if(living_human.stat == DEAD) + return FALSE + return TRUE diff --git a/code/modules/mob/living/basic/pets/parrot/parrot_ai/parroting_action.dm b/code/modules/mob/living/basic/pets/parrot/parrot_ai/parroting_action.dm new file mode 100644 index 0000000000000..d1488a60b3bb9 --- /dev/null +++ b/code/modules/mob/living/basic/pets/parrot/parrot_ai/parroting_action.dm @@ -0,0 +1,50 @@ +/// When a parrot... parrots... +/datum/ai_planning_subtree/parrot_as_in_repeat + operational_datums = list(/datum/component/listen_and_repeat) + +/datum/ai_planning_subtree/parrot_as_in_repeat/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/atom/speaking_pawn = controller.pawn + + var/switch_up_probability = controller.blackboard[BB_PARROT_PHRASE_CHANGE_PROBABILITY] + if(SPT_PROB(switch_up_probability, seconds_per_tick) || isnull(controller.blackboard[BB_PARROT_REPEAT_STRING])) + if(SEND_SIGNAL(speaking_pawn, COMSIG_NEEDS_NEW_PHRASE) & NO_NEW_PHRASE_AVAILABLE) + return + + if(!SPT_PROB(controller.blackboard[BB_PARROT_REPEAT_PROBABILITY], seconds_per_tick)) + return + + var/potential_string = controller.blackboard[BB_PARROT_REPEAT_STRING] + if(isnull(potential_string)) + stack_trace("Parrot As In Repeat Subtree somehow is getting a null potential string while not getting `NO_NEW_PHRASE_AVAILABLE`!") + return + + controller.queue_behavior(/datum/ai_behavior/perform_speech/parrot, potential_string) + +/datum/ai_behavior/perform_speech/parrot + action_cooldown = 7.5 SECONDS // gets really annoying (moreso than usual) really fast otherwise + +/datum/ai_behavior/perform_speech/parrot/perform(seconds_per_tick, datum/ai_controller/controller, speech, speech_sound) + var/mob/living/basic/parrot/speaking_pawn = controller.pawn + var/list/available_channels = speaking_pawn.get_available_channels() + var/modified_speech = speech + var/use_radio = prob(50) // we might not even use the radio if we even have a channel + +#define HAS_CHANNEL_PREFIX (speech[1] in GLOB.department_radio_prefixes) && (copytext_char(speech, 2, 3) in GLOB.department_radio_keys) // determine if we need to crop the channel prefix + + if(!length(available_channels)) // might not even use the radio at all + if(HAS_CHANNEL_PREFIX) + modified_speech = copytext_char(speech, 3) + + else + if(HAS_CHANNEL_PREFIX) + modified_speech = "[use_radio ? pick(available_channels) : ""][copytext_char(speech, 3)]" + else + modified_speech = "[use_radio ? pick(available_channels) : ""][speech]" + + + speaking_pawn.say(modified_speech, forced = "AI Controller") + if(speech_sound) + playsound(speaking_pawn, speech_sound, 80, vary = TRUE) + finish_action(controller, TRUE) + +#undef HAS_CHANNEL_PREFIX diff --git a/code/modules/mob/living/basic/pets/parrot/parrot_items.dm b/code/modules/mob/living/basic/pets/parrot/parrot_items.dm new file mode 100644 index 0000000000000..c071bf7fdbe62 --- /dev/null +++ b/code/modules/mob/living/basic/pets/parrot/parrot_items.dm @@ -0,0 +1,60 @@ +/datum/strippable_item/parrot_headset + key = STRIPPABLE_ITEM_PARROT_HEADSET + +/datum/strippable_item/parrot_headset/get_item(atom/source) + var/mob/living/basic/parrot/poly/parrot_source = source + return istype(parrot_source) ? parrot_source.ears : null + +/datum/strippable_item/parrot_headset/try_equip(atom/source, obj/item/equipping, mob/user) + . = ..() + if (!.) + return FALSE + + if (!istype(equipping, /obj/item/radio/headset)) + to_chat(user, span_warning("[equipping] won't fit!")) + return FALSE + + return TRUE + +// There is no delay for putting a headset on a parrot. +/datum/strippable_item/parrot_headset/start_equip(atom/source, obj/item/equipping, mob/user) + return TRUE + +/datum/strippable_item/parrot_headset/finish_equip(atom/source, obj/item/equipping, mob/user) + var/obj/item/radio/headset/radio = equipping + if (!istype(radio)) + return + + var/mob/living/basic/parrot/parrot_source = source + if (!istype(parrot_source)) + return + + if (!user.transferItemToLoc(radio, source)) + return + + parrot_source.ears = radio + + to_chat(user, span_notice("You fit [radio] onto [source].")) + +/datum/strippable_item/parrot_headset/start_unequip(atom/source, mob/user) + . = ..() + if (!.) + return FALSE + + var/mob/living/basic/parrot/parrot_source = source + if (!istype(parrot_source)) + return + + if (parrot_source.stat == CONSCIOUS) + var/list/list_of_channels = parrot_source.get_available_channels() + parrot_source.say("[list_of_channels ? "[pick(list_of_channels)] " : null]BAWWWWWK LEAVE THE HEADSET BAWKKKKK!", forced = "attempted headset removal") + + return TRUE + +/datum/strippable_item/parrot_headset/finish_unequip(atom/source, mob/user) + var/mob/living/basic/parrot/parrot_source = source + if (!istype(parrot_source)) + return + + parrot_source.ears.forceMove(parrot_source.drop_location()) + parrot_source.ears = null diff --git a/code/modules/mob/living/basic/pets/parrot/parrot_subtypes.dm b/code/modules/mob/living/basic/pets/parrot/parrot_subtypes.dm new file mode 100644 index 0000000000000..c654f4cfa51ad --- /dev/null +++ b/code/modules/mob/living/basic/pets/parrot/parrot_subtypes.dm @@ -0,0 +1,15 @@ +// file just for other parrot subtypes that aren't poly + +/// Parrot that will just randomly spawn with a headset. Nothing too special beyond that. +/mob/living/basic/parrot/headsetted + +/// Will simply set up the headset for the parrot to use. Stub, implemented on subtypes. +/mob/living/basic/parrot/headsetted/setup_headset() + var/headset = pick( + /obj/item/radio/headset/headset_cargo, + /obj/item/radio/headset/headset_eng, + /obj/item/radio/headset/headset_med, + /obj/item/radio/headset/headset_sci, + /obj/item/radio/headset/headset_sec, + ) + ears = new headset(src) diff --git a/code/modules/mob/living/basic/pets/parrot/poly.dm b/code/modules/mob/living/basic/pets/parrot/poly.dm new file mode 100644 index 0000000000000..f6788006b1c54 --- /dev/null +++ b/code/modules/mob/living/basic/pets/parrot/poly.dm @@ -0,0 +1,251 @@ +/// Default poly, presumably died the last shift and has no special traits. +#define POLY_DEFAULT "default" +/// Poly has survived a number of rounds equivalent to the longest survival of his being. +#define POLY_LONGEST_SURVIVAL "longest_survival" +/// Poly has survived a number of rounds equivalent to the longest deathstreak of his being. +#define POLY_BEATING_DEATHSTREAK "longest_deathstreak" +/// Poly has only just survived a round, and is doing a consecutive one. +#define POLY_CONSECUTIVE_ROUND "consecutive_round" +/// haunt filter we apply to who we possess +#define POLY_POSSESS_FILTER +/// haunt filter color we apply to who we possess +#define POLY_POSSESS_GLOW "#522059" + +/// The classically famous compadre to the Chief Engineer, Poly. +/mob/living/basic/parrot/poly + name = "Poly" + desc = "Poly the Parrot. An expert on quantum cracker theory." + gold_core_spawnable = NO_SPAWN + speech_probability_rate = 30 // FIXME: might need to nerf this + + /// Callback to save our memory at the end of the round. + var/datum/callback/roundend_callback = null + /// Did we write the memory to disk? + var/memory_saved = FALSE + /// How long has this bird been alive for? + var/rounds_survived = 0 + /// How long have we survived for at max? + var/longest_survival = 0 + /// How many rounds in a row have we been dead for? + var/longest_deathstreak = 0 + +/mob/living/basic/parrot/poly/Initialize(mapload) + . = ..() + + if(!memory_saved) + roundend_callback = CALLBACK(src, PROC_REF(Write_Memory)) + SSticker.OnRoundend(roundend_callback) + + REGISTER_REQUIRED_MAP_ITEM(1, 1) // every map needs a poly! + update_appearance() + + if(!SStts.tts_enabled) + return + + voice = pick(SStts.available_speakers) + if(SStts.pitch_enabled) + if(findtext(voice, "Woman")) + pitch = 12 // up-pitch by one octave + else + pitch = 24 // up-pitch by 2 octaves + else + voice_filter = "rubberband=pitch=1.5" // Use the filter to pitch up if we can't naturally pitch up. + +/mob/living/basic/parrot/poly/Destroy() + LAZYREMOVE(SSticker.round_end_events, roundend_callback) // we do the memory writing stuff on death, but this is important to yeet as fast as we can if we need to destroy + roundend_callback = null + return ..() + +/mob/living/basic/parrot/poly/death(gibbed) + if(HAS_TRAIT(src, TRAIT_DONT_WRITE_MEMORY)) + return ..() // Don't read memory either. + if(!memory_saved) + Write_Memory(TRUE) + var/special_status = determine_special_poly() + if(special_status == POLY_LONGEST_SURVIVAL || special_status == POLY_BEATING_DEATHSTREAK || prob(0.666)) + var/mob/living/basic/parrot/poly/ghost/specter = new(loc) // san7890 make this the transfer mob proc + if(mind) + mind.transfer_to(specter) + else + specter.key = key + return ..() + +/mob/living/basic/parrot/poly/get_static_list_of_phrases() // there's only one poly, so there should only be one ongoing list of phrases. i guess + var/static/list/phrases_to_return = list() + if(length(phrases_to_return)) + return phrases_to_return + + phrases_to_return += read_memory() // must come first!!! + // now add some valuable lines every poly should have + phrases_to_return += list( + ":e Check the crystal, you chucklefucks!", + ":e OH GOD ITS ABOUT TO DELAMINATE CALL THE SHUTTLE", + ":e WHO TOOK THE DAMN MODSUITS?", + ":e Wire the solars, you lazy bums!", + "Poly wanna cracker!", + ) + switch(determine_special_poly()) + if(POLY_DEFAULT) + phrases_to_return += pick("...alive?", "This isn't parrot heaven!", "I live, I die, I live again!", "The void fades!") + if(POLY_LONGEST_SURVIVAL) + phrases_to_return += pick("...[longest_survival].", "The things I've seen!", "I have lived many lives!", "What are you before me?") + if(POLY_BEATING_DEATHSTREAK) + phrases_to_return += pick("What are you waiting for!", "Violence breeds violence!", "Blood! Blood!", "Strike me down if you dare!") + if(POLY_CONSECUTIVE_ROUND) + phrases_to_return += pick("...again?", "No, It was over!", "Let me out!", "It never ends!") + + return phrases_to_return + +/mob/living/basic/parrot/poly/update_desc() + . = ..() + switch(determine_special_poly()) + if(POLY_LONGEST_SURVIVAL) + desc += " Old as sin, and just as loud. Claimed to be [rounds_survived]." + if(POLY_BEATING_DEATHSTREAK) + desc += " The squawks of [-rounds_survived] dead parrots ring out in your ears..." + if(POLY_CONSECUTIVE_ROUND) + desc += " Over [rounds_survived] shifts without a \"terrible\" \"accident\"!" + +/mob/living/basic/parrot/poly/update_icon() + . = ..() + switch(determine_special_poly()) + if(POLY_LONGEST_SURVIVAL) + add_atom_colour("#EEEE22", FIXED_COLOUR_PRIORITY) + if(POLY_BEATING_DEATHSTREAK) + add_atom_colour("#BB7777", FIXED_COLOUR_PRIORITY) + +/// Reads the memory of the parrot, and updates the necessary variables. Returns a list of phrases to add to the parrot's speech buffer. +/mob/living/basic/parrot/poly/proc/read_memory() + RETURN_TYPE(/list) + var/list/returnable_list = list() + if(fexists("data/npc_saves/Poly.sav")) //legacy compatability to convert old format to new + var/savefile/legacy = new /savefile("data/npc_saves/Poly.sav") + legacy["phrases"] >> returnable_list + legacy["roundssurvived"] >> rounds_survived + legacy["longestsurvival"] >> longest_survival + legacy["longestdeathstreak"] >> longest_deathstreak + fdel("data/npc_saves/Poly.sav") + + else + var/json_file = file("data/npc_saves/Poly.json") + if(!fexists(json_file)) + return + var/list/json = json_decode(file2text(json_file)) + returnable_list = json["phrases"] + rounds_survived = json["roundssurvived"] + longest_survival = json["longestsurvival"] + longest_deathstreak = json["longestdeathstreak"] + + return returnable_list + +/// Determines the type of Poly we might have here based on the statistics we got from the memory. +/mob/living/basic/parrot/poly/proc/determine_special_poly() + if(rounds_survived == longest_survival) + return POLY_LONGEST_SURVIVAL + else if(rounds_survived == longest_deathstreak) + return POLY_BEATING_DEATHSTREAK + else if(rounds_survived > 0) + return POLY_CONSECUTIVE_ROUND + else + return POLY_DEFAULT + +/mob/living/basic/parrot/poly/Write_Memory(dead, gibbed) + . = ..() + if(!. || memory_saved) // if we die, no more memory + return + + if(!dead && (stat != DEAD)) + dead = FALSE + + var/file_path = "data/npc_saves/Poly.json" + var/list/file_data = list() + + var/list/exportable_speech_buffer = ai_controller.blackboard[BB_EXPORTABLE_STRING_BUFFER_LIST] // should have been populated when we sent the signal out on parent + if(length(exportable_speech_buffer)) + file_data["phrases"] = exportable_speech_buffer + + if(dead) + file_data["roundssurvived"] = min(rounds_survived - 1, 0) + file_data["longestsurvival"] = longest_survival + if(rounds_survived - 1 < longest_deathstreak) + file_data["longestdeathstreak"] = rounds_survived - 1 + else + file_data["longestdeathstreak"] = longest_deathstreak + else + + file_data["roundssurvived"] = max(rounds_survived, 0) + 1 + if(rounds_survived + 1 > longest_survival) + file_data["longestsurvival"] = rounds_survived + 1 + else + file_data["longestsurvival"] = longest_survival + file_data["longestdeathstreak"] = longest_deathstreak + + var/formatted_data +#if DM_VERSION >= 515 + formatted_data = json_encode(file_data, JSON_PRETTY_PRINT) +#else + formatted_data = json_encode(file_data) +#endif + + rustg_file_write(formatted_data, file_path) + memory_saved = TRUE + +/mob/living/basic/parrot/poly/setup_headset() + ears = new /obj/item/radio/headset/headset_eng(src) + +/mob/living/basic/parrot/poly/ghost + name = "The Ghost of Poly" + desc = "Doomed to squawk the Earth." + color = "#FFFFFF77" + status_flags = GODMODE + sentience_type = SENTIENCE_BOSS //This is so players can't mindswap into ghost poly to become a literal god + incorporeal_move = INCORPOREAL_MOVE_BASIC + butcher_results = list(/obj/item/ectoplasm = 1) + ai_controller = /datum/ai_controller/basic_controller/parrot/ghost + speech_probability_rate = 1 + +/mob/living/basic/parrot/poly/ghost/Initialize(mapload) + // block anything and everything that could possibly happen with writing memory for ghosts + memory_saved = TRUE + ADD_TRAIT(src, TRAIT_DONT_WRITE_MEMORY, INNATE_TRAIT) + RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved)) + return ..() + +//we perch on human souls +/mob/living/basic/parrot/poly/ghost/perch_on_human(mob/living/carbon/human/target) + if(loc == target) //dismount + forceMove(get_turf(target)) + return FALSE + if(ishuman(loc)) + balloon_alert(src, "already possessing!") + return FALSE + forceMove(target) + return TRUE + +/mob/living/basic/parrot/poly/ghost/proc/on_moved(atom/movable/movable, atom/old_loc) + SIGNAL_HANDLER + + if(ishuman(old_loc)) + var/mob/living/unpossessed_human = old_loc + unpossessed_human.remove_filter(POLY_POSSESS_FILTER) + return + + if(!ishuman(loc)) + return + + var/mob/living/possessed_human = loc + possessed_human.add_filter(POLY_POSSESS_FILTER, 2, list("type" = "outline", "color" = POLY_POSSESS_GLOW, "size" = 2)) + var/filter = possessed_human.get_filter(POLY_POSSESS_FILTER) + + if(filter) + animate(filter, alpha = 200, time = 2 SECONDS, loop = -1) + animate(alpha = 60, time = 2 SECONDS) + + var/datum/disease/parrot_possession/on_possession = new /datum/disease/parrot_possession + on_possession.set_parrot(src) + possessed_human.ForceContractDisease(on_possession, make_copy = FALSE, del_on_fail = TRUE) + +#undef POLY_DEFAULT +#undef POLY_LONGEST_SURVIVAL +#undef POLY_BEATING_DEATHSTREAK +#undef POLY_CONSECUTIVE_ROUND diff --git a/code/modules/mob/living/carbon/emote.dm b/code/modules/mob/living/carbon/emote.dm index bc9384de23b68..9ba533a33b5d2 100644 --- a/code/modules/mob/living/carbon/emote.dm +++ b/code/modules/mob/living/carbon/emote.dm @@ -13,6 +13,7 @@ /datum/emote/living/carbon/blink_r key = "blink_r" + name = "blink (Rapid)" message = "blinks rapidly." /datum/emote/living/carbon/clap diff --git a/code/modules/mob/living/carbon/human/death.dm b/code/modules/mob/living/carbon/human/death.dm index f8cee3b4851f8..6f647e7a3bc3a 100644 --- a/code/modules/mob/living/carbon/human/death.dm +++ b/code/modules/mob/living/carbon/human/death.dm @@ -21,9 +21,8 @@ GLOBAL_LIST_EMPTY(dead_players_during_shift) if(stat == DEAD) return stop_sound_channel(CHANNEL_HEARTBEAT) - var/obj/item/organ/internal/heart/H = get_organ_slot(ORGAN_SLOT_HEART) - if(H) - H.beat = BEAT_NONE + var/obj/item/organ/internal/heart/human_heart = get_organ_slot(ORGAN_SLOT_HEART) + human_heart?.beat = BEAT_NONE . = ..() diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm index 88e5941b5e51e..ba2f545ac097b 100644 --- a/code/modules/mob/living/carbon/human/human_defines.dm +++ b/code/modules/mob/living/carbon/human/human_defines.dm @@ -67,7 +67,7 @@ var/list/datum/bioware/biowares /// What types of mobs are allowed to ride/buckle to this mob - var/static/list/can_ride_typecache = typecacheof(list(/mob/living/carbon/human, /mob/living/simple_animal/slime, /mob/living/simple_animal/parrot)) + var/static/list/can_ride_typecache = typecacheof(list(/mob/living/carbon/human, /mob/living/simple_animal/slime, /mob/living/basic/parrot)) var/lastpuke = 0 var/account_id diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm index 66948253d9e35..616541bffe7c6 100644 --- a/code/modules/mob/living/carbon/life.dm +++ b/code/modules/mob/living/carbon/life.dm @@ -27,11 +27,9 @@ if(stat == DEAD) stop_sound_channel(CHANNEL_HEARTBEAT) else - if(getStaminaLoss() > 0 && stam_regen_start_time <= world.time) adjustStaminaLoss(-INFINITY) - - handle_bodyparts(seconds_per_tick, times_fired) + handle_bodyparts(seconds_per_tick, times_fired) if(. && mind) //. == not dead for(var/key in mind.addiction_points) @@ -769,12 +767,19 @@ */ /mob/living/carbon/proc/undergoing_cardiac_arrest() var/obj/item/organ/internal/heart/heart = get_organ_slot(ORGAN_SLOT_HEART) - if(istype(heart) && heart.beating) + if(istype(heart) && heart.is_beating()) return FALSE else if(!needs_heart()) return FALSE return TRUE +/** + * Causes the mob to either start or stop having a heart attack. + * + * status - Pass TRUE to start a heart attack, or FALSE to stop one. + * + * Returns TRUE if heart status was changed (heart attack -> no heart attack, or visa versa) + */ /mob/living/carbon/proc/set_heartattack(status) if(!can_heartattack()) return FALSE @@ -783,5 +788,7 @@ if(!istype(heart)) return FALSE - heart.beating = !status - return TRUE + if(status) + return heart.Stop() + + return heart.Restart() diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm index 09478e995a130..9faf70427bcfd 100644 --- a/code/modules/mob/living/emote.dm +++ b/code/modules/mob/living/emote.dm @@ -163,6 +163,7 @@ /datum/emote/living/flap/aflap key = "aflap" key_third_person = "aflaps" + name = "flap (Angry)" message = "flaps their wings ANGRILY!" hands_use_check = TRUE wing_time = 10 @@ -190,6 +191,7 @@ /datum/emote/living/gasp_shock key = "gaspshock" key_third_person = "gaspsshock" + name = "gasp (Shock)" message = "gasps in shock!" message_mime = "gasps in silent shock!" emote_type = EMOTE_VISIBLE | EMOTE_AUDIBLE @@ -506,6 +508,7 @@ /datum/emote/living/twitch_s key = "twitch_s" + name = "twitch (Slight)" message = "twitches." /datum/emote/living/twitch_s/run_emote(mob/living/user, params, type_override, intentional) @@ -530,6 +533,7 @@ /datum/emote/living/wsmile key = "wsmile" key_third_person = "wsmiles" + name = "smile (Weak)" message = "smiles weakly." /// The base chance for your yawn to propagate to someone else if they're on the same tile as you diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index b9b9d1e3b1b6b..6664ace55cfd7 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1439,6 +1439,7 @@ /mob/living/basic/morph, /mob/living/basic/mouse, /mob/living/basic/mushroom, + /mob/living/basic/parrot, /mob/living/basic/pet/dog/breaddog, /mob/living/basic/pet/dog/corgi, /mob/living/basic/pet/dog/pug, @@ -1449,7 +1450,6 @@ /mob/living/basic/stickman, /mob/living/basic/stickman/dog, /mob/living/simple_animal/hostile/megafauna/dragon/lesser, - /mob/living/simple_animal/parrot, /mob/living/simple_animal/pet/cat, /mob/living/simple_animal/pet/cat/cak, ) diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index 1cd72a3cc4046..6cf82771444ca 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -250,7 +250,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( /mob/living/Hear(message, atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, list/message_mods = list(), message_range=0) - if(!GET_CLIENT(src)) + if((SEND_SIGNAL(src, COMSIG_MOVABLE_PRE_HEAR, args) & COMSIG_MOVABLE_CANCEL_HEARING) || !GET_CLIENT(src)) return FALSE var/deaf_message @@ -526,7 +526,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( I.talk_into(src, message, , spans, language, message_mods) return ITALICS | REDUCE_RANGE - return 0 + return NONE /mob/living/say_mod(input, list/message_mods = list()) if(message_mods[WHISPER_MODE] == MODE_WHISPER) diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm deleted file mode 100644 index a1f12082ff1b7..0000000000000 --- a/code/modules/mob/living/simple_animal/parrot.dm +++ /dev/null @@ -1,1071 +0,0 @@ -/* Parrots! - * Contains - * Defines - * Inventory (headset stuff) - * Attack responces - * AI - * Procs / Verbs (usable by players) - * Sub-types - * Hear & say (the things we do for gimmicks) - */ - -/* - * Defines - */ - -//Only a maximum of one action and one intent should be active at any given time. -//Actions -#define PARROT_PERCH (1<<0) //Sitting/sleeping, not moving -#define PARROT_SWOOP (1<<1) //Moving towards or away from a target -#define PARROT_WANDER (1<<2) //Moving without a specific target in mind - -//Intents -#define PARROT_STEAL (1<<3) //Flying towards a target to steal it/from it -#define PARROT_ATTACK (1<<4) //Flying towards a target to attack it -#define PARROT_RETURN (1<<5) //Flying towards its perch -#define PARROT_FLEE (1<<6) //Flying away from its attacker - - -/mob/living/simple_animal/parrot - name = "parrot" - desc = "The parrot squawks, \"They're a Parrot! BAWWK!\"" //' - icon = 'icons/mob/simple/animal.dmi' - icon_state = "parrot_fly" - icon_living = "parrot_fly" - icon_dead = "parrot_dead" - var/icon_sit = "parrot_sit" - density = FALSE - health = 80 - maxHealth = 80 - pass_flags = PASSTABLE | PASSMOB - - speak = list("Hi!","Hello!","Cracker?","BAWWWWK george mellons griffing me!") - speak_emote = list("squawks","says","yells") - emote_hear = list("squawks.","bawks!") - emote_see = list("flutters their wings.") - - speak_chance = 1 //1% (1 in 100) chance every tick; So about once per 150 seconds, assuming an average tick is 1.5s - turns_per_move = 5 - butcher_results = list(/obj/item/food/cracker = 1) - melee_damage_upper = 10 - melee_damage_lower = 5 - - response_help_continuous = "pets" - response_help_simple = "pet" - response_disarm_continuous = "gently moves aside" - response_disarm_simple = "gently move aside" - response_harm_continuous = "swats" - response_harm_simple = "swat" - stop_automated_movement = 1 - combat_mode = TRUE //parrots now start "aggressive" since only player parrots will nuzzle. - attack_verb_continuous = "chomps" - attack_verb_simple = "chomp" - attack_vis_effect = ATTACK_EFFECT_BITE - friendly_verb_continuous = "grooms" - friendly_verb_simple = "groom" - mob_size = MOB_SIZE_SMALL - gold_core_spawnable = FRIENDLY_SPAWN - - var/parrot_damage_upper = 10 - var/parrot_state = PARROT_WANDER //Hunt for a perch when created - var/parrot_sleep_max = 25 //The time the parrot sits while perched before looking around. Mosly a way to avoid the parrot's AI in life() being run every single tick. - var/parrot_sleep_dur = 25 //Same as above, this is the var that physically counts down - var/parrot_dam_zone = list(BODY_ZONE_CHEST, BODY_ZONE_HEAD, BODY_ZONE_L_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_ARM, BODY_ZONE_R_LEG) //For humans, select a bodypart to attack - - var/parrot_speed = 5 //"Delay in world ticks between movement." according to byond. Yeah, that's BS but it does directly affect movement. Higher number = slower. - var/parrot_lastmove = null //Updates/Stores position of the parrot while it's moving - var/parrot_stuck = 0 //If parrot_lastmove hasn't changed, this will increment until it reaches parrot_stuck_threshold - var/parrot_stuck_threshold = 10 //if this == parrot_stuck, it'll force the parrot back to wandering - - var/list/speech_buffer = list() - var/speech_shuffle_rate = 20 - var/list/available_channels = list() - - //Headset for Poly to yell at engineers :) - var/obj/item/radio/headset/ears = null - - //Wheter the Parrot should come with a headset - var/spawn_headset = TRUE - - //The thing the parrot is currently interested in. This gets used for items the parrot wants to pick up, mobs it wants to steal from, - //mobs it wants to attack or mobs that have attacked it - var/atom/movable/parrot_interest = null - - //Parrots will generally sit on their perch unless something catches their eye. - //These vars store their preffered perch and if they dont have one, what they can use as a perch - var/obj/parrot_perch = null - var/obj/desired_perches = list(/obj/structure/frame/computer, - /obj/structure/displaycase, - /obj/structure/filingcabinet, - /obj/machinery/teleport, - /obj/machinery/dna_scannernew, - /obj/machinery/telecomms, - /obj/machinery/nuclearbomb, - /obj/machinery/recharge_station, - /obj/machinery/smartfridge, - /obj/machinery/computer, - /obj/machinery/suit_storage_unit, - ) - - //Parrots are kleptomaniacs. This variable ... stores the item a parrot is holding. - var/obj/item/held_item = null - - -/mob/living/simple_animal/parrot/Initialize(mapload) - . = ..() - parrot_sleep_dur = parrot_sleep_max //In case someone decides to change the max without changing the duration var - - add_verb(src, list(/mob/living/simple_animal/parrot/proc/steal_from_ground, \ - /mob/living/simple_animal/parrot/proc/steal_from_mob, \ - /mob/living/simple_animal/parrot/verb/drop_held_item_player, \ - /mob/living/simple_animal/parrot/proc/perch_player, \ - /mob/living/simple_animal/parrot/proc/toggle_mode, - /mob/living/simple_animal/parrot/proc/perch_mob_player)) - - AddElement(/datum/element/strippable, GLOB.strippable_parrot_items) - AddElement(/datum/element/simple_flying) - if(!spawn_headset) - return - if(!ears) - var/headset = pick(/obj/item/radio/headset/headset_sec, \ - /obj/item/radio/headset/headset_eng, \ - /obj/item/radio/headset/headset_med, \ - /obj/item/radio/headset/headset_sci, \ - /obj/item/radio/headset/headset_cargo) - ears = new headset(src) - -/mob/living/simple_animal/parrot/Destroy() - QDEL_NULL(ears) - return ..() - -/mob/living/simple_animal/parrot/examine(mob/user) - . = ..() - if(stat != DEAD) - return - - if(HAS_MIND_TRAIT(user, TRAIT_NAIVE)) - . += pick( - "It seems tired and shagged out after a long squawk.", - "It seems to be pining for the fjords.", - "It's resting. It's a beautiful bird. Lovely plumage.", - ) - else - . += pick( - "This parrot is no more.", - "This is a late parrot.", - "This is an ex-parrot.", - ) - -/mob/living/simple_animal/parrot/death(gibbed) - if(held_item) - held_item.forceMove(drop_location()) - held_item = null - SSmove_manager.stop_looping(src) - - if(buckled) - buckled.unbuckle_mob(src,force=1) - buckled = null - pixel_x = base_pixel_x - pixel_y = base_pixel_y - - return ..() - - -/mob/living/simple_animal/parrot/get_status_tab_items() - . = ..() - . += "Held Item: [held_item]" - -/mob/living/simple_animal/parrot/Hear(message, atom/movable/speaker, message_langs, raw_message, radio_freq, list/spans, list/message_mods = list(), message_range) - . = ..() - if(speaker != src && prob(50)) //Dont imitate ourselves - if(!radio_freq || prob(10)) - if(speech_buffer.len >= 500) - speech_buffer -= pick(speech_buffer) - speech_buffer |= html_decode(raw_message) - if(speaker == src && !client) //If a parrot squawks in the woods and no one is around to hear it, does it make a sound? This code says yes! - return message - -/mob/living/simple_animal/parrot/radio(message, list/message_mods = list(), list/spans, language) //literally copied from human/radio(), but there's no other way to do this. at least it's better than it used to be. - . = ..() - if(.) - return - - if(message_mods[MODE_HEADSET]) - if(ears) - ears.talk_into(src, message, , spans, language, message_mods) - return ITALICS | REDUCE_RANGE - else if(message_mods[RADIO_EXTENSION] == MODE_DEPARTMENT) - if(ears) - ears.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods) - return ITALICS | REDUCE_RANGE - else if(message_mods[RADIO_EXTENSION] in GLOB.radiochannels) - if(ears) - ears.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods) - return ITALICS | REDUCE_RANGE - - return FALSE - -GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list( - /datum/strippable_item/parrot_headset, -))) - -/datum/strippable_item/parrot_headset - key = STRIPPABLE_ITEM_PARROT_HEADSET - -/datum/strippable_item/parrot_headset/get_item(atom/source) - var/mob/living/simple_animal/parrot/parrot_source = source - return istype(parrot_source) ? parrot_source.ears : null - -/datum/strippable_item/parrot_headset/try_equip(atom/source, obj/item/equipping, mob/user) - . = ..() - if (!.) - return FALSE - - if (!istype(equipping, /obj/item/radio/headset)) - to_chat(user, span_warning("[equipping] won't fit!")) - return FALSE - - return TRUE - -// There is no delay for putting a headset on a parrot. -/datum/strippable_item/parrot_headset/start_equip(atom/source, obj/item/equipping, mob/user) - return TRUE - -/datum/strippable_item/parrot_headset/finish_equip(atom/source, obj/item/equipping, mob/user) - var/obj/item/radio/headset/radio = equipping - if (!istype(radio)) - return - - var/mob/living/simple_animal/parrot/parrot_source = source - if (!istype(parrot_source)) - return - - if (!user.transferItemToLoc(radio, source)) - return - - parrot_source.ears = radio - - to_chat(user, span_notice("You fit [radio] onto [source].")) - - parrot_source.available_channels.Cut() - - for (var/channel in radio.channels) - var/channel_to_add - - switch (channel) - if (RADIO_CHANNEL_ENGINEERING) - channel_to_add = RADIO_TOKEN_ENGINEERING - if (RADIO_CHANNEL_COMMAND) - channel_to_add = RADIO_TOKEN_COMMAND - if (RADIO_CHANNEL_SECURITY) - channel_to_add = RADIO_TOKEN_SECURITY - if (RADIO_CHANNEL_SCIENCE) - channel_to_add = RADIO_TOKEN_SCIENCE - if (RADIO_CHANNEL_MEDICAL) - channel_to_add = RADIO_TOKEN_MEDICAL - if (RADIO_CHANNEL_SUPPLY) - channel_to_add = RADIO_TOKEN_SUPPLY - if (RADIO_CHANNEL_SERVICE) - channel_to_add = RADIO_TOKEN_SERVICE - - if (channel_to_add) - parrot_source.available_channels += channel_to_add - - if (radio.translate_binary) - parrot_source.available_channels.Add(MODE_TOKEN_BINARY) - -/datum/strippable_item/parrot_headset/start_unequip(atom/source, mob/user) - . = ..() - if (!.) - return FALSE - - var/mob/living/simple_animal/parrot/parrot_source = source - if (!istype(parrot_source)) - return - - if (!parrot_source.stat) - parrot_source.say("[parrot_source.available_channels.len ? "[pick(parrot_source.available_channels)] " : null]BAWWWWWK LEAVE THE HEADSET BAWKKKKK!") - - return TRUE - -/datum/strippable_item/parrot_headset/finish_unequip(atom/source, mob/user) - var/mob/living/simple_animal/parrot/parrot_source = source - if (!istype(parrot_source)) - return - - parrot_source.ears.forceMove(parrot_source.drop_location()) - parrot_source.ears = null - -/* - * Attack responces - */ -//Humans, monkeys, aliens -/mob/living/simple_animal/parrot/attack_hand(mob/living/carbon/user, list/modifiers) - ..() - if(client) - return - if(!stat && user.combat_mode) - - icon_state = icon_living //It is going to be flying regardless of whether it flees or attacks - - if(parrot_state == PARROT_PERCH) - parrot_sleep_dur = parrot_sleep_max //Reset it's sleep timer if it was perched - - set_parrot_interest(user) - parrot_state = PARROT_SWOOP //The parrot just got hit, it WILL move, now to pick a direction.. - - if(health > 30) //Let's get in there and squawk it up! - parrot_state |= PARROT_ATTACK - else - parrot_state |= PARROT_FLEE //Otherwise, fly like a bat out of hell! - drop_held_item(0) - if(stat != DEAD && !user.combat_mode) - handle_automated_speech(1) //assured speak/emote - return - -/mob/living/simple_animal/parrot/attack_paw(mob/living/carbon/human/user, list/modifiers) - return attack_hand(user, modifiers) - -/mob/living/simple_animal/parrot/attack_alien(mob/living/carbon/alien/user, list/modifiers) - return attack_hand(user, modifiers) - -//Simple animals -/mob/living/simple_animal/parrot/attack_animal(mob/living/simple_animal/user, list/modifiers) - . = ..() //goodbye immortal parrots - - if(client) - return - - if(parrot_state == PARROT_PERCH) - parrot_sleep_dur = parrot_sleep_max //Reset it's sleep timer if it was perched - - if(user.melee_damage_upper > 0 && !stat) - set_parrot_interest(user) - parrot_state = PARROT_SWOOP | PARROT_ATTACK //Attack other animals regardless - icon_state = icon_living - -//Mobs with objects -/mob/living/simple_animal/parrot/attackby(obj/item/O, mob/living/user, params) - if(!stat && !client && !istype(O, /obj/item/stack/medical) && !istype(O, /obj/item/food/cracker)) - if(O.force) - if(parrot_state == PARROT_PERCH) - parrot_sleep_dur = parrot_sleep_max //Reset it's sleep timer if it was perched - - set_parrot_interest(user) - parrot_state = PARROT_SWOOP - if(health > 30) //Let's get in there and squawk it up! - parrot_state |= PARROT_ATTACK - else - parrot_state |= PARROT_FLEE - icon_state = icon_living - drop_held_item(0) - else if(istype(O, /obj/item/food/cracker)) //Poly wants a cracker. - qdel(O) - if(health < maxHealth) - adjustBruteLoss(-10) - speak_chance *= 1.27 // 20 crackers to go from 1% to 100% - speech_shuffle_rate += 10 - to_chat(user, span_notice("[src] eagerly devours the cracker.")) - ..() - return - -//Bullets -/mob/living/simple_animal/parrot/bullet_act(obj/projectile/Proj) - . = ..() - if(!stat && !client) - if(parrot_state == PARROT_PERCH) - parrot_sleep_dur = parrot_sleep_max //Reset it's sleep timer if it was perched - - set_parrot_interest(null) - parrot_state = PARROT_WANDER | PARROT_FLEE //Been shot and survived! RUN LIKE HELL! - //parrot_been_shot += 5 - icon_state = icon_living - drop_held_item(0) - -/mob/living/simple_animal/parrot/Process_Spacemove(movement_dir = 0, continuous_move = FALSE) - if(!stat) //Birds can fly, fun fact. No I don't care that space doesn't have air. Space parrots bitch - return TRUE - return ..() -/* - * AI - Not really intelligent, but I'm calling it AI anyway. - */ -/mob/living/simple_animal/parrot/Life(seconds_per_tick = SSMOBS_DT, times_fired) - ..() - - //Sprite update for when a parrot gets pulled - if(pulledby && !stat && parrot_state != PARROT_WANDER) - if(buckled) - buckled.unbuckle_mob(src, TRUE) - buckled = null - icon_state = icon_living - parrot_state = PARROT_WANDER - pixel_x = initial(pixel_x) - pixel_y = initial(pixel_y) - return - - -//-----SPEECH - /* Parrot speech mimickry! - Phrases that the parrot Hear()s get added to speach_buffer. - Every once in a while, the parrot picks one of the lines from the buffer and replaces an element of the 'speech' list. */ -/mob/living/simple_animal/parrot/handle_automated_speech() - ..() - if(speech_buffer.len && prob(speech_shuffle_rate)) //shuffle out a phrase and add in a new one - if(speak.len) - speak.Remove(pick(speak)) - - speak.Add(pick(speech_buffer)) - - -/mob/living/simple_animal/parrot/handle_automated_movement() - if(!isturf(src.loc) || !(mobility_flags & MOBILITY_MOVE) || buckled) - return //If it can't move, dont let it move. (The buckled check probably isn't necessary thanks to canmove) - - if(client && stat == CONSCIOUS && parrot_state != icon_living) - icon_state = icon_living - -//-----SLEEPING - if(parrot_state == PARROT_PERCH) - if(parrot_perch && parrot_perch.loc != src.loc) //Make sure someone hasn't moved our perch on us - if(parrot_perch in view(src)) - parrot_state = PARROT_SWOOP | PARROT_RETURN - icon_state = icon_living - return - else - parrot_state = PARROT_WANDER - icon_state = icon_living - return - - parrot_sleep_dur-- - if(parrot_sleep_dur) //Zzz - return - - else - //This way we only call the stuff below once every [sleep_max] ticks. - parrot_sleep_dur = parrot_sleep_max - - //Cycle through message modes for the headset - if(speak.len) - var/list/newspeak = list() - - if(available_channels.len && src.ears) - for(var/possible_phrase in speak) - - //50/50 chance to not use the radio at all - var/useradio = 0 - if(prob(50)) - useradio = 1 - - if((possible_phrase[1] in GLOB.department_radio_prefixes) && (copytext_char(possible_phrase, 2, 3) in GLOB.department_radio_keys)) - possible_phrase = "[useradio?pick(available_channels):""][copytext_char(possible_phrase, 3)]" //crop out the channel prefix - else - possible_phrase = "[useradio?pick(available_channels):""][possible_phrase]" - - newspeak.Add(possible_phrase) - - else //If we have no headset or channels to use, dont try to use any! - for(var/possible_phrase in speak) - if((possible_phrase[1] in GLOB.department_radio_prefixes) && (copytext_char(possible_phrase, 2, 3) in GLOB.department_radio_keys)) - possible_phrase = copytext_char(possible_phrase, 3) //crop out the channel prefix - newspeak.Add(possible_phrase) - speak = newspeak - - //Search for item to steal - set_parrot_interest(search_for_item()) - if(parrot_interest) - manual_emote("looks in [parrot_interest]'s direction and takes flight.") - parrot_state = PARROT_SWOOP | PARROT_STEAL - icon_state = icon_living - return - -//-----WANDERING - This is basically a 'I dont know what to do yet' state - else if(parrot_state == PARROT_WANDER) - //Stop movement, we'll set it later - SSmove_manager.stop_looping(src) - set_parrot_interest(null) - - //Wander around aimlessly. This will help keep the loops from searches down - //and possibly move the mob into a new are in view of something they can use - if(prob(90)) - step(src, pick(GLOB.cardinals)) - return - - if(!held_item && !parrot_perch) //If we've got nothing to do.. look for something to do. - var/atom/movable/AM = search_for_perch_and_item() //This handles checking through lists so we know it's either a perch or stealable item - if(AM) - if(isitem(AM) || isliving(AM)) //If stealable item - set_parrot_interest(AM) - manual_emote("turns and flies towards [parrot_interest].") - parrot_state = PARROT_SWOOP | PARROT_STEAL - return - else //Else it's a perch - parrot_perch = AM - parrot_state = PARROT_SWOOP | PARROT_RETURN - return - return - - if(parrot_interest && (parrot_interest in view(src))) - parrot_state = PARROT_SWOOP | PARROT_STEAL - return - - if(parrot_perch && (parrot_perch in view(src))) - parrot_state = PARROT_SWOOP | PARROT_RETURN - return - - else //Have an item but no perch? Find one! - parrot_perch = search_for_perch() - if(parrot_perch) - parrot_state = PARROT_SWOOP | PARROT_RETURN - return -//-----STEALING - else if(parrot_state == (PARROT_SWOOP | PARROT_STEAL)) - SSmove_manager.stop_looping(src) - if(!parrot_interest || held_item) - parrot_state = PARROT_SWOOP | PARROT_RETURN - return - - if(!(parrot_interest in view(src))) - parrot_state = PARROT_SWOOP | PARROT_RETURN - return - - if(Adjacent(parrot_interest)) - - if(isliving(parrot_interest)) - steal_from_mob() - - else //This should ensure that we only grab the item we want, and make sure it's not already collected on our perch - if(!parrot_perch || parrot_interest.loc != parrot_perch.loc) - held_item = parrot_interest - parrot_interest.forceMove(src) - visible_message(span_notice("[src] grabs [held_item]!"), span_notice("You grab [held_item]!"), span_hear("You hear the sounds of wings flapping furiously.")) - - set_parrot_interest(null) - parrot_state = PARROT_SWOOP | PARROT_RETURN - return - - SSmove_manager.move_to(src, parrot_interest, 1, parrot_speed) - if(isStuck()) - return - - return - -//-----RETURNING TO PERCH - else if(parrot_state == (PARROT_SWOOP | PARROT_RETURN)) - SSmove_manager.stop_looping(src) - if(!parrot_perch || !isturf(parrot_perch.loc)) //Make sure the perch exists and somehow isn't inside of something else. - parrot_perch = null - parrot_state = PARROT_WANDER - return - - if(Adjacent(parrot_perch)) - forceMove(parrot_perch.loc) - drop_held_item() - parrot_state = PARROT_PERCH - icon_state = icon_sit - return - - SSmove_manager.move_to(src, parrot_perch, 1, parrot_speed) - if(isStuck()) - return - - return - -//-----FLEEING - else if(parrot_state == (PARROT_SWOOP | PARROT_FLEE)) - SSmove_manager.stop_looping(src) - if(!parrot_interest || !isliving(parrot_interest)) //Sanity - parrot_state = PARROT_WANDER - - SSmove_manager.move_away(src, parrot_interest, 1, parrot_speed) - if(isStuck()) - return - - return - -//-----ATTACKING - else if(parrot_state == (PARROT_SWOOP | PARROT_ATTACK)) - - //If we're attacking a nothing, an object, a turf or a ghost for some stupid reason, switch to wander - if(!parrot_interest || !isliving(parrot_interest)) - set_parrot_interest(null) - parrot_state = PARROT_WANDER - return - - var/mob/living/L = parrot_interest - if(melee_damage_upper == 0) - melee_damage_upper = parrot_damage_upper - set_combat_mode(TRUE) - - //If the mob is close enough to interact with - if(Adjacent(parrot_interest)) - - //If the mob we've been chasing/attacking dies or falls into crit, check for loot! - if(L.stat) - set_parrot_interest(null) - if(!held_item) - held_item = steal_from_ground() - if(!held_item) - held_item = steal_from_mob() //Apparently it's possible for dead mobs to hang onto items in certain circumstances. - if(parrot_perch in view(src)) //If we have a home nearby, go to it, otherwise find a new home - parrot_state = PARROT_SWOOP | PARROT_RETURN - else - parrot_state = PARROT_WANDER - return - - attack_verb_continuous = pick("claws at", "chomps") - attack_verb_simple = pick("claw at", "chomp") - L.attack_animal(src)//Time for the hurt to begin! - //Otherwise, fly towards the mob! - else - SSmove_manager.move_to(src, parrot_interest, 1, parrot_speed) - if(isStuck()) - return - - return -//-----STATE MISHAP - else //This should not happen. If it does lets reset everything and try again - SSmove_manager.stop_looping(src) - set_parrot_interest(null) - parrot_perch = null - drop_held_item() - parrot_state = PARROT_WANDER - return - -/* - * Procs - */ - -/mob/living/simple_animal/parrot/proc/set_parrot_interest(atom/movable/shiny) - if(parrot_interest) - UnregisterSignal(parrot_interest, COMSIG_QDELETING) - parrot_interest = shiny - if(parrot_interest) - RegisterSignal(parrot_interest, COMSIG_QDELETING, PROC_REF(shiny_deleted)) - -/mob/living/simple_animal/parrot/proc/shiny_deleted(datum/source) - SIGNAL_HANDLER - set_parrot_interest(null) - -/mob/living/simple_animal/parrot/proc/isStuck() - //Check to see if the parrot is stuck due to things like windows or doors or windowdoors - if(parrot_lastmove) - if(parrot_lastmove == src.loc) - if(parrot_stuck_threshold >= ++parrot_stuck) //If it has been stuck for a while, go back to wander. - parrot_state = PARROT_WANDER - parrot_stuck = 0 - parrot_lastmove = null - return TRUE - else - parrot_lastmove = null - else - parrot_lastmove = src.loc - return FALSE - -/mob/living/simple_animal/parrot/proc/search_for_item() - var/item - for(var/atom/movable/AM in view(src)) - //Skip items we already stole or are wearing or are too big - if(parrot_perch && AM.loc == parrot_perch.loc || AM.loc == src) - continue - if(isitem(AM)) - var/obj/item/I = AM - if(I.w_class < WEIGHT_CLASS_SMALL) - item = I - else if(iscarbon(AM)) - var/mob/living/carbon/C = AM - for(var/obj/item/I in C.held_items) - if(I.w_class <= WEIGHT_CLASS_SMALL) - item = I - break - if(item) - if(!length(get_path_to(src, item))) // WHY DO WE DISREGARD THE PATH AHHHHHH - item = null - continue - return item - -/mob/living/simple_animal/parrot/proc/search_for_perch() - for(var/obj/O in view(src)) - for(var/path in desired_perches) - if(istype(O, path)) - return O - return null - -//This proc was made to save on doing two 'in view' loops seperatly -/mob/living/simple_animal/parrot/proc/search_for_perch_and_item() - for(var/atom/movable/AM in view(src)) - for(var/perch_path in desired_perches) - if(istype(AM, perch_path)) - return AM - - //Skip items we already stole or are wearing or are too big - if(parrot_perch && AM.loc == parrot_perch.loc || AM.loc == src) - continue - - if(isitem(AM)) - var/obj/item/I = AM - if(I.w_class <= WEIGHT_CLASS_SMALL) - return I - - if(iscarbon(AM)) - var/mob/living/carbon/C = AM - for(var/obj/item/I in C.held_items) - if(I.w_class <= WEIGHT_CLASS_SMALL) - return C - return null - - -/* - * Verbs - These are actually procs, but can be used as verbs by player-controlled parrots. - */ -/mob/living/simple_animal/parrot/proc/steal_from_ground() - set name = "Steal from ground" - set category = "Parrot" - set desc = "Grabs a nearby item." - - if(stat) - return -1 - - if(held_item) - to_chat(src, span_warning("You are already holding [held_item]!")) - return 1 - - for(var/obj/item/I in view(1,src)) - //Make sure we're not already holding it and it's small enough - if(I.loc != src && I.w_class <= WEIGHT_CLASS_SMALL) - - //If we have a perch and the item is sitting on it, continue - if(!client && parrot_perch && I.loc == parrot_perch.loc) - continue - - held_item = I - I.forceMove(src) - visible_message(span_notice("[src] grabs [held_item]!"), span_notice("You grab [held_item]!"), span_hear("You hear the sounds of wings flapping furiously.")) - return held_item - - to_chat(src, span_warning("There is nothing of interest to take!")) - return 0 - -/mob/living/simple_animal/parrot/proc/steal_from_mob() - set name = "Steal from mob" - set category = "Parrot" - set desc = "Steals an item right out of a person's hand!" - - if(stat) - return -1 - - if(held_item) - to_chat(src, span_warning("You are already holding [held_item]!")) - return 1 - - var/obj/item/stolen_item = null - - for(var/mob/living/carbon/C in view(1,src)) - for(var/obj/item/I in C.held_items) - if(I.w_class <= WEIGHT_CLASS_SMALL) - stolen_item = I - break - - if(stolen_item) - C.transferItemToLoc(stolen_item, src, TRUE) - held_item = stolen_item - visible_message(span_notice("[src] grabs [held_item] out of [C]'s hand!"), span_notice("You snag [held_item] out of [C]'s hand!"), span_hear("You hear the sounds of wings flapping furiously.")) - return held_item - - to_chat(src, span_warning("There is nothing of interest to take!")) - return 0 - -/mob/living/simple_animal/parrot/verb/drop_held_item_player() - set name = "Drop held item" - set category = "Parrot" - set desc = "Drop the item you're holding." - - if(stat) - return - - src.drop_held_item() - - return - -/mob/living/simple_animal/parrot/proc/drop_held_item(drop_gently = 1) - set name = "Drop held item" - set category = "Parrot" - set desc = "Drop the item you're holding." - - if(stat) - return -1 - - if(!held_item) - if(src == usr) //So that other mobs won't make this message appear when they're bludgeoning you. - to_chat(src, span_warning("You have nothing to drop!")) - return 0 - - -//parrots will eat crackers instead of dropping them - if(istype(held_item, /obj/item/food/cracker) && (drop_gently)) - qdel(held_item) - held_item = null - if(health < maxHealth) - adjustBruteLoss(-10) - manual_emote("[src] eagerly downs the cracker.") - return 1 - - - if(!drop_gently) - if(isgrenade(held_item)) - var/obj/item/grenade/G = held_item - G.forceMove(drop_location()) - G.detonate() - to_chat(src, span_danger("You let go of [held_item]!")) - held_item = null - return 1 - - to_chat(src, span_notice("You drop [held_item].")) - - held_item.forceMove(drop_location()) - held_item = null - return 1 - -/mob/living/simple_animal/parrot/proc/perch_player() - set name = "Sit" - set category = "Parrot" - set desc = "Sit on a nice comfy perch." - - if(stat || !client) - return - - if(icon_state == icon_living) - for(var/atom/movable/AM in view(src,1)) - for(var/perch_path in desired_perches) - if(istype(AM, perch_path)) - src.forceMove(AM.loc) - icon_state = icon_sit - parrot_state = PARROT_PERCH - return - to_chat(src, span_warning("There is no perch nearby to sit on!")) - return - -/mob/living/simple_animal/parrot/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) - . = ..() - if(. && !stat && client && parrot_state == PARROT_PERCH) - parrot_state = PARROT_WANDER - icon_state = icon_living - pixel_x = initial(pixel_x) - pixel_y = initial(pixel_y) - -/mob/living/simple_animal/parrot/proc/perch_mob_player() - set name = "Sit on Human's Shoulder" - set category = "Parrot" - set desc = "Sit on a nice comfy human being!" - - if(stat || !client) - return - - if(!buckled) - for(var/mob/living/carbon/human/H in view(src,1)) - if(H.has_buckled_mobs() && H.buckled_mobs.len >= H.max_buckled_mobs) //Already has a parrot, or is being eaten by a slime - continue - perch_on_human(H) - return - to_chat(src, span_warning("There is nobody nearby that you can sit on!")) - else - icon_state = icon_living - parrot_state = PARROT_WANDER - if(buckled) - to_chat(src, span_notice("You are no longer sitting on [buckled]'s shoulder.")) - buckled.unbuckle_mob(src, TRUE) - buckled = null - pixel_x = initial(pixel_x) - pixel_y = initial(pixel_y) - - - -/mob/living/simple_animal/parrot/proc/perch_on_human(mob/living/carbon/human/H) - if(!H) - return - forceMove(get_turf(H)) - if(H.buckle_mob(src, TRUE)) - pixel_y = 9 - pixel_x = pick(-8,8) //pick left or right shoulder - icon_state = icon_sit - parrot_state = PARROT_PERCH - to_chat(src, span_notice("You sit on [H]'s shoulder.")) - - -/mob/living/simple_animal/parrot/proc/toggle_mode() - set name = "Toggle mode" - set category = "Parrot" - set desc = "Time to bear those claws!" - - if(stat || !client) - return - - if(combat_mode) - melee_damage_upper = 0 - set_combat_mode(FALSE) - else - melee_damage_upper = parrot_damage_upper - set_combat_mode(TRUE) - to_chat(src, span_notice("You will now [combat_mode ? "Harm" : "Help"] others.")) - return - -/mob/living/simple_animal/parrot/natural - spawn_headset = FALSE -/* - * Sub-types - */ -/mob/living/simple_animal/parrot/poly - name = "Poly" - desc = "Poly the Parrot. An expert on quantum cracker theory." - speak = list("Poly wanna cracker!", ":e Check the crystal, you chucklefucks!",":e Wire the solars, you lazy bums!",":e WHO TOOK THE DAMN MODSUITS?",":e OH GOD ITS ABOUT TO DELAMINATE CALL THE SHUTTLE") - gold_core_spawnable = NO_SPAWN - speak_chance = 3 - - var/memory_saved = FALSE - var/rounds_survived = 0 - var/longest_survival = 0 - var/longest_deathstreak = 0 - - -/mob/living/simple_animal/parrot/poly/Initialize(mapload) - ears = new /obj/item/radio/headset/headset_eng(src) - if(SStts.tts_enabled) - voice = pick(SStts.available_speakers) - if(SStts.pitch_enabled) - if(findtext(voice, "Woman")) - pitch = 12 // up-pitch by one octave - else - pitch = 24 // up-pitch by 2 octaves - else - voice_filter = "rubberband=pitch=1.5" // Use the filter to pitch up if we can't naturally pitch up. - - available_channels = list(":e") - Read_Memory() - if(rounds_survived == longest_survival) - speak += pick("...[longest_survival].", "The things I've seen!", "I have lived many lives!", "What are you before me?") - desc += " Old as sin, and just as loud. Claimed to be [rounds_survived]." - speak_chance = 20 //His hubris has made him more annoying/easier to justify killing - add_atom_colour("#EEEE22", FIXED_COLOUR_PRIORITY) - else if(rounds_survived == longest_deathstreak) - speak += pick("What are you waiting for!", "Violence breeds violence!", "Blood! Blood!", "Strike me down if you dare!") - desc += " The squawks of [-rounds_survived] dead parrots ring out in your ears..." - add_atom_colour("#BB7777", FIXED_COLOUR_PRIORITY) - else if(rounds_survived > 0) - speak += pick("...again?", "No, It was over!", "Let me out!", "It never ends!") - desc += " Over [rounds_survived] shifts without a \"terrible\" \"accident\"!" - else - speak += pick("...alive?", "This isn't parrot heaven!", "I live, I die, I live again!", "The void fades!") - - . = ..() - - // Ensure 1 Poly exists - REGISTER_REQUIRED_MAP_ITEM(1, 1) - -/mob/living/simple_animal/parrot/poly/Life(seconds_per_tick = SSMOBS_DT, times_fired) - if(!stat && SSticker.current_state == GAME_STATE_FINISHED && !memory_saved) - Write_Memory(FALSE) - memory_saved = TRUE - ..() - -/mob/living/simple_animal/parrot/poly/death(gibbed) - if(HAS_TRAIT(src, TRAIT_DONT_WRITE_MEMORY)) - return ..() // Don't read memory either. - if(!memory_saved) - Write_Memory(TRUE) - if(rounds_survived == longest_survival || rounds_survived == longest_deathstreak || prob(0.666)) - var/mob/living/simple_animal/parrot/poly/ghost/G = new(loc) - if(mind) - mind.transfer_to(G) - else - G.key = key - return ..() - -/mob/living/simple_animal/parrot/poly/proc/Read_Memory() - if(fexists("data/npc_saves/Poly.sav")) //legacy compatability to convert old format to new - var/savefile/S = new /savefile("data/npc_saves/Poly.sav") - S["phrases"] >> speech_buffer - S["roundssurvived"] >> rounds_survived - S["longestsurvival"] >> longest_survival - S["longestdeathstreak"] >> longest_deathstreak - fdel("data/npc_saves/Poly.sav") - else - var/json_file = file("data/npc_saves/Poly.json") - if(!fexists(json_file)) - return - var/list/json = json_decode(file2text(json_file)) - speech_buffer = json["phrases"] - rounds_survived = json["roundssurvived"] - longest_survival = json["longestsurvival"] - longest_deathstreak = json["longestdeathstreak"] - if(!islist(speech_buffer)) - speech_buffer = list() - -/mob/living/simple_animal/parrot/poly/Write_Memory(dead, gibbed) - . = ..() - if(!.) - return - var/json_file = file("data/npc_saves/Poly.json") - var/list/file_data = list() - if(islist(speech_buffer)) - file_data["phrases"] = speech_buffer - if(dead) - file_data["roundssurvived"] = min(rounds_survived - 1, 0) - file_data["longestsurvival"] = longest_survival - if(rounds_survived - 1 < longest_deathstreak) - file_data["longestdeathstreak"] = rounds_survived - 1 - else - file_data["longestdeathstreak"] = longest_deathstreak - else - file_data["roundssurvived"] = max(rounds_survived, 0) + 1 - if(rounds_survived + 1 > longest_survival) - file_data["longestsurvival"] = rounds_survived + 1 - else - file_data["longestsurvival"] = longest_survival - file_data["longestdeathstreak"] = longest_deathstreak - fdel(json_file) - WRITE_FILE(json_file, json_encode(file_data)) - -/mob/living/simple_animal/parrot/poly/ghost - name = "The Ghost of Poly" - desc = "Doomed to squawk the Earth." - color = "#FFFFFF77" - speak_chance = 20 - status_flags = GODMODE - sentience_type = SENTIENCE_BOSS //This is so players can't mindswap into ghost poly to become a literal god - incorporeal_move = INCORPOREAL_MOVE_BASIC - butcher_results = list(/obj/item/ectoplasm = 1) - -/mob/living/simple_animal/parrot/poly/ghost/Initialize(mapload) - memory_saved = TRUE //At this point nothing is saved - . = ..() - -/mob/living/simple_animal/parrot/poly/ghost/handle_automated_speech() - if(ismob(loc)) - return - ..() - -/mob/living/simple_animal/parrot/poly/ghost/handle_automated_movement() - if(isliving(parrot_interest)) - if(!ishuman(parrot_interest)) - set_parrot_interest(null) - else if(parrot_state == (PARROT_SWOOP | PARROT_ATTACK) && Adjacent(parrot_interest)) - SSmove_manager.move_to(src, parrot_interest, 0, parrot_speed) - Possess(parrot_interest) - ..() - -/mob/living/simple_animal/parrot/poly/ghost/proc/Possess(mob/living/carbon/human/H) - if(!ishuman(H)) - return - var/datum/disease/parrot_possession/P = new - P.parrot = src - forceMove(H) - H.ForceContractDisease(P, FALSE) - set_parrot_interest(null) - H.visible_message(span_danger("[src] dive bombs into [H]'s chest and vanishes!"), span_userdanger("[src] dive bombs into your chest, vanishing! This can't be good!")) - -#undef PARROT_PERCH -#undef PARROT_SWOOP -#undef PARROT_WANDER -#undef PARROT_STEAL -#undef PARROT_ATTACK -#undef PARROT_RETURN -#undef PARROT_FLEE diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm index 790f9312ea170..6136f8c818964 100644 --- a/code/modules/mob/transform_procs.dm +++ b/code/modules/mob/transform_procs.dm @@ -398,7 +398,7 @@ return TRUE if(ispath(MP, /mob/living/basic/bear)) return TRUE - if(ispath(MP, /mob/living/simple_animal/parrot)) + if(ispath(MP, /mob/living/basic/parrot)) return TRUE //Parrots are no longer unfinished! -Nodrak //Not in here? Must be untested! diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index 8761606568281..b0a75108d569c 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -324,11 +324,14 @@ return FALSE . = ..() + if(!forced) + add_log("manual overriding of permissions and modification of device firmware detected. Reboot and reinstall required.") obj_flags |= EMAGGED device_theme = PDA_THEME_SYNDICATE - balloon_alert(user, "syndieOS loaded") - if (emag_card) - to_chat(user, span_notice("You swipe \the [src] with [emag_card]. A console window momentarily fills the screen, with white text rapidly scrolling past.")) + if(user) + balloon_alert(user, "syndieOS loaded") + if (emag_card) + to_chat(user, span_notice("You swipe \the [src] with [emag_card]. A console window momentarily fills the screen, with white text rapidly scrolling past.")) return TRUE /obj/item/modular_computer/examine(mob/user) diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm index 7ac9ade2c0b14..d050bd804b327 100644 --- a/code/modules/modular_computers/file_system/program.dm +++ b/code/modules/modular_computers/file_system/program.dm @@ -84,9 +84,9 @@ ///Attempts to generate an Ntnet log, returns the log on success, FALSE otherwise. /datum/computer_file/program/proc/generate_network_log(text) - if(computer) - return computer.add_log(text) - return FALSE + if(!computer || computer.obj_flags & EMAGGED) + return FALSE + return computer.add_log(text) /** *Runs when the device is used to attack an atom in non-combat mode using right click (secondary). diff --git a/code/modules/modular_computers/file_system/programs/frontier.dm b/code/modules/modular_computers/file_system/programs/frontier.dm index df57302871b45..3b001e9f39d23 100644 --- a/code/modules/modular_computers/file_system/programs/frontier.dm +++ b/code/modules/modular_computers/file_system/programs/frontier.dm @@ -65,9 +65,9 @@ singular_partner["path"] = partner.type singular_partner["boostedNodes"] = list() singular_partner["acceptedExperiments"] = list() - for (var/node_id in partner.boosted_nodes) + for (var/node_id in partner.boostable_nodes) var/datum/techweb_node/node = SSresearch.techweb_node_by_id(node_id) - singular_partner["boostedNodes"] += list(list("name" = node.display_name, "discount" = partner.boosted_nodes[node_id], "id"=node_id)) + singular_partner["boostedNodes"] += list(list("name" = node.display_name, "discount" = partner.boostable_nodes[node_id], "id" = node_id)) for (var/datum/experiment/ordnance/ordnance_experiment as anything in partner.accepted_experiments) singular_partner["acceptedExperiments"] += initial(ordnance_experiment.name) parsed_partners += list(singular_partner) @@ -154,7 +154,7 @@ data["purchaseableBoosts"][partner.type] = list() for(var/node_id in linked_techweb.get_available_nodes()) // Not from our partner - if(!(node_id in partner.boosted_nodes)) + if(!(node_id in partner.boostable_nodes)) continue if(!partner.allowed_to_boost(linked_techweb, node_id)) continue diff --git a/code/modules/recycling/disposal/eject.dm b/code/modules/recycling/disposal/eject.dm index febd4f5d604e0..523f034703c10 100644 --- a/code/modules/recycling/disposal/eject.dm +++ b/code/modules/recycling/disposal/eject.dm @@ -2,11 +2,33 @@ * General proc used to expel a holder's contents through src (for bins holder is also the src). */ /obj/proc/pipe_eject(obj/holder, direction, throw_em = TRUE, turf/target, throw_range = 5, throw_speed = 1) - var/turf/src_T = get_turf(src) - for(var/A in holder) - var/atom/movable/AM = A - AM.forceMove(src_T) - SEND_SIGNAL(AM, COMSIG_MOVABLE_PIPE_EJECTING, direction) - if(throw_em && !QDELETED(AM)) - var/turf/T = target || get_offset_target_turf(loc, rand(5)-rand(5), rand(5)-rand(5)) - AM.throw_at(T, throw_range, throw_speed) + var/turf/origin_turf = get_turf(src) + var/turf/target_turf + if(isnull(target)) // done up here as a safety + target_turf = get_offset_target_turf(loc, rand(5) - rand(5), rand(5) - rand(5)) + else + target_turf = target + + if(QDELETED(origin_turf)) + stack_trace("pipe_eject() attempted to operate on a qdeleted turf! In order to avoid sending things to nullspace, we are going to send everything directly to the target turf instead.") + origin_turf = target_turf + + var/list/contents_to_eject = holder.contents + var/list/contents_to_throw = list() + + for(var/atom/movable/thing in contents_to_eject) + thing.forceMove(origin_turf) + SEND_SIGNAL(thing, COMSIG_MOVABLE_PIPE_EJECTING, direction) + if(QDELETED(thing)) + continue + + contents_to_throw += thing + + if(!throw_em) + return + + for(var/atom/movable/throwable as anything in contents_to_throw) + if(isnull(target)) // we want the thrown things to be spread out a bit if we weren't given a target + target_turf = get_offset_target_turf(loc, rand(5) - rand(5), rand(5) - rand(5)) + + throwable.throw_at(target_turf, throw_range, throw_speed) diff --git a/code/modules/recycling/disposal/holder.dm b/code/modules/recycling/disposal/holder.dm index cf9ae8f6b4e10..2d964d0f8fb3f 100644 --- a/code/modules/recycling/disposal/holder.dm +++ b/code/modules/recycling/disposal/holder.dm @@ -42,7 +42,7 @@ if(M.client) M.reset_perspective(src) hasmob = TRUE - RegisterSignal(M, COMSIG_LIVING_RESIST, PROC_REF(struggle_prep), M) + RegisterSignal(M, COMSIG_LIVING_RESIST, PROC_REF(struggle_prep)) //Checks 1 contents level deep. This means that players can be sent through disposals mail... //...but it should require a second person to open the package. (i.e. person inside a wrapped locker) @@ -179,12 +179,12 @@ /// Merge two holder objects, used when a holder meets a stuck holder /obj/structure/disposalholder/proc/merge(obj/structure/disposalholder/other) - for(var/A in other) - var/atom/movable/AM = A - AM.forceMove(src) // move everything in other holder to this one - if(ismob(AM)) - var/mob/M = AM - M.reset_perspective(src) // if a client mob, update eye to follow this holder + for(var/atom/movable/movable as anything in other) + movable.forceMove(src) // move everything in other holder to this one + if(ismob(movable)) + var/mob/mob = movable + mob.reset_perspective(src) // if a client mob, update eye to follow this holder + RegisterSignal(mob, COMSIG_LIVING_RESIST, PROC_REF(struggle_prep)) hasmob = TRUE if(destinationTag == 0 && other.destinationTag != 0) destinationTag = other.destinationTag diff --git a/code/modules/recycling/disposal/outlet.dm b/code/modules/recycling/disposal/outlet.dm index 4327567fb1746..4fa9f1e0aab15 100644 --- a/code/modules/recycling/disposal/outlet.dm +++ b/code/modules/recycling/disposal/outlet.dm @@ -47,6 +47,11 @@ /obj/structure/disposaloutlet/Destroy() if(trunk) + // preemptively expel the contents from the trunk + // in case the outlet is deleted before expel_holder could be called. + var/obj/structure/disposalholder/holder = locate() in trunk + if(holder) + trunk.expel(holder) trunk.linked = null trunk = null QDEL_NULL(stored) @@ -60,15 +65,15 @@ if((start_eject + 30) < world.time) start_eject = world.time playsound(src, 'sound/machines/warning-buzzer.ogg', 50, FALSE, FALSE) - addtimer(CALLBACK(src, PROC_REF(expel_holder), H, TRUE), 20) + addtimer(CALLBACK(src, PROC_REF(expel_holder), H, TRUE), 2 SECONDS) else - addtimer(CALLBACK(src, PROC_REF(expel_holder), H), 20) + addtimer(CALLBACK(src, PROC_REF(expel_holder), H), 2 SECONDS) /obj/structure/disposaloutlet/proc/expel_holder(obj/structure/disposalholder/H, playsound=FALSE) if(playsound) playsound(src, 'sound/machines/hiss.ogg', 50, FALSE, FALSE) - if(!H) + if(QDELETED(H)) return pipe_eject(H, dir, TRUE, target, eject_range, eject_speed) diff --git a/code/modules/research/destructive_analyzer.dm b/code/modules/research/destructive_analyzer.dm index 2d1e786208cfd..03a6d9934f5f7 100644 --- a/code/modules/research/destructive_analyzer.dm +++ b/code/modules/research/destructive_analyzer.dm @@ -1,225 +1,189 @@ -/* -Destructive Analyzer - -It is used to destroy hand-held objects and advance technological research. Controls are in the linked R&D console. - -Note: Must be placed within 3 tiles of the R&D Console -*/ +///How much power it costs to deconstruct an item. +#define DESTRUCTIVE_ANALYZER_POWER_USAGE (BASE_MACHINE_IDLE_CONSUMPTION * 2.5) +///The 'ID' for deconstructing items for Research points instead of nodes. +#define DESTRUCTIVE_ANALYZER_DESTROY_POINTS "research_points" + +/** + * ## Destructive Analyzer + * It is used to destroy hand-held objects and advance technological research. + */ /obj/machinery/rnd/destructive_analyzer name = "destructive analyzer" desc = "Learn science by destroying things!" icon_state = "d_analyzer" base_icon_state = "d_analyzer" circuit = /obj/item/circuitboard/machine/destructive_analyzer - var/decon_mod = 0 -/obj/machinery/rnd/destructive_analyzer/RefreshParts() +/obj/machinery/rnd/destructive_analyzer/Initialize(mapload) . = ..() - var/T = 0 - for(var/datum/stock_part/stock_part in component_parts) - T += stock_part.tier - decon_mod = T - -/obj/machinery/rnd/destructive_analyzer/proc/ConvertReqString2List(list/source_list) - var/list/temp_list = params2list(source_list) - for(var/O in temp_list) - temp_list[O] = text2num(temp_list[O]) - return temp_list - -/obj/machinery/rnd/destructive_analyzer/Insert_Item(obj/item/O, mob/living/user) - if(!user.combat_mode) - . = 1 - if(!is_insertion_ready(user)) - return - if(!user.transferItemToLoc(O, src)) - to_chat(user, span_warning("\The [O] is stuck to your hand, you cannot put it in the [src.name]!")) - return - busy = TRUE - loaded_item = O - to_chat(user, span_notice("You add the [O.name] to the [src.name]!")) - flick("d_analyzer_la", src) - addtimer(CALLBACK(src, PROC_REF(finish_loading)), 10) - updateUsrDialog() + register_context() + +/obj/machinery/rnd/destructive_analyzer/add_context(atom/source, list/context, obj/item/held_item, mob/living/user) + if(loaded_item) + context[SCREENTIP_CONTEXT_ALT_LMB] = "Remove Item" + else if(!isnull(held_item)) + context[SCREENTIP_CONTEXT_LMB] = "Insert Item" + return CONTEXTUAL_SCREENTIP_SET + +/obj/machinery/rnd/destructive_analyzer/attackby(obj/item/weapon, mob/living/user, params) + if(user.combat_mode) + return ..() + if(!is_insertion_ready(user)) + return ..() + if(!user.transferItemToLoc(weapon, src)) + to_chat(user, span_warning("\The [weapon] is stuck to your hand, you cannot put it in the [name]!")) + return TRUE + busy = TRUE + loaded_item = weapon + to_chat(user, span_notice("You add the [weapon.name] to the [name]!")) + flick("[base_icon_state]_la", src) + addtimer(CALLBACK(src, PROC_REF(finish_loading)), 1 SECONDS) + return TRUE -/obj/machinery/rnd/destructive_analyzer/proc/finish_loading() - update_appearance() - reset_busy() +/obj/machinery/rnd/destructive_analyzer/AltClick(mob/user) + . = ..() + unload_item() /obj/machinery/rnd/destructive_analyzer/update_icon_state() icon_state = "[base_icon_state][loaded_item ? "_l" : null]" return ..() -/obj/machinery/rnd/destructive_analyzer/proc/destroy_item(obj/item/thing, innermode = FALSE) - if(QDELETED(thing) || QDELETED(src)) - return FALSE - if(!innermode) - flick("d_analyzer_process", src) - busy = TRUE - addtimer(CALLBACK(src, PROC_REF(reset_busy)), 24) - use_power(250) - if(thing == loaded_item) - loaded_item = null - var/list/food = thing.GetDeconstructableContents() - for(var/obj/item/innerthing in food) - destroy_item(innerthing, TRUE) - for(var/mob/living/victim in thing) - if(victim.stat != DEAD) - victim.investigate_log("has been killed by a destructive analyzer.", INVESTIGATE_DEATHS) - victim.death() - - qdel(thing) - loaded_item = null - if (!innermode) - update_appearance() - return TRUE +/obj/machinery/rnd/destructive_analyzer/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "DestructiveAnalyzer") + ui.open() + +/obj/machinery/rnd/destructive_analyzer/ui_data(mob/user) + var/list/data = list() + data["server_connected"] = !!stored_research + data["node_data"] = list() + if(loaded_item) + data["item_icon"] = icon2base64(getFlatIcon(image(icon = loaded_item.icon, icon_state = loaded_item.icon_state), no_anim = TRUE)) + data["indestructible"] = !(loaded_item.resistance_flags & INDESTRUCTIBLE) + data["loaded_item"] = loaded_item + data["already_deconstructed"] = !!stored_research.deconstructed_items[loaded_item.type] + var/list/points = techweb_item_point_check(loaded_item) + data["recoverable_points"] = techweb_point_display_generic(points) + + var/list/boostable_nodes = techweb_item_unlock_check(loaded_item) + for(var/id in boostable_nodes) + var/datum/techweb_node/unlockable_node = SSresearch.techweb_node_by_id(id) + var/list/node_data = list() + node_data["node_name"] = unlockable_node.display_name + node_data["node_id"] = unlockable_node.id + node_data["node_hidden"] = !!stored_research.hidden_nodes[unlockable_node.id] + data["node_data"] += list(node_data) + else + data["loaded_item"] = null + return data -/obj/machinery/rnd/destructive_analyzer/proc/user_try_decon_id(id, mob/user) - if(!istype(loaded_item)) - return FALSE +/obj/machinery/rnd/destructive_analyzer/ui_static_data(mob/user) + var/list/data = list() + data["research_point_id"] = DESTRUCTIVE_ANALYZER_DESTROY_POINTS + return data - if (id && id != RESEARCH_MATERIAL_DESTROY_ID) - var/datum/techweb_node/TN = SSresearch.techweb_node_by_id(id) - if(!istype(TN)) - return FALSE - var/dpath = loaded_item.type - var/list/worths = TN.boost_item_paths[dpath] - var/list/differences = list() - var/list/already_boosted = stored_research.boosted_nodes[TN.id] - for(var/i in worths) - var/used = already_boosted? already_boosted[i] : 0 - var/value = min(worths[i], TN.research_costs[i]) - used - if(value > 0) - differences[i] = value - if(length(worths) && !length(differences)) - return FALSE - var/choice = tgui_alert(user, "Are you sure you want to destroy [loaded_item] to [!length(worths) ? "reveal [TN.display_name]" : "boost [TN.display_name] by [json_encode(differences)] point\s"]?", "Destructive Analyzer", list("Proceed", "Cancel")) - if(choice != "Proceed") - return FALSE - if(QDELETED(loaded_item) || QDELETED(src)) - return FALSE - SSblackbox.record_feedback("nested tally", "item_deconstructed", 1, list("[TN.id]", "[loaded_item.type]")) - if(destroy_item(loaded_item)) - stored_research.boost_with_item(SSresearch.techweb_node_by_id(TN.id), dpath) +/obj/machinery/rnd/destructive_analyzer/ui_act(action, params, datum/tgui/ui) + . = ..() + if(.) + return - else - var/list/point_value = techweb_item_point_check(loaded_item) - if(stored_research.deconstructed_items[loaded_item.type]) - point_value = list() - var/user_mode_string = "" - if(length(point_value)) - user_mode_string = " for [json_encode(point_value)] points" - var/choice = tgui_alert(usr, "Are you sure you want to destroy [loaded_item][user_mode_string]?",, list("Proceed", "Cancel")) - if(choice == "Cancel") - return FALSE - if(QDELETED(loaded_item) || QDELETED(src)) - return FALSE - destroy_item(loaded_item) - return TRUE + var/mob/user = usr + switch(action) + if("eject_item") + if(busy) + balloon_alert(user, "already busy!") + return TRUE + if(loaded_item) + unload_item() + return TRUE + if("deconstruct") + if(!user_try_decon_id(params["deconstruct_id"])) + say("Destructive analysis failed!") + return TRUE + +//This allows people to put syndicate screwdrivers in the machine. Secondary act still passes. +/obj/machinery/rnd/destructive_analyzer/screwdriver_act(mob/living/user, obj/item/tool) + return FALSE +///Drops the loaded item where it can and nulls it. /obj/machinery/rnd/destructive_analyzer/proc/unload_item() if(!loaded_item) return FALSE - loaded_item.forceMove(get_turf(src)) + playsound(loc, 'sound/machines/terminal_insert_disc.ogg', 30, FALSE) + loaded_item.forceMove(drop_location()) loaded_item = null - update_appearance() + update_appearance(UPDATE_ICON) return TRUE -/obj/machinery/rnd/destructive_analyzer/ui_interact(mob/user) - . = ..() - var/datum/browser/popup = new(user, "destructive_analyzer", name, 900, 600) - popup.set_content(ui_deconstruct()) - popup.open() +///Called in a timer callback after loading something into it, this handles resetting the 'busy' state back to its initial state +///So the machine can be used. +/obj/machinery/rnd/destructive_analyzer/proc/finish_loading() + update_appearance(UPDATE_ICON) + reset_busy() -/obj/machinery/rnd/destructive_analyzer/proc/ui_deconstruct() //Legacy code - var/list/l = list() - if(!loaded_item) - l += "
No item loaded. Standing-by...
" - else - l += "
[RDSCREEN_NOBREAK]" - l += "
[icon2html(loaded_item, usr)][loaded_item.name] Eject
[RDSCREEN_NOBREAK]" - l += "Select a node to boost by deconstructing this item. This item can boost:" +/** + * Destroys an item by going through all its contents (including itself) and calling destroy_item_individual + * Args: + * gain_research_points - Whether deconstructing each individual item should check for research points to boost. + */ +/obj/machinery/rnd/destructive_analyzer/proc/destroy_item(gain_research_points = FALSE) + if(QDELETED(loaded_item) || QDELETED(src)) + return FALSE + flick("[base_icon_state]_process", src) + busy = TRUE + addtimer(CALLBACK(src, PROC_REF(reset_busy)), 2.4 SECONDS) + use_power(DESTRUCTIVE_ANALYZER_POWER_USAGE) + var/list/all_contents = loaded_item.get_all_contents() + for(var/innerthing in all_contents) + destroy_item_individual(innerthing, gain_research_points) - var/anything = FALSE - var/list/boostable_nodes = techweb_item_boost_check(loaded_item) - for(var/id in boostable_nodes) - anything = TRUE - var/list/worth = boostable_nodes[id] - var/datum/techweb_node/N = SSresearch.techweb_node_by_id(id) - - l += "
[RDSCREEN_NOBREAK]" - if (stored_research.researched_nodes[N.id]) // already researched - l += "[N.display_name]" - l += "This node has already been researched." - else if(!length(worth)) // reveal only - if (stored_research.hidden_nodes[N.id]) - l += "[N.display_name]" - l += "This node will be revealed." - else - l += "[N.display_name]" - l += "This node has already been revealed." - else // boost by the difference - var/list/differences = list() - var/list/already_boosted = stored_research.boosted_nodes[N.id] - for(var/i in worth) - var/already_boosted_amount = already_boosted? stored_research.boosted_nodes[N.id][i] : 0 - var/amt = min(worth[i], N.research_costs[i]) - already_boosted_amount - if(amt > 0) - differences[i] = amt - if (length(differences)) - l += "[N.display_name]" - l += "This node will be boosted with the following:
[techweb_point_display_generic(differences)]" - else - l += "[N.display_name]" - l += "This node has already been boosted." - l += "
[RDSCREEN_NOBREAK]" - - var/list/point_values = techweb_item_point_check(loaded_item) - if(point_values) - anything = TRUE - l += "
[RDSCREEN_NOBREAK]" - if (stored_research.deconstructed_items[loaded_item.type]) - l += "Point Deconstruction" - l += "This item's points have already been claimed." - else - l += "Point Deconstruction" - l += "This item is worth:
[techweb_point_display_generic(point_values)]!" - l += "
[RDSCREEN_NOBREAK]" - - if(!(loaded_item.resistance_flags & INDESTRUCTIBLE)) - l += "
Destroy Item" - l += "
[RDSCREEN_NOBREAK]" - anything = TRUE - - if (!anything) - l += "Nothing!" - - l += "
" - - for(var/i in 1 to length(l)) - if(!findtextEx(l[i], RDSCREEN_NOBREAK)) - l[i] += "
" - . = l.Join("") - return replacetextEx(., RDSCREEN_NOBREAK, "") - -/obj/machinery/rnd/destructive_analyzer/Topic(raw, ls) - . = ..() - if(.) - return + loaded_item = null + update_appearance(UPDATE_ICON) + return TRUE + +/** + * Destroys the individual provided item + * Args: + * thing - The thing being destroyed. Generally an object, but it can be a mob too, such as intellicards and pAIs. + * gain_research_points - Whether deconstructing this should give research points to the stored techweb, if applicable. + */ +/obj/machinery/rnd/destructive_analyzer/proc/destroy_item_individual(obj/item/thing, gain_research_points = FALSE) + if(isliving(thing)) + var/mob/living/mob_thing = thing + if(mob_thing.stat != DEAD) + mob_thing.investigate_log("has been killed by a destructive analyzer.", INVESTIGATE_DEATHS) + mob_thing.death() + var/list/point_value = techweb_item_point_check(thing) + if(point_value && !stored_research.deconstructed_items[thing.type]) + stored_research.deconstructed_items[thing.type] = TRUE + stored_research.add_point_list(list(TECHWEB_POINT_TYPE_GENERIC = point_value)) + qdel(thing) - add_fingerprint(usr) - usr.set_machine(src) +/** + * Attempts to destroy the loaded item using a provided research id. + * Args: + * id - The techweb ID node that we're meant to unlock if applicable. + */ +/obj/machinery/rnd/destructive_analyzer/proc/user_try_decon_id(id) + if(!istype(loaded_item)) + return FALSE + if(isnull(id)) + return FALSE - if(ls["eject_item"]) //Eject the item inside the destructive analyzer. - if(busy) - to_chat(usr, span_danger("The destructive analyzer is busy at the moment.")) - return - if(loaded_item) - unload_item() - if(ls["deconstruct"]) - if(!user_try_decon_id(ls["deconstruct"], usr)) - say("Destructive analysis failed!") + if(id == DESTRUCTIVE_ANALYZER_DESTROY_POINTS) + if(!destroy_item(gain_research_points = TRUE)) + return FALSE + return TRUE - updateUsrDialog() + var/datum/techweb_node/node_to_discover = SSresearch.techweb_node_by_id(id) + if(!istype(node_to_discover)) + return FALSE + SSblackbox.record_feedback("nested tally", "item_deconstructed", 1, list("[node_to_discover.id]", "[loaded_item.type]")) + if(!destroy_item()) + return FALSE + stored_research.unhide_node(SSresearch.techweb_node_by_id(node_to_discover.id)) + return TRUE -/obj/machinery/rnd/destructive_analyzer/screwdriver_act(mob/living/user, obj/item/tool) - return FALSE +#undef DESTRUCTIVE_ANALYZER_DESTROY_POINTS +#undef DESTRUCTIVE_ANALYZER_POWER_USAGE diff --git a/code/modules/research/experimentor.dm b/code/modules/research/experimentor.dm index 9ffd5448a5db9..51f6c65a85664 100644 --- a/code/modules/research/experimentor.dm +++ b/code/modules/research/experimentor.dm @@ -42,12 +42,6 @@ var/static/list/valid_items //valid items for special reactions like transforming var/list/critical_items_typecache //items that can cause critical reactions -/obj/machinery/rnd/experimentor/proc/ConvertReqString2List(list/source_list) - var/list/temp_list = params2list(source_list) - for(var/O in temp_list) - temp_list[O] = text2num(temp_list[O]) - return temp_list - /obj/machinery/rnd/experimentor/proc/valid_items() RETURN_TYPE(/list) @@ -131,20 +125,22 @@ return FALSE return TRUE -/obj/machinery/rnd/experimentor/Insert_Item(obj/item/O, mob/living/user) - if(!user.combat_mode) - . = 1 - if(!is_insertion_ready(user)) - return - if(!user.transferItemToLoc(O, src)) - return - loaded_item = O - to_chat(user, span_notice("You add [O] to the machine.")) - flick("h_lathe_load", src) +/obj/machinery/rnd/experimentor/attackby(obj/item/weapon, mob/living/user, params) + if(user.combat_mode) + return ..() + if(!is_insertion_ready(user)) + return ..() + if(!user.transferItemToLoc(weapon, src)) + to_chat(user, span_warning("\The [weapon] is stuck to your hand, you cannot put it in the [name]!")) + return TRUE + loaded_item = weapon + to_chat(user, span_notice("You add [weapon] to the machine.")) + flick("h_lathe_load", src) + return TRUE /obj/machinery/rnd/experimentor/default_deconstruction_crowbar(obj/item/O) ejectItem() - . = ..(O) + return ..(O) /obj/machinery/rnd/experimentor/ui_interact(mob/user) var/list/dat = list("
") @@ -161,22 +157,19 @@ if(istype(loaded_item,/obj/item/relic)) dat += "Discover" dat += "Eject" - var/list/listin = techweb_item_boost_check(src) + var/list/listin = techweb_item_unlock_check(src) if(listin) var/list/output = list("Research Boost Data:") var/list/res = list("Already researched:") - var/list/boosted = list("Already boosted:") for(var/node_id in listin) var/datum/techweb_node/N = SSresearch.techweb_node_by_id(node_id) var/str = "[N.display_name]: [listin[N]] points." var/datum/techweb/science_web = locate(/datum/techweb/science) in SSresearch.techwebs if(science_web.researched_nodes[N.id]) res += str - else if(science_web.boosted_nodes[N.id]) - boosted += str if(science_web.visible_nodes[N.id]) //JOY OF DISCOVERY! output += str - output += boosted + res + output += res dat += output else dat += "Nothing loaded." @@ -218,9 +211,9 @@ experiment(dotype,process) use_power(750) if(dotype != FAIL) - var/list/nodes = techweb_item_boost_check(process) + var/list/nodes = techweb_item_unlock_check(process) var/picked = pick_weight(nodes) //This should work. - stored_research.boost_with_item(SSresearch.techweb_node_by_id(picked), process.type) + stored_research.unhide_node(SSresearch.techweb_node_by_id(picked)) updateUsrDialog() /obj/machinery/rnd/experimentor/proc/matchReaction(matching,reaction) @@ -648,10 +641,10 @@ /mob/living/basic/crab, /mob/living/basic/lizard, /mob/living/basic/mouse, + /mob/living/basic/parrot, /mob/living/basic/pet/dog/corgi, /mob/living/basic/pet/dog/pug, /mob/living/basic/pet/fox, - /mob/living/simple_animal/parrot/natural, /mob/living/simple_animal/pet/cat, ) for(var/counter in 1 to rand(1, 25)) diff --git a/code/modules/research/ordnance/_scipaper.dm b/code/modules/research/ordnance/_scipaper.dm index 38f35e75a7718..f1d94af76316a 100644 --- a/code/modules/research/ordnance/_scipaper.dm +++ b/code/modules/research/ordnance/_scipaper.dm @@ -287,21 +287,20 @@ /// List of ordnance experiments that our partner is willing to accept. If this list is not filled it means the partner will accept everything. var/list/accepted_experiments = list() /// Associative list of which technology the partner might be able to boost and by how much. - var/list/boosted_nodes = list() - + var/list/boostable_nodes = list() /datum/scientific_partner/proc/purchase_boost(datum/techweb/purchasing_techweb, datum/techweb_node/node) if(!allowed_to_boost(purchasing_techweb, node.id)) return FALSE - purchasing_techweb.boost_techweb_node(node, list(TECHWEB_POINT_TYPE_GENERIC=boosted_nodes[node.id])) - purchasing_techweb.scientific_cooperation[type] -= boosted_nodes[node.id] * SCIENTIFIC_COOPERATION_PURCHASE_MULTIPLIER + purchasing_techweb.boost_techweb_node(node, list(TECHWEB_POINT_TYPE_GENERIC = boostable_nodes[node.id])) + purchasing_techweb.scientific_cooperation[type] -= boostable_nodes[node.id] * SCIENTIFIC_COOPERATION_PURCHASE_MULTIPLIER return TRUE /datum/scientific_partner/proc/allowed_to_boost(datum/techweb/purchasing_techweb, node_id) - if(purchasing_techweb.scientific_cooperation[type] < (boosted_nodes[node_id] * SCIENTIFIC_COOPERATION_PURCHASE_MULTIPLIER)) // Too expensive + if(purchasing_techweb.scientific_cooperation[type] < (boostable_nodes[node_id] * SCIENTIFIC_COOPERATION_PURCHASE_MULTIPLIER)) // Too expensive return FALSE if(!(node_id in purchasing_techweb.get_available_nodes())) // Not currently available return FALSE - if((TECHWEB_POINT_TYPE_GENERIC in purchasing_techweb.boosted_nodes[node_id]) && (purchasing_techweb.boosted_nodes[node_id][TECHWEB_POINT_TYPE_GENERIC] >= boosted_nodes[node_id])) // Already bought or we have a bigger discount + if((TECHWEB_POINT_TYPE_GENERIC in purchasing_techweb.boosted_nodes[node_id]) && (purchasing_techweb.boosted_nodes[node_id][TECHWEB_POINT_TYPE_GENERIC] >= boostable_nodes[node_id])) // Already bought or we have a bigger discount return FALSE return TRUE diff --git a/code/modules/research/ordnance/scipaper_partner.dm b/code/modules/research/ordnance/scipaper_partner.dm index fe302c73bb06c..712ec4b4127e9 100644 --- a/code/modules/research/ordnance/scipaper_partner.dm +++ b/code/modules/research/ordnance/scipaper_partner.dm @@ -3,7 +3,7 @@ flufftext = "A local group of miners are looking for ways to improve their mining output. They are interested in smaller scale explosives." accepted_experiments = list(/datum/experiment/ordnance/explosive/lowyieldbomb) multipliers = list(SCIPAPER_COOPERATION_INDEX = 0.75, SCIPAPER_FUNDING_INDEX = 0.75) - boosted_nodes = list( + boostable_nodes = list( "bluespace_basic" = 2000, "NVGtech" = 1500, "practical_bluespace" = 2500, @@ -16,7 +16,7 @@ name = "Ghost Writing" flufftext = "A nearby research station ran by a very wealthy captain seems to be struggling with their scientific output. They might reward us handsomely if we ghostwrite for them." multipliers = list(SCIPAPER_COOPERATION_INDEX = 0.25, SCIPAPER_FUNDING_INDEX = 2) - boosted_nodes = list( + boostable_nodes = list( "comp_recordkeeping" = 500, "computer_data_disks" = 500, ) @@ -29,7 +29,7 @@ /datum/experiment/ordnance/explosive/pressurebomb, /datum/experiment/ordnance/explosive/hydrogenbomb, ) - boosted_nodes = list( + boostable_nodes = list( "adv_weaponry" = 5000, "weaponry" = 2500, "sec_basic" = 1250, @@ -47,7 +47,7 @@ /datum/experiment/ordnance/gaseous/nitrous_oxide, /datum/experiment/ordnance/gaseous/bz, ) - boosted_nodes = list( + boostable_nodes = list( "cyber_organs" = 750, "cyber_organs_upgraded" = 1000, "genetics" = 500, @@ -63,7 +63,7 @@ /datum/experiment/ordnance/gaseous/noblium, /datum/experiment/ordnance/explosive/nobliumbomb, ) - boosted_nodes = list( + boostable_nodes = list( "engineering" = 5000, "adv_engi" = 5000, "emp_super" = 3000, diff --git a/code/modules/research/rdmachines.dm b/code/modules/research/rdmachines.dm index 3047c3e1a9ac1..4ab9d73dca7b5 100644 --- a/code/modules/research/rdmachines.dm +++ b/code/modules/research/rdmachines.dm @@ -11,9 +11,10 @@ var/hacked = FALSE var/console_link = TRUE //allow console link. var/disabled = FALSE - var/obj/item/loaded_item = null //the item loaded inside the machine (currently only used by experimentor and destructive analyzer) /// Ref to global science techweb. var/datum/techweb/stored_research + ///The item loaded inside the machine, used by experimentors and destructive analyzers only. + var/obj/item/loaded_item /obj/machinery/rnd/proc/reset_busy() busy = FALSE @@ -59,14 +60,6 @@ else return FALSE -/obj/machinery/rnd/attackby(obj/item/O, mob/user, params) - if(is_refillable() && O.is_drainable()) - return FALSE //inserting reagents into the machine - if(Insert_Item(O, user)) - return TRUE - - return ..() - /obj/machinery/rnd/crowbar_act(mob/living/user, obj/item/tool) return default_deconstruction_crowbar(tool) @@ -103,36 +96,32 @@ wires.interact(user) return TRUE -//proc used to handle inserting items or reagents into rnd machines -/obj/machinery/rnd/proc/Insert_Item(obj/item/I, mob/user) - return - //whether the machine can have an item inserted in its current state. /obj/machinery/rnd/proc/is_insertion_ready(mob/user) if(panel_open) - to_chat(user, span_warning("You can't load [src] while it's opened!")) + balloon_alert(user, "panel open!") return FALSE if(disabled) - to_chat(user, span_warning("The insertion belts of [src] won't engage!")) + balloon_alert(user, "belts disabled!") return FALSE if(busy) - to_chat(user, span_warning("[src] is busy right now.")) + balloon_alert(user, "still busy!") return FALSE if(machine_stat & BROKEN) - to_chat(user, span_warning("[src] is broken.")) + balloon_alert(user, "machine broken!") return FALSE if(machine_stat & NOPOWER) - to_chat(user, span_warning("[src] has no power.")) + balloon_alert(user, "no power!") return FALSE if(loaded_item) - to_chat(user, span_warning("[src] is already loaded.")) + balloon_alert(user, "item already loaded!") return FALSE return TRUE //we eject the loaded item when deconstructing the machine /obj/machinery/rnd/on_deconstruction() if(loaded_item) - loaded_item.forceMove(loc) + loaded_item.forceMove(drop_location()) ..() /obj/machinery/rnd/proc/AfterMaterialInsert(item_inserted, id_inserted, amount_inserted) diff --git a/code/modules/research/techweb/__techweb_helpers.dm b/code/modules/research/techweb/__techweb_helpers.dm index 2b0a294c606f2..469c467bc133b 100644 --- a/code/modules/research/techweb/__techweb_helpers.dm +++ b/code/modules/research/techweb/__techweb_helpers.dm @@ -10,14 +10,15 @@ WARNING("Invalid boost information for node \[[id]\]: [message]") SSresearch.invalid_node_boost[id] = message -///Returns an associative list of techweb node datums with values of the boost it gives. var/list/returned = list() -/proc/techweb_item_boost_check(obj/item/I) - if(SSresearch.techweb_boost_items[I.type]) - return SSresearch.techweb_boost_items[I.type] //It should already be formatted in node datum = list(point type = value) +///Returns an associative list of techweb node datums with values of the nodes it unlocks. +/proc/techweb_item_unlock_check(obj/item/I) + if(SSresearch.techweb_unlock_items[I.type]) + return SSresearch.techweb_unlock_items[I.type] //It should already be formatted in node datum = list(point type = value) /proc/techweb_item_point_check(obj/item/I) if(SSresearch.techweb_point_items[I.type]) return SSresearch.techweb_point_items[I.type] + return FALSE /proc/techweb_point_display_generic(pointlist) var/list/ret = list() diff --git a/code/modules/research/techweb/_techweb.dm b/code/modules/research/techweb/_techweb.dm index bd1e5cc4a80d8..8aeb63a9f8a7d 100644 --- a/code/modules/research/techweb/_techweb.dm +++ b/code/modules/research/techweb/_techweb.dm @@ -26,7 +26,7 @@ var/list/boosted_nodes = list() /// Hidden nodes. id = TRUE. Used for unhiding nodes when requirements are met by removing the entry of the node. var/list/hidden_nodes = list() - /// Items already deconstructed for a generic point boost, path = list(point_type = points) + /// List of items already deconstructed for research points, preventing infinite research point generation. var/list/deconstructed_items = list() /// Available research points, type = number var/list/research_points = list() @@ -400,17 +400,15 @@ LAZYINITLIST(boosted_nodes[node.id]) for(var/point_type in pointlist) boosted_nodes[node.id][point_type] = max(boosted_nodes[node.id][point_type], pointlist[point_type]) - if(node.autounlock_by_boost) - hidden_nodes -= node.id + unhide_node(node) update_node_status(node) return TRUE -/// Boosts a techweb node by using items. -/datum/techweb/proc/boost_with_item(datum/techweb_node/node, itempath) - if(!istype(node) || !ispath(itempath)) +///Removes a node from the hidden_nodes list, making it viewable and researchable (if no experiments are required). +/datum/techweb/proc/unhide_node(datum/techweb_node/node) + if(!istype(node)) return FALSE - var/list/boost_amount = node.boost_item_paths[itempath] - boost_techweb_node(node, boost_amount) + hidden_nodes -= node.id return TRUE /datum/techweb/proc/update_tiers(datum/techweb_node/base) diff --git a/code/modules/research/techweb/_techweb_node.dm b/code/modules/research/techweb/_techweb_node.dm index f5e1481e62cff..c36eb88627137 100644 --- a/code/modules/research/techweb/_techweb_node.dm +++ b/code/modules/research/techweb/_techweb_node.dm @@ -24,8 +24,8 @@ var/list/design_ids = list() /// CALCULATED FROM OTHER NODE'S PREREQUISITIES. Associated list id = TRUE var/list/unlock_ids = list() - /// Associative list, path = list(point type = point_value) - var/list/boost_item_paths = list() + /// List of items you need to deconstruct to unlock this node. + var/list/required_items_to_unlock = list() /// Boosting this will autounlock this node var/autounlock_by_boost = TRUE /// The points cost to research the node, type = amount diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm index 1f1dd423d1a5d..9a3d9ac526107 100644 --- a/code/modules/research/techweb/all_nodes.dm +++ b/code/modules/research/techweb/all_nodes.dm @@ -2160,7 +2160,7 @@ display_name = "Alien Technology" description = "Things used by the greys." prereq_ids = list("biotech","engineering") - boost_item_paths = list( + required_items_to_unlock = list( /obj/item/stack/sheet/mineral/abductor, /obj/item/abductor, /obj/item/cautery/alien, @@ -2203,7 +2203,7 @@ "alien_scalpel", ) - boost_item_paths = list( + required_items_to_unlock = list( /obj/item/abductor, /obj/item/cautery/alien, /obj/item/circuitboard/machine/abductor, @@ -2242,7 +2242,7 @@ "alien_wrench", ) - boost_item_paths = list( + required_items_to_unlock = list( /obj/item/abductor, /obj/item/circuitboard/machine/abductor, /obj/item/crowbar/abductor, @@ -2295,12 +2295,12 @@ /datum/techweb_node/syndicate_basic/proc/register_uplink_items() SIGNAL_HANDLER UnregisterSignal(SSearly_assets, COMSIG_SUBSYSTEM_POST_INITIALIZE) - boost_item_paths = list() + required_items_to_unlock = list() for(var/datum/uplink_item/item_path as anything in SStraitor.uplink_items_by_type) var/datum/uplink_item/item = SStraitor.uplink_items_by_type[item_path] if(!item.item || !item.illegal_tech) continue - boost_item_paths |= item.item //allows deconning to unlock. + required_items_to_unlock |= item.item //allows deconning to unlock. ////////////////////////B.E.P.I.S. Locked Techs//////////////////////// diff --git a/code/modules/surgery/organs/internal/appendix/appendix_golem.dm b/code/modules/surgery/organs/internal/appendix/appendix_golem.dm index 03b076b1b2a13..ede8dfa1e6b0b 100644 --- a/code/modules/surgery/organs/internal/appendix/appendix_golem.dm +++ b/code/modules/surgery/organs/internal/appendix/appendix_golem.dm @@ -2,7 +2,7 @@ /obj/item/organ/internal/appendix/golem name = "internal forge" desc = "This expanded digestive chamber allows golems to smelt minerals, provided that they are immersed in lava." - icon_state = "ethereal_heart" + icon_state = "ethereal_heart-off" color = COLOR_GOLEM_GRAY organ_flags = ORGAN_MINERAL /// Action which performs smelting diff --git a/code/modules/surgery/organs/internal/heart/_heart.dm b/code/modules/surgery/organs/internal/heart/_heart.dm index fb97ca7eda00b..8a5646d18146b 100644 --- a/code/modules/surgery/organs/internal/heart/_heart.dm +++ b/code/modules/surgery/organs/internal/heart/_heart.dm @@ -10,54 +10,84 @@ healing_factor = STANDARD_ORGAN_HEALING decay_factor = 2.5 * STANDARD_ORGAN_DECAY //designed to fail around 6 minutes after death - low_threshold_passed = "Prickles of pain appear then die out from within your chest..." - high_threshold_passed = "Something inside your chest hurts, and the pain isn't subsiding. You notice yourself breathing far faster than before." - now_fixed = "Your heart begins to beat again." - high_threshold_cleared = "The pain in your chest has died down, and your breathing becomes more relaxed." + low_threshold_passed = span_info("Prickles of pain appear then die out from within your chest...") + high_threshold_passed = span_warning("Something inside your chest hurts, and the pain isn't subsiding. You notice yourself breathing far faster than before.") + now_fixed = span_info("Your heart begins to beat again.") + high_threshold_cleared = span_info("The pain in your chest has died down, and your breathing becomes more relaxed.") - // Heart attack code is in code/modules/mob/living/carbon/human/life.dm - var/beating = TRUE attack_verb_continuous = list("beats", "thumps") attack_verb_simple = list("beat", "thump") - var/beat = BEAT_NONE//is this mob having a heatbeat sound played? if so, which? - var/failed = FALSE //to prevent constantly running failing code - var/operated = FALSE //whether the heart's been operated on to fix some of its damages + + // Heart attack code is in code/modules/mob/living/carbon/human/life.dm + + /// Whether the heart is currently beating. + /// Do not set this directly. Use Restart() and Stop() instead. + VAR_PRIVATE/beating = TRUE + + /// is this mob having a heatbeat sound played? if so, which? + var/beat = BEAT_NONE + /// whether the heart's been operated on to fix some of its damages + var/operated = FALSE /obj/item/organ/internal/heart/update_icon_state() + . = ..() icon_state = "[base_icon_state]-[beating ? "on" : "off"]" - return ..() /obj/item/organ/internal/heart/Remove(mob/living/carbon/heartless, special = 0) . = ..() if(!special) - addtimer(CALLBACK(src, PROC_REF(stop_if_unowned)), 120) + addtimer(CALLBACK(src, PROC_REF(stop_if_unowned)), 12 SECONDS) + beat = BEAT_NONE + owner?.stop_sound_channel(CHANNEL_HEARTBEAT) /obj/item/organ/internal/heart/proc/stop_if_unowned() - if(!owner) + if(QDELETED(src)) + return + if(IS_ROBOTIC_ORGAN(src)) + return + if(isnull(owner)) Stop() /obj/item/organ/internal/heart/attack_self(mob/user) - ..() + . = ..() + if(.) + return + if(!beating) - user.visible_message("[user] squeezes [src] to \ - make it beat again!",span_notice("You squeeze [src] to make it beat again!")) + user.visible_message( + span_notice("[user] squeezes [src] to make it beat again!"), + span_notice("You squeeze [src] to make it beat again!"), + ) Restart() - addtimer(CALLBACK(src, PROC_REF(stop_if_unowned)), 80) + addtimer(CALLBACK(src, PROC_REF(stop_if_unowned)), 8 SECONDS) + return TRUE /obj/item/organ/internal/heart/proc/Stop() + if(!beating) + return FALSE + beating = FALSE update_appearance() + beat = BEAT_NONE + owner?.stop_sound_channel(CHANNEL_HEARTBEAT) return TRUE /obj/item/organ/internal/heart/proc/Restart() + if(beating) + return FALSE + beating = TRUE update_appearance() return TRUE /obj/item/organ/internal/heart/OnEatFrom(eater, feeder) . = ..() - beating = FALSE - update_appearance() + Stop() + +/// Checks if the heart is beating. +/// Can be overridden to add more conditions for more complex hearts. +/obj/item/organ/internal/heart/proc/is_beating() + return beating /obj/item/organ/internal/heart/on_life(seconds_per_tick, times_fired) ..() @@ -66,34 +96,32 @@ if(!owner.needs_heart()) return - if(owner.client && beating) - failed = FALSE - var/sound/slowbeat = sound('sound/health/slowbeat.ogg', repeat = TRUE) - var/sound/fastbeat = sound('sound/health/fastbeat.ogg', repeat = TRUE) + // Handle "sudden" heart attack + if(!beating || (organ_flags & ORGAN_FAILING)) + if(owner.can_heartattack() && Stop()) + if(owner.stat == CONSCIOUS) + owner.visible_message(span_danger("[owner] clutches at [owner.p_their()] chest as if [owner.p_their()] heart is stopping!")) + to_chat(owner, span_userdanger("You feel a terrible pain in your chest, as if your heart has stopped!")) + return + + // Beyond deals with sound effects, so nothing needs to be done if no client + if(isnull(owner.client)) + return - if(owner.health <= owner.crit_threshold && beat != BEAT_SLOW) + if(owner.stat == SOFT_CRIT) + if(beat != BEAT_SLOW) beat = BEAT_SLOW - owner.playsound_local(get_turf(owner), slowbeat, 40, 0, channel = CHANNEL_HEARTBEAT, use_reverb = FALSE) to_chat(owner, span_notice("You feel your heart slow down...")) - if(beat == BEAT_SLOW && owner.health > owner.crit_threshold) - owner.stop_sound_channel(CHANNEL_HEARTBEAT) - beat = BEAT_NONE - - if(owner.has_status_effect(/datum/status_effect/jitter)) - if(owner.health > HEALTH_THRESHOLD_FULLCRIT && (!beat || beat == BEAT_SLOW)) - owner.playsound_local(get_turf(owner), fastbeat, 40, 0, channel = CHANNEL_HEARTBEAT, use_reverb = FALSE) - beat = BEAT_FAST - - else if(beat == BEAT_FAST) - owner.stop_sound_channel(CHANNEL_HEARTBEAT) - beat = BEAT_NONE - - if(organ_flags & ORGAN_FAILING && owner.can_heartattack() && !(HAS_TRAIT(src, TRAIT_STABLEHEART))) //heart broke, stopped beating, death imminent... unless you have veins that pump blood without a heart - if(owner.stat == CONSCIOUS) - owner.visible_message(span_danger("[owner] clutches at [owner.p_their()] chest as if [owner.p_their()] heart is stopping!"), \ - span_userdanger("You feel a terrible pain in your chest, as if your heart has stopped!")) - owner.set_heartattack(TRUE) - failed = TRUE + SEND_SOUND(owner, sound('sound/health/slowbeat.ogg', repeat = TRUE, channel = CHANNEL_HEARTBEAT, volume = 40)) + + else if(owner.stat == HARD_CRIT) + if(beat != BEAT_FAST && owner.has_status_effect(/datum/status_effect/jitter)) + SEND_SOUND(owner, sound('sound/health/fastbeat.ogg', repeat = TRUE, channel = CHANNEL_HEARTBEAT, volume = 40)) + beat = BEAT_FAST + + else if(beat == BEAT_SLOW) + owner.stop_sound_channel(CHANNEL_HEARTBEAT) + beat = BEAT_NONE /obj/item/organ/internal/heart/get_availability(datum/species/owner_species, mob/living/owner_mob) return owner_species.mutantheart @@ -222,4 +250,3 @@ owner.heal_overall_damage(brute = 15, burn = 15, required_bodytype = BODYTYPE_ORGANIC) if(owner.reagents.get_reagent_amount(/datum/reagent/medicine/ephedrine) < 20) owner.reagents.add_reagent(/datum/reagent/medicine/ephedrine, 10) - diff --git a/code/modules/surgery/organs/internal/heart/heart_ethereal.dm b/code/modules/surgery/organs/internal/heart/heart_ethereal.dm index 8ad9301fe7412..78ee55b0b2841 100644 --- a/code/modules/surgery/organs/internal/heart/heart_ethereal.dm +++ b/code/modules/surgery/organs/internal/heart/heart_ethereal.dm @@ -1,6 +1,7 @@ /obj/item/organ/internal/heart/ethereal name = "crystal core" - icon_state = "ethereal_heart" //Welp. At least it's more unique in functionaliy. + icon_state = "ethereal_heart-on" + base_icon_state = "ethereal_heart" visual = TRUE //This is used by the ethereal species for color desc = "A crystal-like organ that functions similarly to a heart for Ethereals. It can revive its owner." @@ -18,6 +19,7 @@ /obj/item/organ/internal/heart/ethereal/Initialize(mapload) . = ..() add_atom_colour(ethereal_color, FIXED_COLOUR_PRIORITY) + update_appearance() /obj/item/organ/internal/heart/ethereal/Insert(mob/living/carbon/heart_owner, special = FALSE, drop_if_replaced = TRUE) . = ..() @@ -36,7 +38,7 @@ /obj/item/organ/internal/heart/ethereal/update_overlays() . = ..() - var/mutable_appearance/shine = mutable_appearance(icon, icon_state = "[icon_state]_shine") + var/mutable_appearance/shine = mutable_appearance(icon, icon_state = "[base_icon_state]_overlay-[beating ? "on" : "off"]") shine.appearance_flags = RESET_COLOR //No color on this, just pure white . += shine @@ -193,13 +195,13 @@ add_atom_colour(ethereal_heart.ethereal_color, FIXED_COLOUR_PRIORITY) crystal_heal_timer = addtimer(CALLBACK(src, PROC_REF(heal_ethereal)), CRYSTALIZE_HEAL_TIME, TIMER_STOPPABLE) set_light(4, 10, ethereal_heart.ethereal_color) - update_icon() + update_appearance(UPDATE_OVERLAYS) flick("ethereal_crystal_forming", src) addtimer(CALLBACK(src, PROC_REF(start_crystalization)), 1 SECONDS) /obj/structure/ethereal_crystal/proc/start_crystalization() being_built = FALSE - update_icon() + update_appearance(UPDATE_OVERLAYS) /obj/structure/ethereal_crystal/atom_destruction(damage_flag) playsound(get_turf(ethereal_heart.owner), 'sound/effects/ethereal_revive_fail.ogg', 100) diff --git a/code/modules/unit_tests/required_map_items.dm b/code/modules/unit_tests/required_map_items.dm index 39930afd822c2..1f03295f00e79 100644 --- a/code/modules/unit_tests/required_map_items.dm +++ b/code/modules/unit_tests/required_map_items.dm @@ -18,7 +18,7 @@ expected_types += /obj/machinery/computer/communications expected_types += /mob/living/carbon/human/species/monkey/punpun expected_types += /mob/living/basic/pet/dog/corgi/ian - expected_types += /mob/living/simple_animal/parrot/poly + expected_types += /mob/living/basic/parrot/poly expected_types += /obj/machinery/drone_dispenser /datum/unit_test/required_map_items/Run() diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm index 5bc4e7d82b468..3066ed2f49888 100644 --- a/code/modules/unit_tests/simple_animal_freeze.dm +++ b/code/modules/unit_tests/simple_animal_freeze.dm @@ -92,10 +92,6 @@ /mob/living/simple_animal/hostile/retaliate/goose/vomit, /mob/living/simple_animal/hostile/vatbeast, /mob/living/simple_animal/hostile/zombie, - /mob/living/simple_animal/parrot, - /mob/living/simple_animal/parrot/natural, - /mob/living/simple_animal/parrot/poly, - /mob/living/simple_animal/parrot/poly/ghost, /mob/living/simple_animal/pet, /mob/living/simple_animal/pet/cat, /mob/living/simple_animal/pet/cat/_proc, diff --git a/code/modules/vehicles/mecha/mech_fabricator.dm b/code/modules/vehicles/mecha/mech_fabricator.dm index a840f75ad86ae..c3e5abc39f0fc 100644 --- a/code/modules/vehicles/mecha/mech_fabricator.dm +++ b/code/modules/vehicles/mecha/mech_fabricator.dm @@ -496,15 +496,5 @@ return FALSE return default_deconstruction_crowbar(I) -/obj/machinery/mecha_part_fabricator/proc/is_insertion_ready(mob/user) - if(panel_open) - to_chat(user, span_warning("You can't load [src] while it's opened!")) - return FALSE - if(being_built) - to_chat(user, span_warning("\The [src] is currently processing! Please wait until completion.")) - return FALSE - - return TRUE - /obj/machinery/mecha_part_fabricator/maint link_on_init = FALSE diff --git a/html/changelogs/AutoChangeLog-pr-79605.yml b/html/changelogs/AutoChangeLog-pr-79605.yml deleted file mode 100644 index 473bc2be85157..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-79605.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "TwistedSilicon" -delete-after: True -changes: - - bugfix: "invisimin verb now makes you invisible to all HUDs too! No more floating healthbars or job identifiers giving you away while you sneak around." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-79643.yml b/html/changelogs/AutoChangeLog-pr-79643.yml deleted file mode 100644 index 8edcbd418085a..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-79643.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "mc-oofert" -delete-after: True -changes: - - bugfix: "The plaguebearer can no longer depower virology on Tramstation" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-79688.yml b/html/changelogs/AutoChangeLog-pr-79688.yml deleted file mode 100644 index 994aeca543311..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-79688.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Jacquerel" -delete-after: True -changes: - - balance: "Sapient brimdemons can't hurt themselves with their own beams" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-79701.yml b/html/changelogs/AutoChangeLog-pr-79701.yml deleted file mode 100644 index 497cdeeb27797..0000000000000 --- a/html/changelogs/AutoChangeLog-pr-79701.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Ben10Omintrix" -delete-after: True -changes: - - bugfix: "bileworms will now attack" \ No newline at end of file diff --git a/html/changelogs/archive/2023-11.yml b/html/changelogs/archive/2023-11.yml index a970177c1dd0a..f1f2dc2c47085 100644 --- a/html/changelogs/archive/2023-11.yml +++ b/html/changelogs/archive/2023-11.yml @@ -562,3 +562,57 @@ timothymtorres: - rscadd: Add bamboo seeds to ash walker den. This lets them craft blowguns, crude syringes, bamboo spears, punji stick traps, and more! +2023-11-15: + Ben10Omintrix: + - bugfix: bileworms will now attack + D4C-420: + - spellcheck: hopefully changed all instances of the word 'mjolnir' to 'mjollnir' + Ghommie: + - bugfix: Fixed a small issue with disposal outlets leaving contents about to be + ejected stuck inside the pipe beneath it if deleted. + Jacquerel: + - balance: Sapient brimdemons can't hurt themselves with their own beams + JohnFulpWillard: + - refactor: Destructive Analyzers now have a TGUI menu. + - bugfix: PDAs now log that they've been emagged, but will no longer log any further + programs they open beyond that. This means Nukies don't sell themselves out + by opening their disk tracking app. + LT3: + - bugfix: Bad luck omen again raises your chance of getting shocked by the tram + plate + - bugfix: Tram plate checks and energizes when the tram is moving + - code_imp: Omen component now applies the cursed trait + Melbert: + - code_imp: General heart code cleanup. + - bugfix: Heartbeat sound effects are no longer sourced to the exact tile you fell + into crit at + - bugfix: Abductors glands are less likely to become invisible or look wrong + - bugfix: Ethereal hearts are less likely to become invisible or look wrong, and + now properly spawn with their shine overlay + - image: Adds heartbeat animation to beating Ethereal Hearts + - bugfix: Fixes hallucination and encrypted announcements printing to the Newscaster. + SyncIt21: + - bugfix: drying rack now shows correct examines & screen tips. + - code_imp: tone of code organization for smart fridges overall. changed ui to typescript. + - qol: added more detailed examines & screen tips for smart fridges. drying racks + can be dismantled with a crowbar and not simply pried open with it. + TwistedSilicon: + - bugfix: invisimin verb now makes you invisible to all HUDs too! No more floating + healthbars or job identifiers giving you away while you sneak around. + jjpark-kb: + - qol: looms will now attempt to loop through stackable items (cotton as an example) + larentoun: + - rscadd: Emote Panel TGUI added in IC category. + mc-oofert: + - bugfix: The plaguebearer can no longer depower virology on Tramstation + san7890: + - bugfix: Safeties in the code have been added to prevent things in disposals going + into nullspace whenever they get ejected from a pipe - you will just magically + spawn at the turf that you were meant to be flung towards. + - qol: You will no longer be added to the list for ghost-orbit role polls if you + have opted out of getting antag ghost roles in your preferences. + - qol: You will get a tgui_alert to accept the ghost role if you were selected via + the orbit poll, instead of it just throwing you intot he role. + san7890, Ghommie: + - bugfix: The Blessing of Insanity now grants no damage slowdown and free hyperspace + movement correctly. diff --git a/icons/mob/clothing/back.dmi b/icons/mob/clothing/back.dmi index ab4f8c85f30b5..9b8417d882b45 100644 Binary files a/icons/mob/clothing/back.dmi and b/icons/mob/clothing/back.dmi differ diff --git a/icons/obj/medical/organs/organs.dmi b/icons/obj/medical/organs/organs.dmi index 94ba46568c929..3feaf4a4ca8d0 100644 Binary files a/icons/obj/medical/organs/organs.dmi and b/icons/obj/medical/organs/organs.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 9de6a5770425c..df5283992d9f9 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1076,6 +1076,7 @@ #include "code\datums\components\life_link.dm" #include "code\datums\components\light_eater.dm" #include "code\datums\components\ling_decoy_brain.dm" +#include "code\datums\components\listen_and_repeat.dm" #include "code\datums\components\lock_on_cursor.dm" #include "code\datums\components\magnet.dm" #include "code\datums\components\manual_blinking.dm" @@ -2227,7 +2228,6 @@ #include "code\game\objects\items\shooting_range.dm" #include "code\game\objects\items\shrapnel.dm" #include "code\game\objects\items\signs.dm" -#include "code\game\objects\items\singularityhammer.dm" #include "code\game\objects\items\skub.dm" #include "code\game\objects\items\spear.dm" #include "code\game\objects\items\sticker.dm" @@ -2247,6 +2247,7 @@ #include "code\game\objects\items\wall_mounted.dm" #include "code\game\objects\items\weaponry.dm" #include "code\game\objects\items\wiki_manuals.dm" +#include "code\game\objects\items\wizard_weapons.dm" #include "code\game\objects\items\AI_modules\_AI_modules.dm" #include "code\game\objects\items\AI_modules\freeform.dm" #include "code\game\objects\items\AI_modules\full_lawsets.dm" @@ -3733,6 +3734,7 @@ #include "code\modules\economy\account.dm" #include "code\modules\economy\holopay.dm" #include "code\modules\emoji\emoji_parse.dm" +#include "code\modules\emote_panel\emote_panel.dm" #include "code\modules\error_handler\error_handler.dm" #include "code\modules\error_handler\error_viewer.dm" #include "code\modules\escape_menu\details.dm" @@ -4562,6 +4564,12 @@ #include "code\modules\mob\living\basic\pets\dog\corgi.dm" #include "code\modules\mob\living\basic\pets\dog\dog_subtypes.dm" #include "code\modules\mob\living\basic\pets\dog\strippable_items.dm" +#include "code\modules\mob\living\basic\pets\parrot\parrot.dm" +#include "code\modules\mob\living\basic\pets\parrot\parrot_items.dm" +#include "code\modules\mob\living\basic\pets\parrot\parrot_subtypes.dm" +#include "code\modules\mob\living\basic\pets\parrot\poly.dm" +#include "code\modules\mob\living\basic\pets\parrot\parrot_ai\parrot_controller.dm" +#include "code\modules\mob\living\basic\pets\parrot\parrot_ai\parroting_action.dm" #include "code\modules\mob\living\basic\ruin_defender\flesh.dm" #include "code\modules\mob\living\basic\ruin_defender\living_floor.dm" #include "code\modules\mob\living\basic\ruin_defender\skeleton.dm" @@ -4810,7 +4818,6 @@ #include "code\modules\mob\living\silicon\robot\robot_say.dm" #include "code\modules\mob\living\simple_animal\animal_defense.dm" #include "code\modules\mob\living\simple_animal\damage_procs.dm" -#include "code\modules\mob\living\simple_animal\parrot.dm" #include "code\modules\mob\living\simple_animal\simple_animal.dm" #include "code\modules\mob\living\simple_animal\bot\bot.dm" #include "code\modules\mob\living\simple_animal\bot\bot_announcement.dm" diff --git a/tgui/packages/tgui/interfaces/DestructiveAnalyzer.tsx b/tgui/packages/tgui/interfaces/DestructiveAnalyzer.tsx new file mode 100644 index 0000000000000..5bfd7353cc71e --- /dev/null +++ b/tgui/packages/tgui/interfaces/DestructiveAnalyzer.tsx @@ -0,0 +1,135 @@ +import { BooleanLike } from 'common/react'; +import { useBackend } from '../backend'; +import { Button, Box, Section, NoticeBox } from '../components'; +import { Window } from '../layouts'; + +type Data = { + server_connected: BooleanLike; + loaded_item: string; + item_icon: string; + indestructible: BooleanLike; + already_deconstructed: BooleanLike; + recoverable_points: string; + node_data: NodeData[]; + research_point_id: string; +}; + +type NodeData = { + node_name: string; + node_id: string; + node_hidden: BooleanLike; +}; + +export const DestructiveAnalyzer = (props, context) => { + const { act, data } = useBackend(context); + const { + server_connected, + indestructible, + loaded_item, + item_icon, + already_deconstructed, + recoverable_points, + research_point_id, + node_data = [], + } = data; + if (!server_connected) { + return ( + + + + Not connected to a server. Please sync one using a multitool. + + + + ); + } + if (!loaded_item) { + return ( + + + + No item loaded!
+ Put any item inside to see what it's capable of! +
+
+
+ ); + } + return ( + + +
act('eject_item')} + /> + }> + +
+
+ {!indestructible && ( + + This item can't be deconstructed! + + )} + {!!indestructible && ( + <> + {!!recoverable_points && ( + <> + Research points from deconstruction + {recoverable_points} + + )} + + act('deconstruct', { deconstruct_id: research_point_id }) + } + /> + + )} + {node_data.map((node) => ( + + act('deconstruct', { deconstruct_id: node.node_id }) + } + /> + ))} +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/EmotePanel.tsx b/tgui/packages/tgui/interfaces/EmotePanel.tsx new file mode 100644 index 0000000000000..81df051236217 --- /dev/null +++ b/tgui/packages/tgui/interfaces/EmotePanel.tsx @@ -0,0 +1,283 @@ +import { useBackend, useLocalState } from '../backend'; +import { Window } from '../layouts'; +import { Button, Section, Flex, Icon, Box } from '../components'; +import { BooleanLike } from '../../common/react'; +import { SearchBar } from './Fabrication/SearchBar'; +import { capitalizeFirst } from '../../common/string'; + +type Emote = { + key: string; + name: string; + hands: BooleanLike; + visible: BooleanLike; + audible: BooleanLike; + sound: BooleanLike; + use_params: BooleanLike; +}; + +type EmotePanelData = { + emotes: Emote[]; +}; + +export const EmotePanelContent = (props, context) => { + const { act, data } = useBackend(context); + const { emotes } = data; + + const [filterVisible, toggleVisualFilter] = useLocalState( + context, + 'filterVisible', + false + ); + + const [filterAudible, toggleAudibleFilter] = useLocalState( + context, + 'filterAudible', + false + ); + + const [filterSound, toggleSoundFilter] = useLocalState( + context, + 'filterSound', + false + ); + + const [filterHands, toggleHandsFilter] = useLocalState( + context, + 'filterHands', + false + ); + + const [filterUseParams, toggleUseParamsFilter] = useLocalState( + context, + 'filterUseParams', + false + ); + + const [useParams, toggleUseParams] = useLocalState( + context, + 'useParams', + false + ); + + const [searchText, setSearchText] = useLocalState( + context, + 'search_text', + '' + ); + + const [showNames, toggleShowNames] = useLocalState( + context, + 'showNames', + true + ); + + const [showIcons, toggleShowIcons] = useLocalState( + context, + 'showIcons', + false + ); + + return ( +
+
+
+
0 + ? `Search results of "${searchText}"` + : `All Emotes` + } + buttons={ + + + + + + + + + + }> + + + {emotes + .filter( + (emote) => + emote.key && + (searchText.length > 0 + ? emote.key + .toLowerCase() + .includes(searchText.toLowerCase()) || + emote.name.toLowerCase().includes(searchText.toLowerCase()) + : true) && + (filterVisible ? emote.visible : true) && + (filterAudible ? emote.audible : true) && + (filterSound ? emote.sound : true) && + (filterHands ? emote.hands : true) && + (filterUseParams ? emote.use_params : true) + ) + .sort((a, b) => (a.name > b.name ? 1 : -1)) + .map((emote) => ( + + ))} + + +
+
+ ); +}; + +const EmoteIcons = (props, context) => { + const { visible, audible, sound, hands, use_params, margin } = props; + + return ( + + + + + + + + ); +}; + +export const EmotePanel = (props, context) => { + return ( + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/SmartVend.js b/tgui/packages/tgui/interfaces/SmartVend.tsx similarity index 81% rename from tgui/packages/tgui/interfaces/SmartVend.js rename to tgui/packages/tgui/interfaces/SmartVend.tsx index 9ca12a87b7def..54470b18a3547 100644 --- a/tgui/packages/tgui/interfaces/SmartVend.js +++ b/tgui/packages/tgui/interfaces/SmartVend.tsx @@ -1,10 +1,23 @@ -import { map } from 'common/collections'; +import { BooleanLike } from 'common/react'; import { useBackend } from '../backend'; import { Button, NoticeBox, Section, Table } from '../components'; import { Window } from '../layouts'; +type Item = { + name: string; + amount: number; +}; + +type Data = { + contents: Item[]; + name: string; + isdryer: BooleanLike; + drying: BooleanLike; +}; + export const SmartVend = (props, context) => { - const { act, data } = useBackend(context); + const { act, data } = useBackend(context); + const { contents = [] } = data; return ( @@ -19,18 +32,18 @@ export const SmartVend = (props, context) => { ) }> - {(data.contents.length === 0 && ( + {contents.length === 0 ? ( Unfortunately, this {data.name} is empty. - )) || ( + ) : ( Item - {data.verb ? data.verb : 'Dispense'} + {data.isdryer ? 'Take' : 'Dispense'} - {map((value, key) => ( + {Object.values(contents).map((value, key) => ( {value.name} @@ -58,7 +71,7 @@ export const SmartVend = (props, context) => { /> - ))(data.contents)} + ))}
)}