From 6d258e55276a61aa96fd8a7623644af6f95d7e96 Mon Sep 17 00:00:00 2001 From: Jacquerel Date: Thu, 7 Sep 2023 20:25:52 +0100 Subject: [PATCH] Choose your own Objective (#78118) ## About The Pull Request ![image](https://github.com/tgstation/tgstation/assets/7483112/12d3cf8d-4538-42c3-a962-bf2d6653ef55) ![image](https://github.com/tgstation/tgstation/assets/7483112/60981726-8495-47e8-8924-dd18c000fdfe) This PR adds the ability for solo antagonists (Traitor, Changeling, Heretic, Wizard, Malfunctioning AI, and Ninja) to "write-in" their own objectives in place of the ones the game gave them. For traitors this is located on the uplink (though the button is not present if you aren't the owner of the uplink, thieves can't give you a troll objective) and for everyone else it is on the antagonist information panel. Pressing this button will replace all of your objectives _except_ ones like "escape alive" or "survive" or "die a glorious death". By default a player can only do this once per round, but the Traitor Panel admin tool contains a button which can display the prompt again. Custom-entered objectives are not mechanically tracked in any way and do not report success or failure on the round end screen, whether they were successful or not is up to you the audience. Resultingly, doing this will make you ineligible for getting the hardcore random points from completing your objectives. Admins are capable of using this button to show the prompt to _any_ antagonist, including team antagonists, but do so at their own risk. A single cultist changing their objective to something else only updates them, not their team. Admins can also choose whether or not it replaces existing objectives or just adds a new one entered by the player. When someone sets a new objective, it informs any active administrators of their choice and provides linked buttons to send them a syndicate radio message or immediately smite them, because unfortunately our players cannot always be trusted with arbitrary text entry. I _didn't_ make this a system which requires approval because I don't think admins _want_ that level of micromanagement, although plausibly if it is thought to be a good idea I could add a time delay and reject button similar to the "rename the station" charter item. Heretics work slightly differently and have an "Are you sure?" confirmation other antagonists lack. This is because Heretic objectives are directly tied to ascension, thus by replacing the objectives you are locking yourself out from being able to do it. I don't _necessarily_ think this is a terrible thing, because "try to ascend" _is_ the default objective. There's no point entering a new one if that is what you want to do anyway. While I was in here I gave Ninjas a very rudimentary custom antagonist panel, because they didn't have one. I also made their C4 display where it can be detonated on examine, in case you no longer have the relevant objective to reference.
Other previews ![image](https://github.com/tgstation/tgstation/assets/7483112/55ca6ce5-a5a5-4201-9bc6-88dd4e4e35a1) ![image](https://github.com/tgstation/tgstation/assets/7483112/cba5b921-addd-4886-8d83-b0b383607efc) ![image](https://github.com/tgstation/tgstation/assets/7483112/69212c87-5a93-4524-ab9a-f2540fc61617) ![image](https://github.com/tgstation/tgstation/assets/7483112/fc5c1d26-18ef-4481-857d-ae4142e13f95) ![image](https://github.com/tgstation/tgstation/assets/7483112/046589ac-e7a5-4006-8087-42e9bcd1016e)
## Why It's Good For The Game It's a reasonably frequent refrain that "Objectives are just guidelines" and that more interesting players should strive to ignore them and do something else, I would hope this would encourage that kind of behaviour. If you _are_ going off and doing your own thing, now other players can actually see what it was that you were trying to do when the round ends (and judge you based on whether you actually did it). ## Changelog :cl: add: Traitors, Changelings, Heretics, Wizards, Malfunctioning AIs, and Ninjas can now all reject their original objectives and provide one of their own in its place. A Heretic doing this will no longer be able to ascend. add: "Custom" objectives which aren't mechanically tracked will no longer report success or failure upon round end. qol: Space Ninja spider charges will now display where they can be detonated when examined, if you are a ninja. /:cl: --- code/__HELPERS/roundend.dm | 13 ++-- code/datums/components/uplink.dm | 4 ++ code/datums/mind/_mind.dm | 35 ++++++++++ code/game/gamemodes/objective.dm | 7 ++ code/modules/admin/antag_panel.dm | 1 + .../antagonists/_common/antag_datum.dm | 70 +++++++++++++++++++ .../modules/antagonists/_common/antag_team.dm | 6 +- .../antagonists/changeling/changeling.dm | 12 ++-- code/modules/antagonists/cult/cult_team.dm | 5 +- .../antagonists/heretic/heretic_antag.dm | 24 +++++-- code/modules/antagonists/malf_ai/malf_ai.dm | 9 +-- .../antagonists/ninja/ninja_explosive.dm | 29 +++++--- .../antagonists/space_ninja/space_ninja.dm | 5 +- .../antagonists/traitor/datum_traitor.dm | 19 +++-- .../antagonists/traitor/uplink_handler.dm | 9 +++ code/modules/antagonists/wizard/wizard.dm | 16 +++-- .../tgui/interfaces/AntagInfoChangeling.tsx | 21 ++++-- .../tgui/interfaces/AntagInfoHeretic.tsx | 42 +++++++---- .../tgui/interfaces/AntagInfoMalf.tsx | 12 +++- .../tgui/interfaces/AntagInfoNinja.tsx | 68 ++++++++++++++++++ .../tgui/interfaces/AntagInfoWizard.tsx | 21 ++++-- .../Uplink/PrimaryObjectiveMenu.tsx | 18 ++++- .../packages/tgui/interfaces/Uplink/index.tsx | 3 + .../tgui/interfaces/common/Objectives.tsx | 38 +++++++++- 24 files changed, 402 insertions(+), 85 deletions(-) create mode 100644 tgui/packages/tgui/interfaces/AntagInfoNinja.tsx 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 ( +