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 (
-
+
+ }
+ />
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 (
+