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