diff --git a/code/modules/antagonists/_common/antag_spawner.dm b/code/modules/antagonists/_common/antag_spawner.dm
index 4b30be12f78..0f81b9ef305 100644
--- a/code/modules/antagonists/_common/antag_spawner.dm
+++ b/code/modules/antagonists/_common/antag_spawner.dm
@@ -18,58 +18,55 @@
desc = "A magic contract previously signed by an apprentice. In exchange for instruction in the magical arts, they are bound to answer your call for aid."
icon = 'icons/obj/wizard.dmi'
icon_state ="scroll2"
+ var/polling = FALSE
-/obj/item/antag_spawner/contract/attack_self(mob/user)
- user.set_machine(src)
- var/dat
- if(used)
- dat = "You have already summoned your apprentice. "
- else
- dat = "Contract of Apprenticeship: "
- dat += "Using this contract, you may summon an apprentice to aid you on your mission. "
- dat += "If you are unable to establish contact with your apprentice, you can feed the contract back to the spellbook to refund your points. "
- dat += "Which school of magic is your apprentice studying?: "
- dat += "Destruction "
- dat += "Your apprentice is skilled in offensive magic. They know Magic Missile and Fireball. "
- dat += "Bluespace Manipulation "
- dat += "Your apprentice is able to defy physics, melting through solid objects and travelling great distances in the blink of an eye. They know Teleport and Ethereal Jaunt. "
- dat += "Healing "
- dat += "Your apprentice is training to cast spells that will aid your survival. They know Forcewall and Charge and come with a Staff of Healing. "
- dat += "Robeless "
- dat += "Your apprentice is training to cast spells without their robes. They know Knock and Mindswap. "
- user << browse(dat, "window=radio")
- onclose(user, "radio")
- return
-
-/obj/item/antag_spawner/contract/Topic(href, href_list)
+/obj/item/antag_spawner/contract/can_interact(mob/user)
. = ..()
+ if(!.)
+ return FALSE
+ if(polling)
+ balloon_alert(user, "already calling an apprentice!")
+ return FALSE
- if(usr.stat != CONSCIOUS || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED))
+/obj/item/antag_spawner/contract/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "ApprenticeContract", name)
+ ui.open()
+
+/obj/item/antag_spawner/contract/ui_state(mob/user)
+ if(used)
+ return GLOB.never_state
+ return GLOB.default_state
+
+/obj/item/antag_spawner/contract/ui_assets(mob/user)
+ . = ..()
+ return list(
+ get_asset_datum(/datum/asset/simple/contracts),
+ )
+
+/obj/item/antag_spawner/contract/ui_act(action, list/params)
+ . = ..()
+ if(used || polling || !ishuman(usr))
return
- if(!ishuman(usr))
- return TRUE
- var/mob/living/carbon/human/H = usr
+ INVOKE_ASYNC(src, .proc/poll_for_student, usr, params["school"])
+ SStgui.close_uis(src)
- if(loc == H || (in_range(src, H) && isturf(loc)))
- H.set_machine(src)
- if(href_list["school"])
- if(used)
- to_chat(H, span_warning("You already used this contract!"))
- return
- var/list/candidates = poll_candidates_for_mob("Do you want to play as a wizard's [href_list["school"]] apprentice?", ROLE_WIZARD, ROLE_WIZARD, 15 SECONDS, src)
- if(LAZYLEN(candidates))
- if(QDELETED(src))
- return
- if(used)
- to_chat(H, span_warning("You already used this contract!"))
- return
- used = TRUE
- var/mob/dead/observer/C = pick(candidates)
- spawn_antag(C.client, get_turf(src), href_list["school"],H.mind)
- else
- to_chat(H, span_warning("Unable to reach your apprentice! You can either attack the spellbook with the contract to refund your points, or wait and try again later."))
+/obj/item/antag_spawner/contract/proc/poll_for_student(mob/living/carbon/human/teacher, apprentice_school)
+ balloon_alert(teacher, "contacting apprentice...")
+ polling = TRUE
+ var/list/candidates = poll_candidates_for_mob("Do you want to play as a wizard's [apprentice_school] apprentice?", ROLE_WIZARD, ROLE_WIZARD, 15 SECONDS, src)
+ polling = FALSE
+ if(!LAZYLEN(candidates))
+ to_chat(teacher, span_warning("Unable to reach your apprentice! You can either attack the spellbook with the contract to refund your points, or wait and try again later."))
+ return
+ if(QDELETED(src) || used)
+ return
+ used = TRUE
+ var/mob/dead/observer/student = pick(candidates)
+ spawn_antag(student.client, get_turf(src), apprentice_school, teacher.mind)
-/obj/item/antag_spawner/contract/spawn_antag(client/C, turf/T, kind ,datum/mind/user)
+/obj/item/antag_spawner/contract/spawn_antag(client/C, turf/T, kind, datum/mind/user)
new /obj/effect/particle_effect/smoke(T)
var/mob/living/carbon/human/M = new/mob/living/carbon/human(T)
C.prefs.safe_transfer_prefs_to(M, is_antag = TRUE)
diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm
index 001068f3a14..c72eabe3b16 100644
--- a/code/modules/asset_cache/asset_list_items.dm
+++ b/code/modules/asset_cache/asset_list_items.dm
@@ -500,6 +500,14 @@
"safe_dial.png" = 'icons/ui_icons/safe/safe_dial.png'
)
+/datum/asset/simple/contracts
+ assets = list(
+ "bluespace.png" = 'icons/ui_icons/contracts/bluespace.png',
+ "destruction.png" = 'icons/ui_icons/contracts/destruction.png',
+ "healing.png" = 'icons/ui_icons/contracts/healing.png',
+ "robeless.png" = 'icons/ui_icons/contracts/robeless.png',
+ )
+
/datum/asset/spritesheet/fish
name = "fish"
diff --git a/icons/UI_Icons/contracts/bluespace.png b/icons/UI_Icons/contracts/bluespace.png
new file mode 100644
index 00000000000..f62bde4c952
Binary files /dev/null and b/icons/UI_Icons/contracts/bluespace.png differ
diff --git a/icons/UI_Icons/contracts/destruction.png b/icons/UI_Icons/contracts/destruction.png
new file mode 100644
index 00000000000..ed101d8f25a
Binary files /dev/null and b/icons/UI_Icons/contracts/destruction.png differ
diff --git a/icons/UI_Icons/contracts/healing.png b/icons/UI_Icons/contracts/healing.png
new file mode 100644
index 00000000000..78d1a8d1a26
Binary files /dev/null and b/icons/UI_Icons/contracts/healing.png differ
diff --git a/icons/UI_Icons/contracts/robeless.png b/icons/UI_Icons/contracts/robeless.png
new file mode 100644
index 00000000000..8da57fb9bab
Binary files /dev/null and b/icons/UI_Icons/contracts/robeless.png differ
diff --git a/icons/ui_icons/contracts/bluespace.png b/icons/ui_icons/contracts/bluespace.png
new file mode 100644
index 00000000000..f62bde4c952
Binary files /dev/null and b/icons/ui_icons/contracts/bluespace.png differ
diff --git a/icons/ui_icons/contracts/destruction.png b/icons/ui_icons/contracts/destruction.png
new file mode 100644
index 00000000000..ed101d8f25a
Binary files /dev/null and b/icons/ui_icons/contracts/destruction.png differ
diff --git a/icons/ui_icons/contracts/healing.png b/icons/ui_icons/contracts/healing.png
new file mode 100644
index 00000000000..78d1a8d1a26
Binary files /dev/null and b/icons/ui_icons/contracts/healing.png differ
diff --git a/icons/ui_icons/contracts/robeless.png b/icons/ui_icons/contracts/robeless.png
new file mode 100644
index 00000000000..8da57fb9bab
Binary files /dev/null and b/icons/ui_icons/contracts/robeless.png differ
diff --git a/tgui/packages/tgui/interfaces/ApprenticeContract.tsx b/tgui/packages/tgui/interfaces/ApprenticeContract.tsx
new file mode 100644
index 00000000000..333b4bc1560
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ApprenticeContract.tsx
@@ -0,0 +1,112 @@
+import { multiline } from 'common/string';
+import { resolveAsset } from '../assets';
+import { useBackend } from '../backend';
+import { BlockQuote, Box, Button, Icon, Section, Stack } from '../components';
+import { Window } from '../layouts';
+
+export const ApprenticeContract = (props, context) => {
+ return (
+
+
+
+
+
+ If you cannot reach any of your apprentices today,
+ you can feed the contract back into your spellbook to refund it.
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const ApprenticeSelection = (props, context) => {
+ const { act } = useBackend(context);
+ const {
+ iconName,
+ fluffName,
+ schoolTitle,
+ assetName,
+ blurb,
+ } = props;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {fluffName}
+
+