diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm
index 16232a71cd5f..c6978d1c884e 100644
--- a/code/__DEFINES/antagonists.dm
+++ b/code/__DEFINES/antagonists.dm
@@ -27,6 +27,7 @@
#define APPRENTICE_ROBELESS "robeless"
#define APPRENTICE_HEALING "healing"
+#define IS_INFERNAL_AGENT(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/infernal_affairs))
//Blob
/// blob gets a free reroll every X time
@@ -57,6 +58,9 @@
/// because they have nothing else that supports an implant.
#define UPLINK_IMPLANT_TELECRYSTAL_COST 4
+///Signal sent to a mob when they purchase an item from their uplink.
+#define COMSIG_ON_UPLINK_PURCHASE "comsig_on_uplink_purchase"
+
//ERT Types
#define ERT_BLUE "Blue"
#define ERT_RED "Red"
diff --git a/code/__DEFINES/contracts.dm b/code/__DEFINES/contracts.dm
deleted file mode 100644
index c6e23394ba2d..000000000000
--- a/code/__DEFINES/contracts.dm
+++ /dev/null
@@ -1,44 +0,0 @@
-#define CONTRACT_POWER "power"
-#define CONTRACT_WEALTH "wealth"
-#define CONTRACT_PRESTIGE "prestige"
-#define CONTRACT_MAGIC "magic"
-#define CONTRACT_REVIVE "revive"
-#define CONTRACT_FRIEND "friend"
-#define CONTRACT_KNOWLEDGE "knowledge"
-#define CONTRACT_UNWILLING "unwilling"
-
-#define BANE_SALT "salt"
-#define BANE_LIGHT "light"
-#define BANE_IRON "iron"
-#define BANE_WHITECLOTHES "whiteclothes"
-#define BANE_SILVER "silver"
-#define BANE_HARVEST "harvest"
-#define BANE_TOOLBOX "toolbox"
-
-#define OBLIGATION_FOOD "food"
-#define OBLIGATION_FIDDLE "fiddle"
-#define OBLIGATION_DANCEOFF "danceoff"
-#define OBLIGATION_GREET "greet"
-#define OBLIGATION_PRESENCEKNOWN "presenceknown"
-#define OBLIGATION_SAYNAME "sayname"
-#define OBLIGATION_ANNOUNCEKILL "announcekill"
-#define OBLIGATION_ANSWERTONAME "answername"
-
-#define BAN_HURTWOMAN "hurtwoman"
-#define BAN_CHAPEL "chapel"
-#define BAN_HURTPRIEST "hurtpriest"
-#define BAN_AVOIDWATER "avoidwater"
-#define BAN_STRIKEUNCONSCIOUS "strikeunconscious"
-#define BAN_HURTLIZARD "hurtlizard"
-#define BAN_HURTANIMAL "hurtanimal"
-
-#define BANISH_WATER "water"
-#define BANISH_COFFIN "coffin"
-#define BANISH_FORMALDYHIDE "embalm"
-#define BANISH_RUNES "runes"
-#define BANISH_CANDLES "candles"
-#define BANISH_DESTRUCTION "destruction"
-#define BANISH_FUNERAL_GARB "funeral"
-
-#define LORE 1
-#define LAW 2
diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm
index 1fa697d1e990..0ff03d6e6e95 100644
--- a/code/__DEFINES/role_preferences.dm
+++ b/code/__DEFINES/role_preferences.dm
@@ -6,58 +6,60 @@
//These are synced with the Database, if you change the values of the defines
//then you MUST update the database!
-#define ROLE_SYNDICATE "Syndicate"
-#define ROLE_TRAITOR "Traitor"
-#define ROLE_OPERATIVE "Operative"
-#define ROLE_CLOWNOP "Clown Operative"
-#define ROLE_CHANGELING "Changeling"
-#define ROLE_WIZARD "Wizard"
-#define ROLE_RAGINMAGES "Ragin Mages"
-#define ROLE_BULLSHITMAGES "Bullshit Mages"
-#define ROLE_MALF "Malf AI"
-#define ROLE_REV "Revolutionary"
-#define ROLE_REV_HEAD "Head Revolutionary"
-#define ROLE_ALIEN "Xenomorph"
-#define ROLE_PAI "pAI"
-#define ROLE_CULTIST "Cultist"
-#define ROLE_HERETIC "Heretic"
-#define ROLE_BLOB "Blob"
-#define ROLE_NINJA "Space Ninja"
-#define ROLE_MONKEY "Monkey"
-#define ROLE_ABDUCTOR "Abductor"
-#define ROLE_REVENANT "Revenant"
-#define ROLE_SERVANT_OF_RATVAR "Servant of Ratvar"
-#define ROLE_BROTHER "Blood Brother"
-#define ROLE_BRAINWASHED "Brainwashed Victim"
-#define ROLE_HIVE "Hivemind Host"
-#define ROLE_OBSESSED "Obsessed"
-#define ROLE_SENTIENCE "Sentient Creature"
-#define ROLE_MOUSE "Mouse"
-#define ROLE_MIND_TRANSFER "Mind Transfer Potion"
-#define ROLE_POSIBRAIN "Posibrain"
-#define ROLE_DRONE "Drone"
-#define ROLE_DEATHSQUAD "Deathsquad"
-#define ROLE_LAVALAND "Lavaland"
-#define ROLE_FUGITIVE "Fugitive"
-#define ROLE_SHADOWLING "Shadowling" // Yogs
-#define ROLE_VAMPIRE "Vampire" // Yogs
-#define ROLE_GANG "gangster" // Yogs
-#define ROLE_DARKSPAWN "darkspawn" // Yogs
-#define ROLE_HOLOPARASITE "Holoparasite" // Yogs
-#define ROLE_HORROR "Eldritch Horror" // Yogs
-#define ROLE_INFILTRATOR "Infiltrator" // Yogs
-#define ROLE_ZOMBIE "Zombie"
-#define ROLE_BLOODSUCKER "Bloodsucker"
-#define ROLE_VAMPIRICACCIDENT "Vampiric Accident"
-#define ROLE_BLOODSUCKERBREAKOUT "Bloodsucker Breakout"
-#define ROLE_MONSTERHUNTER "Monster Hunter"
-#define ROLE_SPACE_DRAGON "Space Dragon"
-#define ROLE_GOLEM "Golem"
-#define ROLE_SINFULDEMON "Demon of Sin"
-#define ROLE_GHOSTBEACON "Ghost Beacon"
-#define ROLE_NIGHTMARE "Nightmare"
-#define ROLE_DISEASE "Disease"
-#define ROLE_PIRATE "Pirate"
+#define ROLE_SYNDICATE "Syndicate"
+#define ROLE_TRAITOR "Traitor"
+#define ROLE_OPERATIVE "Operative"
+#define ROLE_CLOWNOP "Clown Operative"
+#define ROLE_CHANGELING "Changeling"
+#define ROLE_WIZARD "Wizard"
+#define ROLE_INFERNAL_AFFAIRS "Infernal Affairs Agent"
+#define ROLE_INFERNAL_AFFAIRS_DEVIL "Infernal Devil"
+#define ROLE_RAGINMAGES "Ragin Mages"
+#define ROLE_BULLSHITMAGES "Bullshit Mages"
+#define ROLE_MALF "Malf AI"
+#define ROLE_REV "Revolutionary"
+#define ROLE_REV_HEAD "Head Revolutionary"
+#define ROLE_ALIEN "Xenomorph"
+#define ROLE_PAI "pAI"
+#define ROLE_CULTIST "Cultist"
+#define ROLE_HERETIC "Heretic"
+#define ROLE_BLOB "Blob"
+#define ROLE_NINJA "Space Ninja"
+#define ROLE_MONKEY "Monkey"
+#define ROLE_ABDUCTOR "Abductor"
+#define ROLE_REVENANT "Revenant"
+#define ROLE_SERVANT_OF_RATVAR "Servant of Ratvar"
+#define ROLE_BROTHER "Blood Brother"
+#define ROLE_BRAINWASHED "Brainwashed Victim"
+#define ROLE_HIVE "Hivemind Host"
+#define ROLE_OBSESSED "Obsessed"
+#define ROLE_SENTIENCE "Sentient Creature"
+#define ROLE_MOUSE "Mouse"
+#define ROLE_MIND_TRANSFER "Mind Transfer Potion"
+#define ROLE_POSIBRAIN "Posibrain"
+#define ROLE_DRONE "Drone"
+#define ROLE_DEATHSQUAD "Deathsquad"
+#define ROLE_LAVALAND "Lavaland"
+#define ROLE_FUGITIVE "Fugitive"
+#define ROLE_SHADOWLING "Shadowling" // Yogs
+#define ROLE_VAMPIRE "Vampire" // Yogs
+#define ROLE_GANG "gangster" // Yogs
+#define ROLE_DARKSPAWN "darkspawn" // Yogs
+#define ROLE_HOLOPARASITE "Holoparasite" // Yogs
+#define ROLE_HORROR "Eldritch Horror" // Yogs
+#define ROLE_INFILTRATOR "Infiltrator" // Yogs
+#define ROLE_ZOMBIE "Zombie"
+#define ROLE_BLOODSUCKER "Bloodsucker"
+#define ROLE_VAMPIRICACCIDENT "Vampiric Accident"
+#define ROLE_BLOODSUCKERBREAKOUT "Bloodsucker Breakout"
+#define ROLE_MONSTERHUNTER "Monster Hunter"
+#define ROLE_SPACE_DRAGON "Space Dragon"
+#define ROLE_GOLEM "Golem"
+#define ROLE_SINFULDEMON "Demon of Sin"
+#define ROLE_GHOSTBEACON "Ghost Beacon"
+#define ROLE_NIGHTMARE "Nightmare"
+#define ROLE_DISEASE "Disease"
+#define ROLE_PIRATE "Pirate"
//Missing assignment means it's not a gamemode specific role, IT'S NOT A BUG OR ERROR.
@@ -68,9 +70,11 @@ GLOBAL_LIST_INIT(special_roles, list(
ROLE_TRAITOR = /datum/antagonist/traitor,
ROLE_OPERATIVE = /datum/antagonist/nukeop,
ROLE_CLOWNOP = /datum/antagonist/nukeop/clownop,
- ROLE_CHANGELING = /datum/antagonist/changeling,
+ ROLE_CHANGELING = /datum/antagonist/changeling,
ROLE_WIZARD = /datum/antagonist/wizard,
- ROLE_RAGINMAGES = /datum/antagonist/wizard,
+ ROLE_INFERNAL_AFFAIRS = /datum/antagonist/infernal_affairs,
+ ROLE_INFERNAL_AFFAIRS_DEVIL = /datum/antagonist/devil,
+ ROLE_RAGINMAGES = /datum/antagonist/wizard,
ROLE_BULLSHITMAGES = /datum/antagonist/wizard,
ROLE_MALF = /datum/antagonist/traitor/malf,
ROLE_REV_HEAD = /datum/antagonist/rev/head,
@@ -79,7 +83,7 @@ GLOBAL_LIST_INIT(special_roles, list(
ROLE_HERETIC = /datum/antagonist/heretic,
ROLE_BLOB = /datum/antagonist/blob,
ROLE_NINJA = /datum/antagonist/ninja,
- ROLE_MONKEY = /datum/antagonist/monkey,
+ ROLE_MONKEY = /datum/antagonist/monkey,
ROLE_ABDUCTOR = /datum/antagonist/abductor,
ROLE_REVENANT = /datum/antagonist/revenant,
ROLE_SERVANT_OF_RATVAR = /datum/antagonist/clockcult,
@@ -94,7 +98,7 @@ GLOBAL_LIST_INIT(special_roles, list(
ROLE_HOLOPARASITE = /datum/antagonist/guardian, // Yogs
ROLE_HORROR = /datum/antagonist/horror, // Yogs
ROLE_INFILTRATOR = /datum/antagonist/infiltrator, // Yogs
- ROLE_ZOMBIE = /datum/antagonist/zombie,
+ ROLE_ZOMBIE = /datum/antagonist/zombie,
ROLE_BLOODSUCKER = /datum/antagonist/bloodsucker,
ROLE_MONSTERHUNTER = /datum/antagonist/monsterhunter,
ROLE_SPACE_DRAGON = /datum/antagonist/space_dragon,
@@ -108,6 +112,6 @@ GLOBAL_LIST_INIT(special_roles, list(
))
//Job defines for what happens when you fail to qualify for any job during job selection
-#define BEOVERFLOW 1
-#define BERANDOMJOB 2
-#define RETURNTOLOBBY 3
+#define BEOVERFLOW 1
+#define BERANDOMJOB 2
+#define RETURNTOLOBBY 3
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index cb8e7bc9a13b..2ba3c8850a0a 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -326,6 +326,7 @@
#define GENETIC_MUTATION "genetic"
#define OBESITY "obesity"
#define MAGIC_TRAIT "magic"
+#define DEVIL_TRAIT "devil"
#define TRAUMA_TRAIT "trauma"
#define DISEASE_TRAIT "disease"
#define SPECIES_TRAIT "species"
diff --git a/code/controllers/subsystem/infernal_affairs.dm b/code/controllers/subsystem/infernal_affairs.dm
new file mode 100644
index 000000000000..d8200dcb8833
--- /dev/null
+++ b/code/controllers/subsystem/infernal_affairs.dm
@@ -0,0 +1,64 @@
+/**
+ * ##infernal_affairs subsystem
+ *
+ * Supposed to handle the objectives of all Agents, ensuring they all have to kill eachother.
+ * Also keeps track of their Antag gear, to remove it when their soul is collected.
+ */
+SUBSYSTEM_DEF(infernal_affairs)
+ name = "Devil Affairs"
+ flags = SS_NO_INIT|SS_NO_FIRE
+
+ ///List of all devils in-game. There is supposed to have only one, so this is in-case admins do some wacky shit.
+ var/list/datum/antagonist/devil/devils = list()
+ ///List of all Agents in the loop and the gear they have.
+ var/list/datum/antagonist/infernal_affairs/agent_datums = list()
+
+/**
+ * Enters a for() loop for all agents while assigning their target to be the first available agent.
+ *
+ * We assign all IAAs their position in the list to later assign them as objectives of one another.
+ * Lists starts at 1, so we will immediately imcrement to get their target.
+ * When the list goes over, we go back to the start AFTER incrementing the list, so they will have the first player as a target.
+ * We skip over Hellbound people, and when there's only one left alive, we'll end the loop.
+ */
+/datum/controller/subsystem/infernal_affairs/proc/update_objective_datums()
+ if(!agent_datums.len)
+ return
+ var/list_position = 1
+ for(var/datum/antagonist/infernal_affairs/agents as anything in agent_datums)
+ if(!agents.active_objective)
+ agents.active_objective = new(src)
+ var/objective_set = FALSE
+ while(!objective_set)
+ list_position++
+ if(list_position > agent_datums.len)
+ list_position = initial(list_position)
+ var/datum/antagonist/infernal_affairs/next_agent = agent_datums[list_position]
+ if(HAS_TRAIT(next_agent.owner, TRAIT_HELLBOUND))
+ continue
+ if(next_agent == agents)
+ end_loop(agents)
+ objective_set = TRUE
+ break
+ if(agents.active_objective.target != agent_datums[list_position])
+ agents.active_objective.target = agent_datums[list_position]
+ agents.active_objective.update_explanation_text()
+ agents.update_static_data(agents.owner.current)
+ objective_set = TRUE
+ break
+ return TRUE
+
+/**
+ * ## end_loop
+ *
+ * We unregister signal to stop listening for people in the loop, as it's over.
+ * We will then give the last man standing their hijack objective, to end the subsystem off
+ * Args:
+ * - last_man_standing: The antag datum of the last remaining player left alive.
+ */
+/datum/controller/subsystem/infernal_affairs/proc/end_loop(datum/antagonist/infernal_affairs/last_one_standing)
+ var/datum/objective/hijack/hijack_objective = new()
+ hijack_objective.owner = last_one_standing.owner
+ hijack_objective.explanation_text = hijack_objective
+ last_one_standing.objectives += hijack_objective
+ last_one_standing.update_static_data(last_one_standing.owner.current)
diff --git a/code/datums/achievements/achievements.dm b/code/datums/achievements/achievements.dm
index c78c87ff3c06..7bd316c34d6e 100644
--- a/code/datums/achievements/achievements.dm
+++ b/code/datums/achievements/achievements.dm
@@ -249,6 +249,12 @@
desc = "As a revenant, complete your objectives"
id = GREENTEXT + 17
+/datum/achievement/greentext/devil
+ name = "Eternal Infernal"
+ desc = "As a devil, ascend to your greatest power"
+ id = GREENTEXT + 17
+
+
//end-greentext
//start-redtext
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index 4ed8b69605fe..6f03a9163a0d 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -346,32 +346,32 @@
if(!uplink_loc) // We've looked everywhere, let's just implant you
implant = TRUE
-
+
+ var/datum/component/uplink/uplink_component
+
if(!implant)
. = uplink_loc
- var/datum/component/uplink/U = uplink_loc.AddComponent(/datum/component/uplink, traitor_mob.key)
- if(!U)
+ uplink_component = uplink_loc.AddComponent(/datum/component/uplink, traitor_mob.key)
+ if(!uplink_component)
CRASH("Uplink creation failed.")
- U.setup_unlock_code()
+ uplink_component.setup_unlock_code()
if(!silent)
if(uplink_loc == R)
- to_chat(traitor_mob, "[employer] has cunningly disguised a Syndicate Uplink as your [R.name]. Simply dial the frequency [format_frequency(U.unlock_code)] to unlock its hidden features.")
+ to_chat(traitor_mob, "[employer] has cunningly disguised a Syndicate Uplink as your [R.name]. Simply dial the frequency [format_frequency(uplink_component.unlock_code)] to unlock its hidden features.")
else if(uplink_loc == PDA)
- to_chat(traitor_mob, "[employer] has cunningly disguised a Syndicate Uplink as your [PDA.name]. Simply enter the code \"[U.unlock_code]\" into the ringtone select to unlock its hidden features.")
+ to_chat(traitor_mob, "[employer] has cunningly disguised a Syndicate Uplink as your [PDA.name]. Simply enter the code \"[uplink_component.unlock_code]\" into the ringtone select to unlock its hidden features.")
else if(uplink_loc == P)
- to_chat(traitor_mob, "[employer] has cunningly disguised a Syndicate Uplink as your [P.name]. Simply twist the top of the pen [english_list(U.unlock_code)] from its starting position to unlock its hidden features.")
+ to_chat(traitor_mob, "[employer] has cunningly disguised a Syndicate Uplink as your [P.name]. Simply twist the top of the pen [english_list(uplink_component.unlock_code)] from its starting position to unlock its hidden features.")
if(uplink_owner)
- uplink_owner.antag_memory += U.unlock_note + "
"
+ uplink_owner.antag_memory += uplink_component.unlock_note + "
"
else
- traitor_mob.mind.store_memory(U.unlock_note)
+ traitor_mob.mind.store_memory(uplink_component.unlock_note)
else
- var/obj/item/implant/uplink/starting/I = new(traitor_mob)
- I.implant(traitor_mob, null, silent = TRUE)
+ var/obj/item/implant/uplink/starting/I = new()
+ uplink_component = I.implant(traitor_mob, null, silent = TRUE)
if(!silent)
to_chat(traitor_mob, "[employer] has cunningly implanted you with a Syndicate Uplink (although uplink implants cost valuable TC, so you will have slightly less). Simply trigger the uplink to access it.")
- return I
-
-
+ return uplink_component
//Link a new mobs mind to the creator of said mob. They will join any team they are currently on, and will only switch teams when their creator does.
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
index 8c0e6ff1c1f8..81e870cc473c 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
@@ -219,6 +219,51 @@
M.add_antag_datum(new antag_datum())
return TRUE
+//////////////////////////////////////////////
+// //
+// INFERNAL AFFAIRS //
+// //
+//////////////////////////////////////////////
+
+/datum/dynamic_ruleset/roundstart/infernal_affairs
+ name = "Devil Affairs"
+ antag_flag = ROLE_INFERNAL_AFFAIRS
+ antag_datum = /datum/antagonist/infernal_affairs
+ protected_roles = list("Chaplain","Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Research Director", "Chief Engineer", "Chief Medical Officer", "Brig Physician")
+ restricted_roles = list("AI", "Cyborg")
+ required_candidates = 6
+ weight = 0
+ cost = 8
+ scaling_cost = 2
+ requirements = list(8,8,8,8,8,8,8,8,8,8)
+ antag_cap = list("denominator" = 24, "offset" = 3)
+
+/datum/dynamic_ruleset/roundstart/infernal_affairs/pre_execute(population)
+ . = ..()
+ var/num_traitors= get_antag_cap(population)
+ for(var/affair_number = 1 to num_traitors)
+ if(candidates.len <= 0)
+ break
+ var/mob/M = pick_n_take(candidates)
+ if(!SSinfernal_affairs.devils.len)
+ var/datum/antagonist/devil/devil_agent = new()
+ M.mind.add_antag_datum(devil_agent)
+ M.mind.special_role = ROLE_INFERNAL_AFFAIRS_DEVIL
+ else
+ assigned += M.mind
+ M.mind.special_role = ROLE_INFERNAL_AFFAIRS
+ M.mind.restricted_roles = restricted_roles
+ return TRUE
+
+/datum/dynamic_ruleset/roundstart/infernal_affairs/execute()
+ . = ..()
+ if(!.)
+ return FALSE
+
+ SSinfernal_affairs.update_objective_datums()
+
+ return TRUE
+
//////////////////////////////////////////////
// //
// BLOOD CULT //
diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm
index 7213efde1add..ac6551e1ca39 100644
--- a/code/game/gamemodes/objective.dm
+++ b/code/game/gamemodes/objective.dm
@@ -233,6 +233,20 @@ GLOBAL_LIST_EMPTY(objectives)
/datum/objective/assassinate/admin_edit(mob/admin)
admin_simple_target_pick(admin)
+/datum/objective/assassinate/internal
+ name = "assassinate internal"
+
+//We do not find a target, we'll be set manually in the game.
+/datum/objective/assassinate/internal/find_target_by_role(role, role_type = FALSE, invert = FALSE)
+ return
+
+/datum/objective/assassinate/internal/update_explanation_text()
+ . = ..()
+ if(target && target.current)
+ explanation_text = "Assassinate [target.name], the [!target_role_type ? target.assigned_role : target.special_role]."
+ else
+ explanation_text = "Turn in the corpse of [target.name], who has been obliterated, to the Devil."
+
/datum/objective/assassinate/once
name = "assassinate revival allowed"
diff --git a/code/game/objects/items/implants/implantuplink.dm b/code/game/objects/items/implants/implantuplink.dm
index 113a1eb191a7..f0b6a2a69ce4 100644
--- a/code/game/objects/items/implants/implantuplink.dm
+++ b/code/game/objects/items/implants/implantuplink.dm
@@ -7,11 +7,16 @@
righthand_file = 'icons/mob/inhands/misc/devices_righthand.dmi'
var/starting_tc = 0
-/obj/item/implant/uplink/Initialize(mapload, _owner)
+/obj/item/implant/uplink/Initialize(mapload)
. = ..()
- AddComponent(/datum/component/uplink, _owner, TRUE, FALSE, null, starting_tc)
RegisterSignal(src, COMSIG_COMPONENT_REMOVING, PROC_REF(_component_removal))
+/obj/item/implant/uplink/implant(mob/living/target, mob/user, silent = FALSE, force = FALSE)
+ . = ..()
+ if(!.)
+ return FALSE
+ return AddComponent(/datum/component/uplink, target, TRUE, FALSE, null, starting_tc)
+
/**
* Proc called when component is removed; ie. uplink component
*
diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm
index ecadf6fb6425..0d5448371ddd 100644
--- a/code/modules/admin/sql_ban_system.dm
+++ b/code/modules/admin/sql_ban_system.dm
@@ -295,7 +295,7 @@
"Ghost and Other Roles" = list(ROLE_BRAINWASHED, ROLE_DEATHSQUAD, ROLE_DRONE, ROLE_FUGITIVE, ROLE_HOLOPARASITE, ROLE_HORROR, ROLE_LAVALAND, ROLE_MIND_TRANSFER, ROLE_POSIBRAIN, ROLE_SENTIENCE, ROLE_MOUSE, ROLE_GOLEM, ROLE_GHOSTBEACON),
"Antagonist Positions" = list(ROLE_ABDUCTOR, ROLE_ALIEN, ROLE_BLOB,
ROLE_BLOODSUCKER, ROLE_BROTHER, ROLE_CHANGELING, ROLE_CULTIST,
- ROLE_FUGITIVE, ROLE_HOLOPARASITE, ROLE_MALF,
+ ROLE_FUGITIVE, ROLE_HOLOPARASITE, ROLE_INFERNAL_AFFAIRS, ROLE_MALF,
ROLE_MONKEY, ROLE_MONSTERHUNTER, ROLE_NINJA, ROLE_OPERATIVE,
ROLE_REV, ROLE_REVENANT, ROLE_SINFULDEMON,
ROLE_REV_HEAD, ROLE_SERVANT_OF_RATVAR, ROLE_SYNDICATE,
diff --git a/code/modules/antagonists/devil_affairs/devil.dm b/code/modules/antagonists/devil_affairs/devil.dm
new file mode 100644
index 000000000000..b43f3e556d1c
--- /dev/null
+++ b/code/modules/antagonists/devil_affairs/devil.dm
@@ -0,0 +1,95 @@
+/datum/antagonist/devil
+ name = "Devil"
+ roundend_category = "infernal affairs agents"
+ antagpanel_category = "Devil Affairs"
+ job_rank = ROLE_INFERNAL_AFFAIRS_DEVIL
+ greentext_achieve = /datum/achievement/greentext/devil
+
+ ///The amount of souls the devil has so far.
+ var/souls = 0
+ ///List of Powers we currently have unlocked.
+ var/list/datum/action/devil_powers = list()
+
+/datum/antagonist/devil/on_gain()
+ . = ..()
+ SSinfernal_affairs.devils += src
+ obtain_power(/datum/action/cooldown/spell/pointed/summon_contract)
+ obtain_power(/datum/action/cooldown/spell/pointed/collect_soul)
+
+/datum/antagonist/devil/on_removal()
+ clear_power(/datum/action/cooldown/spell/pointed/summon_contract)
+ clear_power(/datum/action/cooldown/spell/pointed/collect_soul)
+ SSinfernal_affairs.devils -= src
+ return ..()
+
+/**
+ * ##update_souls_owned
+ *
+ * Used to edit the amount of souls a Devil has. This can be a negative number to take away.
+ * Will give Powers when getting to the proper level, or attempts to take them away if they go under
+ * That way this works for both adding and removing.
+ */
+/datum/antagonist/devil/proc/update_souls_owned(souls_adding)
+ souls += souls_adding
+
+ switch(souls)
+ if(0)
+ clear_power(/datum/action/devil_transfer_body)
+ if(1)
+ obtain_power(/datum/action/devil_transfer_body)
+ clear_power(/datum/action/cooldown/spell/conjure_item/violin)
+ if(2)
+ obtain_power(/datum/action/cooldown/spell/conjure_item/violin)
+ clear_power(/datum/action/cooldown/spell/conjure_item/summon_pitchfork)
+ if(3)
+ obtain_power(/datum/action/cooldown/spell/conjure_item/summon_pitchfork)
+ clear_power(/datum/action/cooldown/spell/jaunt/infernal_jaunt)
+ if(4)
+ obtain_power(/datum/action/cooldown/spell/jaunt/infernal_jaunt)
+ if(7)
+ clear_power(/datum/action/cooldown/spell/summon_dancefloor)
+ clear_power(/datum/action/cooldown/spell/pointed/projectile/fireball/hellish)
+ clear_power(/datum/action/cooldown/spell/shapeshift/devil)
+ if(8)
+ obtain_power(/datum/action/cooldown/spell/summon_dancefloor)
+ obtain_power(/datum/action/cooldown/spell/pointed/projectile/fireball/hellish)
+ obtain_power(/datum/action/cooldown/spell/shapeshift/devil)
+
+/datum/antagonist/devil/proc/obtain_power(datum/action/new_power)
+ new_power = new new_power
+ devil_powers[new_power.type] = new_power
+ new_power.Grant(owner.current)
+ return TRUE
+
+///Called when a Bloodsucker loses a power: (power)
+/datum/antagonist/devil/proc/clear_power(datum/action/removed_power)
+ if(devil_powers[removed_power])
+ QDEL_NULL(devil_powers[removed_power])
+
+/datum/action/cooldown/spell/pointed/summon_contract
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "ab_goldborder"
+
+/datum/action/devil_transfer_body
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "ab_goldborder"
+
+/datum/action/cooldown/spell/conjure_item/summon_pitchfork
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "ab_goldborder"
+
+/datum/action/cooldown/spell/jaunt/infernal_jaunt
+ name = "Infernal Jaunt"
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "ab_goldborder"
+
+/datum/action/cooldown/spell/conjure_item/violin
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "ab_goldborder"
+
+/**
+ * Ascended Powers
+ */
+/datum/action/cooldown/spell/pointed/projectile/fireball/hellish
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "ab_goldborder"
diff --git a/code/modules/antagonists/devil_affairs/devil_powers/collect_soul.dm b/code/modules/antagonists/devil_affairs/devil_powers/collect_soul.dm
new file mode 100644
index 000000000000..d70f790cd8de
--- /dev/null
+++ b/code/modules/antagonists/devil_affairs/devil_powers/collect_soul.dm
@@ -0,0 +1,59 @@
+/datum/action/cooldown/spell/pointed/collect_soul
+ name = "Collect Soul"
+ desc = "This ranged spell allows you to take the soul out of someone indebted to you.."
+// base_icon_state = "ignite"
+
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "ab_goldborder"
+
+ school = SCHOOL_TRANSMUTATION
+ invocation = "P'y y'ur de'ts"
+ invocation_type = INVOCATION_WHISPER
+
+ active_msg = span_notice("You prepare to collect a soul...")
+ sound = 'sound/magic/fireball.ogg'
+ cooldown_time = 1 MINUTES
+ spell_requirements = NONE
+
+/datum/action/cooldown/spell/pointed/collect_soul/is_valid_target(mob/living/target)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(!target || !istype(target) || !target.mind)
+ return FALSE
+ if(target.stat != DEAD)
+ target.balloon_alert(owner, "target has to be dead!")
+ return FALSE
+ var/datum/antagonist/devil/devil_datum = owner.mind.has_antag_datum(/datum/antagonist/infernal_affairs)
+ if(!devil_datum)
+ return FALSE
+ var/datum/antagonist/infernal_affairs/agent_datum = target.mind.has_antag_datum(/datum/antagonist/infernal_affairs)
+ if(!agent_datum)
+ target.balloon_alert(owner, "not an agent!")
+ return FALSE
+ var/obj/item/paper/calling_card/card = locate() in target.get_all_contents()
+ if(!card)
+ target.balloon_alert(owner, "no card found!")
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/spell/pointed/collect_soul/InterceptClickOn(mob/living/user, params, mob/living/target)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(!do_after(user, 10 SECONDS, target))
+ target.balloon_alert(user, "interrupted!")
+ return FALSE
+
+ var/obj/item/paper/calling_card/card = locate() in target.get_all_contents()
+ //safety
+ if(!card)
+ target.balloon_alert(owner, "no card found!")
+ return FALSE
+
+ target.balloon_alert(user, "soul ripped!")
+ var/datum/antagonist/infernal_affairs/agent_datum = target.mind.has_antag_datum(/datum/antagonist/infernal_affairs)
+ var/datum/antagonist/infernal_affairs/hunter_datum = card.signed_by_ref?.resolve()
+ agent_datum.soul_harvested(hunter_datum)
+ return TRUE
diff --git a/code/modules/antagonists/devil_affairs/devil_powers/dancefloor.dm b/code/modules/antagonists/devil_affairs/devil_powers/dancefloor.dm
new file mode 100644
index 000000000000..47176d277c02
--- /dev/null
+++ b/code/modules/antagonists/devil_affairs/devil_powers/dancefloor.dm
@@ -0,0 +1,53 @@
+/datum/action/cooldown/spell/summon_dancefloor
+ name = "Summon Dancefloor"
+ desc = "When what a Devil really needs is funk."
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "ab_goldborder"
+
+ spell_requirements = NONE
+ school = SCHOOL_EVOCATION
+ cooldown_time = 5 SECONDS //5 seconds, so the smoke can't be spammed
+
+ button_icon = 'icons/mob/actions/actions_minor_antag.dmi'
+ button_icon_state = "funk"
+
+ var/dancefloor_exists = FALSE
+ var/list/dancefloor_turfs
+ var/list/dancefloor_turfs_types
+ var/datum/effect_system/fluid_spread/smoke/transparent/dancefloor_devil/smoke
+
+/datum/action/cooldown/spell/summon_dancefloor/cast(atom/target)
+ . = ..()
+ LAZYINITLIST(dancefloor_turfs)
+ LAZYINITLIST(dancefloor_turfs_types)
+
+ if(!smoke)
+ smoke = new()
+ smoke.set_up(0, get_turf(owner))
+ smoke.start()
+
+ if(dancefloor_exists)
+ dancefloor_exists = FALSE
+ for(var/i in 1 to dancefloor_turfs.len)
+ var/turf/T = dancefloor_turfs[i]
+ T.ChangeTurf(dancefloor_turfs_types[i], flags = CHANGETURF_INHERIT_AIR)
+ else
+ var/list/funky_turfs = RANGE_TURFS(1, owner)
+ for(var/turf/closed/solid in funky_turfs)
+ to_chat(owner, "You're too close to a wall.")
+ return
+ dancefloor_exists = TRUE
+ var/i = 1
+ dancefloor_turfs.len = funky_turfs.len
+ dancefloor_turfs_types.len = funky_turfs.len
+ for(var/turf/T as anything in funky_turfs)
+ dancefloor_turfs[i] = T
+ dancefloor_turfs_types[i] = T.type
+ T.ChangeTurf((i % 2 == 0) ? /turf/open/floor/light/colour_cycle/dancefloor_a : /turf/open/floor/light/colour_cycle/dancefloor_b, flags = CHANGETURF_INHERIT_AIR)
+ i++
+
+/datum/effect_system/fluid_spread/smoke/transparent/dancefloor_devil
+ effect_type = /obj/effect/particle_effect/fluid/smoke/transparent/dancefloor_devil
+
+/obj/effect/particle_effect/fluid/smoke/transparent/dancefloor_devil
+ lifetime = 2
diff --git a/code/modules/antagonists/devil_affairs/devil_powers/devil_form.dm b/code/modules/antagonists/devil_affairs/devil_powers/devil_form.dm
new file mode 100644
index 000000000000..a8c5b901ca6b
--- /dev/null
+++ b/code/modules/antagonists/devil_affairs/devil_powers/devil_form.dm
@@ -0,0 +1,10 @@
+/datum/action/cooldown/spell/shapeshift/devil
+ name = "Devil Form"
+ desc = "Take on the true shape of a devil."
+ invocation = "P'ease't d'y fo' ' w'lk!"
+ invocation_type = INVOCATION_WHISPER
+ spell_requirements = NONE
+ background_icon_state = "bg_demon"
+ overlay_icon_state = "ab_goldborder"
+
+ possible_shapes = list(/mob/living/simple_animal/hostile/devil)
diff --git a/code/modules/antagonists/devil_affairs/infernal_agent.dm b/code/modules/antagonists/devil_affairs/infernal_agent.dm
new file mode 100644
index 000000000000..e29fc37a0bf2
--- /dev/null
+++ b/code/modules/antagonists/devil_affairs/infernal_agent.dm
@@ -0,0 +1,98 @@
+/datum/antagonist/infernal_affairs
+ name = "Infernal Affairs Agent"
+ roundend_category = "infernal affairs agents"
+ antagpanel_category = "Devil Affairs"
+ job_rank = ROLE_INFERNAL_AFFAIRS
+ preview_outfit = /datum/outfit/devil_affair_agent
+
+ ///Reference to the Uplink, so we can mess with it when required.
+ var/datum/component/uplink/uplink_holder
+ ///The active objective this agent has to currently complete.
+ var/datum/objective/assassinate/internal/active_objective
+
+ var/list/purchased_uplink_items = list()
+
+/datum/antagonist/infernal_affairs/on_gain(mob/living/mob_override)
+ . = ..()
+ SSinfernal_affairs.agent_datums += src
+ uplink_holder = owner.equip_traitor(employer = "The Devil", uplink_owner = src)
+ RegisterSignal(uplink_holder, COMSIG_ON_UPLINK_PURCHASE, PROC_REF(on_uplink_purchase))
+
+/datum/antagonist/infernal_affairs/on_removal()
+ . = ..()
+ SSinfernal_affairs.agent_datums -= src
+ UnregisterSignal(uplink_holder, COMSIG_ON_UPLINK_PURCHASE)
+ QDEL_NULL(uplink_holder)
+
+/**
+ * ## on_uplink_purchase
+ *
+ * Called when an uplink item is purchased.
+ * We will keep track of their items to destroy them when the Agent dies.
+ */
+/datum/antagonist/infernal_affairs/proc/on_uplink_purchase(datum/component/uplink/source, atom/purchased_item, mob/living/purchaser)
+ SIGNAL_HANDLER
+ if(!isitem(purchased_item))
+ return
+ purchased_uplink_items += purchased_item.get_all_contents()
+
+/**
+ * ##soul_harvested
+ *
+ * Handles making their mind unrevivable and the deletion of all their items,
+ * on top of all misc effects like updating objectives and giving rewards.
+ */
+/datum/antagonist/infernal_affairs/proc/soul_harvested(datum/antagonist/infernal_affairs/killer)
+ ADD_TRAIT(owner, TRAIT_HELLBOUND, DEVIL_TRAIT)
+ QDEL_LIST(purchased_uplink_items)
+
+ //grant the soul to ALL devils, though without admin intervention there should only be one.
+ for(var/datum/antagonist/devil/devil as anything in SSinfernal_affairs.devils)
+ if(devil.owner.current)
+ devil.update_souls_owned(1)
+
+ if(killer)
+ killer.uplink_holder.telecrystals += rand(3,5)
+
+ SSinfernal_affairs.update_objective_datums()
+
+/datum/outfit/devil_affair_agent
+ name = "Devil Affairs Agent (Preview only)"
+
+ uniform = /obj/item/clothing/under/rank/centcom_officer
+ head = /obj/item/clothing/head/devil_horns
+ glasses = /obj/item/clothing/glasses/sunglasses
+ r_hand = /obj/item/melee/transforming/energy/sword
+
+/datum/outfit/devil_affair_agent/post_equip(mob/living/carbon/human/owner, visualsOnly)
+ var/obj/item/melee/transforming/energy/sword/sword = locate() in owner.held_items
+ sword.transform_weapon(owner, TRUE)
+
+/obj/item/clothing/head/devil_horns
+ desc = "The one the only Devil."
+ icon_state = "devil_horns"
+
+/**
+ * ##calling card
+ *
+ * This item is required to be on someone to know if rewards should be given out.
+ */
+/obj/item/paper/calling_card
+ name = "calling card"
+ color = "#ff5050"
+ foldable = FALSE
+ info = {"**Death to Allentown.**
"}
+
+ ///A weakref to the antag datum who signed the paper, so simply holding a paper for YOUR target doesn't make you eligible.
+ var/datum/weakref/signed_by_ref
+
+/obj/item/paper/calling_card/Initialize(mapload, datum/antagonist/infernal_affairs/agent_datum)
+ . = ..()
+ if(!agent_datum)
+ stack_trace("calling card made but does not have an agent datum. This will cause errors as it is expected!")
+ return INITIALIZE_HINT_QDEL
+ signed_by_ref = WEAKREF(agent_datum)
+
+/obj/item/paper/calling_card/Destroy()
+ signed_by_ref = null
+ return ..()
diff --git a/code/modules/antagonists/devil_affairs/true_devil.dm b/code/modules/antagonists/devil_affairs/true_devil.dm
new file mode 100644
index 000000000000..c1636a6403c4
--- /dev/null
+++ b/code/modules/antagonists/devil_affairs/true_devil.dm
@@ -0,0 +1,76 @@
+/mob/living/simple_animal/hostile/devil
+ name = "True Devil"
+ desc = "A pile of infernal energy, taking a vaguely humanoid form."
+ icon = 'icons/mob/32x64.dmi'
+ icon_state = "true_devil"
+ gender = NEUTER
+
+ del_on_death = TRUE
+ dextrous = TRUE
+ spacewalk = TRUE
+ density = TRUE
+ pass_flags = NONE
+ health = 350
+ maxHealth = 350
+ ventcrawler = VENTCRAWLER_NONE
+ sight = (SEE_TURFS | SEE_OBJS)
+ status_flags = CANPUSH
+ mob_size = MOB_SIZE_LARGE
+ held_items = list(null, null)
+
+/mob/living/simple_animal/hostile/devil/Initialize(mapload)
+ grant_all_languages(TRUE, FALSE, TRUE)
+ return ..()
+
+/mob/living/simple_animal/hostile/devil/examine(mob/user)
+ . = list("This is [icon2html(src, user)] [src]!")
+
+ //Left hand items
+ for(var/obj/item/I in held_items)
+ if(!(I.item_flags & ABSTRACT))
+ . += "It is holding [I.get_examine_string(user)] in its [get_held_index_name(get_held_index_of_item(I))]."
+
+ //Braindead
+ if(!client && stat != DEAD)
+ . += "The devil seems to be in deep contemplation."
+
+ //Damaged
+ if(stat == DEAD)
+ . += span_deadsay("The hellfire seems to have been extinguished, for now at least.")
+ else if(health < (maxHealth/10))
+ . += span_warning("You can see hellfire inside its gaping wounds.")
+ else if(health < (maxHealth/2))
+ . += span_warning("You can see hellfire inside its wounds.")
+ . += ""
+
+/mob/living/simple_animal/hostile/devil/resist_buckle()
+ if(buckled)
+ buckled.user_unbuckle_mob(src,src)
+ visible_message(
+ span_warning("[src] easily breaks out of [p_their()] handcuffs!"),
+ span_notice("With just a thought your handcuffs fall off."),
+ )
+
+/mob/living/simple_animal/hostile/devil/assess_threat(judgement_criteria, lasercolor = "", datum/callback/weaponcheck=null)
+ return 666
+
+/mob/living/simple_animal/hostile/devil/get_ear_protection()
+ return 2
+
+/mob/living/simple_animal/hostile/devil/can_be_revived()
+ return 1
+
+/mob/living/simple_animal/hostile/devil/is_literate()
+ return TRUE
+
+/mob/living/simple_animal/hostile/devil/ex_act(severity, ex_target)
+ var/b_loss
+ switch(severity)
+ if (EXPLODE_DEVASTATE)
+ b_loss = 500
+ if (EXPLODE_HEAVY)
+ b_loss = 150
+ if (EXPLODE_LIGHT)
+ b_loss = 30
+ adjustBruteLoss(b_loss)
+ return ..()
diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm
index 0251a429a9f3..80aefaf43298 100644
--- a/code/modules/antagonists/traitor/datum_traitor.dm
+++ b/code/modules/antagonists/traitor/datum_traitor.dm
@@ -67,9 +67,7 @@
qdel(A.malf_picker)
owner.remove_employee(company)
if(uplink_holder)
- var/datum/component/uplink/uplink = uplink_holder.GetComponent(/datum/component/uplink)
- if(uplink)//remove uplink so they can't keep using it if admin abuse happens
- uplink.RemoveComponent()
+ uplink_holder.RemoveComponent()
UnregisterSignal(owner.current, COMSIG_MOVABLE_HEAR)
SSticker.mode.traitors -= owner
if(!silent && owner.current)
@@ -295,7 +293,7 @@
/datum/antagonist/traitor/proc/equip(silent = FALSE)
if(traitor_kind == TRAITOR_HUMAN)
- uplink_holder = owner.equip_traitor(employer, silent, src) //yogs - uplink_holder =
+ uplink_holder = owner.equip_traitor(employer, silent, src) //yogs - keeps track of their uplink.
/datum/antagonist/traitor/proc/assign_exchange_role()
//set faction
diff --git a/code/modules/mob/living/carbon/carbon_defines.dm b/code/modules/mob/living/carbon/carbon_defines.dm
index 7ba9e49c058a..ec19276c5649 100644
--- a/code/modules/mob/living/carbon/carbon_defines.dm
+++ b/code/modules/mob/living/carbon/carbon_defines.dm
@@ -7,7 +7,7 @@
held_items = list(null, null)
/// List of /obj/item/organ in the mob.
/// They don't go in the contents for some reason I don't want to know.
- var/list/internal_organs = list()
+ var/list/internal_organs = list()
/// List of /obj/item/organ in the mob by slot ID for easy access.
/// They don't go in the contents for some reason I don't want to know.
var/list/internal_organs_slot= list()
diff --git a/code/modules/paperwork/paper.dm b/code/modules/paperwork/paper.dm
index c88a58096077..864235d3a149 100644
--- a/code/modules/paperwork/paper.dm
+++ b/code/modules/paperwork/paper.dm
@@ -8,6 +8,7 @@
/datum/langtext // A datum to describe a piece of writing that stores a language value with it.
var/text = "" // The text that is written.
var/datum/language/lang // the language it's written in.
+
/datum/langtext/New(t,datum/language/l)
text = t
lang = l
@@ -46,7 +47,8 @@
var/contact_poison // Reagent ID to transfer on contact
var/contact_poison_volume = 0
var/next_write_time = 0 // prevent crash exploit
-
+ ///Whether the paper can be folded into a paper airplane.
+ var/foldable = TRUE
/obj/item/paper/pickup(user)
if(contact_poison && ishuman(user))
diff --git a/code/modules/paperwork/paperplane.dm b/code/modules/paperwork/paperplane.dm
index 97985c89f6ff..f3ed1bf721d0 100644
--- a/code/modules/paperwork/paperplane.dm
+++ b/code/modules/paperwork/paperplane.dm
@@ -121,12 +121,25 @@
/obj/item/paper/examine(mob/user)
. = ..()
- . += span_notice("Alt-click [src] to fold it into a paper plane.")
+ if(foldable)
+ . += span_notice("Alt-click [src] to fold it into a paper plane.")
/obj/item/paper/AltClick(mob/living/carbon/user, obj/item/I)
+ if(!foldable)
+ return ..()
+ var/datum/antagonist/infernal_affairs/affair_agent = IS_INFERNAL_AGENT(user)
+ if(affair_agent)
+ var/plane_response = tgui_input_list(user, "Do you wish for a plane a calling card?", "Your Calling", list("Calling Card", "Airplane"))
+ if(plane_response == "Calling Card")
+ user.balloon_alert(user, "folded paper.")
+ user.temporarilyRemoveItemFromInventory(src)
+ var/obj/item/paper/calling_card/new_card = new(user, affair_agent)
+ user.put_in_hands(new_card, no_sound = TRUE)
+ qdel(src)
+ return
if(!istype(user) || !user.canUseTopic(src, BE_CLOSE, ismonkey(user)))
return
- to_chat(user, span_notice("You fold [src] into the shape of a plane!"))
+ user.balloon_alert(user, "folded paper.")
user.temporarilyRemoveItemFromInventory(src)
var/obj/item/paperplane/plane_type = /obj/item/paperplane
//Origami Master
diff --git a/code/modules/uplink/uplink_items.dm b/code/modules/uplink/uplink_items.dm
index d90b6a4a74d6..e24809b355f9 100644
--- a/code/modules/uplink/uplink_items.dm
+++ b/code/modules/uplink/uplink_items.dm
@@ -126,12 +126,12 @@ GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item))
A = new spawn_path(get_turf(user))
else
A = spawn_path
- if(ishuman(user) && istype(A, /obj/item))
- var/mob/living/carbon/human/H = user
- if(H.put_in_hands(A))
- to_chat(H, "[A] materializes into your hands!")
- return A
- to_chat(user, "[A] materializes onto the floor.")
+ var/mob/living/carbon/human/H = user
+ if(istype(H) && isitem(A) && H.put_in_hands(A))
+ to_chat(H, "[A] materializes into your hands!")
+ else
+ to_chat(user, "[A] materializes onto the floor.")
+ SEND_SIGNAL(U, COMSIG_ON_UPLINK_PURCHASE, A, user)
return A
//Discounts (dynamically filled above)
diff --git a/icons/mob/clothing/head/head.dmi b/icons/mob/clothing/head/head.dmi
index 63de684a7a19..dc3c4b03f6ba 100644
Binary files a/icons/mob/clothing/head/head.dmi and b/icons/mob/clothing/head/head.dmi differ
diff --git a/icons/obj/clothing/hats.dmi b/icons/obj/clothing/hats.dmi
index 0f0e20e7be39..2d0b03d55c24 100644
Binary files a/icons/obj/clothing/hats.dmi and b/icons/obj/clothing/hats.dmi differ
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/infernal_affairs.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/infernal_affairs.ts
new file mode 100644
index 000000000000..ad06b9ecb619
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/antagonists/antagonists/infernal_affairs.ts
@@ -0,0 +1,19 @@
+import { Antagonist, Category } from '../base';
+import { multiline } from 'common/string';
+import { TRAITOR_MECHANICAL_DESCRIPTION } from './traitor';
+
+const InfernalAffairsAgent: Antagonist = {
+ key: 'infernalaffairsagent',
+ name: 'Infernal Affairs Agent',
+ description: [
+ multiline`
+ Tricked by the Devil into selling your soul,
+ you must collect his other debts to pay back yours.
+ `,
+
+ TRAITOR_MECHANICAL_DESCRIPTION,
+ ],
+ category: Category.Roundstart,
+};
+
+export default InfernalAffairsAgent;
diff --git a/yogstation.dme b/yogstation.dme
index 7ad82064c42c..651147963eb4 100644
--- a/yogstation.dme
+++ b/yogstation.dme
@@ -43,7 +43,6 @@
#include "code\__DEFINES\combat.dm"
#include "code\__DEFINES\configuration.dm"
#include "code\__DEFINES\construction.dm"
-#include "code\__DEFINES\contracts.dm"
#include "code\__DEFINES\cooldowns.dm"
#include "code\__DEFINES\cult.dm"
#include "code\__DEFINES\diseases.dm"
@@ -344,6 +343,7 @@
#include "code\controllers\subsystem\garbage.dm"
#include "code\controllers\subsystem\icon_smooth.dm"
#include "code\controllers\subsystem\idlenpcpool.dm"
+#include "code\controllers\subsystem\infernal_affairs.dm"
#include "code\controllers\subsystem\input.dm"
#include "code\controllers\subsystem\ipintel.dm"
#include "code\controllers\subsystem\job.dm"
@@ -1729,6 +1729,12 @@
#include "code\modules\antagonists\demon\sins\greed.dm"
#include "code\modules\antagonists\demon\sins\pride.dm"
#include "code\modules\antagonists\demon\sins\wrath.dm"
+#include "code\modules\antagonists\devil_affairs\devil.dm"
+#include "code\modules\antagonists\devil_affairs\infernal_agent.dm"
+#include "code\modules\antagonists\devil_affairs\true_devil.dm"
+#include "code\modules\antagonists\devil_affairs\devil_powers\collect_soul.dm"
+#include "code\modules\antagonists\devil_affairs\devil_powers\dancefloor.dm"
+#include "code\modules\antagonists\devil_affairs\devil_powers\devil_form.dm"
#include "code\modules\antagonists\disease\disease_abilities.dm"
#include "code\modules\antagonists\disease\disease_datum.dm"
#include "code\modules\antagonists\disease\disease_disease.dm"
diff --git a/yogstation/code/datums/components/uplink.dm b/yogstation/code/datums/components/uplink.dm
index 364565590be9..9c736e6b55f4 100644
--- a/yogstation/code/datums/components/uplink.dm
+++ b/yogstation/code/datums/components/uplink.dm
@@ -18,5 +18,5 @@
if(canBuy)
return ..()
- to_chat(user, span_warning("The Syndicate only permits [U.name][U.name[LAZYLEN(U.name)] != "s" ? "s" : ""] to specific agents. \
- Your mission does not require this equipment."))
+ to_chat(user, span_warning("The Syndicate only permits [U.name][U.name[LAZYLEN(U.name)] != "s" ? "s" : ""] \
+ to specific agents. Your mission does not require this equipment."))
diff --git a/yogstation/code/modules/antagonists/traitor/datum_traitor.dm b/yogstation/code/modules/antagonists/traitor/datum_traitor.dm
index 9de14d0e5974..ac02d1d00e28 100644
--- a/yogstation/code/modules/antagonists/traitor/datum_traitor.dm
+++ b/yogstation/code/modules/antagonists/traitor/datum_traitor.dm
@@ -1,2 +1,2 @@
/datum/antagonist/traitor
- var/obj/item/uplink_holder
\ No newline at end of file
+ var/datum/component/uplink/uplink_holder