diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index 541d89f293e6..2083dda5dd65 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -281,6 +281,8 @@ #define COMPONENT_CANCEL_THROW (1<<0) ///from base of atom/movable/throw_at(): (datum/thrownthing, spin) #define COMSIG_MOVABLE_POST_THROW "movable_post_throw" +///from base of datum/thrownthing/finalize(): (obj/thrown_object, datum/thrownthing) used for when a throw is finished +#define COMSIG_MOVABLE_THROW_LANDED "movable_throw_landed" ///from base of atom/movable/onTransitZ(): (old_z, new_z) #define COMSIG_MOVABLE_Z_CHANGED "movable_ztransit" ///called when the movable is placed in an unaccessible area, used for stationloving: () diff --git a/code/controllers/subsystem/SSthrowing.dm b/code/controllers/subsystem/SSthrowing.dm index b11c4bc24a7c..b31673d3312d 100644 --- a/code/controllers/subsystem/SSthrowing.dm +++ b/code/controllers/subsystem/SSthrowing.dm @@ -142,6 +142,7 @@ SUBSYSTEM_DEF(throwing) if(callback) callback.Invoke() + SEND_SIGNAL(thrownthing, COMSIG_MOVABLE_THROW_LANDED, src) thrownthing.end_throw() /datum/thrownthing/proc/hit_atom(atom/A) diff --git a/code/datums/components/boomerang.dm b/code/datums/components/boomerang.dm new file mode 100644 index 000000000000..973f3c0229f0 --- /dev/null +++ b/code/datums/components/boomerang.dm @@ -0,0 +1,86 @@ +///The cooldown period between last_boomerang_throw and it's methods of implementing a rebound proc. +#define BOOMERANG_REBOUND_INTERVAL (1 SECONDS) + +/** + * If an object is given the boomerang component, it should be thrown back to the thrower after either hitting it's target, or landing on the thrown tile. + * Thrown objects should be thrown back to the original thrower with this component, a number of tiles defined by boomerang_throw_range. + */ +/datum/component/boomerang + ///How far should the boomerang try to travel to return to the thrower? + var/boomerang_throw_range = 3 + ///If this boomerang is thrown, does it re-enable the throwers throw mode? + var/thrower_easy_catch_enabled = FALSE + ///This cooldown prevents our 2 throwing signals from firing too often based on how we implement those signals within thrown impacts. + var/last_boomerang_throw = 0 + +/datum/component/boomerang/Initialize(boomerang_throw_range, thrower_easy_catch_enabled) + . = ..() + if(!isitem(parent)) //Only items support being thrown around like a boomerang, feel free to make this apply to humans later on. + return COMPONENT_INCOMPATIBLE + + //Assignments + src.boomerang_throw_range = boomerang_throw_range + src.thrower_easy_catch_enabled = thrower_easy_catch_enabled + +/datum/component/boomerang/RegisterWithParent() + RegisterSignal(parent, COMSIG_MOVABLE_POST_THROW, PROC_REF(prepare_throw)) //Collect data on current thrower and the throwing datum + RegisterSignal(parent, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(return_missed_throw)) + RegisterSignal(parent, COMSIG_MOVABLE_IMPACT, PROC_REF(return_hit_throw)) + +/datum/component/boomerang/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_MOVABLE_POST_THROW, COMSIG_MOVABLE_THROW_LANDED, COMSIG_MOVABLE_IMPACT)) + +/** + * Proc'd before the first thrown is performed in order to gather information regarding each throw as well as handle throw_mode as necessary. + * * source: Datum src from original signal call. + * * thrown_thing: The thrownthing datum from the parent object's latest throw. Updates thrown_boomerang. + * * spin: Carry over from POST_THROW, the speed of rotation on the boomerang when thrown. + */ +/datum/component/boomerang/proc/prepare_throw(datum/source, datum/thrownthing/thrown_thing, spin) + SIGNAL_HANDLER + if(thrower_easy_catch_enabled && iscarbon(thrown_thing?.thrower)) + var/mob/living/carbon/C = thrown_thing.thrower + C.throw_mode_on() + +/** + * Proc that triggers when the thrown boomerang hits an object. + * * source: Datum src from original signal call. + * * hit_atom: The atom that has been hit by the boomerang component. + * * init_throwing_datum: The thrownthing datum that originally impacted the object, that we use to build the new throwing datum for the rebound. + */ +/datum/component/boomerang/proc/return_hit_throw(datum/source, atom/hit_atom, datum/thrownthing/init_throwing_datum) + SIGNAL_HANDLER + if(world.time <= last_boomerang_throw) + return + var/obj/item/true_parent = parent + aerodynamic_swing(init_throwing_datum, true_parent) + +/** + * Proc that triggers when the thrown boomerang does not hit a target. + * * source: Datum src from original signal call. + * * throwing_datum: The thrownthing datum that originally impacted the object, that we use to build the new throwing datum for the rebound. + */ +/datum/component/boomerang/proc/return_missed_throw(datum/source, datum/thrownthing/throwing_datum) + SIGNAL_HANDLER + if(world.time <= last_boomerang_throw) + return + var/obj/item/true_parent = parent + aerodynamic_swing(throwing_datum, true_parent) + +/** + * Proc that triggers when the thrown boomerang has been fully thrown, rethrowing the boomerang back to the thrower, and producing visible feedback. + * * throwing_datum: The thrownthing datum that originally impacted the object, that we use to build the new throwing datum for the rebound. + * * hit_atom: The atom that has been hit by the boomerang'd object. + */ +/datum/component/boomerang/proc/aerodynamic_swing(datum/thrownthing/throwing_datum, obj/item/true_parent) + var/mob/thrown_by = locateUID(true_parent.thrownby) + if(istype(thrown_by)) + var/dir = get_dir(true_parent, thrown_by) + var/turf/T = get_ranged_target_turf(thrown_by, dir, 2) + addtimer(CALLBACK(true_parent, TYPE_PROC_REF(/atom/movable, throw_at), T, boomerang_throw_range, throwing_datum.speed, null, TRUE), 1) + last_boomerang_throw = world.time + BOOMERANG_REBOUND_INTERVAL + true_parent.visible_message("[true_parent] is flying back at [throwing_datum.thrower]!", \ + "You see [true_parent] fly back at you!", \ + "You hear an aerodynamic woosh!") + +#undef BOOMERANG_REBOUND_INTERVAL diff --git a/code/datums/spells/charge.dm b/code/datums/spells/charge.dm index d5362e35b5c6..806767153a13 100644 --- a/code/datums/spells/charge.dm +++ b/code/datums/spells/charge.dm @@ -47,6 +47,16 @@ to_chat(L, "Glowing red letters appear on the front cover...") to_chat(L, "[pick("NICE TRY BUT NO!","CLEVER BUT NOT CLEVER ENOUGH!", "SUCH FLAGRANT CHEESING IS WHY WE ACCEPTED YOUR APPLICATION!", "CUTE!", "YOU DIDN'T THINK IT'D BE THAT EASY, DID YOU?")]") burnt_out = TRUE + else if(istype(item, /obj/item/book/granter)) + var/obj/item/book/granter/I = item + if(prob(80)) + L.visible_message("[I] catches fire!") + qdel(I) + else + I.uses += 1 + charged_item = I + break + else if(istype(item, /obj/item/gun/magic)) var/obj/item/gun/magic/I = item if(prob(80) && !I.can_charge) diff --git a/code/datums/uplink_items/uplink_traitor.dm b/code/datums/uplink_items/uplink_traitor.dm index 5060d8c32ccb..d6faf63bfe0f 100644 --- a/code/datums/uplink_items/uplink_traitor.dm +++ b/code/datums/uplink_items/uplink_traitor.dm @@ -67,6 +67,15 @@ cost = 50 job = list("Mime") +/datum/uplink_item/jobspecific/combat_baking + name = "Combat Bakery Kit" + desc = "A kit of clandestine baked weapons. Contains a baguette which a skilled mime could use as a sword, \ + a pair of throwing croissants, and the recipe to make more on demand. Once the job is done, eat the evidence." + reference = "CBK" + item = /obj/item/storage/box/syndie_kit/combat_baking + cost = 25 //A chef can get a knife that sharp easily, though it won't block. While you can get endless boomerang, they are less deadly than a stech, and slower / more predictable. + job = list("Mime", "Chef") + /datum/uplink_item/jobspecific/pressure_mod name = "Kinetic Accelerator Pressure Mod" desc = "A modification kit which allows Kinetic Accelerators to do greatly increased damage while indoors. Occupies 35% mod capacity." diff --git a/code/game/objects/items/granters/_granters.dm b/code/game/objects/items/granters/_granters.dm new file mode 100644 index 000000000000..b6d0e9487d63 --- /dev/null +++ b/code/game/objects/items/granters/_granters.dm @@ -0,0 +1,106 @@ +/** + * Books that teach things. + * + * (Intrinsic actions like bar flinging, spells like fireball or smoke, or martial arts) + */ +/obj/item/book/granter + /// Flavor messages displayed to mobs reading the granter + var/list/remarks = list() + /// Controls how long a mob must keep the book in his hand to actually successfully learn + var/pages_to_mastery = 3 + /// Sanity, whether it's currently being read + var/reading = FALSE + /// The amount of uses on the granter. + var/uses = 1 + /// The time it takes to read the book + var/reading_time = 5 SECONDS + /// The sounds played as the user's reading the book. + var/list/book_sounds = list( + 'sound/effects/pageturn1.ogg', + 'sound/effects/pageturn2.ogg', + 'sound/effects/pageturn3.ogg' + ) + +/obj/item/book/granter/attack_self(mob/living/user) + if(reading) + to_chat(user, "You're already reading this!") + return FALSE + if(!user.has_vision()) + to_chat(user, "You are blind and can't read anything!") + return FALSE + if(!isliving(user)) + return FALSE + if(!can_learn(user)) + return FALSE + + if(uses <= 0) + recoil(user) + return FALSE + + on_reading_start(user) + reading = TRUE + for(var/i in 1 to pages_to_mastery) + if(!turn_page(user)) + on_reading_stopped() + reading = FALSE + return + if(do_after(user, reading_time, src)) + uses-- + on_reading_finished(user) + reading = FALSE + + return TRUE + +/// Called when the user starts to read the granter. +/obj/item/book/granter/proc/on_reading_start(mob/living/user) + to_chat(user, "You start reading [name]...") + +/// Called when the reading is interrupted without finishing. +/obj/item/book/granter/proc/on_reading_stopped(mob/living/user) + to_chat(user, "You stop reading...") + +/// Called when the reading is completely finished. This is where the actual granting should happen. +/obj/item/book/granter/proc/on_reading_finished(mob/living/user) + to_chat(user, "You finish reading [name]!") + +/// The actual "turning over of the page" flavor bit that happens while someone is reading the granter. +/obj/item/book/granter/proc/turn_page(mob/living/user) + playsound(user, pick(book_sounds), 30, TRUE) + + if(!do_after(user, reading_time, src)) + return FALSE + + to_chat(user, "[length(remarks) ? pick(remarks) : "You keep reading..."]") + return TRUE + +/// Effects that occur whenever the book is read when it has no uses left. +/obj/item/book/granter/proc/recoil(mob/living/user) + return + +/// Checks if the user can learn whatever this granter... grants +/obj/item/book/granter/proc/can_learn(mob/living/user) + return TRUE + +// Generic action giver +/obj/item/book/granter/action + /// The typepath of action that is given + var/datum/action/granted_action + /// The name of the action, formatted in a more text-friendly way. + var/action_name = "" + +/obj/item/book/granter/action/can_learn(mob/living/user) + if(!granted_action) + CRASH("Someone attempted to learn [type], which did not have an action set.") + if(locate(granted_action) in user.actions) + to_chat(user, "You already know all about [action_name]!") + return FALSE + return TRUE + +/obj/item/book/granter/action/on_reading_start(mob/living/user) + to_chat(user, "You start reading about [action_name]...") + +/obj/item/book/granter/action/on_reading_finished(mob/living/user) + to_chat(user, "You feel like you've got a good handle on [action_name]!") + // Action goes on the mind as the user actually learns the thing in your brain + var/datum/action/new_action = new granted_action(user.mind || user) + new_action.Grant(user) diff --git a/code/game/objects/items/granters/crafting_granters/_crafting_granter.dm b/code/game/objects/items/granters/crafting_granters/_crafting_granter.dm new file mode 100644 index 000000000000..9c9a26c13250 --- /dev/null +++ b/code/game/objects/items/granters/crafting_granters/_crafting_granter.dm @@ -0,0 +1,18 @@ +/obj/item/book/granter/crafting_recipe + /// A list of all recipe types we grant on learn + var/list/crafting_recipe_types = list() + +/obj/item/book/granter/crafting_recipe/on_reading_finished(mob/user) + ..() + if(!user.mind) + return + for(var/datum/crafting_recipe/crafting_recipe_type as anything in crafting_recipe_types) + user.mind.teach_crafting_recipe(crafting_recipe_type) + to_chat(user, "You learned how to make [initial(crafting_recipe_type.name)].") + +/obj/item/book/granter/crafting_recipe/dusting + icon_state = "book1" + +/obj/item/book/granter/crafting_recipe/dusting/recoil(mob/living/user) + to_chat(user, "The book turns to dust in your hands.") + qdel(src) diff --git a/code/game/objects/items/granters/crafting_granters/combat_baking.dm b/code/game/objects/items/granters/crafting_granters/combat_baking.dm new file mode 100644 index 000000000000..6c6068eadd7a --- /dev/null +++ b/code/game/objects/items/granters/crafting_granters/combat_baking.dm @@ -0,0 +1,19 @@ +/obj/item/book/granter/crafting_recipe/combat_baking + name = "the anarchist's cookbook" + desc = "A widely illegal recipe book which will teach you how to bake croissants to die for." + crafting_recipe_types = list( + /datum/crafting_recipe/throwing_croissant + ) + icon_state = "cooking_learing_illegal" + remarks = list( + "\"Austrian? Not French?\"", + "\"Got to get the butter ratio right...\"", + "\"This is the greatest thing since sliced bread!\"", + "\"I'll leave no trace except crumbs!\"", + "\"Who knew that bread could hurt a man so badly?\"" + ) + +/obj/item/book/granter/crafting_recipe/combat_baking/recoil(mob/living/user) + to_chat(user, "The book dissolves into burnt flour!") + new /obj/effect/decal/cleanable/ash(get_turf(src)) + qdel(src) diff --git a/code/game/objects/items/weapons/storage/uplink_kits.dm b/code/game/objects/items/weapons/storage/uplink_kits.dm index a27d3415cf3b..8b2e2b0043c9 100644 --- a/code/game/objects/items/weapons/storage/uplink_kits.dm +++ b/code/game/objects/items/weapons/storage/uplink_kits.dm @@ -282,6 +282,11 @@ new /obj/item/spellbook/oneuse/mime/greaterwall(src) new /obj/item/spellbook/oneuse/mime/fingergun(src) +/obj/item/storage/box/syndie_kit/combat_baking/populate_contents() + new /obj/item/reagent_containers/food/snacks/baguette/combat(src) + for(var/i in 1 to 2) + new /obj/item/reagent_containers/food/snacks/croissant/throwing(src) + new /obj/item/book/granter/crafting_recipe/combat_baking(src) /obj/item/storage/box/syndie_kit/atmosn2ogrenades name = "atmos N2O grenades" diff --git a/code/modules/crafting/recipes.dm b/code/modules/crafting/recipes.dm index 62920cbf20a3..b2e0d6fdfee0 100644 --- a/code/modules/crafting/recipes.dm +++ b/code/modules/crafting/recipes.dm @@ -67,6 +67,17 @@ category = CAT_WEAPONRY subcategory = CAT_WEAPON +/datum/crafting_recipe/throwing_croissant + name = "Throwing croissant" + reqs = list( + /obj/item/reagent_containers/food/snacks/croissant = 1, + /obj/item/stack/rods = 1 + ) + result = list(/obj/item/reagent_containers/food/snacks/croissant) + category = CAT_WEAPONRY + subcategory = CAT_WEAPON + always_availible = FALSE + /datum/crafting_recipe/advancedegun name = "Advanced Energy Gun" tools = list(TOOL_SCREWDRIVER, TOOL_WIRECUTTER) diff --git a/code/modules/food_and_drinks/food/foods/baked_goods.dm b/code/modules/food_and_drinks/food/foods/baked_goods.dm index 33c3f0fe2601..137c33a52e16 100644 --- a/code/modules/food_and_drinks/food/foods/baked_goods.dm +++ b/code/modules/food_and_drinks/food/foods/baked_goods.dm @@ -551,5 +551,15 @@ list_reagents = list("nutriment" = 4, "sugar" = 2) tastes = list("croissant" = 1) +/obj/item/reagent_containers/food/snacks/croissant/throwing + throwforce = 20 + throw_range = 9 //now with extra throwing action + tastes = list("croissant" = 2, "butter" = 1, "metal" = 1) + list_reagents = list("nutriment" = 4, "sugar" = 2, "iron" = 1) + +/obj/item/reagent_containers/food/snacks/croissant/throwing/Initialize(mapload) + . = ..() + AddComponent(/datum/component/boomerang, throw_range, TRUE) + #undef DONUT_NORMAL #undef DONUT_FROSTED diff --git a/code/modules/food_and_drinks/food/foods/bread.dm b/code/modules/food_and_drinks/food/foods/bread.dm index 0d84f42eeb93..5580f600dfbb 100644 --- a/code/modules/food_and_drinks/food/foods/bread.dm +++ b/code/modules/food_and_drinks/food/foods/bread.dm @@ -170,6 +170,14 @@ list_reagents = list("nutriment" = 6, "vitamin" = 1) tastes = list("bread" = 2) +/obj/item/reagent_containers/food/snacks/baguette/combat + sharp = TRUE + force = 20 + +/obj/item/reagent_containers/food/snacks/baguette/combat/Initialize(mapload) + . = ..() + AddComponent(/datum/component/parry, _stamina_constant = 2, _stamina_coefficient = 0.5, _parryable_attack_types = ALL_ATTACK_TYPES) + /obj/item/reagent_containers/food/snacks/twobread name = "two bread" desc = "It is very bitter and winy." diff --git a/icons/obj/library.dmi b/icons/obj/library.dmi index d472f52f9fd9..90499093986d 100644 Binary files a/icons/obj/library.dmi and b/icons/obj/library.dmi differ diff --git a/paradise.dme b/paradise.dme index a841a4e9ba64..11f99b4df6bf 100644 --- a/paradise.dme +++ b/paradise.dme @@ -363,6 +363,7 @@ #include "code\datums\cache\crew.dm" #include "code\datums\cache\powermonitor.dm" #include "code\datums\components\_component.dm" +#include "code\datums\components\boomerang.dm" #include "code\datums\components\caltrop.dm" #include "code\datums\components\deadchat_control.dm" #include "code\datums\components\decal.dm" @@ -987,6 +988,9 @@ #include "code\game\objects\items\devices\radio\headset.dm" #include "code\game\objects\items\devices\radio\intercom.dm" #include "code\game\objects\items\devices\radio\radio_objects.dm" +#include "code\game\objects\items\granters\_granters.dm" +#include "code\game\objects\items\granters\crafting_granters\_crafting_granter.dm" +#include "code\game\objects\items\granters\crafting_granters\combat_baking.dm" #include "code\game\objects\items\mountable_frames\air_alarm_frame.dm" #include "code\game\objects\items\mountable_frames\apc_frame.dm" #include "code\game\objects\items\mountable_frames\buttons_switches.dm"