diff --git a/code/__DEFINES/_flags/_flags.dm b/code/__DEFINES/_flags/_flags.dm index dae3df6f0b..b1324000b8 100644 --- a/code/__DEFINES/_flags/_flags.dm +++ b/code/__DEFINES/_flags/_flags.dm @@ -141,3 +141,8 @@ GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 204 // melee_attack_chain() attackchain_flags /// The attack is from a parry counterattack. #define ATTACKCHAIN_PARRY_COUNTERATTACK (1<<0) + +/// If the thing can reflect light (lasers/energy) +#define RICOCHET_SHINY (1<<0) +/// If the thing can reflect matter (bullets/bomb shrapnel) +#define RICOCHET_HARD (1<<1) \ No newline at end of file diff --git a/code/__DEFINES/combat.dm b/code/__DEFINES/combat.dm index bf053635af..3cb60b1143 100644 --- a/code/__DEFINES/combat.dm +++ b/code/__DEFINES/combat.dm @@ -163,9 +163,8 @@ GLOBAL_LIST_INIT(shove_disarming_types, typecacheof(list( /obj/item/gun))) -//Combat object defines - //Embedded objects + #define EMBEDDED_PAIN_CHANCE 15 //Chance for embedded objects to cause pain (damage user) #define EMBEDDED_ITEM_FALLOUT 5 //Chance for embedded object to fall out (causing pain but removing the object) #define EMBED_CHANCE 45 //Chance for an object to embed into somebody when thrown (if it's sharp) @@ -174,7 +173,16 @@ GLOBAL_LIST_INIT(shove_disarming_types, typecacheof(list( #define EMBEDDED_IMPACT_PAIN_MULTIPLIER 4 //Coefficient of multiplication for the damage the item does when it first embeds (this*item.w_class) #define EMBED_THROWSPEED_THRESHOLD 4 //The minimum value of an item's throw_speed for it to embed (Unless it has embedded_ignore_throwspeed_threshold set to 1) #define EMBEDDED_UNSAFE_REMOVAL_PAIN_MULTIPLIER 8 //Coefficient of multiplication for the damage the item does when removed without a surgery (this*item.w_class) -#define EMBEDDED_UNSAFE_REMOVAL_TIME 150 //A Time in ticks, total removal time = (this/item.w_class) +#define EMBEDDED_UNSAFE_REMOVAL_TIME 30 //A Time in ticks, total removal time = (this*item.w_class) +#define EMBEDDED_JOSTLE_CHANCE 5 //Chance for embedded objects to cause pain every time they move (jostle) +#define EMBEDDED_JOSTLE_PAIN_MULTIPLIER 1 //Coefficient of multiplication for the damage the item does while +#define EMBEDDED_PAIN_STAM_PCT 0.0 //This percentage of all pain will be dealt as stam damage rather than brute (0-1) +#define EMBED_CHANCE_TURF_MOD -15 //You are this many percentage points less likely to embed into a turf (good for things glass shards and spears vs walls) + +#define EMBED_HARMLESS list("pain_mult" = 0, "jostle_pain_mult" = 0, "ignore_throwspeed_threshold" = TRUE) +#define EMBED_HARMLESS_SUPERIOR list("pain_mult" = 0, "jostle_pain_mult" = 0, "ignore_throwspeed_threshold" = TRUE, "embed_chance" = 100, "fall_chance" = 0.1) +#define EMBED_POINTY list("ignore_throwspeed_threshold" = TRUE) +#define EMBED_POINTY_SUPERIOR list("embed_chance" = 100, "ignore_throwspeed_threshold" = TRUE) //Gun weapon weight #define WEAPON_LIGHT 1 @@ -189,6 +197,14 @@ GLOBAL_LIST_INIT(shove_disarming_types, typecacheof(list( #define EGUN_SELFCHARGE 1 #define EGUN_SELFCHARGE_BORG 2 +//Gun suppression +#define SUPPRESSED_NONE 0 +#define SUPPRESSED_QUIET 1 ///standard suppressed +#define SUPPRESSED_VERY 2 /// no message + +//Nice shot bonus +#define NICE_SHOT_RICOCHET_BONUS 10 //if the shooter has the NICE_SHOT trait and they fire a ricocheting projectile, add this to the ricochet chance and auto aim angle + ///Time to spend without clicking on other things required for your shots to become accurate. #define GUN_AIMING_TIME (2 SECONDS) diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index ea9ebbec02..141f5626b2 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -26,6 +26,11 @@ #define COMPONENT_ADD_TRAIT (1<<0) #define COMPONENT_REMOVE_TRAIT (1<<1) +/// fires on the target datum when an element is attached to it (/datum/element) +#define COMSIG_ELEMENT_ATTACH "element_attach" +/// fires on the target datum when an element is attached to it (/datum/element) +#define COMSIG_ELEMENT_DETACH "element_detach" + // /atom signals #define COMSIG_PARENT_ATTACKBY "atom_attackby" //from base of atom/attackby(): (/obj/item, /mob/living, params) #define COMPONENT_NO_AFTERATTACK 1 //Return this in response if you don't want afterattack to be called @@ -231,6 +236,8 @@ #define COMSIG_CARBON_SOUNDBANG "carbon_soundbang" //from base of mob/living/carbon/soundbang_act(): (list(intensity)) #define COMSIG_CARBON_IDENTITY_TRANSFERRED_TO "carbon_id_transferred_to" //from datum/dna/transfer_identity(): (datum/dna, transfer_SE) #define COMSIG_CARBON_TACKLED "carbon_tackled" //sends from tackle.dm on tackle completion +#define COMSIG_CARBON_EMBED_RIP "item_embed_start_rip" // defined twice, in carbon and human's topics, fired when interacting with a valid embedded_object to pull it out (mob/living/carbon/target, /obj/item, /obj/item/bodypart/L) +#define COMSIG_CARBON_EMBED_REMOVAL "item_embed_remove_safe" // called when removing a given item from a mob, from mob/living/carbon/remove_embedded_object(mob/living/carbon/target, /obj/item) // /mob/living/silicon signals #define COMSIG_ROBOT_UPDATE_ICONS "robot_update_icons" //from base of robot/update_icons(): () @@ -277,9 +284,15 @@ #define COMSIG_ITEM_MOUSE_EXIT "item_mouse_exit" //from base of obj/item/MouseExited(): (location, control, params) #define COMSIG_ITEM_MOUSE_ENTER "item_mouse_enter" //from base of obj/item/MouseEntered(): (location, control, params) #define COMSIG_ITEM_DECONSTRUCTOR_DEEPSCAN "deconstructor_deepscan" //Called by deconstructive analyzers deepscanning an item: (obj/machinery/rnd/destructive_analyzer/analyzer_machine, mob/user, list/information_list) +#define COMSIG_ITEM_DISABLE_EMBED "item_disable_embed" ///from [/obj/item/proc/disableEmbedding]: +#define COMSIG_MINE_TRIGGERED "minegoboom" ///from [/obj/effect/mine/proc/triggermine]: // Uncovered information #define COMPONENT_DEEPSCAN_UNCOVERED_INFORMATION 1 +// /obj/item/grenade signals +#define COMSIG_GRENADE_PRIME "grenade_prime" //called in /obj/item/gun/process_fire (user, target, params, zone_override) +#define COMSIG_GRENADE_ARMED "grenade_armed" //called in /obj/item/gun/process_fire (user, target, params, zone_override) + // /obj/item/clothing signals #define COMSIG_SHOES_STEP_ACTION "shoes_step_action" //from base of obj/item/clothing/shoes/proc/step_action(): () #define COMSIG_SUIT_MADE_HELMET "suit_made_helmet" //from base of obj/item/clothing/suit/MakeHelmet(): (helmet) @@ -307,8 +320,15 @@ #define COMSIG_PEN_ROTATED "pen_rotated" //called after rotation in /obj/item/pen/attack_self(): (rotation, mob/living/carbon/user) // /obj/item/projectile signals (sent to the firer) +#define COMSIG_PROJECTILE_SELF_ON_HIT "projectile_self_on_hit" // from base of /obj/item/projectile/proc/on_hit(): (atom/movable/firer, atom/target, Angle) #define COMSIG_PROJECTILE_ON_HIT "projectile_on_hit" // from base of /obj/item/projectile/proc/on_hit(): (atom/movable/firer, atom/target, Angle) #define COMSIG_PROJECTILE_BEFORE_FIRE "projectile_before_fire" // from base of /obj/item/projectile/proc/fire(): (obj/item/projectile, atom/original_target) +#define COMSIG_PROJECTILE_FIRE "projectile_fire" ///from the base of /obj/projectile/proc/fire(): () +#define COMSIG_PROJECTILE_RANGE_OUT "projectile_range_out" // sent to targets during the process_hit proc of projectiles +#define COMSIG_EMBED_TRY_FORCE "item_try_embed" // sent when trying to force an embed (mainly for projectiles, only used in the embed element) +#define COMSIG_PROJECTILE_PREHIT "com_proj_prehit" ///sent to targets during the process_hit proc of projectiles + +#define COMSIG_PELLET_CLOUD_INIT "pellet_cloud_init" // sent to targets during the process_hit proc of projectiles // /mob/living/carbon/human signals #define COMSIG_HUMAN_MELEE_UNARMED_ATTACK "human_melee_unarmed_attack" //from mob/living/carbon/human/UnarmedAttack(): (atom/target) diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index c55662e86f..de9ada7560 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -180,6 +180,12 @@ GLOBAL_LIST_INIT(turfs_without_ground, typecacheof(list( #define isitem(A) (istype(A, /obj/item)) +#define isgrenade(A) (istype(A, /obj/item/grenade)) + +#define islandmine(A) (istype(A, /obj/effect/mine)) + +#define isammocasing(A) (istype(A, /obj/item/ammo_casing)) + #define isidcard(I) (istype(I, /obj/item/card/id)) #define isstructure(A) (istype(A, /obj/structure)) diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index f3f9f1e8b5..3977b3c085 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -196,6 +196,7 @@ #define TRAIT_NO_ALCOHOL "alcohol_intolerance" #define TRAIT_MUTATION_STASIS "mutation_stasis" //Prevents processed genetics mutations from processing. #define TRAIT_FAST_PUMP "fast_pump" +#define TRAIT_NICE_SHOT "nice_shot" //hnnnnnnnggggg..... you're pretty good.... // mobility flag traits // IN THE FUTURE, IT WOULD BE NICE TO DO SOMETHING SIMILAR TO https://github.com/tgstation/tgstation/pull/48923/files (ofcourse not nearly the same because I have my.. thoughts on it) @@ -301,3 +302,4 @@ #define ACTIVE_BLOCK_TRAIT "active_block" /// This trait is added by the parry system. #define ACTIVE_PARRY_TRAIT "active_parry" +#define STICKY_NODROP "sticky-nodrop" //sticky nodrop sounds like a bad soundcloud rapper's name \ No newline at end of file diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 508b737474..337d44aa1e 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -675,12 +675,6 @@ Turf and target are separate in case you want to teleport some distance from a t loc = loc.loc return null -//For objects that should embed, but make no sense being is_sharp or is_pointed() -//e.g: rods -GLOBAL_LIST_INIT(can_embed_types, typecacheof(list( - /obj/item/stack/rods, - /obj/item/pipe))) - /* Checks if that loc and dir has an item on the wall */ diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm index c3004a4501..85f5e4fd9b 100644 --- a/code/_globalvars/bitfields.dm +++ b/code/_globalvars/bitfields.dm @@ -140,6 +140,10 @@ GLOBAL_LIST_INIT(bitfields, list( "BLOCK_FACE_ATOM_1" = BLOCK_FACE_ATOM_1, "PREVENT_CONTENTS_EXPLOSION_1" = PREVENT_CONTENTS_EXPLOSION_1 ), + "flags_ricochet" = list( + "RICOCHET_SHINY" = RICOCHET_SHINY, + "RICOCHET_HARD" = RICOCHET_HARD + ), "clothing_flags" = list( "LAVAPROTECT" = LAVAPROTECT, "STOPSPRESSUREDAMAGE" = STOPSPRESSUREDAMAGE, diff --git a/code/_globalvars/lists/maintenance_loot.dm b/code/_globalvars/lists/maintenance_loot.dm index 69d4794e87..865f405442 100644 --- a/code/_globalvars/lists/maintenance_loot.dm +++ b/code/_globalvars/lists/maintenance_loot.dm @@ -116,6 +116,7 @@ GLOBAL_LIST_INIT(maintenance_loot, list( /obj/item/autosurgeon/testicles = 1, /obj/item/storage/box/marshmallow = 2, /obj/item/clothing/gloves/tackler/offbrand = 1, + /obj/item/stack/sticky_tape = 1, "" = 3 )) diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm index 1b100bebaf..8d8b8820b2 100644 --- a/code/_globalvars/traits.dm +++ b/code/_globalvars/traits.dm @@ -93,6 +93,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_PASSTABLE" = TRAIT_PASSTABLE, "TRAIT_GIANT" = TRAIT_GIANT, "TRAIT_DWARF" = TRAIT_DWARF, + "TRAIT_NICE_SHOT" = TRAIT_NICE_SHOT, "TRAIT_COMBAT_MODE_LOCKED" = TRAIT_COMBAT_MODE_LOCKED, "TRAIT_SPRINT_LOCKED" = TRAIT_SPRINT_LOCKED, "TRAIT_AUTO_CATCH_ITEM" = TRAIT_AUTO_CATCH_ITEM, @@ -116,7 +117,8 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_MASO" = TRAIT_MASO, "TRAIT_HIGH_BLOOD" = TRAIT_HIGH_BLOOD, "TRAIT_EMPATH" = TRAIT_EMPATH, - "TRAIT_FRIENDLY" = TRAIT_FRIENDLY + "TRAIT_FRIENDLY" = TRAIT_FRIENDLY, + "TRAIT_NICE_SHOT" = TRAIT_NICE_SHOT ), /obj/item/bodypart = list( "TRAIT_PARALYSIS" = TRAIT_PARALYSIS diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm index 3cedd92255..11531a701e 100644 --- a/code/_onclick/hud/alert.dm +++ b/code/_onclick/hud/alert.dm @@ -242,7 +242,7 @@ If you're feeling frisky, examine yourself and click the underlined item to pull /obj/screen/alert/embeddedobject/Click() if(isliving(usr)) - var/mob/living/carbon/human/M = usr + var/mob/living/carbon/M = usr return M.help_shake_act(M) /obj/screen/alert/weightless diff --git a/code/datums/components/_component.dm b/code/datums/components/_component.dm index 7f7440daf4..9e4e16f813 100644 --- a/code/datums/components/_component.dm +++ b/code/datums/components/_component.dm @@ -38,8 +38,9 @@ parent = raw_args[1] var/list/arguments = raw_args.Copy(2) if(Initialize(arglist(arguments)) == COMPONENT_INCOMPATIBLE) + stack_trace("Incompatible [type] assigned to a [parent.type]! args: [json_encode(arguments)]") qdel(src, TRUE, TRUE) - CRASH("Incompatible [type] assigned to a [parent.type]! args: [json_encode(arguments)]") + return _JoinParent(parent) diff --git a/code/datums/components/embedded.dm b/code/datums/components/embedded.dm new file mode 100644 index 0000000000..137a1bb864 --- /dev/null +++ b/code/datums/components/embedded.dm @@ -0,0 +1,363 @@ +/* + This component is responsible for handling individual instances of embedded objects. The embeddable element is what allows an item to be embeddable and stores its embedding stats, + and when it impacts and meets the requirements to stick into something, it instantiates an embedded component. Once the item falls out, the component is destroyed, while the + element survives to embed another day. + + There are 2 different things that can be embedded presently: carbons, and closed turfs (see: walls) + + - Carbon embedding has all the classical embedding behavior, and tracks more events and signals. The main behaviors and hooks to look for are: + -- Every process tick, there is a chance to randomly proc pain, controlled by pain_chance. There may also be a chance for the object to fall out randomly, per fall_chance + -- Every time the mob moves, there is a chance to proc jostling pain, controlled by jostle_chance (and only 50% as likely if the mob is walking or crawling) + -- Various signals hooking into carbon topic() and the embed removal surgery in order to handle removals. + + - Turf embedding is much simpler. All we do here is draw an overlay of the item's inhand on the turf, hide the item, and create an HTML link in the turf's inspect + that allows you to rip the item out. There's nothing dynamic about this, so far less checks. + + + In addition, there are 2 cases of embedding: embedding, and sticking + + - Embedding involves harmful and dangerous embeds, whether they cause brute damage, stamina damage, or a mix. This is the default behavior for embeddings, for when something is "pointy" + + - Sticking occurs when an item should not cause any harm while embedding (imagine throwing a sticky ball of tape at someone, rather than a shuriken). An item is considered "sticky" + when it has 0 for both pain multiplier and jostle pain multiplier. It's a bit arbitrary, but fairly straightforward. + + Stickables differ from embeds in the following ways: + -- Text descriptors use phrasing like "X is stuck to Y" rather than "X is embedded in Y" + -- There is no slicing sound on impact + -- All damage checks and bloodloss are skipped for carbons + -- Pointy objects create sparks when embedding into a turf + +*/ + + +/datum/component/embedded + dupe_mode = COMPONENT_DUPE_ALLOWED + var/obj/item/bodypart/limb + var/obj/item/weapon + + // all of this stuff is explained in _DEFINES/combat.dm + var/embed_chance // not like we really need it once we're already stuck in but hey + var/fall_chance + var/pain_chance + var/pain_mult + var/impact_pain_mult + var/remove_pain_mult + var/rip_time + var/ignore_throwspeed_threshold + var/jostle_chance + var/jostle_pain_mult + var/pain_stam_pct + var/embed_chance_turf_mod + + ///if both our pain multiplier and jostle pain multiplier are 0, we're harmless and can omit most of the damage related stuff + var/harmful + var/mutable_appearance/overlay + +/datum/component/embedded/Initialize(obj/item/I, + datum/thrownthing/throwingdatum, + obj/item/bodypart/part, + embed_chance = EMBED_CHANCE, + fall_chance = EMBEDDED_ITEM_FALLOUT, + pain_chance = EMBEDDED_PAIN_CHANCE, + pain_mult = EMBEDDED_PAIN_MULTIPLIER, + remove_pain_mult = EMBEDDED_UNSAFE_REMOVAL_PAIN_MULTIPLIER, + impact_pain_mult = EMBEDDED_IMPACT_PAIN_MULTIPLIER, + rip_time = EMBEDDED_UNSAFE_REMOVAL_TIME, + ignore_throwspeed_threshold = FALSE, + jostle_chance = EMBEDDED_JOSTLE_CHANCE, + jostle_pain_mult = EMBEDDED_JOSTLE_PAIN_MULTIPLIER, + pain_stam_pct = EMBEDDED_PAIN_STAM_PCT, + embed_chance_turf_mod = EMBED_CHANCE_TURF_MOD) + + if((!iscarbon(parent) && !isclosedturf(parent)) || !isitem(I)) + return COMPONENT_INCOMPATIBLE + + if(part) + limb = part + src.embed_chance = embed_chance + src.fall_chance = fall_chance + src.pain_chance = pain_chance + src.pain_mult = pain_mult + src.remove_pain_mult = remove_pain_mult + src.rip_time = rip_time + src.impact_pain_mult = impact_pain_mult + src.ignore_throwspeed_threshold = ignore_throwspeed_threshold + src.jostle_chance = jostle_chance + src.jostle_pain_mult = jostle_pain_mult + src.pain_stam_pct = pain_stam_pct + src.embed_chance_turf_mod = embed_chance_turf_mod + + src.weapon = I + + if(!weapon.isEmbedHarmless()) + harmful = TRUE + + weapon.embedded(parent) + + if(iscarbon(parent)) + initCarbon() + else if(isclosedturf(parent)) + initTurf(throwingdatum) + +/datum/component/embedded/RegisterWithParent() + if(iscarbon(parent)) + RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/jostleCheck) + RegisterSignal(parent, COMSIG_CARBON_EMBED_RIP, .proc/ripOutCarbon) + RegisterSignal(parent, COMSIG_CARBON_EMBED_REMOVAL, .proc/safeRemoveCarbon) + else if(isclosedturf(parent)) + RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/examineTurf) + RegisterSignal(parent, COMSIG_PARENT_QDELETING, .proc/itemMoved) + +/datum/component/embedded/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_MOVABLE_MOVED, COMSIG_CARBON_EMBED_RIP, COMSIG_CARBON_EMBED_REMOVAL, COMSIG_PARENT_EXAMINE)) + +/datum/component/embedded/process() + if(iscarbon(parent)) + processCarbon() + +/datum/component/embedded/Destroy() + if(weapon) + UnregisterSignal(weapon, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING)) + if(overlay) + var/atom/A = parent + A.cut_overlay(overlay, TRUE) + qdel(overlay) + + return ..() + +//////////////////////////////////////// +/////////////HUMAN PROCS//////////////// +//////////////////////////////////////// + +/// Set up an instance of embedding for a carbon. This is basically an extension of Initialize() so not much to say +/datum/component/embedded/proc/initCarbon() + START_PROCESSING(SSdcs, src) + var/mob/living/carbon/victim = parent + if(!istype(limb)) + limb = pick(victim.bodyparts) + + limb.embedded_objects |= weapon // on the inside... on the inside... + weapon.forceMove(victim) + RegisterSignal(weapon, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING), .proc/byeItemCarbon) + + if(harmful) + victim.visible_message("[weapon] embeds itself in [victim]'s [limb.name]!",ignored_mobs=victim) + to_chat(victim, "[weapon] embeds itself in your [limb.name]!") + victim.throw_alert("embeddedobject", /obj/screen/alert/embeddedobject) + playsound(victim,'sound/weapons/bladeslice.ogg', 40) + weapon.add_mob_blood(victim)//it embedded itself in you, of course it's bloody! + var/damage = weapon.w_class * impact_pain_mult + limb.receive_damage(brute=(1-pain_stam_pct) * damage, stamina=pain_stam_pct * damage) + SEND_SIGNAL(victim, COMSIG_ADD_MOOD_EVENT, "embedded", /datum/mood_event/embedded) + else + victim.visible_message("[weapon] sticks itself to [victim]'s [limb.name]!",ignored_mobs=victim) + to_chat(victim, "[weapon] sticks itself to your [limb.name]!") + +/// Called every time a carbon with a harmful embed moves, rolling a chance for the item to cause pain. The chance is halved if the carbon is crawling or walking. +/datum/component/embedded/proc/jostleCheck() + var/mob/living/carbon/victim = parent + + var/chance = jostle_chance + if(victim.m_intent == MOVE_INTENT_WALK || !(victim.mobility_flags & MOBILITY_STAND)) + chance *= 0.5 + + if(harmful && prob(chance)) + var/damage = weapon.w_class * jostle_pain_mult + limb.receive_damage(brute=(1-pain_stam_pct) * damage, stamina=pain_stam_pct * damage) + to_chat(victim, "[weapon] embedded in your [limb.name] jostles and stings!") + + +/// Called when then item randomly falls out of a carbon. This handles the damage and descriptors, then calls safe_remove() +/datum/component/embedded/proc/fallOutCarbon() + var/mob/living/carbon/victim = parent + + if(harmful) + var/damage = weapon.w_class * remove_pain_mult + limb.receive_damage(brute=(1-pain_stam_pct) * damage, stamina=pain_stam_pct * damage) + victim.visible_message("[weapon] falls out of [victim.name]'s [limb.name]!", ignored_mobs=victim) + to_chat(victim, "[weapon] falls out of your [limb.name]!") + else + victim.visible_message("[weapon] falls off of [victim.name]'s [limb.name]!", ignored_mobs=victim) + to_chat(victim, "[weapon] falls off of your [limb.name]!") + + safeRemoveCarbon() + + +/// Called when a carbon with an object embedded/stuck to them inspects themselves and clicks the appropriate link to begin ripping the item out. This handles the ripping attempt, descriptors, and dealing damage, then calls safe_remove() +/datum/component/embedded/proc/ripOutCarbon(datum/source, obj/item/I, obj/item/bodypart/limb) + if(I != weapon || src.limb != limb) + return + + var/mob/living/carbon/victim = parent + var/time_taken = rip_time * weapon.w_class + + victim.visible_message("[victim] attempts to remove [weapon] from [victim.p_their()] [limb.name].","You attempt to remove [weapon] from your [limb.name]... (It will take [DisplayTimeText(time_taken)].)") + if(do_after(victim, time_taken, target = victim)) + if(!weapon || !limb || weapon.loc != victim || !(weapon in limb.embedded_objects)) + qdel(src) + return + + if(harmful) + var/damage = weapon.w_class * remove_pain_mult + limb.receive_damage(brute=(1-pain_stam_pct) * damage, stamina=pain_stam_pct * damage) //It hurts to rip it out, get surgery you dingus. + victim.emote("scream") + victim.visible_message("[victim] successfully rips [weapon] out of [victim.p_their()] [limb.name]!", "You successfully remove [weapon] from your [limb.name].") + else + victim.visible_message("[victim] successfully rips [weapon] off of [victim.p_their()] [limb.name]!", "You successfully remove [weapon] from your [limb.name].") + + safeRemoveCarbon(TRUE) + + +/// This proc handles the final step and actual removal of an embedded/stuck item from a carbon, whether or not it was actually removed safely. +/// Pass TRUE for to_hands if we want it to go to the victim's hands when they pull it out +/datum/component/embedded/proc/safeRemoveCarbon(to_hands) + var/mob/living/carbon/victim = parent + limb.embedded_objects -= weapon + + UnregisterSignal(weapon, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING)) // have to unhook these here so they don't also register as having disappeared + + if(!weapon) + if(!victim.has_embedded_objects()) + victim.clear_alert("embeddedobject") + SEND_SIGNAL(victim, COMSIG_CLEAR_MOOD_EVENT, "embedded") + qdel(src) + return + + if(weapon.unembedded()) // if it deleted itself + weapon = null + if(!victim.has_embedded_objects()) + victim.clear_alert("embeddedobject") + SEND_SIGNAL(victim, COMSIG_CLEAR_MOOD_EVENT, "embedded") + qdel(src) + return + + if(to_hands) + victim.put_in_hands(weapon) + else + weapon.forceMove(get_turf(victim)) + + if(!victim.has_embedded_objects()) + victim.clear_alert("embeddedobject") + SEND_SIGNAL(victim, COMSIG_CLEAR_MOOD_EVENT, "embedded") + qdel(src) + + +/// Something deleted or moved our weapon while it was embedded, how rude! +/datum/component/embedded/proc/byeItemCarbon() + var/mob/living/carbon/victim = parent + limb.embedded_objects -= weapon + UnregisterSignal(weapon, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING)) + + if(victim) + to_chat(victim, "\The [weapon] that was embedded in your [limb.name] disappears!") + if(!victim.has_embedded_objects()) + victim.clear_alert("embeddedobject") + SEND_SIGNAL(victim, COMSIG_CLEAR_MOOD_EVENT, "embedded") + weapon = null + qdel(src) + + +/// Items embedded/stuck to carbons both check whether they randomly fall out (if applicable), as well as if the target mob and limb still exists. +/// Items harmfully embedded in carbons have an additional check for random pain (if applicable) +/datum/component/embedded/proc/processCarbon() + var/mob/living/carbon/victim = parent + + if(!victim || !limb) // in case the victim and/or their limbs exploded (say, due to a sticky bomb) + weapon.forceMove(get_turf(weapon)) + qdel(src) + + if(victim.stat == DEAD) + return + + var/damage = weapon.w_class * pain_mult + var/chance = pain_chance + if(pain_stam_pct && IS_STAMCRIT(victim)) //if it's a less-lethal embed, give them a break if they're already stamcritted + chance *= 0.3 + damage *= 0.7 + + if(harmful && prob(chance)) + limb.receive_damage(brute=(1-pain_stam_pct) * damage, stamina=pain_stam_pct * damage) + to_chat(victim, "[weapon] embedded in your [limb.name] hurts!") + + if(prob(fall_chance)) + fallOutCarbon() + + + +//////////////////////////////////////// +//////////////TURF PROCS//////////////// +//////////////////////////////////////// + +/// Turfs are much lower maintenance, since we don't care if they're in pain, but since they don't bleed or scream, we draw an overlay to show their status. +/// The only difference pointy/sticky items make here is text descriptors and pointy objects making a spark shower on impact. +/datum/component/embedded/proc/initTurf(datum/thrownthing/throwingdatum) + var/turf/closed/hit = parent + + // we can't store the item IN the turf (cause turfs are just kinda... there), so we fake it by making the item invisible and bailing if it moves due to a blast + weapon.forceMove(hit) + weapon.invisibility = INVISIBILITY_ABSTRACT + RegisterSignal(weapon, COMSIG_MOVABLE_MOVED, .proc/itemMoved) + + var/pixelX = rand(-2, 2) + var/pixelY = rand(-1, 3) // bias this upwards since in-hands are usually on the lower end of the sprite + + switch(throwingdatum.init_dir) + if(NORTH) + pixelY -= 2 + if(SOUTH) + pixelY += 2 + if(WEST) + pixelX += 2 + if(EAST) + pixelX -= 2 + + if(throwingdatum.init_dir in list(NORTH, WEST, NORTHWEST, SOUTHWEST)) + overlay = mutable_appearance(icon=weapon.righthand_file,icon_state=weapon.item_state) + else + overlay = mutable_appearance(icon=weapon.lefthand_file,icon_state=weapon.item_state) + + var/matrix/M = matrix() + M.Translate(pixelX, pixelY) + overlay.transform = M + hit.add_overlay(overlay, TRUE) + + if(harmful) + hit.visible_message("[weapon] embeds itself in [hit]!") + playsound(hit,'sound/weapons/bladeslice.ogg', 70) + + var/datum/effect_system/spark_spread/sparks = new + sparks.set_up(1, 1, parent) + sparks.attach(parent) + sparks.start() + else + hit.visible_message("[weapon] sticks itself to [hit]!") + + +/datum/component/embedded/proc/examineTurf(datum/source, mob/user, list/examine_list) + if(harmful) + examine_list += "\t There is \a [weapon] embedded in [parent]!" + else + examine_list += "\t There is \a [weapon] stuck to [parent]!" + + +/// Someone is ripping out the item from the turf by hand +/datum/component/embedded/Topic(datum/source, href_list) + var/mob/living/us = usr + if(in_range(us, parent) && locate(href_list["embedded_object"]) == weapon) + if(harmful) + us.visible_message("[us] begins unwedging [weapon] from [parent].", "You begin unwedging [weapon] from [parent]...") + else + us.visible_message("[us] begins unsticking [weapon] from [parent].", "You begin unsticking [weapon] from [parent]...") + + if(do_after(us, 30, target = parent)) + us.put_in_hands(weapon) + weapon.unembedded() + qdel(src) + + +/// This proc handles if something knocked the invisible item loose from the turf somehow (probably an explosion). Just make it visible and say it fell loose, then get outta here. +/datum/component/embedded/proc/itemMoved() + weapon.invisibility = initial(weapon.invisibility) + weapon.visible_message("[weapon] falls loose from [parent].") + weapon.unembedded() + qdel(src) diff --git a/code/datums/components/fantasy/suffixes.dm b/code/datums/components/fantasy/suffixes.dm index f32ca50595..59328b9bc9 100644 --- a/code/datums/components/fantasy/suffixes.dm +++ b/code/datums/components/fantasy/suffixes.dm @@ -144,7 +144,7 @@ var/obj/item/projectile/picked_projectiletype = pickweight(weighted_projectile_types) var/obj/item/master = comp.parent - comp.appliedComponents += master.AddComponent(/datum/component/shrapnel, picked_projectiletype) + comp.appliedComponents += master.AddComponent(/datum/component/mirv, picked_projectiletype) return "[newName] of [initial(picked_projectiletype.name)] shrapnel" /datum/fantasy_affix/strength diff --git a/code/datums/components/shrapnel.dm b/code/datums/components/mirv.dm similarity index 70% rename from code/datums/components/shrapnel.dm rename to code/datums/components/mirv.dm index 4d1fe21b95..2a1663776c 100644 --- a/code/datums/components/shrapnel.dm +++ b/code/datums/components/mirv.dm @@ -1,36 +1,37 @@ - -/datum/component/shrapnel +/datum/component/mirv var/projectile_type var/radius // shoots a projectile for every turf on this radius from the hit target var/override_projectile_range -/datum/component/shrapnel/Initialize(projectile_type, radius=1, override_projectile_range) - if(!isgun(parent) && !ismachinery(parent) && !isstructure(parent)) +/datum/component/mirv/Initialize(projectile_type, radius=1, override_projectile_range) + if(!isgun(parent) && !ismachinery(parent) && !isstructure(parent) && !isgrenade(parent)) return COMPONENT_INCOMPATIBLE src.projectile_type = projectile_type src.radius = radius src.override_projectile_range = override_projectile_range + if(isgrenade(parent)) + parent.AddComponent(/datum/component/pellet_cloud, projectile_type=projectile_type) -/datum/component/shrapnel/RegisterWithParent() +/datum/component/mirv/RegisterWithParent() . = ..() if(ismachinery(parent) || isstructure(parent) || isgun(parent)) // turrets, etc RegisterSignal(parent, COMSIG_PROJECTILE_ON_HIT, .proc/projectile_hit) -/datum/component/shrapnel/UnregisterFromParent() +/datum/component/mirv/UnregisterFromParent() . = ..() UnregisterSignal(parent, list(COMSIG_PROJECTILE_ON_HIT)) -/datum/component/shrapnel/proc/projectile_hit(atom/fired_from, atom/movable/firer, atom/target, Angle) +/datum/component/mirv/proc/projectile_hit(atom/fired_from, atom/movable/firer, atom/target, Angle) do_shrapnel(firer, target) -/datum/component/shrapnel/proc/do_shrapnel(mob/firer, atom/target) +/datum/component/mirv/proc/do_shrapnel(mob/firer, atom/target) if(radius < 1) return var/turf/target_turf = get_turf(target) for(var/turf/shootat_turf in RANGE_TURFS(radius, target) - RANGE_TURFS(radius-1, target)) - var/obj/item/projectile/P = new projectile_type(target_turf) + var/obj/item/projectile/P = new projectile_type(target_turf) //Shooting Code: P.range = radius+1 if(override_projectile_range) diff --git a/code/datums/components/pellet_cloud.dm b/code/datums/components/pellet_cloud.dm new file mode 100644 index 0000000000..938d9f5f56 --- /dev/null +++ b/code/datums/components/pellet_cloud.dm @@ -0,0 +1,283 @@ +/* + * This component is used when you want to create a bunch of shrapnel or projectiles (say, shrapnel from a fragmentation grenade, or buckshot from a shotgun) from a central point, + * without necessarily printing a separate message for every single impact. This component should be instantiated right when you need it (like the moment of firing), then activated + * by signal. + * + * Pellet cloud currently works on two classes of sources: directed (ammo casings), and circular (grenades, landmines). + * -Directed: This means you're shooting multiple pellets, like buckshot. If an ammo casing is defined as having multiple pellets, it will automatically create a pellet cloud + * and call COMSIG_PELLET_CLOUD_INIT (see [/obj/item/ammo_casing/proc/fire_casing]). Thus, the only projectiles fired will be the ones fired here. + * The magnitude var controls how many pellets are created. + * -Circular: This results in a big spray of shrapnel flying all around the detonation point when the grenade fires COMSIG_GRENADE_PRIME or landmine triggers COMSIG_MINE_TRIGGERED. + * The magnitude var controls how big the detonation radius is (the bigger the magnitude, the more shrapnel is created). Grenades can be covered with bodies to reduce shrapnel output. + * + * Once all of the fired projectiles either hit a target or disappear due to ranging out/whatever else, we resolve the list of all the things we hit and print aggregate messages so we get + * one "You're hit by 6 buckshot pellets" vs 6x "You're hit by the buckshot blah blah" messages. + * + * Note that this is how all guns handle shooting ammo casings with multiple pellets, in case such a thing comes up. +*/ + +/datum/component/pellet_cloud + /// What's the projectile path of the shrapnel we're shooting? + var/projectile_type + + /// How many shrapnel projectiles are we responsible for tracking? May be reduced for grenades if someone dives on top of it. Defined by ammo casing for casings, derived from magnitude otherwise + var/num_pellets + /// For grenades/landmines, how big is the radius of turfs we're targeting? Note this does not effect the projectiles range, only how many we generate + var/radius = 4 + + /// The list of pellets we're responsible for tracking, once these are all accounted for, we finalize. + var/list/pellets = list() + /// An associated list with the atom hit as the key and how many pellets they've eaten for the value, for printing aggregate messages + var/list/targets_hit = list() + /// For grenades, any /mob/living's the grenade is moved onto, see [/datum/component/pellet_cloud/proc/handle_martyrs()] + var/list/bodies + /// For grenades, tracking people who die covering a grenade for achievement purposes, see [/datum/component/pellet_cloud/proc/handle_martyrs()] + var/list/purple_hearts + + /// For grenades, tracking how many pellets are removed due to martyrs and how many pellets are added due to the last person to touch it being on top of it + var/pellet_delta = 0 + /// how many pellets ranged out without hitting anything + var/terminated + /// how many pellets impacted something + var/hits + /// If the parent tried deleting and we're not done yet, we send it to nullspace then delete it after + var/queued_delete = FALSE + + /// for if we're an ammo casing being fired + var/mob/living/shooter + +/datum/component/pellet_cloud/Initialize(projectile_type=/obj/item/shrapnel, magnitude=5) + if(!isammocasing(parent) && !isgrenade(parent) && !islandmine(parent)) + return COMPONENT_INCOMPATIBLE + + if(magnitude < 1) + stack_trace("Invalid magnitude [magnitude] < 1 on pellet_cloud, parent: [parent]") + magnitude = 1 + + src.projectile_type = projectile_type + + if(isammocasing(parent)) + num_pellets = magnitude + else if(isgrenade(parent) || islandmine(parent)) + radius = magnitude + +/datum/component/pellet_cloud/Destroy(force, silent) + purple_hearts = null + pellets = null + targets_hit = null + bodies = null + return ..() + +/datum/component/pellet_cloud/RegisterWithParent() + RegisterSignal(parent, COMSIG_PARENT_PREQDELETED, .proc/nullspace_parent) + if(isammocasing(parent)) + RegisterSignal(parent, COMSIG_PELLET_CLOUD_INIT, .proc/create_casing_pellets) + else if(isgrenade(parent)) + RegisterSignal(parent, COMSIG_GRENADE_ARMED, .proc/grenade_armed) + RegisterSignal(parent, COMSIG_GRENADE_PRIME, .proc/create_blast_pellets) + else if(islandmine(parent)) + RegisterSignal(parent, COMSIG_MINE_TRIGGERED, .proc/create_blast_pellets) + +/datum/component/pellet_cloud/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_PARENT_PREQDELETED, COMSIG_PELLET_CLOUD_INIT, COMSIG_GRENADE_PRIME, COMSIG_GRENADE_ARMED, COMSIG_MOVABLE_MOVED, COMSIG_MOVABLE_UNCROSSED, COMSIG_MINE_TRIGGERED, COMSIG_ITEM_DROPPED)) + +/** + * create_casing_pellets() is for directed pellet clouds for ammo casings that have multiple pellets (buckshot and scatter lasers for instance) + * + * Honestly this is mostly just a rehash of [/obj/item/ammo_casing/proc/fire_casing()] for pellet counts > 1, except this lets us tamper with the pellets and hook onto them for tracking purposes. + * The arguments really don't matter, this proc is triggered by COMSIG_PELLET_CLOUD_INIT which is only for this really, it's just a big mess of the state vars we need for doing the stuff over here. + */ +/datum/component/pellet_cloud/proc/create_casing_pellets(obj/item/ammo_casing/shell, atom/target, mob/living/user, fired_from, randomspread, spread, zone_override, params, distro) + shooter = user + var/targloc = get_turf(target) + if(!zone_override) + zone_override = shooter.zone_selected + + for(var/i in 1 to num_pellets) + shell.ready_proj(target, user, SUPPRESSED_VERY, zone_override, fired_from) + if(distro) + if(randomspread) + spread = round((rand() - 0.5) * distro) + else //Smart spread + spread = round((i / num_pellets - 0.5) * distro) + + RegisterSignal(shell.BB, COMSIG_PROJECTILE_SELF_ON_HIT, .proc/pellet_hit) + RegisterSignal(shell.BB, list(COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PARENT_QDELETING), .proc/pellet_range) + pellets += shell.BB + if(!shell.throw_proj(target, targloc, shooter, params, spread)) + return + if(i != num_pellets) + shell.newshot() + +/** + * create_blast_pellets() is for when we have a central point we want to shred the surroundings of with a ring of shrapnel, namely frag grenades and landmines. + * + * Note that grenades have extra handling for someone throwing themselves/being thrown on top of it, while landmines do not (obviously, it's a landmine!). See [/datum/component/pellet_cloud/proc/handle_martyrs()] + */ +/datum/component/pellet_cloud/proc/create_blast_pellets(obj/O, mob/living/lanced_by) + var/atom/A = parent + + if(isgrenade(parent)) // handle_martyrs can reduce the radius and thus the number of pellets we produce if someone dives on top of a frag grenade + handle_martyrs(lanced_by) // note that we can modify radius in this proc + + if(radius < 1) + return + + var/list/all_the_turfs_were_gonna_lacerate = RANGE_TURFS(radius, A) - RANGE_TURFS(radius-1, A) + num_pellets = all_the_turfs_were_gonna_lacerate.len + pellet_delta + + for(var/T in all_the_turfs_were_gonna_lacerate) + var/turf/shootat_turf = T + pew(shootat_turf) + +/** + * handle_martyrs() is used for grenades that shoot shrapnel to check if anyone threw themselves/were thrown on top of the grenade, thus absorbing a good chunk of the shrapnel + * + * Between the time the grenade is armed and the actual detonation, we set var/list/bodies to the list of mobs currently on the new tile, as if the grenade landed on top of them, tracking if any of them move off the tile and removing them from the "under" list + * Once the grenade detonates, handle_martyrs() is called and gets all the new mobs on the tile, and add the ones not in var/list/bodies to var/list/martyrs + * We then iterate through the martyrs and reduce the shrapnel magnitude for each mob on top of it, shredding each of them with some of the shrapnel they helped absorb. This can snuff out all of the shrapnel if there's enough bodies + * + * Note we track anyone who's alive and client'd when they get shredded in var/list/purple_hearts, for achievement checking later + */ +/datum/component/pellet_cloud/proc/handle_martyrs(mob/living/lanced_by) + var/magnitude_absorbed + var/list/martyrs = list() + + var/self_harm_radius_mult = 3 + + if(lanced_by && prob(60)) + to_chat(lanced_by, "Your plan to whack someone with a grenade on a stick backfires on you, literally!") + self_harm_radius_mult = 1 // we'll still give the guy who got hit some extra shredding, but not 3*radius + pellet_delta += radius + for(var/i in 1 to radius) + pew(lanced_by) // thought you could be tricky and lance someone with no ill effects!! + + for(var/mob/living/body in get_turf(parent)) + if(body == shooter) + pellet_delta += radius * self_harm_radius_mult + for(var/i in 1 to radius * self_harm_radius_mult) + pew(body) // free shrapnel if it goes off in your hand, and it doesn't even count towards the absorbed. fun! + else if(!(body in bodies)) + martyrs += body // promoted from a corpse to a hero + + for(var/M in martyrs) + var/mob/living/martyr = M + if(radius > 4) + martyr.visible_message("[martyr] heroically covers \the [parent] with [martyr.p_their()] body, absorbing a load of the shrapnel!", "You heroically cover \the [parent] with your body, absorbing a load of the shrapnel!") + magnitude_absorbed += round(radius * 0.5) + else if(radius >= 2) + martyr.visible_message("[martyr] heroically covers \the [parent] with [martyr.p_their()] body, absorbing some of the shrapnel!", "You heroically cover \the [parent] with your body, absorbing some of the shrapnel!") + magnitude_absorbed += 2 + else + martyr.visible_message("[martyr] heroically covers \the [parent] with [martyr.p_their()] body, snuffing out the shrapnel!", "You heroically cover \the [parent] with your body, snuffing out the shrapnel!") + magnitude_absorbed = radius + + var/pellets_absorbed = (radius ** 2) - ((radius - magnitude_absorbed - 1) ** 2) + radius -= magnitude_absorbed + pellet_delta -= round(pellets_absorbed * 0.5) + + if(martyr.stat != DEAD && martyr.client) + LAZYADD(purple_hearts, martyr) + RegisterSignal(martyr, COMSIG_PARENT_QDELETING, .proc/on_target_qdel, override=TRUE) + + for(var/i in 1 to round(pellets_absorbed * 0.5)) + pew(martyr) + + if(radius < 1) + break + +///One of our pellets hit something, record what it was and check if we're done (terminated == num_pellets) +/datum/component/pellet_cloud/proc/pellet_hit(obj/item/projectile/P, atom/movable/firer, atom/target, Angle) + pellets -= P + terminated++ + hits++ + targets_hit[target]++ + if(targets_hit[target] == 1) + RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/on_target_qdel, override=TRUE) + UnregisterSignal(P, list(COMSIG_PARENT_QDELETING, COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PROJECTILE_SELF_ON_HIT)) + if(terminated == num_pellets) + finalize() + +///One of our pellets disappeared due to hitting their max range (or just somehow got qdel'd), remove it from our list and check if we're done (terminated == num_pellets) +/datum/component/pellet_cloud/proc/pellet_range(obj/item/projectile/P) + pellets -= P + terminated++ + UnregisterSignal(P, list(COMSIG_PARENT_QDELETING, COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PROJECTILE_SELF_ON_HIT)) + if(terminated == num_pellets) + finalize() + +/// Minor convenience function for creating each shrapnel piece with circle explosions, mostly stolen from the MIRV component +/datum/component/pellet_cloud/proc/pew(atom/target, spread=0) + var/obj/item/projectile/P = new projectile_type(get_turf(parent)) + + //Shooting Code: + P.spread = spread + P.original = target + P.fired_from = parent + P.firer = parent // don't hit ourself that would be really annoying + P.permutated += parent // don't hit the target we hit already with the flak + P.suppressed = SUPPRESSED_VERY // set the projectiles to make no message so we can do our own aggregate message + P.preparePixelProjectile(target, parent) + RegisterSignal(P, COMSIG_PROJECTILE_SELF_ON_HIT, .proc/pellet_hit) + RegisterSignal(P, list(COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PARENT_QDELETING), .proc/pellet_range) + pellets += P + P.fire() + +///All of our pellets are accounted for, time to go target by target and tell them how many things they got hit by. +/datum/component/pellet_cloud/proc/finalize() + var/obj/item/projectile/P = projectile_type + var/proj_name = initial(P.name) + + for(var/atom/target in targets_hit) + var/num_hits = targets_hit[target] + UnregisterSignal(target, COMSIG_PARENT_QDELETING) + if(num_hits > 1) + target.visible_message("[target] is hit by [num_hits] [proj_name]s!", null, null, COMBAT_MESSAGE_RANGE, target) + to_chat(target, "You're hit by [num_hits] [proj_name]s!") + else + target.visible_message("[target] is hit by a [proj_name]!", null, null, COMBAT_MESSAGE_RANGE, target) + to_chat(target, "You're hit by a [proj_name]!") + + UnregisterSignal(parent, COMSIG_PARENT_PREQDELETED) + if(queued_delete) + qdel(parent) + qdel(src) + +/// Look alive, we're armed! Now we start watching to see if anyone's covering us +/datum/component/pellet_cloud/proc/grenade_armed(obj/item/nade) + if(ismob(nade.loc)) + shooter = nade.loc + LAZYINITLIST(bodies) + RegisterSignal(parent, COMSIG_ITEM_DROPPED, .proc/grenade_dropped) + RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/grenade_moved) + RegisterSignal(parent, COMSIG_MOVABLE_UNCROSSED, .proc/grenade_uncrossed) + +/// Someone dropped the grenade, so set them to the shooter in case they're on top of it when it goes off +/datum/component/pellet_cloud/proc/grenade_dropped(obj/item/nade, mob/living/slick_willy) + shooter = slick_willy + grenade_moved() + +/// Our grenade has moved, reset var/list/bodies so we're "on top" of any mobs currently on the tile +/datum/component/pellet_cloud/proc/grenade_moved() + LAZYCLEARLIST(bodies) + for(var/mob/living/L in get_turf(parent)) + RegisterSignal(L, COMSIG_PARENT_QDELETING, .proc/on_target_qdel, override=TRUE) + bodies += L + +/// Someone who was originally "under" the grenade has moved off the tile and is now eligible for being a martyr and "covering" it +/datum/component/pellet_cloud/proc/grenade_uncrossed(datum/source, atom/movable/AM) + bodies -= AM + +/// Our grenade or landmine or caseless shell or whatever tried deleting itself, so we intervene and nullspace it until we're done here +/datum/component/pellet_cloud/proc/nullspace_parent() + var/atom/movable/AM = parent + AM.moveToNullspace() + queued_delete = TRUE + return TRUE + +/// Someone who was originally "under" the grenade has moved off the tile and is now eligible for being a martyr and "covering" it +/datum/component/pellet_cloud/proc/on_target_qdel(atom/target) + UnregisterSignal(target, COMSIG_PARENT_QDELETING) + targets_hit -= target + bodies -= target + purple_hearts -= target diff --git a/code/datums/components/tackle.dm b/code/datums/components/tackle.dm index 9a98953985..bd12ecc092 100644 --- a/code/datums/components/tackle.dm +++ b/code/datums/components/tackle.dm @@ -206,7 +206,7 @@ user.set_resting(FALSE, TRUE, FALSE) user.forceMove(get_turf(target)) target.adjustStaminaLoss(65) - target.Paralyze(10) + target.Paralyze(10) target.DefaultCombatKnockdown(20) if(ishuman(target) && iscarbon(user)) target.grabbedby(user) @@ -415,10 +415,10 @@ for(var/i = 0, i < speed, i++) var/obj/item/shard/shard = new /obj/item/shard(get_turf(user)) //shard.embedding = list(embed_chance = 100, ignore_throwspeed_threshold = TRUE, impact_pain_mult=3, pain_chance=5) - //shard.AddElement(/datum/element/embed, shard.embedding) + shard.updateEmbedding() user.hitby(shard, skipcatch = TRUE, hitpush = FALSE) - //shard.embedding = list() - //shard.AddElement(/datum/element/embed, shard.embedding) + shard.embedding = list() + shard.updateEmbedding() W.obj_destruction() user.adjustStaminaLoss(10 * speed) user.DefaultCombatKnockdown(40) diff --git a/code/datums/elements/_element.dm b/code/datums/elements/_element.dm index f74dcd0b6e..a147325170 100644 --- a/code/datums/elements/_element.dm +++ b/code/datums/elements/_element.dm @@ -19,11 +19,13 @@ SHOULD_CALL_PARENT(1) if(type == /datum/element) return ELEMENT_INCOMPATIBLE + SEND_SIGNAL(target, COMSIG_ELEMENT_ATTACH, src) if(element_flags & ELEMENT_DETACH) RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/Detach, override = TRUE) /// Deactivates the functionality defines by the element on the given datum /datum/element/proc/Detach(datum/source, force) + SEND_SIGNAL(source, COMSIG_ELEMENT_DETACH, src) SHOULD_CALL_PARENT(1) UnregisterSignal(source, COMSIG_PARENT_QDELETING) diff --git a/code/datums/elements/embed.dm b/code/datums/elements/embed.dm new file mode 100644 index 0000000000..86189b67e1 --- /dev/null +++ b/code/datums/elements/embed.dm @@ -0,0 +1,227 @@ +/* + The presence of this element allows an item (or a projectile carrying an item) to embed itself in a human or turf when it is thrown into a target (whether by hand, gun, or explosive wave) with either + at least 4 throwspeed (EMBED_THROWSPEED_THRESHOLD) or ignore_throwspeed_threshold set to TRUE. Items meant to be used as shrapnel for projectiles should have ignore_throwspeed_threshold set to true. + + Whether we're dealing with a direct /obj/item (throwing a knife at someone) or an /obj/projectile with a shrapnel_type, how we handle things plays out the same, with one extra step separating them. + Items simply make their COMSIG_MOVABLE_IMPACT or COMSIG_MOVABLE_IMPACT_ZONE check (against a closed turf or a carbon, respectively), while projectiles check on COMSIG_PROJECTILE_SELF_ON_HIT. + Upon a projectile hitting a valid target, it spawns whatever type of payload it has defined, then has that try to embed itself in the target on its own. + + Otherwise non-embeddable or stickable items can be made embeddable/stickable through wizard events/sticky tape/admin memes. +*/ + +#define STANDARD_WALL_HARDNESS 40 + +/datum/element/embed + element_flags = ELEMENT_BESPOKE + id_arg_index = 2 + var/initialized = FALSE /// whether we can skip assigning all the vars (since these are bespoke elements, we don't have to reset the vars every time we attach to something, we already know what we are!) + + // all of this stuff is explained in _DEFINES/combat.dm + var/embed_chance + var/fall_chance + var/pain_chance + var/pain_mult + var/remove_pain_mult + var/impact_pain_mult + var/rip_time + var/ignore_throwspeed_threshold + var/jostle_chance + var/jostle_pain_mult + var/pain_stam_pct + var/payload_type + var/embed_chance_turf_mod + +/datum/element/embed/Attach(datum/target, embed_chance, fall_chance, pain_chance, pain_mult, remove_pain_mult, impact_pain_mult, rip_time, ignore_throwspeed_threshold, jostle_chance, jostle_pain_mult, pain_stam_pct, embed_chance_turf_mod, projectile_payload=/obj/item/shard) + . = ..() + + if(!isitem(target) && !isprojectile(target)) + return ELEMENT_INCOMPATIBLE + + if(isitem(target)) + RegisterSignal(target, COMSIG_MOVABLE_IMPACT_ZONE, .proc/checkEmbedMob) + RegisterSignal(target, COMSIG_MOVABLE_IMPACT, .proc/checkEmbedOther) + RegisterSignal(target, COMSIG_ELEMENT_ATTACH, .proc/severancePackage) + RegisterSignal(target, COMSIG_PARENT_EXAMINE, .proc/examined) + RegisterSignal(target, COMSIG_EMBED_TRY_FORCE, .proc/tryForceEmbed) + RegisterSignal(target, COMSIG_ITEM_DISABLE_EMBED, .proc/detachFromWeapon) + if(!initialized) + src.embed_chance = embed_chance + src.fall_chance = fall_chance + src.pain_chance = pain_chance + src.pain_mult = pain_mult + src.remove_pain_mult = remove_pain_mult + src.rip_time = rip_time + src.impact_pain_mult = impact_pain_mult + src.ignore_throwspeed_threshold = ignore_throwspeed_threshold + src.jostle_chance = jostle_chance + src.jostle_pain_mult = jostle_pain_mult + src.pain_stam_pct = pain_stam_pct + src.embed_chance_turf_mod = embed_chance_turf_mod + initialized = TRUE + else + payload_type = projectile_payload + RegisterSignal(target, COMSIG_PROJECTILE_SELF_ON_HIT, .proc/checkEmbedProjectile) + + +/datum/element/embed/Detach(obj/target) + . = ..() + if(isitem(target)) + UnregisterSignal(target, list(COMSIG_MOVABLE_IMPACT_ZONE, COMSIG_ELEMENT_ATTACH, COMSIG_MOVABLE_IMPACT, COMSIG_PARENT_EXAMINE, COMSIG_EMBED_TRY_FORCE, COMSIG_ITEM_DISABLE_EMBED)) + else + UnregisterSignal(target, list(COMSIG_PROJECTILE_SELF_ON_HIT)) + + +/// Checking to see if we're gonna embed into a human +/datum/element/embed/proc/checkEmbedMob(obj/item/weapon, mob/living/carbon/victim, hit_zone, datum/thrownthing/throwingdatum, forced=FALSE) + if(!istype(victim) || HAS_TRAIT(victim, TRAIT_PIERCEIMMUNE)) + return + + var/actual_chance = embed_chance + + if(!weapon.isEmbedHarmless()) // all the armor in the world won't save you from a kick me sign + var/armor = max(victim.run_armor_check(hit_zone, "bullet", silent=TRUE), victim.run_armor_check(hit_zone, "bomb", silent=TRUE)) // we'll be nice and take the better of bullet and bomb armor + + if(armor) // we only care about armor penetration if there's actually armor to penetrate + var/pen_mod = -armor + weapon.armour_penetration // even a little bit of armor can make a big difference for shrapnel with large negative armor pen + actual_chance += pen_mod // doing the armor pen as a separate calc just in case this ever gets expanded on + if(actual_chance <= 0) + victim.visible_message("[weapon] bounces off [victim]'s armor!", "[weapon] bounces off your armor!", vision_distance = COMBAT_MESSAGE_RANGE) + return + + var/roll_embed = prob(actual_chance) + var/pass = forced || ((((throwingdatum ? throwingdatum.speed : weapon.throw_speed) >= EMBED_THROWSPEED_THRESHOLD) || ignore_throwspeed_threshold) && roll_embed) + if(!pass) + return + + var/obj/item/bodypart/limb = victim.get_bodypart(hit_zone) || pick(victim.bodyparts) + victim.AddComponent(/datum/component/embedded,\ + weapon,\ + throwingdatum,\ + part = limb,\ + embed_chance = embed_chance,\ + fall_chance = fall_chance,\ + pain_chance = pain_chance,\ + pain_mult = pain_mult,\ + remove_pain_mult = remove_pain_mult,\ + rip_time = rip_time,\ + ignore_throwspeed_threshold = ignore_throwspeed_threshold,\ + jostle_chance = jostle_chance,\ + jostle_pain_mult = jostle_pain_mult,\ + pain_stam_pct = pain_stam_pct,\ + embed_chance_turf_mod = embed_chance_turf_mod) + + return TRUE + +/// We need the hit_zone if we're embedding into a human, so this proc only handles if we're embedding into a turf +/datum/element/embed/proc/checkEmbedOther(obj/item/weapon, turf/closed/hit, datum/thrownthing/throwingdatum, forced=FALSE) + if(!istype(hit)) + return + + var/chance = embed_chance + embed_chance_turf_mod + if(iswallturf(hit)) + var/turf/closed/wall/W = hit + chance += 2 * (W.hardness - STANDARD_WALL_HARDNESS) + + if(!forced && chance <= 0 || embed_chance_turf_mod <= -100) + return + + var/pass = ((((throwingdatum ? throwingdatum.speed : weapon.throw_speed) >= EMBED_THROWSPEED_THRESHOLD) || ignore_throwspeed_threshold) && prob(chance)) + if(!pass) + return + + hit.AddComponent(/datum/component/embedded,\ + weapon,\ + throwingdatum,\ + embed_chance = embed_chance,\ + fall_chance = fall_chance,\ + pain_chance = pain_chance,\ + pain_mult = pain_mult,\ + remove_pain_mult = remove_pain_mult,\ + rip_time = rip_time,\ + ignore_throwspeed_threshold = ignore_throwspeed_threshold,\ + jostle_chance = jostle_chance,\ + jostle_pain_mult = jostle_pain_mult,\ + pain_stam_pct = pain_stam_pct,\ + embed_chance_turf_mod = embed_chance_turf_mod) + + return TRUE + +///A different embed element has been attached, so we'll detach and let them handle things +/datum/element/embed/proc/severancePackage(obj/item/weapon, datum/element/E) + if(istype(E, /datum/element/embed)) + Detach(weapon) + +///If we don't want to be embeddable anymore (deactivating an e-dagger for instance) +/datum/element/embed/proc/detachFromWeapon(obj/weapon) + Detach(weapon) + +///Someone inspected our embeddable item +/datum/element/embed/proc/examined(obj/item/I, mob/user, list/examine_list) + if(I.isEmbedHarmless()) + examine_list += "[I] feels sticky, and could probably get stuck to someone if thrown properly!" + else + examine_list += "[I] has a fine point, and could probably embed in someone if thrown properly!" + +/** + * checkEmbedProjectile() is what we get when a projectile with a defined shrapnel_type impacts a target. + * + * If we hit a valid target (carbon or closed turf), we create the shrapnel_type object and immediately call tryEmbed() on it, targeting what we impacted. That will lead + * it to call tryForceEmbed() on its own embed element (it's out of our hands here, our projectile is done), where it will run through all the checks it needs to. + */ +/datum/element/embed/proc/checkEmbedProjectile(obj/item/projectile/P, atom/movable/firer, atom/hit, angle, hit_zone) + if(!iscarbon(hit) && !isclosedturf(hit)) + Detach(P) + return // we don't care + + var/obj/item/payload = new payload_type(get_turf(hit)) + var/did_embed + if(iscarbon(hit)) + var/mob/living/carbon/C = hit + var/obj/item/bodypart/limb + limb = C.get_bodypart(hit_zone) + if(!limb) + limb = C.get_bodypart() + did_embed = payload.tryEmbed(limb) + else + did_embed = payload.tryEmbed(hit) + + if(!did_embed) + payload.failedEmbed() + Detach(P) + +/** + * tryForceEmbed() is called here when we fire COMSIG_EMBED_TRY_FORCE from [/obj/item/proc/tryEmbed]. Mostly, this means we're a piece of shrapnel from a projectile that just impacted something, and we're trying to embed in it. + * + * The reason for this extra mucking about is avoiding having to do an extra hitby(), and annoying the target by impacting them once with the projectile, then again with the shrapnel (which likely represents said bullet), and possibly + * AGAIN if we actually embed. This way, we save on at least one message. Runs the standard embed checks on the mob/turf. + * + * Arguments: + * * I- what we're trying to embed, obviously + * * target- what we're trying to shish-kabob, either a bodypart, a carbon, or a closed turf + * * hit_zone- if our target is a carbon, try to hit them in this zone, if we don't have one, pick a random one. If our target is a bodypart, we already know where we're hitting. + * * forced- if we want this to succeed 100% + */ +/datum/element/embed/proc/tryForceEmbed(obj/item/I, atom/target, hit_zone, forced=FALSE) + var/obj/item/bodypart/limb + var/mob/living/carbon/C + var/turf/closed/T + + if(!forced && !prob(embed_chance)) + return + + if(iscarbon(target)) + C = target + if(!hit_zone) + limb = pick(C.bodyparts) + hit_zone = limb.body_zone + else if(isbodypart(target)) + limb = target + hit_zone = limb.body_zone + C = limb.owner + else if(isclosedturf(target)) + T = target + + if(C) + return checkEmbedMob(I, C, hit_zone, forced=TRUE) + else if(T) + return checkEmbedOther(I, T, forced=TRUE) diff --git a/code/datums/embedding_behavior.dm b/code/datums/embedding_behavior.dm deleted file mode 100644 index a261ea48c6..0000000000 --- a/code/datums/embedding_behavior.dm +++ /dev/null @@ -1,53 +0,0 @@ -#define EMBEDID "embed-[embed_chance]-[embedded_fall_chance]-[embedded_pain_chance]-[embedded_pain_multiplier]-[embedded_fall_pain_multiplier]-[embedded_impact_pain_multiplier]-[embedded_unsafe_removal_pain_multiplier]-[embedded_unsafe_removal_time]" - -/proc/getEmbeddingBehavior(embed_chance = EMBED_CHANCE, - embedded_fall_chance = EMBEDDED_ITEM_FALLOUT, - embedded_pain_chance = EMBEDDED_PAIN_CHANCE, - embedded_pain_multiplier = EMBEDDED_PAIN_MULTIPLIER, - embedded_fall_pain_multiplier = EMBEDDED_FALL_PAIN_MULTIPLIER, - embedded_impact_pain_multiplier = EMBEDDED_IMPACT_PAIN_MULTIPLIER, - embedded_unsafe_removal_pain_multiplier = EMBEDDED_UNSAFE_REMOVAL_PAIN_MULTIPLIER, - embedded_unsafe_removal_time = EMBEDDED_UNSAFE_REMOVAL_TIME) - . = locate(EMBEDID) - if (!.) - . = new /datum/embedding_behavior(embed_chance, embedded_fall_chance, embedded_pain_chance, embedded_pain_multiplier, embedded_fall_pain_multiplier, embedded_impact_pain_multiplier, embedded_unsafe_removal_pain_multiplier, embedded_unsafe_removal_time) - -/datum/embedding_behavior - var/embed_chance - var/embedded_fall_chance - var/embedded_pain_chance - var/embedded_pain_multiplier //The coefficient of multiplication for the damage this item does while embedded (this*w_class) - var/embedded_fall_pain_multiplier //The coefficient of multiplication for the damage this item does when falling out of a limb (this*w_class) - var/embedded_impact_pain_multiplier //The coefficient of multiplication for the damage this item does when first embedded (this*w_class) - var/embedded_unsafe_removal_pain_multiplier //The coefficient of multiplication for the damage removing this without surgery causes (this*w_class) - var/embedded_unsafe_removal_time //A time in ticks, multiplied by the w_class. - -/datum/embedding_behavior/New(embed_chance = EMBED_CHANCE, - embedded_fall_chance = EMBEDDED_ITEM_FALLOUT, - embedded_pain_chance = EMBEDDED_PAIN_CHANCE, - embedded_pain_multiplier = EMBEDDED_PAIN_MULTIPLIER, - embedded_fall_pain_multiplier = EMBEDDED_FALL_PAIN_MULTIPLIER, - embedded_impact_pain_multiplier = EMBEDDED_IMPACT_PAIN_MULTIPLIER, - embedded_unsafe_removal_pain_multiplier = EMBEDDED_UNSAFE_REMOVAL_PAIN_MULTIPLIER, - embedded_unsafe_removal_time = EMBEDDED_UNSAFE_REMOVAL_TIME) - src.embed_chance = embed_chance - src.embedded_fall_chance = embedded_fall_chance - src.embedded_pain_chance = embedded_pain_chance - src.embedded_pain_multiplier = embedded_pain_multiplier - src.embedded_fall_pain_multiplier = embedded_fall_pain_multiplier - src.embedded_impact_pain_multiplier = embedded_impact_pain_multiplier - src.embedded_unsafe_removal_pain_multiplier = embedded_unsafe_removal_pain_multiplier - src.embedded_unsafe_removal_time = embedded_unsafe_removal_time - tag = EMBEDID - -/datum/embedding_behavior/proc/setRating(embed_chance, embedded_fall_chance, embedded_pain_chance, embedded_pain_multiplier, embedded_fall_pain_multiplier, embedded_impact_pain_multiplier, embedded_unsafe_removal_pain_multiplier, embedded_unsafe_removal_time) - return getEmbeddingBehavior((isnull(embed_chance) ? src.embed_chance : embed_chance),\ - (isnull(embedded_fall_chance) ? src.embedded_fall_chance : embedded_fall_chance),\ - (isnull(embedded_pain_chance) ? src.embedded_pain_chance : embedded_pain_chance),\ - (isnull(embedded_pain_multiplier) ? src.embedded_pain_multiplier : embedded_pain_multiplier),\ - (isnull(embedded_fall_pain_multiplier) ? src.embedded_fall_pain_multiplier : embedded_fall_pain_multiplier),\ - (isnull(embedded_impact_pain_multiplier) ? src.embedded_impact_pain_multiplier : embedded_impact_pain_multiplier),\ - (isnull(embedded_unsafe_removal_pain_multiplier) ? src.embedded_unsafe_removal_pain_multiplier : embedded_unsafe_removal_pain_multiplier),\ - (isnull(embedded_unsafe_removal_time) ? src.embedded_unsafe_removal_time : embedded_unsafe_removal_time)) - -#undef EMBEDID diff --git a/code/datums/mutations/actions.dm b/code/datums/mutations/actions.dm index e933e02c15..1475cfa356 100644 --- a/code/datums/mutations/actions.dm +++ b/code/datums/mutations/actions.dm @@ -406,6 +406,8 @@ w_class = WEIGHT_CLASS_SMALL sharpness = IS_SHARP var/mob/living/carbon/human/fired_by + /// if we missed our target + var/missed = TRUE /obj/item/hardened_spike/Initialize(mapload, firedby) . = ..() @@ -413,13 +415,12 @@ addtimer(CALLBACK(src, .proc/checkembedded), 5 SECONDS) /obj/item/hardened_spike/proc/checkembedded() - if(ishuman(loc)) - var/mob/living/carbon/human/embedtest = loc - for(var/l in embedtest.bodyparts) - var/obj/item/bodypart/limb = l - if(src in limb.embedded_objects) - return limb - unembedded() + if(missed) + unembedded() + +/obj/item/hardened_spike/embedded(atom/target) + if(isbodypart(target)) + missed = FALSE /obj/item/hardened_spike/unembedded() var/turf/T = get_turf(src) @@ -490,11 +491,7 @@ var/obj/item/bodypart/L = spikey.checkembedded() - L.embedded_objects -= spikey //this is where it would deal damage, if it transfers chems it removes itself so no damage spikey.forceMove(get_turf(L)) transfered.visible_message("[spikey] falls out of [transfered]!") - if(!transfered.has_embedded_objects()) - transfered.clear_alert("embeddedobject") - SEND_SIGNAL(transfered, COMSIG_CLEAR_MOOD_EVENT, "embedded") - spikey.unembedded() + diff --git a/code/game/atoms.dm b/code/game/atoms.dm index a570a77125..edd657e6ce 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -8,6 +8,13 @@ var/interaction_flags_atom = NONE var/datum/reagents/reagents = null + var/flags_ricochet = NONE + + ///When a projectile tries to ricochet off this atom, the projectile ricochet chance is multiplied by this + var/ricochet_chance_mod = 1 + ///When a projectile ricochets off this atom, it deals the normal damage * this modifier to this atom + var/ricochet_damage_mod = 0.33 + //This atom's HUD (med/sec, etc) images. Associative list. var/list/image/hud_list = null //HUD images that this atom can provide. @@ -148,11 +155,20 @@ /atom/proc/check_projectile_ricochet(obj/item/projectile/P) return (flags_1 & DEFAULT_RICOCHET_1)? PROJECTILE_RICOCHET_YES : PROJECTILE_RICOCHET_NO -/** - * Handle a projectile ricochet. Return TRUE if we did something to the projectile like reflecting it/whatnot. - */ -/atom/proc/handle_projectile_ricochet(obj/item/projectile/P) - return FALSE +/atom/proc/handle_ricochet(obj/item/projectile/P) + var/turf/p_turf = get_turf(P) + var/face_direction = get_dir(src, p_turf) + var/face_angle = dir2angle(face_direction) + var/incidence_s = GET_ANGLE_OF_INCIDENCE(face_angle, (P.Angle + 180)) + var/a_incidence_s = abs(incidence_s) + if(a_incidence_s > 90 && a_incidence_s < 270) + return FALSE + if((P.flag in list("bullet", "bomb")) && P.ricochet_incidence_leeway) + if((a_incidence_s < 90 && a_incidence_s < 90 - P.ricochet_incidence_leeway) || (a_incidence_s > 270 && a_incidence_s -270 > P.ricochet_incidence_leeway)) + return + var/new_angle_s = SIMPLIFY_DEGREES(face_angle + incidence_s) + P.setAngle(new_angle_s) + return TRUE /atom/proc/CanPass(atom/movable/mover, turf/target) return !density diff --git a/code/game/gamemodes/clown_ops/clown_weapons.dm b/code/game/gamemodes/clown_ops/clown_weapons.dm index 4d3ea2d02e..afa3dbfeaa 100644 --- a/code/game/gamemodes/clown_ops/clown_weapons.dm +++ b/code/game/gamemodes/clown_ops/clown_weapons.dm @@ -202,7 +202,7 @@ icon_state = "moustacheg" clumsy_check = GRENADE_NONCLUMSY_FUMBLE -/obj/item/grenade/chem_grenade/teargas/moustache/prime() +/obj/item/grenade/chem_grenade/teargas/moustache/prime(mob/living/lanced_by) var/list/check_later = list() for(var/mob/living/carbon/C in get_turf(src)) check_later += C diff --git a/code/game/gamemodes/gangs/gang_items.dm b/code/game/gamemodes/gangs/gang_items.dm index d3ccb5a355..7d6ecd00f6 100644 --- a/code/game/gamemodes/gangs/gang_items.dm +++ b/code/game/gamemodes/gangs/gang_items.dm @@ -288,7 +288,7 @@ datum/gang_item/clothing/shades //Addition: Why not have cool shades on a gang m name = "Fragmentation Grenade" id = "frag nade" cost = 5 - item_path = /obj/item/grenade/syndieminibomb/concussion/frag + item_path = /obj/item/grenade/frag /datum/gang_item/equipment/implant_breaker name = "Implant Breaker" diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm index e2f26d6617..096b40ddb7 100644 --- a/code/game/machinery/_machinery.dm +++ b/code/game/machinery/_machinery.dm @@ -92,6 +92,8 @@ Class Procs: pressure_resistance = 15 max_integrity = 200 layer = BELOW_OBJ_LAYER //keeps shit coming out of the machine from ending up underneath it. + flags_ricochet = RICOCHET_HARD + ricochet_chance_mod = 0.3 anchored = TRUE interaction_flags_atom = INTERACT_ATOM_ATTACK_HAND | INTERACT_ATOM_UI_INTERACT diff --git a/code/game/machinery/deployable.dm b/code/game/machinery/deployable.dm index a2d3cccc35..252e6cf6b3 100644 --- a/code/game/machinery/deployable.dm +++ b/code/game/machinery/deployable.dm @@ -169,7 +169,8 @@ to_chat(user, "[src] is now in [mode] mode.") -/obj/item/grenade/barrier/prime() +/obj/item/grenade/barrier/prime(mob/living/lanced_by) + . = ..() new /obj/structure/barricade/security(get_turf(src.loc)) switch(mode) if(VERTICAL) diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm index 98a888fa66..1fb50e13c6 100644 --- a/code/game/machinery/doors/door.dm +++ b/code/game/machinery/doors/door.dm @@ -12,6 +12,7 @@ armor = list("melee" = 30, "bullet" = 30, "laser" = 20, "energy" = 20, "bomb" = 10, "bio" = 100, "rad" = 100, "fire" = 80, "acid" = 70) CanAtmosPass = ATMOS_PASS_DENSITY flags_1 = PREVENT_CLICK_UNDER_1 + ricochet_chance_mod = 0.8 interaction_flags_atom = INTERACT_ATOM_UI_INTERACT diff --git a/code/game/objects/effects/mines.dm b/code/game/objects/effects/mines.dm index 1fe5e46e25..40104d5ea8 100644 --- a/code/game/objects/effects/mines.dm +++ b/code/game/objects/effects/mines.dm @@ -5,19 +5,21 @@ anchored = TRUE icon = 'icons/obj/items_and_weapons.dmi' icon_state = "uglymine" - var/triggered = 0 + /// We manually check to see if we've been triggered in case multiple atoms cross us in the time between the mine being triggered and it actually deleting, to avoid a race condition with multiple detonations + var/triggered = FALSE /obj/effect/mine/proc/mineEffect(mob/victim) to_chat(victim, "*click*") -/obj/effect/mine/Crossed(AM as mob|obj) - if(isturf(loc)) - if(ismob(AM)) - var/mob/MM = AM - if(!(MM.movement_type & FLYING)) - triggermine(AM) - else - triggermine(AM) +/obj/effect/mine/Crossed(atom/movable/AM) + if(triggered || !isturf(loc)) + return + . = ..() + + if(AM.movement_type & FLYING) + return + + triggermine(AM) /obj/effect/mine/proc/triggermine(mob/victim) if(triggered) @@ -27,9 +29,13 @@ s.set_up(3, 1, src) s.start() mineEffect(victim) + SEND_SIGNAL(src, COMSIG_MINE_TRIGGERED) triggered = 1 qdel(src) +/obj/effect/mine/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir) + . = ..() + triggermine() /obj/effect/mine/explosive name = "explosive mine" @@ -50,6 +56,18 @@ if(isliving(victim)) victim.DefaultCombatKnockdown(stun_time) +/obj/effect/mine/shrapnel + name = "shrapnel mine" + var/shrapnel_type = /obj/item/projectile/bullet/shrapnel + var/shrapnel_magnitude = 3 + +/obj/effect/mine/shrapnel/mineEffect(mob/victim) + AddComponent(/datum/component/pellet_cloud, projectile_type=shrapnel_type, magnitude=shrapnel_magnitude) + +/obj/effect/mine/shrapnel/sting + name = "stinger mine" + shrapnel_type = /obj/item/projectile/bullet/pellet/stingball + /obj/effect/mine/kickmine name = "kick mine" @@ -105,7 +123,7 @@ /obj/effect/mine/pickup/triggermine(mob/victim) if(triggered) return - triggered = 1 + triggered = TRUE invisibility = INVISIBILITY_ABSTRACT mineEffect(victim) qdel(src) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index b7e671b9e2..5aa47cfb76 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -4,6 +4,9 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) // if true, everyone item when created will have its name changed to be // more... RPG-like. +GLOBAL_VAR_INIT(stickpocalypse, FALSE) // if true, all non-embeddable items will be able to harmlessly stick to people when thrown +GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to embed in people, takes precedence over stickpocalypse + /obj/item name = "item" icon = 'icons/obj/items_and_weapons.dmi' @@ -104,7 +107,7 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) mouse_drag_pointer = MOUSE_ACTIVE_POINTER //the icon to indicate this object is being dragged - var/datum/embedding_behavior/embedding + var/list/embedding = NONE var/flags_cover = 0 //for flags such as GLASSESCOVERSEYES var/heat = 0 @@ -159,7 +162,7 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) /obj/item/Initialize() - if (attack_verb) + if(attack_verb) attack_verb = typelist("attack_verb", attack_verb) . = ..() @@ -167,9 +170,6 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) new path(src) actions_types = null - if(GLOB.rpg_loot_items) - AddComponent(/datum/component/fantasy) - if(force_string) item_flags |= FORCE_STRING_OVERRIDE @@ -179,16 +179,6 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) if(damtype == "brute") hitsound = "swing_hit" - if (!embedding) - embedding = getEmbeddingBehavior() - else if (islist(embedding)) - embedding = getEmbeddingBehavior(arglist(embedding)) - else if (!istype(embedding, /datum/embedding_behavior)) - stack_trace("Invalid type [embedding.type] found in .embedding during /obj/item Initialize()") - - if(sharpness) //give sharp objects butchering functionality, for consistency - AddComponent(/datum/component/butchering, 80 * toolspeed) - /obj/item/Destroy() item_flags &= ~DROPDEL //prevent reqdels if(ismob(loc)) @@ -198,6 +188,26 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) qdel(X) return ..() +/obj/item/ComponentInitialize() + . = ..() + + // this proc says it's for initializing components, but we're initializing elements too because it's you and me against the world >:) + if(!LAZYLEN(embedding)) + if(GLOB.embedpocalypse) + embedding = EMBED_POINTY + name = "pointy [name]" + else if(GLOB.stickpocalypse) + embedding = EMBED_HARMLESS + name = "sticky [name]" + + updateEmbedding() + + if(GLOB.rpg_loot_items) + AddComponent(/datum/component/fantasy) + + if(sharpness) //give sharp objects butchering functionality, for consistency + AddComponent(/datum/component/butchering, 80 * toolspeed) + /obj/item/proc/check_allowed_items(atom/target, not_inside, target_self) if(((src in target) && !target_self) || (!isturf(target.loc) && !isturf(target) && not_inside)) return 0 @@ -918,11 +928,13 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) // if w_volume is 0 you fucked up anyways lol return w_volume || AUTO_SCALE_VOLUME(w_class) -/obj/item/proc/embedded(mob/living/carbon/human/embedded_mob) +/obj/item/proc/embedded(atom/embedded_target) return /obj/item/proc/unembedded() - return + if(item_flags & DROPDEL) + QDEL_NULL(src) + return TRUE /** * Sets our slowdown and updates equipment slowdown of any mob we're equipped on. @@ -938,3 +950,135 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) . = ..() if(var_name == NAMEOF(src, slowdown)) set_slowdown(var_value) //don't care if it's a duplicate edit as slowdown'll be set, do it anyways to force normal behavior. +/** + * Does the current embedding var meet the criteria for being harmless? Namely, does it explicitly define the pain multiplier and jostle pain mult to be 0? If so, return true. + * + */ +/obj/item/proc/isEmbedHarmless() + if(embedding) + return !isnull(embedding["pain_mult"]) && !isnull(embedding["jostle_pain_mult"]) && embedding["pain_mult"] == 0 && embedding["jostle_pain_mult"] == 0 + +///In case we want to do something special (like self delete) upon failing to embed in something, return true +/obj/item/proc/failedEmbed() + if(item_flags & DROPDEL) + QDEL_NULL(src) + return TRUE + +/** + + + + * tryEmbed() is for when you want to try embedding something without dealing with the damage + hit messages of calling hitby() on the item while targetting the target. + + + + * + + + + * Really, this is used mostly with projectiles with shrapnel payloads, from [/datum/element/embed/proc/checkEmbedProjectile], and called on said shrapnel. Mostly acts as an intermediate between different embed elements. + + + + * + + + + * Arguments: + + + + * * target- Either a body part, a carbon, or a closed turf. What are we hitting? + + + + * * forced- Do we want this to go through 100%? + + + + */ + + + +/obj/item/proc/tryEmbed(atom/target, forced=FALSE, silent=FALSE) + + + + if(!isbodypart(target) && !iscarbon(target) && !isclosedturf(target)) + + + + return + + + + if(!forced && !LAZYLEN(embedding)) + + + + return + + + + + + + + if(SEND_SIGNAL(src, COMSIG_EMBED_TRY_FORCE, target, forced, silent)) + + + + return TRUE + + + + failedEmbed() + + + + + + + +///For when you want to disable an item's embedding capabilities (like transforming weapons and such), this proc will detach any active embed elements from it. + + + +/obj/item/proc/disableEmbedding() + + + + SEND_SIGNAL(src, COMSIG_ITEM_DISABLE_EMBED) + + + + return + + + + + + + +///For when you want to add/update the embedding on an item. Uses the vars in [/obj/item/embedding], and defaults to config values for values that aren't set. Will automatically detach previous embed elements on this item. + + + +/obj/item/proc/updateEmbedding() + if(!islist(embedding) || !LAZYLEN(embedding)) + return + + AddElement(/datum/element/embed,\ + embed_chance = (!isnull(embedding["embed_chance"]) ? embedding["embed_chance"] : EMBED_CHANCE),\ + fall_chance = (!isnull(embedding["fall_chance"]) ? embedding["fall_chance"] : EMBEDDED_ITEM_FALLOUT),\ + pain_chance = (!isnull(embedding["pain_chance"]) ? embedding["pain_chance"] : EMBEDDED_PAIN_CHANCE),\ + pain_mult = (!isnull(embedding["pain_mult"]) ? embedding["pain_mult"] : EMBEDDED_PAIN_MULTIPLIER),\ + remove_pain_mult = (!isnull(embedding["remove_pain_mult"]) ? embedding["remove_pain_mult"] : EMBEDDED_UNSAFE_REMOVAL_PAIN_MULTIPLIER),\ + rip_time = (!isnull(embedding["rip_time"]) ? embedding["rip_time"] : EMBEDDED_UNSAFE_REMOVAL_TIME),\ + ignore_throwspeed_threshold = (!isnull(embedding["ignore_throwspeed_threshold"]) ? embedding["ignore_throwspeed_threshold"] : FALSE),\ + impact_pain_mult = (!isnull(embedding["impact_pain_mult"]) ? embedding["impact_pain_mult"] : EMBEDDED_IMPACT_PAIN_MULTIPLIER),\ + jostle_chance = (!isnull(embedding["jostle_chance"]) ? embedding["jostle_chance"] : EMBEDDED_JOSTLE_CHANCE),\ + jostle_pain_mult = (!isnull(embedding["jostle_pain_mult"]) ? embedding["jostle_pain_mult"] : EMBEDDED_JOSTLE_PAIN_MULTIPLIER),\ + pain_stam_pct = (!isnull(embedding["pain_stam_pct"]) ? embedding["pain_stam_pct"] : EMBEDDED_PAIN_STAM_PCT),\ + embed_chance_turf_mod = (!isnull(embedding["embed_chance_turf_mod"]) ? embedding["embed_chance_turf_mod"] : EMBED_CHANCE_TURF_MOD)) + return TRUE \ No newline at end of file diff --git a/code/game/objects/items/grenades/antigravity.dm b/code/game/objects/items/grenades/antigravity.dm index a4bc207be0..b6700599a3 100644 --- a/code/game/objects/items/grenades/antigravity.dm +++ b/code/game/objects/items/grenades/antigravity.dm @@ -7,7 +7,8 @@ var/forced_value = 0 var/duration = 300 -/obj/item/grenade/antigravity/prime() +/obj/item/grenade/antigravity/prime(mob/living/lanced_by) + . = ..() update_mob() for(var/turf/T in view(range,src)) diff --git a/code/game/objects/items/grenades/chem_grenade.dm b/code/game/objects/items/grenades/chem_grenade.dm index 91dde2899d..4ce0e811c3 100644 --- a/code/game/objects/items/grenades/chem_grenade.dm +++ b/code/game/objects/items/grenades/chem_grenade.dm @@ -174,10 +174,11 @@ message_admins(message) user.log_message("primed [src] ([reagent_string])",LOG_GAME) -/obj/item/grenade/chem_grenade/prime() +/obj/item/grenade/chem_grenade/prime(mob/living/lanced_by) if(stage != READY) return FALSE + . = ..() var/list/datum/reagents/reactants = list() for(var/obj/item/reagent_containers/glass/G in beakers) reactants += G.reagents @@ -217,7 +218,7 @@ ignition_temp = 25 // Large grenades are slightly more effective at setting off heat-sensitive mixtures than smaller grenades. threatscale = 1.1 // 10% more effective. -/obj/item/grenade/chem_grenade/large/prime() +/obj/item/grenade/chem_grenade/large/prime(mob/living/lanced_by) if(stage != READY) return FALSE @@ -286,7 +287,7 @@ return ..() -/obj/item/grenade/chem_grenade/adv_release/prime() +/obj/item/grenade/chem_grenade/adv_release/prime(mob/living/lanced_by) if(stage != READY) return FALSE diff --git a/code/game/objects/items/grenades/clusterbuster.dm b/code/game/objects/items/grenades/clusterbuster.dm index c16b4a30a1..9980ff34ce 100644 --- a/code/game/objects/items/grenades/clusterbuster.dm +++ b/code/game/objects/items/grenades/clusterbuster.dm @@ -14,7 +14,8 @@ var/max_spawned = 8 var/segment_chance = 35 -/obj/item/grenade/clusterbuster/prime() +/obj/item/grenade/clusterbuster/prime(mob/living/lanced_by) + . = ..() update_mob() var/numspawned = rand(min_spawned,max_spawned) var/again = 0 @@ -59,7 +60,7 @@ step_away(src,loc) addtimer(CALLBACK(src, .proc/prime), rand(15,60)) -/obj/item/grenade/clusterbuster/segment/prime() +/obj/item/grenade/clusterbuster/segment/prime(mob/living/lanced_by) new payload_spawner(drop_location(), payload, rand(min_spawned,max_spawned)) playsound(src, prime_sound, 75, 1, -3) qdel(src) diff --git a/code/game/objects/items/grenades/emgrenade.dm b/code/game/objects/items/grenades/emgrenade.dm index 99cf4bdf99..815a3b343c 100644 --- a/code/game/objects/items/grenades/emgrenade.dm +++ b/code/game/objects/items/grenades/emgrenade.dm @@ -4,7 +4,8 @@ icon_state = "emp" item_state = "emp" -/obj/item/grenade/empgrenade/prime() +/obj/item/grenade/empgrenade/prime(mob/living/lanced_by) + . = ..() update_mob() empulse(src, 4, 10) qdel(src) diff --git a/code/game/objects/items/grenades/flashbang.dm b/code/game/objects/items/grenades/flashbang.dm index 6f79df28fe..f51db9fa4c 100644 --- a/code/game/objects/items/grenades/flashbang.dm +++ b/code/game/objects/items/grenades/flashbang.dm @@ -6,7 +6,8 @@ righthand_file = 'icons/mob/inhands/equipment/security_righthand.dmi' var/flashbang_range = 7 //how many tiles away the mob will be stunned. -/obj/item/grenade/flashbang/prime() +/obj/item/grenade/flashbang/prime(mob/living/lanced_by) + . = ..() update_mob() var/flashbang_turf = get_turf(src) if(!flashbang_turf) @@ -42,3 +43,92 @@ var/distance = get_dist(get_turf(M), source) if(M.flash_act(affect_silicon = 1)) M.DefaultCombatKnockdown(max(200/max(1,distance), 60)) + +/obj/item/grenade/stingbang + name = "stingbang" + icon_state = "timeg" + item_state = "flashbang" + lefthand_file = 'icons/mob/inhands/equipment/security_lefthand.dmi' + righthand_file = 'icons/mob/inhands/equipment/security_righthand.dmi' + var/flashbang_range = 1 //how many tiles away the mob will be stunned. + shrapnel_type = /obj/item/projectile/bullet/pellet/stingball + shrapnel_radius = 5 + custom_premium_price = 700 // mostly gotten through cargo, but throw in one for the sec vendor ;) + +/obj/item/grenade/stingbang/mega + name = "mega stingbang" + shrapnel_type = /obj/item/projectile/bullet/pellet/stingball/mega + shrapnel_radius = 12 + +/obj/item/grenade/stingbang/prime(mob/living/lanced_by) + if(iscarbon(loc)) + var/mob/living/carbon/C = loc + var/obj/item/bodypart/B = C.get_holding_bodypart_of_item(src) + if(B) + C.visible_message("[src] goes off in [C]'s hand, blowing [C.p_their()] [B.name] to bloody shreds!", "[src] goes off in your hand, blowing your [B.name] to bloody shreds!") + B.dismember() + + . = ..() + update_mob() + var/flashbang_turf = get_turf(src) + if(!flashbang_turf) + return + do_sparks(rand(5, 9), FALSE, src) + playsound(flashbang_turf, 'sound/weapons/flashbang.ogg', 50, TRUE, 8, 0.9) + new /obj/effect/dummy/lighting_obj (flashbang_turf, LIGHT_COLOR_WHITE, (flashbang_range + 2), 2, 1) + for(var/mob/living/M in get_hearers_in_view(flashbang_range, flashbang_turf)) + pop(get_turf(M), M) + qdel(src) + +/obj/item/grenade/stingbang/proc/pop(turf/T , mob/living/M) + if(M.stat == DEAD) //They're dead! + return + M.show_message("POP", MSG_AUDIBLE) + var/distance = max(0,get_dist(get_turf(src),T)) + +//Flash + if(M.flash_act(affect_silicon = 1)) + M.Paralyze(max(10/max(1,distance), 5)) + M.Knockdown(max(100/max(1,distance), 60)) + +//Bang + if(!distance || loc == M || loc == M.loc) + M.Paralyze(20) + M.Knockdown(200) + M.soundbang_act(1, 200, 10, 15) + if(M.apply_damages(10, 10)) + to_chat(M, "The blast from \the [src] bruises and burns you!") + + // only checking if they're on top of the tile, cause being one tile over will be its own punishment + +// Grenade that releases more shrapnel the more times you use it in hand between priming and detonation (sorta like the 9bang from MW3), for admin goofs +/obj/item/grenade/primer + name = "rotfrag grenade" + desc = "A grenade that generates more shrapnel the more you rotate it in your hand after pulling the pin. This one releases shrapnel shards." + icon_state = "timeg" + item_state = "flashbang" + lefthand_file = 'icons/mob/inhands/equipment/security_lefthand.dmi' + righthand_file = 'icons/mob/inhands/equipment/security_righthand.dmi' + var/rots_per_mag = 3 /// how many times we need to "rotate" the charge in hand per extra tile of magnitude + shrapnel_type = /obj/item/projectile/bullet/shrapnel + var/rots = 1 /// how many times we've "rotated" the charge + +/obj/item/grenade/primer/attack_self(mob/user) + . = ..() + if(active) + user.playsound_local(user, 'sound/misc/box_deploy.ogg', 50, TRUE) + rots++ + user.changeNext_move(CLICK_CD_RAPID) + +/obj/item/grenade/primer/prime(mob/living/lanced_by) + shrapnel_radius = round(rots / rots_per_mag) + . = ..() + qdel(src) + +/obj/item/grenade/primer/stingbang + name = "rotsting" + desc = "A grenade that generates more shrapnel the more you rotate it in your hand after pulling the pin. This one releases stingballs." + lefthand_file = 'icons/mob/inhands/equipment/security_lefthand.dmi' + righthand_file = 'icons/mob/inhands/equipment/security_righthand.dmi' + rots_per_mag = 2 + shrapnel_type = /obj/item/projectile/bullet/pellet/stingball diff --git a/code/game/objects/items/grenades/ghettobomb.dm b/code/game/objects/items/grenades/ghettobomb.dm index 1725d84dcc..3553d79faf 100644 --- a/code/game/objects/items/grenades/ghettobomb.dm +++ b/code/game/objects/items/grenades/ghettobomb.dm @@ -45,12 +45,13 @@ /obj/item/grenade/iedcasing/attack_self(mob/user) // if(!active) - if(clown_check(user)) + if(!botch_check(user)) to_chat(user, "You light the [name]!") cut_overlay("improvised_grenade_filled") preprime(user, null, FALSE) -/obj/item/grenade/iedcasing/prime() //Blowing that can up +/obj/item/grenade/iedcasing/prime(mob/living/lanced_by) //Blowing that can up + . = ..() update_mob() explosion(src.loc,-1,-1,2, flame_range = 4) // small explosion, plus a very large fireball. qdel(src) diff --git a/code/game/objects/items/grenades/grenade.dm b/code/game/objects/items/grenades/grenade.dm index 8a41ee4601..7a6157bf65 100644 --- a/code/game/objects/items/grenades/grenade.dm +++ b/code/game/objects/items/grenades/grenade.dm @@ -17,10 +17,31 @@ var/det_time = 50 var/display_timer = 1 var/clumsy_check = GRENADE_CLUMSY_FUMBLE + var/sticky = FALSE + // I moved the explosion vars and behavior to base grenades because we want all grenades to call [/obj/item/grenade/proc/prime] so we can send COMSIG_GRENADE_PRIME + ///how big of a devastation explosion radius on prime + var/ex_dev = 0 + ///how big of a heavy explosion radius on prime + var/ex_heavy = 0 + ///how big of a light explosion radius on prime + var/ex_light = 0 + ///how big of a flame explosion radius on prime + var/ex_flame = 0 + + // dealing with creating a [/datum/component/pellet_cloud] on prime + /// if set, will spew out projectiles of this type + var/shrapnel_type + /// the higher this number, the more projectiles are created as shrapnel + var/shrapnel_radius + var/shrapnel_initialized /obj/item/grenade/suicide_act(mob/living/carbon/user) user.visible_message("[user] primes [src], then eats it! It looks like [user.p_theyre()] trying to commit suicide!") + if(shrapnel_type && shrapnel_radius) + shrapnel_initialized = TRUE + AddComponent(/datum/component/pellet_cloud, projectile_type=shrapnel_type, magnitude=shrapnel_radius) playsound(src, 'sound/items/eatfood.ogg', 50, 1) + SEND_SIGNAL(src, COMSIG_GRENADE_ARMED, det_time) preprime(user, det_time) user.transferItemToLoc(src, user, TRUE)//>eat a grenade set to 5 seconds >rush captain sleep(det_time)//so you dont die instantly @@ -32,19 +53,21 @@ if(!QDELETED(src)) qdel(src) -/obj/item/grenade/proc/clown_check(mob/living/carbon/human/user) +/obj/item/grenade/proc/botch_check(mob/living/carbon/human/user) var/clumsy = HAS_TRAIT(user, TRAIT_CLUMSY) if(clumsy) if(clumsy_check == GRENADE_CLUMSY_FUMBLE && prob(50)) to_chat(user, "Huh? How does this thing work?") preprime(user, 5, FALSE) - return FALSE + return TRUE else if(clumsy_check == GRENADE_NONCLUMSY_FUMBLE && !(user.mind && HAS_TRAIT(user.mind, TRAIT_CLOWN_MENTALITY))) to_chat(user, "You pull the pin on [src]. Attached to it is a pink ribbon that says, \"HONK\"") preprime(user, 5, FALSE) - return FALSE - return TRUE + return TRUE + else if(sticky && prob(50)) // to add risk to sticky tape grenade cheese, no return cause we still prime as normal after + to_chat(user, "What the... [src] is stuck to your hand!") + ADD_TRAIT(src, TRAIT_NODROP, STICKY_NODROP) /obj/item/grenade/examine(mob/user) . = ..() @@ -56,8 +79,16 @@ /obj/item/grenade/attack_self(mob/user) + if(HAS_TRAIT(src, TRAIT_NODROP)) + to_chat(user, "You try prying [src] off your hand...") + if(do_after(user, 70, target=src)) + to_chat(user, "You manage to remove [src] from your hand.") + REMOVE_TRAIT(src, TRAIT_NODROP, STICKY_NODROP) + + return + if(!active) - if(clown_check(user)) + if(!botch_check(user)) // if they botch the prime, it'll be handled in botch_check preprime(user) /obj/item/grenade/proc/log_grenade(mob/user, turf/T) @@ -81,10 +112,18 @@ icon_state = initial(icon_state) + "_active" addtimer(CALLBACK(src, .proc/prime), isnull(delayoverride)? det_time : delayoverride) -/obj/item/grenade/proc/prime() +/obj/item/grenade/proc/prime(mob/living/lanced_by) var/turf/T = get_turf(src) log_game("Grenade detonation at [AREACOORD(T)], location [loc]") + if(shrapnel_type && shrapnel_radius && !shrapnel_initialized) // add a second check for adding the component in case whatever triggered the grenade went straight to prime (badminnery for example) + shrapnel_initialized = TRUE + AddComponent(/datum/component/pellet_cloud, projectile_type=shrapnel_type, magnitude=shrapnel_radius) + + SEND_SIGNAL(src, COMSIG_GRENADE_PRIME, lanced_by) + if(ex_dev || ex_heavy || ex_light || ex_flame) + explosion(loc, ex_dev, ex_heavy, ex_light, flame_range = ex_flame) + /obj/item/grenade/proc/update_mob() if(ismob(loc)) var/mob/M = loc diff --git a/code/game/objects/items/grenades/plastic.dm b/code/game/objects/items/grenades/plastic.dm index fe2a27f1a8..a2b0e3edd4 100644 --- a/code/game/objects/items/grenades/plastic.dm +++ b/code/game/objects/items/grenades/plastic.dm @@ -122,7 +122,9 @@ var/obj/item/I = AM I.throw_speed = max(1, (I.throw_speed - 3)) I.throw_range = max(1, (I.throw_range - 3)) - I.embedding = I.embedding.setRating(embed_chance = 0) + if(I.embedding) + I.embedding["embed_chance"] = 0 + I.updateEmbedding() target.add_overlay(plastic_overlay, TRUE) if(!nadeassembly) @@ -205,9 +207,10 @@ else return ..() -/obj/item/grenade/plastic/c4/prime() +/obj/item/grenade/plastic/c4/prime(mob/living/lanced_by) if(QDELETED(src)) return + . = ..() var/turf/location if(target) if(!QDELETED(target)) diff --git a/code/game/objects/items/grenades/smokebomb.dm b/code/game/objects/items/grenades/smokebomb.dm index f26390faf6..1b856fc013 100644 --- a/code/game/objects/items/grenades/smokebomb.dm +++ b/code/game/objects/items/grenades/smokebomb.dm @@ -17,7 +17,8 @@ qdel(smoke) return ..() -/obj/item/grenade/smokebomb/prime() +/obj/item/grenade/smokebomb/prime(mob/living/lanced_by) + . = ..() update_mob() playsound(src.loc, 'sound/effects/smoke.ogg', 50, 1, -3) smoke.set_up(4, src) diff --git a/code/game/objects/items/grenades/spawnergrenade.dm b/code/game/objects/items/grenades/spawnergrenade.dm index 2cc72f3694..a8b0fa3cd3 100644 --- a/code/game/objects/items/grenades/spawnergrenade.dm +++ b/code/game/objects/items/grenades/spawnergrenade.dm @@ -7,7 +7,8 @@ var/spawner_type = null // must be an object path var/deliveryamt = 1 // amount of type to deliver -/obj/item/grenade/spawnergrenade/prime() // Prime now just handles the two loops that query for people in lockers and people who can see it. +/obj/item/grenade/spawnergrenade/prime(mob/living/lanced_by) // Prime now just handles the two loops that query for people in lockers and people who can see it. + . = ..() update_mob() if(spawner_type && deliveryamt) // Make a quick flash diff --git a/code/game/objects/items/grenades/syndieminibomb.dm b/code/game/objects/items/grenades/syndieminibomb.dm index 35e9abd982..9f12edeb17 100644 --- a/code/game/objects/items/grenades/syndieminibomb.dm +++ b/code/game/objects/items/grenades/syndieminibomb.dm @@ -4,27 +4,45 @@ icon = 'icons/obj/grenade.dmi' icon_state = "syndicate" item_state = "flashbang" + ex_dev = 1 + ex_heavy = 2 + ex_light = 4 + ex_flame = 2 -/obj/item/grenade/syndieminibomb/prime() +/obj/item/grenade/syndieminibomb/prime(mob/living/lanced_by) + . = ..() update_mob() - explosion(src.loc,1,2,4,flame_range = 2) qdel(src) /obj/item/grenade/syndieminibomb/concussion name = "HE Grenade" desc = "A compact shrapnel grenade meant to devastate nearby organisms and cause some damage in the process. Pull pin and throw opposite direction." icon_state = "concussion" + ex_heavy = 2 + ex_light = 3 + ex_flame = 3 -/obj/item/grenade/syndieminibomb/concussion/prime() - update_mob() - explosion(src.loc,0,2,3,flame_range = 3) - qdel(src) - -/obj/item/grenade/syndieminibomb/concussion/frag +/obj/item/grenade/frag name = "frag grenade" - desc = "Fire in the hole." + desc = "An anti-personnel fragmentation grenade, this weapon excels at killing soft targets by shredding them with metal shrapnel." icon_state = "frag" + shrapnel_type = /obj/item/projectile/bullet/shrapnel + shrapnel_radius = 4 + ex_heavy = 1 + ex_light = 3 + ex_flame = 4 + +/obj/item/grenade/frag/mega + name = "FRAG grenade" + desc = "An anti-everything fragmentation grenade, this weapon excels at killing anything any everything by shredding them with metal shrapnel." + shrapnel_type = /obj/item/projectile/bullet/shrapnel/mega + shrapnel_radius = 12 + +/obj/item/grenade/frag/prime(mob/living/lanced_by) + . = ..() + update_mob() + qdel(src) /obj/item/grenade/gluon desc = "An advanced grenade that releases a harmful stream of gluons inducing radiation in those nearby. These gluon streams will also make victims feel exhausted, and induce shivering. This extreme coldness will also likely wet any nearby floors." @@ -36,7 +54,8 @@ var/rad_damage = 350 var/stamina_damage = 30 -/obj/item/grenade/gluon/prime() +/obj/item/grenade/gluon/prime(mob/living/lanced_by) + . = ..() update_mob() playsound(loc, 'sound/effects/empulse.ogg', 50, 1) radiation_pulse(src, rad_damage) diff --git a/code/game/objects/items/kitchen.dm b/code/game/objects/items/kitchen.dm index e44212ba3f..dda41494ff 100644 --- a/code/game/objects/items/kitchen.dm +++ b/code/game/objects/items/kitchen.dm @@ -138,6 +138,7 @@ icon_state = "buckknife" item_state = "knife" desc = "A military combat utility survival knife." + embedding = list("pain_mult" = 4, "embed_chance" = 65, "fall_chance" = 10, "ignore_throwspeed_threshold" = TRUE) force = 20 throwforce = 20 attack_verb = list("slashed", "stabbed", "sliced", "torn", "ripped", "cut") @@ -148,6 +149,7 @@ icon_state = "survivalknife" item_state = "knife" desc = "A hunting grade survival knife." + embedding = list("pain_mult" = 4, "embed_chance" = 35, "fall_chance" = 10) force = 15 throwforce = 15 bayonet = TRUE @@ -159,6 +161,7 @@ lefthand_file = 'icons/mob/inhands/weapons/swords_lefthand.dmi' righthand_file = 'icons/mob/inhands/weapons/swords_righthand.dmi' desc = "A sharpened bone. The bare minimum in survival." + embedding = list("pain_mult" = 4, "embed_chance" = 35, "fall_chance" = 10) force = 15 throwforce = 15 custom_materials = null diff --git a/code/game/objects/items/melee/energy.dm b/code/game/objects/items/melee/energy.dm index 63279d2ddc..aec3c333b7 100644 --- a/code/game/objects/items/melee/energy.dm +++ b/code/game/objects/items/melee/energy.dm @@ -103,7 +103,7 @@ throw_speed = 3 throw_range = 5 sharpness = IS_SHARP - embedding = list("embed_chance" = 75, "embedded_impact_pain_multiplier" = 10) + embedding = list("embed_chance" = 75, "impact_pain_mult" = 10) armour_penetration = 35 item_flags = NEEDS_PERMIT | ITEM_CAN_PARRY block_parry_data = /datum/block_parry_data/energy_sword diff --git a/code/game/objects/items/melee/transforming.dm b/code/game/objects/items/melee/transforming.dm index 5bccf91868..386a6e9acc 100644 --- a/code/game/objects/items/melee/transforming.dm +++ b/code/game/objects/items/melee/transforming.dm @@ -53,6 +53,8 @@ throw_speed = 4 if(attack_verb_on.len) attack_verb = attack_verb_on + if(embedding) + updateEmbedding() icon_state = icon_state_on w_class = w_class_on else @@ -62,6 +64,8 @@ throw_speed = initial(throw_speed) if(attack_verb_off.len) attack_verb = attack_verb_off + if(embedding) + updateEmbedding() icon_state = initial(icon_state) w_class = initial(w_class) total_mass = initial(total_mass) diff --git a/code/game/objects/items/shrapnel.dm b/code/game/objects/items/shrapnel.dm new file mode 100644 index 0000000000..7108080ecc --- /dev/null +++ b/code/game/objects/items/shrapnel.dm @@ -0,0 +1,64 @@ +/obj/item/shrapnel // frag grenades + name = "shrapnel shard" + embedding = list(embed_chance=70, ignore_throwspeed_threshold=TRUE, fall_chance=4, embed_chance_turf_mod=-100) + custom_materials = list(/datum/material/iron=50) + armour_penetration = -20 + icon = 'icons/obj/shards.dmi' + icon_state = "large" + w_class = WEIGHT_CLASS_TINY + item_flags = DROPDEL + +/obj/item/shrapnel/stingball // stingbang grenades + name = "stingball" + embedding = list(embed_chance=90, fall_chance=3, jostle_chance=7, ignore_throwspeed_threshold=TRUE, pain_stam_pct=0.7, pain_mult=5, jostle_pain_mult=6, rip_time=15, embed_chance_turf_mod=-100) + icon_state = "tiny" + +/obj/item/shrapnel/bullet // bullets + name = "bullet" + icon = 'icons/obj/ammo.dmi' + icon_state = "s-casing" + item_flags = NONE + +/obj/item/shrapnel/bullet/c38 // .38 round + name = "\improper .38 bullet" + +/obj/item/shrapnel/bullet/c38/dumdum // .38 DumDum round + name = "\improper .38 DumDum bullet" + embedding = list(embed_chance=70, fall_chance=7, jostle_chance=7, ignore_throwspeed_threshold=TRUE, pain_stam_pct=0.4, pain_mult=5, jostle_pain_mult=6, rip_time=10, embed_chance_turf_mod=-100) + +/obj/item/projectile/bullet/shrapnel + name = "flying shrapnel shard" + damage = 9 + range = 10 + armour_penetration = -30 + dismemberment = 5 + ricochets_max = 2 + ricochet_chance = 40 + shrapnel_type = /obj/item/shrapnel + ricochet_incidence_leeway = 60 + +/obj/item/projectile/bullet/shrapnel/mega + name = "flying shrapnel hunk" + range = 25 + dismemberment = 10 + ricochets_max = 4 + ricochet_chance = 90 + ricochet_decay_chance = 0.9 + +/obj/item/projectile/bullet/pellet/stingball + name = "stingball pellet" + damage = 3 + stamina = 8 + ricochets_max = 4 + ricochet_chance = 66 + ricochet_decay_chance = 1 + ricochet_decay_damage = 0.9 + ricochet_auto_aim_angle = 10 + ricochet_auto_aim_range = 2 + ricochet_incidence_leeway = 0 + shrapnel_type = /obj/item/shrapnel/stingball + +/obj/item/projectile/bullet/pellet/stingball/mega + name = "megastingball pellet" + ricochets_max = 6 + ricochet_chance = 110 diff --git a/code/game/objects/items/stacks/rods.dm b/code/game/objects/items/stacks/rods.dm index 4c2c21a7db..6fdea26683 100644 --- a/code/game/objects/items/stacks/rods.dm +++ b/code/game/objects/items/stacks/rods.dm @@ -21,6 +21,7 @@ GLOBAL_LIST_INIT(rod_recipes, list ( \ max_amount = 50 attack_verb = list("hit", "bludgeoned", "whacked") hitsound = 'sound/weapons/grenadelaunch.ogg' + embedding = list() novariants = TRUE /obj/item/stack/rods/suicide_act(mob/living/carbon/user) diff --git a/code/game/objects/items/stacks/sheets/glass.dm b/code/game/objects/items/stacks/sheets/glass.dm index c34ad44d5c..57af862b69 100644 --- a/code/game/objects/items/stacks/sheets/glass.dm +++ b/code/game/objects/items/stacks/sheets/glass.dm @@ -291,6 +291,7 @@ GLOBAL_LIST_INIT(plastitaniumglass_recipes, list( max_integrity = 40 sharpness = IS_SHARP var/icon_prefix + embedding = list("embed_chance" = 65) /obj/item/shard/suicide_act(mob/user) diff --git a/code/game/objects/items/stacks/tape.dm b/code/game/objects/items/stacks/tape.dm new file mode 100644 index 0000000000..177260febb --- /dev/null +++ b/code/game/objects/items/stacks/tape.dm @@ -0,0 +1,61 @@ + + +/obj/item/stack/sticky_tape + name = "sticky tape" + singular_name = "sticky tape" + desc = "Used for sticking to things for sticking said things to people." + icon = 'icons/obj/tapes.dmi' + icon_state = "tape_w" + var/prefix = "sticky" + item_flags = NOBLUDGEON + amount = 5 + max_amount = 5 + resistance_flags = FLAMMABLE + + var/list/conferred_embed = EMBED_HARMLESS + var/overwrite_existing = FALSE + +/obj/item/stack/sticky_tape/afterattack(obj/item/I, mob/living/user) + if(!istype(I)) + return + + if(I.embedding && I.embedding == conferred_embed) + to_chat(user, "[I] is already coated in [src]!") + return + + user.visible_message("[user] begins wrapping [I] with [src].", "You begin wrapping [I] with [src].") + + if(do_after(user, 30, target=I)) + I.embedding = conferred_embed + I.updateEmbedding() + to_chat(user, "You finish wrapping [I] with [src].") + use(1) + I.name = "[prefix] [I.name]" + + if(istype(I, /obj/item/grenade)) + var/obj/item/grenade/sticky_bomb = I + sticky_bomb.sticky = TRUE + +/obj/item/stack/sticky_tape/super + name = "super sticky tape" + singular_name = "super sticky tape" + desc = "Quite possibly the most mischevious substance in the galaxy. Use with extreme lack of caution." + icon_state = "tape_y" + prefix = "super sticky" + conferred_embed = EMBED_HARMLESS_SUPERIOR + +/obj/item/stack/sticky_tape/pointy + name = "pointy tape" + singular_name = "pointy tape" + desc = "Used for sticking to things for sticking said things inside people." + icon_state = "tape_evil" + prefix = "pointy" + conferred_embed = EMBED_POINTY + +/obj/item/stack/sticky_tape/pointy/super + name = "super pointy tape" + singular_name = "super pointy tape" + desc = "You didn't know tape could look so sinister. Welcome to Space Station 13." + icon_state = "tape_spikes" + prefix = "super pointy" + conferred_embed = EMBED_POINTY_SUPERIOR \ No newline at end of file diff --git a/code/game/objects/items/storage/belt.dm b/code/game/objects/items/storage/belt.dm index 7809f6d73b..684f8709ac 100755 --- a/code/game/objects/items/storage/belt.dm +++ b/code/game/objects/items/storage/belt.dm @@ -512,16 +512,16 @@ new /obj/item/grenade/smokebomb(src) new /obj/item/grenade/empgrenade(src) new /obj/item/grenade/empgrenade(src) - new /obj/item/grenade/syndieminibomb/concussion/frag(src) - new /obj/item/grenade/syndieminibomb/concussion/frag(src) - new /obj/item/grenade/syndieminibomb/concussion/frag(src) - new /obj/item/grenade/syndieminibomb/concussion/frag(src) - new /obj/item/grenade/syndieminibomb/concussion/frag(src) - new /obj/item/grenade/syndieminibomb/concussion/frag(src) - new /obj/item/grenade/syndieminibomb/concussion/frag(src) - new /obj/item/grenade/syndieminibomb/concussion/frag(src) - new /obj/item/grenade/syndieminibomb/concussion/frag(src) - new /obj/item/grenade/syndieminibomb/concussion/frag(src) + new /obj/item/grenade/frag(src) + new /obj/item/grenade/frag(src) + new /obj/item/grenade/frag(src) + new /obj/item/grenade/frag(src) + new /obj/item/grenade/frag(src) + new /obj/item/grenade/frag(src) + new /obj/item/grenade/frag(src) + new /obj/item/grenade/frag(src) + new /obj/item/grenade/frag(src) + new /obj/item/grenade/frag(src) new /obj/item/grenade/gluon(src) new /obj/item/grenade/gluon(src) new /obj/item/grenade/gluon(src) diff --git a/code/game/objects/items/storage/boxes.dm b/code/game/objects/items/storage/boxes.dm index 08b5f18ead..cdd3781748 100644 --- a/code/game/objects/items/storage/boxes.dm +++ b/code/game/objects/items/storage/boxes.dm @@ -275,6 +275,16 @@ for(var/i in 1 to 7) new /obj/item/grenade/flashbang(src) +obj/item/storage/box/stingbangs + name = "box of stingbangs (WARNING)" + desc = "WARNING: These devices are extremely dangerous and can cause severe injuries or death in repeated use." + icon_state = "secbox" + illustration = "flashbang" + +/obj/item/storage/box/stingbangs/PopulateContents() + for(var/i in 1 to 5) + new /obj/item/grenade/stingbang(src) + /obj/item/storage/box/flashes name = "box of flashbulbs" desc = "WARNING: Flashes can cause serious eye damage, protective eyewear is required." diff --git a/code/game/objects/items/twohanded.dm b/code/game/objects/items/twohanded.dm index 4437eefbf8..8c9cde5b76 100644 --- a/code/game/objects/items/twohanded.dm +++ b/code/game/objects/items/twohanded.dm @@ -628,7 +628,7 @@ force_wielded = 18 throwforce = 20 throw_speed = 4 - embedding = list("embedded_impact_pain_multiplier" = 1.5, "embed_chance" = 65) + embedding = list("impact_pain_mult" = 3) armour_penetration = 10 custom_materials = list(/datum/material/iron=1150, /datum/material/glass=2075) hitsound = 'sound/weapons/bladeslice.ogg' @@ -713,7 +713,7 @@ force_wielded = 19 force_unwielded = 11 throwforce = 21 - embedding = getEmbeddingBehavior(embed_chance = 75, embedded_pain_multiplier = 1.5) //plasmaglass spears are sharper + embedding = list(embed_chance = 75, pain_mult = 1.5) //plasmaglass spears are sharper icon_prefix = "spearplasma" qdel(tip) var/obj/item/twohanded/spear/S = locate() in parts_list @@ -728,7 +728,7 @@ if(G) explosive = G name = "explosive lance" - embedding = getEmbeddingBehavior(embed_chance = 0, embedded_pain_multiplier = 1)//elances should not be embeddable + embedding = list(embed_chance = 0, pain_mult = 1)//elances should not be embeddable desc = "A makeshift spear with [G] attached to it." update_icon() @@ -1108,7 +1108,7 @@ block_slowdown = 2 // no attacking while blocking block_lock_attacking = TRUE - + parry_time_windup = 1 parry_time_active = 5 parry_time_spindown = 0 diff --git a/code/game/objects/items/weaponry.dm b/code/game/objects/items/weaponry.dm index b48d2a81e4..67dcd71e65 100644 --- a/code/game/objects/items/weaponry.dm +++ b/code/game/objects/items/weaponry.dm @@ -299,12 +299,28 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 force = 2 throwforce = 20 //This is never used on mobs since this has a 100% embed chance. throw_speed = 4 - embedding = list("embedded_pain_multiplier" = 4, "embed_chance" = 100, "embedded_fall_chance" = 0) + embedding = list("pain_mult" = 4, "embed_chance" = 100, "fall_chance" = 0, "embed_chance_turf_mod" = 15) + armour_penetration = 40 + w_class = WEIGHT_CLASS_SMALL sharpness = IS_SHARP custom_materials = list(/datum/material/iron=500, /datum/material/glass=500) resistance_flags = FIRE_PROOF +/obj/item/throwing_star/stamina + name = "shock throwing star" + desc = "An aerodynamic disc designed to cause excruciating pain when stuck inside fleeing targets, hopefully without causing fatal harm." + throwforce = 5 + embedding = list("pain_chance" = 5, "embed_chance" = 100, "fall_chance" = 0, "jostle_chance" = 10, "pain_stam_pct" = 0.8, "jostle_pain_mult" = 3) + +/obj/item/throwing_star/toy + name = "toy throwing star" + desc = "An aerodynamic disc strapped with adhesive for sticking to people, good for playing pranks and getting yourself killed by security." + sharpness = IS_BLUNT + force = 0 + throwforce = 0 + embedding = list("pain_mult" = 0, "jostle_pain_mult" = 0, "embed_chance" = 100, "fall_chance" = 0) + /obj/item/switchblade name = "switchblade" icon_state = "switchblade" diff --git a/code/game/objects/obj_defense.dm b/code/game/objects/obj_defense.dm index e64b7d8f8d..54ac2a5559 100644 --- a/code/game/objects/obj_defense.dm +++ b/code/game/objects/obj_defense.dm @@ -74,7 +74,8 @@ /obj/bullet_act(obj/item/projectile/P) . = ..() playsound(src, P.hitsound, 50, 1) - visible_message("[src] is hit by \a [P]!", null, null, COMBAT_MESSAGE_RANGE) + if(P.suppressed != SUPPRESSED_VERY) + visible_message("[src] is hit by \a [P]!", null, null, COMBAT_MESSAGE_RANGE) if(!QDELETED(src)) //Bullet on_hit effect might have already destroyed this object take_damage(P.damage, P.damage_type, P.flag, 0, turn(P.dir, 180), P.armour_penetration) diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index 0b2fe5ac2a..ba3eba9bd3 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -323,3 +323,8 @@ /obj/proc/rnd_crafted(obj/machinery/rnd/production/P) return + +/obj/handle_ricochet(obj/item/projectile/P) + . = ..() + if(. && ricochet_damage_mod) + take_damage(P.damage * ricochet_damage_mod, P.damage_type, P.flag, 0, turn(P.dir, 180), P.armour_penetration) // pass along ricochet_damage_mod damage to the structure for the ricochet diff --git a/code/game/objects/structures.dm b/code/game/objects/structures.dm index e185defc0a..d30f617919 100644 --- a/code/game/objects/structures.dm +++ b/code/game/objects/structures.dm @@ -9,6 +9,8 @@ var/mob/living/structureclimber var/broken = 0 //similar to machinery's stat BROKEN layer = BELOW_OBJ_LAYER + flags_ricochet = RICOCHET_HARD + ricochet_chance_mod = 0.5 /obj/structure/Initialize() if (!armor) diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index ba7c0d2fbe..e896f8072b 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -38,6 +38,8 @@ GLOBAL_LIST_EMPTY(electrochromatic_window_lookup) var/hitsound = 'sound/effects/Glasshit.ogg' rad_insulation = RAD_VERY_LIGHT_INSULATION rad_flags = RAD_PROTECT_CONTENTS + flags_ricochet = RICOCHET_HARD + ricochet_chance_mod = 0.4 /// Electrochromatic status var/electrochromatic_status = NOT_ELECTROCHROMATIC @@ -525,6 +527,7 @@ GLOBAL_LIST_EMPTY(electrochromatic_window_lookup) explosion_block = 1 glass_type = /obj/item/stack/sheet/rglass rad_insulation = RAD_HEAVY_INSULATION + ricochet_chance_mod = 0.8 /obj/structure/window/reinforced/spawner/east dir = EAST @@ -690,6 +693,7 @@ GLOBAL_LIST_EMPTY(electrochromatic_window_lookup) level = 3 glass_type = /obj/item/stack/sheet/titaniumglass glass_amount = 2 + ricochet_chance_mod = 0.9 /obj/structure/window/shuttle/narsie_act() add_atom_colour("#3C3434", FIXED_COLOUR_PRIORITY) diff --git a/code/game/turfs/simulated/walls.dm b/code/game/turfs/simulated/walls.dm index da5d3c643d..b01a1df2c0 100644 --- a/code/game/turfs/simulated/walls.dm +++ b/code/game/turfs/simulated/walls.dm @@ -41,17 +41,6 @@ /turf/closed/wall/attack_tk() return -/turf/closed/wall/handle_projectile_ricochet(obj/item/projectile/P) //A huge pile of shitcode! - var/turf/p_turf = get_turf(P) - var/face_direction = get_dir(src, p_turf) - var/face_angle = dir2angle(face_direction) - var/incidence_s = GET_ANGLE_OF_INCIDENCE(face_angle, (P.Angle + 180)) - if(abs(incidence_s) > 90 && abs(incidence_s) < 270) - return FALSE - var/new_angle_s = SIMPLIFY_DEGREES(face_angle + incidence_s) - P.setAngle(new_angle_s) - return TRUE - /turf/closed/wall/proc/dismantle_wall(devastated=0, explode=0) if(devastated) devastate_wall() diff --git a/code/modules/antagonists/blob/blob/blobs/shield.dm b/code/modules/antagonists/blob/blob/blobs/shield.dm index 384df935f5..a2a6ce94d3 100644 --- a/code/modules/antagonists/blob/blob/blobs/shield.dm +++ b/code/modules/antagonists/blob/blob/blobs/shield.dm @@ -53,14 +53,3 @@ /obj/structure/blob/shield/reflective/check_projectile_ricochet(obj/item/projectile/P) return PROJECTILE_RICOCHET_FORCE -/obj/structure/blob/shield/reflective/handle_projectile_ricochet(obj/item/projectile/P) - var/turf/p_turf = get_turf(P) - var/face_direction = get_dir(src, p_turf) - var/face_angle = dir2angle(face_direction) - var/incidence_s = GET_ANGLE_OF_INCIDENCE(face_angle, (P.Angle + 180)) - if(abs(incidence_s) > 90 && abs(incidence_s) < 270) - return FALSE - var/new_angle_s = SIMPLIFY_DEGREES(face_angle + incidence_s) - P.setAngle(new_angle_s) - visible_message("[P] reflects off [src]!") - return TRUE diff --git a/code/modules/antagonists/bloodsucker/items/bloodsucker_stake.dm b/code/modules/antagonists/bloodsucker/items/bloodsucker_stake.dm index b13238f199..2aed251189 100644 --- a/code/modules/antagonists/bloodsucker/items/bloodsucker_stake.dm +++ b/code/modules/antagonists/bloodsucker/items/bloodsucker_stake.dm @@ -30,7 +30,7 @@ hitsound = 'sound/weapons/bladeslice.ogg' force = 6 throwforce = 10 - embedding = list("embed_chance" = 25, "embedded_fall_chance" = 0.5) // UPDATE 2/10/18 embedding_behavior.dm is how this is handled + embedding = list("embed_chance" = 25, "fall_chance" = 0.5) // UPDATE 2/10/18 embedding_behavior.dm is how this is handled //embed_chance = 25 // Look up "is_pointed" to see where we set stakes able to do this. //embedded_fall_chance = 0.5 // Chance it will fall out. obj_integrity = 30 @@ -93,7 +93,7 @@ embedded() add_mob_blood(target)//Place blood on the stake loc = C // Put INSIDE the character - B.receive_damage(w_class * embedding.embedded_impact_pain_multiplier) + B.receive_damage(w_class * embedding["pain_mult"]) if(C.mind) var/datum/antagonist/bloodsucker/bloodsucker = C.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) if(bloodsucker) @@ -118,7 +118,7 @@ force = 8 throwforce = 12 armour_penetration = 10 - embedding = list("embed_chance" = 50, "embedded_fall_chance" = 0) // UPDATE 2/10/18 embedding_behavior.dm is how this is handled + embedding = list("embed_chance" = 50, "fall_chance" = 0) // UPDATE 2/10/18 embedding_behavior.dm is how this is handled obj_integrity = 120 max_integrity = 120 diff --git a/code/modules/cargo/packs/security.dm b/code/modules/cargo/packs/security.dm index a48874e974..738eb03fbf 100644 --- a/code/modules/cargo/packs/security.dm +++ b/code/modules/cargo/packs/security.dm @@ -228,3 +228,35 @@ access = ACCESS_ARMORY crate_name = "sporting crate" crate_type = /obj/structure/closet/crate/secure // Would have liked a wooden crate but access >:( + +/datum/supply_pack/security/dumdum + name = ".38 DumDum Speedloader" + desc = "Contains one speedloader of .38 DumDum ammunition, good for embedding in soft targets. Requires Security or Forensics access to open." + cost = 1200 + access = FALSE + access_any = list(ACCESS_SECURITY, ACCESS_FORENSICS_LOCKERS) + contains = list(/obj/item/ammo_box/c38/dumdum) + crate_name = ".38 match crate" + +/datum/supply_pack/security/match + name = ".38 Match Grade Speedloader" + desc = "Contains one speedloader of match grade .38 ammunition, perfect for showing off trickshots. Requires Security or Forensics access to open." + cost = 1200 + access = FALSE + access_any = list(ACCESS_SECURITY, ACCESS_FORENSICS_LOCKERS) + contains = list(/obj/item/ammo_box/c38/match) + crate_name = ".38 match crate" + +/datum/supply_pack/security/stingpack + name = "Stingbang Grenade Pack" + desc = "Contains five \"stingbang\" grenades, perfect for stopping riots and playing morally unthinkable pranks. Requires Security access to open." + cost = 2500 + contains = list(/obj/item/storage/box/stingbangs) + crate_name = "stingbang grenade pack crate" + +/datum/supply_pack/security/stingpack/single + name = "Stingbang Single-Pack" + desc = "Contains one \"stingbang\" grenade, perfect for playing meanhearted pranks. Requires Security access to open." + cost = 1400 + contains = list(/obj/item/grenade/stingbang) + diff --git a/code/modules/events/wizard/embeddies.dm b/code/modules/events/wizard/embeddies.dm new file mode 100644 index 0000000000..fe08b9c743 --- /dev/null +++ b/code/modules/events/wizard/embeddies.dm @@ -0,0 +1,46 @@ +/datum/round_event_control/wizard/embedpocalypse + name = "Make Everything Embeddable" + weight = 2 + typepath = /datum/round_event/wizard/embedpocalypse + max_occurrences = 1 + earliest_start = 0 MINUTES + +/datum/round_event/wizard/embedpocalypse/start() + for(var/obj/item/I in world) + CHECK_TICK + + if(!(I.flags_1 & INITIALIZED_1)) + continue + + if(!I.embedding || I.embedding == EMBED_HARMLESS) + I.embedding = EMBED_POINTY + I.updateEmbedding() + I.name = "pointy [I.name]" + + GLOB.embedpocalypse = TRUE + GLOB.stickpocalypse = FALSE // embedpocalypse takes precedence over stickpocalypse + +/datum/round_event_control/wizard/embedpocalypse/sticky + name = "Make Everything Sticky" + weight = 6 + typepath = /datum/round_event/wizard/embedpocalypse/sticky + max_occurrences = 1 + earliest_start = 0 MINUTES + +/datum/round_event_control/wizard/embedpocalypse/sticky/canSpawnEvent(players_amt, gamemode) + if(GLOB.embedpocalypse) + return FALSE + +/datum/round_event/wizard/embedpocalypse/sticky/start() + for(var/obj/item/I in world) + CHECK_TICK + + if(!(I.flags_1 & INITIALIZED_1)) + continue + + if(!I.embedding) + I.embedding = EMBED_HARMLESS + I.updateEmbedding() + I.name = "sticky [I.name]" + + GLOB.stickpocalypse = TRUE diff --git a/code/modules/hydroponics/grown/citrus.dm b/code/modules/hydroponics/grown/citrus.dm index 851753da46..f4748ccde8 100644 --- a/code/modules/hydroponics/grown/citrus.dm +++ b/code/modules/hydroponics/grown/citrus.dm @@ -195,7 +195,7 @@ /obj/item/reagent_containers/food/snacks/grown/firelemon/ex_act(severity) qdel(src) //Ensuring that it's deleted by its own explosion -/obj/item/reagent_containers/food/snacks/grown/firelemon/proc/prime() +/obj/item/reagent_containers/food/snacks/grown/firelemon/proc/prime(mob/living/lanced_by) switch(seed.potency) //Combustible lemons are alot like IEDs, lots of flame, very little bang. if(0 to 30) update_mob() diff --git a/code/modules/hydroponics/grown/misc.dm b/code/modules/hydroponics/grown/misc.dm index f356ed7b68..e5c8f72dfe 100644 --- a/code/modules/hydroponics/grown/misc.dm +++ b/code/modules/hydroponics/grown/misc.dm @@ -223,7 +223,7 @@ /obj/item/reagent_containers/food/snacks/grown/cherry_bomb/ex_act(severity) qdel(src) //Ensuring that it's deleted by its own explosion. Also prevents mass chain reaction with piles of cherry bombs -/obj/item/reagent_containers/food/snacks/grown/cherry_bomb/proc/prime() +/obj/item/reagent_containers/food/snacks/grown/cherry_bomb/proc/prime(mob/living/lanced_by) icon_state = "cherry_bomb_lit" playsound(src, 'sound/effects/fuse.ogg', seed.potency, 0) addtimer(CALLBACK(src, /obj/item/reagent_containers/food/snacks/grown/cherry_bomb/proc/detonate), rand(50, 100)) diff --git a/code/modules/mob/living/blood.dm b/code/modules/mob/living/blood.dm index a5275f677d..612358e802 100644 --- a/code/modules/mob/living/blood.dm +++ b/code/modules/mob/living/blood.dm @@ -94,7 +94,9 @@ //We want an accurate reading of .len listclearnulls(BP.embedded_objects) - temp_bleed += 0.5 * BP.embedded_objects.len + for(var/obj/item/embeddies in BP.embedded_objects) + if(!embeddies.isEmbedHarmless()) + temp_bleed += 0.5 if(brutedamage >= 20) temp_bleed += (brutedamage * 0.013) diff --git a/code/modules/mob/living/carbon/alien/alien_defense.dm b/code/modules/mob/living/carbon/alien/alien_defense.dm index 00ce3a320a..042451b7dd 100644 --- a/code/modules/mob/living/carbon/alien/alien_defense.dm +++ b/code/modules/mob/living/carbon/alien/alien_defense.dm @@ -54,6 +54,8 @@ In all, this is a lot like the monkey code. /N return switch(M.a_intent) if(INTENT_HELP) + if(M == src && check_self_for_injuries()) + return help_shake_act(M) if(INTENT_GRAB) grabbedby(M) diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index e4ec902654..7b201e7492 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -247,7 +247,7 @@ /mob/living/carbon/Topic(href, href_list) ..() //strip panel - if(usr.canUseTopic(src, BE_CLOSE, NO_DEXTERY)) + if(usr.canUseTopic(src, BE_CLOSE)) if(href_list["internal"] && !HAS_TRAIT(src, TRAIT_NO_INTERNALS)) var/slot = text2num(href_list["internal"]) var/obj/item/ITEM = get_item_by_slot(slot) @@ -267,7 +267,15 @@ visible_message("[usr] [internal ? "opens" : "closes"] the valve on [src]'s [ITEM.name].", \ "[usr] [internal ? "opens" : "closes"] the valve on your [ITEM.name].", \ target = usr, target_message = "You [internal ? "opens" : "closes"] the valve on [src]'s [ITEM.name].") - + if(href_list["embedded_object"] && usr.canUseTopic(src, BE_CLOSE)) + var/obj/item/bodypart/L = locate(href_list["embedded_limb"]) in bodyparts + if(!L) + return + var/obj/item/I = locate(href_list["embedded_object"]) in L.embedded_objects + if(!I || I.loc != src) //no item, no limb, or item is not in limb or in the person anymore + return + SEND_SIGNAL(src, COMSIG_CARBON_EMBED_RIP, I, L) + return /mob/living/carbon/fall(forced) loc.handle_fall(src, forced)//it's loc so it doesn't call the mob's handle_fall which does nothing diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm index f9d38828e3..41a034ecd8 100644 --- a/code/modules/mob/living/carbon/carbon_defense.dm +++ b/code/modules/mob/living/carbon/carbon_defense.dm @@ -72,7 +72,7 @@ I.add_mob_blood(src)//it embedded itself in you, of course it's bloody! I.forceMove(src) I.embedded() - L.receive_damage(I.w_class*I.embedding.embedded_impact_pain_multiplier) + L.receive_damage(I.w_class*I.embedding["impact_pain_mult"]) visible_message("[I] embeds itself in [src]'s [L.name]!","[I] embeds itself in your [L.name]!") SEND_SIGNAL(src, COMSIG_ADD_MOOD_EVENT, "embedded", /datum/mood_event/embedded) @@ -274,6 +274,9 @@ to_chat(M, "You can't put [p_them()] out with just your bare hands!") return + if(M == src && check_self_for_injuries()) + return + if(health >= 0 && !(HAS_TRAIT(src, TRAIT_FAKEDEATH))) var/friendly_check = FALSE if(mob_run_block(M, 0, M.name, ATTACK_TYPE_UNARMED, 0, null, null, null)) @@ -341,6 +344,26 @@ update_mobility() playsound(loc, 'sound/weapons/thudswoosh.ogg', 50, 1, -1) +/// Check ourselves to see if we've got any shrapnel, return true if we do. This is a much simpler version of what humans do, we only indicate we're checking ourselves if there's actually shrapnel +/mob/living/carbon/proc/check_self_for_injuries() + if(stat == DEAD || stat == UNCONSCIOUS) + return + + var/embeds = FALSE + for(var/X in bodyparts) + var/obj/item/bodypart/LB = X + for(var/obj/item/I in LB.embedded_objects) + if(!embeds) + embeds = TRUE + // this way, we only visibly try to examine ourselves if we have something embedded, otherwise we'll still hug ourselves :) + visible_message("[src] examines [p_them()]self.", \ + "You check yourself for shrapnel.") + if(I.isEmbedHarmless()) + to_chat(src, "\t There is \a [I] stuck to your [LB.name]!") + else + to_chat(src, "\t There is \a [I] embedded in your [LB.name]!") + + return embeds /mob/living/carbon/flash_act(intensity = 1, override_blindness_check = 0, affect_silicon = 0, visual = 0) . = ..() diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm index 8dbfba47b1..b07aab30d1 100644 --- a/code/modules/mob/living/carbon/examine.dm +++ b/code/modules/mob/living/carbon/examine.dm @@ -31,14 +31,35 @@ else if(get_bodypart(BODY_ZONE_HEAD)) . += "It appears that [t_his] brain is missing..." - var/list/missing = get_missing_limbs() + var/list/msg = list("") + var/list/missing = list(BODY_ZONE_HEAD, BODY_ZONE_CHEST, BODY_ZONE_R_ARM, BODY_ZONE_L_ARM, BODY_ZONE_R_LEG, BODY_ZONE_L_LEG) + var/list/disabled = list() + for(var/X in bodyparts) + var/obj/item/bodypart/BP = X + if(BP.disabled) + disabled += BP + missing -= BP.body_zone + for(var/obj/item/I in BP.embedded_objects) + if(I.isEmbedHarmless()) + msg += "[t_He] [t_has] \a [icon2html(I, user)] [I] stuck to [t_his] [BP.name]!\n" + else + msg += "[t_He] [t_has] \a [icon2html(I, user)] [I] embedded in [t_his] [BP.name]!\n" + + for(var/X in disabled) + var/obj/item/bodypart/BP = X + var/damage_text + if(!(BP.get_damage(include_stamina = FALSE) >= BP.max_damage)) //Stamina is disabling the limb + damage_text = "limp and lifeless" + else + damage_text = (BP.brute_dam >= BP.burn_dam) ? BP.heavy_brute_msg : BP.heavy_burn_msg + msg += "[capitalize(t_his)] [BP.name] is [damage_text]!\n" + for(var/t in missing) if(t==BODY_ZONE_HEAD) - . += "[t_His] [parse_zone(t)] is missing!" + msg += "[t_His] [parse_zone(t)] is missing!\n" continue - . += "[t_His] [parse_zone(t)] is missing!" + msg += "[t_His] [parse_zone(t)] is missing!\n" - var/list/msg = list() var/temp = getBruteLoss() if(!(user == src && src.hal_screwyhud == SCREWYHUD_HEALTHY)) //fake healthy if(temp) diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm index cb21534d25..d32184edb5 100644 --- a/code/modules/mob/living/carbon/human/examine.dm +++ b/code/modules/mob/living/carbon/human/examine.dm @@ -159,7 +159,10 @@ disabled += BP missing -= BP.body_zone for(var/obj/item/I in BP.embedded_objects) - msg += "[t_He] [t_has] \a [icon2html(I, user)] [I] embedded in [t_his] [BP.name]!\n" + if(I.isEmbedHarmless()) + msg += "[t_He] [t_has] \a [icon2html(I, user)] [I] stuck to [t_his] [BP.name]!\n" + else + msg += "[t_He] [t_has] \a [icon2html(I, user)] [I] embedded in [t_his] [BP.name]!\n" for(var/X in disabled) var/obj/item/bodypart/BP = X diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index de703b8ad4..4421c383e6 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -220,22 +220,7 @@ var/obj/item/I = locate(href_list["embedded_object"]) in L.embedded_objects if(!I || I.loc != src) //no item, no limb, or item is not in limb or in the person anymore return - var/time_taken = I.embedding.embedded_unsafe_removal_time/I.w_class //Citadel Change from * to / - usr.visible_message("[usr] attempts to remove [I] from [usr.p_their()] [L.name].","You attempt to remove [I] from your [L.name]... (It will take [DisplayTimeText(time_taken)].)") - if(do_after(usr, time_taken, needhand = 1, target = src)) - remove_embedded_unsafe(L, I, usr) - /* CITADEL EDIT: remove_embedded_unsafe replaces this code - if(!I || !L || I.loc != src || !(I in L.embedded_objects)) - return - L.embedded_objects -= I - L.receive_damage(I.embedding.embedded_unsafe_removal_pain_multiplier*I.w_class)//It hurts to rip it out, get surgery you dingus. - I.forceMove(get_turf(src)) - usr.put_in_hands(I) - usr.emote("scream") - usr.visible_message("[usr] successfully rips [I] out of [usr.p_their()] [L.name]!","You successfully remove [I] from your [L.name].") - if(!has_embedded_objects()) - clear_alert("embeddedobject") - SEND_SIGNAL(usr, COMSIG_CLEAR_MOOD_EVENT, "embedded") */ + SEND_SIGNAL(src, COMSIG_CARBON_EMBED_RIP, I, L) return if(href_list["item"]) diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index 6f013f560a..63296021ff 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -52,11 +52,6 @@ return martial_art_result return ..() -/mob/living/carbon/human/can_embed(obj/item/I) - if(I.get_sharpness() || is_pointed(I) || is_type_in_typecache(I, GLOB.can_embed_types)) - return TRUE - return FALSE - /mob/living/carbon/human/proc/check_martial_melee_block() if(mind) if(mind.martial_art && prob(mind.martial_art.block_chance) && mind.martial_art.can_use(src) && in_throw_mode && !incapacitated(FALSE, TRUE)) @@ -620,7 +615,10 @@ to_send += "\t Your [LB.name] [HAS_TRAIT(src, TRAIT_SELF_AWARE) ? "has" : "is"] [status].\n" for(var/obj/item/I in LB.embedded_objects) - to_send += "\t There is \a [I] embedded in your [LB.name]!\n" + if(I.isEmbedHarmless()) + to_chat(src, "\t There is \a [I] stuck to your [LB.name]!") + else + to_chat(src, "\t There is \a [I] embedded in your [LB.name]!") for(var/t in missing) to_send += "Your [parse_zone(t)] is missing!\n" diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm index eb22934a2b..86b6406081 100644 --- a/code/modules/mob/living/carbon/human/life.dm +++ b/code/modules/mob/living/carbon/human/life.dm @@ -35,10 +35,6 @@ //heart attack stuff handle_heart() - if(stat != DEAD) - //Stuff jammed in your limbs hurts - handle_embedded_objects() - //Update our name based on whether our face is obscured/disfigured name = get_visible_name() @@ -303,25 +299,6 @@ return TRUE return ..() - -/mob/living/carbon/human/proc/handle_embedded_objects() - for(var/X in bodyparts) - var/obj/item/bodypart/BP = X - for(var/obj/item/I in BP.embedded_objects) - if(prob(I.embedding.embedded_pain_chance)) - BP.receive_damage(I.w_class*I.embedding.embedded_pain_multiplier) - to_chat(src, "[I] embedded in your [BP.name] hurts!") - - if(prob(I.embedding.embedded_fall_chance)) - BP.receive_damage(I.w_class*I.embedding.embedded_fall_pain_multiplier) - BP.embedded_objects -= I - I.forceMove(drop_location()) - I.unembedded() - visible_message("[I] falls out of [name]'s [BP.name]!","[I] falls out of your [BP.name]!") - if(!has_embedded_objects()) - clear_alert("embeddedobject") - SEND_SIGNAL(src, COMSIG_CLEAR_MOOD_EVENT, "embedded") - /mob/living/carbon/human/proc/handle_active_genes() if(HAS_TRAIT(src, TRAIT_MUTATION_STASIS)) return diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index 2ca1b63b14..d0aff933b5 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -1,6 +1,9 @@ -/mob/living/proc/run_armor_check(def_zone = null, attack_flag = "melee", absorb_text = "Your armor absorbs the blow!", soften_text = "Your armor softens the blow!", armour_penetration, penetrated_text = "Your armor was penetrated!") +/mob/living/proc/run_armor_check(def_zone = null, attack_flag = "melee", absorb_text = "Your armor absorbs the blow!", soften_text = "Your armor softens the blow!", armour_penetration, penetrated_text = "Your armor was penetrated!", silent=FALSE) var/armor = getarmor(def_zone, attack_flag) + + if(silent) + return max(0, armor - armour_penetration) //the if "armor" check is because this is used for everything on /living, including humans if(armor && armour_penetration) @@ -126,7 +129,7 @@ skipcatch = TRUE blocked = TRUE total_damage = block_calculate_resultant_damage(total_damage, block_return) - else if(I && I.throw_speed >= EMBED_THROWSPEED_THRESHOLD && can_embed(I, src) && prob(I.embedding.embed_chance) && !HAS_TRAIT(src, TRAIT_PIERCEIMMUNE) && (!HAS_TRAIT(src, TRAIT_AUTO_CATCH_ITEM) || incapacitated() || get_active_held_item())) + else if(I && I.throw_speed >= EMBED_THROWSPEED_THRESHOLD && can_embed(I, src) && prob(I.embedding["embed_chance"]) && !HAS_TRAIT(src, TRAIT_PIERCEIMMUNE) && (!HAS_TRAIT(src, TRAIT_AUTO_CATCH_ITEM) || incapacitated() || get_active_held_item())) embed_item(I) hitpush = FALSE skipcatch = TRUE //can't catch the now embedded item diff --git a/code/modules/ninja/suit/n_suit_verbs/ninja_stars.dm b/code/modules/ninja/suit/n_suit_verbs/ninja_stars.dm index 31ac62d9fc..508722ecf2 100644 --- a/code/modules/ninja/suit/n_suit_verbs/ninja_stars.dm +++ b/code/modules/ninja/suit/n_suit_verbs/ninja_stars.dm @@ -15,4 +15,4 @@ /obj/item/throwing_star/ninja name = "ninja throwing star" throwforce = 30 - embedding = list("embedded_pain_multiplier" = 6, "embed_chance" = 100, "embedded_fall_chance" = 0) + embedding = list("pain_mult" = 6, "embed_chance" = 100, "fall_chance" = 0, "embed_chance_turf_mod" = 15) diff --git a/code/modules/paperwork/pen.dm b/code/modules/paperwork/pen.dm index 1c14a30a8a..08252d4e05 100644 --- a/code/modules/paperwork/pen.dm +++ b/code/modules/paperwork/pen.dm @@ -27,6 +27,7 @@ var/colour = "black" //what colour the ink is! var/degrees = 0 var/font = PEN_FONT + embedding = list() /obj/item/pen/suicide_act(mob/user) user.visible_message("[user] is scribbling numbers all over [user.p_them()]self with [src]! It looks like [user.p_theyre()] trying to commit sudoku...") @@ -41,6 +42,7 @@ desc = "It's a normal red ink pen." icon_state = "pen_red" colour = "red" + throw_speed = 4 // red ones go faster (in this case, fast enough to embed!) /obj/item/pen/invisible desc = "It's an invisible pen marker." @@ -56,8 +58,10 @@ switch(colour) if("black") colour = "red" + throw_speed++ if("red") colour = "green" + throw_speed = initial(throw_speed) if("green") colour = "blue" else @@ -178,6 +182,7 @@ attack_verb = list("slashed", "stabbed", "sliced", "torn", "ripped", "diced", "cut") //these wont show up if the pen is off sharpness = IS_SHARP var/on = FALSE + embedding = list(embed_chance = EMBED_CHANCE) /obj/item/pen/edagger/ComponentInitialize() . = ..() @@ -198,16 +203,18 @@ throwforce = initial(throwforce) playsound(user, 'sound/weapons/saberoff.ogg', 5, 1) to_chat(user, "[src] can now be concealed.") + updateEmbedding() else on = TRUE force = 18 w_class = WEIGHT_CLASS_NORMAL name = "energy dagger" hitsound = 'sound/weapons/blade1.ogg' - embedding = getEmbeddingBehavior(embed_chance = 100, embedded_fall_chance = 0) //rule of cool + embedding = list(embed_chance = 100, fall_chance = 0) //rule of cool throwforce = 35 playsound(user, 'sound/weapons/saberon.ogg', 5, 1) to_chat(user, "[src] is now active.") + updateEmbedding() update_icon() /obj/item/pen/edagger/update_icon_state() diff --git a/code/modules/projectiles/ammunition/_firing.dm b/code/modules/projectiles/ammunition/_firing.dm index 1a6786524c..340cfc2e98 100644 --- a/code/modules/projectiles/ammunition/_firing.dm +++ b/code/modules/projectiles/ammunition/_firing.dm @@ -1,17 +1,21 @@ /obj/item/ammo_casing/proc/fire_casing(atom/target, mob/living/user, params, distro, quiet, zone_override, spread, atom/fired_from) distro += variance - for (var/i = max(1, pellets), i > 0, i--) - var/targloc = get_turf(target) - ready_proj(target, user, quiet, zone_override, fired_from) + var/targloc = get_turf(target) + ready_proj(target, user, quiet, zone_override, fired_from) + if(pellets == 1) if(distro) //We have to spread a pixel-precision bullet. throw_proj was called before so angles should exist by now... if(randomspread) spread = round((rand() - 0.5) * distro) else //Smart spread - spread = round((i / pellets - 0.5) * distro) + spread = round(1 - 0.5) * distro if(!throw_proj(target, targloc, user, params, spread)) - return 0 - if(i > 1) - newshot() + return FALSE + else + if(isnull(BB)) + return FALSE + AddComponent(/datum/component/pellet_cloud, projectile_type, pellets) + SEND_SIGNAL(src, COMSIG_PELLET_CLOUD_INIT, target, user, fired_from, randomspread, spread, zone_override, params, distro) + if(click_cooldown_override) user.changeNext_move(click_cooldown_override) else @@ -31,7 +35,7 @@ else BB.def_zone = user.zone_selected BB.suppressed = quiet - + if(isgun(fired_from)) var/obj/item/gun/G = fired_from BB.damage *= G.projectile_damage_multiplier diff --git a/code/modules/projectiles/ammunition/ballistic/lmg.dm b/code/modules/projectiles/ammunition/ballistic/lmg.dm index 04cc4df349..0688d09e3c 100644 --- a/code/modules/projectiles/ammunition/ballistic/lmg.dm +++ b/code/modules/projectiles/ammunition/ballistic/lmg.dm @@ -21,3 +21,16 @@ name = "1.95x129mm incendiary bullet casing" desc = "A 1.95x129mm bullet casing designed with a chemical-filled capsule on the tip that when bursted, reacts with the atmosphere to produce a fireball, engulfing the target in flames." projectile_type = /obj/item/projectile/bullet/incendiary/mm195x129 + +/obj/item/ammo_casing/mm712x82/match + name = "7.12x82mm match bullet casing" + desc = "A 7.12x82mm bullet casing manufactured to unfailingly high standards, you could pull off some cool trickshots with this." + projectile_type = /obj/item/projectile/bullet/mm712x82_match + +/obj/item/projectile/bullet/mm712x82_match + name = "7.12x82mm match bullet" + damage = 40 + ricochets_max = 2 + ricochet_chance = 60 + ricochet_auto_aim_range = 4 + ricochet_incidence_leeway = 35 \ No newline at end of file diff --git a/code/modules/projectiles/ammunition/ballistic/revolver.dm b/code/modules/projectiles/ammunition/ballistic/revolver.dm index d5a9fab4b4..693b258e3d 100644 --- a/code/modules/projectiles/ammunition/ballistic/revolver.dm +++ b/code/modules/projectiles/ammunition/ballistic/revolver.dm @@ -11,6 +11,12 @@ desc = "A .357 armor-piercing bullet casing." projectile_type = /obj/item/projectile/bullet/a357/ap +/obj/item/ammo_casing/a357/match + name = ".357 match bullet casing" + desc = "A .357 bullet casing, manufactured to exceedingly high standards." + caliber = "357" + projectile_type = /obj/item/projectile/bullet/a357/match + // 7.62x38mmR (Nagant Revolver) /obj/item/ammo_casing/n762 @@ -47,4 +53,19 @@ name = ".38 Iceblox bullet casing" desc = "A .38 Iceblox bullet casing." caliber = "38" - projectile_type = /obj/item/projectile/bullet/c38/iceblox \ No newline at end of file + projectile_type = /obj/item/projectile/bullet/c38/iceblox + +/obj/item/ammo_casing/c38/match + name = ".38 Match bullet casing" + desc = "A .38 bullet casing, manufactured to exceedingly high standards." + projectile_type = /obj/item/projectile/bullet/c38/match + +/obj/item/ammo_casing/c38/match/bouncy + name = ".38 Rubber bullet casing" + desc = "A .38 rubber bullet casing, manufactured to exceedingly high standards." + projectile_type = /obj/item/projectile/bullet/c38/match/bouncy + +/obj/item/ammo_casing/c38/dumdum + name = ".38 DumDum bullet casing" + desc = "A .38 DumDum bullet casing." + projectile_type = /obj/item/projectile/bullet/c38/dumdum \ No newline at end of file diff --git a/code/modules/projectiles/boxes_magazines/ammo_boxes.dm b/code/modules/projectiles/boxes_magazines/ammo_boxes.dm index 4e638ce1fc..86d66ec354 100644 --- a/code/modules/projectiles/boxes_magazines/ammo_boxes.dm +++ b/code/modules/projectiles/boxes_magazines/ammo_boxes.dm @@ -7,6 +7,11 @@ max_ammo = 7 multiple_sprites = 1 +/obj/item/ammo_box/a357/match + name = "speed loader (.357 Match)" + desc = "Designed to quickly reload revolvers. These rounds are manufactured within extremely tight tolerances, making them easy to show off trickshots with." + ammo_type = /obj/item/ammo_casing/a357/match + /obj/item/ammo_box/a357/ap name = "speed loader (.357 AP)" ammo_type = /obj/item/ammo_casing/a357/ap @@ -40,6 +45,16 @@ desc = "Designed to quickly reload revolvers. Iceblox bullets contain a cryogenic payload." ammo_type = /obj/item/ammo_casing/c38/iceblox +/obj/item/ammo_box/c38/dumdum + name = "speed loader (.38 DumDum)" + desc = "Designed to quickly reload revolvers. DumDum bullets shatter on impact and shred the target's innards, likely getting caught inside." + ammo_type = /obj/item/ammo_casing/c38/dumdum + +/obj/item/ammo_box/c38/match + name = "speed loader (.38 Match)" + desc = "Designed to quickly reload revolvers. These rounds are manufactured within extremely tight tolerances, making them easy to show off trickshots with." + ammo_type = /obj/item/ammo_casing/c38/match + /obj/item/ammo_box/c32mm name = "ammo box (.32 acp)" desc = "Lethal .32 acp bullets, there's forty in the box." diff --git a/code/modules/projectiles/boxes_magazines/external/lmg.dm b/code/modules/projectiles/boxes_magazines/external/lmg.dm index e42c6a4719..55f5e491da 100644 --- a/code/modules/projectiles/boxes_magazines/external/lmg.dm +++ b/code/modules/projectiles/boxes_magazines/external/lmg.dm @@ -20,3 +20,7 @@ /obj/item/ammo_box/magazine/mm195x129/update_icon() ..() icon_state = "a762-[round(ammo_count(),10)]" + +/obj/item/ammo_box/magazine/mm712x82/match + name = "box magazine (Match 7.12x82mm)" + ammo_type = /obj/item/ammo_casing/mm712x82/match \ No newline at end of file diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index d2643eeeb7..f279047356 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -58,9 +58,25 @@ var/nondirectional_sprite = FALSE //Set TRUE to prevent projectiles from having their sprites rotated based on firing angle var/spread = 0 //amount (in degrees) of projectile spread animate_movement = 0 //Use SLIDE_STEPS in conjunction with legacy + /// how many times we've ricochet'd so far (instance variable, not a stat) var/ricochets = 0 - var/ricochets_max = 2 - var/ricochet_chance = 30 + /// how many times we can ricochet max + var/ricochets_max = 0 + /// 0-100, the base chance of ricocheting, before being modified by the atom we shoot and our chance decay + var/ricochet_chance = 0 + /// 0-1 (or more, I guess) multiplier, the ricochet_chance is modified by multiplying this after each ricochet + var/ricochet_decay_chance = 0.7 + /// 0-1 (or more, I guess) multiplier, the projectile's damage is modified by multiplying this after each ricochet + var/ricochet_decay_damage = 0.7 + /// On ricochet, if nonzero, we consider all mobs within this range of our projectile at the time of ricochet to home in on like Revolver Ocelot, as governed by ricochet_auto_aim_angle + var/ricochet_auto_aim_range = 0 + /// On ricochet, if ricochet_auto_aim_range is nonzero, we'll consider any mobs within this range of the normal angle of incidence to home in on, higher = more auto aim + var/ricochet_auto_aim_angle = 30 + /// the angle of impact must be within this many degrees of the struck surface, set to 0 to allow any angle + var/ricochet_incidence_leeway = 40 + + ///If the object being hit can pass ths damage on to something else, it should not do it for this bullet + var/force_hit = FALSE //Hitscan var/hitscan = FALSE //Whether this is hitscan. If it is, speed is basically ignored. @@ -133,6 +149,11 @@ var/temporary_unstoppable_movement = FALSE + ///If defined, on hit we create an item of this type then call hitby() on the hit target with this + var/shrapnel_type + ///If TRUE, hit mobs even if they're on the floor and not our target + var/hit_stunned_targets = FALSE + /obj/item/projectile/Initialize() . = ..() permutated = list() @@ -148,6 +169,7 @@ on_range() /obj/item/projectile/proc/on_range() //if we want there to be effects when they reach the end of their range + SEND_SIGNAL(src, COMSIG_PROJECTILE_RANGE_OUT) qdel(src) //to get the correct limb (if any) for the projectile hit message @@ -164,17 +186,17 @@ /obj/item/projectile/proc/prehit(atom/target) return TRUE -/** - * Called when we hit something. - * - * @params - * * target - what we hit - * * blocked - 0 to 100 percentage mitigation/block - * * def zone - where we hit if we hit a mob. - */ -/obj/item/projectile/proc/on_hit(atom/target, blocked = 0, def_zone) +/obj/item/projectile/proc/on_hit(atom/target, blocked = FALSE) if(fired_from) SEND_SIGNAL(fired_from, COMSIG_PROJECTILE_ON_HIT, firer, target, Angle) + + // i know that this is probably more with wands and gun mods in mind, but it's a bit silly that the projectile on_hit signal doesn't ping the projectile itself. + // maybe we care what the projectile thinks! See about combining these via args some time when it's not 5AM + var/obj/item/bodypart/hit_limb + if(isliving(target)) + var/mob/living/L = target + hit_limb = L.check_limb_hit(def_zone) + SEND_SIGNAL(src, COMSIG_PROJECTILE_SELF_ON_HIT, firer, target, Angle, hit_limb) var/turf/target_loca = get_turf(target) var/hitx @@ -226,10 +248,13 @@ new impact_effect_type(target_loca, hitx, hity) var/organ_hit_text = "" - var/limb_hit = L.check_limb_hit(def_zone)//to get the correct message info. + var/limb_hit = hit_limb if(limb_hit) organ_hit_text = " in \the [parse_zone(limb_hit)]" - if(suppressed) + + if(suppressed==SUPPRESSED_VERY) + playsound(loc, hitsound, 5, TRUE, -1) + else if(suppressed) playsound(loc, hitsound, 5, 1, -1) to_chat(L, "You're shot by \a [src][organ_hit_text]!") else @@ -259,53 +284,43 @@ else return 50 //if the projectile doesn't do damage, play its hitsound at 50% volume +/obj/item/projectile/proc/on_ricochet(atom/A) + if(!ricochet_auto_aim_angle || !ricochet_auto_aim_range) + return + + var/mob/living/unlucky_sob + var/best_angle = ricochet_auto_aim_angle + if(firer && HAS_TRAIT(firer, TRAIT_NICE_SHOT)) + best_angle += NICE_SHOT_RICOCHET_BONUS + for(var/mob/living/L in range(ricochet_auto_aim_range, src.loc)) + if(L.stat == DEAD || !isInSight(src, L)) + continue + var/our_angle = abs(closer_angle_difference(Angle, get_projectile_angle(src.loc, L.loc))) + if(our_angle < best_angle) + best_angle = our_angle + unlucky_sob = L + + if(unlucky_sob) + setAngle(get_projectile_angle(src, unlucky_sob.loc)) + /obj/item/projectile/proc/store_hitscan_collision(datum/point/pcache) beam_segments[beam_index] = pcache beam_index = pcache beam_segments[beam_index] = null -/** - * Determines if we should ricochet off of something. - * By default, asks the thing if we should ricochet off it, but because we're called first, we get final say. - * Returns TRUE or FALSE. - */ -/obj/item/projectile/proc/check_ricochet(atom/A) - if(ricochets > ricochets_max) //safety thing, we don't care about what the other thing says about this. - return FALSE - var/them = A.check_projectile_ricochet(src) - switch(them) - if(PROJECTILE_RICOCHET_PREVENT) - return FALSE - if(PROJECTILE_RICOCHET_FORCE) - return TRUE - if(PROJECTILE_RICOCHET_NO) - return FALSE - if(PROJECTILE_RICOCHET_YES) - return prob(ricochet_chance) - else - CRASH("Invalid return value for projectile ricochet check from [A].") - -/** - * Handles ricocheting off of something. - * By default, also asks the thing to handle it, but because we're called first, we get final say. - */ -/obj/item/projectile/proc/handle_ricochet(atom/A) - ricochets++ - ignore_source_check = TRUE - decayedRange = max(0, decayedRange - reflect_range_decrease) - pixel_move_interrupted = TRUE - range = decayedRange - return A.handle_projectile_ricochet(src) - /obj/item/projectile/Bump(atom/A) if(!trajectory) return var/turf/T = get_turf(A) - if(check_ricochet(A)) + if(check_ricochet(A) && A.handle_ricochet(src)) //if you can ricochet, attempt to ricochet off the object + on_ricochet(A) //if allowed, use autoaim to ricochet into someone, otherwise default to ricocheting off the object from above var/datum/point/pcache = trajectory.copy_to() if(hitscan) store_hitscan_collision(pcache) - handle_ricochet(A) + decayedRange = max(0, decayedRange - reflect_range_decrease) + ricochet_chance *= ricochet_decay_chance + damage *= ricochet_decay_damage + range = decayedRange return TRUE var/distance = get_dist(T, starting) // Get the distance between the turf shot from and the mob we hit and use that for the calculations. @@ -381,6 +396,33 @@ return T //Returns null if nothing at all was found. +/obj/item/projectile/proc/check_ricochet(atom/A) + if(ricochets > ricochets_max) //safety thing, we don't care about what the other thing says about this. + return FALSE + var/them = A.check_projectile_ricochet(src) + switch(them) + if(PROJECTILE_RICOCHET_PREVENT) + return FALSE + if(PROJECTILE_RICOCHET_FORCE) + return TRUE + if(PROJECTILE_RICOCHET_NO) + return FALSE + if(PROJECTILE_RICOCHET_YES) + var/chance = ricochet_chance * A.ricochet_chance_mod + if(firer && HAS_TRAIT(firer, TRAIT_NICE_SHOT)) + chance += NICE_SHOT_RICOCHET_BONUS + if(prob(chance)) + return TRUE + else + CRASH("Invalid return value for projectile ricochet check from [A].") + +/obj/item/projectile/proc/check_ricochet_flag(atom/A) + if((flag in list("energy", "laser")) && (A.flags_ricochet & RICOCHET_SHINY)) + return TRUE + if((flag in list("bomb", "bullet")) && (A.flags_ricochet & RICOCHET_HARD)) + return TRUE + return FALSE + /// one move is a tile. /obj/item/projectile/proc/return_predicted_turf_after_moves(moves, forced_angle) //I say predicted because there's no telling that the projectile won't change direction/location in flight. if(!trajectory && isnull(forced_angle) && isnull(Angle)) @@ -418,6 +460,8 @@ /obj/item/projectile/proc/fire(angle, atom/direct_target) if(fired_from) SEND_SIGNAL(fired_from, COMSIG_PROJECTILE_BEFORE_FIRE, src, original) //If no angle needs to resolve it from xo/yo! + if(shrapnel_type) + AddElement(/datum/element/embed, projectile_payload = shrapnel_type) if(!log_override && firer && original) log_combat(firer, original, "fired at", src, "from [get_area_name(src, TRUE)]") if(direct_target) diff --git a/code/modules/projectiles/projectile/bullets/revolver.dm b/code/modules/projectiles/projectile/bullets/revolver.dm index c5c649a57e..5643804ac1 100644 --- a/code/modules/projectiles/projectile/bullets/revolver.dm +++ b/code/modules/projectiles/projectile/bullets/revolver.dm @@ -15,6 +15,38 @@ /obj/item/projectile/bullet/c38 name = ".38 bullet" damage = 25 + ricochets_max = 2 + ricochet_chance = 50 + ricochet_auto_aim_angle = 10 + ricochet_auto_aim_range = 3 + +/obj/item/projectile/bullet/c38/match + name = ".38 Match bullet" + ricochets_max = 4 + ricochet_chance = 100 + ricochet_auto_aim_angle = 40 + ricochet_auto_aim_range = 5 + ricochet_incidence_leeway = 50 + ricochet_decay_chance = 1 + ricochet_decay_damage = 1 + +/obj/item/projectile/bullet/c38/match/bouncy + name = ".38 Rubber bullet" + damage = 10 + stamina = 30 + armour_penetration = -30 + ricochets_max = 6 + ricochet_incidence_leeway = 70 + ricochet_chance = 130 + ricochet_decay_damage = 0.8 + shrapnel_type = NONE + +/obj/item/projectile/bullet/c38/dumdum + name = ".38 DumDum bullet" + damage = 15 + armour_penetration = -30 + ricochets_max = 0 + shrapnel_type = /obj/item/shrapnel/bullet/c38/dumdum /obj/item/projectile/bullet/c38/rubber name = ".38 rubber bullet" @@ -24,6 +56,7 @@ /obj/item/projectile/bullet/c38/trac name = ".38 TRAC bullet" damage = 10 + ricochets_max = 0 /obj/item/projectile/bullet/c38/trac/on_hit(atom/target, blocked = FALSE) . = ..() @@ -39,6 +72,7 @@ /obj/item/projectile/bullet/c38/hotshot //similar to incendiary bullets, but do not leave a flaming trail name = ".38 Hot Shot bullet" damage = 20 + ricochets_max = 0 /obj/item/projectile/bullet/c38/hotshot/on_hit(atom/target, blocked = FALSE) . = ..() @@ -51,6 +85,7 @@ name = ".38 Iceblox bullet" damage = 20 var/temperature = 100 + ricochets_max = 0 /obj/item/projectile/bullet/c38/iceblox/on_hit(atom/target, blocked = FALSE) . = ..() @@ -68,4 +103,14 @@ /obj/item/projectile/bullet/a357/ap name = ".357 armor-piercing bullet" damage = 45 - armour_penetration = 45 \ No newline at end of file + armour_penetration = 45 + +// admin only really, for ocelot memes +/obj/item/projectile/bullet/a357/match + name = ".357 match bullet" + ricochets_max = 5 + ricochet_chance = 140 + ricochet_auto_aim_angle = 50 + ricochet_auto_aim_range = 6 + ricochet_incidence_leeway = 80 + ricochet_decay_chance = 1 \ No newline at end of file diff --git a/code/modules/research/designs/misc_designs.dm b/code/modules/research/designs/misc_designs.dm index c59ee3ec61..0a1fde30c2 100644 --- a/code/modules/research/designs/misc_designs.dm +++ b/code/modules/research/designs/misc_designs.dm @@ -762,6 +762,41 @@ category = list("Equipment") departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING +///////////////////////////////////////// +/////////////////Tape//////////////////// +///////////////////////////////////////// + +/datum/design/sticky_tape + name = "Sticky Tape" + id = "sticky_tape" + build_type = PROTOLATHE + materials = list(/datum/material/plastic = 500) + build_path = /obj/item/stack/sticky_tape + category = list("Equipment") + departmental_flags = DEPARTMENTAL_FLAG_SERVICE + +/datum/design/super_sticky_tape + name = "Super Sticky Tape" + id = "super_sticky_tape" + build_type = PROTOLATHE + materials = list(/datum/material/plastic = 3000) + build_path = /obj/item/stack/sticky_tape/super + category = list("Equipment") + departmental_flags = DEPARTMENTAL_FLAG_SERVICE + +/datum/design/pointy_tape + name = "Pointy Tape" + id = "pointy_tape" + build_type = PROTOLATHE + materials = list(/datum/material/iron = 1500, /datum/material/plastic = 1000) + build_path = /obj/item/stack/sticky_tape/pointy + category = list("Equipment") + departmental_flags = DEPARTMENTAL_FLAG_SERVICE + +///////////////////////////////////////// +/////////////////Shuttle Upgrades//////// +///////////////////////////////////////// + /datum/design/shuttle_speed_upgrade name = "Shuttle Route Optimisation Upgrade" desc = "A disk that allows for calculating shorter routes when inserted into a flight control console." @@ -790,4 +825,4 @@ materials = list(/datum/material/iron = 1000, /datum/material/glass = 1000) build_path = /obj/item/shuttle_route_optimisation/void category = list("Equipment") - departmental_flags = DEPARTMENTAL_FLAG_SCIENCE | DEPARTMENTAL_FLAG_ENGINEERING \ No newline at end of file + departmental_flags = DEPARTMENTAL_FLAG_SCIENCE | DEPARTMENTAL_FLAG_ENGINEERING diff --git a/code/modules/research/techweb/nodes/bepis_nodes.dm b/code/modules/research/techweb/nodes/bepis_nodes.dm index 0033564274..41d3a08d64 100644 --- a/code/modules/research/techweb/nodes/bepis_nodes.dm +++ b/code/modules/research/techweb/nodes/bepis_nodes.dm @@ -80,3 +80,12 @@ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) hidden = TRUE experimental = TRUE + +/datum/techweb_node/sticky_advanced + id = "sticky_advanced" + display_name = "Advanced Sticky Technology" + description = "Taking a good joke too far? Nonsense!" + design_ids = list("super_sticky_tape", "pointy_tape") + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + hidden = TRUE + experimental = TRUE diff --git a/code/modules/research/techweb/nodes/misc_nodes.dm b/code/modules/research/techweb/nodes/misc_nodes.dm index d34afca90a..3f8ad8592a 100644 --- a/code/modules/research/techweb/nodes/misc_nodes.dm +++ b/code/modules/research/techweb/nodes/misc_nodes.dm @@ -66,3 +66,24 @@ design_ids = list("air_horn", "honker_main", "honker_peri", "honker_targ", "honk_chassis", "honk_head", "honk_torso", "honk_left_arm", "honk_right_arm", "honk_left_leg", "honk_right_leg", "mech_banana_mortar", "mech_mousetrap_mortar", "mech_honker", "mech_punching_face", "implant_trombone", "borg_transform_clown") research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + +////////////////////////Tape tech//////////////////////////// +/datum/techweb_node/sticky_basic + id = "sticky_basic" + display_name = "Basic Sticky Technology" + description = "The only thing left to do after researching this tech is to start printing out a bunch of 'kick me' signs." + prereq_ids = list("base") + design_ids = list("sticky_tape") + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) + hidden = TRUE + experimental = TRUE + +// Can be researched after getting the basic sticky technology from the BEPIS major reward +/datum/techweb_node/sticky_advanced + id = "sticky_advanced" + display_name = "Advanced Sticky Technology" + description = "Taking a good joke too far? Nonsense!" + prereq_ids = list("sticky_basic") + design_ids = list("super_sticky_tape", "pointy_tape") + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 5000) + hidden = TRUE \ No newline at end of file diff --git a/code/modules/spells/spell_types/summonitem.dm b/code/modules/spells/spell_types/summonitem.dm index 94de75faac..2f3ed64655 100644 --- a/code/modules/spells/spell_types/summonitem.dm +++ b/code/modules/spells/spell_types/summonitem.dm @@ -76,20 +76,6 @@ break M.dropItemToGround(item_to_retrieve) - if(iscarbon(M)) //Edge case housekeeping - var/mob/living/carbon/C = M - if(C.stomach_contents && (item_to_retrieve in C.stomach_contents)) - C.stomach_contents -= item_to_retrieve - for(var/X in C.bodyparts) - var/obj/item/bodypart/part = X - if(item_to_retrieve in part.embedded_objects) - part.embedded_objects -= item_to_retrieve - to_chat(C, "The [item_to_retrieve] that was embedded in your [L] has mysteriously vanished. How fortunate!") - if(!C.has_embedded_objects()) - C.clear_alert("embeddedobject") - SEND_SIGNAL(C, COMSIG_CLEAR_MOOD_EVENT, "embedded") - break - else if(istype(item_to_retrieve.loc, /obj/machinery/portable_atmospherics/)) //Edge cases for moved machinery var/obj/machinery/portable_atmospherics/P = item_to_retrieve.loc diff --git a/code/modules/surgery/bodyparts/helpers.dm b/code/modules/surgery/bodyparts/helpers.dm index a1f1bd7daf..29aca7166f 100644 --- a/code/modules/surgery/bodyparts/helpers.dm +++ b/code/modules/surgery/bodyparts/helpers.dm @@ -152,26 +152,24 @@ disabled += zone return disabled -//Remove all embedded objects from all limbs on the carbon mob +///Remove a specific embedded item from the carbon mob +/mob/living/carbon/proc/remove_embedded_object(obj/item/I) + SEND_SIGNAL(src, COMSIG_CARBON_EMBED_REMOVAL, I) + +///Remove all embedded objects from all limbs on the carbon mob /mob/living/carbon/proc/remove_all_embedded_objects() - var/turf/T = get_turf(src) - for(var/X in bodyparts) var/obj/item/bodypart/L = X for(var/obj/item/I in L.embedded_objects) - L.embedded_objects -= I - I.forceMove(T) - I.unembedded() + remove_embedded_object(I) - clear_alert("embeddedobject") - SEND_SIGNAL(src, COMSIG_CLEAR_MOOD_EVENT, "embedded") - -/mob/living/carbon/proc/has_embedded_objects() - . = 0 +/mob/living/carbon/proc/has_embedded_objects(include_harmless=FALSE) for(var/X in bodyparts) var/obj/item/bodypart/L = X for(var/obj/item/I in L.embedded_objects) - return 1 + if(!include_harmless && I.isEmbedHarmless()) + continue + return TRUE //Helper for quickly creating a new limb - used by augment code in species.dm spec_attacked_by diff --git a/code/modules/surgery/remove_embedded_object.dm b/code/modules/surgery/remove_embedded_object.dm index 7d7cd2df63..f974c39bb4 100644 --- a/code/modules/surgery/remove_embedded_object.dm +++ b/code/modules/surgery/remove_embedded_object.dm @@ -23,12 +23,7 @@ var/objects = 0 for(var/obj/item/I in L.embedded_objects) objects++ - I.forceMove(get_turf(H)) - L.embedded_objects -= I - I.unembedded() - if(!H.has_embedded_objects()) - H.clear_alert("embeddedobject") - SEND_SIGNAL(H, COMSIG_CLEAR_MOOD_EVENT, "embedded") + H.remove_embedded_object(I) if(objects > 0) display_results(user, target, "You successfully remove [objects] objects from [H]'s [L.name].", diff --git a/code/modules/uplink/uplink_items/uplink_ammo.dm b/code/modules/uplink/uplink_items/uplink_ammo.dm index 4a7e1f146e..ce74773f8d 100644 --- a/code/modules/uplink/uplink_items/uplink_ammo.dm +++ b/code/modules/uplink/uplink_items/uplink_ammo.dm @@ -189,6 +189,13 @@ cost = 4 include_modes = list(/datum/game_mode/nuclear) +/datum/uplink_item/ammo/machinegun/match + name = "7.12x82mm (Match) Box Magazine" + desc = "A 50-round magazine of 7.12x82mm ammunition for use in the L6 SAW; you didn't know there was a demand for match grade \ + precision bullet hose ammo, but these rounds are finely tuned and perfect for ricocheting off walls all fancy-like." + item = /obj/item/ammo_box/magazine/mm712x82/match + cost = 10 + /datum/uplink_item/ammo/machinegun cost = 6 surplus = 0 diff --git a/code/modules/vending/security.dm b/code/modules/vending/security.dm index 43f3a96d3d..35e4b3232a 100644 --- a/code/modules/vending/security.dm +++ b/code/modules/vending/security.dm @@ -22,6 +22,7 @@ /obj/item/clothing/suit/armor/vest/blueshirt = 1, /obj/item/clothing/under/rank/security/officer/blueshirt = 1, /obj/item/clothing/gloves/tackler = 5, + /obj/item/grenade/stingbang = 1, /obj/item/ssword_kit = 1, /obj/item/storage/bag/ammo = 2) armor = list("melee" = 100, "bullet" = 100, "laser" = 100, "energy" = 100, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 50) diff --git a/icons/obj/tapes.dmi b/icons/obj/tapes.dmi new file mode 100644 index 0000000000..a406a4dd1f Binary files /dev/null and b/icons/obj/tapes.dmi differ diff --git a/modular_citadel/code/modules/mob/living/carbon/human/human.dm b/modular_citadel/code/modules/mob/living/carbon/human/human.dm index 474c9d2823..47de66fcd1 100644 --- a/modular_citadel/code/modules/mob/living/carbon/human/human.dm +++ b/modular_citadel/code/modules/mob/living/carbon/human/human.dm @@ -16,7 +16,7 @@ if(!I || !L || I.loc != src || !(I in L.embedded_objects)) return L.embedded_objects -= I - L.receive_damage(I.embedding.embedded_unsafe_removal_pain_multiplier*I.w_class*painmul)//It hurts to rip it out, get surgery you dingus. And if you're ripping it out quickly via resist, it's gonna hurt even more + L.receive_damage(I.embedding["remove_pain_mult"]*I.w_class*painmul)//It hurts to rip it out, get surgery you dingus. And if you're ripping it out quickly via resist, it's gonna hurt even more I.forceMove(get_turf(src)) I.unembedded() user.put_in_hands(I) diff --git a/tgstation.dme b/tgstation.dme index 4bb0fb13e6..51837a03ef 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -358,7 +358,6 @@ #include "code\datums\datumvars.dm" #include "code\datums\dna.dm" #include "code\datums\dog_fashion.dm" -#include "code\datums\embedding_behavior.dm" #include "code\datums\emotes.dm" #include "code\datums\ert.dm" #include "code\datums\explosion.dm" @@ -407,6 +406,7 @@ #include "code\datums\components\dejavu.dm" #include "code\datums\components\earprotection.dm" #include "code\datums\components\edit_complainer.dm" +#include "code\datums\components\embedded.dm" #include "code\datums\components\explodable.dm" #include "code\datums\components\field_of_vision.dm" #include "code\datums\components\footstep.dm" @@ -421,11 +421,13 @@ #include "code\datums\components\magnetic_catch.dm" #include "code\datums\components\material_container.dm" #include "code\datums\components\mirage_border.dm" +#include "code\datums\components\mirv.dm" #include "code\datums\components\mood.dm" #include "code\datums\components\nanites.dm" #include "code\datums\components\ntnet_interface.dm" #include "code\datums\components\orbiter.dm" #include "code\datums\components\paintable.dm" +#include "code\datums\components\pellet_cloud.dm" #include "code\datums\components\phantomthief.dm" #include "code\datums\components\rad_insulation.dm" #include "code\datums\components\radioactive.dm" @@ -433,7 +435,6 @@ #include "code\datums\components\riding.dm" #include "code\datums\components\rotation.dm" #include "code\datums\components\shielded.dm" -#include "code\datums\components\shrapnel.dm" #include "code\datums\components\shrink.dm" #include "code\datums\components\sizzle.dm" #include "code\datums\components\slippery.dm" @@ -538,6 +539,7 @@ #include "code\datums\elements\dusts_on_leaving_area.dm" #include "code\datums\elements\dwarfism.dm" #include "code\datums\elements\earhealing.dm" +#include "code\datums\elements\embed.dm" #include "code\datums\elements\empprotection.dm" #include "code\datums\elements\firestacker.dm" #include "code\datums\elements\flavor_text.dm" @@ -999,6 +1001,7 @@ #include "code\game\objects\items\sharpener.dm" #include "code\game\objects\items\shields.dm" #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\stunbaton.dm" @@ -1101,6 +1104,7 @@ #include "code\game\objects\items\stacks\medical.dm" #include "code\game\objects\items\stacks\rods.dm" #include "code\game\objects\items\stacks\stack.dm" +#include "code\game\objects\items\stacks\tape.dm" #include "code\game\objects\items\stacks\telecrystal.dm" #include "code\game\objects\items\stacks\wrap.dm" #include "code\game\objects\items\stacks\sheets\glass.dm" @@ -1929,6 +1933,7 @@ #include "code\modules\events\wizard\blobies.dm" #include "code\modules\events\wizard\curseditems.dm" #include "code\modules\events\wizard\departmentrevolt.dm" +#include "code\modules\events\wizard\embeddies.dm" #include "code\modules\events\wizard\fakeexplosion.dm" #include "code\modules\events\wizard\ghost.dm" #include "code\modules\events\wizard\greentext.dm"