diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index 684adf17300..e5e65a067d8 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -288,8 +288,10 @@ ///called in /obj/item/gun/fire_gun (user, target, flag, params) #define COMSIG_GUN_TRY_FIRE "gun_try_fire" #define COMPONENT_CANCEL_GUN_FIRE (1<<0) -///called in /obj/item/gun/process_fire (src, target, params, zone_override) +///called in /obj/item/gun/process_fire (src, target, params, zone_override, bonus_spread_values) #define COMSIG_MOB_FIRED_GUN "mob_fired_gun" + #define MIN_BONUS_SPREAD_INDEX 1 + #define MAX_BONUS_SPREAD_INDEX 2 ///called in /obj/item/gun/process_fire (user, target, params, zone_override) #define COMSIG_GUN_FIRED "gun_fired" ///called in /obj/item/gun/process_chamber (src) diff --git a/code/__DEFINES/materials.dm b/code/__DEFINES/materials.dm index 6ffcc733928..f58ec028dbb 100644 --- a/code/__DEFINES/materials.dm +++ b/code/__DEFINES/materials.dm @@ -61,6 +61,10 @@ #define MATERIAL_SOURCE(mat) "[mat.name]_material" +///Special return values of [/datum/component/material_container/insert_item] +#define MATERIAL_INSERT_ITEM_NO_MATS -1 +#define MATERIAL_INSERT_ITEM_NO_SPACE -2 +#define MATERIAL_INSERT_ITEM_FAILURE 0 // Slowdown values. diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 8ca1e485263..7261cacc66f 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -486,6 +486,11 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_HATED_BY_DOGS "hated_by_dogs" /// Mobs with this trait will not be immobilized when held up #define TRAIT_NOFEAR_HOLDUPS "no_fear_holdup" +/// Mobs with this trait won't be able to dual wield guns. +#define TRAIT_NO_GUN_AKIMBO "no_gun_akimbo" + +/// Projectile with this trait will always hit the defined zone of a struck living mob. +#define TRAIT_ALWAYS_HIT_ZONE "always_hit_zone" // METABOLISMS // Various jobs on the station have historically had better reactions @@ -656,7 +661,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_SELF_AWARE "self_aware" #define TRAIT_FREERUNNING "freerunning" #define TRAIT_SKITTISH "skittish" -#define TRAIT_POOR_AIM "poor_aim" #define TRAIT_PROSOPAGNOSIA "prosopagnosia" #define TRAIT_TAGGER "tagger" #define TRAIT_PHOTOGRAPHER "photographer" diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm index ea14400d5d4..9b82322e99a 100644 --- a/code/_globalvars/traits.dm +++ b/code/_globalvars/traits.dm @@ -156,7 +156,6 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_SELF_AWARE" = TRAIT_SELF_AWARE, "TRAIT_FREERUNNING" = TRAIT_FREERUNNING, "TRAIT_SKITTISH" = TRAIT_SKITTISH, - "TRAIT_POOR_AIM" = TRAIT_POOR_AIM, "TRAIT_PROSOPAGNOSIA" = TRAIT_PROSOPAGNOSIA, "TRAIT_TAGGER" = TRAIT_TAGGER, "TRAIT_PHOTOGRAPHER" = TRAIT_PHOTOGRAPHER, diff --git a/code/datums/components/material_container.dm b/code/datums/components/material_container.dm index 404b2e99709..27a85480c13 100644 --- a/code/datums/components/material_container.dm +++ b/code/datums/components/material_container.dm @@ -30,11 +30,22 @@ var/datum/callback/precondition /// A callback invoked after materials are inserted into this container var/datum/callback/after_insert + /// A callback invoked after sheets are retrieve from this container + var/datum/callback/after_retrieve /// The material container flags. See __DEFINES/materials.dm. var/mat_container_flags /// Sets up the proper signals and fills the list of materials with the appropriate references. -/datum/component/material_container/Initialize(list/init_mats, max_amt = 0, _mat_container_flags=NONE, list/allowed_mats=init_mats, list/allowed_items, datum/callback/_insertion_check, datum/callback/_precondition, datum/callback/_after_insert) +/datum/component/material_container/Initialize(list/init_mats, + max_amt = 0, + _mat_container_flags=NONE, + list/allowed_mats=init_mats, + list/allowed_items, + datum/callback/_insertion_check, + datum/callback/_precondition, + datum/callback/_after_insert, + datum/callback/_after_retrieve) + if(!isatom(parent)) return COMPONENT_INCOMPATIBLE @@ -52,6 +63,7 @@ insertion_check = _insertion_check precondition = _precondition after_insert = _after_insert + after_retrieve = _after_retrieve for(var/mat in init_mats) //Make the assoc list material reference -> amount var/mat_ref = GET_MATERIAL_REF(mat) @@ -79,6 +91,8 @@ QDEL_NULL(precondition) if(after_insert) QDEL_NULL(after_insert) + if(after_retrieve) + QDEL_NULL(after_retrieve) return ..() @@ -240,40 +254,40 @@ /// Proc specifically for inserting items, returns the amount of materials entered. /datum/component/material_container/proc/insert_item(obj/item/weapon, multiplier = 1, breakdown_flags = mat_container_flags) if(QDELETED(weapon)) - return -1 + return MATERIAL_INSERT_ITEM_NO_MATS multiplier = CEILING(multiplier, 0.01) var/obj/item/target = weapon var/material_amount = get_item_material_amount(target, breakdown_flags) * multiplier if(!material_amount) - return -1 + return MATERIAL_INSERT_ITEM_NO_MATS var/obj/item/stack/item_stack if(isstack(weapon) && !has_space(material_amount)) //not enugh space split and feed as many sheets possible item_stack = weapon var/space_left = max_amount - total_amount if(!space_left) - return -2 + return MATERIAL_INSERT_ITEM_NO_SPACE var/material_per_sheet = material_amount / item_stack.amount var/sheets_to_insert = round(space_left / material_per_sheet) if(!sheets_to_insert) - return -2 + return MATERIAL_INSERT_ITEM_NO_SPACE target = split_stack(item_stack, sheets_to_insert) material_amount = get_item_material_amount(target, breakdown_flags) * multiplier if(!has_space(material_amount)) - return -2 + return MATERIAL_INSERT_ITEM_NO_SPACE last_inserted_id = insert_item_materials(target, multiplier, breakdown_flags) if(!isnull(last_inserted_id)) if(after_insert) - after_insert.Invoke(target, last_inserted_id, material_amount) + after_insert.Invoke(target, last_inserted_id, material_amount, src) qdel(target) //item gone return material_amount else if(!isnull(item_stack) && item_stack != target) //insertion failed, merge the split stack back into the original var/obj/item/stack/inserting_stack = target item_stack.add(inserting_stack.amount) qdel(inserting_stack) - return 0 + return MATERIAL_INSERT_ITEM_FAILURE /** * Inserts the relevant materials from an item into this material container. @@ -406,8 +420,8 @@ return total_amount_save - total_amount /// For spawning mineral sheets at a specific location. Used by machines to output sheets. -/datum/component/material_container/proc/retrieve_sheets(sheet_amt, datum/material/M, atom/target = null) - if(!M.sheet_type) +/datum/component/material_container/proc/retrieve_sheets(sheet_amt, datum/material/material, atom/target = null) + if(!material.sheet_type) return 0 //Add greyscale sheet handling here later if(sheet_amt <= 0) return 0 @@ -415,18 +429,20 @@ if(!target) var/atom/parent_atom = parent target = parent_atom.drop_location() - if(materials[M] < (sheet_amt * SHEET_MATERIAL_AMOUNT)) - sheet_amt = round(materials[M] / SHEET_MATERIAL_AMOUNT) + if(materials[material] < (sheet_amt * SHEET_MATERIAL_AMOUNT)) + sheet_amt = round(materials[material] / SHEET_MATERIAL_AMOUNT) var/count = 0 while(sheet_amt > MAX_STACK_SIZE) - new M.sheet_type(target, MAX_STACK_SIZE, null, list((M) = SHEET_MATERIAL_AMOUNT)) + var/obj/item/stack/sheet/new_sheets = new material.sheet_type(target, MAX_STACK_SIZE, null, list((material) = SHEET_MATERIAL_AMOUNT)) + after_retrieve?.Invoke(new_sheets) count += MAX_STACK_SIZE - use_amount_mat(sheet_amt * SHEET_MATERIAL_AMOUNT, M) + use_amount_mat(sheet_amt * SHEET_MATERIAL_AMOUNT, material) sheet_amt -= MAX_STACK_SIZE if(sheet_amt >= 1) - new M.sheet_type(target, sheet_amt, null, list((M) = SHEET_MATERIAL_AMOUNT)) + var/obj/item/stack/sheet/new_sheets = new material.sheet_type(target, sheet_amt, null, list((material) = SHEET_MATERIAL_AMOUNT)) + after_retrieve?.Invoke(new_sheets) count += sheet_amt - use_amount_mat(sheet_amt * SHEET_MATERIAL_AMOUNT, M) + use_amount_mat(sheet_amt * SHEET_MATERIAL_AMOUNT, material) return count diff --git a/code/datums/quirks/negative_quirks.dm b/code/datums/quirks/negative_quirks.dm index 648043ec41a..33ec7178c4b 100644 --- a/code/datums/quirks/negative_quirks.dm +++ b/code/datums/quirks/negative_quirks.dm @@ -534,11 +534,21 @@ desc = "You've never hit anything you were aiming for in your life." icon = FA_ICON_BULLSEYE value = -4 - mob_trait = TRAIT_POOR_AIM medical_record_text = "Patient possesses a strong tremor in both hands." hardcore_value = 3 mail_goodies = list(/obj/item/cardboard_cutout) // for target practice +/datum/quirk/poor_aim/add(client/client_source) + RegisterSignal(quirk_holder, COMSIG_MOB_FIRED_GUN, PROC_REF(on_mob_fired_gun)) + +/datum/quirk/poor_aim/remove(client/client_source) + UnregisterSignal(quirk_holder, COMSIG_MOB_FIRED_GUN) + +/datum/quirk/poor_aim/proc/on_mob_fired_gun(mob/user, obj/item/gun/gun_fired, target, params, zone_override, list/bonus_spread_values) + SIGNAL_HANDLER + bonus_spread_values[MIN_BONUS_SPREAD_INDEX] += 10 + bonus_spread_values[MAX_BONUS_SPREAD_INDEX] += 35 + /datum/quirk/prosopagnosia name = "Prosopagnosia" desc = "You have a mental disorder that prevents you from being able to recognize faces at all." diff --git a/code/game/objects/effects/spawners/random/mod.dm b/code/game/objects/effects/spawners/random/mod.dm index 7b5c3cc5852..bcd2eaa3ff2 100644 --- a/code/game/objects/effects/spawners/random/mod.dm +++ b/code/game/objects/effects/spawners/random/mod.dm @@ -12,7 +12,8 @@ /obj/item/mod/module/balloon = 1, /obj/item/mod/module/paper_dispenser = 1, /obj/item/mod/module/hat_stabilizer = 2, - /obj/item/mod/module/stamp = 1 + /obj/item/mod/module/stamp = 1, + /obj/item/mod/module/recycler/donk/safe = 1, ) /obj/effect/spawner/random/mod/maint/Initialize(mapload) diff --git a/code/modules/cargo/markets/market_items/misc.dm b/code/modules/cargo/markets/market_items/misc.dm index d5ee60f49e4..d0ba6b26e9f 100644 --- a/code/modules/cargo/markets/market_items/misc.dm +++ b/code/modules/cargo/markets/market_items/misc.dm @@ -41,6 +41,15 @@ stock_max = 8 availability_prob = 60 +/datum/market_item/misc/donk_recycler + name = "MOD Riot Foam Dart Recycler Module" + desc = "If you love toy guns, hate cleaning and got a MODsuit, this module is a must have." + item = /obj/item/mod/module/recycler/donk + price_min = CARGO_CRATE_VALUE * 2 + price_max = CARGO_CRATE_VALUE * 4.5 + stock_max = 2 + availability_prob = 30 + /datum/market_item/misc/holywater name = "Flask of holy water" desc = "Father Lootius' own brand of ready-made holy water." diff --git a/code/modules/mod/modules/modules_general.dm b/code/modules/mod/modules/modules_general.dm index 65f7eb7c191..8054ccdc78c 100644 --- a/code/modules/mod/modules/modules_general.dm +++ b/code/modules/mod/modules/modules_general.dm @@ -596,3 +596,181 @@ /obj/item/mod/module/signlang_radio/on_suit_deactivation(deleting = FALSE) REMOVE_TRAIT(mod.wearer, TRAIT_CAN_SIGN_ON_COMMS, MOD_TRAIT) + +///A module that recharges the suit by an itsy tiny bit whenever the user takes a step. Originally called "magneto module" but the videogame reference sounds cooler. +/obj/item/mod/module/joint_torsion + name = "MOD joint torsion ratchet module" + desc = "A compact, weak AC generator that charges the suit's internal cell through the power of deambulation. It doesn't work in zero G." + icon_state = "joint_torsion" + complexity = 1 + incompatible_modules = list(/obj/item/mod/module/joint_torsion) + var/power_per_step = DEFAULT_CHARGE_DRAIN * 0.3 + +/obj/item/mod/module/joint_torsion/on_suit_activation() + if(!(mod.wearer.movement_type & (FLOATING|FLYING))) + RegisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved)) + /// This way we don't even bother to call on_moved() while flying/floating + RegisterSignal(mod.wearer, COMSIG_MOVETYPE_FLAG_ENABLED, PROC_REF(on_movetype_flag_enabled)) + RegisterSignal(mod.wearer, COMSIG_MOVETYPE_FLAG_DISABLED, PROC_REF(on_movetype_flag_disabled)) + +/obj/item/mod/module/joint_torsion/on_suit_deactivation(deleting = FALSE) + UnregisterSignal(mod.wearer, list(COMSIG_MOVABLE_MOVED, COMSIG_MOVETYPE_FLAG_ENABLED, COMSIG_MOVETYPE_FLAG_DISABLED)) + +/obj/item/mod/module/joint_torsion/proc/on_movetype_flag_enabled(datum/source, flag, old_state) + SIGNAL_HANDLER + if(!(old_state & (FLOATING|FLYING)) && flag & (FLOATING|FLYING)) + UnregisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED) + +/obj/item/mod/module/joint_torsion/proc/on_movetype_flag_disabled(datum/source, flag, old_state) + SIGNAL_HANDLER + if(old_state & (FLOATING|FLYING) && !(mod.wearer.movement_type & (FLOATING|FLYING))) + RegisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved)) + +/obj/item/mod/module/joint_torsion/proc/on_moved(mob/living/carbon/human/wearer, atom/old_loc, movement_dir, forced) + SIGNAL_HANDLER + //Shouldn't work if the wearer isn't really walking/running around. + if(forced || wearer.throwing || wearer.body_position == LYING_DOWN || wearer.buckled || CHECK_MOVE_LOOP_FLAGS(wearer, MOVEMENT_LOOP_OUTSIDE_CONTROL)) + return + mod.core.add_charge(power_per_step) + +/// Module that shoves garbage inside its material container when the user crosses it, and eject the recycled material with MMB. +/obj/item/mod/module/recycler + name = "MOD recycler module" + desc = "An innovative garbage collection module that recycles gathered trash into usable material. \ + Doesn't work on debris and some items. May recycle live ammunition. \ + Activate on a nearby turf or storage to unload stored material." + icon_state = "recycler" + module_type = MODULE_ACTIVE + active_power_cost = DEFAULT_CHARGE_DRAIN * 0.5 + complexity = 2 + incompatible_modules = list(/obj/item/mod/module/recycler) + overlay_state_inactive = "module_recycler" + overlay_state_active = "module_recycler" + /// A multiplier of the amount of material extracted from the item + var/efficiency = 1 + /// Items that will be collected + var/list/allowed_item_types = list( + /obj/item/trash, + /obj/item/shard, + /obj/item/light, + /obj/item/broken_bottle, + /obj/item/ammo_casing, + /obj/item/cigbutt, + ) + /// Materials that will be extracted. + var/list/accepted_mats = list( + /datum/material/iron, + /datum/material/glass, + /datum/material/silver, + /datum/material/plasma, + /datum/material/gold, + /datum/material/diamond, + /datum/material/plastic, + /datum/material/uranium, + /datum/material/bananium, + /datum/material/titanium, + /datum/material/bluespace, + ) + var/static/list/loc_connections = list( + COMSIG_ATOM_ENTERED = PROC_REF(on_obj_entered), + COMSIG_ATOM_INITIALIZED_ON = PROC_REF(on_atom_initialized_on), + ) + var/datum/component/connect_loc_behalf/connector + +/obj/item/mod/module/recycler/Initialize(mapload) + . = ..() + AddComponent(/datum/component/material_container, accepted_mats, 50 * SHEET_MATERIAL_AMOUNT, MATCONTAINER_EXAMINE|MATCONTAINER_NO_INSERT, _after_retrieve=CALLBACK(src, PROC_REF(attempt_insert_storage))) + +/obj/item/mod/module/recycler/on_activation() + . = ..() + if(!.) + return + connector = AddComponent(/datum/component/connect_loc_behalf, mod.wearer, loc_connections) + RegisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED, PROC_REF(on_wearer_moved)) + +/obj/item/mod/module/recycler/on_deactivation(display_message, deleting = FALSE) + . = ..() + if(!.) + return + QDEL_NULL(connector) + UnregisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED, PROC_REF(on_wearer_moved)) + +/obj/item/mod/module/recycler/proc/on_wearer_moved(datum/source, atom/old_loc, dir, forced) + SIGNAL_HANDLER + for(var/obj/item/item in mod.wearer.loc) + if(!is_type_in_list(item, allowed_item_types)) + return + insert_trash(item) + +/obj/item/mod/module/recycler/proc/on_obj_entered(atom/new_loc, atom/movable/arrived, atom/old_loc) + SIGNAL_HANDLER + if(!is_type_in_list(arrived, allowed_item_types)) + return + insert_trash(arrived) + +/obj/item/mod/module/recycler/proc/on_atom_initialized_on(atom/loc, atom/new_atom) + SIGNAL_HANDLER + if(!is_type_in_list(new_atom, allowed_item_types)) + return + //Give the new atom the time to fully initialize and maybe live if the wearer moves away. + addtimer(CALLBACK(src, TYPE_PROC_REF(/obj/item/mod/module/recycler, insert_trash_if_nearby), new_atom), 0.5 SECONDS) + +/obj/item/mod/module/recycler/proc/insert_trash_if_nearby(atom/new_atom) + if(new_atom && mod?.wearer && new_atom.loc == mod.wearer.loc) + insert_trash(new_atom) + +/obj/item/mod/module/recycler/proc/insert_trash(obj/item/item) + var/datum/component/material_container/container = GetComponent(/datum/component/material_container) + var/retrieved = container.insert_item(item, multiplier = efficiency, breakdown_flags = BREAKDOWN_FLAGS_RECYCLER) + if(retrieved == MATERIAL_INSERT_ITEM_NO_MATS) //even if it doesn't have any material to give, trash is trash. + qdel(item) + playsound(src, SFX_RUSTLE, 50, TRUE, -5) + +/obj/item/mod/module/recycler/on_select_use(atom/target) + . = ..() + if(!.) + return + if(!target?.atom_storage) + target = get_turf(target) + if(!isopenturf(target) || !mod.wearer.Adjacent(target)) + return FALSE + dispense(target) + +/obj/item/mod/module/recycler/proc/dispense(atom/target) + var/datum/component/material_container/container = GetComponent(/datum/component/material_container) + if(container.retrieve_all(target)) + balloon_alert(mod.wearer, "material dispensed") + playsound(src, 'sound/machines/microwave/microwave-end.ogg', 50, TRUE) + return + balloon_alert(mod.wearer, "not enough material") + playsound(src, 'sound/machines/buzz-sigh.ogg', 50, TRUE) + +/obj/item/mod/module/recycler/proc/attempt_insert_storage(obj/item/to_drop) + if(!isturf(to_drop.loc) && !to_drop.loc.atom_storage?.attempt_insert(to_drop, mod.wearer, override = TRUE)) + to_drop.forceMove(to_drop.loc.drop_location()) + +///A black market variant of the above that dispenses riot foam dart boxes +/obj/item/mod/module/recycler/donk + name = "MOD riot foam dart recycler module" + desc = "A mod module collects and repackages fired foam darts (and garbage) into half-sized boxes of riot foam darts. \ + Activate on a nearby turf or storage to unload stored ammo boxes." + icon_state = "donk_recycler" + overlay_state_inactive = "module_donk_recycler" + overlay_state_active = "module_donk_recycler" + efficiency = 0.7 // Stops getting as many riot foam darts as one consumes. + accepted_mats = list(/datum/material/iron) + ///The type of ammo box that it dispenses + var/ammobox_type = /obj/item/ammo_box/foambox/riot/mini + ///The cost of each dispensed ammo box + var/required_amount = SHEET_MATERIAL_AMOUNT*12.5 + +/obj/item/mod/module/recycler/donk/dispense(atom/target) + var/datum/component/material_container/container = GetComponent(/datum/component/material_container) + if(!container.use_amount_mat(required_amount, /datum/material/iron)) + balloon_alert(mod.wearer, "not enough material") + playsound(src, 'sound/machines/buzz-sigh.ogg', 50, TRUE) + return + var/obj/item/ammo_box/product = new ammobox_type(target) + attempt_insert_storage(product) + balloon_alert(mod.wearer, "ammo box dispensed.") + playsound(src, 'sound/machines/microwave/microwave-end.ogg', 50, TRUE) diff --git a/code/modules/mod/modules/modules_maint.dm b/code/modules/mod/modules/modules_maint.dm index 9829321f640..b973beeb603 100644 --- a/code/modules/mod/modules/modules_maint.dm +++ b/code/modules/mod/modules/modules_maint.dm @@ -334,3 +334,16 @@ QDEL_IN(mod.wearer, FLY_TIME) #undef FLY_TIME + +/obj/item/mod/module/recycler/donk/safe + name = "MOD foam dart recycler module" + desc = "A mod module that collects and repackages fired foam darts into half-sized ammo boxes. \ + Activate on a nearby turf or storage to unload stored ammo boxes." + icon_state = "donk_safe_recycler" + overlay_state_inactive = "module_donk_safe_recycler" + overlay_state_active = "module_donk_safe_recycler" + complexity = 1 + efficiency = 1 + allowed_item_types = list(/obj/item/ammo_casing/caseless/foam_dart) + ammobox_type = /obj/item/ammo_box/foambox/mini + required_amount = SMALL_MATERIAL_AMOUNT*2.5 diff --git a/code/modules/mod/modules/modules_security.dm b/code/modules/mod/modules/modules_security.dm index a861f5f639e..4c58f516785 100644 --- a/code/modules/mod/modules/modules_security.dm +++ b/code/modules/mod/modules/modules_security.dm @@ -385,3 +385,116 @@ creatures_detected++ playsound(mod.wearer, 'sound/effects/ping_hit.ogg', vol = 75, vary = TRUE, extrarange = MEDIUM_RANGE_SOUND_EXTRARANGE) // Should be audible for the radius of the sonar to_chat(mod.wearer, span_notice("You slam your fist into the ground, sending out a sonic wave that detects [creatures_detected] living beings nearby!")) + +#define SHOOTING_ASSISTANT_OFF "Currently Off" +#define STORMTROOPER_MODE "Quick Fire Stormtrooper" +#define SHARPSHOOTER_MODE "Slow Ricochet Sharpshooter" + +/** + * A module that enhances the user's ability with firearms, with a couple drawbacks: + * In 'Stormtrooper' mode, the user will be given faster firerate, but lower accuracy. + * In 'Sharpshooter' mode, the user will have better accuracy and ricochet to his shots, but slower movement speed. + * Both modes prevent the user from dual wielding guns. + */ +/obj/item/mod/module/shooting_assistant + name = "MOD shooting assistant module" + desc = "A botched prototype meant to boost the TGMC crayon eaters' ability with firearms. \ + It has only two modes available in its configurations: \ + 'Quick Fire Stormtrooper' and 'Slow Ricochet Sharpshooter', \ + both incompatible with dual wielding firearms." + icon_state = "shooting_assistant" + module_type = MODULE_PASSIVE + complexity = 3 + incompatible_modules = list(/obj/item/mod/module/shooting_assistant) + var/selected_mode = SHOOTING_ASSISTANT_OFF + ///Association list, the assoc values are the balloon alerts shown to the user when the mode is set. + var/static/list/available_modes = list( + SHOOTING_ASSISTANT_OFF = "assistant off", + STORMTROOPER_MODE = "stormtrooper mode", + SHARPSHOOTER_MODE = "sharpshooter mode", + ) + +/obj/item/mod/module/shooting_assistant/get_configuration() + . = ..() + .["shooting_mode"] = add_ui_configuration("Mode", "list", selected_mode, assoc_to_keys(available_modes)) + +/obj/item/mod/module/shooting_assistant/configure_edit(key, value) + switch(key) + if("shooting_mode") + set_shooting_mode(value) + +/obj/item/mod/module/shooting_assistant/proc/set_shooting_mode(new_mode) + if(new_mode == selected_mode || !mod.active) + return + if(new_mode != SHOOTING_ASSISTANT_OFF && !mod.get_charge()) + balloon_alert(mod.wearer, "no charge!") + playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE) + return + + //Remove the effects of the previously selected mode + if(mod.active) + remove_mode_effects() + + balloon_alert(mod.wearer, available_modes[new_mode]) + selected_mode = new_mode + + //Apply the effects of the new mode + if(mod.active) + apply_mode_effects() + +/obj/item/mod/module/shooting_assistant/proc/apply_mode_effects() + switch(selected_mode) + if(SHOOTING_ASSISTANT_OFF) + idle_power_cost = 0 + if(STORMTROOPER_MODE) + idle_power_cost = DEFAULT_CHARGE_DRAIN * 0.4 + mod.wearer.add_traits(list(TRAIT_NO_GUN_AKIMBO, TRAIT_DOUBLE_TAP), MOD_TRAIT) + RegisterSignal(mod.wearer, COMSIG_MOB_FIRED_GUN, PROC_REF(stormtrooper_fired_gun)) + if(SHARPSHOOTER_MODE) + idle_power_cost = DEFAULT_CHARGE_DRAIN * 0.6 + mod.wearer.add_traits(list(TRAIT_NO_GUN_AKIMBO, TRAIT_NICE_SHOT), MOD_TRAIT) + RegisterSignal(mod.wearer, COMSIG_MOB_FIRED_GUN, PROC_REF(sharpshooter_fired_gun)) + RegisterSignal(mod.wearer, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE, PROC_REF(apply_ricochet)) + mod.wearer.add_movespeed_modifier(/datum/movespeed_modifier/shooting_assistant) + +/obj/item/mod/module/shooting_assistant/proc/remove_mode_effects() + switch(selected_mode) + if(STORMTROOPER_MODE) + UnregisterSignal(mod.wearer, COMSIG_MOB_FIRED_GUN) + mod.wearer.remove_traits(list(TRAIT_NO_GUN_AKIMBO, TRAIT_DOUBLE_TAP), MOD_TRAIT) + if(SHARPSHOOTER_MODE) + UnregisterSignal(mod.wearer, list(COMSIG_MOB_FIRED_GUN, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE)) + mod.wearer.remove_traits(list(TRAIT_NO_GUN_AKIMBO, TRAIT_NICE_SHOT), MOD_TRAIT) + mod.wearer.remove_movespeed_modifier(/datum/movespeed_modifier/shooting_assistant) + +/obj/item/mod/module/shooting_assistant/drain_power(amount) + . = ..() + if(!.) + set_shooting_mode(SHOOTING_ASSISTANT_OFF) + +/obj/item/mod/module/shooting_assistant/on_suit_activation() + apply_mode_effects() + +/obj/item/mod/module/shooting_assistant/on_suit_deactivation(deleting = FALSE) + remove_mode_effects() + +/obj/item/mod/module/shooting_assistant/proc/stormtrooper_fired_gun(mob/user, obj/item/gun/gun_fired, target, params, zone_override, list/bonus_spread_values) + SIGNAL_HANDLER + bonus_spread_values[MIN_BONUS_SPREAD_INDEX] += 15 + bonus_spread_values[MAX_BONUS_SPREAD_INDEX] += 25 + +/obj/item/mod/module/shooting_assistant/proc/sharpshooter_fired_gun(mob/user, obj/item/gun/gun_fired, target, params, zone_override, list/bonus_spread_values) + SIGNAL_HANDLER + bonus_spread_values[MIN_BONUS_SPREAD_INDEX] -= 20 + bonus_spread_values[MAX_BONUS_SPREAD_INDEX] -= 10 + +/obj/item/mod/module/shooting_assistant/proc/apply_ricochet(mob/user, obj/projectile/projectile, datum/fired_from, atom/clicked_atom) + SIGNAL_HANDLER + projectile.ricochets_max += 1 + projectile.min_ricochets += 1 + projectile.ricochet_incidence_leeway = 0 //allows the projectile to bounce at any angle. + ADD_TRAIT(projectile, TRAIT_ALWAYS_HIT_ZONE, MOD_TRAIT) + +#undef SHOOTING_ASSISTANT_OFF +#undef STORMTROOPER_MODE +#undef SHARPSHOOTER_MODE diff --git a/code/modules/movespeed/modifiers/items.dm b/code/modules/movespeed/modifiers/items.dm index e1ec36c3a15..433200e3223 100644 --- a/code/modules/movespeed/modifiers/items.dm +++ b/code/modules/movespeed/modifiers/items.dm @@ -16,3 +16,6 @@ /datum/movespeed_modifier/sphere multiplicative_slowdown = -0.5 + +/datum/movespeed_modifier/shooting_assistant + multiplicative_slowdown = 0.5 diff --git a/code/modules/projectiles/boxes_magazines/ammo_boxes.dm b/code/modules/projectiles/boxes_magazines/ammo_boxes.dm index 87ec9e59bcf..f04df872798 100644 --- a/code/modules/projectiles/boxes_magazines/ammo_boxes.dm +++ b/code/modules/projectiles/boxes_magazines/ammo_boxes.dm @@ -113,7 +113,17 @@ max_ammo = 40 custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT*5) +/obj/item/ammo_box/foambox/mini + icon_state = "foambox_mini" + max_ammo = 20 + custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT*2.5) + /obj/item/ammo_box/foambox/riot icon_state = "foambox_riot" ammo_type = /obj/item/ammo_casing/caseless/foam_dart/riot custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT*25) + +/obj/item/ammo_box/foambox/riot/mini + icon_state = "foambox_riot_mini" + max_ammo = 20 + custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT*12.5) diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 30752d2868c..3de0aad078d 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -306,9 +306,8 @@ //DUAL (or more!) WIELDING var/bonus_spread = 0 var/loop_counter = 0 - if(ishuman(user) && user.combat_mode) - var/mob/living/carbon/human/H = user - for(var/obj/item/gun/gun in H.held_items) + if(user.combat_mode && !HAS_TRAIT(user, TRAIT_NO_GUN_AKIMBO)) + for(var/obj/item/gun/gun in user.held_items) if(gun == src || gun.weapon_weight >= WEAPON_MEDIUM) continue else if(gun.can_trigger_gun(user, akimbo_usage = TRUE)) @@ -354,7 +353,7 @@ /obj/item/gun/proc/recharge_newshot() return -/obj/item/gun/proc/process_burst(mob/living/user, atom/target, message = TRUE, params=null, zone_override = "", sprd = 0, randomized_gun_spread = 0, randomized_bonus_spread = 0, rand_spr = 0, iteration = 0) +/obj/item/gun/proc/process_burst(mob/living/user, atom/target, message = TRUE, params=null, zone_override = "", random_spread = 0, burst_spread_mult = 0, iteration = 0) if(!user || !firing_burst) firing_burst = FALSE return FALSE @@ -367,10 +366,11 @@ if(chambered.harmful) // Is the bullet chambered harmful? to_chat(user, span_warning("[src] is lethally chambered! You don't want to risk harming anyone...")) return + var/sprd if(randomspread) - sprd = round((rand(0, 1) - 0.5) * DUALWIELD_PENALTY_EXTRA_MULTIPLIER * (randomized_gun_spread + randomized_bonus_spread)) + sprd = round((rand(0, 1) - 0.5) * DUALWIELD_PENALTY_EXTRA_MULTIPLIER * (random_spread)) else //Smart spread - sprd = round((((rand_spr/burst_size) * iteration) - (0.5 + (rand_spr * 0.25))) * (randomized_gun_spread + randomized_bonus_spread)) + sprd = round((((burst_spread_mult/burst_size) * iteration) - (0.5 + (burst_spread_mult * 0.25))) * (random_spread)) before_firing(target,user) if(!chambered.fire_casing(target, user, params, ,suppressed, zone_override, sprd, src)) shoot_with_empty_chamber(user) @@ -392,8 +392,12 @@ return TRUE /obj/item/gun/proc/process_fire(atom/target, mob/living/user, message = TRUE, params = null, zone_override = "", bonus_spread = 0) + var/base_bonus_spread = 0 if(user) - SEND_SIGNAL(user, COMSIG_MOB_FIRED_GUN, src, target, params, zone_override) + var/list/bonus_spread_values = list(base_bonus_spread, bonus_spread) + SEND_SIGNAL(user, COMSIG_MOB_FIRED_GUN, src, target, params, zone_override, bonus_spread_values) + base_bonus_spread = bonus_spread_values[MIN_BONUS_SPREAD_INDEX] + bonus_spread = bonus_spread_values[MAX_BONUS_SPREAD_INDEX] SEND_SIGNAL(src, COMSIG_GUN_FIRED, user, target, params, zone_override) @@ -403,17 +407,10 @@ return //Vary by at least this much - var/base_bonus_spread = 0 - var/sprd = 0 - var/randomized_gun_spread = 0 - var/rand_spr = rand() - if(user && HAS_TRAIT(user, TRAIT_POOR_AIM)) //Nice job hotshot - bonus_spread += 35 - base_bonus_spread += 10 - - if(spread) - randomized_gun_spread = rand(0,spread) var/randomized_bonus_spread = rand(base_bonus_spread, bonus_spread) + var/randomized_gun_spread = spread ? rand(0, spread) : 0 + var/total_random_spread = max(0, randomized_bonus_spread + randomized_gun_spread) + var/burst_spread_mult = rand() var/modified_delay = fire_delay if(user && HAS_TRAIT(user, TRAIT_DOUBLE_TAP)) @@ -422,14 +419,14 @@ if(burst_size > 1) firing_burst = TRUE for(var/i = 1 to burst_size) - addtimer(CALLBACK(src, PROC_REF(process_burst), user, target, message, params, zone_override, sprd, randomized_gun_spread, randomized_bonus_spread, rand_spr, i), modified_delay * (i - 1)) + addtimer(CALLBACK(src, PROC_REF(process_burst), user, target, message, params, zone_override, total_random_spread, burst_spread_mult, i), modified_delay * (i - 1)) else if(chambered) if(HAS_TRAIT(user, TRAIT_PACIFISM)) // If the user has the pacifist trait, then they won't be able to fire [src] if the round chambered inside of [src] is lethal. if(chambered.harmful) // Is the bullet chambered harmful? to_chat(user, span_warning("[src] is lethally chambered! You don't want to risk harming anyone...")) return - sprd = round((rand(0, 1) - 0.5) * DUALWIELD_PENALTY_EXTRA_MULTIPLIER * (randomized_gun_spread + randomized_bonus_spread)) + var/sprd = round((rand(0, 1) - 0.5) * DUALWIELD_PENALTY_EXTRA_MULTIPLIER * total_random_spread) before_firing(target,user) if(!chambered.fire_casing(target, user, params, , suppressed, zone_override, sprd, src)) shoot_with_empty_chamber(user) diff --git a/code/modules/projectiles/guns/ballistic.dm b/code/modules/projectiles/guns/ballistic.dm index 876adfdd3b5..cdb2c393f0b 100644 --- a/code/modules/projectiles/guns/ballistic.dm +++ b/code/modules/projectiles/guns/ballistic.dm @@ -249,10 +249,11 @@ stack_trace("Trying to move a qdeleted casing of type [casing.type]!") chambered = null else if(casing_ejector || !from_firing) - casing.forceMove(drop_location()) //Eject casing onto ground. - casing.bounce_away(TRUE) - SEND_SIGNAL(casing, COMSIG_CASING_EJECTED) chambered = null + casing.forceMove(drop_location()) //Eject casing onto ground. + if(!QDELETED(casing)) + casing.bounce_away(TRUE) + SEND_SIGNAL(casing, COMSIG_CASING_EJECTED) else if(empty_chamber) chambered = null if (chamber_next_round && (magazine?.max_ammo > 1)) diff --git a/code/modules/projectiles/guns/energy.dm b/code/modules/projectiles/guns/energy.dm index 4ea530c51a4..d7bdfa6ad2a 100644 --- a/code/modules/projectiles/guns/energy.dm +++ b/code/modules/projectiles/guns/energy.dm @@ -190,7 +190,7 @@ process_chamber() // If the gun was drained and then recharged, load a new shot. return ..() -/obj/item/gun/energy/process_burst(mob/living/user, atom/target, message = TRUE, params = null, zone_override="", sprd = 0, randomized_gun_spread = 0, randomized_bonus_spread = 0, rand_spr = 0, iteration = 0) +/obj/item/gun/energy/process_burst(mob/living/user, atom/target, message = TRUE, params = null, zone_override="", randomized_gun_spread = 0, randomized_bonus_spread = 0, rand_spr = 0, iteration = 0) if(!chambered && can_shoot()) process_chamber() // Ditto. return ..() diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 0bbe5b221b3..d704586af1d 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -94,6 +94,8 @@ var/ricochets = 0 /// how many times we can ricochet max var/ricochets_max = 0 + /// how many times we have to ricochet min (unless we hit an atom we can ricochet off) + var/min_ricochets = 0 /// 0-100 (or more, I guess), 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 @@ -449,8 +451,9 @@ store_hitscan_collision(point_cache) 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. - def_zone = ran_zone(def_zone, max(100-(7*distance), 5)) //Lower accurancy/longer range tradeoff. 7 is a balanced number to use. + if(!HAS_TRAIT(src, TRAIT_ALWAYS_HIT_ZONE)) + 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. + def_zone = ran_zone(def_zone, max(100-(7*distance), 5)) //Lower accurancy/longer range tradeoff. 7 is a balanced number to use. return process_hit(T, select_target(T, A, A), A) // SELECT TARGET FIRST! @@ -684,7 +687,7 @@ var/chance = ricochet_chance * A.receive_ricochet_chance_mod if(firer && HAS_TRAIT(firer, TRAIT_NICE_SHOT)) chance += NICE_SHOT_RICOCHET_BONUS - if(prob(chance)) + if(ricochets < min_ricochets || prob(chance)) return TRUE return FALSE diff --git a/code/modules/religion/honorbound/honorbound_trauma.dm b/code/modules/religion/honorbound/honorbound_trauma.dm index 350e02c29f6..fc0f317154c 100644 --- a/code/modules/religion/honorbound/honorbound_trauma.dm +++ b/code/modules/religion/honorbound/honorbound_trauma.dm @@ -168,7 +168,7 @@ SIGNAL_HANDLER punishment(user, spell_cast.school) -/datum/brain_trauma/special/honorbound/proc/staff_check(mob/user, obj/item/gun/gun_fired, target, params, zone_override) +/datum/brain_trauma/special/honorbound/proc/staff_check(mob/user, obj/item/gun/gun_fired, target, params, zone_override, list/bonus_spread_values) SIGNAL_HANDLER if(!istype(gun_fired, /obj/item/gun/magic)) return diff --git a/code/modules/religion/sparring/sparring_datum.dm b/code/modules/religion/sparring/sparring_datum.dm index 5d4853b378e..f292293f90c 100644 --- a/code/modules/religion/sparring/sparring_datum.dm +++ b/code/modules/religion/sparring/sparring_datum.dm @@ -149,7 +149,7 @@ flubbed_match() ///someone used a gun -/datum/sparring_match/proc/gun_violation(datum/offender) +/datum/sparring_match/proc/gun_violation(mob/offender, obj/item/gun/gun_fired, target, params, zone_override, list/bonus_spread_values) SIGNAL_HANDLER violation(offender, "using guns") diff --git a/code/modules/research/designs/mechfabricator_designs.dm b/code/modules/research/designs/mechfabricator_designs.dm index 6bfaa526e11..3ad98ff733f 100644 --- a/code/modules/research/designs/mechfabricator_designs.dm +++ b/code/modules/research/designs/mechfabricator_designs.dm @@ -1925,6 +1925,33 @@ RND_CATEGORY_MODSUIT_MODULES + RND_SUBCATEGORY_MODSUIT_MODULES_SUPPLY ) +/datum/design/module/joint_torsion + name = "Joint Torsion Ratchet Module" + id = "mod_joint_torsion" + materials = list(/datum/material/iron = HALF_SHEET_MATERIAL_AMOUNT, /datum/material/gold = SMALL_MATERIAL_AMOUNT*2.5, /datum/material/titanium = SMALL_MATERIAL_AMOUNT) + build_path = /obj/item/mod/module/joint_torsion + category = list( + RND_CATEGORY_MODSUIT_MODULES + RND_SUBCATEGORY_MODSUITS_MISC + ) + +/datum/design/module/recycler + name = "Recycler Module" + id = "mod_recycler" + materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT, /datum/material/glass = HALF_SHEET_MATERIAL_AMOUNT, /datum/material/plastic = SMALL_MATERIAL_AMOUNT*2) + build_path = /obj/item/mod/module/recycler + category = list( + RND_CATEGORY_MODSUIT_MODULES + RND_SUBCATEGORY_MODSUIT_MODULES_SERVICE + ) + +/datum/design/module/shooting_assistant + name = "Shooting Assistant Module" + id = "mod_shooting" + materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT, /datum/material/silver = SMALL_MATERIAL_AMOUNT*2, /datum/material/gold = SMALL_MATERIAL_AMOUNT, /datum/material/diamond = SMALL_MATERIAL_AMOUNT) + build_path = /obj/item/mod/module/shooting_assistant + category = list( + RND_CATEGORY_MODSUIT_MODULES + RND_SUBCATEGORY_MODSUIT_MODULES_SECURITY + ) + //MODsuit anomalock modules /datum/design/module/mod_antigrav name = "Anti-Gravity Module" diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm index 82baf4d2b81..719887a0864 100644 --- a/code/modules/research/techweb/all_nodes.dm +++ b/code/modules/research/techweb/all_nodes.dm @@ -2301,10 +2301,13 @@ /datum/techweb_node/mod_experimental id = "mod_experimental" display_name = "Experimental Modular Suits" - description = "Applications of experimentality when creating MODsuits has created these..." + description = "Applications of experimentality when creating MODsuits have created these..." prereq_ids = list("base") design_ids = list( "mod_disposal", + "mod_joint_torsion", + "mod_recycler", + "mod_shooting", ) research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500) hidden = TRUE diff --git a/icons/mob/clothing/modsuit/mod_modules.dmi b/icons/mob/clothing/modsuit/mod_modules.dmi index 1001ae77d2d..3c41dac86b2 100644 Binary files a/icons/mob/clothing/modsuit/mod_modules.dmi and b/icons/mob/clothing/modsuit/mod_modules.dmi differ diff --git a/icons/obj/clothing/modsuit/mod_modules.dmi b/icons/obj/clothing/modsuit/mod_modules.dmi index 037e7fe6963..8df1945d800 100644 Binary files a/icons/obj/clothing/modsuit/mod_modules.dmi and b/icons/obj/clothing/modsuit/mod_modules.dmi differ diff --git a/icons/obj/weapons/guns/toy.dmi b/icons/obj/weapons/guns/toy.dmi index b59dfca6740..83a85e7e447 100644 Binary files a/icons/obj/weapons/guns/toy.dmi and b/icons/obj/weapons/guns/toy.dmi differ