diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 3354f6306d3..894f83531b0 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -187,6 +187,7 @@ #define INIT_ORDER_MINOR_MAPPING -40 #define INIT_ORDER_PATH -50 #define INIT_ORDER_EXPLOSIONS -69 +#define INIT_ORDER_CREDITS -93 #define INIT_ORDER_STATPANELS -97 #define INIT_ORDER_BAN_CACHE -98 #define INIT_ORDER_INIT_PROFILER -99 //Near the end, logs the costs of initialize diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index 9d9afc57bcb..82505fc26e5 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -1291,3 +1291,94 @@ GLOBAL_LIST_EMPTY(transformation_animation_objects) if(PLANE_TO_TRUE(underlay.plane) != base_plane) appearance.underlays -= underlay return appearance + + +/// Renders a ckey's preferences appearance from their savefile +/proc/render_offline_appearance(ckey, mob/living/carbon/human/dummy/our_human) + if(!ckey || is_guest_key(ckey) || (!isnull(our_human) && !istype(our_human))) + return FALSE + var/save_path = "data/player_saves/[ckey[1]]/[ckey]/preferences.json" + if(!fexists(save_path)) + return FALSE + var/list/tree = json_decode(rustg_file_read(save_path)) // Reading savefile + var/default_slot = tree["default_slot"] || 1 + var/selected_char = tree["character[default_slot]"] || tree["character1"] + + var/list/job_preferences = SANITIZE_LIST(selected_char?["job_preferences"]) + + var/datum/job/selected_job + var/highest_pref = 0 + + for(var/job in job_preferences) + if(job_preferences[job] > highest_pref) + selected_job = SSjob.get_job(job) + highest_pref = job_preferences[job] + + if(selected_job && !ispath(selected_job::spawn_type, /mob/living/carbon/human)) // If the selected job's spawn_type isnt human (AI, cyborg etc.) we just returning mutable_appearance + var/mob/living/spawn_type = selected_job::spawn_type + var/mutable_appearance/appearance = mutable_appearance(spawn_type::icon, spawn_type::icon_state) + appearance.name = ckey + if(ispath(spawn_type, /mob/living/silicon/ai)) + var/ai_core_value = selected_char?["preferred_ai_core_display"] + appearance.icon_state = resolve_ai_icon_sync(ai_core_value) + return appearance + + var/we_created = FALSE + if(isnull(our_human)) + our_human = new() + we_created = TRUE + else + our_human.wipe_state() // We're wiping the dummys overlays and outfit + our_human.set_species(/datum/species/human) // We're setting it to human beacuse if the savefile doesnt have species entry, it doesnt use previous icon's species + our_human.icon_render_keys = list() + our_human.update_body(is_creating = TRUE) // We're recreating bodyparts etc. + + for (var/datum/preference/preference as anything in get_preferences_in_priority_order()) // Apply the preferences in priority order + if (preference.savefile_identifier != PREFERENCE_CHARACTER) + continue + var/saved_data = selected_char?[preference.savefile_key] + if(!saved_data) + continue + var/new_value = preference.deserialize(saved_data) + + preference.apply_to_human(our_human, new_value) + + var/datum/outfit/equipped_outfit + + if(selected_job) // Selecting and creating outfit datum + var/datum/outfit/outfit_type + if(selected_job::outfit) + outfit_type = selected_job::outfit + if(selected_char?["species"] == SPECIES_PLASMAMAN && selected_job::plasmaman_outfit) // If they are plasmaman, give them plasmaman outfit + outfit_type = selected_job::outfit + if(outfit_type) + equipped_outfit = new outfit_type() + else + equipped_outfit = new() + + var/list/loadout_preference_list = SANITIZE_LIST(selected_char?["loadout_list"]) + var/datum/preference/loadout/loadout_preference = GLOB.preference_entries[/datum/preference/loadout] + + var/list/loadout_datums = loadout_list_to_datums(loadout_preference.deserialize(loadout_preference_list)) + + for(var/datum/loadout_item/item as anything in loadout_datums) // Adding loadout items to outfit + item.insert_path_into_outfit(equipped_outfit, our_human, TRUE) + + equipped_outfit.equip(our_human, TRUE) + + var/list/all_quirks = SANITIZE_LIST(selected_char?["all_quirks"]) + if(SSquirks?.initialized) + our_human.cleanse_quirk_datums() // We need to clean all the quirk datums every time + for(var/quirk_name as anything in all_quirks) + var/datum/quirk/quirk_type = SSquirks.quirks[quirk_name] + if(!(initial(quirk_type.quirk_flags) & QUIRK_CHANGES_APPEARANCE)) + continue + our_human.add_quirk(quirk_type) + + var/mutable_appearance/appearance = new + appearance.appearance = our_human.appearance + appearance.name = ckey + if(we_created) + qdel(our_human) + + return appearance diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index 8f04ec2bd9b..6c00aacc2f7 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -1,6 +1,9 @@ #define POPCOUNT_SURVIVORS "survivors" //Not dead at roundend #define POPCOUNT_ESCAPEES "escapees" //Not dead and on centcom/shuttles marked as escaped +#define POPCOUNT_ESCAPEES_HUMANONLY "human_escapees" +#define POPCOUNT_ESCAPEES_HUMANONLY_LIST "human_escapees_list" #define POPCOUNT_SHUTTLE_ESCAPEES "shuttle_escapees" //Emergency shuttle only. +#define POPCOUNT_STATION_INTEGRITY "station_integrity" #define PERSONAL_LAST_ROUND "personal last round" #define SERVER_LAST_ROUND "server last round" @@ -14,7 +17,9 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) var/list/file_data = list("escapees" = list("humans" = list(), "silicons" = list(), "others" = list(), "npcs" = list()), "abandoned" = list("humans" = list(), "silicons" = list(), "others" = list(), "npcs" = list()), "ghosts" = list(), "additional data" = list()) var/num_survivors = 0 //Count of non-brain non-eye mobs with mind that are alive var/num_escapees = 0 //Above and on centcom z - var/num_shuttle_escapees = 0 //Above and on escape shuttle + var/num_human_escapees = 0 //Above but humans only + var/num_shuttle_escapees = 0 //Above's above and on escape shuttle + var/list/list_of_human_escapees = list() //References to all escaped humans var/list/area/shuttle_areas if(SSshuttle?.emergency) shuttle_areas = SSshuttle.emergency.shuttle_areas @@ -39,6 +44,9 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) escape_status = "escapees" if(shuttle_areas[get_area(M)]) num_shuttle_escapees++ + if(ishuman(M)) + num_human_escapees++ + list_of_human_escapees += M if(isliving(M)) var/mob/living/L = M mob_data["location"] = get_area(L) @@ -91,8 +99,8 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) var/datum/station_state/end_state = new /datum/station_state() end_state.count() - var/station_integrity = min(PERCENT(GLOB.start_state.score(end_state)), 100) - file_data["additional data"]["station integrity"] = station_integrity + var/roundend_station_integrity = min(PERCENT(GLOB.start_state.score(end_state)), 100) + file_data["additional data"]["station integrity"] = roundend_station_integrity WRITE_FILE(json_file, json_encode(file_data)) SSblackbox.record_feedback("nested tally", "round_end_stats", num_survivors, list("survivors", "total")) @@ -102,8 +110,10 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) . = list() .[POPCOUNT_SURVIVORS] = num_survivors .[POPCOUNT_ESCAPEES] = num_escapees + .[POPCOUNT_ESCAPEES_HUMANONLY] = num_human_escapees .[POPCOUNT_SHUTTLE_ESCAPEES] = num_shuttle_escapees - .["station_integrity"] = station_integrity + .[POPCOUNT_ESCAPEES_HUMANONLY_LIST] = list_of_human_escapees + .[POPCOUNT_STATION_INTEGRITY] = roundend_station_integrity /datum/controller/subsystem/ticker/proc/gather_antag_data() var/team_gid = 1 @@ -220,6 +230,9 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) LAZYCLEARLIST(round_end_events) var/speed_round = (STATION_TIME_PASSED() <= 10 MINUTES) + popcount = gather_roundend_feedback() + SScredits.draft() + SScredits.finalize() for(var/client/C in GLOB.clients) if(!C?.credits) @@ -229,7 +242,6 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) C?.give_award(/datum/award/achievement/misc/speed_round, C?.mob) HandleRandomHardcoreScore(C) - var/popcount = gather_roundend_feedback() display_report(popcount) CHECK_TICK diff --git a/code/_onclick/hud/credits.dm b/code/_onclick/hud/credits.dm index 2ce3923c887..68992954693 100644 --- a/code/_onclick/hud/credits.dm +++ b/code/_onclick/hud/credits.dm @@ -1,28 +1,43 @@ -#define CREDIT_ROLL_SPEED 125 -#define CREDIT_SPAWN_SPEED 10 -#define CREDIT_ANIMATE_HEIGHT (14 * ICON_SIZE_Y) -#define CREDIT_EASE_DURATION 22 -#define CREDITS_PATH "[global.config.directory]/contributors.dmi" +#define CREDIT_ROLL_SPEED 9 SECONDS +#define CREDIT_SPAWN_SPEED 1 SECONDS +#define CREDIT_ANIMATE_HEIGHT (16 * world.icon_size) +#define CREDIT_EASE_DURATION 2.2 SECONDS /client/proc/RollCredits() set waitfor = FALSE - if(!fexists(CREDITS_PATH)) + if(!prefs?.read_preference(/datum/preference/toggle/show_roundend_credits)) return - var/icon/credits_icon = new(CREDITS_PATH) LAZYINITLIST(credits) var/list/_credits = credits add_verb(src, /client/proc/ClearCredits) - var/static/list/credit_order_for_this_round - if(isnull(credit_order_for_this_round)) - credit_order_for_this_round = list("Thanks for playing!") + (shuffle(icon_states(credits_icon)) - "Thanks for playing!") + var/list/credit_order_for_this_round = SScredits.credit_order_for_this_round + + var/count = 0 for(var/I in credit_order_for_this_round) if(!credits) return - _credits += new /atom/movable/screen/credit(null, null, I, src, credits_icon) - sleep(CREDIT_SPAWN_SPEED) + if(istype(I, /obj/effect/title_card_object)) //huge image sleep + sleep(CREDIT_SPAWN_SPEED * 3.3) + count = 0 + if(count && !istype(I, /mutable_appearance) && !istype(I, /obj/effect/cast_object)) + sleep(CREDIT_SPAWN_SPEED) + + _credits += new /atom/movable/screen/credit(null, null, I, src) + if(istype(I, /mutable_appearance)) + count++ + if(count >= 6) + count = 0 + sleep(CREDIT_SPAWN_SPEED) + else if(istype(I, /obj/effect/cast_object)) + count++ + if(count >= 2) + count = 0 + sleep(CREDIT_SPAWN_SPEED) + else + sleep(CREDIT_SPAWN_SPEED) + count = 0 sleep(CREDIT_ROLL_SPEED - CREDIT_SPAWN_SPEED) - remove_verb(src, /client/proc/ClearCredits) - qdel(credits_icon) + INVOKE_ASYNC(src, TYPE_PROC_REF(/client, ClearCredits)) /client/proc/ClearCredits() set name = "Hide Credits" @@ -34,20 +49,54 @@ /atom/movable/screen/credit mouse_opacity = MOUSE_OPACITY_TRANSPARENT alpha = 0 - screen_loc = "12,1" plane = SPLASHSCREEN_PLANE + screen_loc = "3,1" var/client/parent var/matrix/target -/atom/movable/screen/credit/Initialize(mapload, datum/hud/hud_owner, credited, client/P, icon/I) +/atom/movable/screen/credit/Initialize(mapload, datum/hud/hud_owner, credited, client/P) . = ..() - icon = I parent = P - icon_state = credited - maptext = MAPTEXT_PIXELLARI(credited) - maptext_x = ICON_SIZE_X + 8 - maptext_y = (ICON_SIZE_Y / 2) - 4 - maptext_width = ICON_SIZE_X * 3 + var/view = P?.view + var/list/offsets = screen_loc_to_offset("3,1", view) + + if(istype(credited, /mutable_appearance)) + var/mutable_appearance/choice = credited + choice.plane = plane + choice.screen_loc = screen_loc + choice.alpha = alpha + maptext_width = choice.maptext_width + maptext = choice.maptext + appearance = choice.appearance + screen_loc = offset_to_screen_loc(offsets[1] + choice.pixel_x, offsets[2] + choice.pixel_y) + add_overlay(choice) + + if(istype(credited, /obj/effect/title_card_object)) + var/obj/effect/title_card_object/choice = credited + choice.plane = plane + choice.screen_loc = screen_loc + choice.alpha = alpha + maptext_width = choice.maptext_width + maptext = choice.maptext + appearance = choice.appearance + screen_loc = offset_to_screen_loc(offsets[1] + choice.pixel_x, offsets[2] + choice.pixel_y) + add_overlay(choice) + + if(istype(credited, /obj/effect/cast_object)) + var/obj/effect/cast_object/choice = credited + maptext = MAPTEXT_PIXELLARI(choice.maptext) + maptext_x = choice.maptext_x + maptext_y = choice.maptext_y + maptext_width = choice.maptext_width + maptext_height = choice.maptext_height + + if(istext(credited)) + maptext = MAPTEXT_PIXELLARI(credited) + maptext_x = world.icon_size + 8 + maptext_y = (world.icon_size / 2) - 4 + maptext_width = world.icon_size * 12 + maptext_height = world.icon_size * 3 + var/matrix/M = matrix(transform) M.Translate(0, CREDIT_ANIMATE_HEIGHT) animate(src, transform = M, time = CREDIT_ROLL_SPEED) @@ -55,14 +104,12 @@ animate(src, alpha = 255, time = CREDIT_EASE_DURATION, flags = ANIMATION_PARALLEL) addtimer(CALLBACK(src, PROC_REF(FadeOut)), CREDIT_ROLL_SPEED - CREDIT_EASE_DURATION) QDEL_IN(src, CREDIT_ROLL_SPEED) - if(parent) - parent.screen += src + parent?.screen += src /atom/movable/screen/credit/Destroy() - icon = null if(parent) parent.screen -= src - LAZYREMOVE(parent.credits, src) + parent.credits -= src parent = null return ..() @@ -73,4 +120,3 @@ #undef CREDIT_EASE_DURATION #undef CREDIT_ROLL_SPEED #undef CREDIT_SPAWN_SPEED -#undef CREDITS_PATH diff --git a/code/controllers/subsystem/credits.dm b/code/controllers/subsystem/credits.dm new file mode 100644 index 00000000000..2d96ad1dcdb --- /dev/null +++ b/code/controllers/subsystem/credits.dm @@ -0,0 +1,625 @@ +SUBSYSTEM_DEF(credits) + name = "Roundend Credits" + wait = 10 MINUTES + init_order = INIT_ORDER_CREDITS + + var/list/disclaimers = list() + var/list/datum/episode_name/episode_names = list() + + var/episode_name = "" + + var/list/episode_string + var/list/disclaimers_string + var/list/cast_string + + var/customized_name = "" + + var/list/patron_appearances = list() + var/list/admin_appearances = list() + var/list/antag_appearances = list() + + var/list/processing_icons = list() + var/list/currentrun = list() + + var/list/credit_order_for_this_round = list() + +/datum/controller/subsystem/credits/Initialize() +#if defined(UNIT_TESTS) + return SS_INIT_NO_NEED +#else + generate_patron_icons() + return SS_INIT_SUCCESS +#endif + +/datum/controller/subsystem/credits/fire(resumed = 0) + if (!resumed) + src.currentrun = processing_icons.Copy() + + //cache for sanic speed + var/list/currentrun = src.currentrun + + while(currentrun.len) + var/datum/weakref/weakref = currentrun[currentrun.len] + var/mutable_appearance/appearance = currentrun[weakref] + currentrun.len-- + var/datum/mind/antag_mind = weakref.resolve() + var/mob/living/living_mob = antag_mind?.current + if(!isnull(living_mob) && living_mob.stat != DEAD) + appearance.appearance = living_mob.appearance + appearance.transform = matrix() + appearance.setDir(SOUTH) + var/bound_width = living_mob.bound_width || world.icon_size + appearance.maptext_width = 88 + appearance.maptext_height = world.icon_size * 1.5 + appearance.maptext_x = ((88 - bound_width) * -0.5) - living_mob.base_pixel_x + appearance.maptext_y = -16 + appearance.maptext = "
[antag_mind.name]
" + if (MC_TICK_CHECK) + return + +/datum/controller/subsystem/credits/Recover() + processing_icons = SScredits.processing_icons + antag_appearances = SScredits.antag_appearances + customized_name = SScredits.customized_name + patron_appearances = SScredits.patron_appearances + +/datum/controller/subsystem/credits/proc/draft() + if(!customized_name) + draft_episode_names() + draft_disclaimers() + draft_caststring() + generate_admin_icons() + +/datum/controller/subsystem/credits/proc/finalize() + finalize_name() + finalize_episodestring() + finalize_disclaimerstring() + finalize_credits() + +/datum/controller/subsystem/credits/proc/finalize_name() + if(customized_name) + episode_name = customized_name + return + var/list/drafted_names = list() + for(var/datum/episode_name/N as anything in episode_names) + drafted_names["[N.name]"] = N.weight + episode_name = pick_weight(drafted_names) + +/datum/controller/subsystem/credits/proc/finalize_episodestring() + var/season = time2text(world.timeofday,"YY") + var/episodenum = GLOB.round_id || 1 + episode_string = list("
SEZON [season] BÖLÜM [episodenum]
") + episode_string += "
[episode_name]
" + +/datum/controller/subsystem/credits/proc/finalize_disclaimerstring() + disclaimers_string = list() + for(var/disclaimer in disclaimers) + disclaimers_string += "
[disclaimer]
" + +/datum/controller/subsystem/credits/proc/finalize_credits() + credit_order_for_this_round = list() + credit_order_for_this_round += episode_string + credit_order_for_this_round += "" + credit_order_for_this_round += disclaimers_string + credit_order_for_this_round += cast_string + var/list/admins = shuffle(admin_appearances) + var/admins_length = length(admins) + var/y_offset = 0 + if(admins_length) + credit_order_for_this_round += "
Yetkili Ekibi
" + for(var/i in 1 to CEILING(admins_length / 6, 1)) + var/x_offset = -16 + for(var/b in 1 to 6) + var/mutable_appearance/picked = pick_n_take(admins) + if(!picked) + break + picked.pixel_x = x_offset + picked.pixel_y = y_offset + x_offset += 96 + credit_order_for_this_round += picked + + var/list/patrons = shuffle(patron_appearances) + var/patrons_length = length(patrons) + if(patrons_length) + credit_order_for_this_round += "
Sevgili Destekçilerimiz
" + for(var/i in 1 to CEILING(patrons_length / 6, 1)) + var/x_offset = -16 + for(var/b in 1 to 6) + var/mutable_appearance/picked = pick_n_take(patrons) + if(!picked) + break + picked.pixel_x = x_offset + picked.pixel_y = y_offset + x_offset += 96 + credit_order_for_this_round += picked + + for(var/obj/effect/title_card_object/MA as anything in antag_appearances) + credit_order_for_this_round += MA + var/list/antagonist_icons = antag_appearances[MA] + var/antagonists_length = length(antagonist_icons) + for(var/i in 1 to CEILING(antagonists_length / 6, 1)) + var/x_offset = -16 + for(var/b in 1 to 6) + if(!length(antagonist_icons)) + break + var/reference = pick(antagonist_icons) + var/mutable_appearance/picked = antagonist_icons[reference] + antagonist_icons -= reference + if(!picked) + break + picked.pixel_x = x_offset + picked.pixel_y = y_offset + x_offset += 96 + credit_order_for_this_round += picked + +/datum/controller/subsystem/credits/proc/draft_disclaimers() + disclaimers += "[locale_suffix_locative(station_name())] çekilmiştir.
" + disclaimers += "BYOND© kameraları ve lensleri ile çekilmiştir. Uzay görüntüleri NASA tarafından sağlanmıştır.
" + disclaimers += "Özel görsel efektler LUMMOX® JR. Motion Picture Productions tarafından sağlanmıştır.
" + disclaimers += "Tüm hakları saklıdır.
" + disclaimers += "
" + disclaimers += "Tüm tehlikeli sahneler düşük maaşlı ve harcanabilir stajyerler tarafından gerçekleştirilmiştir. EVDE DENEMEYİN.
" + disclaimers += "Bu film, Türkiye ve evrenin dört bir yanındaki tüm ülkelerin telif hakkı yasaları tarafından korunmaktadır(korunmamaktadır).
" + disclaimers += "İlk yayınlanan ülke: Türkiye.

" + disclaimers += "Bu filmin veya herhangi bir kısmının (ses bandı dahil) izinsiz gösterimi, dağıtımı veya kopyalanması ilgili telif hakkı yasalarının ihlalidir ve suçlu kişiyi hukuki ve cezai yaptırımlara tabi tutar.
" + disclaimers += "Bu yapımda anlatılan hikaye, tüm isimler, karakterler ve olaylar tamamen kurgusaldır.
" + disclaimers += "Gerçek kişi (yaşayan veya ölü), mekan, bina veya ürünlerle herhangi bir benzerlik amaçlanmamıştır ve böyle bir bağlantı çıkarılmamalıdır.
" + +/datum/controller/subsystem/credits/proc/draft_caststring() + cast_string = list("
OYUNCULAR:
") + var/is_anyone_there = FALSE + for(var/mob/living/carbon/human/H in GLOB.player_list) + if(!H.ckey && !(H.stat == DEAD) && !H.mind) + continue + + var/obj/effect/cast_object/human_name = new + human_name.maptext = "

[H.mind.name]

" + human_name.maptext_width = 232 + + var/obj/effect/cast_object/human_key = new + human_key.maptext = "

[H.mind.key]

" + human_key.maptext_width = 232 + human_key.maptext_x = 240 + + cast_string += list(human_name, human_key) + is_anyone_there = TRUE + CHECK_TICK + + for(var/mob/living/silicon/S in GLOB.silicon_mobs) + if(!S.ckey && !S.mind) + continue + var/obj/effect/cast_object/silicon_name = new + silicon_name.maptext = "

[S.name]

" + silicon_name.maptext_width = 224 + + var/obj/effect/cast_object/silicon_key = new + silicon_key.maptext = "

[S.mind.key]

" + silicon_key.maptext_width = 224 + silicon_key.maptext_x = 240 + + cast_string += list(silicon_name, silicon_key) + is_anyone_there = TRUE + CHECK_TICK + + if(!is_anyone_there) + cast_string += "
Kimse!
" + + var/is_anyone_died = FALSE + cast_string += "

[pick("GERÇEK OLAYLARDAN İLHAM ALINMIŞTIR","GERÇEK BİR HİKAYEDEN ESİNLENİLMİŞTİR")]

" + + for(var/mob/living/carbon/human/H in GLOB.dead_mob_list) + if(!H.last_mind || !H.ckey) + continue + if(!is_anyone_died) + cast_string += "
Hayatta kalamayanların anısına.

" + is_anyone_died = TRUE + + var/obj/effect/cast_object/human_name = new + human_name.maptext = "

[H.last_mind.name]

" + human_name.maptext_width = 224 + + var/obj/effect/cast_object/human_key = new + human_key.maptext = "

[H.last_mind.key]

" + human_key.maptext_width = 224 + human_key.maptext_x = 240 + + cast_string += list(human_name, human_key) + CHECK_TICK + + cast_string += "
" + +/datum/controller/subsystem/credits/proc/draft_episode_names() + var/uppr_name = locale_uppertext(station_name()) + + episode_names += new /datum/episode_name("[locale_uppertext(locale_suffix_genitive(pick(200;"[uppr_name]", 150;"ASTRONOT", 150;"INSANLIK", "İTİBAR", "AKIL SAĞLIĞI", "BİLİM", "MERAK", "ÇALIŞANLAR", "PARANOYA", "ŞEMPANZELER")))] [pick("DÜŞÜŞÜ", "YÜKSELİŞİ", "SORUNU", "KARANLIK YÜZÜ")] ") + episode_names += new /datum/episode_name("MÜRETTEBAT [pick("REFAHDA", "TÜKENDİ", "HAREKETE GEÇİYOR", "PLAZMA KRİZİNİ ÇÖZÜYOR", "YOLA ÇIKIYOR", "YÜKSELİYOR", "EMEKLİ OLUYOR", "CEHENNEME GİDİYOR", "KLİP ÇEKİYOR", "DENETLENIYOR", "HAYATSIZLAŞIYOR", "SALDIRIYOR", "ÇOK İLERİ GİDİYOR", "KAZANIYOR... AMA NE PAHASINA!!")]") + episode_names += new /datum/episode_name("MÜRETTEBATIN [pick("GÜNÜBİRLİK GEZİSİ", "SON GÜNÜ", "[pick("ÇILGIN", "TUHAF", "SÖNÜK", "BEKLENMEDİK")] TATİLİ", "FİKİR DEĞİŞİMİ", "YENİ RİTMİ", "OKUL MÜZİKALİ", "TARİH DERSİ", "UÇAN SİRKİ", "KÜÇÜK BİR SORUNU", "BÜYÜK BAŞARISI", "HATA KAYITLARI", "KÜÇÜK SIRRI", "ÖZEL TEKLİFİ", "UZMANLIĞI", "ZAYIF NOKTASI", "MERAKI", "ALİBİSİ", "MİRASI", "KEŞFİ", "SON OYUNU", "KURTARMA OPERASYONU", "İNTİKAMI")]") + episode_names += new /datum/episode_name("İSTASYONUN [pick("UZAY", "SEKSİ", "BÜYÜLÜ", "KİRLİ", "SİLAH", "REKLAM", "KÖPEK", "KARBONMONOKSİT", "NİNJA", "SİHİRBAZ", "SOKRATİK", "GENÇLİK SUÇLULUĞU", "POLİTİK GÜDÜMLÜ", "RADİKAL DERECEDE HARİKA", "KURUMSAL", "MEGA")] [pick("MACERASI", "GÜCÜ", "YOLCULUĞU", "YOLU")]", 25) + var/roundend_station_integrity = SSticker.popcount[POPCOUNT_STATION_INTEGRITY] + switch(roundend_station_integrity) + if(0 to 50) + episode_names += new /datum/episode_name("[pick("MÜRETTEBATIN CEZASI", "BİR HALKLA İLİŞKİLER KÂBUSU", "[uppr_name]: ULUSAL BİR MESELE", "MÜRETTEBATTAN ÖZÜR DİLERİZ", "MÜRETTEBAT TOZU YUTUYOR", "MÜRETTEBAT HER ŞEYİ BATIRIYOR", "MÜRETTEBAT HAYALLERİNDEN VAZGEÇİYOR", "MÜRETTEBATIN SONU GELDİ", "MÜRETTEBAT TELEVİZYONA ÇIKMAMALI", "[locale_uppertext(locale_suffix_genitive(uppr_name))] SONU GELDİĞİNİ BİLİYORUZ")]", 250) + if(80 to 100) + episode_names += new /datum/episode_name("[pick("MÜRETTEBATIN GEZİSİ", "CENNETİN BU TARAFI", "[uppr_name]: BİR DURUM KOMEDİSİ", "MÜRETTEBATIN ÖĞLE MOLASI", "MÜRETTEBAT YENİDEN İŞE DÖNÜYOR", "MÜRETTEBATIN BÜYÜK ÇIKIŞI", "MÜRETTEBAT GÜNÜ KURTARIYOR", "MÜRETTEBAT DÜNYAYA HÜKMEDİYOR", "TÜM BİLİM, GELİŞİM, TERFİLER VE HAVALI ŞEYLERİN OLDUĞU BÖLÜM", "DÖNÜM NOKTASI")]", 250) + + switch(SSdynamic.threat_level) + if(0 to 65) + episode_names += new /datum/episode_name("[pick("O GÜN [uppr_name] DURDU", "BOŞ YERE KOPAN FIRTINA", "SESSİZLİĞİN KİRALIK OLDUĞU YER", "YANILTMA TAKTİĞİ", "EVDE TEK BAŞINA", "YA BÜYÜK OYNA YA DA [uppr_name]", "PLASEBO ETKİSİ", "YANKILAR", "SESSİZ ORTAKLAR", "BÖYLE DOSTLARIN OLDUĞUNDA...", "FIRTINANIN GÖZÜ", "USLU DOĞDUK", "DURGUN SULAR")]", 150) + if(roundend_station_integrity && roundend_station_integrity < 35) + episode_names += new /datum/episode_name("[pick("NASIL OLDU DA HER ŞEY BU KADAR YANLIŞ GİTTİ?!", "BUNU YÖNETİCİLERE AÇIKLAYIN", "MÜRETTEBAT SAFARİDE", "EN BÜYÜK DÜŞMANIMIZ", "İÇERİDEN İŞ", "VEKİL KATİL")]", roundend_station_integrity/150*-2) + if(66 to 79) + episode_names += new /datum/episode_name("[pick("KAN DÖKÜLEBİLİR", "O [locale_uppertext(locale_suffix_ablative(uppr_name))] GELDİ!", "[uppr_name] OLAYI", "İÇİMİZDEKİ DÜŞMAN", "ÖĞLE ÇILGINLIĞI", "SAAT ON İKİYİ VURDUĞUNDA", "ÖZGÜVEN VE PARANOYA", "FAZLA KAÇAN ŞAKA", "İKİYE BÖLÜNMÜŞ EV", "[uppr_name] YARDIMA KOŞUYOR!", "[locale_uppertext(locale_suffix_ablative(uppr_name))] KAÇIŞ", \ + "HİT VE KAÇ", "UYANIŞ", "BÜYÜK KAÇIŞ", "[locale_uppertext(locale_suffix_genitive(uppr_name))] SON AYARTISI", "[locale_uppertext(locale_suffix_genitive(uppr_name))] DÜŞÜŞÜ", "ATEŞLE OYNAMAK", "BASKI ALTINDA", "SON GÜNDEN ÖNCEKİ GÜN", "[locale_uppertext(locale_suffix_genitive(uppr_name))] ARANANLARI")]", 150) + if(80 to 100) + episode_names += new /datum/episode_name("[pick("SALDIRI! SALDIRI! SALDIRI!", "DELİLİĞİ TAMİR EDEMEZSİN", "KIYAMET", "KIYAMETİN TADI", "OPERASYON: YOK EDİN!", "KUSURSUZ FIRTINA", "MÜRETTEBATIN ZAMANI DOLDU", "HERKES [locale_uppertext(locale_suffix_ablative(uppr_name))] NEFRET EDİYOR", "[uppr_name] SAVAŞI", \ + "KAPIŞMA", "İNSAN AVI", "KAVGANIN OLDUĞU BÖLÜM", "[locale_uppertext(locale_suffix_genitive(uppr_name))] HESAP GÜNÜ", "İNCELEN KIRMIZI ÇİZGİ", "EMEKLİLİĞE BİR GÜN KALA")]", 250) + if(get_station_avg_temp() < T0C) + episode_names += new /datum/episode_name("CEHENNEMDE SOĞUK BİR GÜN", 1000) + + CHECK_TICK + + var/list/ran_events = SSdynamic.executed_rules.Copy() + if(locate(/datum/dynamic_ruleset/roundstart/malf_ai) in ran_events) + episode_names += new /datum/episode_name("[pick("GARİP BİR OYUN", "YAPAY ZEKA DELİYE DÖNÜYOR", "MAKİNELERİN YÜKSELİŞİ")]", 300) + if(locate(/datum/dynamic_ruleset/roundstart/revs) in ran_events) + episode_names += new /datum/episode_name("[pick("MÜRETTEBAT DEVRİME BAŞLIYOR", "CEHENNEMİN DİĞER YÜZÜ", "[pick("İSYAN","DEVRİM")]!!", "MÜRETTEBATIN YÜKSELİŞİ")]", 350) + if((locate(/datum/dynamic_ruleset/roundstart/bloodcult) in ran_events) && blackbox_feedback_num("narsies_spawned") > 0) + episode_names += new /datum/episode_name("[pick("NAR-SIE'NIN BOŞ GÜNÜ'", "NAR-SIE TATİLDE")]", 500) + + if(check_holidays(CHRISTMAS)) + episode_names += new /datum/episode_name("[pick("MUTLU", "BİLİMLİ", "GÜVENLİ", "PLASMALI")] NOELLER", 1000) + if(blackbox_feedback_num("guns_spawned") > 0) + episode_names += new /datum/episode_name("[pick("SILAHLAR, SILAHLAR HERYERDE", "ŞİMŞEK HIZINDA SİLAH TESLIMATI")]", min(750, blackbox_feedback_num("guns_spawned")*25)) + if(blackbox_feedback_num("heartattacks") > 2) + episode_names += new /datum/episode_name("KALBIM AĞRIYOR!!", min(1500, blackbox_feedback_num("heartattacks")*250)) + + var/datum/bank_account/mr_moneybags + var/static/list/typecache_bank = typecacheof(list(/datum/bank_account/department, /datum/bank_account/remote)) + for(var/i in SSeconomy.bank_accounts_by_id) + var/datum/bank_account/current_acc = SSeconomy.bank_accounts_by_id[i] + if(typecache_bank[current_acc.type]) + continue + if(!mr_moneybags || mr_moneybags.account_balance < current_acc.account_balance) + mr_moneybags = current_acc + CHECK_TICK + + if(mr_moneybags && mr_moneybags.account_balance > 30000) + episode_names += new /datum/episode_name("[pick("BEREKET VERSİN", "[locale_uppertext(locale_suffix_genitive(mr_moneybags.account_holder))] KÂRI", "BUNA BIREYSEL EKONOMİ DENİR, SALAK HERİF")]", min(450, mr_moneybags.account_balance/500)) + if(blackbox_feedback_num("ai_deaths") > 3) + episode_names += new /datum/episode_name("YAPAY ZEKANIN [blackbox_feedback_num("ai_deaths")] KEZ ÖLÜMÜ", min(1500, blackbox_feedback_num("ai_deaths")*300)) + if(blackbox_feedback_num("law_changes") > 12) + episode_names += new /datum/episode_name("[pick("MÜRETTEBAT YASA DEĞİŞMEYİ ÖĞRENİYOR", 15;"ASIMOV DİYORKİ")]", min(750, blackbox_feedback_num("law_changes")*25)) + if(blackbox_feedback_num("slips") > 50) + episode_names += new /datum/episode_name("YERLERI KİM YAĞLADI", min(500, blackbox_feedback_num("slips")/2)) + + if(blackbox_feedback_num("turfs_singulod") > 200) + episode_names += new /datum/episode_name("[pick("SINGULARITYNIN KAYBOLUŞU", "MUHAFAZA SORUNU", 50;"MÜHENDİSLER UYUYOR")]", min(1000, blackbox_feedback_num("turfs_singulod")/2)) //no "singularity's day out" please we already have enough + if(blackbox_feedback_num("spacevines_grown") > 150) + episode_names += new /datum/episode_name("[pick("NE EKERSEN ONU BİÇERSİN", "ORMANDAN ÇIKMAK", "TOHUMLU İŞLER", "[uppr_name] VE FASULYE SIRIKLARI")]", min(1500, blackbox_feedback_num("spacevines_grown")*2)) + if(blackbox_feedback_num("devastating_booms") >= 6) + episode_names += new /datum/episode_name("ISTASYONDAKİ BOMBALI SALDIRILAR", min(1000, blackbox_feedback_num("devastating_booms")*100)) + + if(SSpersistence.tram_hits_this_round >= 10) + episode_names += new /datum/episode_name("TRAMVAY KAZASI", 250) + + CHECK_TICK + + if(!EMERGENCY_ESCAPED_OR_ENDGAMED) + return + + var/dead = GLOB.joined_player_list.len - SSticker.popcount[POPCOUNT_ESCAPEES] + var/escaped = SSticker.popcount[POPCOUNT_ESCAPEES] + var/human_escapees = SSticker.popcount[POPCOUNT_ESCAPEES_HUMANONLY] + if(dead == 0) + episode_names += new /datum/episode_name("[pick("ÇALIŞAN TRANSFERİ", "UZUN YAŞA VE GELİŞ", "[locale_uppertext(locale_suffix_locative(uppr_name))] HUZUR VE SAKİNLİK", "SAVAŞSIZ OLAN")]", 4000) //in practice, this one is very very very rare, so if it happens let's pick it more often + if(escaped == 0 || SSshuttle.emergency.is_hijacked()) + episode_names += new /datum/episode_name("[pick("ÖLÜM ALANI", "MÜRETTEBAT KAYBOLUYOR", "[uppr_name]: SİLİNEN SAHNELER", "[locale_uppertext(locale_suffix_locative(uppr_name))] OLAN [locale_uppertext(locale_suffix_locative(uppr_name))] KALIR", "MİSYONDA KAYIP", "SCOOBY-DOO: MÜRETTEBAT NEREDE?")]", 300) + if(escaped < 6 && escaped > 0 && dead > escaped*2) + episode_names += new /datum/episode_name("[pick("YA ÖZGÜRLÜK YA ÖLÜM", "[locale_uppertext(locale_suffix_locative(uppr_name))] KAYBETTİĞİMİZ ŞEYLER", "[uppr_name] İLE GİDİP", "[locale_uppertext(locale_suffix_locative(uppr_name))] SON TANGO", "CANLI KAL, YA DA ÖL", "AMAN TANRIM, MÜRETTEBAT ÖLÜYOR")]", 400) + + var/clowncount = 0 + var/mimecount = 0 + var/assistantcount = 0 + var/chefcount = 0 + var/chaplaincount = 0 + var/minercount = 0 + var/baldycount = 0 + var/horsecount = 0 + for(var/mob/living/carbon/human/H as anything in SSticker.popcount[POPCOUNT_ESCAPEES_HUMANONLY_LIST]) + if(HAS_TRAIT(H, TRAIT_MIMING)) + mimecount++ + if(H.is_wearing_item_of_type(list(/obj/item/clothing/mask/gas/clown_hat, /obj/item/clothing/mask/gas/sexyclown)) || (H.mind && H.mind.assigned_role.title == "Clown")) + clowncount++ + if(H.is_wearing_item_of_type(/obj/item/clothing/under/color/grey) || (H.mind && H.mind.assigned_role.title == "Assistant")) + assistantcount++ + if(H.is_wearing_item_of_type(/obj/item/clothing/head/utility/chefhat) || (H.mind && H.mind.assigned_role.title == "Chef")) + chefcount++ + if(H.mind && H.mind.assigned_role.title == "Shaft Miner") + minercount++ + if(H.mind && H.mind.assigned_role.title == "Chaplain") + chaplaincount++ + if(IS_CHANGELING(H)) + episode_names += new /datum/episode_name("[locale_uppertext(H.mind.name)]: A BLESSING IN DISGUISE", 400) + if(H.dna.species.type == /datum/species/human && (H.hairstyle == "Bald" || H.hairstyle == "Skinhead") && !(BODY_ZONE_HEAD in H.get_covered_body_zones())) + baldycount++ + if(H.is_wearing_item_of_type(/obj/item/clothing/mask/animal/horsehead)) + horsecount++ + CHECK_TICK + + if(clowncount > 2) + episode_names += new /datum/episode_name("SAYISIZ PALYAÇO", min(1500, clowncount*250)) + if(mimecount > 2) + episode_names += new /datum/episode_name("SESSİZ SİNEMA", min(1500, mimecount*250)) + if(chaplaincount > 2) + episode_names += new /datum/episode_name("DUALARINIZ BIZIMLE", min(1500, chaplaincount*450)) + if(chefcount > 2) + episode_names += new /datum/episode_name("ÇOK FAZLA ŞEF", min(1500, chefcount*450)) //intentionally not capitalized, as the theme will customize it + + if(human_escapees) + if(assistantcount / human_escapees > 0.6 && human_escapees > 3) + episode_names += new /datum/episode_name("GRİ GİYEN ADAMLAR", min(1500, assistantcount*200)) + + if(baldycount / human_escapees > 0.6 && SSshuttle.emergency.launch_status == EARLY_LAUNCHED) + episode_names += new /datum/episode_name("BAŞIMIN ÜSTÜNDE KELİM VAR", min(1500, baldycount*250)) + if(horsecount / human_escapees > 0.6 && human_escapees> 3) + episode_names += new /datum/episode_name("AT KAFASI", min(1500, horsecount*250)) + + CHECK_TICK + + if(human_escapees == 1) + var/mob/living/carbon/human/H = SSticker.popcount[POPCOUNT_ESCAPEES_HUMANONLY_LIST][1] + + if(H.stat == CONSCIOUS && H.mind && H.mind.assigned_role.title) + switch(H.mind.assigned_role.title) + if("Chef") + var/chance = 250 + if(H.is_wearing_item_of_type(/obj/item/clothing/head/utility/chefhat)) + chance += 500 + if(H.is_wearing_item_of_type(/obj/item/clothing/suit/toggle/chef)) + chance += 500 + if(H.is_wearing_item_of_type(/obj/item/clothing/under/costume/buttondown/slacks/service)) + chance += 250 + episode_names += new /datum/episode_name("ŞEFİ SELAMLAYIN!!", chance) + if("Clown") + var/chance = 250 + if(H.is_wearing_item_of_type(/obj/item/clothing/mask/gas/clown_hat)) + chance += 500 + if(H.is_wearing_item_of_type(list(/obj/item/clothing/shoes/clown_shoes, /obj/item/clothing/shoes/clown_shoes/jester))) + chance += 500 + if(H.is_wearing_item_of_type(list(/obj/item/clothing/under/rank/civilian/clown, /obj/item/clothing/under/rank/civilian/clown/jester))) + chance += 250 + episode_names += new /datum/episode_name("SON GÜLEN İYİ GÜLER", chance) + if("Detective") + var/chance = 250 + if(H.is_wearing_item_of_type(/obj/item/storage/belt/holster/detective)) + chance += 1000 + if(H.is_wearing_item_of_type(/obj/item/clothing/head/fedora/det_hat)) + chance += 500 + if(H.is_wearing_item_of_type(/obj/item/clothing/suit/jacket/det_suit)) + chance += 500 + if(H.is_wearing_item_of_type(/obj/item/clothing/under/rank/security/detective)) + chance += 250 + episode_names += new /datum/episode_name("[locale_uppertext(H.mind.name)]: KAYIP İPUCU", chance) + if("Shaft Miner") + var/chance = 250 + if(H.is_wearing_item_of_type(/obj/item/pickaxe)) + chance += 1000 + if(H.is_wearing_item_of_type(/obj/item/storage/backpack/explorer)) + chance += 500 + if(H.is_wearing_item_of_type(/obj/item/clothing/suit/hooded/explorer)) + chance += 250 + episode_names += new /datum/episode_name("[pick("ASTEROİTİN MERKEZİNE YOLCULUK", "BİR MAĞARA HİKAYESİ")]", chance) + if("Librarian") + var/chance = 750 + if(H.is_wearing_item_of_type(/obj/item/book)) + chance += 1000 + episode_names += new /datum/episode_name("KİTAP KURDU", chance) + if("Chemist") + var/chance = 1000 + if(H.is_wearing_item_of_type(/obj/item/clothing/suit/toggle/labcoat/chemist)) + chance += 500 + if(H.is_wearing_item_of_type(/obj/item/clothing/under/rank/medical/chemist)) + chance += 250 + episode_names += new /datum/episode_name("HAPI YUTTUK", chance) + if("Chaplain") //We don't check for uniform here because the chaplain's thing kind of is to improvise their garment gimmick + episode_names += new /datum/episode_name("YANLIZ DEGİLİM TANRI YANIMDA", 1250) + + if(H.is_wearing_item_of_type(/obj/item/clothing/mask/luchador) && H.is_wearing_item_of_type(/obj/item/clothing/gloves/boxing)) + episode_names += new /datum/episode_name("[pick("DÖVÜŞ KLUBÜ", "NAKAVT")]", 1500) + + if(human_escapees == 2) + if(chefcount == 2) + episode_names += new /datum/episode_name("MASTERCHEF", 2500) + if(minercount == 2) + episode_names += new /datum/episode_name("ÇİFTE KAZICILAR", 2500) + if(clowncount == 2) + episode_names += new /datum/episode_name("BİR SİRK İKİ PALYAÇO", 2500) + if(clowncount == 1 && mimecount == 1) + episode_names += new /datum/episode_name("DİNAMİK İKİLİ", 2500) + + else + //more than 0 human escapees + var/braindamage_total = 0 + var/all_braindamaged = TRUE + for(var/mob/living/carbon/human/H as anything in SSticker.popcount[POPCOUNT_ESCAPEES_HUMANONLY_LIST]) + var/obj/item/organ/brain/hbrain = H.get_organ_slot(ORGAN_SLOT_BRAIN) + if(hbrain.damage < 60) + all_braindamaged = FALSE + braindamage_total += hbrain.damage + CHECK_TICK + + var/average_braindamage = braindamage_total / human_escapees + if(average_braindamage > 30) + episode_names += new /datum/episode_name("[pick("MÜRETTEBATIN DÜŞÜK IQ PROBLEMI", "AH! KAFAM", "[pick("BEYİN HASARI", "BEVİN HASAVI","HASAVI BEVIN")]", "[locale_uppertext(locale_suffix_genitive(uppr_name))] ÇOK ÖZEL MÜRETTEBATI")]", min(1000, average_braindamage*10)) + if(all_braindamaged && human_escapees > 2) + episode_names += new /datum/episode_name("UMARIM UZAYIN BİRYERLERİNDE AKILLI YAŞAM FORMLARI VARDIR, ÇÜNKÜ [locale_uppertext(locale_suffix_locative(uppr_name))] YOK!!", human_escapees * 500) + +/datum/controller/subsystem/credits/proc/get_title_card(passed_icon_state) + if(!passed_icon_state) + return + var/obj/effect/title_card_object/MA + for(var/obj/effect/title_card_object/effect as anything in antag_appearances) + if(effect.icon_state == passed_icon_state) + MA = effect + break + if(!MA) + MA = new + MA.icon_state = passed_icon_state + MA.pixel_x = 80 + antag_appearances += MA + antag_appearances[MA] = list() + return MA + +/datum/controller/subsystem/credits/proc/generate_admin_icons() + admin_appearances = list() + + var/list/all_admins = GLOB.admin_datums + GLOB.deadmins + var/mob/living/carbon/human/dummy/our_dummy = new() + for(var/ckey in all_admins) + var/datum/admins/admin_datum = all_admins[ckey] + if(!(admin_datum.rank_flags() & R_AUTOADMIN)) + continue + var/mutable_appearance/appearance + if(GLOB.preferences_datums[ckey]) + var/datum/preferences/preferences_datum = GLOB.preferences_datums[ckey] + our_dummy.wipe_state() + appearance = new + appearance.appearance = preferences_datum.render_new_preview_appearance(our_dummy, TRUE) + else + appearance = render_offline_appearance(ckey, our_dummy) + if(!appearance) + continue + appearance.setDir(SOUTH) + appearance.maptext_width = 88 + appearance.maptext_height = world.icon_size * 1.5 + appearance.maptext_x = ((88 - world.icon_size) * -0.5) - our_dummy.base_pixel_x + appearance.maptext_y = -16 + appearance.maptext = "
[ckey]
" + admin_appearances += appearance + + qdel(our_dummy) + +/datum/controller/subsystem/credits/proc/generate_patron_icons() + set waitfor = FALSE + + var/list/all_patrons = get_patrons() + if(isnull(all_patrons)) + return + patron_appearances = list() + var/mob/living/carbon/human/dummy/our_dummy = new() + for(var/ckey in all_patrons) + var/mutable_appearance/appearance + if(GLOB.preferences_datums[ckey]) + var/datum/preferences/preferences_datum = GLOB.preferences_datums[ckey] + our_dummy.wipe_state() + appearance = new + appearance.appearance = preferences_datum.render_new_preview_appearance(our_dummy, TRUE) + else + appearance = render_offline_appearance(ckey, our_dummy) + if(!appearance) + continue + appearance.setDir(SOUTH) + appearance.maptext_width = 88 + appearance.maptext_height = world.icon_size * 1.5 + appearance.maptext_x = ((88 - world.icon_size) * -0.5) - our_dummy.base_pixel_x + appearance.maptext_y = -16 + appearance.maptext = "
[ckey]
" + patron_appearances += appearance + qdel(our_dummy) + +/datum/controller/subsystem/credits/proc/create_antagonist_icon(client/client, mob/living/living_mob, passed_icon_state) + if(!client || !living_mob || !living_mob.mind || !passed_icon_state) + return + var/obj/effect/title_card_object/MA = get_title_card(passed_icon_state) + var/mutable_appearance/appearance + if(processing_icons[WEAKREF(living_mob.mind)]) + appearance = processing_icons[WEAKREF(living_mob.mind)] + else + appearance = new (living_mob.appearance) + appearance.transform = matrix() + appearance.setDir(SOUTH) + var/bound_width = living_mob.bound_width || world.icon_size + appearance.maptext_width = 88 + appearance.maptext_height = world.icon_size * 1.5 + appearance.maptext_x = ((88 - bound_width) * -0.5) - living_mob.base_pixel_x + appearance.maptext_y = -16 + appearance.maptext = "
[living_mob.mind.name]
" + antag_appearances[MA] += list(REF(living_mob.mind) = appearance) + processing_icons[WEAKREF(living_mob.mind)] = appearance + +/datum/controller/subsystem/credits/proc/get_antagonist_icon(datum/weakref/weakref) + if(isnull(weakref)) + return + return processing_icons[weakref] + +/datum/controller/subsystem/credits/proc/blackbox_feedback_num(key) + if(SSblackbox.feedback_list[key]) + var/datum/feedback_variable/FV = SSblackbox.feedback_list[key] + return FV.json["data"] + return null + +/datum/controller/subsystem/credits/proc/get_station_avg_temp() + var/avg_temp = 0 + var/avg_divide = 0 + for(var/obj/machinery/airalarm/alarm as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/airalarm)) + var/turf/location = alarm.loc + if(!istype(location) || !is_station_level(alarm.z)) + continue + var/datum/gas_mixture/environment = location.return_air() + if(!environment) + continue + avg_temp += environment.temperature + avg_divide++ + CHECK_TICK + + if(avg_divide) + return avg_temp / avg_divide + return T0C + +/datum/controller/subsystem/credits/proc/get_patrons() + if(!CONFIG_GET(string/apiurl) || !CONFIG_GET(string/apitoken)) + return + + var/datum/http_request/request = new () + request.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/apiurl)]/patreon/patrons", headers = list("X-EXP-KEY" = "[CONFIG_GET(string/apitoken)]")) + request.begin_async() + + UNTIL(request.is_complete()) + + var/datum/http_response/response = request.into_response() + + if(!response.errored && response.status_code == 200) + var/list/json = json_decode(response.body) + return json["patrons"] + +/obj/effect/title_card_object + plane = SPLASHSCREEN_PLANE + icon = 'icons/psychonaut/effects/title_cards.dmi' + +/obj/effect/cast_object + plane = SPLASHSCREEN_PLANE + +/datum/episode_name + var/name = "" + var/weight = 100 + +/datum/episode_name/New(name, weight) + if(!name) + return + src.name = name + if(weight) + src.weight = weight + + switch(rand(1,15)) + if(0 to 5) + name += ": PART I" + if(6 to 10) + name += ": PART II" + if(11 to 12) + name += ": PART III" + if(13) + name += ": ŞİMDİ 3D" + if(14) + name += ": BUZULLARIN ÜZERİNDE!" + if(15) + name += ": SEZON FİNALİ" diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index d3c280abe66..bdd9dc94913 100755 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -70,6 +70,8 @@ SUBSYSTEM_DEF(ticker) /// ID of round reboot timer, if it exists var/reboot_timer = null + var/list/popcount + /datum/controller/subsystem/ticker/Initialize() var/list/byond_sound_formats = list( "mid" = TRUE, diff --git a/code/modules/admin/verbs/admin.dm b/code/modules/admin/verbs/admin.dm index 7195f447721..c2413a2170f 100644 --- a/code/modules/admin/verbs/admin.dm +++ b/code/modules/admin/verbs/admin.dm @@ -158,6 +158,21 @@ ADMIN_VERB(drop_everything, R_ADMIN, "Drop Everything", ADMIN_VERB_NO_DESCRIPTIO admin_ticket_log(dropee, msg) BLACKBOX_LOG_ADMIN_VERB("Drop Everything") +ADMIN_VERB(set_credits_title, R_FUN, "Set Credits Title", "Set the title that will show on round end credits.", ADMIN_CATEGORY_FUN) + var/title = tgui_input_text(user, "What do you want the title to be?", title = "Credits Title") + if(!title) + return + if(SSshuttle.emergency && (SSshuttle.emergency.mode == SHUTTLE_ENDGAME)) + to_chat(user, span_warning("You cant change the title of credits on endof the game!")) + return + + if(!user.holder.check_for_rights(R_SERVER)) + title = adminscrub(title,500) + + SScredits.customized_name = title + log_admin("[key_name(user)] set the credits title to [title]") + BLACKBOX_LOG_ADMIN_VERB("Set Credits Title") + /proc/cmd_admin_mute(whom, mute_type, automute = 0) if(!whom) return diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm index 44ca783ffcd..99d748f8402 100644 --- a/code/modules/antagonists/_common/antag_datum.dm +++ b/code/modules/antagonists/_common/antag_datum.dm @@ -65,6 +65,8 @@ GLOBAL_LIST_EMPTY(antagonists) var/stinger_sound /// Whether this antag datum blocks rolling new antag datums var/block_midrounds = TRUE + /// The icon state for the credits major event icons + var/credits_icon //ANTAG UI @@ -276,6 +278,9 @@ GLOBAL_LIST_EMPTY(antagonists) for (var/datum/atom_hud/alternate_appearance/basic/antag_hud as anything in GLOB.active_alternate_appearances) antag_hud.apply_to_new_mob(owner.current) + if(credits_icon && owner.current.client) + SScredits.create_antagonist_icon(owner.current.client, owner.current, credits_icon) + SEND_SIGNAL(owner, COMSIG_ANTAGONIST_GAINED, src) /** diff --git a/code/modules/antagonists/abductor/abductor.dm b/code/modules/antagonists/abductor/abductor.dm index 9b8dca4a0e2..be106f2d817 100644 --- a/code/modules/antagonists/abductor/abductor.dm +++ b/code/modules/antagonists/abductor/abductor.dm @@ -8,6 +8,7 @@ show_to_ghosts = TRUE suicide_cry = "FOR THE MOTHERSHIP!!" // They can't even talk but y'know stinger_sound = 'sound/music/antag/ayylien.ogg' + credits_icon = "abductors" var/datum/team/abductor_team/team var/sub_role var/outfit diff --git a/code/modules/antagonists/blob/blob_antag.dm b/code/modules/antagonists/blob/blob_antag.dm index c5dfa6a1e10..9616c67bedc 100644 --- a/code/modules/antagonists/blob/blob_antag.dm +++ b/code/modules/antagonists/blob/blob_antag.dm @@ -7,6 +7,7 @@ job_rank = ROLE_BLOB ui_name = "AntagInfoBlob" stinger_sound = 'sound/music/antag/blobalert.ogg' + credits_icon = "blob" /// Action to release a blob infection var/datum/action/innate/blobpop/pop_action /// Initial points for a human blob diff --git a/code/modules/antagonists/blob/blob_minion.dm b/code/modules/antagonists/blob/blob_minion.dm index e0ff3beb9f9..c460f930788 100644 --- a/code/modules/antagonists/blob/blob_minion.dm +++ b/code/modules/antagonists/blob/blob_minion.dm @@ -4,6 +4,7 @@ show_name_in_check_antagonists = TRUE show_to_ghosts = TRUE show_in_antagpanel = FALSE + credits_icon = "blob" /// The blob core that this minion is attached to var/datum/weakref/overmind diff --git a/code/modules/antagonists/changeling/changeling.dm b/code/modules/antagonists/changeling/changeling.dm index 4ef2c210925..b69f531d945 100644 --- a/code/modules/antagonists/changeling/changeling.dm +++ b/code/modules/antagonists/changeling/changeling.dm @@ -15,7 +15,7 @@ default_custom_objective = "Consume the station's most valuable genomes." hardcore_random_bonus = TRUE stinger_sound = 'sound/music/antag/ling_alert.ogg' - + credits_icon = "changeling" /// Whether to give this changeling objectives or not var/give_objectives = TRUE /// Weather we assign objectives which compete with other lings diff --git a/code/modules/antagonists/cult/datums/cultist.dm b/code/modules/antagonists/cult/datums/cultist.dm index 433c9ebedbe..e604aca278d 100644 --- a/code/modules/antagonists/cult/datums/cultist.dm +++ b/code/modules/antagonists/cult/datums/cultist.dm @@ -8,6 +8,7 @@ job_rank = ROLE_CULTIST antag_hud_name = "cult" stinger_sound = 'sound/music/antag/bloodcult/bloodcult_gain.ogg' + credits_icon = "cult" ///The vote ability Cultists have to elect someone to be the leader. var/datum/action/innate/cult/mastervote/vote_ability diff --git a/code/modules/antagonists/nukeop/datums/operative.dm b/code/modules/antagonists/nukeop/datums/operative.dm index ebaaf8692b2..88daf20ef78 100644 --- a/code/modules/antagonists/nukeop/datums/operative.dm +++ b/code/modules/antagonists/nukeop/datums/operative.dm @@ -9,6 +9,7 @@ hijack_speed = 2 //If you can't take out the station, take the shuttle instead. suicide_cry = "FOR THE SYNDICATE!!" stinger_sound = 'sound/music/antag/ops.ogg' + credits_icon = "nukeops" /// Which nukie team are we on? var/datum/team/nuclear/nuke_team diff --git a/code/modules/antagonists/revolution/revolution.dm b/code/modules/antagonists/revolution/revolution.dm index fb3c7a16bdf..1b46a3a9328 100644 --- a/code/modules/antagonists/revolution/revolution.dm +++ b/code/modules/antagonists/revolution/revolution.dm @@ -7,6 +7,7 @@ antag_hud_name = "rev" suicide_cry = "VIVA LA REVOLUTION!!" stinger_sound = 'sound/music/antag/revolutionary_tide.ogg' + credits_icon = "revolution" var/datum/team/revolution/rev_team /// When this antagonist is being de-antagged, this is the source. Can be a mob (for mindshield/blunt force trauma) or a #define string. diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index 7300dfe51bd..54c02446237 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -18,6 +18,7 @@ default_custom_objective = "Perform an overcomplicated heist on valuable Nanotrasen assets." hardcore_random_bonus = TRUE stinger_sound = 'sound/music/antag/traitor/tatoralert.ogg' + credits_icon = "traitor" ///The flag of uplink that this traitor is supposed to have. var/uplink_flag_given = UPLINK_TRAITORS diff --git a/code/modules/antagonists/wizard/wizard.dm b/code/modules/antagonists/wizard/wizard.dm index caf7f7bd561..5cd6431ca1e 100644 --- a/code/modules/antagonists/wizard/wizard.dm +++ b/code/modules/antagonists/wizard/wizard.dm @@ -15,6 +15,7 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key) can_assign_self_objectives = FALSE default_custom_objective = "Demonstrate your incredible and destructive magical powers." hardcore_random_bonus = TRUE + credits_icon = "wizard" var/give_objectives = TRUE var/strip = TRUE //strip before equipping @@ -228,6 +229,11 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key) wiz_mob.fully_replace_character_name(wiz_mob.real_name, newname) + if(credits_icon) + var/mutable_appearance/antag_appereance = SScredits.get_antagonist_icon(WEAKREF(wiz_mob.mind)) + if(!isnull(antag_appereance)) + antag_appereance.maptext = "
[newname]
" + /datum/antagonist/wizard/apply_innate_effects(mob/living/mob_override) var/mob/living/wizard_mob = mob_override || owner.current wizard_mob.faction |= ROLE_WIZARD diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 644ea6fccb7..7c84cf4e392 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -600,7 +600,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( GLOB.interviews.client_logout(src) GLOB.requests.client_logout(src) SSserver_maint.UpdateHubStatus() - if(credits) + if(islist(credits)) QDEL_LIST(credits) if(holder) holder.owner = null diff --git a/code/modules/client/preferences/names.dm b/code/modules/client/preferences/names.dm index f2b2c89cadf..1020678767b 100644 --- a/code/modules/client/preferences/names.dm +++ b/code/modules/client/preferences/names.dm @@ -55,13 +55,15 @@ if (!input) return input - if (CONFIG_GET(flag/humans_need_surnames) && preferences.read_preference(/datum/preference/choiced/species) == /datum/species/human) + var/datum/species/race = preferences?.read_preference(/datum/preference/choiced/species) || /datum/species/human + + if (CONFIG_GET(flag/humans_need_surnames) && race == /datum/species/human) var/first_space = findtext(input, " ") if(!first_space) //we need a surname input += " [pick(GLOB.last_names)]" else if(first_space == length(input)) input += "[pick(GLOB.last_names)]" - var/datum/species/race = preferences.read_preference(/datum/preference/choiced/species) + return reject_bad_name(input, race::allow_numbers_in_name || allow_numbers) /datum/preference/name/real_name/serialize(input, datum/preferences/preferences) diff --git a/code/modules/client/preferences/show_roundend_credits.dm b/code/modules/client/preferences/show_roundend_credits.dm new file mode 100644 index 00000000000..35e332b1592 --- /dev/null +++ b/code/modules/client/preferences/show_roundend_credits.dm @@ -0,0 +1,9 @@ +/datum/preference/toggle/show_roundend_credits + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "show_roundend_credits" + savefile_identifier = PREFERENCE_PLAYER + default_value = TRUE + +/datum/preference/toggle/show_roundend_credits/apply_to_client_updated(client/client, value) + if(!value) + INVOKE_ASYNC(client, TYPE_PROC_REF(/client, ClearCredits)) diff --git a/code/modules/loadout/loadout_preference.dm b/code/modules/loadout/loadout_preference.dm index 703f82975e7..cb5a5bd6e50 100644 --- a/code/modules/loadout/loadout_preference.dm +++ b/code/modules/loadout/loadout_preference.dm @@ -16,7 +16,7 @@ // Sanitize on load to ensure no invalid paths from older saves get in /datum/preference/loadout/deserialize(input, datum/preferences/preferences) - return sanitize_loadout_list(input, preferences.parent?.mob) + return sanitize_loadout_list(input, preferences?.parent?.mob) // Default value is null - the loadout list is a lazylist /datum/preference/loadout/create_default_value(datum/preferences/preferences) diff --git a/code/modules/mob/living/carbon/inventory.dm b/code/modules/mob/living/carbon/inventory.dm index b60c9dedb3a..f50d30b8b7b 100644 --- a/code/modules/mob/living/carbon/inventory.dm +++ b/code/modules/mob/living/carbon/inventory.dm @@ -522,3 +522,16 @@ qdel(item) return placed_in + +///Bruteforce check for any type or subtype of an item. +/mob/living/carbon/human/proc/is_wearing_item_of_type(type2check) + var/found + var/list/my_items = get_equipped_items() + if(islist(type2check)) + for(var/type_iterator in type2check) + found = locate(type_iterator) in my_items + if(found) + return found + else + found = locate(type2check) in my_items + return found diff --git a/icons/psychonaut/effects/title_cards.dmi b/icons/psychonaut/effects/title_cards.dmi new file mode 100644 index 00000000000..b3e30718bc0 Binary files /dev/null and b/icons/psychonaut/effects/title_cards.dmi differ diff --git a/tgstation.dme b/tgstation.dme index ab7023e60a8..a32242bd9fa 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -655,6 +655,7 @@ #include "code\controllers\subsystem\blackbox.dm" #include "code\controllers\subsystem\chat.dm" #include "code\controllers\subsystem\circuit_component.dm" +#include "code\controllers\subsystem\credits.dm" #include "code\controllers\subsystem\dbcore.dm" #include "code\controllers\subsystem\dcs.dm" #include "code\controllers\subsystem\discord.dm" @@ -3869,6 +3870,7 @@ #include "code\modules\client\preferences\scarred_eye.dm" #include "code\modules\client\preferences\screentips.dm" #include "code\modules\client\preferences\security_department.dm" +#include "code\modules\client\preferences\show_roundend_credits.dm" #include "code\modules\client\preferences\skin_tone.dm" #include "code\modules\client\preferences\sounds.dm" #include "code\modules\client\preferences\species.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/show_roundend_credits.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/show_roundend_credits.tsx new file mode 100644 index 00000000000..3287be14fa9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/show_roundend_credits.tsx @@ -0,0 +1,8 @@ +import { CheckboxInput, FeatureToggle } from '../base'; + +export const show_roundend_credits: FeatureToggle = { + name: 'Show Roundend Credits', + category: 'UI', + description: 'Show the credits roll after the round ends.', + component: CheckboxInput, +};