From d42d2f647dc215ea0cf5613527f7f528537afc98 Mon Sep 17 00:00:00 2001
From: Qwertytoforty <52090703+Qwertytoforty@users.noreply.github.com>
Date: Wed, 13 Sep 2023 16:32:30 -0400
Subject: [PATCH] Added the Combat Bakery Kit (#21901)
* QUASO
* Apply suggestions from code review
Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
* blame lewc for giving me spaces
* Update code/datums/uplink_items/uplink_traitor.dm
Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
* Update code/datums/components/boomerang.dm
Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
* Apply suggestions from code review
Co-authored-by: Henri215 <77684085+Henri215@users.noreply.github.com>
* Update code/game/objects/items/granters/_granters.dm
Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
* purge that
---------
Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
Co-authored-by: Henri215 <77684085+Henri215@users.noreply.github.com>
---
code/__DEFINES/dcs/signals.dm | 2 +
code/controllers/subsystem/SSthrowing.dm | 1 +
code/datums/components/boomerang.dm | 86 ++++++++++++++
code/datums/spells/charge.dm | 10 ++
code/datums/uplink_items/uplink_traitor.dm | 9 ++
code/game/objects/items/granters/_granters.dm | 106 ++++++++++++++++++
.../crafting_granters/_crafting_granter.dm | 18 +++
.../crafting_granters/combat_baking.dm | 19 ++++
.../items/weapons/storage/uplink_kits.dm | 5 +
code/modules/crafting/recipes.dm | 11 ++
.../food_and_drinks/food/foods/baked_goods.dm | 10 ++
.../food_and_drinks/food/foods/bread.dm | 8 ++
icons/obj/library.dmi | Bin 26447 -> 26767 bytes
paradise.dme | 4 +
14 files changed, 289 insertions(+)
create mode 100644 code/datums/components/boomerang.dm
create mode 100644 code/game/objects/items/granters/_granters.dm
create mode 100644 code/game/objects/items/granters/crafting_granters/_crafting_granter.dm
create mode 100644 code/game/objects/items/granters/crafting_granters/combat_baking.dm
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 541d89f293e..2083dda5dd6 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 b11c4bc24a7..b31673d3312 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 00000000000..973f3c0229f
--- /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 d5362e35b5c..806767153a1 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 5060d8c32cc..d6faf63bfe0 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 00000000000..b6d0e9487d6
--- /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 00000000000..9c9a26c1325
--- /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 00000000000..6c6068eadd7
--- /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 a27d3415cf3..8b2e2b0043c 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 62920cbf20a..b2e0d6fdfee 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 33c3f0fe260..137c33a52e1 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 0d84f42eeb9..5580f600dfb 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 d472f52f9fd967bc5ea741a010eec3ee22eae107..90499093986d8acddd983ddc1a8fb830143909c8 100644
GIT binary patch
delta 5476
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
Ddkh_m
delta 5168
zcmZ9Qc|6qn_s2hD86*^y8)+!fMYtrTG^p%rLQzIS_Hnas^AQrsQb=|R6*9K6o4J;R
zY}v9kmdHA`!7yg#`x)x~?&CLq%*^BSdB4v&?=$E1Jm)hp-LTFMSga_l>e;E2hc0~Y
zzrsCyy@0ja#RHD^JtpSMHr>IlL*vv_`>lgK
z626a!a^?hTnmMFPIm$@Y(CL80xMZVi@@us$R%5W$u4rrji%zS^qYDq<7SC^*o0R$4
zCv-VxzN72Ds*8P#fTwwJMcios9&}w4?&c}%=EfVB#`u^|YQWG#3Kl18j)UCjS7GBS
z+RLYHG)y*q43GPJn1`9&zy{T3eHgcu?8M
z*K&{2HuU?EQo*%ilZ6kzQl*W&UvNIG<%r{Rf2~ID-mu1c{n!f2&7nI+%4)SI?yBzl
z8T8QsrPZt4-f90Ly(d8CO*;zIcdtve3%t->`KH}~_QJH{y=-=<>F}(osa1DYAMdEG
zm+z&qYoE?fI(u~bj~^P+d-P4~V6K_qN;a?KVivF1^(9RKq?xIR*FV<$-DeM*A6@L7
z>asqR{8z_|mih+Ei=GZ_sr&r9H5}Vxk{)=CUzlh0gC~XoXK1?mdM(mzaBW_+j}IWFw2eC1t)|@TYF0*%Ae5%HBnis
z-&Ac9E>-FwEc<7-xBlGXjGM>2kOP<+0YDhgx~h7^Cv_psH{j6JI2~Ea#=LeN#C>~P
zxDyR>J*cj>#J}uK^SLL-ZO?m@(@@N_jQ7R+!NjW{!b7GXNkv
zf8cQ#%8n3l={;=z+*%|GFCv|iSXH1&k^N}mSnnb01*AmV355u$PrxL-5MP9dU#Q;1
zGzz+diV;2Q-BG(4&~cP!jnbY5x_A0+hQxSm3W1A_V@k`s3D>seY;
z<@S$%VmQBn8w6t{1yMj?+Q^`mPb${31bUGFh~`G{$s_UY`$Hi&;GQ2&0^82Y*PFJK
z91S>E%$g{37UZ50L(5KXV&;-<=Y-`(f!j%#xBP*<&}I|+(A!)Uv28FQT^|?T){}@J
zYHr0n>@q-7Nj(O*0*rp6>h4!06-MU5!O(+M?S1fXkA5F*!->hQ_$CQ-p3Si?KJBre
zu?t$?MjT)nOGr5ULrg5cb#<%8<(9X1d9JgqZPV(dU!G%vQRH*k^?jm}6mzEnW11+LHW{3lu(qwP
ztrY@fm6e6SqqyTGl!Sqr+S=N=b+cBRO$(7~(9f@G^}-E_1PNO}VmRstKQ}(c(^=EZ
z%&dE4q+k$z!xG(mn6|kJa#C<~>XO#anC8j)=63`@ZfkgGD6iEqqm4)?N25#z>+98Q
zt4mAy0Jlxz=x6eJ>?pQoIxQ{D1dF>aR*hOhgPGD!XK`WxDkbfTlarGPbX-L^n_`${
zCn2@DquJY0;(B6CRMf2HQr)?W_lN%2j-n!W;sWbw!#GrI2cf6up06}OB9S!bK~lFy
zeybDYdTD71E0K|u^v+qFmjl4Bu0F8Y@%v0Pvs}A+!f1V4(l%-uN;03gMsgSmynI_>
zoy5s@QA*IVt?@^E?m}bB!M39cvu{wHxT)$CSokSGZX4#0yVf#;rQ(f^s}0m~Nyb`$
z&1SyxhYoepyk&5qvfEkzygb!4#h+YFc5+5DZ!&0(I3<{@S{m?qnr(q$VNn!%Yft^~
zPm$41Jq{*0V@t=VLBBVs(TNoWR1cOR+|CJ@iQ&>%GL3eUlZr@hsoK(N%17CM)rBMt
z^jXSy6hqHfq`kVkqXv6)qG1rA_1q=;RFR$5adwR3S~V{!Nn
zg(@k9QL_n!y^Mw5wYUGg`m(#*=ufJuIG{?2NRiDBl}xVT*(y?CnbH
z3Up_>vZq2ABqc?St=RlPQe@`dfkO0w;0?AkpkmScEW6H9$ft#n78fXpJj^<{9dJ1&
zD=VuNk9ekI6Gh}vg0T-FJV2%cWyl#8ybI11@RFOkn$6vf$YlsWu$MlYz{6aN7tcqX
zg~j&CBmbm0ht6N0H6E27C+WK95=(XNbD3*UUN+9ui(Asoz
z6TCJT2IA9NqQ?xki>A9d_GI28lTSDc{1g)dSBPwgS>i?7kYp9t%aoM9Ko6$hD4657
zczT!m5AyACu5TZl%8B>xyrmxTA>OBltXl{N(nFR9J~p9|5dUMMKZbD8GLYKo{H#9q!b&Ns;1or~AUH^Xsl=zo`
zNjteX7@;k_sdnMkxH=QjPWb@a2!u^Z|Z
zTx0)o48qtH17i}yt!j;P*CG!0M#>xL>O0to>qp=0m=pBE%>@4_*}ldv`i3BgJI#oP
zOsz`H1f6(}wWsHu)fD>CJA6XUsD1&eahvr>F@WSh$#$;9rP`Cx(#F>~>F|QbO2A9j
zD_HjW-(r~Agx9vAc0pGC*btzRM()qhyVsI=jw#FqW~5X0UztVb<2*&yQZDo%>f|Wb
zc!BNFP0)`hZ}D^2>?Y`x`|7N86<}!&F6NllvRHX|
z#AA4NLJw>msACZ#J=X5R;0L(|<+iS2Rk{q=Mf!W!1>NG=8G`0v$pEg78n8d(gj=1s
z+Qp&LONF*s0b-#Cc(vt{o-ThwD1Y~xe_zFWA4{!=SP4S(#C^7ro1gU%7{H>uJuqfw
z{3U}0lfny+Q8R*Wi&39X{Nf>09&fiJYS>xFAd;A&K;w$9JOq$!sPbVdADsRexBX#x
z1xq7|@5&?erS-g`Rqdafjz-f>>%qd*9u)i{(>Z&bS>z2yqi+m<@#2Vwrzd#fvf=`U
zyS_v0``xgid^$Z7LG1BP!J|lnVv*xsT_Wg&DQh
zh}nbjcbS+jtu??5=6I@6QI?s1aq5!z#HG%hVKsk@z>EUU=VnxdetgND?#aL6VWswx
z`idi{0AplB8is(nW^oGm08W)`EnbhwaCV#TKLauMX-v#TC1is3_}3hrtPLmED+2B5
z<{&jn(Ws6Rb@JUoi;u1il%?8xTVmM^J;vk_o_r2CXL
zzFnEWo*S5)d;h3)BQD0X$`pHAGABP_vHx3KI)yk=X*}4m{xb*-II|V)j2+Bdv>j8S
z0Ym~0dqJ@P(*vZYylkvS1Y}n)k)5zJb+Dh)cAkhd6~-urFu@(C;Y(ZpQ;{6AiG{^*Lp>6Hz)YyD)XragxoJbDi-Hg&m?pT(Cw-|J
zZ8^WQisp4}-wpu{l?95N23$uq2H5L6X{Gc9(2X|qk-W%bOh2h27I(`)EN(~$#Y{#3
zYdq-#&WJxQ1ToWnRouE$*B(poRzCDB4YUl+|9L|N2
zXO{muCUn^@UrkY7g+WQE4%ZFgcZCMAwL6P!t-=NO*+3#L;3)ob-`bE4-ghjr0hPI_
zxB=I#oGp)B4#Xp5qD(%b{A{|G&tv6&n>cKZBin*;gPQi(t-&syUNfs-p(t-V(>jQVH
z6(SYI^)_;E{|!}tWnEFUUs{mtmIBPj)|Rz0VvhepMme(1yjC;lcPn{4S8i=R`w<+!
zjAUBwYw6V8sNxIiJAa2=RU{z~t7l1hguWv2C4d#UX<^Zjt2e|^);nczD*bKz{mxY5
zKa(>6fJKV&K*bDX^9UU>Vc?k5gKg-fFP1+nd`rU8S7YkP(8LS_0s{jJd-+UpZUCm>
zr0Kh&%F0`2*=EFk!5AnhAT*rNPG_;aw~>qJMADBH-`o%Nhtr4lg$;KqZys|UsmQXp
zte{{kd4Mtt!QQFV-QLCJy~I3`_-SnHQx|0`W$oJ*xnrm&ueJHpCl7F`0veMCeScL6
zhub!3R?L=Y^K6Vi(nVB5eM&p+F3E{$IU??)UV{vxPylqK)vHoARrR1=(W;+%tT^D{
zFA0kGg|pzUzhhAmluB5YrFhDy4%8W`m%hpS9t#b+cdsd<&;9b!a%E!W{KSe+pL?wf
z@QrSTSr~#gJOY_byQ+64_IPNtuJP#Y2xmbOhfd6pj@*fgB-mF*%0P`i#I@*S7fOiz
zwH5O6^5h?J2F_IFomEa&D`>!Nl!5L|%stW3XDUKq;EYS6K$&+uALE|8Gf(~VsD}?9
zW~Qh>*Mo1ut){|k-h@lT4x1t&u1~P$!p_IF6ujwixQ)Y%8cp4~bBBR?{Sd1z48wpV
zSrC+k7V-l7@z4;h3EZT?N4|ayE-aMRNGLBWJFg_8yStE6m)WiI_NT42pYy!&ZBtbp
zTwkHS6YBHG0kAYT&$BQz5K^UZK$ftwT=;U=QMD7#@MLnu>sI4;hotXR{g@e%CGXxo
zkRJZJynMj4sq%J**S-1$aS-x5(kKTID&3p^o?1`MH)~$_f#OUnr6iT#Sz{d2(#l8h
z`7V}HCR-ES)4tf*{=iXP1j=!Gg07OC^Xt2dUI_+4aPpH&^3Z#(E9SzgDo>AYzqRl!
zRbj+5ePB|Y@r)40_c!CA0oLV~FY-?2uYr<#?ngk#%;eyG!bb5mXi0dWfCR9FnRe+J
ztD+p0CG)DWJFtf`yLFYXvn(=fFhb?u$LitXaXzoc@brHZw7fFeOm);a4K-Y~aI+cs
z*n`Taocz+{k@)4kvv+ghb#!zzMR^OK*xA{I#&8WmIRry}qNdPzeSS4V&7A9=X^656
zD8?102B4;@Y9BB&KcBCY**i(9$%Oj9T6p42M`+HjSg<&nK#C&zQ?$@U-SW7W?7Nj!
z`UitIc9Zd1FvuX|@@Temv-MwPA3we_JpIy@Syl@BzUN8iZg>MXJ6O-~?m)932)Gp8
zXq^|a-z%6GAw~bKWgLie^;OInZPeK0^gmB;f
sRR;D?wr#)vr!u31p$SASoU(&V4zS3m7=o1o&`ax@KI_#&HLKwN0cDPH5&!@I
diff --git a/paradise.dme b/paradise.dme
index a841a4e9ba6..11f99b4df6b 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"