diff --git a/code/__HELPERS/hearted.dm b/code/__HELPERS/hearted.dm new file mode 100644 index 000000000000..1b2c60a25321 --- /dev/null +++ b/code/__HELPERS/hearted.dm @@ -0,0 +1,98 @@ +/// Called when the shuttle starts launching back to centcom, polls a few random players who joined the round for commendations +/datum/controller/subsystem/ticker/proc/poll_hearts() + if(!CONFIG_GET(number/commendation_percent_poll)) + return + var/number_to_ask = round(LAZYLEN(GLOB.joined_player_list) * CONFIG_GET(number/commendation_percent_poll)) + rand(0,1) + + if(number_to_ask == 0) + message_admins("Not enough eligible players to poll for commendations.") + return + + message_admins("Polling [number_to_ask] players for commendations.") + + for(var/i in GLOB.joined_player_list) + var/mob/check_mob = get_mob_by_ckey(i) + if(!check_mob?.mind || !check_mob.client) + continue + // maybe some other filters like bans or whatever + INVOKE_ASYNC(check_mob, TYPE_PROC_REF(/mob, query_heart), 1) + number_to_ask-- + if(number_to_ask <= 0) + break + +/// Once the round is actually over, cycle through the ckeys in the hearts list and give them the hearted status +/datum/controller/subsystem/ticker/proc/handle_hearts() + var/list/message = list("The following players were commended this round: ") + var/i = 0 + for(var/hearted_ckey in hearts) + i++ + var/mob/hearted_mob = get_mob_by_ckey(hearted_ckey) + if(!hearted_mob?.client) + continue + hearted_mob.client.adjust_heart() + message += "[hearted_ckey][i==hearts.len ? "" : ", "]" + message_admins(message.Join()) + +/// Ask someone if they'd like to award a commendation for the round, 3 tries to get the name they want before we give up +/mob/proc/query_heart(attempt=1) + if(!client || attempt > 3) + return + if(attempt == 1 && tgui_alert(src, "Was there another character you noticed being kind this round that you would like to anonymously thank?", "<3?", list("Yes", "No"), timeout = 30 SECONDS) != "Yes") + return + + var/heart_nominee + switch(attempt) + if(1) + heart_nominee = input(src, "What was their name? Just a first or last name may be enough. (Leave blank to cancel)", "<3?") + if(2) + heart_nominee = input(src, "Try again, what was their name? Just a first or last name may be enough. (Leave blank to cancel)", "<3?") + if(3) + heart_nominee = input(src, "One more try, what was their name? Just a first or last name may be enough. (Leave blank to cancel)", "<3?") + if(!heart_nominee) + return + + heart_nominee = lowertext(heart_nominee) + var/list/name_checks = get_mob_by_name(heart_nominee) + if(!name_checks || name_checks.len == 0) + query_heart(attempt + 1) + return + name_checks = shuffle(name_checks) + + for(var/i in name_checks) + var/mob/heart_contender = i + if(heart_contender == src) + continue + + switch(tgui_alert(src, "Is this the person: [heart_contender.name] ([heart_contender.real_name])", "<3?", list("Yes!", "Nope", "Cancel"), timeout = 15 SECONDS)) + if("Yes!") + heart_contender.receive_heart(src) + return + if("Nope") + continue + else + return + + query_heart(attempt + 1) + +/* +* Once we've confirmed who we're commending, either set their status now or log it for the end of the round +* +* This used to be reversed, being named nominate_heart and being called on the mob sending the commendation and the first argument being +* the heart_recepient, but that was confusing and unintuitive, so now src is the person being commended and the sender is now the first argument. +* +* Arguments: +* * heart_sender: The reference to the mob who sent the commendation, just for the purposes of logging +* * duration: How long from the moment it's applied the heart will last +* * instant: If TRUE (or if the round is already over), we'll give them the heart status now, if FALSE, we wait until the end of the round (which is the standard behavior) +*/ + +/mob/proc/receive_heart(mob/heart_sender, duration = 24 HOURS, instant = FALSE) + if(!client) + return + to_chat(heart_sender, span_nicegreen("Commendation sent!")) + message_admins("[key_name(heart_sender)] commended [key_name(src)] [instant ? "(instant)" : ""]") + log_admin("[key_name(heart_sender)] commended [key_name(src)] [instant ? "(instant)" : ""]") + if(instant || SSticker.current_state == GAME_STATE_FINISHED) + client.adjust_heart(duration) + else + LAZYADD(SSticker.hearts, ckey) diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index 01fd964120ea..32e482bfc35d 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -226,6 +226,11 @@ CHECK_TICK + //check config blah blah + handle_hearts() + + CHECK_TICK + //Now print them all into the log! log_game("Antagonists at round end were...") for(var/antag_name in total_antagonists) diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm index 0cd455d172a6..b9955b4f5cc9 100644 --- a/code/controllers/configuration/entries/game_options.dm +++ b/code/controllers/configuration/entries/game_options.dm @@ -382,3 +382,6 @@ max_val = 255 config_entry_value = 127 min_val = 127 + +/datum/config_entry/number/commendation_percent_poll + integer = FALSE diff --git a/code/controllers/subsystem/shuttle.dm b/code/controllers/subsystem/shuttle.dm index a6a3dafd1590..ed1a93c50e08 100644 --- a/code/controllers/subsystem/shuttle.dm +++ b/code/controllers/subsystem/shuttle.dm @@ -80,6 +80,8 @@ SUBSYSTEM_DEF(shuttle) jump_timer = addtimer(VARSET_CALLBACK(src, jump_mode, BS_JUMP_COMPLETED), jump_completion_time, TIMER_STOPPABLE) priority_announce("Jump initiated. ETA: [jump_completion_time / (1 MINUTES)] minutes.", null, null, "Priority") + INVOKE_ASYNC(SSticker, TYPE_PROC_REF(/datum/controller/subsystem/ticker,poll_hearts)) + /datum/controller/subsystem/shuttle/proc/request_transit_dock(obj/docking_port/mobile/M) if(!istype(M)) CRASH("[M] is not a mobile docking port") diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 7f0e9c8ee627..2b3ac0619c5a 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -62,6 +62,8 @@ SUBSYSTEM_DEF(ticker) /// Why an emergency shuttle was called var/emergency_reason + /// People who have been commended and will receive a heart + var/list/hearts /datum/controller/subsystem/ticker/Initialize(timeofday) load_mode() diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index b62a7830cc0d..a438fa57a066 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -195,6 +195,7 @@ body += "Thunderdome 2" body += "Thunderdome Admin" body += "Thunderdome Observer" + body += "Commend Behavior | " body += "
" body += "" diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 5123eed0be58..9b757bae25fc 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -2166,6 +2166,19 @@ var/datum/poll_question/poll = locate(href_list["submitoptionpoll"]) in GLOB.polls poll_option_parse_href(href_list, poll, option) + else if(href_list["admincommend"]) + var/mob/heart_recepient = locate(href_list["admincommend"]) + if(!heart_recepient?.ckey) + to_chat(usr, "This mob either no longer exists or no longer is being controlled by someone!") + return + switch(tgui_alert(usr, "Would you like the effects to apply immediately or at the end of the round? Applying them now will make it clear it was an admin commendation.", "<3?", list("Apply now", "Apply at round end", "Cancel"))) + if("Apply now") + heart_recepient.receive_heart(usr, instant = TRUE) + if("Apply at round end") + heart_recepient.receive_heart(usr) + else + return + else if (href_list["interview"]) if(!check_rights(R_ADMIN)) return diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm index 5ac4714257ef..9f7792628424 100644 --- a/code/modules/admin/verbs/adminhelp.dm +++ b/code/modules/admin/verbs/adminhelp.dm @@ -883,3 +883,34 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new) return_list[ASAY_LINK_NEW_MESSAGE_INDEX] = jointext(msglist, " ") // without tuples, we must make do! return_list[ASAY_LINK_PINGED_ADMINS_INDEX] = pinged_admins return return_list + +/proc/get_mob_by_name(msg) + //This is a list of words which are ignored by the parser when comparing message contents for names. MUST BE IN LOWER CASE! + var/list/ignored_words = list("unknown","the","a","an","of","monkey","alien","as", "i") + + //explode the input msg into a list + var/list/msglist = splittext(msg, " ") + + //who might fit the shoe + var/list/potential_hits = list() + + for(var/i in GLOB.mob_list) + var/mob/M = i + var/list/nameWords = list() + if(!M.mind) + continue + + for(var/string in splittext(lowertext(M.real_name), " ")) + if(!(string in ignored_words)) + nameWords += string + for(var/string in splittext(lowertext(M.name), " ")) + if(!(string in ignored_words)) + nameWords += string + + for(var/string in nameWords) + testing("Name word [string]") + if(string in msglist) + potential_hits += M + break + + return potential_hits diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 368acae22a7c..5f1c211412c8 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -1172,3 +1172,13 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if("Set-Tab") stat_tab = payload["tab"] SSstatpanels.immediate_send_stat_data(src) + +///Gives someone hearted status for OOC, from behavior commendations +/client/proc/adjust_heart(duration = 24 HOURS) + var/new_duration = world.realtime + duration + if(prefs.hearted_until > new_duration) + return + to_chat(src, "Someone awarded you a heart!") + prefs.hearted_until = new_duration + prefs.hearted = TRUE + prefs.save_preferences() diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 18c9a5374443..d339d6b715c8 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -215,6 +215,13 @@ GLOBAL_LIST_EMPTY(preferences_datums) ///The outfit we currently want to preview on our character var/datum/outfit/job/selected_outfit + ///Someone thought we were nice! We get a little heart in OOC until we join the server past the below time (we can keep it until the end of the round otherwise) + var/hearted + /// + var/hearted_until + + + /datum/preferences/New(client/C) parent = C @@ -1135,6 +1142,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) if(unlock_content || check_rights_for(user.client, R_ADMIN) || custom_ooc) dat += "OOC Color:     Change
" + if(hearted_until) + dat += "Clear OOC Commend Heart
" + dat += "" if(user.client.holder) @@ -2430,6 +2440,13 @@ GLOBAL_LIST_EMPTY(preferences_datums) if(current_tab == 2) show_loadout = TRUE + if("clear_heart") + hearted = FALSE + hearted_until = null + to_chat(user, "OOC Commendation Heart disabled") + save_preferences() + + ShowChoices(user) return 1 diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index 71d968d16130..7d9aae456185 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -208,15 +208,10 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car READ_FILE(S["pda_color"], pda_color) READ_FILE(S["whois_visible"], whois_visible) - // Custom hotkeys - READ_FILE(S["key_bindings"], key_bindings) - check_keybindings() - READ_FILE(S["show_credits"], show_credits) //favorite outfits READ_FILE(S["favorite_outfits"], favorite_outfits) - var/list/parsed_favs = list() for(var/typetext in favorite_outfits) var/datum/outfit/path = text2path(typetext) @@ -224,6 +219,15 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car parsed_favs += path favorite_outfits = uniqueList(parsed_favs) + // OOC commendations + READ_FILE(S["hearted_until"], hearted_until) + if(hearted_until > world.realtime) + hearted = TRUE + + // Custom hotkeys + READ_FILE(S["key_bindings"], key_bindings) + check_keybindings() + //try to fix any outdated data if necessary if(needs_update >= 0) var/bacpath = "[path].updatebac" //todo: if the savefile version is higher then the server, check the backup, and give the player a prompt to load the backup @@ -352,6 +356,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car WRITE_FILE(S["key_bindings"], key_bindings) WRITE_FILE(S["favorite_outfits"], favorite_outfits) WRITE_FILE(S["whois_visible"], whois_visible) + WRITE_FILE(S["hearted_until"], (hearted_until > world.realtime ? hearted_until : null)) return TRUE /datum/preferences/proc/load_character(slot) diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm index ea626f397e58..fb2ade22a6da 100644 --- a/code/modules/client/verbs/ooc.dm +++ b/code/modules/client/verbs/ooc.dm @@ -61,6 +61,9 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8") keyname = "[icon2html('icons/member_content.dmi', world, "blag")][keyname]" if(prefs.custom_ooc) keyname = "[keyname]" + if(prefs.hearted) + var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/chat) + keyname = "[sheet.icon_tag("emoji-heart")][keyname]" //The linkify span classes and linkify=TRUE below make ooc text get clickable chat href links if you pass in something resembling a url for(var/client/C in GLOB.clients) if(C.prefs.chat_toggles & CHAT_OOC) diff --git a/config/game_options.txt b/config/game_options.txt index 4e200748a5c3..a3dd9a0051b9 100644 --- a/config/game_options.txt +++ b/config/game_options.txt @@ -516,3 +516,7 @@ BLUESPACE_JUMP_WAIT 12000 ## If admins are allowed to use the authentication server as a regular server for testing AUTH_ADMIN_TESTING + +## HEART COMMENDATIONS ### +## Uncomment this if you'd like to enable commendation pollings for this percentage of players near the end of the round (5% suggested) +COMMENDATION_PERCENT_POLL 0.05 diff --git a/shiptest.dme b/shiptest.dme index 8f18e6c5773d..13511d8bc40d 100644 --- a/shiptest.dme +++ b/shiptest.dme @@ -200,6 +200,7 @@ #include "code\__HELPERS\game.dm" #include "code\__HELPERS\generators.dm" #include "code\__HELPERS\global_lists.dm" +#include "code\__HELPERS\hearted.dm" #include "code\__HELPERS\heap.dm" #include "code\__HELPERS\icon_smoothing.dm" #include "code\__HELPERS\icons.dm"