diff --git a/code/datums/traits/_quirk.dm b/code/datums/traits/_quirk.dm
index a2a8d64909..a0be01b42d 100644
--- a/code/datums/traits/_quirk.dm
+++ b/code/datums/traits/_quirk.dm
@@ -1,20 +1,33 @@
//every quirk in this folder should be coded around being applied on spawn
//these are NOT "mob quirks" like GOTTAGOFAST, but exist as a medium to apply them and other different effects
/datum/quirk
+ /// The name of the quirk
var/name = "Test Quirk"
+ /// The description of the quirk
var/desc = "This is a test quirk."
+ /// What the quirk is worth in preferences, zero = neutral / free
var/value = 0
var/human_only = TRUE
+ /// Text displayed when this quirk is assigned to a mob (and not transferred)
var/gain_text
+ /// Text displayed when this quirk is removed from a mob (and not transferred)
var/lose_text
- var/medical_record_text //This text will appear on medical records for the trait. Not yet implemented
- var/antag_removal_text // Text will be given to the quirk holder if they get an antag that has it blacklisted.
+ /// This text will appear on medical records for the trait.
+ var/medical_record_text
+ /// Text will be given to the quirk holder if they get an antag that has it blacklisted.
+ var/antag_removal_text
var/mood_quirk = FALSE //if true, this quirk affects mood and is unavailable if moodlets are disabled
- var/mob_trait //if applicable, apply and remove this mob trait
+ /// if applicable, apply and remove this mob trait
+ var/mob_trait
/// should we immediately call on_spawn or add a timer to trigger
var/on_spawn_immediate = TRUE
+ /// Reference to the mob currently tied to this quirk datum. Quirks are not singletons.
var/mob/living/quirk_holder
var/processing_quirk = FALSE
+ /// A lazylist of items people can receive from mail who have this quirk enabled
+ /// The base weight for the each quirk's mail goodies list to be selected is 5
+ /// then the item selected is determined by pick(selected_quirk.mail_goodies)
+ var/list/mail_goodies
/datum/quirk/New(mob/living/quirk_mob, spawn_effects)
if(!quirk_mob || (human_only && !ishuman(quirk_mob)) || quirk_mob.has_quirk(type))
diff --git a/code/datums/traits/negative.dm b/code/datums/traits/negative.dm
index c9dcc552eb..6a68334c85 100644
--- a/code/datums/traits/negative.dm
+++ b/code/datums/traits/negative.dm
@@ -9,6 +9,12 @@
antag_removal_text = "Your antagonistic nature has removed your blood deficiency."
medical_record_text = "Patient requires regular treatment for blood loss due to low production of blood."
+/datum/quirk/blooddeficiency/add()
+ RegisterSignal(quirk_holder, COMSIG_SPECIES_GAIN, PROC_REF(update_mail))
+
+ var/mob/living/carbon/human/human_holder = quirk_holder
+ update_mail(new_species = human_holder.dna.species)
+
/datum/quirk/blooddeficiency/on_process()
var/mob/living/carbon/human/H = quirk_holder
if(NOBLOOD in H.dna.species.species_traits) //can't lose blood if your species doesn't have any
@@ -16,6 +22,24 @@
else
quirk_holder.blood_volume -= 0.2
+/datum/quirk/blooddeficiency/proc/update_mail(datum/source, datum/species/new_species, datum/species/old_species)
+ SIGNAL_HANDLER
+
+ mail_goodies.Cut()
+
+ if(isnull(new_species.exotic_blood)) // && isnull(new_species.exotic_bloodtype)) // We don't really support your blood yet :(
+ if(NOBLOOD in new_species.inherent_traits)
+ return
+
+ mail_goodies += /obj/item/reagent_containers/blood/OMinus
+ return
+
+ for(var/obj/item/reagent_containers/blood/blood_bag as anything in typesof(/obj/item/reagent_containers/blood))
+ var/right_blood_type = !isnull(new_species.exotic_bloodtype) && initial(blood_bag.blood_type) == new_species.exotic_bloodtype
+// var/right_blood_reagent = !isnull(new_species.exotic_blood) && initial(blood_bag.unique_blood) == new_species.exotic_blood
+ if(right_blood_type) // || right_blood_reagent)
+ mail_goodies += blood_bag
+
/datum/quirk/depression
name = "Depression"
desc = "You sometimes just hate life."
diff --git a/code/game/objects/items/mail.dm b/code/game/objects/items/mail.dm
index e7a33a3c36..9f5f102d1a 100644
--- a/code/game/objects/items/mail.dm
+++ b/code/game/objects/items/mail.dm
@@ -13,8 +13,8 @@
mouse_drag_pointer = MOUSE_ACTIVE_POINTER
/// Destination tagging for the mail sorter.
var/sort_tag = 0
- /// Who this mail is for and who can open it.
- var/datum/weakref/recipient
+ /// Weak reference to who this mail is for and who can open it.
+ var/datum/weakref/recipient_ref
/// How many goodies this mail contains.
var/goodie_count = 1
/// Goodies which can be given to anyone. The base weight for cash is 56. For there to be a 50/50 chance of getting a department item, they need 56 weight as well.
@@ -83,8 +83,8 @@
pixel_y = stamp_offset_y + bonus_stamp_offset
)
stamp_image.appearance_flags |= RESET_COLOR
- add_overlay(stamp_image)
bonus_stamp_offset -= 5
+ . += stamp_image
if(postmarked == TRUE)
var/image/postmark_image = image(
@@ -94,7 +94,7 @@
pixel_y = stamp_offset_y + rand(bonus_stamp_offset + 3, 1)
)
postmark_image.appearance_flags |= RESET_COLOR
- add_overlay(postmark_image)
+ . += postmark_image
/obj/item/mail/attackby(obj/item/W, mob/user, params)
// Destination tagging
@@ -103,52 +103,106 @@
if(sort_tag != destination_tag.currTag)
var/tag = uppertext(GLOB.TAGGERLOCATIONS[destination_tag.currTag])
- to_chat(user, "*[tag]*")
+ to_chat(user, span_notice("*[tag]*"))
sort_tag = destination_tag.currTag
- playsound(loc, 'sound/machines/twobeep_high.ogg', 100, TRUE)
+ playsound(loc, 'sound/machines/twobeep_high.ogg', vol = 100, vary = TRUE)
+
+/obj/item/mail/multitool_act(mob/living/user, obj/item/tool)
+ if(user.get_inactive_held_item() == src)
+ balloon_alert(user, "nothing to disable!")
+ return TRUE
+ balloon_alert(user, "hold it!")
+ return FALSE
/obj/item/mail/attack_self(mob/user)
- if(recipient && user != recipient)
- to_chat(user, "You can't open somebody else's mail! That's illegal!")
- return
+ if(!unwrap(user))
+ return FALSE
+ return after_unwrap(user)
- user.visible_message("You start to unwrap the package...")
+/// proc for unwrapping a mail. Goes just for an unwrapping procces, returns FALSE if it fails.
+/obj/item/mail/proc/unwrap(mob/user)
+ if(recipient_ref)
+ var/datum/mind/recipient = recipient_ref.resolve()
+ // If the recipient's mind has gone, then anyone can open their mail
+ // whether a mind can actually be qdel'd is an exercise for the reader
+ if(recipient && recipient != user?.mind)
+ to_chat(user, span_notice("You can't open somebody else's mail! That's illegal!"))
+ return FALSE
+
+ balloon_alert(user, "unwrapping...")
if(!do_after(user, 1.5 SECONDS, target = user))
- return
- user.temporarilyRemoveItemFromInventory(src, TRUE)
- if(contents.len)
- user.put_in_hands(contents[1])
- playsound(loc, 'sound/items/poster_ripped.ogg', 50, TRUE)
- qdel(src)
+ return FALSE
+ return TRUE
-/// Accepts a mob to initialize goodies for a piece of mail.
-/obj/item/mail/proc/initialize_for_recipient(mob/new_recipient)
- recipient = new_recipient
- name = "[initial(name)] for [new_recipient.real_name] ([new_recipient.job])"
+// proc that goes after unwrapping a mail.
+/obj/item/mail/proc/after_unwrap(mob/user)
+ user.temporarilyRemoveItemFromInventory(src, force = TRUE)
+ for(var/obj/stuff as anything in contents) // Mail and envelope actually can have more than 1 item.
+ if(isitem(stuff))
+ user.put_in_hands(stuff)
+ else
+ stuff.forceMove(drop_location())
+ playsound(loc, 'sound/items/poster_ripped.ogg', vol = 50, vary = TRUE)
+ qdel(src)
+ return TRUE
+
+/obj/item/mail/examine_more(mob/user)
+ . = ..()
+ if(!postmarked)
+ . += span_info("This mail has no postmarking of any sort...")
+ else
+ . += span_notice("You notice the postmarking on the front of the mail...")
+ var/datum/mind/recipient = recipient_ref.resolve()
+ if(recipient)
+ . += span_info("[postmarked ? "Certified NT" : "Uncertfieid"] mail for [recipient].")
+ else if(postmarked)
+ . += span_info("Certified mail for [GLOB.station_name].")
+ else
+ . += span_info("This is a dead letter mail with no recipient.")
+ . += span_info("Distribute by hand or via destination tagger using the certified NT disposal system.")
+
+/// Accepts a mind to initialize goodies for a piece of mail.
+/obj/item/mail/proc/initialize_for_recipient(datum/mind/recipient)
+ name = "[initial(name)] for [recipient.name] ([recipient.assigned_role])"
+ recipient_ref = WEAKREF(recipient)
+
+ var/mob/living/body = recipient.current
var/list/goodies = generic_goodies
- var/datum/job/this_job = SSjob.name_occupations[new_recipient.job]
+ var/datum/job/this_job = SSjob.name_occupations[recipient.assigned_role]
+ var/is_mail_restricted = FALSE // certain roles and jobs (prisoner) do not receive generic gifts
+
if(this_job)
if(this_job.paycheck_department && department_colors[this_job.paycheck_department])
color = department_colors[this_job.paycheck_department]
+
var/list/job_goodies = this_job.get_mail_goodies()
+ is_mail_restricted = this_job.exclusive_mail_goodies
if(LAZYLEN(job_goodies))
- // certain roles and jobs (prisoner) do not receive generic gifts.
- if(this_job.exclusive_mail_goodies)
+ if(is_mail_restricted)
goodies = job_goodies
else
goodies += job_goodies
- for(var/iterator = 0, iterator < goodie_count, iterator++)
+ if(!is_mail_restricted)
+ // the weighted list is 50 (generic items) + 50 (job items)
+ // every quirk adds 5 to the final weighted list (regardless the number of items or weights in the quirk list)
+ // 5% is not too high or low so that stacking multiple quirks doesn't tilt the weighted list too much
+ for(var/datum/quirk/quirk as anything in body.roundstart_quirks)
+ if(LAZYLEN(quirk.mail_goodies))
+ var/quirk_goodie = pick(quirk.mail_goodies)
+ goodies[quirk_goodie] = 5
+
+ // A little boost for the special times!
+ for(var/datum/holiday/holiday as anything in SSevents.holidays)
+ if(LAZYLEN(holiday.mail_goodies))
+ var/holiday_goodie = pick(holiday.mail_goodies)
+ goodies[holiday_goodie] = holiday.mail_goodies[holiday_goodie]
+
+ for(var/iterator in 1 to goodie_count)
var/target_good = pickweight(goodies)
- if(ispath(target_good, /datum/reagent))
- var/obj/item/reagent_containers/target_container = new /obj/item/reagent_containers/glass/bottle(src)
- target_container.reagents.add_reagent(target_good, target_container.volume)
- target_container.name = "[target_container.reagents.reagent_list[1].name] bottle"
- new_recipient.log_message("[key_name(new_recipient)] received reagent container [target_container.name] in the mail ([target_good])", LOG_GAME)
- else
- var/atom/movable/target_atom = new target_good(src)
- new_recipient.log_message("[key_name(new_recipient)] received [target_atom.name] in the mail ([target_good])", LOG_GAME)
+ var/atom/movable/target_atom = new target_good(src)
+ body.log_message("received [target_atom.name] in the mail ([target_good])", LOG_GAME)
return TRUE
@@ -190,41 +244,87 @@
name = "mail crate"
desc = "A certified post crate from CentCom."
icon_state = "mail"
+ base_icon_state = "mail"
+ ///if it'll show the nt mark on the crate
+ var/postmarked = TRUE
+
+/obj/structure/closet/crate/mail/update_icon_state()
+ . = ..()
+ if(opened)
+ icon_state = "[base_icon_state]open"
+ if(locate(/obj/item/mail) in src)
+ icon_state = base_icon_state
+ else
+ icon_state = "[base_icon_state]sealed"
+
+/obj/structure/closet/crate/mail/update_overlays()
+ . = ..()
+ if(postmarked)
+ . += "mail_nt"
+
+/// Fills this mail crate with N pieces of mail, where N is the lower of the amount var passed, and the maximum capacity of this crate. If N is larger than the number of alive human players, the excess will be junkmail.
+/obj/structure/closet/crate/mail/proc/populate(amount)
+ var/mail_count = min(amount, storage_capacity)
+ // Fills the
+ var/list/mail_recipients = list()
+
+ for(var/mob/living/carbon/human/human in GLOB.player_list)
+ if(human.stat == DEAD || !human.mind)
+ continue
+ // Skip wizards, nuke ops, cyborgs; Centcom does not send them mail
+ if(!(human.mind.assigned_role in get_all_jobs()))
+ continue
+
+ mail_recipients += human.mind
+
+ for(var/i in 1 to mail_count)
+ var/obj/item/mail/new_mail
+ if(prob(FULL_CRATE_LETTER_ODDS))
+ new_mail = new /obj/item/mail(src)
+ else
+ new_mail = new /obj/item/mail/envelope(src)
+
+ var/datum/mind/recipient = pick_n_take(mail_recipients)
+ if(recipient)
+ new_mail.initialize_for_recipient(recipient)
+ else
+ new_mail.junk_mail()
+
+ update_icon()
+
+/// Crate for mail that automatically depletes the economy subsystem's pending mail counter.
+/obj/structure/closet/crate/mail/economy/Initialize(mapload)
+ . = ..()
+ populate(SSeconomy.mail_waiting)
+ SSeconomy.mail_waiting = 0
/// Crate for mail that automatically generates a lot of mail. Usually only normal mail, but on lowpop it may end up just being junk.
/obj/structure/closet/crate/mail/full
name = "brimming mail crate"
desc = "A certified post crate from CentCom. Looks stuffed to the gills."
-/obj/structure/closet/crate/mail/update_icon_state()
+/obj/structure/closet/crate/mail/full/Initialize(mapload)
. = ..()
- if(opened)
- icon_state = "[initial(icon_state)]open"
- if(locate(/obj/item/mail) in src)
- icon_state = initial(icon_state)
- else
- icon_state = "[initial(icon_state)]sealed"
+ populate(INFINITY)
-/obj/structure/closet/crate/mail/full/Initialize()
- . = ..()
- var/list/mail_recipients = list()
- for(var/mob/living/carbon/human/alive in GLOB.player_list)
- if(alive.stat != DEAD)
- mail_recipients += alive
- for(var/iterator in 1 to storage_capacity)
- var/obj/item/mail/new_mail
- if(prob(FULL_CRATE_LETTER_ODDS))
- new_mail = new /obj/item/mail(src)
- else
- new_mail = new /obj/item/mail/envelope(src)
- var/mob/living/carbon/human/mail_to
- mail_to = pick(mail_recipients)
- if(mail_to)
- new_mail.initialize_for_recipient(mail_to)
- mail_recipients -= mail_to //Once picked, the mail crate will need a new recipient.
- else
- new_mail.junk_mail()
+///Used in the mail strike shuttle loan event
+/* /obj/structure/closet/crate/mail/full/mail_strike
+ desc = "A post crate from somewhere else. It has no NT logo on it."
+ postmarked = FALSE
+/obj/structure/closet/crate/mail/full/mail_strike/populate(amount)
+ var/strike_mail_to_spawn = rand(1, storage_capacity-1)
+ for(var/i in 1 to strike_mail_to_spawn)
+ if(prob(95))
+ new /obj/item/mail/mail_strike(src)
+ else
+ new /obj/item/mail/traitor/mail_strike(src)
+ return ..(storage_capacity - strike_mail_to_spawn) */
+
+/// Opened mail crate
+/obj/structure/closet/crate/mail/preopen
+ opened = TRUE
+ icon_state = "mailopen"
/// Mailbag.
/obj/item/storage/bag/mail
@@ -274,3 +374,106 @@
/obj/item/paper/fluff/junkmail_generic/Initialize()
. = ..()
info = pick(GLOB.junkmail_messages)
+
+/* Maybe some other time
+/obj/item/mail/traitor
+ var/armed = FALSE
+ var/datum/weakref/made_by_ref
+ /// Cached information about who made it for logging purposes
+ var/made_by_cached_name
+ /// Cached information about who made it for logging purposes
+ var/made_by_cached_ckey
+ goodie_count = 0
+
+/obj/item/mail/traitor/envelope
+ name = "envelope"
+ icon_state = "mail_large"
+ stamp_max = 2
+ stamp_offset_y = 5
+
+/obj/item/mail/traitor/after_unwrap(mob/user)
+ user.temporarilyRemoveItemFromInventory(src, force = TRUE)
+ playsound(loc, 'sound/items/poster_ripped.ogg', vol = 50, vary = TRUE)
+ for(var/obj/item/stuff as anything in contents) // Mail and envelope actually can have more than 1 item.
+ if(user.put_in_hands(stuff) && armed)
+ var/whomst = made_by_cached_name ? "[made_by_cached_name] ([made_by_cached_ckey])" : "no one in particular"
+ log_bomber(user, "opened armed mail made by [whomst], activating", stuff)
+ INVOKE_ASYNC(stuff, TYPE_PROC_REF(/obj/item, attack_self), user)
+ qdel(src)
+ return TRUE
+
+/obj/item/mail/traitor/multitool_act(mob/living/user, obj/item/tool)
+ if(armed == FALSE || user.get_inactive_held_item() != src)
+ return ..()
+ if(IS_WEAKREF_OF(user.mind, made_by_ref))
+ balloon_alert(user, "disarming trap...")
+ if(!do_after(user, 2 SECONDS, target = src))
+ return FALSE
+ balloon_alert(user, "disarmed")
+ playsound(src, 'sound/machines/defib_ready.ogg', vol = 100, vary = TRUE)
+ armed = FALSE
+ return TRUE
+ else
+ balloon_alert(user, "tinkering with something...")
+
+ if(!do_after(user, 2 SECONDS, target = src))
+ after_unwrap(user)
+ return FALSE
+ if(prob(50))
+ balloon_alert(user, "disarmed something...?")
+ playsound(src, 'sound/machines/defib_ready.ogg', vol = 100, vary = TRUE)
+ armed = FALSE
+ return TRUE
+ else
+ after_unwrap(user)
+ return TRUE
+
+///Generic mail used in the mail strike shuttle loan event
+/obj/item/mail/mail_strike
+ name = "dead mail"
+ desc = "An unmarked parcel of unknown origins, effectively undeliverable."
+ postmarked = FALSE
+ generic_goodies = list(
+ /obj/effect/spawner/random/entertainment/money_medium = 2,
+ /obj/effect/spawner/random/contraband = 2,
+ /obj/effect/spawner/random/entertainment/money_large = 1,
+ /obj/effect/spawner/random/entertainment/coin = 1,
+ /obj/effect/spawner/random/food_or_drink/any_snack_or_beverage = 1,
+ /obj/effect/spawner/random/entertainment/drugs = 1,
+ /obj/effect/spawner/random/contraband/grenades = 1,
+ )
+
+/obj/item/mail/mail_strike/Initialize(mapload)
+ if(prob(35))
+ stamped = FALSE
+ if(prob(35))
+ name = "dead envelope"
+ icon_state = "mail_large"
+ goodie_count = 2
+ stamp_max = 2
+ stamp_offset_y = 5
+ . = ..()
+ color = pick(COLOR_SILVER, COLOR_DARK, COLOR_DRIED_TAN, COLOR_ORANGE_BROWN, COLOR_BROWN, COLOR_SYNDIE_RED)
+ for(var/goodie in 1 to goodie_count)
+ var/target_good = pick_weight(generic_goodies)
+ new target_good(src)
+
+///Also found in the mail strike shuttle loan. It contains a random grenade that'll be triggered when unwrapped
+/obj/item/mail/traitor/mail_strike
+ name = "dead mail"
+ desc = "An unmarked parcel of unknown origins, effectively undeliverable."
+ postmarked = FALSE
+
+/obj/item/mail/traitor/mail_strike/Initialize(mapload)
+ if(prob(35))
+ stamped = FALSE
+ if(prob(35))
+ name = "dead envelope"
+ icon_state = "mail_large"
+ goodie_count = 2
+ stamp_max = 2
+ stamp_offset_y = 5
+ . = ..()
+ color = pick(COLOR_SILVER, COLOR_DARK, COLOR_DRIED_TAN, COLOR_ORANGE_BROWN, COLOR_BROWN, COLOR_SYNDIE_RED)
+ new /obj/effect/spawner/random/contraband/grenades/dangerous(src)
+*/
diff --git a/code/modules/holiday/holidays.dm b/code/modules/holiday/holidays.dm
index 6afda0ed20..9a8f4329ab 100644
--- a/code/modules/holiday/holidays.dm
+++ b/code/modules/holiday/holidays.dm
@@ -12,6 +12,9 @@
var/year_offset = 0
var/obj/item/drone_hat //If this is defined, drones without a default hat will spawn with this one during the holiday; check drones_as_items.dm to see this used
+ // Special things to be given during this!
+ var/list/mail_goodies
+
// This proc gets run before the game starts when the holiday is activated. Do festive shit here.
/datum/holiday/proc/celebrate()
return
@@ -398,6 +401,11 @@
end_day = 2
end_month = NOVEMBER
+ mail_goodies = list(
+ /obj/item/reagent_containers/food/snacks/lollipop = 10,
+ /obj/item/reagent_containers/food/snacks/chocolatebar = 10
+ )
+
/datum/holiday/halloween/greet()
return "Have a spooky Halloween!"
diff --git a/code/modules/shuttle/supply.dm b/code/modules/shuttle/supply.dm
index 0cebcbb20c..ea5b4aed0d 100644
--- a/code/modules/shuttle/supply.dm
+++ b/code/modules/shuttle/supply.dm
@@ -304,40 +304,16 @@ GLOBAL_LIST_INIT(cargo_shuttle_leave_behind_typecache, typecacheof(list(
//Early return if there's no mail waiting to prevent taking up a slot.
if(!SSeconomy.mail_waiting)
return
+
//spawn crate
var/list/empty_turfs = list()
- for(var/place as anything in shuttle_areas)
- var/area/shuttle/shuttle_area = place
+ for(var/area/shuttle/shuttle_area as anything in shuttle_areas)
for(var/turf/open/floor/shuttle_floor in shuttle_area)
if(is_blocked_turf(shuttle_floor))
continue
empty_turfs += shuttle_floor
- var/obj/structure/closet/crate/mail/mailcrate = new(pick(empty_turfs))
- //collect recipients
- var/list/mail_recipients = list()
- for(var/mob/living/carbon/human/player_human in GLOB.player_list)
- if(player_human.stat != DEAD)
- mail_recipients += player_human
-
- //Creates mail for all the mail waiting to arrive, if there's nobody to recieve it it's just junkmail.
- for(var/mail_iterator in 1 to SSeconomy.mail_waiting)
- var/obj/item/mail/new_mail
- if(prob(FULL_CRATE_LETTER_ODDS))
- new_mail = new /obj/item/mail(mailcrate)
- else
- new_mail = new /obj/item/mail/envelope(mailcrate)
- var/mob/living/carbon/human/mail_to
- if(mail_recipients.len)
- mail_to = pick(mail_recipients)
- new_mail.initialize_for_recipient(mail_to)
- mail_recipients -= mail_to
- else
- new_mail.junk_mail()
- if(new_mail)
- SSeconomy.mail_waiting += 1
- mailcrate.update_icon()
- return mailcrate
+ new /obj/structure/closet/crate/mail/economy(pick(empty_turfs))
#undef GOODY_FREE_SHIPPING_MAX
#undef CRATE_TAX
diff --git a/icons/obj/crates.dmi b/icons/obj/crates.dmi
index 1310342019..5c1be2ecc1 100644
Binary files a/icons/obj/crates.dmi and b/icons/obj/crates.dmi differ