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.

<details>
  <summary>Other previews</summary>
  

![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)
</details>

## 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

🆑
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.
/🆑
This commit is contained in:
Jacquerel
2023-09-07 20:25:52 +01:00
committed by GitHub
parent 772d3453e4
commit 6d258e5527
24 changed files with 402 additions and 85 deletions
+4 -9
View File
@@ -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 += "<b>[objective.objective_name] #[count]</b>: [objective.explanation_text] [span_greentext("Success!")]"
else
objective_parts += "<b>[objective.objective_name] #[count]</b>: [objective.explanation_text] [span_redtext("Fail.")]"
objective_parts += "<b>[objective.objective_name] #[count]</b>: [objective.explanation_text] [objective.get_roundend_success_suffix()]"
count++
return objective_parts.Join("<br>")
+4
View File
@@ -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
+35
View File
@@ -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")
+7
View File
@@ -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()
+1
View File
@@ -53,6 +53,7 @@ GLOBAL_VAR(antag_prototypes)
<br>"
obj_count++
result += "<a href='?src=[REF(owner)];obj_add=1;target_antag=[REF(src)]'>Add objective</a><br>"
result += "<a href='?src=[REF(owner)];obj_prompt_custom=1;target_antag=[REF(src)]'>Prompt custom objective entry</a><br>"
result += "<a href='?src=[REF(owner)];obj_announce=1'>Announce objectives</a><br>"
return result
@@ -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
@@ -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 += "<B>Objective #[objective_count]</B>: [objective.explanation_text] [span_greentext("Success!")]"
else
report += "<B>Objective #[objective_count]</B>: [objective.explanation_text] [span_redtext("Fail.")]"
if(!objective.check_completion())
win = FALSE
report += "<B>Objective #[objective_count]</B>: [objective.explanation_text] [objective.get_roundend_success_suffix()]"
objective_count++
if(win)
report += span_greentext("The [name] was successful!")
@@ -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 += "<b>Objective #[count]</b>: [objective.explanation_text] [span_greentext("Success!</b>")]"
else
parts += "<b>Objective #[count]</b>: [objective.explanation_text] [span_redtext("Fail.")]"
if(!objective.check_completion())
changeling_win = FALSE
parts += "<b>Objective #[count]</b>: [objective.explanation_text] [objective.get_roundend_success_suffix()]"
count++
if(changeling_win)
+1 -4
View File
@@ -114,10 +114,7 @@
parts += "<b>The cultists' objectives were:</b>"
var/count = 1
for(var/datum/objective/objective in objectives)
if(objective.check_completion())
parts += "<b>Objective #[count]</b>: [objective.explanation_text] [span_greentext("Success!")]"
else
parts += "<b>Objective #[count]</b>: [objective.explanation_text] [span_redtext("Fail.")]"
parts += "<b>Objective #[count]</b>: [objective.explanation_text] [objective.get_roundend_success_suffix()]"
count++
if(members.len)
@@ -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 += "<b>Objective #[count]</b>: [objective.explanation_text] [span_greentext("Success!")]"
else
parts += "<b>Objective #[count]</b>: [objective.explanation_text] [span_redtext("Fail.")]"
if(!objective.check_completion())
succeeded = FALSE
parts += "<b>Objective #[count]</b>: [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
+5 -4
View File
@@ -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 += "<br><B>Objective #[count]</B>: [objective.explanation_text] [span_greentext("Success!")]"
else
objectives_text += "<br><B>Objective #[count]</B>: [objective.explanation_text] [span_redtext("Fail.")]"
if(!objective.check_completion())
malf_ai_won = FALSE
objectives_text += "<br><B>Objective #[count]</B>: [objective.explanation_text] [objective.get_roundend_success_suffix()]"
count++
result += objectives_text
@@ -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
@@ -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.<br>"
/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
@@ -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 += "<a href='?src=[REF(owner)];common=give_objective'>Force add objective</a><br>"
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 += "<br><B>Objective #[count]</B>: [objective.explanation_text] [span_greentext("Success!")]"
else
objectives_text += "<br><B>Objective #[count]</B>: [objective.explanation_text] [span_redtext("Fail.")]"
if(!objective.check_completion())
traitor_won = FALSE
objectives_text += "<br><B>Objective #[count]</B>: [objective.explanation_text] [objective.get_roundend_success_suffix()]"
count++
if(uplink_handler.final_objective)
objectives_text += "<br>[span_greentext("[traitor_won ? "Additionally" : "However"], the final objective \"[uplink_handler.final_objective]\" was completed!")]"
@@ -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)
+9 -7
View File
@@ -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 += "<br><B>Grand Rituals completed:</B> [ritual.times_completed]<br>"
var/count = 1
var/wizardwin = 1
var/wizardwin = TRUE
for(var/datum/objective/objective in objectives)
if(objective.check_completion())
parts += "<B>Objective #[count]</B>: [objective.explanation_text] [span_greentext("Success!")]"
else
parts += "<B>Objective #[count]</B>: [objective.explanation_text] [span_redtext("Fail.")]"
wizardwin = 0
if(!objective.check_completion())
wizardwin = FALSE
parts += "<B>Objective #[count]</B>: [objective.explanation_text] [objective.get_roundend_success_suffix()]"
count++
if(wizardwin)
@@ -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 (
<Window width={720} height={720}>
<Window width={720} height={750}>
<Window.Content
style={{
'backgroundImage': 'none',
}}>
<Stack vertical fill>
<Stack.Item maxHeight={13.2}>
<Stack.Item maxHeight={16}>
<IntroductionSection />
</Stack.Item>
<Stack.Item grow={4}>
@@ -115,7 +117,7 @@ const HivemindSection = (props, context) => {
const IntroductionSection = (props, context) => {
const { act, data } = useBackend<Info>(context);
const { true_name, hive_name, objectives } = data;
const { true_name, hive_name, objectives, can_change_objective } = data;
return (
<Section
fill
@@ -127,7 +129,16 @@ const IntroductionSection = (props, context) => {
<span style={hivestyle}> {hive_name}</span>.
</Stack.Item>
<Stack.Item>
<ObjectivePrintout objectives={objectives} />
<ObjectivePrintout
objectives={objectives}
objectiveFollowup={
<ReplaceObjectivesButton
can_change_objective={can_change_objective}
button_title={'Evolve New Directives'}
button_colour={'green'}
/>
}
/>
</Stack.Item>
</Stack>
</Section>
@@ -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<Info>(context);
const { objectives } = data;
const { data, act } = useBackend<Info>(context);
const { objectives, ascended, can_change_objective } = data;
return (
<Stack justify="space-evenly" height="100%" width="100%">
@@ -62,20 +63,33 @@ const IntroductionSection = (props, context) => {
<Stack vertical>
<FlavorSection />
<Stack.Divider />
<GuideSection />
<Stack.Divider />
<InformationSection />
<Stack.Divider />
<Stack.Item>
<ObjectivePrintout
fill
titleMessage="In order to ascend, you have these tasks to fulfill:"
objectives={objectives}
/>
</Stack.Item>
{!ascended && (
<Stack.Item>
<ObjectivePrintout
fill
titleMessage={
can_change_objective
? 'In order to ascend, you have these tasks to fulfill'
: 'Use your dark knowledge to fulfil your personal goal'
}
objectives={objectives}
objectiveFollowup={
<ReplaceObjectivesButton
can_change_objective={can_change_objective}
button_title={'Reject Ascension'}
button_colour={'red'}
button_tooltip={
'Turn your back on the Mansus to accomplish a task of your choosing. Selecting this option will prevent you from ascending!'
}
/>
}
/>
</Stack.Item>
)}
</Stack>
</Section>
</Stack.Item>
@@ -292,7 +306,7 @@ export const AntagInfoHeretic = (props, context) => {
const [currentTab, setTab] = useLocalState(context, 'currentTab', 0);
return (
<Window width={675} height={625}>
<Window width={675} height={635}>
<Window.Content
style={{
'background-image': 'none',
@@ -4,7 +4,7 @@ import { GenericUplink, Item } from './Uplink/GenericUplink';
import { BlockQuote, Button, Section, Stack, Tabs } from '../components';
import { BooleanLike } from 'common/react';
import { Window } from '../layouts';
import { ObjectivePrintout, Objective } from './common/Objectives';
import { ObjectivePrintout, Objective, ReplaceObjectivesButton } from './common/Objectives';
const allystyle = {
fontWeight: 'bold',
@@ -32,11 +32,12 @@ type Info = {
processingTime: string;
objectives: Objective[];
categories: any[];
can_change_objective: BooleanLike;
};
const IntroductionSection = (props, context) => {
const { act, data } = useBackend<Info>(context);
const { intro, objectives } = data;
const { intro, objectives, can_change_objective } = data;
return (
<Section fill title="Intro" scrollable>
<Stack vertical fill>
@@ -46,6 +47,13 @@ const IntroductionSection = (props, context) => {
objectives={objectives}
titleMessage="Your prime objectives:"
objectivePrefix="&#8805-"
objectiveFollowup={
<ReplaceObjectivesButton
can_change_objective={can_change_objective}
button_title={'Overwrite Objectives Data'}
button_colour={'green'}
/>
}
/>
</Stack.Item>
</Stack>
@@ -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<NinjaInfo>(context);
const { objectives, can_change_objective } = data;
return (
<Window width={550} height={450} theme="hackerman">
<Window.Content>
<Icon
size={30}
name="spider"
color="#003300"
position="absolute"
top="10%"
left="10%"
/>
<Section scrollable fill>
<Stack vertical textColor="green">
<Stack.Item textAlign="center" fontSize="20px">
I am an elite mercenary of the Spider Clan.
<br />A <span style={ninja_emphasis}> SPACE NINJA</span>!
</Stack.Item>
<Stack.Item textAlign="center" italic>
Surprise is my weapon. Shadows are my armor. Without them, I am
nothing.
</Stack.Item>
<Stack.Item>
<Section fill>
Your advanced ninja suit contains many powerful modules.
<br /> It can be recharged by right clicking on station APCs or
other power sources, in order to drain their battery.
<br />
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!
</Section>
</Stack.Item>
<Stack.Item>
<ObjectivePrintout
objectives={objectives}
objectiveFollowup={
<ReplaceObjectivesButton
can_change_objective={can_change_objective}
button_title={'Adapt Mission Parameters'}
button_colour={'green'}
/>
}
/>
</Stack.Item>
</Stack>
</Section>
</Window.Content>
</Window>
);
};
@@ -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<Info>(context);
const { ritual, objectives } = data;
const { data, act } = useBackend<Info>(context);
const { ritual, objectives, can_change_objective } = data;
return (
<Window width={620} height={620} theme="wizard">
<Window width={620} height={630} theme="wizard">
<Window.Content>
<Stack vertical fill>
<Stack.Item grow>
@@ -64,9 +66,18 @@ export const AntagInfoWizard = (props, context) => {
<ObjectivePrintout
objectives={objectives}
titleMessage="The Space Wizard Federation has given you the following tasks:"
objectiveFollowup={<RitualPrintout ritual={ritual} />}
objectiveFollowup={
<ReplaceObjectivesButton
can_change_objective={can_change_objective}
button_title={'Declare Personal Quest'}
button_colour={'violet'}
/>
}
/>
</Stack.Item>
<Stack.Item>
<RitualPrintout ritual={ritual} />
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
@@ -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 (
<Section fill>
<Section>
@@ -77,6 +80,17 @@ export const PrimaryObjectiveMenu = (
))}
</Stack>
</Section>
{!!can_renegotiate && (
<Box mt={3} mb={5} bold fontSize={1.2} align="center" color="white">
<Button
content={'Renegotiate Contract'}
tooltip={
'Replace your existing primary objectives with a custom one. This action can only be performed once.'
}
onClick={() => act('renegotiate_objectives')}
/>
</Box>
)}
</Section>
);
};
@@ -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> {
<PrimaryObjectiveMenu
primary_objectives={primary_objectives}
final_objective={completed_final_objective}
can_renegotiate={can_renegotiate}
/>
)) ||
(currentTab === 1 && has_objectives && (
@@ -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) => {
</Stack>
);
};
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 (
<Button
color={button_colour}
content={button_title}
tooltip={button_tooltip}
onClick={() => act('change_objectives')}
/>
);
};