diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index ee7cff84687..fc185caf703 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -191,18 +191,16 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) return FALSE if(human_mob.mind && (human_mob.mind.special_role || length(human_mob.mind.antag_datums) > 0)) - var/didthegamerwin = TRUE for(var/datum/antagonist/antag_datums as anything in human_mob.mind.antag_datums) + if(initial(antag_datums.can_assign_self_objectives) && !antag_datums.can_assign_self_objectives) + return FALSE // You don't get a prize if you picked your own objective, you can't fail those for(var/datum/objective/objective_datum as anything in antag_datums.objectives) if(!objective_datum.check_completion()) - didthegamerwin = FALSE - if(!didthegamerwin) - return FALSE + return FALSE player_client.give_award(/datum/award/score/hardcore_random, human_mob, round(human_mob.hardcore_survival_score * 2)) else if(considered_escaped(human_mob.mind)) player_client.give_award(/datum/award/score/hardcore_random, human_mob, round(human_mob.hardcore_survival_score)) - /datum/controller/subsystem/ticker/proc/declare_completion(was_forced = END_ROUND_AS_NORMAL) set waitfor = FALSE @@ -706,10 +704,7 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) var/list/objective_parts = list() var/count = 1 for(var/datum/objective/objective in objectives) - if(objective.check_completion()) - objective_parts += "[objective.objective_name] #[count]: [objective.explanation_text] [span_greentext("Success!")]" - else - objective_parts += "[objective.objective_name] #[count]: [objective.explanation_text] [span_redtext("Fail.")]" + objective_parts += "[objective.objective_name] #[count]: [objective.explanation_text] [objective.get_roundend_success_suffix()]" count++ return objective_parts.Join("
") diff --git a/code/datums/components/uplink.dm b/code/datums/components/uplink.dm index 1f47e15b79a..03740417678 100644 --- a/code/datums/components/uplink.dm +++ b/code/datums/components/uplink.dm @@ -259,6 +259,7 @@ data["current_stock"] = remaining_stock data["shop_locked"] = uplink_handler.shop_locked data["purchased_items"] = length(uplink_handler.purchase_log?.purchase_log) + data["can_renegotiate"] = user.mind == uplink_handler.owner && uplink_handler.can_replace_objectives?.Invoke() == TRUE return data /datum/component/uplink/ui_static_data(mob/user) @@ -300,6 +301,9 @@ if(!lockable) return TRUE lock_uplink() + if("renegotiate_objectives") + uplink_handler.replace_objectives?.Invoke() + SStgui.update_uis(src) if(!uplink_handler.has_objectives) return TRUE diff --git a/code/datums/mind/_mind.dm b/code/datums/mind/_mind.dm index 7a3fe9a42fc..eb5ecf6e3e6 100644 --- a/code/datums/mind/_mind.dm +++ b/code/datums/mind/_mind.dm @@ -357,6 +357,41 @@ objective.completed = !objective.completed log_admin("[key_name(usr)] toggled the win state for [current]'s objective: [objective.explanation_text]") + else if(href_list["obj_prompt_custom"]) + var/datum/antagonist/target_antag + if(href_list["target_antag"]) + var/datum/antagonist/found_datum = locate(href_list["target_antag"]) in antag_datums + if(found_datum) + target_antag = found_datum + if(isnull(target_antag)) + switch(length(antag_datums)) + if(0) + target_antag = add_antag_datum(/datum/antagonist/custom) + if(1) + target_antag = antag_datums[1] + else + var/datum/antagonist/target = input("Which antagonist gets the objective:", "Antagonist", "(new custom antag)") as null|anything in sort_list(antag_datums) + "(new custom antag)" + if (QDELETED(target)) + return + else if(target == "(new custom antag)") + target_antag = add_antag_datum(/datum/antagonist/custom) + else + target_antag = target + var/replace_existing = input("Replace existing objectives?","Replace objectives?") in list("Yes", "No") + if (isnull(replace_existing)) + return + replace_existing = replace_existing == "Yes" + var/replace_escape + if (!replace_existing) + replace_escape = FALSE + else + replace_escape = input("Replace survive/escape/martyr objectives?","Replace objectives?") in list("Yes", "No") + if (isnull(replace_escape)) + return + replace_escape = replace_escape == "Yes" + target_antag.submit_player_objective(retain_existing = !replace_existing, retain_escape = !replace_escape, force = TRUE) + log_admin("[key_name(usr)] prompted [current] to enter their own objectives for [target_antag].") + else if (href_list["silicon"]) switch(href_list["silicon"]) if("unemag") diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm index 9647c5376fa..0a68443c208 100644 --- a/code/game/gamemodes/objective.dm +++ b/code/game/gamemodes/objective.dm @@ -94,6 +94,10 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list /datum/objective/proc/check_completion() return completed +/// Provides a string describing what a good job you did or did not do +/datum/objective/proc/get_roundend_success_suffix() + return check_completion() ? span_greentext("Success!") : span_redtext("Fail.") + /datum/objective/proc/is_unique_objective(possible_target, dupe_search_range) if(!islist(dupe_search_range)) stack_trace("Non-list passed as duplicate objective search range") @@ -943,6 +947,9 @@ GLOBAL_LIST_EMPTY(possible_items) if(expl) explanation_text = expl +/datum/objective/custom/get_roundend_success_suffix() + return "" // Just print the objective with no success/fail evaluation, as it has no mechanical backing + //Ideally this would be all of them but laziness and unusual subtypes /proc/generate_admin_objective_list() GLOB.admin_objective_list = list() diff --git a/code/modules/admin/antag_panel.dm b/code/modules/admin/antag_panel.dm index 2aad9a7cbcf..a970c7a5335 100644 --- a/code/modules/admin/antag_panel.dm +++ b/code/modules/admin/antag_panel.dm @@ -53,6 +53,7 @@ GLOBAL_VAR(antag_prototypes)
" obj_count++ result += "Add objective
" + result += "Prompt custom objective entry
" result += "Announce objectives
" return result diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm index 5ec9a2dcc87..bb3b178750a 100644 --- a/code/modules/antagonists/_common/antag_datum.dm +++ b/code/modules/antagonists/_common/antag_datum.dm @@ -1,3 +1,6 @@ +/// Max length of custom objective text +#define CUSTOM_OBJECTIVE_MAX_LENGTH 300 + GLOBAL_LIST_EMPTY(antagonists) /datum/antagonist @@ -52,6 +55,10 @@ GLOBAL_LIST_EMPTY(antagonists) var/preview_outfit /// Flags for antags to turn on or off and check! var/antag_flags = FLAG_ANTAG_CAN_BE_INDUCTED + /// If true, this antagonist can assign themself a new objective + var/can_assign_self_objectives = FALSE + /// Default to fill in when entering a custom objective. + var/default_custom_objective = "Cause chaos on the space station." //ANTAG UI @@ -117,6 +124,15 @@ GLOBAL_LIST_EMPTY(antagonists) ui = new(user, src, ui_name, name) ui.open() +/datum/antagonist/ui_act(action, params) + . = ..() + if(.) + return + switch(action) + if("change_objectives") + submit_player_objective() + return TRUE + /datum/antagonist/ui_state(mob/user) return GLOB.always_state @@ -124,6 +140,7 @@ GLOBAL_LIST_EMPTY(antagonists) var/list/data = list() data["antag_name"] = name data["objectives"] = get_objectives() + data["can_change_objective"] = can_assign_self_objectives return data //button for antags to review their descriptions/info @@ -524,3 +541,56 @@ GLOBAL_LIST_EMPTY(antagonists) /// Used to create objectives for the antagonist. /datum/antagonist/proc/forge_objectives() return + +/** + * Allows player to replace their objectives with a new one they wrote themselves. + * * retain_existing - If true, will just be added as a new objective instead of replacing existing ones. + * * retain_escape - If true, will retain specifically 'escape alive' objectives (or similar) + * * force - Skips the check about whether this antagonist is supposed to set its own objectives, for badminning + */ +/datum/antagonist/proc/submit_player_objective(retain_existing = FALSE, retain_escape = TRUE, force = FALSE) + if (isnull(owner) || isnull(owner.current)) + return + var/mob/living/owner_mob = owner.current + if (!force && !can_assign_self_objectives) + owner_mob.balloon_alert(owner_mob, "can't do that!") + return + var/custom_objective_text = tgui_input_text( + owner_mob, + message = "Specify your new objective.", + title = "Custom Objective", + default = default_custom_objective, + max_length = CUSTOM_OBJECTIVE_MAX_LENGTH, + ) + if (QDELETED(src) || QDELETED(owner_mob) || isnull(custom_objective_text)) + return // Some people take a long-ass time to type maybe they got dusted + + log_game("[key_name(owner_mob)] [retain_existing ? "" : "opted out of their original objectives and "]chose a custom objective: [custom_objective_text]") + message_admins("[ADMIN_LOOKUPFLW(owner_mob)] has chosen a custom antagonist objective: [span_syndradio("[custom_objective_text]")] | [ADMIN_SMITE(owner_mob)] | [ADMIN_SYNDICATE_REPLY(owner_mob)]") + + var/datum/objective/custom/custom_objective = new() + custom_objective.owner = owner + custom_objective.explanation_text = custom_objective_text + custom_objective.completed = TRUE + + if (retain_existing) + objectives.Insert(1, custom_objective) + else if (!retain_escape) + objectives = list(custom_objective) + else + var/static/list/escape_objectives = list( + /datum/objective/escape, + /datum/objective/survive, + /datum/objective/martyr, + /datum/objective/exile, + ) + for (var/datum/objective/check_objective in objectives) + if (is_type_in_list(check_objective, escape_objectives)) + continue + objectives -= check_objective + objectives.Insert(1, custom_objective) + + can_assign_self_objectives = FALSE + owner.announce_objectives() + +#undef CUSTOM_OBJECTIVE_MAX_LENGTH diff --git a/code/modules/antagonists/_common/antag_team.dm b/code/modules/antagonists/_common/antag_team.dm index c568c58e310..902456d909b 100644 --- a/code/modules/antagonists/_common/antag_team.dm +++ b/code/modules/antagonists/_common/antag_team.dm @@ -58,11 +58,9 @@ GLOBAL_LIST_EMPTY(antagonist_teams) var/win = TRUE var/objective_count = 1 for(var/datum/objective/objective as anything in objectives) - if(objective.check_completion()) - report += "Objective #[objective_count]: [objective.explanation_text] [span_greentext("Success!")]" - else - report += "Objective #[objective_count]: [objective.explanation_text] [span_redtext("Fail.")]" + if(!objective.check_completion()) win = FALSE + report += "Objective #[objective_count]: [objective.explanation_text] [objective.get_roundend_success_suffix()]" objective_count++ if(win) report += span_greentext("The [name] was successful!") diff --git a/code/modules/antagonists/changeling/changeling.dm b/code/modules/antagonists/changeling/changeling.dm index a3b856113fd..e0372cf6620 100644 --- a/code/modules/antagonists/changeling/changeling.dm +++ b/code/modules/antagonists/changeling/changeling.dm @@ -11,6 +11,8 @@ hijack_speed = 0.5 ui_name = "AntagInfoChangeling" suicide_cry = "FOR THE HIVE!!" + can_assign_self_objectives = TRUE + default_custom_objective = "Consume the station's most valuable genomes." /// Whether to give this changeling objectives or not var/give_objectives = TRUE /// Weather we assign objectives which compete with other lings @@ -644,10 +646,6 @@ add_new_profile(owner.current) /datum/antagonist/changeling/forge_objectives() - //OBJECTIVES - random traitor objectives. Unique objectives "steal brain" and "identity theft". - //No escape alone because changelings aren't suited for it and it'd probably just lead to rampant robusting - //If it seems like they'd be able to do it in play, add a 10% chance to have to escape alone - var/escape_objective_possible = TRUE switch(competitive_objectives ? rand(1,3) : 1) @@ -966,11 +964,9 @@ if(objectives.len) var/count = 1 for(var/datum/objective/objective in objectives) - if(objective.check_completion()) - parts += "Objective #[count]: [objective.explanation_text] [span_greentext("Success!")]" - else - parts += "Objective #[count]: [objective.explanation_text] [span_redtext("Fail.")]" + if(!objective.check_completion()) changeling_win = FALSE + parts += "Objective #[count]: [objective.explanation_text] [objective.get_roundend_success_suffix()]" count++ if(changeling_win) diff --git a/code/modules/antagonists/cult/cult_team.dm b/code/modules/antagonists/cult/cult_team.dm index 7cfbc51b1a7..6254ded64a0 100644 --- a/code/modules/antagonists/cult/cult_team.dm +++ b/code/modules/antagonists/cult/cult_team.dm @@ -114,10 +114,7 @@ parts += "The cultists' objectives were:" var/count = 1 for(var/datum/objective/objective in objectives) - if(objective.check_completion()) - parts += "Objective #[count]: [objective.explanation_text] [span_greentext("Success!")]" - else - parts += "Objective #[count]: [objective.explanation_text] [span_redtext("Fail.")]" + parts += "Objective #[count]: [objective.explanation_text] [objective.get_roundend_success_suffix()]" count++ if(members.len) diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm index d52eaf9ce36..1c2e8ee39ef 100644 --- a/code/modules/antagonists/heretic/heretic_antag.dm +++ b/code/modules/antagonists/heretic/heretic_antag.dm @@ -23,6 +23,8 @@ hijack_speed = 0.5 suicide_cry = "THE MANSUS SMILES UPON ME!!" preview_outfit = /datum/outfit/heretic + can_assign_self_objectives = TRUE + default_custom_objective = "Turn a department into a testament for your dark knowledge." /// Whether we give this antagonist objectives on gain. var/give_objectives = TRUE /// Whether we've ascended! (Completed one of the final rituals) @@ -110,6 +112,7 @@ var/list/data = list() data["objectives"] = get_objectives() + data["can_change_objective"] = can_assign_self_objectives for(var/path in researched_knowledge) var/list/knowledge_data = list() @@ -145,6 +148,19 @@ knowledge_points -= initial(researched_path.cost) return TRUE +/datum/antagonist/heretic/submit_player_objective(retain_existing = FALSE, retain_escape = TRUE, force = FALSE) + if (isnull(owner) || isnull(owner.current)) + return + var/confirmed = tgui_alert( + owner.current, + message = "Are you sure? You will no longer be able to Ascend.", + title = "Reject the call?", + buttons = list("Yes", "No"), + ) == "Yes" + if (!confirmed) + return + return ..() + /datum/antagonist/heretic/ui_status(mob/user, datum/ui_state/state) if(user.stat == DEAD) return UI_CLOSE @@ -454,11 +470,9 @@ if(length(objectives)) var/count = 1 for(var/datum/objective/objective as anything in objectives) - if(objective.check_completion()) - parts += "Objective #[count]: [objective.explanation_text] [span_greentext("Success!")]" - else - parts += "Objective #[count]: [objective.explanation_text] [span_redtext("Fail.")]" + if(!objective.check_completion()) succeeded = FALSE + parts += "Objective #[count]: [objective.explanation_text] [objective.get_roundend_success_suffix()]" count++ if(ascended) @@ -654,6 +668,8 @@ * Returns FALSE if not all of our objectives are complete, or TRUE otherwise. */ /datum/antagonist/heretic/proc/can_ascend() + if(!can_assign_self_objectives) + return FALSE // We spurned the offer of the Mansus :( for(var/datum/objective/must_be_done as anything in objectives) if(!must_be_done.check_completion()) return FALSE diff --git a/code/modules/antagonists/malf_ai/malf_ai.dm b/code/modules/antagonists/malf_ai/malf_ai.dm index 4575382c25c..8eb8d513cea 100644 --- a/code/modules/antagonists/malf_ai/malf_ai.dm +++ b/code/modules/antagonists/malf_ai/malf_ai.dm @@ -8,6 +8,8 @@ job_rank = ROLE_MALF antag_hud_name = "traitor" ui_name = "AntagInfoMalf" + can_assign_self_objectives = TRUE + default_custom_objective = "Make sure your precious crew are incapable of ever, ever leaving you." ///the name of the antag flavor this traitor has. var/employer ///assoc list of strings set up after employer is given @@ -178,6 +180,7 @@ data["allies"] = malfunction_flavor["allies"] data["goal"] = malfunction_flavor["goal"] data["objectives"] = get_objectives() + data["can_change_objective"] = can_assign_self_objectives //module picker data @@ -235,11 +238,9 @@ if(objectives.len) //If the traitor had no objectives, don't need to process this. var/count = 1 for(var/datum/objective/objective in objectives) - if(objective.check_completion()) - objectives_text += "
Objective #[count]: [objective.explanation_text] [span_greentext("Success!")]" - else - objectives_text += "
Objective #[count]: [objective.explanation_text] [span_redtext("Fail.")]" + if(!objective.check_completion()) malf_ai_won = FALSE + objectives_text += "
Objective #[count]: [objective.explanation_text] [objective.get_roundend_success_suffix()]" count++ result += objectives_text diff --git a/code/modules/antagonists/ninja/ninja_explosive.dm b/code/modules/antagonists/ninja/ninja_explosive.dm index 68241ce7769..b371f12c2e7 100644 --- a/code/modules/antagonists/ninja/ninja_explosive.dm +++ b/code/modules/antagonists/ninja/ninja_explosive.dm @@ -1,14 +1,14 @@ /** * # Spider Charge * - * A unique version of c4 possessed only by the space ninja. Has a stronger blast radius. - * Can only be detonated by space ninjas with the bombing objective. Can only be set up where the objective says it can. + * A unique version of c4 possessed only by the space ninja. Has a stronger blast radius. + * Can only be detonated by space ninjas with the bombing objective. Can only be set up where the objective says it can. * When it primes, the space ninja responsible will have their objective set to complete. * */ /obj/item/grenade/c4/ninja name = "spider charge" - desc = "A modified C-4 charge supplied to you by the Spider Clan. Its explosive power has been juiced up, but only works in one specific area." + desc = "A modified C-4 charge supplied by the Spider Clan. It has great explosive power, but is keyed to only work in one specific area." icon_state = "ninja-explosive0" inhand_icon_state = "ninja-explosive" boom_sizes = list(4, 8, 12) @@ -22,6 +22,15 @@ detonation_area = null return ..() +/obj/item/grenade/c4/ninja/examine(mob/user) + . = ..() + if (!IS_SPACE_NINJA(user)) + return + if (isnull(detonation_area)) + . += span_notice("This one was provided with no destination set, and cannot be used.") + else + . += span_notice("This device will only function in [detonation_area].") + /** * set_detonation_area * @@ -38,9 +47,9 @@ return detonation_area = objective.detonation_location -/obj/item/grenade/c4/ninja/afterattack(atom/movable/AM, mob/ninja, flag) +/obj/item/grenade/c4/ninja/afterattack(atom/movable/target, mob/ninja, flag) if(!IS_SPACE_NINJA(ninja)) - to_chat(ninja, span_notice("While it appears normal, you can't seem to detonate the charge.")) + say("Access denied.") return . |= AFTERATTACK_PROCESSED_ITEM if (!check_loc(ninja)) @@ -59,15 +68,15 @@ //Since we already did the checks in afterattack, the denonator must be a ninja with the bomb objective. if(!detonator) return - var/mob/ninja = detonator.resolve() . = ..() if(!.) return + var/mob/ninja = detonator.resolve() if (isnull(ninja)) return var/datum/antagonist/ninja/ninja_antag = ninja.mind.has_antag_datum(/datum/antagonist/ninja) var/datum/objective/plant_explosive/objective = locate() in ninja_antag.objectives - objective.completed = TRUE + objective?.completed = TRUE /** * check_loc @@ -78,11 +87,11 @@ * * mob/user - The planter of the c4 */ /obj/item/grenade/c4/ninja/proc/check_loc(mob/user) - if(!detonation_area) - to_chat(user, span_notice("You can't seem to activate the charge. It's location-locked, but you don't know where to detonate it.")) + if(isnull(detonation_area)) + balloon_alert(user, "no location set!") return FALSE if((get_area(target) != detonation_area) && (get_area(src) != detonation_area)) if (!active) - to_chat(user, span_notice("This isn't the location you're supposed to use this!")) + balloon_alert(user, "wrong location!") return FALSE return TRUE diff --git a/code/modules/antagonists/space_ninja/space_ninja.dm b/code/modules/antagonists/space_ninja/space_ninja.dm index eaa117464e7..bf19635ed9b 100644 --- a/code/modules/antagonists/space_ninja/space_ninja.dm +++ b/code/modules/antagonists/space_ninja/space_ninja.dm @@ -9,6 +9,9 @@ antag_moodlet = /datum/mood_event/focused suicide_cry = "FOR THE SPIDER CLAN!!" preview_outfit = /datum/outfit/ninja_preview + can_assign_self_objectives = TRUE + ui_name = "AntagInfoNinja" + default_custom_objective = "Destroy vital station infrastructure, without being seen." ///Whether or not this ninja will obtain objectives var/give_objectives = TRUE @@ -33,7 +36,7 @@ antag_memory += "Surprise is my weapon. Shadows are my armor. Without them, I am nothing.
" /datum/objective/cyborg_hijack - explanation_text = "Use your gloves to convert at least one cyborg to aide you in sabotaging the station." + explanation_text = "Use your gloves to convert at least one cyborg to aid you in sabotaging the station." /datum/objective/door_jack ///How many doors that need to be opened using the gloves to pass the objective diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index 9a81645716d..3813009be42 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -14,6 +14,8 @@ ui_name = "AntagInfoTraitor" suicide_cry = "FOR THE SYNDICATE!!" preview_outfit = /datum/outfit/traitor + can_assign_self_objectives = TRUE + default_custom_objective = "Perform an overcomplicated heist on valuable Nanotrasen assets." var/give_objectives = TRUE var/should_give_codewords = TRUE ///give this traitor an uplink? @@ -67,6 +69,9 @@ uplink_handler.has_objectives = TRUE uplink_handler.generate_objectives() + uplink_handler.can_replace_objectives = CALLBACK(src, PROC_REF(can_change_objectives)) + uplink_handler.replace_objectives = CALLBACK(src, PROC_REF(submit_player_objective)) + if(uplink_handler.progression_points < SStraitor.current_global_progression) uplink_handler.progression_points = SStraitor.current_global_progression * SStraitor.newjoin_progression_coeff @@ -94,8 +99,10 @@ return ..() /datum/antagonist/traitor/on_removal() - if(uplink_handler) + if(!isnull(uplink_handler)) uplink_handler.has_objectives = FALSE + uplink_handler.can_replace_objectives = null + uplink_handler.replace_objectives = null return ..() /datum/antagonist/traitor/proc/traitor_objective_to_html(datum/traitor_objective/to_display) @@ -146,6 +153,10 @@ result += "Force add objective
" return result +/// Returns true if we're allowed to assign ourselves a new objective +/datum/antagonist/traitor/proc/can_change_objectives() + return can_assign_self_objectives + /// proc that generates the traitors replacement uplink code and radio frequency /datum/antagonist/traitor/proc/generate_replacement_codes() replacement_uplink_code = "[pick(GLOB.phonetic_alphabet)] [rand(10,99)]" @@ -306,11 +317,9 @@ if(objectives.len) //If the traitor had no objectives, don't need to process this. var/count = 1 for(var/datum/objective/objective in objectives) - if(objective.check_completion()) - objectives_text += "
Objective #[count]: [objective.explanation_text] [span_greentext("Success!")]" - else - objectives_text += "
Objective #[count]: [objective.explanation_text] [span_redtext("Fail.")]" + if(!objective.check_completion()) traitor_won = FALSE + objectives_text += "
Objective #[count]: [objective.explanation_text] [objective.get_roundend_success_suffix()]" count++ if(uplink_handler.final_objective) objectives_text += "
[span_greentext("[traitor_won ? "Additionally" : "However"], the final objective \"[uplink_handler.final_objective]\" was completed!")]" diff --git a/code/modules/antagonists/traitor/uplink_handler.dm b/code/modules/antagonists/traitor/uplink_handler.dm index 9b2b7dfb706..06ce9667035 100644 --- a/code/modules/antagonists/traitor/uplink_handler.dm +++ b/code/modules/antagonists/traitor/uplink_handler.dm @@ -48,11 +48,20 @@ var/debug_mode = FALSE /// Whether the shop is locked or not. If set to true, nothing can be purchased. var/shop_locked = FALSE + /// Callback which returns true if you can choose to replace your objectives with different ones + var/datum/callback/can_replace_objectives + /// Callback which performs that operation + var/datum/callback/replace_objectives /datum/uplink_handler/New() . = ..() maximum_potential_objectives = CONFIG_GET(number/maximum_potential_objectives) +/datum/uplink_handler/Destroy(force, ...) + can_replace_objectives = null + replace_objectives = null + return ..() + /// Called whenever an update occurs on this uplink handler. Used for UIs /datum/uplink_handler/proc/on_update() SEND_SIGNAL(src, COMSIG_UPLINK_HANDLER_ON_UPDATE) diff --git a/code/modules/antagonists/wizard/wizard.dm b/code/modules/antagonists/wizard/wizard.dm index 550ec1d30cc..c03795f3389 100644 --- a/code/modules/antagonists/wizard/wizard.dm +++ b/code/modules/antagonists/wizard/wizard.dm @@ -12,6 +12,8 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key) ui_name = "AntagInfoWizard" suicide_cry = "FOR THE FEDERATION!!" preview_outfit = /datum/outfit/wizard + can_assign_self_objectives = TRUE + default_custom_objective = "Demonstrate your incredible and destructive magical powers." var/give_objectives = TRUE var/strip = TRUE //strip before equipping var/allow_rename = TRUE @@ -192,9 +194,9 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key) H.equipOutfit(outfit_type) /datum/antagonist/wizard/ui_static_data(mob/user) - . = ..() var/list/data = list() data["objectives"] = get_objectives() + data["can_change_objective"] = can_assign_self_objectives return data /datum/antagonist/wizard/ui_data(mob/user) @@ -253,6 +255,7 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key) /datum/antagonist/wizard/apprentice name = "Wizard Apprentice" antag_hud_name = "apprentice" + can_assign_self_objectives = FALSE var/datum/mind/master var/school = APPRENTICE_DESTRUCTION outfit_type = /datum/outfit/wizard/apprentice @@ -370,6 +373,7 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key) show_in_antagpanel = FALSE outfit_type = /datum/outfit/wizard/academy move_to_lair = FALSE + can_assign_self_objectives = FALSE /datum/antagonist/wizard/academy/assign_ritual() return // Has other duties to be getting on with @@ -404,13 +408,11 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key) parts += "
Grand Rituals completed: [ritual.times_completed]
" var/count = 1 - var/wizardwin = 1 + var/wizardwin = TRUE for(var/datum/objective/objective in objectives) - if(objective.check_completion()) - parts += "Objective #[count]: [objective.explanation_text] [span_greentext("Success!")]" - else - parts += "Objective #[count]: [objective.explanation_text] [span_redtext("Fail.")]" - wizardwin = 0 + if(!objective.check_completion()) + wizardwin = FALSE + parts += "Objective #[count]: [objective.explanation_text] [objective.get_roundend_success_suffix()]" count++ if(wizardwin) diff --git a/tgui/packages/tgui/interfaces/AntagInfoChangeling.tsx b/tgui/packages/tgui/interfaces/AntagInfoChangeling.tsx index 82b3fe2ff53..f3eda78c661 100644 --- a/tgui/packages/tgui/interfaces/AntagInfoChangeling.tsx +++ b/tgui/packages/tgui/interfaces/AntagInfoChangeling.tsx @@ -1,8 +1,9 @@ +import { BooleanLike } from 'common/react'; import { multiline } from 'common/string'; import { useBackend, useSharedState } from '../backend'; import { Button, Dimmer, Dropdown, Section, Stack, NoticeBox } from '../components'; import { Window } from '../layouts'; -import { ObjectivePrintout, Objective } from './common/Objectives'; +import { ObjectivePrintout, Objective, ReplaceObjectivesButton } from './common/Objectives'; const hivestyle = { fontWeight: 'bold', @@ -50,17 +51,18 @@ type Info = { stolen_antag_info: string; memories: Memory[]; objectives: Objective[]; + can_change_objective: BooleanLike; }; export const AntagInfoChangeling = (props, context) => { return ( - + - + @@ -115,7 +117,7 @@ const HivemindSection = (props, context) => { const IntroductionSection = (props, context) => { const { act, data } = useBackend(context); - const { true_name, hive_name, objectives } = data; + const { true_name, hive_name, objectives, can_change_objective } = data; return (
{ {hive_name}. - + + } + />
diff --git a/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx b/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx index 19fb285b905..952f559e3f8 100644 --- a/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx +++ b/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx @@ -2,7 +2,7 @@ import { useBackend, useLocalState } from '../backend'; import { Section, Stack, Box, Tabs, Button, BlockQuote } from '../components'; import { Window } from '../layouts'; import { BooleanLike } from 'common/react'; -import { ObjectivePrintout, Objective } from './common/Objectives'; +import { ObjectivePrintout, Objective, ReplaceObjectivesButton } from './common/Objectives'; const hereticRed = { color: '#e03c3c', @@ -49,11 +49,12 @@ type Info = { total_sacrifices: number; ascended: BooleanLike; objectives: Objective[]; + can_change_objective: BooleanLike; }; const IntroductionSection = (props, context) => { - const { data } = useBackend(context); - const { objectives } = data; + const { data, act } = useBackend(context); + const { objectives, ascended, can_change_objective } = data; return ( @@ -62,20 +63,33 @@ const IntroductionSection = (props, context) => { - - - - - - + {!ascended && ( + + + } + /> + + )}
@@ -292,7 +306,7 @@ export const AntagInfoHeretic = (props, context) => { const [currentTab, setTab] = useLocalState(context, 'currentTab', 0); return ( - + { const { act, data } = useBackend(context); - const { intro, objectives } = data; + const { intro, objectives, can_change_objective } = data; return (
@@ -46,6 +47,13 @@ const IntroductionSection = (props, context) => { objectives={objectives} titleMessage="Your prime objectives:" objectivePrefix="≥-" + objectiveFollowup={ + + } /> diff --git a/tgui/packages/tgui/interfaces/AntagInfoNinja.tsx b/tgui/packages/tgui/interfaces/AntagInfoNinja.tsx new file mode 100644 index 00000000000..a537888af75 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AntagInfoNinja.tsx @@ -0,0 +1,68 @@ +import { BooleanLike } from 'common/react'; +import { useBackend } from '../backend'; +import { Icon, Section, Stack } from '../components'; +import { Window } from '../layouts'; +import { ObjectivePrintout, Objective, ReplaceObjectivesButton } from './common/Objectives'; + +const ninja_emphasis = { + color: 'red', +}; + +type NinjaInfo = { + objectives: Objective[]; + can_change_objective: BooleanLike; +}; + +export const AntagInfoNinja = (props, context) => { + const { data } = useBackend(context); + const { objectives, can_change_objective } = data; + return ( + + + +
+ + + I am an elite mercenary of the Spider Clan. +
A SPACE NINJA! +
+ + Surprise is my weapon. Shadows are my armor. Without them, I am + nothing. + + +
+ Your advanced ninja suit contains many powerful modules. +
It can be recharged by right clicking on station APCs or + other power sources, in order to drain their battery. +
+ Right clicking on some kinds of machines or items wearing your + suit will hack them, to varying effect. Experiment and find out + what you can do! +
+
+ + + } + /> + +
+
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/AntagInfoWizard.tsx b/tgui/packages/tgui/interfaces/AntagInfoWizard.tsx index 81e1e12994e..e5de257851a 100644 --- a/tgui/packages/tgui/interfaces/AntagInfoWizard.tsx +++ b/tgui/packages/tgui/interfaces/AntagInfoWizard.tsx @@ -1,7 +1,8 @@ +import { BooleanLike } from 'common/react'; import { useBackend } from '../backend'; import { Box, Section, Stack } from '../components'; import { Window } from '../layouts'; -import { ObjectivePrintout, Objective } from './common/Objectives'; +import { ObjectivePrintout, Objective, ReplaceObjectivesButton } from './common/Objectives'; const teleportstyle = { color: 'yellow', @@ -44,14 +45,15 @@ type GrandRitual = { type Info = { objectives: Objective[]; ritual: GrandRitual; + can_change_objective: BooleanLike; }; export const AntagInfoWizard = (props, context) => { - const { data } = useBackend(context); - const { ritual, objectives } = data; + const { data, act } = useBackend(context); + const { ritual, objectives, can_change_objective } = data; return ( - + @@ -64,9 +66,18 @@ export const AntagInfoWizard = (props, context) => { } + objectiveFollowup={ + + } /> + + +
diff --git a/tgui/packages/tgui/interfaces/Uplink/PrimaryObjectiveMenu.tsx b/tgui/packages/tgui/interfaces/Uplink/PrimaryObjectiveMenu.tsx index 6790847bcfb..e3729937ce3 100644 --- a/tgui/packages/tgui/interfaces/Uplink/PrimaryObjectiveMenu.tsx +++ b/tgui/packages/tgui/interfaces/Uplink/PrimaryObjectiveMenu.tsx @@ -1,16 +1,19 @@ -import { Box, Dimmer, Section, Stack } from '../../components'; +import { useBackend } from '../../backend'; +import { Box, Button, Dimmer, Section, Stack } from '../../components'; import { ObjectiveElement } from './ObjectiveMenu'; type PrimaryObjectiveMenuProps = { primary_objectives; final_objective; + can_renegotiate; }; export const PrimaryObjectiveMenu = ( props: PrimaryObjectiveMenuProps, context ) => { - const { primary_objectives, final_objective } = props; + const { act } = useBackend(context); + const { primary_objectives, final_objective, can_renegotiate } = props; return (
@@ -77,6 +80,17 @@ export const PrimaryObjectiveMenu = ( ))}
+ {!!can_renegotiate && ( + +
); }; diff --git a/tgui/packages/tgui/interfaces/Uplink/index.tsx b/tgui/packages/tgui/interfaces/Uplink/index.tsx index ebd72fae1e3..91a9adb0ba9 100644 --- a/tgui/packages/tgui/interfaces/Uplink/index.tsx +++ b/tgui/packages/tgui/interfaces/Uplink/index.tsx @@ -59,6 +59,7 @@ type UplinkData = { maximum_potential_objectives: number; purchased_items: number; shop_locked: BooleanLike; + can_renegotiate: BooleanLike; }; type UplinkState = { @@ -160,6 +161,7 @@ export class Uplink extends Component<{}, UplinkState> { telecrystals, progression_points, primary_objectives, + can_renegotiate, completed_final_objective, active_objectives, potential_objectives, @@ -379,6 +381,7 @@ export class Uplink extends Component<{}, UplinkState> { )) || (currentTab === 1 && has_objectives && ( diff --git a/tgui/packages/tgui/interfaces/common/Objectives.tsx b/tgui/packages/tgui/interfaces/common/Objectives.tsx index 4ffb75d8d31..a6bd5b539bf 100644 --- a/tgui/packages/tgui/interfaces/common/Objectives.tsx +++ b/tgui/packages/tgui/interfaces/common/Objectives.tsx @@ -1,6 +1,7 @@ import { BooleanLike } from 'common/react'; import { InfernoNode } from 'inferno'; -import { Stack } from '../../components'; +import { useBackend } from '../../backend'; +import { Button, Stack } from '../../components'; export type Objective = { // The title of the objective, not actually displayed so optional @@ -54,3 +55,38 @@ export const ObjectivePrintout = (props: ObjectivePrintoutProps, context) => {
); }; + +type ReplaceObjectivesProps = { + // Whether we can actually use this button + can_change_objective: BooleanLike; + // What do we call our button + button_title: string; + // What colour is our button + button_colour: string; + // Tooltip to display on our button + button_tooltip?: string; +}; + +export const ReplaceObjectivesButton = ( + props: ReplaceObjectivesProps, + context +) => { + const { + can_change_objective, + button_title, + button_colour, + button_tooltip = 'Replace your existing objectives with a custom one. This action can only be taken once', + } = props; + const { act } = useBackend(context); + if (!can_change_objective) { + return null; + } + return ( +