diff --git a/code/ATMOSPHERICS/components/trinary_devices/mixer.dm b/code/ATMOSPHERICS/components/trinary_devices/mixer.dm
index be4bf969009..52845f1154d 100644
--- a/code/ATMOSPHERICS/components/trinary_devices/mixer.dm
+++ b/code/ATMOSPHERICS/components/trinary_devices/mixer.dm
@@ -152,7 +152,7 @@
return 1
/obj/machinery/atmospherics/trinary/mixer/attack_ghost(mob/user)
- ui_interact(user)
+ tgui_interact(user)
/obj/machinery/atmospherics/trinary/mixer/attack_hand(mob/user)
if(..())
@@ -163,62 +163,62 @@
return
add_fingerprint(user)
- ui_interact(user)
+ tgui_interact(user)
-/obj/machinery/atmospherics/trinary/mixer/ui_interact(mob/user, ui_key = "main", datum/nanoui/ui = null, force_open = 1, var/master_ui = null, var/datum/topic_state/state = GLOB.default_state)
+/obj/machinery/atmospherics/trinary/mixer/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_default_state)
user.set_machine(src)
- ui = SSnanoui.try_update_ui(user, src, ui_key, ui, force_open)
+ ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
if(!ui)
- ui = new(user, src, ui_key, "atmos_mixer.tmpl", name, 370, 165, state = state)
+ ui = new(user, src, ui_key, "AtmosMixer", name, 330, 165, master_ui, state)
ui.open()
-/obj/machinery/atmospherics/trinary/mixer/ui_data(mob/user)
- var/list/data = list()
- data["on"] = on
- data["pressure"] = round(target_pressure)
- data["max_pressure"] = round(MAX_OUTPUT_PRESSURE)
- data["node1_concentration"] = round(node1_concentration*100)
- data["node2_concentration"] = round(node2_concentration*100)
+/obj/machinery/atmospherics/trinary/mixer/tgui_data(mob/user)
+ var/list/data = list(
+ "on" = on,
+ "pressure" = round(target_pressure, 0.01),
+ "max_pressure" = MAX_OUTPUT_PRESSURE,
+ "node1_concentration" = round(node1_concentration * 100),
+ "node2_concentration" = round(node2_concentration * 100)
+ )
return data
-/obj/machinery/atmospherics/trinary/mixer/Topic(href,href_list)
+
+
+/obj/machinery/atmospherics/trinary/mixer/tgui_act(action, list/params)
if(..())
- return 1
+ return
- if(href_list["power"])
- on = !on
- investigate_log("was turned [on ? "on" : "off"] by [key_name(usr)]", "atmos")
- . = TRUE
- if(href_list["pressure"])
- var/pressure = href_list["pressure"]
- if(pressure == "max")
- pressure = MAX_OUTPUT_PRESSURE
- . = TRUE
- else if(pressure == "input")
- pressure = input("New output pressure (0-[MAX_OUTPUT_PRESSURE] kPa):", name, target_pressure) as num|null
- if(!isnull(pressure) && !..())
- . = TRUE
- else if(text2num(pressure) != null)
- pressure = text2num(pressure)
- . = TRUE
- if(.)
- target_pressure = clamp(pressure, 0, MAX_OUTPUT_PRESSURE)
- investigate_log("was set to [target_pressure] kPa by [key_name(usr)]", "atmos")
- if(href_list["node1"])
- var/value = text2num(href_list["node1"])
- node1_concentration = max(0, min(1, node1_concentration + value))
- node2_concentration = max(0, min(1, node2_concentration - value))
- investigate_log("was set to [node1_concentration] % on node 1 by [key_name(usr)]", "atmos")
- . = TRUE
- if(href_list["node2"])
- var/value = text2num(href_list["node2"])
- node2_concentration = max(0, min(1, node2_concentration + value))
- node1_concentration = max(0, min(1, node1_concentration - value))
- investigate_log("was set to [node2_concentration] % on node 2 by [key_name(usr)]", "atmos")
- . = TRUE
+ switch(action)
+ if("power")
+ toggle()
+ investigate_log("was turned [on ? "on" : "off"] by [key_name(usr)]", "atmos")
+ return TRUE
- update_icon()
- SSnanoui.update_uis(src)
+ if("set_node")
+ if(params["node_name"] == "Node 1")
+ node1_concentration = clamp(round(text2num(params["concentration"]), 0.01), 0, 1)
+ node2_concentration = round(1 - node1_concentration, 0.01)
+ investigate_log("was set to [node1_concentration] % on node 1 by [key_name(usr)]", "atmos")
+ return TRUE
+ else
+ node2_concentration = clamp(round(text2num(params["concentration"]), 0.01), 0, 1)
+ node1_concentration = round(1 - node2_concentration, 0.01)
+ investigate_log("was set to [node2_concentration] % on node 2 by [key_name(usr)]", "atmos")
+ return TRUE
+
+ if("max_pressure")
+ target_pressure = MAX_OUTPUT_PRESSURE
+ . = TRUE
+
+ if("min_pressure")
+ target_pressure = 0
+ . = TRUE
+
+ if("custom_pressure")
+ target_pressure = clamp(text2num(params["pressure"]), 0, MAX_OUTPUT_PRESSURE)
+ . = TRUE
+ if(.)
+ investigate_log("was set to [target_pressure] kPa by [key_name(usr)]", "atmos")
/obj/machinery/atmospherics/trinary/mixer/attackby(obj/item/W, mob/user, params)
if(istype(W, /obj/item/pen))
diff --git a/code/__DEFINES/martial_arts.dm b/code/__DEFINES/martial_arts.dm
new file mode 100644
index 00000000000..4eb139cc44a
--- /dev/null
+++ b/code/__DEFINES/martial_arts.dm
@@ -0,0 +1,16 @@
+#define MARTIAL_COMBO_FAIL 0 // If the combo failed
+#define MARTIAL_COMBO_CONTINUE 1 // If the combo should continue
+#define MARTIAL_COMBO_DONE 2 // If the combo is successful and done
+#define MARTIAL_COMBO_DONE_NO_CLEAR 3 // If the combo is successful and done but the others should have a chance to finish
+#define MARTIAL_COMBO_DONE_BASIC_HIT 4 // If the combo should do a basic hit after it's done
+#define MARTIAL_COMBO_DONE_CLEAR_COMBOS 5 // If the combo should do a basic hit after it's done
+
+#define MARTIAL_ARTS_CANNOT_USE -1
+
+#define MARTIAL_COMBO_STEP_HARM "Harm"
+#define MARTIAL_COMBO_STEP_DISARM "Disarm"
+#define MARTIAL_COMBO_STEP_GRAB "Grab"
+#define MARTIAL_COMBO_STEP_HELP "Help"
+
+// A check used for all act types. Such as disarm_act
+#define MARTIAL_ARTS_ACT_CHECK if((. = ..()) != FALSE) return .
diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm
index 719274857ec..7cd4cc7a918 100644
--- a/code/__DEFINES/mobs.dm
+++ b/code/__DEFINES/mobs.dm
@@ -208,6 +208,7 @@
#define isguardian(A) (istype((A), /mob/living/simple_animal/hostile/guardian))
#define isnymph(A) (istype((A), /mob/living/simple_animal/diona))
#define ishostile(A) (istype(A, /mob/living/simple_animal/hostile))
+#define isterrorspider(A) (istype((A), /mob/living/simple_animal/hostile/poison/terror_spider))
#define issilicon(A) (istype((A), /mob/living/silicon))
#define isAI(A) (istype((A), /mob/living/silicon/ai))
diff --git a/code/_globalvars/misc.dm b/code/_globalvars/misc.dm
index 3a65da81382..9c6fa81c1a2 100644
--- a/code/_globalvars/misc.dm
+++ b/code/_globalvars/misc.dm
@@ -92,6 +92,7 @@ GLOBAL_VAR(map_name) // Self explanatory
GLOBAL_DATUM_INIT(data_core, /datum/datacore, new) // Station datacore, manifest, etc
GLOBAL_VAR_INIT(panic_bunker_enabled, FALSE) // Is the panic bunker enabled
+GLOBAL_VAR_INIT(pending_server_update, FALSE)
//Database connections
//A connection is established on world creation. Ideally, the connection dies when the server restarts (After feedback logging.).
diff --git a/code/datums/cache/air_alarm.dm b/code/datums/cache/air_alarm.dm
index 2edc0792a34..fd9e529d70a 100644
--- a/code/datums/cache/air_alarm.dm
+++ b/code/datums/cache/air_alarm.dm
@@ -1,3 +1,5 @@
+#define AIR_ALARM_DATA_CACHE_DURATION 10 SECONDS
+
GLOBAL_DATUM_INIT(air_alarm_repository, /datum/repository/air_alarm, new())
/datum/repository/air_alarm/proc/air_alarm_data(var/list/monitored_alarms, var/refresh = 0, var/obj/machinery/alarm/passed_alarm)
@@ -8,7 +10,7 @@ GLOBAL_DATUM_INIT(air_alarm_repository, /datum/repository/air_alarm, new())
cache_entry = new/datum/cache_entry
cache_data = cache_entry
- if(!refresh)
+ if(!refresh && cache_entry.timestamp + AIR_ALARM_DATA_CACHE_DURATION > world.time)
return cache_entry.data
if(SSticker && SSticker.current_state < GAME_STATE_PLAYING && istype(passed_alarm)) // Generating the list for the first time as the game hasn't started - no need to run through the machines list everything every time
@@ -29,3 +31,5 @@ GLOBAL_DATUM_INIT(air_alarm_repository, /datum/repository/air_alarm, new())
/datum/repository/air_alarm/proc/update_cache(var/obj/machinery/alarm/alarm)
return air_alarm_data(refresh = 1, passed_alarm = alarm)
+
+#undef AIR_ALARM_DATA_CACHE_DURATION
diff --git a/code/datums/dog_fashion.dm b/code/datums/dog_fashion.dm
index 964fba34c9b..14705ae7375 100644
--- a/code/datums/dog_fashion.dm
+++ b/code/datums/dog_fashion.dm
@@ -204,3 +204,7 @@
D.mutations.Add(BREATHLESS)
D.atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
D.minbodytemp = 0
+
+/datum/dog_fashion/head/fried_vox_empty
+ name = "Colonel REAL_NAME"
+ desc = "Keep away from live vox."
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index 8fa5b909cda..53230d1f83d 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -35,6 +35,7 @@
var/list/restricted_roles = list()
var/list/spell_list = list() // Wizard mode & "Give Spell" badmin button.
+ var/datum/martial_art/martial_art
var/role_alt_title
@@ -1102,8 +1103,8 @@
special_role = null
to_chat(current,"Your infernal link has been severed! You are no longer a devil!")
RemoveSpell(/obj/effect/proc_holder/spell/targeted/infernal_jaunt)
- RemoveSpell(/obj/effect/proc_holder/spell/fireball/hellish)
- RemoveSpell(/obj/effect/proc_holder/spell/targeted/summon_contract)
+ RemoveSpell(/obj/effect/proc_holder/spell/targeted/click/fireball/hellish)
+ RemoveSpell(/obj/effect/proc_holder/spell/targeted/click/summon_contract)
RemoveSpell(/obj/effect/proc_holder/spell/targeted/conjure_item/pitchfork)
RemoveSpell(/obj/effect/proc_holder/spell/targeted/conjure_item/pitchfork/greater)
RemoveSpell(/obj/effect/proc_holder/spell/targeted/conjure_item/pitchfork/ascended)
diff --git a/code/datums/outfits/outfit_admin.dm b/code/datums/outfits/outfit_admin.dm
index 2b48d5da5f7..d5be5f50f35 100644
--- a/code/datums/outfits/outfit_admin.dm
+++ b/code/datums/outfits/outfit_admin.dm
@@ -242,7 +242,7 @@
/obj/item/organ/internal/cyberimp/eyes/shield,
/obj/item/organ/internal/cyberimp/eyes/hud/security,
/obj/item/organ/internal/cyberimp/eyes/xray,
- /obj/item/organ/internal/cyberimp/brain/anti_stun,
+ /obj/item/organ/internal/cyberimp/brain/anti_stun/hardened,
/obj/item/organ/internal/cyberimp/chest/nutriment/plus,
/obj/item/organ/internal/cyberimp/arm/combat/centcom
)
diff --git a/code/datums/spell.dm b/code/datums/spell.dm
index 94779ed67fb..abb9fdc37c9 100644
--- a/code/datums/spell.dm
+++ b/code/datums/spell.dm
@@ -24,6 +24,20 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell))
user.face_atom(A)
return FALSE
+/datum/click_intercept/proc_holder
+ var/obj/effect/proc_holder/spell
+
+/datum/click_intercept/proc_holder/New(client/C, obj/effect/proc_holder/spell_to_cast)
+ . = ..()
+ spell = spell_to_cast
+
+/datum/click_intercept/proc_holder/InterceptClickOn(user, params, atom/object)
+ spell.InterceptClickOn(user, params, object)
+
+/datum/click_intercept/proc_holder/quit()
+ spell.remove_ranged_ability(spell.ranged_ability_user)
+ return ..()
+
/obj/effect/proc_holder/proc/add_ranged_ability(mob/living/user, var/msg)
if(!user || !user.client)
return
@@ -32,7 +46,7 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell))
user.ranged_ability.remove_ranged_ability(user)
user.ranged_ability = src
ranged_ability_user = user
- user.client.click_intercept = user.ranged_ability
+ user.client.click_intercept = new /datum/click_intercept/proc_holder(user.client, user.ranged_ability)
add_mousepointer(user.client)
active = TRUE
if(msg)
@@ -48,15 +62,17 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell))
C.mouse_pointer_icon = initial(C.mouse_pointer_icon)
/obj/effect/proc_holder/proc/remove_ranged_ability(mob/living/user, var/msg)
- if(!user || !user.client || (user.ranged_ability && user.ranged_ability != src)) //To avoid removing the wrong ability
+ if(!user || (user.ranged_ability && user.ranged_ability != src)) //To avoid removing the wrong ability
return
user.ranged_ability = null
ranged_ability_user = null
- user.client.click_intercept = null
- remove_mousepointer(user.client)
active = FALSE
- if(msg)
- to_chat(user, msg)
+ if(user.client)
+ qdel(user.client.click_intercept)
+ user.client.click_intercept = null
+ remove_mousepointer(user.client)
+ if(msg)
+ to_chat(user, msg)
update_icon()
/obj/effect/proc_holder/spell
@@ -114,10 +130,14 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell))
var/sound = null //The sound the spell makes when it is cast
-/obj/effect/proc_holder/spell/proc/cast_check(skipcharge = 0, mob/living/user = usr) //checks if the spell can be cast based on its settings; skipcharge is used when an additional cast_check is called inside the spell
- if(((!user.mind) || !(src in user.mind.spell_list)) && !(src in user.mob_spell_list))
- to_chat(user, "You shouldn't have this spell! Something's wrong.")
- return 0
+/* Checks if the user can cast the spell
+ * @param charge_check If the proc should do the cooldown check
+ * @param start_recharge If the proc should set the cooldown
+ * @param user The caster of the spell
+*/
+/obj/effect/proc_holder/spell/proc/cast_check(charge_check = TRUE, start_recharge = TRUE, mob/living/user = usr) //checks if the spell can be cast based on its settings; skipcharge is used when an additional cast_check is called inside the spell
+ if(!can_cast(user, charge_check, TRUE))
+ return FALSE
if(ishuman(user))
var/mob/living/carbon/human/caster = user
@@ -126,49 +146,7 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell))
caster.reset_perspective(0)
return 0
- if(is_admin_level(user.z) && !centcom_cancast) //Certain spells are not allowed on the centcom zlevel
- return 0
-
- if(!skipcharge)
- switch(charge_type)
- if("recharge")
- if(charge_counter < charge_max)
- to_chat(user, still_recharging_msg)
- return 0
- if("charges")
- if(!charge_counter)
- to_chat(user, "[name] has no charges left.")
- return 0
-
- if(!ghost)
- if(user.stat && !stat_allowed)
- to_chat(user, "You can't cast this spell while incapacitated.")
- return 0
- if(ishuman(user) && (invocation_type == "whisper" || invocation_type == "shout") && user.is_muzzled())
- to_chat(user, "Mmmf mrrfff!")
- return 0
-
- var/obj/effect/proc_holder/spell/noclothes/clothes_spell = locate() in (user.mob_spell_list | (user.mind ? user.mind.spell_list : list()))
- if((ishuman(user) && clothes_req) && !istype(clothes_spell))//clothes check
- var/mob/living/carbon/human/H = user
- var/obj/item/clothing/robe = H.wear_suit
- var/obj/item/clothing/hat = H.head
- var/obj/item/clothing/shoes = H.shoes
- if(!robe || !hat || !shoes)
- to_chat(user, "Your outfit isn't complete, you should put on your robe and wizard hat, as well as sandals.")
- return 0
- if(!robe.magical || !hat.magical || !shoes.magical)
- to_chat(user, "Your outfit isn't magical enough, you should put on your robe and wizard hat, as well as your sandals.")
- return 0
- else if(!ishuman(user))
- if(clothes_req || human_req)
- to_chat(user, "This spell can only be cast by humans!")
- return 0
- if(nonabstract_req && (isbrain(user) || ispAI(user)))
- to_chat(user, "This spell can only be cast by physical beings!")
- return 0
-
- if(!skipcharge)
+ if(start_recharge)
switch(charge_type)
if("recharge")
charge_counter = 0 //doesn't start recharging until the targets selecting ends
@@ -442,6 +420,100 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell))
return
+/obj/effect/proc_holder/spell/targeted/click
+ var/click_radius = 1 // How big the radius around the clicked atom is to find a suitable target. -1 is only the selected atom is considered
+ var/selection_activated_message = "Click on a target to cast the spell."
+ var/selection_deactivated_message = "You choose to not cast this spell."
+ var/allowed_type = /mob/living // Which type the targets have to be
+ var/auto_target_single = TRUE // If the spell should auto select a target if only one is found
+
+/obj/effect/proc_holder/spell/targeted/click/Click()
+ var/mob/living/user = usr
+ if(!istype(user))
+ return
+
+ if(active)
+ remove_ranged_ability(user, selection_deactivated_message)
+ else
+ if(cast_check(TRUE, FALSE, user))
+ if(auto_target_single && attempt_auto_target(user))
+ return
+
+ add_ranged_ability(user, selection_activated_message)
+ else
+ to_chat(user, "[src] is not ready to be used yet.")
+
+/obj/effect/proc_holder/spell/targeted/click/proc/attempt_auto_target(mob/user)
+ var/atom/target
+ for(var/atom/A in view_or_range(range, user, selection_type))
+ if(valid_target(A, user))
+ if(target)
+ return FALSE // Two targets found. ABORT
+ target = A
+
+ if(target && cast_check(TRUE, TRUE, user)) // Singular target found. Cast it instantly
+ to_chat(user, "Only one target found. Casting [src] on [target]!")
+ perform(list(target), user = user)
+ return TRUE
+ return FALSE
+
+/obj/effect/proc_holder/spell/targeted/click/InterceptClickOn(mob/living/user, params, atom/A)
+ if(..() || !cast_check(TRUE, TRUE, user))
+ remove_ranged_ability(user)
+ revert_cast(user)
+ return TRUE
+
+ var/list/targets = list()
+ if(valid_target(A, user))
+ targets.Add(A)
+
+ if((!max_targets || max_targets > targets.len) && click_radius >= 0)
+ var/list/found_others = list()
+ for(var/atom/target in range(click_radius, A))
+ if(valid_target(target, user))
+ found_others |= target
+ if(!max_targets)
+ targets.Add(found_others)
+ else
+ if(max_targets <= found_others.len + targets.len)
+ targets.Add(found_others)
+ else
+ switch(random_target_priority) //Add in the rest
+ if(TARGET_RANDOM)
+ while(targets.len < max_targets && found_others.len) // Add the others
+ targets.Add(pick_n_take(found_others))
+ if(TARGET_CLOSEST)
+ var/list/distances = list()
+ for(var/target in found_others)
+ distances[target] = get_dist(user, target)
+ sortTim(distances, /proc/cmp_numeric_asc, TRUE) // Sort on distance
+ for(var/target in distances)
+ targets.Add(target)
+ if(targets.len >= max_targets)
+ break
+
+
+ if(!targets.len)
+ to_chat(user, "No suitable target found.")
+ revert_cast(user)
+ return FALSE
+
+ perform(targets, user = user)
+ remove_ranged_ability(user)
+ return TRUE
+
+/* Checks if a target is valid
+ * Should not include to_chats or other types of messages since this is used often on tons of targets.
+ * @param target The target to check
+ * @param user The user of the spell
+*/
+/obj/effect/proc_holder/spell/targeted/click/proc/valid_target(target, user)
+ return istype(target, allowed_type) && (include_user || target != user) && \
+ (target in view_or_range(range, user, selection_type))
+
+/obj/effect/proc_holder/spell/targeted/click/choose_targets(mob/living/user, atom/A) // Not used
+ return
+
/obj/effect/proc_holder/spell/aoe_turf/choose_targets(mob/user = usr)
var/list/targets = list()
@@ -475,30 +547,39 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell))
qdel(dummy)
return 1
-/obj/effect/proc_holder/spell/proc/can_cast(mob/user = usr)
+/obj/effect/proc_holder/spell/proc/can_cast(mob/user = usr, charge_check = TRUE, show_message = FALSE)
if(((!user.mind) || !(src in user.mind.spell_list)) && !(src in user.mob_spell_list))
+ if(show_message)
+ to_chat(user, "You shouldn't have this spell! Something's wrong.")
return 0
if(is_admin_level(user.z) && !centcom_cancast) //Certain spells are not allowed on the centcom zlevel
return 0
- switch(charge_type)
- if("recharge")
- if(charge_counter < charge_max)
- return 0
- if("charges")
- if(!charge_counter)
- return 0
-
- if(user.stat && !stat_allowed)
- return 0
+ if(charge_check)
+ switch(charge_type)
+ if("recharge")
+ if(charge_counter < charge_max)
+ if(show_message)
+ to_chat(user, still_recharging_msg)
+ return 0
+ if("charges")
+ if(!charge_counter)
+ if(show_message)
+ to_chat(user, "[name] has no charges left.")
+ return 0
+ if(!ghost)
+ if(user.stat && !stat_allowed)
+ if(show_message)
+ to_chat(user, "You can't cast this spell while incapacitated.")
+ return 0
+ if(ishuman(user) && (invocation_type == "whisper" || invocation_type == "shout") && user.is_muzzled())
+ if(show_message)
+ to_chat(user, "Mmmf mrrfff!")
+ return 0
if(ishuman(user))
var/mob/living/carbon/human/H = user
-
- if((invocation_type == "whisper" || invocation_type == "shout") && H.is_muzzled())
- return 0
-
var/clothcheck = locate(/obj/effect/proc_holder/spell/noclothes) in user.mob_spell_list
var/clothcheck2 = user.mind && (locate(/obj/effect/proc_holder/spell/noclothes) in user.mind.spell_list)
if(clothes_req && !clothcheck && !clothcheck2) //clothes check
@@ -506,12 +587,20 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell))
var/obj/item/clothing/hat = H.head
var/obj/item/clothing/shoes = H.shoes
if(!robe || !hat || !shoes)
+ if(show_message)
+ to_chat(user, "Your outfit isn't complete, you should put on your robe and wizard hat, as well as sandals.")
return 0
if(!robe.magical || !hat.magical || !shoes.magical)
+ if(show_message)
+ to_chat(user, "Your outfit isn't magical enough, you should put on your robe and wizard hat, as well as your sandals.")
return 0
else
if(clothes_req || human_req)
+ if(show_message)
+ to_chat(user, "This spell can only be cast by humans!")
return 0
if(nonabstract_req && (isbrain(user) || ispAI(user)))
+ if(show_message)
+ to_chat(user, "This spell can only be cast by physical beings!")
return 0
return 1
diff --git a/code/datums/spells/area_teleport.dm b/code/datums/spells/area_teleport.dm
index 0d24a984014..5986da3a412 100644
--- a/code/datums/spells/area_teleport.dm
+++ b/code/datums/spells/area_teleport.dm
@@ -11,7 +11,7 @@
/obj/effect/proc_holder/spell/targeted/area_teleport/perform(list/targets, recharge = 1, mob/living/user = usr)
var/thearea = before_cast(targets)
- if(!thearea || !cast_check(1))
+ if(!thearea || !cast_check(TRUE, FALSE, user))
revert_cast()
return
invocation(thearea)
diff --git a/code/datums/spells/chaplain.dm b/code/datums/spells/chaplain.dm
index bdb7bb3f551..f16a2a3fbac 100644
--- a/code/datums/spells/chaplain.dm
+++ b/code/datums/spells/chaplain.dm
@@ -1,25 +1,31 @@
-/obj/effect/proc_holder/spell/targeted/chaplain_bless
+/obj/effect/proc_holder/spell/targeted/click/chaplain_bless
name = "Bless"
desc = "Blesses a single person."
school = "transmutation"
charge_max = 60
- clothes_req = 0
+ clothes_req = FALSE
invocation = "none"
invocation_type = "none"
max_targets = 1
- include_user = 0
- humans_only = 1
-
+ include_user = FALSE
+ allowed_type = /mob/living/carbon/human
+ selection_activated_message = "You prepare a blessing. Click on a target to start blessing."
+ selection_deactivated_message = "The crew will be blessed another time."
range = 1
+ click_radius = -1 // Only precision clicking
cooldown_min = 20
action_icon_state = "shield"
+/obj/effect/proc_holder/spell/targeted/click/chaplain_bless/valid_target(mob/living/carbon/human/target, user)
+ if(!..())
+ return FALSE
-/obj/effect/proc_holder/spell/targeted/chaplain_bless/cast(list/targets, mob/living/user = usr, distanceoverride)
+ return target.mind && target.ckey && !target.stat
+/obj/effect/proc_holder/spell/targeted/click/chaplain_bless/cast(list/targets, mob/living/user = usr)
if(!istype(user))
to_chat(user, "Somehow, you are not a living mob. This should never happen. Report this bug.")
revert_cast()
@@ -35,32 +41,7 @@
revert_cast()
return
- var/mob/living/carbon/human/target = targets[range]
-
- if(!istype(target))
- to_chat(user, "No target.")
- revert_cast()
- return
-
- if(!(target in oview(range)) && !distanceoverride)//If they are not in overview after selection. Do note that !() is necessary for in to work because ! takes precedence over it.
- to_chat(user, "[target] is too far away!")
- revert_cast()
- return
-
- if(!target.mind)
- to_chat(user, "[target] appears to be catatonic. Your blessing would have no effect.")
- revert_cast()
- return
-
- if(!target.ckey)
- to_chat(user, "[target] appears to be too out of it to benefit from this.")
- revert_cast()
- return
-
- if(target.stat == DEAD)
- to_chat(user, "[target] is already dead. There is no point.")
- revert_cast()
- return
+ var/mob/living/carbon/human/target = targets[1]
spawn(0) // allows cast to complete even if recipient ignores the prompt
if(alert(target, "[user] wants to bless you, in the name of [user.p_their()] religion. Accept?", "Accept Blessing?", "Yes", "No") == "Yes") // prevents forced conversions
diff --git a/code/datums/spells/devil.dm b/code/datums/spells/devil.dm
index 1a1069481cf..f9e075bdf81 100644
--- a/code/datums/spells/devil.dm
+++ b/code/datums/spells/devil.dm
@@ -21,13 +21,18 @@
action_background_icon_state = "bg_demon"
-/obj/effect/proc_holder/spell/targeted/summon_contract
+/obj/effect/proc_holder/spell/targeted/click/summon_contract
name = "Summon infernal contract"
desc = "Skip making a contract by hand, just do it by magic."
invocation_type = "whisper"
invocation = "Just sign on the dotted line."
- include_user = 0
+ selection_activated_message = "You prepare a detailed contract. Click on a target to summon the contract in his hands."
+ selection_deactivated_message = "You archive the contract for later use."
+ include_user = FALSE
range = 5
+ auto_target_single = FALSE // Prevent an accidental contract from summoning
+ click_radius = -1 // Precision clicking required
+ allowed_type = /mob/living/carbon
clothes_req = FALSE
school = "conjuration"
charge_max = 150
@@ -35,8 +40,9 @@
action_icon_state = "spell_default"
action_background_icon_state = "bg_demon"
-/obj/effect/proc_holder/spell/targeted/summon_contract/cast(list/targets, mob/user = usr)
- for(var/mob/living/carbon/C in targets)
+/obj/effect/proc_holder/spell/targeted/click/summon_contract/cast(list/targets, mob/user = usr)
+ for(var/target in targets)
+ var/mob/living/carbon/C = target
if(C.mind && user.mind)
if(C.stat == DEAD)
if(user.drop_item())
@@ -63,7 +69,7 @@
to_chat(user,"[C] seems to not be sentient. You are unable to summon a contract for them.")
-/obj/effect/proc_holder/spell/fireball/hellish
+/obj/effect/proc_holder/spell/targeted/click/fireball/hellish
name = "Hellfire"
desc = "This spell launches hellfire at the target."
school = "evocation"
@@ -74,7 +80,7 @@
fireball_type = /obj/item/projectile/magic/fireball/infernal
action_background_icon_state = "bg_demon"
-/obj/effect/proc_holder/spell/fireball/hellish/cast(list/targets, mob/living/user = usr)
+/obj/effect/proc_holder/spell/targeted/click/fireball/hellish/cast(list/targets, mob/living/user = usr)
add_attack_logs(user, targets, "has fired a Hellfire ball", ATKLOG_FEW)
.=..()
diff --git a/code/datums/spells/horsemask.dm b/code/datums/spells/horsemask.dm
index 0140fbdf951..d5f686fb763 100644
--- a/code/datums/spells/horsemask.dm
+++ b/code/datums/spells/horsemask.dm
@@ -1,39 +1,31 @@
-/obj/effect/proc_holder/spell/targeted/horsemask
+/obj/effect/proc_holder/spell/targeted/click/horsemask
name = "Curse of the Horseman"
desc = "This spell triggers a curse on a target, causing them to wield an unremovable horse head mask. They will speak like a horse! Any masks they are wearing will be disintegrated. This spell does not require robes."
school = "transmutation"
charge_type = "recharge"
charge_max = 150
charge_counter = 0
- clothes_req = 0
- stat_allowed = 0
+ clothes_req = FALSE
+ stat_allowed = FALSE
invocation = "KN'A FTAGHU, PUCK 'BTHNK!"
invocation_type = "shout"
range = 7
cooldown_min = 30 //30 deciseconds reduction per rank
selection_type = "range"
+ selection_activated_message = "You start to quietly neigh an incantation. Click on or near a target to cast the spell."
+ selection_deactivated_message = "You stop neighing to yourself."
+ allowed_type = /mob/living/carbon/human
+
action_icon_state = "barn"
sound = 'sound/magic/HorseHead_curse.ogg'
-/obj/effect/proc_holder/spell/targeted/horsemask/cast(list/targets, mob/user = usr)
+/obj/effect/proc_holder/spell/targeted/click/horsemask/cast(list/targets, mob/user = usr)
if(!targets.len)
to_chat(user, "No target found in range.")
return
- var/mob/living/carbon/target = targets[1]
-
- if(!target)
- return
-
-
- if(!ishuman(target))
- to_chat(user, "It'd be stupid to curse [target] with a horse's head!")
- return
-
- if(!(target in oview(range)))//If they are not in overview after selection.
- to_chat(user, "They are too far away!")
- return
+ var/mob/living/carbon/human/target = targets[1]
var/obj/item/clothing/mask/horsehead/magichead = new /obj/item/clothing/mask/horsehead
magichead.flags |= NODROP | DROPDEL //curses!
diff --git a/code/datums/spells/lightning.dm b/code/datums/spells/lightning.dm
index ed6b7cd543e..20cd8dccdf5 100644
--- a/code/datums/spells/lightning.dm
+++ b/code/datums/spells/lightning.dm
@@ -25,10 +25,10 @@
/obj/effect/proc_holder/spell/targeted/lightning/Click()
if(!ready && start_time == 0)
- if(cast_check())
+ if(cast_check(TRUE, FALSE, usr))
StartChargeup()
else
- if(ready && cast_check(skipcharge=1))
+ if(ready && cast_check(TRUE, TRUE, usr))
choose_targets()
return 1
diff --git a/code/datums/spells/magnet.dm b/code/datums/spells/magnet.dm
index dfdac6f1b9c..155a3cb0b45 100644
--- a/code/datums/spells/magnet.dm
+++ b/code/datums/spells/magnet.dm
@@ -20,10 +20,10 @@
/obj/effect/proc_holder/spell/targeted/magnet/Click()
if(!ready && start_time == 0)
- if(cast_check())
+ if(cast_check(TRUE, FALSE, usr))
StartChargeup()
else
- if(ready && cast_check(skipcharge=1))
+ if(ready && cast_check(TRUE, TRUE, usr))
choose_targets()
return 1
diff --git a/code/datums/spells/mind_transfer.dm b/code/datums/spells/mind_transfer.dm
index 3d9031a8e03..e4924ee1c2f 100644
--- a/code/datums/spells/mind_transfer.dm
+++ b/code/datums/spells/mind_transfer.dm
@@ -1,4 +1,4 @@
-/obj/effect/proc_holder/spell/targeted/mind_transfer
+/obj/effect/proc_holder/spell/targeted/click/mind_transfer
name = "Mind Transfer"
desc = "This spell allows the user to switch bodies with a target."
@@ -8,33 +8,29 @@
invocation = "GIN'YU CAPAN"
invocation_type = "whisper"
range = 1
+ click_radius = 0 // Still gotta be pretty accurate
+ selection_activated_message = "You prepare to transfer your mind. Click on a target to cast the spell."
+ selection_deactivated_message = "You decide that your current form is good enough."
cooldown_min = 200 //100 deciseconds reduction per rank
var/list/protected_roles = list("Wizard","Changeling","Cultist") //which roles are immune to the spell
var/paralysis_amount_caster = 20 //how much the caster is paralysed for after the spell
var/paralysis_amount_victim = 20 //how much the victim is paralysed for after the spell
action_icon_state = "mindswap"
+/obj/effect/proc_holder/spell/targeted/click/mind_transfer/valid_target(mob/living/target, user)
+ if(!..())
+ return FALSE
+ return target.stat != DEAD && target.key && target.mind
+
/*
Urist: I don't feel like figuring out how you store object spells so I'm leaving this for you to do.
Make sure spells that are removed from spell_list are actually removed and deleted when mind transfering.
Also, you never added distance checking after target is selected. I've went ahead and did that.
*/
-/obj/effect/proc_holder/spell/targeted/mind_transfer/cast(list/targets, mob/user = usr, distanceoverride)
+/obj/effect/proc_holder/spell/targeted/click/mind_transfer/cast(list/targets, mob/user = usr)
var/mob/living/target = targets[range]
- if(!(target in oview(range)) && !distanceoverride)//If they are not in overview after selection. Do note that !() is necessary for in to work because ! takes precedence over it.
- to_chat(user, "They are too far away!")
- return
-
- if(target.stat == DEAD)
- to_chat(user, "You don't particularly want to be dead.")
- return
-
- if(!target.key || !target.mind)
- to_chat(user, "[target.p_they(TRUE)] appear[target.p_s()] to be catatonic. Not even magic can affect [target.p_their()] vacant mind.")
- return
-
if(user.suiciding)
to_chat(user, "You're killing yourself! You can't concentrate enough to do this!")
return
diff --git a/code/datums/spells/wizard.dm b/code/datums/spells/wizard.dm
index 94383f1b1e2..a99655222a4 100644
--- a/code/datums/spells/wizard.dm
+++ b/code/datums/spells/wizard.dm
@@ -308,76 +308,50 @@
duration = 300
sound = 'sound/magic/blind.ogg'
-/obj/effect/proc_holder/spell/fireball
+/obj/effect/proc_holder/spell/targeted/click/fireball
name = "Fireball"
desc = "This spell fires a fireball at a target and does not require wizard garb."
school = "evocation"
charge_max = 60
- clothes_req = 0
+ clothes_req = FALSE
invocation = "ONI SOMA"
invocation_type = "shout"
+ auto_target_single = FALSE // Having this true won't ever find a single target and is just lost processing power
range = 20
cooldown_min = 20 //10 deciseconds reduction per rank
+
+ click_radius = -1
+ selection_activated_message = "Your prepare to cast your fireball spell! Left-click to cast at a target!"
+ selection_deactivated_message = "You extinguish your fireball...for now."
+ allowed_type = /atom // FIRE AT EVERYTHING
+
var/fireball_type = /obj/item/projectile/magic/fireball
action_icon_state = "fireball0"
sound = 'sound/magic/fireball.ogg'
active = FALSE
-/obj/effect/proc_holder/spell/fireball/Click()
- var/mob/living/user = usr
- if(!istype(user))
- return
-
- var/msg
-
- if(!can_cast(user))
- msg = "You can no longer cast Fireball."
- remove_ranged_ability(user, msg)
- return
-
- if(active)
- msg = "You extinguish your fireball...for now."
- remove_ranged_ability(user, msg)
- else
- msg = "Your prepare to cast your fireball spell! Left-click to cast at a target!"
- add_ranged_ability(user, msg)
-
-/obj/effect/proc_holder/spell/fireball/update_icon()
+/obj/effect/proc_holder/spell/targeted/click/fireball/update_icon()
if(!action)
return
action.button_icon_state = "fireball[active]"
action.UpdateButtonIcon()
-/obj/effect/proc_holder/spell/fireball/InterceptClickOn(mob/living/user, params, atom/target)
- if(..())
- return FALSE
-
- if(!cast_check(0, user))
- remove_ranged_ability(user)
- return FALSE
-
- var/list/targets = list(target)
- perform(targets, user = user)
-
- return TRUE
-
-/obj/effect/proc_holder/spell/fireball/cast(list/targets, mob/living/user = usr)
+/obj/effect/proc_holder/spell/targeted/click/fireball/cast(list/targets, mob/living/user = usr)
var/target = targets[1] //There is only ever one target for fireball
var/turf/T = user.loc
var/turf/U = get_step(user, user.dir) // Get the tile infront of the move, based on their direction
if(!isturf(U) || !isturf(T))
- return 0
+ return FALSE
var/obj/item/projectile/magic/fireball/FB = new fireball_type(user.loc)
FB.current = get_turf(user)
FB.preparePixelProjectile(target, get_turf(target), user)
FB.fire()
user.newtonian_move(get_dir(U, T))
- remove_ranged_ability(user)
- return 1
+ return TRUE
/obj/effect/proc_holder/spell/aoe_turf/repulse
name = "Repulse"
diff --git a/code/datums/uplink_item.dm b/code/datums/uplink_item.dm
index 8a29d2cb0d0..42f74e234a0 100644
--- a/code/datums/uplink_item.dm
+++ b/code/datums/uplink_item.dm
@@ -1563,11 +1563,11 @@ GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item))
cost = 10
/datum/uplink_item/cyber_implants/antistun
- name = "CNS Rebooter Implant"
- desc = "This implant will help you get back up on your feet faster after being stunned. \
+ name = "Hardened CNS Rebooter Implant"
+ desc = "This implant will help you get back up on your feet faster after being stunned. It is invulnerable to EMPs. \
Comes with an automated implanting tool."
reference = "CIAS"
- item = /obj/item/organ/internal/cyberimp/brain/anti_stun
+ item = /obj/item/organ/internal/cyberimp/brain/anti_stun/hardened
cost = 12
/datum/uplink_item/cyber_implants/reviver
diff --git a/code/game/dna/genes/goon_powers.dm b/code/game/dna/genes/goon_powers.dm
index 87bbdf97c5c..77eb58e84b7 100644
--- a/code/game/dna/genes/goon_powers.dm
+++ b/code/game/dna/genes/goon_powers.dm
@@ -123,13 +123,13 @@
instability = GENE_INSTABILITY_MODERATE
mutation = CRYO
- spelltype = /obj/effect/proc_holder/spell/targeted/cryokinesis
+ spelltype = /obj/effect/proc_holder/spell/targeted/click/cryokinesis
/datum/dna/gene/basic/grant_spell/cryo/New()
..()
block = GLOB.cryoblock
-/obj/effect/proc_holder/spell/targeted/cryokinesis
+/obj/effect/proc_holder/spell/targeted/click/cryokinesis
name = "Cryokinesis"
desc = "Drops the bodytemperature of another person."
panel = "Abilities"
@@ -137,45 +137,44 @@
charge_type = "recharge"
charge_max = 1200
- clothes_req = 0
- stat_allowed = 0
+ clothes_req = FALSE
+ stat_allowed = FALSE
+
+ click_radius = 0
+ auto_target_single = FALSE // Give the clueless geneticists a way out and to have them not target themselves
+ selection_activated_message = "Your mind grow cold. Click on a target to cast the spell."
+ selection_deactivated_message = "Your mind returns to normal."
+ allowed_type = /mob/living/carbon
invocation_type = "none"
range = 7
selection_type = "range"
- include_user = 1
+ include_user = TRUE
var/list/compatible_mobs = list(/mob/living/carbon/human)
action_icon_state = "genetic_cryo"
-/obj/effect/proc_holder/spell/targeted/cryokinesis/cast(list/targets, mob/user = usr)
- if(!targets.len)
- to_chat(user, "No target found in range.")
- return
+/obj/effect/proc_holder/spell/targeted/click/cryokinesis/cast(list/targets, mob/user = usr)
var/mob/living/carbon/C = targets[1]
- if(!iscarbon(C))
- to_chat(user, "This will only work on normal organic beings.")
- return
-
if(COLDRES in C.mutations)
C.visible_message("A cloud of fine ice crystals engulfs [C.name], but disappears almost instantly!")
return
- var/handle_suit = 0
+ var/handle_suit = FALSE
if(ishuman(C))
var/mob/living/carbon/human/H = C
if(istype(H.head, /obj/item/clothing/head/helmet/space))
if(istype(H.wear_suit, /obj/item/clothing/suit/space))
- handle_suit = 1
+ handle_suit = TRUE
if(H.internal)
H.visible_message("[user] sprays a cloud of fine ice crystals, engulfing [H]!",
"[user] sprays a cloud of fine ice crystals over your [H.head]'s visor.")
- add_attack_logs(user, C, "Cryokinesis")
else
H.visible_message("[user] sprays a cloud of fine ice crystals engulfing, [H]!",
"[user] sprays a cloud of fine ice crystals cover your [H.head]'s visor and make it into your air vents!.")
- add_attack_logs(user, C, "Cryokinesis")
+
H.bodytemperature = max(0, H.bodytemperature - 100)
+ add_attack_logs(user, C, "Cryokinesis")
if(!handle_suit)
C.bodytemperature = max(0, C.bodytemperature - 200)
C.ExtinguishMob()
@@ -454,7 +453,7 @@
name = "Polymorphism"
desc = "Enables the subject to reconfigure their appearance to mimic that of others."
- spelltype =/obj/effect/proc_holder/spell/targeted/polymorph
+ spelltype =/obj/effect/proc_holder/spell/targeted/click/polymorph
//cooldown = 1800
activation_messages = list("You don't feel entirely like yourself somehow.")
deactivation_messages = list("You feel secure in your identity.")
@@ -465,34 +464,36 @@
..()
block = GLOB.polymorphblock
-/obj/effect/proc_holder/spell/targeted/polymorph
+/obj/effect/proc_holder/spell/targeted/click/polymorph
name = "Polymorph"
desc = "Mimic the appearance of others!"
panel = "Abilities"
charge_max = 1800
- clothes_req = 0
- human_req = 1
- stat_allowed = 0
+ clothes_req = FALSE
+ stat_allowed = FALSE
+
+ click_radius = -1 // Precision required
+ auto_target_single = FALSE // Safety to not turn into monkey (420)
+ selection_activated_message = "You body becomes unstable. Click on a target to cast transform into them."
+ selection_deactivated_message = "Your body calms down again."
+ allowed_type = /mob/living/carbon/human
+
invocation_type = "none"
range = 1
selection_type = "range"
action_icon_state = "genetic_poly"
-/obj/effect/proc_holder/spell/targeted/polymorph/cast(list/targets, mob/user = usr)
- var/mob/living/M = targets[1]
- if(!ishuman(M))
- to_chat(usr, "You can only change your appearance to that of another human.")
- return
+/obj/effect/proc_holder/spell/targeted/click/polymorph/cast(list/targets, mob/user = usr)
+ var/mob/living/carbon/human/target = targets[1]
user.visible_message("[user]'s body shifts and contorts.")
spawn(10)
- if(M && user)
+ if(target && user)
playsound(user.loc, 'sound/goonstation/effects/gib.ogg', 50, 1)
var/mob/living/carbon/human/H = user
- var/mob/living/carbon/human/target = M
H.UpdateAppearance(target.dna.UI)
H.real_name = target.real_name
H.name = target.name
diff --git a/code/game/dna/genes/vg_powers.dm b/code/game/dna/genes/vg_powers.dm
index 9092448e398..e08d35b681d 100644
--- a/code/game/dna/genes/vg_powers.dm
+++ b/code/game/dna/genes/vg_powers.dm
@@ -214,25 +214,21 @@
/obj/effect/proc_holder/spell/targeted/remotetalk/choose_targets(mob/user = usr)
var/list/targets = new /list()
- var/list/validtargets = new /list()
- var/turf/T = get_turf(user)
- for(var/mob/living/M in range(14, T))
- if(M && M.mind)
- if(M == user)
- continue
- validtargets += M
+ var/list/validtargets = user.get_telepathic_targets()
- if(!validtargets.len)
+ if(!length(validtargets))
to_chat(user, "There are no valid targets!")
start_recharge()
return
- targets += input("Choose the target to talk to.", "Targeting") as null|mob in validtargets
+ var/target_name = input("Choose the target to talk to.", "Targeting") as null|anything in validtargets
- if(!targets.len || !targets[1]) //doesn't waste the spell
+ var/mob/living/target
+ if(!target_name || !(target = validtargets[target_name]))
revert_cast(user)
return
+ targets += target
perform(targets, user = user)
/obj/effect/proc_holder/spell/targeted/remotetalk/cast(list/targets, mob/user = usr)
@@ -249,7 +245,7 @@
target.show_message("You hear [user.real_name]'s voice: [say]")
else
target.show_message("You hear a voice that seems to echo around the room: [say]")
- user.show_message("You project your mind into [target.name]: [say]")
+ user.show_message("You project your mind into [(target in user.get_visible_mobs()) ? target.name : "the unknown entity"]: [say]")
for(var/mob/dead/observer/G in GLOB.player_list)
G.show_message("Telepathic message from [user] ([ghost_follow_link(user, ghost=G)]) to [target] ([ghost_follow_link(target, ghost=G)]): [say]")
@@ -266,26 +262,22 @@
var/list/available_targets = list()
/obj/effect/proc_holder/spell/targeted/mindscan/choose_targets(mob/user = usr)
- var/list/targets = new /list()
- var/list/validtargets = new /list()
- var/turf/T = get_turf(user)
- for(var/mob/living/M in range(14, T))
- if(M && M.mind)
- if(M == user)
- continue
- validtargets += M
+ var/list/targets = list()
+ var/list/validtargets = user.get_telepathic_targets()
- if(!validtargets.len)
+ if(!length(validtargets))
to_chat(user, "There are no valid targets!")
start_recharge()
return
- targets += input("Choose the target to listen to.", "Targeting") as null|mob in validtargets
+ var/target_name = input("Choose the target to listen to.", "Targeting") as null|anything in validtargets
- if(!targets.len || !targets[1]) //doesn't waste the spell
+ var/mob/living/target
+ if(!target_name || !(target = validtargets[target_name]))
revert_cast(user)
return
+ targets += target
perform(targets, user = user)
/obj/effect/proc_holder/spell/targeted/mindscan/cast(list/targets, mob/user = usr)
@@ -295,7 +287,7 @@
var/message = "You feel your mind expand briefly... (Click to send a message.)"
if(REMOTE_TALK in target.mutations)
message = "You feel [user.real_name] request a response from you... (Click here to project mind.)"
- user.show_message("You offer your mind to [target.name].")
+ user.show_message("You offer your mind to [(target in user.get_visible_mobs()) ? target.name : "the unknown entity"].")
target.show_message("[message]")
available_targets += target
addtimer(CALLBACK(src, .proc/removeAvailability, target), 100)
diff --git a/code/game/gamemodes/devil/devilinfo.dm b/code/game/gamemodes/devil/devilinfo.dm
index ecbeb54f7f7..22a5f4a6cde 100644
--- a/code/game/gamemodes/devil/devilinfo.dm
+++ b/code/game/gamemodes/devil/devilinfo.dm
@@ -92,7 +92,7 @@ GLOBAL_LIST_INIT(lawlorify, list (
var/form = BASIC_DEVIL
var/exists = 0
var/static/list/dont_remove_spells = list(
- /obj/effect/proc_holder/spell/targeted/summon_contract,
+ /obj/effect/proc_holder/spell/targeted/click/summon_contract,
/obj/effect/proc_holder/spell/targeted/conjure_item/violin,
/obj/effect/proc_holder/spell/targeted/summon_dancefloor)
var/ascendable = FALSE
@@ -326,12 +326,12 @@ GLOBAL_LIST_INIT(lawlorify, list (
owner.RemoveSpell(S)
/datum/devilinfo/proc/give_summon_contract()
- owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/summon_contract(null))
+ owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/summon_contract(null))
/datum/devilinfo/proc/give_base_spells(give_summon_contract = 0)
remove_spells()
- owner.AddSpell(new /obj/effect/proc_holder/spell/fireball/hellish(null))
+ owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/fireball/hellish(null))
owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/conjure_item/pitchfork(null))
if(give_summon_contract)
give_summon_contract()
@@ -343,13 +343,13 @@ GLOBAL_LIST_INIT(lawlorify, list (
/datum/devilinfo/proc/give_lizard_spells()
remove_spells()
owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/conjure_item/pitchfork(null))
- owner.AddSpell(new /obj/effect/proc_holder/spell/fireball/hellish(null))
+ owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/fireball/hellish(null))
owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/infernal_jaunt(null))
/datum/devilinfo/proc/give_true_spells()
remove_spells()
owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/conjure_item/pitchfork/greater(null))
- owner.AddSpell(new /obj/effect/proc_holder/spell/fireball/hellish(null))
+ owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/fireball/hellish(null))
owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/infernal_jaunt(null))
owner.AddSpell(new /obj/effect/proc_holder/spell/targeted/sintouch(null))
diff --git a/code/game/gamemodes/miniantags/revenant/revenant_abilities.dm b/code/game/gamemodes/miniantags/revenant/revenant_abilities.dm
index 91ed583efc7..47e5e759c77 100644
--- a/code/game/gamemodes/miniantags/revenant/revenant_abilities.dm
+++ b/code/game/gamemodes/miniantags/revenant/revenant_abilities.dm
@@ -153,7 +153,7 @@
else
name = "[initial(name)] ([cast_amount]E)"
-/obj/effect/proc_holder/spell/aoe_turf/revenant/can_cast(mob/living/simple_animal/revenant/user = usr)
+/obj/effect/proc_holder/spell/aoe_turf/revenant/can_cast(mob/living/simple_animal/revenant/user = usr, charge_check = TRUE, show_message = FALSE)
if(user.inhibited)
return 0
if(charge_counter < charge_max)
diff --git a/code/game/gamemodes/shadowling/shadowling_abilities.dm b/code/game/gamemodes/shadowling/shadowling_abilities.dm
index 7692aa202b2..4be50a6cd59 100644
--- a/code/game/gamemodes/shadowling/shadowling_abilities.dm
+++ b/code/game/gamemodes/shadowling/shadowling_abilities.dm
@@ -18,80 +18,56 @@
return 0
-/obj/effect/proc_holder/spell/targeted/glare //Stuns and mutes a human target, depending on the distance relative to the shadowling
+/obj/effect/proc_holder/spell/targeted/click/glare //Stuns and mutes a human target, depending on the distance relative to the shadowling
name = "Glare"
desc = "Stuns and mutes a target for a decent duration. Duration depends on the proximity to the target."
panel = "Shadowling Abilities"
charge_max = 300
- clothes_req = 0
+ clothes_req = FALSE
range = 10 //has no effect beyond this range, so setting this makes invalid/useless targets not show up in popup
action_icon_state = "glare"
- humans_only = 1 //useless since we override chose_targets, but might be used for other code later??? Might remove, idk
-/obj/effect/proc_holder/spell/targeted/glare/choose_targets(mob/user)
- var/list/possible_targets = list()
- for(var/mob/living/carbon/human/target in view_or_range(range, user, "view"))
- if(target.stat)
- continue
- if(is_shadow_or_thrall(target))
- continue
- possible_targets += target
- var/mob/living/carbon/human/M
- var/list/targets = list()
- if(possible_targets.len == 1)//no choice involved
- targets = possible_targets
- else
- M = input("Choose the target for the spell.", "Targeting") as mob in possible_targets
- if(M in view_or_range(range, user, "view"))
- targets += M
+ selection_activated_message = "Your prepare to your eyes for a stunning glare! Left-click to cast at a target!"
+ selection_deactivated_message = "Your eyes relax... for now."
+ allowed_type = /mob/living/carbon/human
+/obj/effect/proc_holder/spell/targeted/click/glare/cast_check(charge_check = TRUE, start_recharge = TRUE, mob/living/user = usr)
+ if(!shadowling_check(user))
+ return FALSE
+ return ..()
- if(!targets.len) //doesn't waste the spell
- revert_cast(user)
+/obj/effect/proc_holder/spell/targeted/click/glare/valid_target(mob/living/carbon/human/target, user)
+ if(!..())
+ return FALSE
+ return !target.stat && !is_shadow_or_thrall(target)
+
+/obj/effect/proc_holder/spell/targeted/click/glare/cast(list/targets, mob/user = usr)
+ var/mob/living/carbon/human/H = targets[1]
+
+ user.visible_message("[user]'s eyes flash a blinding red!")
+ var/distance = get_dist(H, user)
+ if (distance <= 1) //Melee glare
+ H.visible_message("[H] freezes in place, [H.p_their()] eyes glazing over...", \
+ "Your gaze is forcibly drawn into [user]'s eyes, and you are mesmerized by [user.p_their()] heavenly beauty...")
+ H.Stun(10)
+ H.AdjustSilence(10)
+ else //Distant glare
+ var/loss = 10 - distance
+ var/duration = 10 - loss
+ if(loss <= 0)
+ to_chat(user, "Your glare had no effect over a such long distance!")
+ return
+ H.slowed = duration
+ H.AdjustSilence(10)
+ to_chat(H, "A red light flashes across your vision, and your mind tries to resist them.. you are exhausted.. you are not able to speak..")
+ addtimer(CALLBACK(src, .proc/do_stun, H, user, loss), duration SECONDS)
+
+/obj/effect/proc_holder/spell/targeted/click/glare/proc/do_stun(mob/living/carbon/human/target, user, stun_time)
+ if(!istype(target) || target.stat)
return
-
- perform(targets, user = user)
- return
-
-
-/obj/effect/proc_holder/spell/targeted/glare/cast(list/targets, mob/user = usr)
- for(var/mob/living/carbon/human/target in targets)
- if(!ishuman(target))
- to_chat(user, "You may only glare at humans!")
- charge_counter = charge_max
- return
- if(!shadowling_check(user))
- charge_counter = charge_max
- return
- if(target.stat)
- to_chat(user, "[target] must be conscious!")
- charge_counter = charge_max
- return
- if(is_shadow_or_thrall(target))
- to_chat(user, "You don't see why you would want to paralyze an ally.")
- charge_counter = charge_max
- return
- var/mob/living/carbon/human/M = target
- user.visible_message("[user]'s eyes flash a blinding red!")
- var/distance = get_dist(target, user)
- if (distance <= 1) //Melee glare
- target.visible_message("[target] freezes in place, [target.p_their()] eyes glazing over...")
- to_chat(target, "Your gaze is forcibly drawn into [user]'s eyes, and you are mesmerized by [user.p_their()] heavenly beauty...")
- target.Stun(10)
- M.AdjustSilence(10)
- else //Distant glare
- var/loss = 10 - distance
- var/duration = 10 - loss
- if(loss <= 0)
- to_chat(user, "Your glare had no effect over a such long distance!")
- return
- target.slowed = duration
- M.AdjustSilence(10)
- to_chat(target, "A red light flashes across your vision, and your mind tries to resist them.. you are exhausted.. you are not able to speak..")
- sleep(duration*10)
- target.Stun(loss)
- target.visible_message("[target] freezes in place, [target.p_their()] eyes glazing over...")
- to_chat(target, "Red lights suddenly dance in your vision, and you are mesmerized by the heavenly lights...")
+ target.Stun(stun_time)
+ target.visible_message("[target] freezes in place, [target.p_their()] eyes glazing over...",\
+ "Red lights suddenly dance in your vision, and you are mesmerized by the heavenly lights...")
/obj/effect/proc_holder/spell/aoe_turf/veil
name = "Veil"
@@ -228,96 +204,80 @@
M.reagents.add_reagent("frostoil", 15) //Half of a cryosting
-/obj/effect/proc_holder/spell/targeted/enthrall //Turns a target into the shadowling's slave. This overrides all previous loyalties
+/obj/effect/proc_holder/spell/targeted/click/enthrall //Turns a target into the shadowling's slave. This overrides all previous loyalties
name = "Enthrall"
desc = "Allows you to enslave a conscious, non-braindead, non-catatonic human to your will. This takes some time to cast."
panel = "Shadowling Abilities"
charge_max = 0
- clothes_req = 0
+ clothes_req = FALSE
range = 1 //Adjacent to user
- var/enthralling = 0
+ var/enthralling = FALSE
action_icon_state = "enthrall"
- humans_only = 1
-/obj/effect/proc_holder/spell/targeted/enthrall/cast(list/targets, mob/user = usr)
+ click_radius = -1 // Precision baby
+ selection_activated_message = "Your prepare your mind to entrall a mortal. Left-click to cast at a target!"
+ selection_deactivated_message = "Your mind relaxes."
+ allowed_type = /mob/living/carbon/human
+
+/obj/effect/proc_holder/spell/targeted/click/enthrall/can_cast(mob/user = usr, charge_check = TRUE, show_message = FALSE)
+ if(enthralling || !shadowling_check(user))
+ return FALSE
+ return ..()
+
+/obj/effect/proc_holder/spell/targeted/click/enthrall/valid_target(mob/living/carbon/human/target, user)
+ if(!..())
+ return FALSE
+ return target.key && target.mind && !target.stat && !is_shadow_or_thrall(target) && target.client
+
+/obj/effect/proc_holder/spell/targeted/click/enthrall/cast(list/targets, mob/user = usr)
var/mob/living/carbon/human/ling = user
listclearnulls(SSticker.mode.shadowling_thralls)
if(!(ling.mind in SSticker.mode.shadows))
return
- if(!isshadowling(ling))
- if(SSticker.mode.shadowling_thralls.len >= 5)
- charge_counter = charge_max
- return
- for(var/mob/living/carbon/human/target in targets)
- if(!in_range(user, target))
- to_chat(user, "You need to be closer to enthrall [target].")
- charge_counter = charge_max
- return
- if(!target.key || !target.mind)
- to_chat(user, "The target has no mind.")
- charge_counter = charge_max
- return
- if(target.stat)
- to_chat(user, "The target must be conscious.")
- charge_counter = charge_max
- return
- if(is_shadow_or_thrall(target))
- to_chat(user, "You can not enthrall allies.")
- charge_counter = charge_max
- return
- if(!ishuman(target))
- to_chat(user, "You can only enthrall humans.")
- charge_counter = charge_max
- return
- if(enthralling)
- to_chat(user, "You are already enthralling!")
- charge_counter = charge_max
- return
- if(!target.client)
- to_chat(user, "[target]'s mind is vacant of activity.")
- enthralling = 1
- to_chat(user, "This target is valid. You begin the enthralling.")
- to_chat(target, "[user] stares at you. You feel your head begin to pulse.")
+ var/mob/living/carbon/human/target = targets[1]
+ enthralling = TRUE
+ to_chat(user, "This target is valid. You begin the enthralling.")
+ to_chat(target, "[user] stares at you. You feel your head begin to pulse.")
- for(var/progress = 0, progress <= 3, progress++)
- switch(progress)
- if(1)
- to_chat(user, "You place your hands to [target]'s head...")
- user.visible_message("[user] places [user.p_their()] hands onto the sides of [target]'s head!")
- if(2)
- to_chat(user, "You begin preparing [target]'s mind as a blank slate...")
- user.visible_message("[user]'s palms flare a bright red against [target]'s temples!")
- to_chat(target, "A terrible red light floods your mind. You collapse as conscious thought is wiped away.")
- target.Weaken(12)
- sleep(20)
- if(ismindshielded(target))
- to_chat(user, "They have a mindshield implant. You begin to deactivate it - this will take some time.")
- user.visible_message("[user] pauses, then dips [user.p_their()] head in concentration!")
- to_chat(target, "Your mindshield implant becomes hot as it comes under attack!")
- sleep(100) //10 seconds - not spawn() so the enthralling takes longer
- to_chat(user, "The nanobots composing the mindshield implant have been rendered inert. Now to continue.")
- user.visible_message("[user] relaxes again.")
- for(var/obj/item/implant/mindshield/L in target)
- if(L && L.implanted)
- qdel(L)
- to_chat(target, "Your mental protection implant unexpectedly falters, dims, dies.")
- if(3)
- to_chat(user, "You begin planting the tumor that will control the new thrall...")
- user.visible_message("A strange energy passes from [user]'s hands into [target]'s head!")
- to_chat(target, "You feel your memories twisting, morphing. A sense of horror dominates your mind.")
- if(!do_mob(user, target, 70)) //around 21 seconds total for enthralling, 31 for someone with a mindshield implant
- to_chat(user, "The enthralling has been interrupted - your target's mind returns to its previous state.")
- to_chat(target, "You wrest yourself away from [user]'s hands and compose yourself")
- enthralling = 0
- return
+ for(var/progress = 0, progress <= 3, progress++)
+ switch(progress)
+ if(1)
+ to_chat(user, "You place your hands to [target]'s head...")
+ user.visible_message("[user] places [user.p_their()] hands onto the sides of [target]'s head!")
+ if(2)
+ to_chat(user, "You begin preparing [target]'s mind as a blank slate...")
+ user.visible_message("[user]'s palms flare a bright red against [target]'s temples!")
+ to_chat(target, "A terrible red light floods your mind. You collapse as conscious thought is wiped away.")
+ target.Weaken(12)
+ sleep(20)
+ if(ismindshielded(target))
+ to_chat(user, "They have a mindshield implant. You begin to deactivate it - this will take some time.")
+ user.visible_message("[user] pauses, then dips [user.p_their()] head in concentration!")
+ to_chat(target, "Your mindshield implant becomes hot as it comes under attack!")
+ sleep(100) //10 seconds - not spawn() so the enthralling takes longer
+ to_chat(user, "The nanobots composing the mindshield implant have been rendered inert. Now to continue.")
+ user.visible_message("[user] relaxes again.")
+ for(var/obj/item/implant/mindshield/L in target)
+ if(L && L.implanted)
+ qdel(L)
+ to_chat(target, "Your mental protection implant unexpectedly falters, dims, dies.")
+ if(3)
+ to_chat(user, "You begin planting the tumor that will control the new thrall...")
+ user.visible_message("A strange energy passes from [user]'s hands into [target]'s head!")
+ to_chat(target, "You feel your memories twisting, morphing. A sense of horror dominates your mind.")
+ if(!do_mob(user, target, 70)) //around 21 seconds total for enthralling, 31 for someone with a mindshield implant
+ to_chat(user, "The enthralling has been interrupted - your target's mind returns to its previous state.")
+ to_chat(target, "You wrest yourself away from [user]'s hands and compose yourself")
+ enthralling = FALSE
+ return
- enthralling = 0
- to_chat(user, "You have enthralled [target]!")
- target.visible_message("[target] looks to have experienced a revelation!", \
- "False faces all dark not real not real not--")
- target.setOxyLoss(0) //In case the shadowling was choking them out
- SSticker.mode.add_thrall(target.mind)
- target.mind.special_role = SPECIAL_ROLE_SHADOWLING_THRALL
+ enthralling = FALSE
+ to_chat(user, "You have enthralled [target]!")
+ target.visible_message("[target] looks to have experienced a revelation!", \
+ "False faces all dark not real not real not--")
+ target.setOxyLoss(0) //In case the shadowling was choking them out
+ SSticker.mode.add_thrall(target.mind)
+ target.mind.special_role = SPECIAL_ROLE_SHADOWLING_THRALL
/obj/effect/proc_holder/spell/targeted/shadowling_regenarmor //Resets a shadowling's species to normal, removes genetic defects, and re-equips their armor
name = "Rapid Re-Hatch"
@@ -405,7 +365,7 @@
reviveThrallAcquired = 1
to_chat(target, "The power of your thralls has granted you the Black Recuperation ability. \
This will, after a short time, bring a dead thrall completely back to life with no bodily defects.")
- target.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/reviveThrall(null))
+ target.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/reviveThrall(null))
if(thralls < victory_threshold)
to_chat(target, "You do not have the power to ascend. You require [victory_threshold] thralls, but only [thralls] living thralls are present.")
@@ -571,169 +531,170 @@
-/obj/effect/proc_holder/spell/targeted/reviveThrall
+/obj/effect/proc_holder/spell/targeted/click/reviveThrall
name = "Black Recuperation"
desc = "Revives or empowers a thrall."
panel = "Shadowling Abilities"
range = 1
charge_max = 600
- clothes_req = 0
- include_user = 0
+ clothes_req = FALSE
+ include_user = FALSE
action_icon_state = "revive_thrall"
- humans_only = 1
+ click_radius = -1 // Precision baby
+ selection_activated_message = "You start focusing your powers on mending wounds of allies. Left-click to cast at a target!"
+ selection_deactivated_message = "Your mind relaxes."
+ allowed_type = /mob/living/carbon/human
-/obj/effect/proc_holder/spell/targeted/reviveThrall/cast(list/targets, mob/user = usr)
+/obj/effect/proc_holder/spell/targeted/click/reviveThrall/can_cast(mob/user = usr)
if(!shadowling_check(user))
- charge_counter = charge_max
- return
- for(var/mob/living/carbon/human/thrallToRevive in targets)
- var/choice = alert(user,"Empower a living thrall or revive a dead one?",,"Empower","Revive","Cancel")
- switch(choice)
- if("Empower")
- if(!is_thrall(thrallToRevive))
- to_chat(user, "[thrallToRevive] is not a thrall.")
- charge_counter = charge_max
- return
- if(thrallToRevive.stat != CONSCIOUS)
- to_chat(user, "[thrallToRevive] must be conscious to become empowered.")
- charge_counter = charge_max
- return
- if(isshadowlinglesser(thrallToRevive))
- to_chat(user, "[thrallToRevive] is already empowered.")
- charge_counter = charge_max
- return
- var/empowered_thralls = 0
- for(var/datum/mind/M in SSticker.mode.shadowling_thralls)
- if(!ishuman(M.current))
- return
- var/mob/living/carbon/human/H = M.current
- if(isshadowlinglesser(H))
- empowered_thralls++
- if(empowered_thralls >= EMPOWERED_THRALL_LIMIT)
- to_chat(user, "You cannot spare this much energy. There are too many empowered thralls.")
- charge_counter = charge_max
- return
- user.visible_message("[user] places [user.p_their()] hands over [thrallToRevive]'s face, red light shining from beneath.", \
- "You place your hands on [thrallToRevive]'s face and begin gathering energy...")
- to_chat(thrallToRevive, "[user] places [user.p_their()] hands over your face. You feel energy gathering. Stand still...")
- if(!do_mob(user, thrallToRevive, 80))
- to_chat(user, "Your concentration snaps. The flow of energy ebbs.")
- charge_counter = charge_max
- return
- to_chat(user, "You release a massive surge of power into [thrallToRevive]!")
- user.visible_message("Red lightning surges into [thrallToRevive]'s face!")
- playsound(thrallToRevive, 'sound/weapons/egloves.ogg', 50, 1)
- playsound(thrallToRevive, 'sound/machines/defib_zap.ogg', 50, 1)
- user.Beam(thrallToRevive,icon_state="red_lightning",icon='icons/effects/effects.dmi',time=1)
- thrallToRevive.Weaken(5)
- thrallToRevive.visible_message("[thrallToRevive] collapses, [thrallToRevive.p_their()] skin and face distorting!", \
- "AAAAAAAAAAAAAAAAAAAGH-")
- sleep(20)
- thrallToRevive.visible_message("[thrallToRevive] slowly rises, no longer recognizable as human.", \
- "You feel new power flow into you. You have been gifted by your masters. You now closely resemble them. You are empowered in \
- darkness but wither slowly in light. In addition, you now have glare and true shadow walk.")
- thrallToRevive.set_species(/datum/species/shadow/ling/lesser)
- thrallToRevive.mind.RemoveSpell(/obj/effect/proc_holder/spell/targeted/lesser_shadow_walk)
- thrallToRevive.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/glare(null))
- thrallToRevive.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/shadow_walk(null))
- if("Revive")
- if(!is_thrall(thrallToRevive))
- to_chat(user, "[thrallToRevive] is not a thrall.")
- charge_counter = charge_max
- return
- if(thrallToRevive.stat != DEAD)
- to_chat(user, "[thrallToRevive] is not dead.")
- charge_counter = charge_max
- return
- user.visible_message("[user] kneels over [thrallToRevive], placing [user.p_their()] hands on [thrallToRevive.p_their()] chest.", \
- "You crouch over the body of your thrall and begin gathering energy...")
- thrallToRevive.notify_ghost_cloning("Your masters are resuscitating you! Re-enter your corpse if you wish to be brought to life.", source = thrallToRevive)
- if(!do_mob(user, thrallToRevive, 30))
- to_chat(user, "Your concentration snaps. The flow of energy ebbs.")
- charge_counter = charge_max
- return
- to_chat(user, "You release a massive surge of power into [thrallToRevive]!")
- user.visible_message("Red lightning surges from [user]'s hands into [thrallToRevive]'s chest!")
- playsound(thrallToRevive, 'sound/weapons/egloves.ogg', 50, 1)
- playsound(thrallToRevive, 'sound/machines/defib_zap.ogg', 50, 1)
- user.Beam(thrallToRevive,icon_state="red_lightning",icon='icons/effects/effects.dmi',time=1)
- sleep(10)
- if(thrallToRevive.revive())
- thrallToRevive.visible_message("[thrallToRevive] heaves in breath, dim red light shining in [thrallToRevive.p_their()] eyes.", \
- "You have returned. One of your masters has brought you from the darkness beyond.")
- thrallToRevive.Weaken(4)
- thrallToRevive.emote("gasp")
- playsound(thrallToRevive, "bodyfall", 50, 1)
- else
- charge_counter = charge_max
- return
+ return FALSE
+ return ..()
-/obj/effect/proc_holder/spell/targeted/shadowling_extend_shuttle
+/obj/effect/proc_holder/spell/targeted/click/reviveThrall/valid_target(mob/living/carbon/human/target, user)
+ if(!..())
+ return FALSE
+
+ return is_thrall(target)
+
+/obj/effect/proc_holder/spell/targeted/click/reviveThrall/cast(list/targets, mob/user = usr)
+ var/mob/living/carbon/human/thrallToRevive = targets[1]
+ if(thrallToRevive.stat == CONSCIOUS)
+ if(isshadowlinglesser(thrallToRevive))
+ to_chat(user, "[thrallToRevive] is already empowered.")
+ revert_cast(user)
+ return
+ var/empowered_thralls = 0
+ for(var/datum/mind/M in SSticker.mode.shadowling_thralls)
+ if(!ishuman(M.current))
+ return
+ var/mob/living/carbon/human/H = M.current
+ if(isshadowlinglesser(H))
+ empowered_thralls++
+ if(empowered_thralls >= EMPOWERED_THRALL_LIMIT)
+ to_chat(user, "You cannot spare this much energy. There are too many empowered thralls.")
+ revert_cast(user)
+ return
+ user.visible_message("[user] places [user.p_their()] hands over [thrallToRevive]'s face, red light shining from beneath.", \
+ "You place your hands on [thrallToRevive]'s face and begin gathering energy...")
+ to_chat(thrallToRevive, "[user] places [user.p_their()] hands over your face. You feel energy gathering. Stand still...")
+ if(!do_mob(user, thrallToRevive, 80))
+ to_chat(user, "Your concentration snaps. The flow of energy ebbs.")
+ revert_cast(user)
+ return
+ to_chat(user, "You release a massive surge of power into [thrallToRevive]!")
+ user.visible_message("Red lightning surges into [thrallToRevive]'s face!")
+ playsound(thrallToRevive, 'sound/weapons/egloves.ogg', 50, 1)
+ playsound(thrallToRevive, 'sound/machines/defib_zap.ogg', 50, 1)
+ user.Beam(thrallToRevive,icon_state="red_lightning",icon='icons/effects/effects.dmi',time=1)
+ thrallToRevive.Weaken(5)
+ thrallToRevive.visible_message("[thrallToRevive] collapses, [thrallToRevive.p_their()] skin and face distorting!", \
+ "AAAAAAAAAAAAAAAAAAAGH-")
+ sleep(20)
+ thrallToRevive.visible_message("[thrallToRevive] slowly rises, no longer recognizable as human.", \
+ "You feel new power flow into you. You have been gifted by your masters. You now closely resemble them. You are empowered in \
+ darkness but wither slowly in light. In addition, you now have glare and true shadow walk.")
+ thrallToRevive.set_species(/datum/species/shadow/ling/lesser)
+ thrallToRevive.mind.RemoveSpell(/obj/effect/proc_holder/spell/targeted/lesser_shadow_walk)
+ thrallToRevive.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/glare(null))
+ thrallToRevive.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/shadow_walk(null))
+ else if(thrallToRevive.stat == DEAD)
+ user.visible_message("[user] kneels over [thrallToRevive], placing [user.p_their()] hands on [thrallToRevive.p_their()] chest.", \
+ "You crouch over the body of your thrall and begin gathering energy...")
+ thrallToRevive.notify_ghost_cloning("Your masters are resuscitating you! Re-enter your corpse if you wish to be brought to life.", source = thrallToRevive)
+ if(!do_mob(user, thrallToRevive, 30))
+ to_chat(user, "Your concentration snaps. The flow of energy ebbs.")
+ revert_cast(user)
+ return
+ to_chat(user, "You release a massive surge of power into [thrallToRevive]!")
+ user.visible_message("Red lightning surges from [user]'s hands into [thrallToRevive]'s chest!")
+ playsound(thrallToRevive, 'sound/weapons/egloves.ogg', 50, 1)
+ playsound(thrallToRevive, 'sound/machines/defib_zap.ogg', 50, 1)
+ user.Beam(thrallToRevive,icon_state="red_lightning",icon='icons/effects/effects.dmi',time=1)
+ sleep(10)
+ if(thrallToRevive.revive())
+ thrallToRevive.visible_message("[thrallToRevive] heaves in breath, dim red light shining in [thrallToRevive.p_their()] eyes.", \
+ "You have returned. One of your masters has brought you from the darkness beyond.")
+ thrallToRevive.Weaken(4)
+ thrallToRevive.emote("gasp")
+ playsound(thrallToRevive, "bodyfall", 50, 1)
+ else
+ to_chat(user, "The target must be awake to empower or dead to revive.")
+ revert_cast(user)
+
+/obj/effect/proc_holder/spell/targeted/click/shadowling_extend_shuttle
name = "Destroy Engines"
desc = "Extends the time of the emergency shuttle's arrival by ten minutes using a life force of our enemy. Shuttle will be unable to be recalled. This can only be used once."
panel = "Shadowling Abilities"
range = 1
- clothes_req = 0
+ clothes_req = FALSE
charge_max = 600
+ click_radius = -1 // Precision baby
+ selection_activated_message = "You start gathering destructive powers to delay the shuttle. Left-click to cast at a target!"
+ selection_deactivated_message = "Your mind relaxes."
+ allowed_type = /mob/living/carbon/human
action_icon_state = "extend_shuttle"
var/global/extendlimit = 0
-/obj/effect/proc_holder/spell/targeted/shadowling_extend_shuttle/cast(list/targets, mob/user = usr)
+/obj/effect/proc_holder/spell/targeted/click/shadowling_extend_shuttle/can_cast(mob/user = usr, charge_check = TRUE, show_message = FALSE)
if(!shadowling_check(user))
- charge_counter = charge_max
- return
+ return FALSE
if(extendlimit == 1)
- to_chat(user, "Shuttle was already delayed.")
- charge_counter = charge_max
- return
- for(var/mob/living/carbon/human/target in targets)
- if(target.stat)
- charge_counter = charge_max
- return
- if(is_shadow_or_thrall(target))
- to_chat(user, "[target] must not be an ally.")
- charge_counter = charge_max
- return
- if(SSshuttle.emergency.mode != SHUTTLE_CALL)
+ if(show_message)
+ to_chat(user, "Shuttle was already delayed.")
+ return FALSE
+ if(SSshuttle.emergency.mode != SHUTTLE_CALL)
+ if(show_message)
to_chat(user, "The shuttle must be inbound only to the station.")
- charge_counter = charge_max
- return
- var/mob/living/carbon/human/M = target
- user.visible_message("[user]'s eyes flash a bright red!", \
- "You begin to draw [M]'s life force.")
- M.visible_message("[M]'s face falls slack, [M.p_their()] jaw slightly distending.", \
- "You are suddenly transported... far, far away...")
- extendlimit = 1
- if(!do_after(user, 150, target = M))
- extendlimit = 0
- to_chat(M, "You are snapped back to reality, your haze dissipating!")
- to_chat(user, "You have been interrupted. The draw has failed.")
- return
- to_chat(user, "You project [M]'s life force toward the approaching shuttle, extending its arrival duration!")
- M.visible_message("[M]'s eyes suddenly flare red. They proceed to collapse on the floor, not breathing.", \
- "...speeding by... ...pretty blue glow... ...touch it... ...no glow now... ...no light... ...nothing at all...")
- M.death()
- if(SSshuttle.emergency.mode == SHUTTLE_CALL)
- var/more_minutes = 6000
- var/timer = SSshuttle.emergency.timeLeft(1) + more_minutes
- GLOB.event_announcement.Announce("Major system failure aboard the emergency shuttle. This will extend its arrival time by approximately 10 minutes and the shuttle is unable to be recalled.", "System Failure", 'sound/misc/notice1.ogg')
- SSshuttle.emergency.setTimer(timer)
- SSshuttle.emergency.canRecall = FALSE
- user.mind.spell_list.Remove(src) //Can only be used once!
- qdel(src)
+ return FALSE
+ return ..()
+
+/obj/effect/proc_holder/spell/targeted/click/shadowling_extend_shuttle/valid_target(mob/living/carbon/human/target, user)
+ if(!..())
+ return FALSE
+ return !target.stat && !is_shadow_or_thrall(target)
+
+
+/obj/effect/proc_holder/spell/targeted/click/shadowling_extend_shuttle/cast(list/targets, mob/user = usr)
+ var/mob/living/carbon/human/target = targets[1]
+
+ user.visible_message("[user]'s eyes flash a bright red!", \
+ "You begin to draw [target]'s life force.")
+ target.visible_message("[target]'s face falls slack, [target.p_their()] jaw slightly distending.", \
+ "You are suddenly transported... far, far away...")
+ extendlimit = 1
+ if(!do_after(user, 150, target = target))
+ extendlimit = 0
+ to_chat(target, "You are snapped back to reality, your haze dissipating!")
+ to_chat(user, "You have been interrupted. The draw has failed.")
+ return
+ to_chat(user, "You project [target]'s life force toward the approaching shuttle, extending its arrival duration!")
+ target.visible_message("[target]'s eyes suddenly flare red. They proceed to collapse on the floor, not breathing.", \
+ "...speeding by... ...pretty blue glow... ...touch it... ...no glow now... ...no light... ...nothing at all...")
+ target.death()
+ if(SSshuttle.emergency.mode == SHUTTLE_CALL)
+ var/more_minutes = 6000
+ var/timer = SSshuttle.emergency.timeLeft(1) + more_minutes
+ GLOB.event_announcement.Announce("Major system failure aboard the emergency shuttle. This will extend its arrival time by approximately 10 minutes and the shuttle is unable to be recalled.", "System Failure", 'sound/misc/notice1.ogg')
+ SSshuttle.emergency.setTimer(timer)
+ SSshuttle.emergency.canRecall = FALSE
+ user.mind.spell_list.Remove(src) //Can only be used once!
+ qdel(src)
// ASCENDANT ABILITIES BEYOND THIS POINT //
-/obj/effect/proc_holder/spell/targeted/annihilate
+/obj/effect/proc_holder/spell/targeted/click/annihilate
name = "Annihilate"
desc = "Gibs someone instantly."
panel = "Ascendant"
range = 7
- charge_max = 0
- clothes_req = 0
+ charge_max = FALSE
+ clothes_req = FALSE
action_icon_state = "annihilate"
+ selection_activated_message = "You start thinking about gibs. Left-click to cast at a target!"
+ selection_deactivated_message = "Your mind relaxes."
+ allowed_type = /mob/living/carbon/human
-/obj/effect/proc_holder/spell/targeted/annihilate/cast(list/targets, mob/user = usr)
+/obj/effect/proc_holder/spell/targeted/click/annihilate/cast(list/targets, mob/user = usr)
var/mob/living/simple_animal/ascendant_shadowling/SHA = user
if(SHA.phasing)
to_chat(user, "You are not in the same plane of existence. Unphase first.")
@@ -756,45 +717,42 @@
-/obj/effect/proc_holder/spell/targeted/hypnosis
+/obj/effect/proc_holder/spell/targeted/click/hypnosis
name = "Hypnosis"
desc = "Instantly enthralls a human."
panel = "Ascendant"
range = 7
- charge_max = 0
- clothes_req = 0
+ charge_max = FALSE
+ clothes_req = FALSE
action_icon_state = "enthrall"
-/obj/effect/proc_holder/spell/targeted/hypnosis/cast(list/targets, mob/user = usr)
- var/mob/living/simple_animal/ascendant_shadowling/SHA = user
- if(SHA.phasing)
- charge_counter = charge_max
- to_chat(user, "You are not in the same plane of existence. Unphase first.")
- return
+ click_radius = -1
+ selection_activated_message = "You start preparing to mindwash over a mortal mind. Left-click to cast at a target!"
+ selection_deactivated_message = "Your mind relaxes."
+ allowed_type = /mob/living/carbon/human
- for(var/mob/living/carbon/human/target in targets)
- if(is_shadow_or_thrall(target))
- to_chat(user, "You cannot enthrall an ally.")
- charge_counter = charge_max
- return
- if(!target.ckey || !target.mind)
- to_chat(user, "The target has no mind.")
- charge_counter = charge_max
- return
- if(target.stat)
- to_chat(user, "The target must be conscious.")
- charge_counter = charge_max
- return
- if(!ishuman(target))
- to_chat(user, "You can only enthrall humans.")
- charge_counter = charge_max
- return
+/obj/effect/proc_holder/spell/targeted/click/hypnosis/can_cast(mob/living/simple_animal/ascendant_shadowling/user = usr, charge_check = TRUE, show_message = FALSE)
+ if(!istype(user))
+ return FALSE
+ if(user.phasing)
+ if(show_message)
+ to_chat(user, "You are not in the same plane of existence. Unphase first.")
+ return FALSE
+ return ..()
- to_chat(user, "You instantly rearrange [target]'s memories, hyptonitizing [target.p_them()] into a thrall.")
- to_chat(target, "An agonizing spike of pain drives into your mind, and--")
- SSticker.mode.add_thrall(target.mind)
- target.mind.special_role = SPECIAL_ROLE_SHADOWLING_THRALL
- target.add_language("Shadowling Hivemind")
+/obj/effect/proc_holder/spell/targeted/click/hypnosis/valid_target(mob/living/carbon/human/target, user)
+ if(!..())
+ return FALSE
+ return !is_shadow_or_thrall(target) && target.ckey && target.mind && !target.stat
+
+/obj/effect/proc_holder/spell/targeted/click/hypnosis/cast(list/targets, mob/user = usr)
+ var/mob/living/carbon/human/target = targets[1]
+
+ to_chat(user, "You instantly rearrange [target]'s memories, hyptonitizing [target.p_them()] into a thrall.")
+ to_chat(target, "An agonizing spike of pain drives into your mind, and--")
+ SSticker.mode.add_thrall(target.mind)
+ target.mind.special_role = SPECIAL_ROLE_SHADOWLING_THRALL
+ target.add_language("Shadowling Hivemind")
diff --git a/code/game/gamemodes/shadowling/special_shadowling_abilities.dm b/code/game/gamemodes/shadowling/special_shadowling_abilities.dm
index 83ca06fe4b7..28c934f7d38 100644
--- a/code/game/gamemodes/shadowling/special_shadowling_abilities.dm
+++ b/code/game/gamemodes/shadowling/special_shadowling_abilities.dm
@@ -99,14 +99,14 @@ GLOBAL_LIST_INIT(possibleShadowlingNames, list("U'ruan", "Y`shej", "Nex", "Hel-u
to_chat(H, "Your powers are awoken. You may now live to your fullest extent. Remember your goal. Cooperate with your thralls and allies.")
H.ExtinguishMob()
H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/shadow_vision(null))
- H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/enthrall(null))
- H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/glare(null))
+ H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/enthrall(null))
+ H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/glare(null))
H.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/veil(null))
H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/shadow_walk(null))
H.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/flashfreeze(null))
H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/collective_mind(null))
H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/shadowling_regenarmor(null))
- H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/shadowling_extend_shuttle(null))
+ H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/shadowling_extend_shuttle(null))
QDEL_NULL(H.hud_used)
H.hud_used = new /datum/hud/human(H, ui_style2icon(H.client.prefs.UI_style), H.client.prefs.UI_style_color, H.client.prefs.UI_style_alpha)
@@ -172,8 +172,8 @@ GLOBAL_LIST_INIT(possibleShadowlingNames, list("U'ruan", "Y`shej", "Nex", "Hel-u
H.mind.transfer_to(A)
A.name = H.real_name
A.languages = H.languages
- A.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/annihilate(null))
- A.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/hypnosis(null))
+ A.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/annihilate(null))
+ A.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/hypnosis(null))
A.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/shadowling_phase_shift(null))
A.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/ascendant_storm(null))
A.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/shadowlingAscendantTransmit(null))
diff --git a/code/game/gamemodes/traitor/traitor.dm b/code/game/gamemodes/traitor/traitor.dm
index 0c3f148b106..e7c5b4541a8 100644
--- a/code/game/gamemodes/traitor/traitor.dm
+++ b/code/game/gamemodes/traitor/traitor.dm
@@ -119,10 +119,10 @@
if(traitorwin)
- text += "
The [special_role_text] was successful!"
+ text += "
The [special_role_text] was successful!
"
feedback_add_details("traitor_success","SUCCESS")
else
- text += "
The [special_role_text] has failed!"
+ text += "
The [special_role_text] has failed!
"
feedback_add_details("traitor_success","FAIL")
if(length(SSticker.mode.implanted))
diff --git a/code/game/gamemodes/vampire/vampire_powers.dm b/code/game/gamemodes/vampire/vampire_powers.dm
index 7906deeb718..406ab50b70d 100644
--- a/code/game/gamemodes/vampire/vampire_powers.dm
+++ b/code/game/gamemodes/vampire/vampire_powers.dm
@@ -15,7 +15,7 @@
if(!gain_desc)
gain_desc = "You have gained \the [src] ability."
-/obj/effect/proc_holder/spell/vampire/cast_check(skipcharge = 0, mob/living/user = usr)
+/obj/effect/proc_holder/spell/vampire/cast_check(charge_check = TRUE, start_recharge = TRUE, mob/living/user = usr)
if(!user.mind)
return 0
if(!ishuman(user))
@@ -45,7 +45,7 @@
return 0
return ..()
-/obj/effect/proc_holder/spell/vampire/can_cast(mob/user = usr)
+/obj/effect/proc_holder/spell/vampire/can_cast(mob/user = usr, charge_check = TRUE, show_message = FALSE)
if(!user.mind)
return 0
if(!ishuman(user))
diff --git a/code/game/gamemodes/wizard/artefact.dm b/code/game/gamemodes/wizard/artefact.dm
index 442e9958fc7..cb0c277b1e6 100644
--- a/code/game/gamemodes/wizard/artefact.dm
+++ b/code/game/gamemodes/wizard/artefact.dm
@@ -61,7 +61,7 @@
switch(href_list["school"])
if("destruction")
M.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/projectile/magic_missile(null))
- M.mind.AddSpell(new /obj/effect/proc_holder/spell/fireball(null))
+ M.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/fireball(null))
to_chat(M, "Your service has not gone unrewarded, however. Studying under [H.real_name], you have learned powerful, destructive spells. You are able to cast magic missile and fireball.")
if("bluespace")
M.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/area_teleport/teleport(null))
@@ -74,7 +74,7 @@
to_chat(M, "Your service has not gone unrewarded, however. Studying under [H.real_name], you have learned livesaving survival spells. You are able to cast charge and forcewall.")
if("robeless")
M.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/knock(null))
- M.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/mind_transfer(null))
+ M.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/mind_transfer(null))
to_chat(M, "Your service has not gone unrewarded, however. Studying under [H.real_name], you have learned stealthy, robeless spells. You are able to cast knock and mindswap.")
M.equip_to_slot_or_del(new /obj/item/radio/headset(M), slot_l_ear)
diff --git a/code/game/gamemodes/wizard/spellbook.dm b/code/game/gamemodes/wizard/spellbook.dm
index d0773947c21..efa3303c26b 100644
--- a/code/game/gamemodes/wizard/spellbook.dm
+++ b/code/game/gamemodes/wizard/spellbook.dm
@@ -132,7 +132,7 @@
/datum/spellbook_entry/horseman
name = "Curse of the Horseman"
- spell_type = /obj/effect/proc_holder/spell/targeted/horsemask
+ spell_type = /obj/effect/proc_holder/spell/targeted/click/horsemask
log_name = "HH"
category = "Offensive"
@@ -144,7 +144,7 @@
/datum/spellbook_entry/fireball
name = "Fireball"
- spell_type = /obj/effect/proc_holder/spell/fireball
+ spell_type = /obj/effect/proc_holder/spell/targeted/click/fireball
log_name = "FB"
category = "Offensive"
@@ -257,7 +257,7 @@
/datum/spellbook_entry/mindswap
name = "Mindswap"
- spell_type = /obj/effect/proc_holder/spell/targeted/mind_transfer
+ spell_type = /obj/effect/proc_holder/spell/targeted/click/mind_transfer
log_name = "MT"
category = "Mobility"
@@ -877,7 +877,7 @@
return
/obj/item/spellbook/oneuse/fireball
- spell = /obj/effect/proc_holder/spell/fireball
+ spell = /obj/effect/proc_holder/spell/targeted/click/fireball
spellname = "fireball"
icon_state = "bookfireball"
desc = "This book feels warm to the touch."
@@ -910,7 +910,7 @@
user.EyeBlind(10)
/obj/item/spellbook/oneuse/mindswap
- spell = /obj/effect/proc_holder/spell/targeted/mind_transfer
+ spell = /obj/effect/proc_holder/spell/targeted/click/mind_transfer
spellname = "mindswap"
icon_state = "bookmindswap"
desc = "This book's cover is pristine, though its pages look ragged and torn."
@@ -934,8 +934,8 @@
to_chat(user, "You stare at the book some more, but there doesn't seem to be anything else to learn...")
return
- var/obj/effect/proc_holder/spell/targeted/mind_transfer/swapper = new
- swapper.cast(user, stored_swap, 1)
+ var/obj/effect/proc_holder/spell/targeted/click/mind_transfer/swapper = new
+ swapper.cast(user, stored_swap)
to_chat(stored_swap, "You're suddenly somewhere else... and someone else?!")
to_chat(user, "Suddenly you're staring at [src] again... where are you, who are you?!")
@@ -966,7 +966,7 @@
user.Weaken(20)
/obj/item/spellbook/oneuse/horsemask
- spell = /obj/effect/proc_holder/spell/targeted/horsemask
+ spell = /obj/effect/proc_holder/spell/targeted/click/horsemask
spellname = "horses"
icon_state = "bookhorses"
desc = "This book is more horse than your mind has room for."
diff --git a/code/game/gamemodes/wizard/wizloadouts.dm b/code/game/gamemodes/wizard/wizloadouts.dm
index c050b27f273..518b9a68ca8 100644
--- a/code/game/gamemodes/wizard/wizloadouts.dm
+++ b/code/game/gamemodes/wizard/wizloadouts.dm
@@ -18,7 +18,7 @@
Care should be taken in hiding the item you choose as your phylactery after using Bind Soul, as you cannot revive if it destroyed or too far from your body!
\
Provides Bind Soul, Ethereal Jaunt, Fireball, Rod Form, Disable Tech, and Greater Forcewall."
log_name = "DL"
- spells_path = list(/obj/effect/proc_holder/spell/targeted/lichdom, /obj/effect/proc_holder/spell/targeted/ethereal_jaunt, /obj/effect/proc_holder/spell/fireball, \
+ spells_path = list(/obj/effect/proc_holder/spell/targeted/lichdom, /obj/effect/proc_holder/spell/targeted/ethereal_jaunt, /obj/effect/proc_holder/spell/targeted/click/fireball, \
/obj/effect/proc_holder/spell/targeted/rod_form, /obj/effect/proc_holder/spell/targeted/emplosion/disable_tech, /obj/effect/proc_holder/spell/targeted/forcewall/greater)
is_ragin_restricted = TRUE
diff --git a/code/game/jobs/job/central.dm b/code/game/jobs/job/central.dm
index f91460ba234..8684ca7d1b0 100644
--- a/code/game/jobs/job/central.dm
+++ b/code/game/jobs/job/central.dm
@@ -94,7 +94,7 @@
)
cybernetic_implants = list(
/obj/item/organ/internal/cyberimp/eyes/xray,
- /obj/item/organ/internal/cyberimp/brain/anti_stun,
+ /obj/item/organ/internal/cyberimp/brain/anti_stun/hardened,
/obj/item/organ/internal/cyberimp/chest/nutriment/plus,
/obj/item/organ/internal/cyberimp/arm/combat/centcom
)
diff --git a/code/game/jobs/job/support.dm b/code/game/jobs/job/support.dm
index 09328865b59..fda831242f9 100644
--- a/code/game/jobs/job/support.dm
+++ b/code/game/jobs/job/support.dm
@@ -458,6 +458,7 @@
total_positions = 0
spawn_positions = 0
supervisors = "the head of personnel"
+ department_head = list("Head of Personnel")
selection_color = "#dddddd"
access = list(ACCESS_MAINT_TUNNELS, ACCESS_GATEWAY, ACCESS_EVA, ACCESS_EXTERNAL_AIRLOCKS)
minimal_access = list(ACCESS_MAINT_TUNNELS, ACCESS_GATEWAY, ACCESS_EVA, ACCESS_EXTERNAL_AIRLOCKS)
diff --git a/code/game/jobs/job/support_chaplain.dm b/code/game/jobs/job/support_chaplain.dm
index b39eed344ae..bfd9c4520f5 100644
--- a/code/game/jobs/job/support_chaplain.dm
+++ b/code/game/jobs/job/support_chaplain.dm
@@ -26,7 +26,7 @@
/obj/item/camera/spooky = 1,
/obj/item/nullrod = 1
)
-
+
/datum/outfit/job/chaplain/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE)
. = ..()
@@ -77,7 +77,7 @@
new_deity = deity_name
B.deity_name = new_deity
- H.AddSpell(new /obj/effect/proc_holder/spell/targeted/chaplain_bless(null))
+ H.AddSpell(new /obj/effect/proc_holder/spell/targeted/click/chaplain_bless(null))
var/accepted = 0
var/outoftime = 0
diff --git a/code/game/machinery/computer/robot.dm b/code/game/machinery/computer/robot.dm
index ed3b3d95099..f676d861f96 100644
--- a/code/game/machinery/computer/robot.dm
+++ b/code/game/machinery/computer/robot.dm
@@ -46,7 +46,7 @@
return FALSE
if(R.scrambledcodes)
return FALSE
- if(!atoms_share_level(src, R))
+ if(!atoms_share_level(get_turf(src), get_turf(R)))
return FALSE
return TRUE
diff --git a/code/game/machinery/vending.dm b/code/game/machinery/vending.dm
index 48b022a65f3..1ac341db260 100644
--- a/code/game/machinery/vending.dm
+++ b/code/game/machinery/vending.dm
@@ -1272,7 +1272,7 @@
ads_list = list("We like plants!","Don't you want some?","The greenest thumbs ever.","We like big plants.","Soft soil...")
icon_state = "nutri"
icon_deny = "nutri-deny"
- products = list(/obj/item/reagent_containers/glass/bottle/nutrient/ez = 30,/obj/item/reagent_containers/glass/bottle/nutrient/l4z = 20,/obj/item/reagent_containers/glass/bottle/nutrient/rh = 10,/obj/item/reagent_containers/spray/pestspray = 20,
+ products = list(/obj/item/reagent_containers/glass/bottle/nutrient/ez = 20,/obj/item/reagent_containers/glass/bottle/nutrient/l4z = 13,/obj/item/reagent_containers/glass/bottle/nutrient/rh = 6,/obj/item/reagent_containers/spray/pestspray = 20,
/obj/item/reagent_containers/syringe = 5,/obj/item/storage/bag/plants = 5,/obj/item/cultivator = 3,/obj/item/shovel/spade = 3,/obj/item/plant_analyzer = 4)
contraband = list(/obj/item/reagent_containers/glass/bottle/ammonia = 10,/obj/item/reagent_containers/glass/bottle/diethylamine = 5)
refill_canister = /obj/item/vending_refill/hydronutrients
diff --git a/code/game/objects/effects/spiders.dm b/code/game/objects/effects/spiders.dm
index 639687a45e9..a1538b5fe92 100644
--- a/code/game/objects/effects/spiders.dm
+++ b/code/game/objects/effects/spiders.dm
@@ -42,7 +42,7 @@
/obj/structure/spider/stickyweb/CanPass(atom/movable/mover, turf/target, height=0)
if(height == 0)
return TRUE
- if(istype(mover, /mob/living/simple_animal/hostile/poison/giant_spider))
+ if(istype(mover, /mob/living/simple_animal/hostile/poison/giant_spider) || isterrorspider(mover))
return TRUE
else if(istype(mover, /mob/living))
if(prob(50))
diff --git a/code/game/objects/items/devices/aicard.dm b/code/game/objects/items/devices/aicard.dm
index 3b3391b506b..7eb1dc9fd56 100644
--- a/code/game/objects/items/devices/aicard.dm
+++ b/code/game/objects/items/devices/aicard.dm
@@ -37,70 +37,83 @@
overlays.Cut()
/obj/item/aicard/attack_self(mob/user)
- ui_interact(user)
+ tgui_interact(user)
-/obj/item/aicard/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1, var/datum/topic_state/state = GLOB.inventory_state)
- ui = SSnanoui.try_update_ui(user, src, ui_key, ui, force_open)
+/obj/item/aicard/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = TRUE, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_inventory_state)
+ ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
if(!ui)
- ui = new(user, src, ui_key, "aicard.tmpl", "[name]", 600, 400, state = state)
+ ui = new(user, src, ui_key, "AICard", "[name]", 600, 394, master_ui, state)
ui.open()
- ui.set_auto_update(1)
-/obj/item/aicard/ui_data(mob/user, ui_key = "main", datum/topic_state/state = GLOB.inventory_state)
+/obj/item/aicard/tgui_data(mob/user, ui_key = "main", datum/topic_state/state = GLOB.inventory_state)
var/data[0]
var/mob/living/silicon/ai/AI = locate() in src
if(istype(AI))
- data["has_ai"] = 1
+ data["has_ai"] = TRUE
data["name"] = AI.name
- data["hardware_integrity"] = ((AI.health + 100) / 2)
+ data["integrity"] = ((AI.health + 100) / 2)
data["radio"] = !AI.aiRadio.disabledAi
data["wireless"] = !AI.control_disabled
data["operational"] = AI.stat != DEAD
data["flushing"] = flush
var/laws[0]
- for(var/datum/ai_law/AL in AI.laws.all_laws())
- laws[++laws.len] = list("index" = AL.get_index(), "law" = sanitize(AL.law))
+ for(var/datum/ai_law/law in AI.laws.all_laws())
+ if(law in AI.laws.ion_laws) // If we're an ion law, give it an ion index code
+ laws.Add(ionnum() + ". " + law.law)
+ else
+ laws.Add(num2text(law.get_index()) + ". " + law.law)
data["laws"] = laws
- data["has_laws"] = laws.len
+ data["has_laws"] = length(AI.laws.all_laws())
+
+ else
+ data["has_ai"] = FALSE // If this isn't passed to tgui, it won't show there isn't a AI in the card.
return data
-/obj/item/aicard/Topic(href, href_list, nowindow, state)
+/obj/item/aicard/tgui_act(action, params)
if(..())
- return 1
+ return
var/mob/living/silicon/ai/AI = locate() in src
if(!istype(AI))
- return 1
+ return
var/user = usr
+ switch(action)
+ if("wipe")
+ if(flush) // Don't doublewipe.
+ to_chat(user, "You are already wiping this AI!")
+ return
+ var/confirm = alert("Are you sure you want to wipe this card's memory? This cannot be undone once started.", "Confirm Wipe", "Yes", "No")
+ if(confirm == "Yes" && (tgui_status(user, GLOB.tgui_inventory_state) == STATUS_INTERACTIVE)) // And make doubly sure they want to wipe (three total clicks)
+ msg_admin_attack("[key_name_admin(user)] wiped [key_name_admin(AI)] with \the [src].", ATKLOG_FEW)
+ add_attack_logs(user, AI, "Wiped with [src].")
+ INVOKE_ASYNC(src, .proc/wipe_ai)
- if(href_list["wipe"])
- var/confirm = alert("Are you sure you want to wipe this card's memory? This cannot be undone once started.", "Confirm Wipe", "Yes", "No")
- if(confirm == "Yes" && (CanUseTopic(user, state) == STATUS_INTERACTIVE))
- add_attack_logs(user, AI, "Wiped with [src].", ATKLOG_FEW)
- flush = 1
- AI.suiciding = 1
- to_chat(AI, "Your core files are being wiped!")
- while(AI && AI.stat != DEAD)
- AI.adjustOxyLoss(2)
- sleep(10)
- flush = 0
+ if("radio")
+ AI.aiRadio.disabledAi = !AI.aiRadio.disabledAi
+ to_chat(AI, "Your Subspace Transceiver has been [AI.aiRadio.disabledAi ? "disabled" : "enabled"]!")
+ to_chat(user, "You [AI.aiRadio.disabledAi ? "disable" : "enable"] the AI's Subspace Transceiver.")
- if(href_list["radio"])
- AI.aiRadio.disabledAi = text2num(href_list["radio"])
- to_chat(AI, "Your Subspace Transceiver has been [AI.aiRadio.disabledAi ? "disabled" : "enabled"]!")
- to_chat(user, "You [AI.aiRadio.disabledAi ? "disable" : "enable"] the AI's Subspace Transceiver.")
+ if("wireless")
+ AI.control_disabled = !AI.control_disabled
+ to_chat(AI, "Your wireless interface has been [AI.control_disabled ? "disabled" : "enabled"]!")
+ to_chat(user, "You [AI.control_disabled ? "disable" : "enable"] the AI's wireless interface.")
+ update_icon()
- if(href_list["wireless"])
- AI.control_disabled = text2num(href_list["wireless"])
- to_chat(AI, "Your wireless interface has been [AI.control_disabled ? "disabled" : "enabled"]!")
- to_chat(user, "You [AI.control_disabled ? "disable" : "enable"] the AI's wireless interface.")
- update_icon()
+ return TRUE
- return 1
+/obj/item/aicard/proc/wipe_ai()
+ var/mob/living/silicon/ai/AI = locate() in src
+ flush = TRUE
+ AI.suiciding = TRUE
+ to_chat(AI, "Your core files are being wiped!")
+ while(AI && AI.stat != DEAD)
+ AI.adjustOxyLoss(2)
+ sleep(10)
+ flush = FALSE
diff --git a/code/game/objects/items/devices/radio/headset.dm b/code/game/objects/items/devices/radio/headset.dm
index d9af70df391..2142dc9e50b 100644
--- a/code/game/objects/items/devices/radio/headset.dm
+++ b/code/game/objects/items/devices/radio/headset.dm
@@ -45,9 +45,6 @@
QDEL_NULL(keyslot2)
return ..()
-/obj/item/radio/headset/list_channels(var/mob/user)
- return list_secure_channels()
-
/obj/item/radio/headset/examine(mob/user)
. = ..()
if(in_range(src, user) && radio_desc)
diff --git a/code/game/objects/items/devices/radio/intercom.dm b/code/game/objects/items/devices/radio/intercom.dm
index d9d3aa3c3c1..f9ba60ab380 100644
--- a/code/game/objects/items/devices/radio/intercom.dm
+++ b/code/game/objects/items/devices/radio/intercom.dm
@@ -245,14 +245,7 @@
usesound = 'sound/items/deconstruct.ogg'
/obj/item/radio/intercom/locked
- var/locked_frequency
-
-/obj/item/radio/intercom/locked/set_frequency(var/frequency)
- if(frequency == locked_frequency)
- ..(locked_frequency)
-
-/obj/item/radio/intercom/locked/list_channels()
- return ""
+ freqlock = TRUE
/obj/item/radio/intercom/locked/ai_private
name = "\improper AI intercom"
diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm
index cdcc2361937..ed6b0190502 100644
--- a/code/game/objects/items/devices/radio/radio.dm
+++ b/code/game/objects/items/devices/radio/radio.dm
@@ -129,7 +129,10 @@ GLOBAL_LIST_INIT(default_medbay_channels, list(
/obj/item/radio/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = TRUE, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_default_state)
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
if(!ui)
- ui = new(user, src, ui_key, "Radio", name, 360, 150 + (length(channels) * 20), master_ui, state)
+ var/list/schannels = list_secure_channels(user)
+ var/list/ichannels = list_internal_channels(user)
+ var/calc_height = 150 + (schannels.len * 20) + (ichannels.len * 10)
+ ui = new(user, src, ui_key, "Radio", name, 400, calc_height, master_ui, state)
ui.open()
/obj/item/radio/tgui_data(mob/user)
@@ -142,9 +145,8 @@ GLOBAL_LIST_INIT(default_medbay_channels, list(
data["maxFrequency"] = freerange ? RADIO_HIGH_FREQ : PUBLIC_HIGH_FREQ
data["canReset"] = frequency == initial(frequency) ? FALSE : TRUE
data["freqlock"] = freqlock
- data["channels"] = list()
- for(var/channel in channels)
- data["channels"][channel] = channels[channel] & FREQ_LISTENING
+ data["schannels"] = list_secure_channels(user)
+ data["ichannels"] = list_internal_channels(user)
data["has_loudspeaker"] = has_loudspeaker
data["loudspeaker"] = loudspeaker
@@ -173,6 +175,12 @@ GLOBAL_LIST_INIT(default_medbay_channels, list(
usr << browse(null, "window=radio")
if(.)
set_frequency(sanitize_frequency(tune, freerange))
+ if("ichannel") // change primary frequency to an internal channel authorized by access
+ if(freqlock)
+ return
+ var/freq = params["ichannel"]
+ if(has_channel_access(usr, freq))
+ set_frequency(text2num(freq))
if("listen")
listening = !listening
if("broadcast")
@@ -198,34 +206,32 @@ GLOBAL_LIST_INIT(default_medbay_channels, list(
if(.)
add_fingerprint(usr)
-/obj/item/radio/proc/list_channels(var/mob/user)
- return list_internal_channels(user)
-
-/obj/item/radio/proc/list_secure_channels(var/mob/user)
- var/dat[0]
-
- for(var/ch_name in channels)
- var/chan_stat = channels[ch_name]
- var/listening = !!(chan_stat & FREQ_LISTENING) != 0
-
- dat.Add(list(list("chan" = ch_name, "display_name" = ch_name, "secure_channel" = 1, "sec_channel_listen" = !listening, "chan_span" = SSradio.frequency_span_class(SSradio.radiochannels[ch_name]))))
-
+/obj/item/radio/proc/list_secure_channels(mob/user)
+ var/list/dat = list()
+ for(var/channel in channels)
+ dat[channel] = channels[channel] & FREQ_LISTENING
return dat
-/obj/item/radio/proc/list_internal_channels(var/mob/user)
- var/dat[0]
+/obj/item/radio/proc/list_internal_channels(mob/user)
+ var/list/dat = list()
+ if(freqlock)
+ return dat
for(var/internal_chan in internal_channels)
+ var/freqnum = text2num(internal_chan)
+ var/freqname = get_frequency_name(freqnum)
if(has_channel_access(user, internal_chan))
- dat.Add(list(list("chan" = internal_chan, "display_name" = get_frequency_name(text2num(internal_chan)), "chan_span" = SSradio.frequency_span_class(text2num(internal_chan)))))
-
+ dat[freqname] = freqnum // unlike secure_channels, this is set to the freq number so Radio.js can use it as an arg
return dat
-/obj/item/radio/proc/has_channel_access(var/mob/user, var/freq)
+/obj/item/radio/proc/has_channel_access(mob/user, freq)
if(!user)
- return 0
+ return FALSE
if(!(freq in internal_channels))
- return 0
+ return FALSE
+
+ if(isrobot(user))
+ return FALSE // cyborgs and drones are not allowed to remotely re-tune intercomms, etc
return user.has_internal_radio_channel_access(user, internal_channels[freq])
@@ -603,13 +609,14 @@ GLOBAL_LIST_INIT(default_medbay_channels, list(
/obj/item/radio/borg
name = "Cyborg Radio"
var/mob/living/silicon/robot/myborg = null // Cyborg which owns this radio. Used for power checks
- var/obj/item/encryptionkey/keyslot = null//Borg radios can handle a single encryption key
+ var/obj/item/encryptionkey/keyslot // Borg radios can handle a single encryption key
icon = 'icons/obj/robot_component.dmi' // Cyborgs radio icons should look like the component.
icon_state = "radio"
has_loudspeaker = TRUE
loudspeaker = FALSE
canhear_range = 0
dog_fashion = null
+ freqlock = TRUE // don't let cyborgs change the default channel of their internal radio away from common
/obj/item/radio/borg/syndicate
keyslot = new /obj/item/encryptionkey/syndicate/nukeops
@@ -623,9 +630,6 @@ GLOBAL_LIST_INIT(default_medbay_channels, list(
myborg = null
return ..()
-/obj/item/radio/borg/list_channels(var/mob/user)
- return list_secure_channels(user)
-
/obj/item/radio/borg/syndicate/New()
..()
syndiekey = keyslot
diff --git a/code/game/objects/items/trash.dm b/code/game/objects/items/trash.dm
index 8508c741524..d369161127f 100644
--- a/code/game/objects/items/trash.dm
+++ b/code/game/objects/items/trash.dm
@@ -59,6 +59,14 @@
/obj/item/trash/fried_vox
name = "Kentucky Fried Vox"
icon_state = "fried_vox_empty"
+ item_state = "fried_vox_empty"
+ slot_flags = SLOT_HEAD
+ dog_fashion = /datum/dog_fashion/head/fried_vox_empty
+ sprite_sheets = list(
+ "Skrell" = 'icons/mob/species/skrell/head.dmi',
+ "Drask" = 'icons/mob/species/drask/head.dmi',
+ "Kidan" = 'icons/mob/species/kidan/head.dmi'
+ )
/obj/item/trash/pistachios
name = "Pistachios pack"
diff --git a/code/game/objects/items/weapons/highlander_swords.dm b/code/game/objects/items/weapons/highlander_swords.dm
index 330da6beba0..568b023d051 100644
--- a/code/game/objects/items/weapons/highlander_swords.dm
+++ b/code/game/objects/items/weapons/highlander_swords.dm
@@ -25,14 +25,14 @@
return ..()
/obj/item/claymore/highlander/equipped(mob/user, slot)
- if(!ishuman(user))
+ if(!ishuman(user) || !user.mind)
return
var/mob/living/carbon/human/H = user
if(slot == slot_r_hand || slot == slot_l_hand)
- if(H.martial_art && H.martial_art != style)
- style.teach(H, 1)
+ if(H.mind.martial_art && H.mind.martial_art != style)
+ style.teach(H, TRUE)
to_chat(H, "THERE CAN ONLY BE ONE!")
- else if(H.martial_art && H.martial_art == style)
+ else if(H.mind.martial_art && H.mind.martial_art == style)
style.remove(H)
var/obj/item/claymore/highlander/sword = H.is_in_hands(/obj/item/claymore/highlander)
if(sword)
diff --git a/code/game/objects/items/weapons/implants/implant_krav_maga.dm b/code/game/objects/items/weapons/implants/implant_krav_maga.dm
index 3c2666f3d66..9c33f43950a 100644
--- a/code/game/objects/items/weapons/implants/implant_krav_maga.dm
+++ b/code/game/objects/items/weapons/implants/implant_krav_maga.dm
@@ -17,12 +17,12 @@
/obj/item/implant/krav_maga/activate()
var/mob/living/carbon/human/H = imp_in
- if(!ishuman(H))
+ if(!ishuman(H) || !H.mind)
return
- if(istype(H.martial_art, /datum/martial_art/krav_maga))
+ if(istype(H.mind.martial_art, /datum/martial_art/krav_maga))
style.remove(H)
else
- style.teach(H,1)
+ style.teach(H, TRUE)
/obj/item/implanter/krav_maga
name = "implanter (krav maga)"
diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm
index bdab5ca2466..e9abfca2dac 100644
--- a/code/game/objects/structures/tables_racks.dm
+++ b/code/game/objects/structures/tables_racks.dm
@@ -90,7 +90,7 @@
..()
if(climber)
climber.Weaken(2)
- climber.visible_message("[climber.name] has been knocked off the table", "You've been knocked off the table", "You see [climber.name] get knocked off the table")
+ climber.visible_message("[climber.name] has been knocked off the table", "You've been knocked off the table", "You hear [climber.name] get knocked off the table")
else if(Adjacent(user) && user.pulling && user.pulling.pass_flags & PASSTABLE)
user.Move_Pulled(src)
if(user.pulling.loc == loc)
diff --git a/code/game/world.dm b/code/game/world.dm
index fbdcf12f9fc..d2828f9c04b 100644
--- a/code/game/world.dm
+++ b/code/game/world.dm
@@ -222,8 +222,12 @@ GLOBAL_VAR_INIT(world_topic_spam_protect_time, world.timeofday)
if(input["key"] != config.comms_password)
return "Bad Key"
else
+ var/prtext = input["announce"]
+ var/pr_substring = copytext(prtext, 1, 23)
+ if(pr_substring == "Pull Request merged by")
+ GLOB.pending_server_update = TRUE
for(var/client/C in GLOB.clients)
- to_chat(C, "PR: [input["announce"]]")
+ to_chat(C, "PR: [prtext]")
else if("kick" in input)
/*
@@ -270,7 +274,7 @@ GLOBAL_VAR_INIT(world_topic_spam_protect_time, world.timeofday)
else if("hostannounce" in input)
if(!key_valid)
return keySpamProtect(addr)
-
+ GLOB.pending_server_update = TRUE
to_chat(world, "
Server Announcement: [input["message"]]
")
/proc/keySpamProtect(var/addr)
@@ -339,8 +343,12 @@ GLOBAL_VAR_INIT(world_topic_spam_protect_time, world.timeofday)
return
#endif
+ var/secs_before_auto_reconnect = 10
+ if(GLOB.pending_server_update)
+ secs_before_auto_reconnect = 60
+ to_chat(world, "Reboot will take a little longer, due to pending updates.")
for(var/client/C in GLOB.clients)
- var/secs_before_auto_reconnect = 10 // TODO: make it higher if server is due for an update @AffectedArc07
+
C << output(list2params(list(secs_before_auto_reconnect)), "browseroutput:reboot")
if(config.server) //if you set a server location in config.txt, it sends you there instead of trying to reconnect to the same world address. -- NeoFite
C << link("byond://[config.server]")
diff --git a/code/modules/arcade/prize_datums.dm b/code/modules/arcade/prize_datums.dm
index 1edad36b8f3..2b9f17c3d04 100644
--- a/code/modules/arcade/prize_datums.dm
+++ b/code/modules/arcade/prize_datums.dm
@@ -203,6 +203,12 @@ GLOBAL_DATUM_INIT(global_prizes, /datum/prizes, new())
typepath = /obj/item/toy/toy_xeno
cost = 80
+/datum/prize_item/rubberducky
+ name = "Rubber Ducky"
+ desc = "Your favorite bathtime buddy, all squeaks and quacks quality assured."
+ typepath = /obj/item/bikehorn/rubberducky
+ cost = 80
+
/datum/prize_item/tacticool
name = "Tacticool Turtleneck"
desc = "A cool-looking turtleneck."
diff --git a/code/modules/client/client defines.dm b/code/modules/client/client defines.dm
index 72537f8ccc3..46ec605e71d 100644
--- a/code/modules/client/client defines.dm
+++ b/code/modules/client/client defines.dm
@@ -36,7 +36,10 @@
////////////
//SECURITY//
////////////
- var/next_allowed_topic_time = 10
+
+ ///Used for limiting the rate of topic sends by the client to avoid abuse
+ var/list/topiclimiter
+
// comment out the line below when debugging locally to enable the options & messages menu
//control_freak = 1
diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm
index ae503be111b..f8ef211d5de 100644
--- a/code/modules/client/client procs.dm
+++ b/code/modules/client/client procs.dm
@@ -11,6 +11,13 @@
#define SUGGESTED_CLIENT_VERSION 511 // only integers (e.g: 510, 511) useful here. Does not properly handle minor versions (e.g: 510.58, 511.848)
#define SSD_WARNING_TIMER 30 // cycles, not seconds, so 30=60s
+#define LIMITER_SIZE 5
+#define CURRENT_SECOND 1
+#define SECOND_COUNT 2
+#define CURRENT_MINUTE 3
+#define MINUTE_COUNT 4
+#define ADMINSWARNED_AT 5
+
/*
When somebody clicks a link in game, this Topic is called first.
It does the stuff in this proc and then is redirected to the Topic() proc for the src=[0xWhatever]
@@ -59,10 +66,38 @@
if(href_list["_src_"] == "chat")
return chatOutput.Topic(href, href_list)
- //Reduces spamming of links by dropping calls that happen during the delay period
- if(next_allowed_topic_time > world.time)
- return
- next_allowed_topic_time = world.time + TOPIC_SPAM_DELAY
+ // Rate limiting
+ var/mtl = 100 // 100 topics per minute
+ if (!holder) // Admins are allowed to spam click, deal with it.
+ var/minute = round(world.time, 600)
+ if (!topiclimiter)
+ topiclimiter = new(LIMITER_SIZE)
+ if (minute != topiclimiter[CURRENT_MINUTE])
+ topiclimiter[CURRENT_MINUTE] = minute
+ topiclimiter[MINUTE_COUNT] = 0
+ topiclimiter[MINUTE_COUNT] += 1
+ if (topiclimiter[MINUTE_COUNT] > mtl)
+ var/msg = "Your previous action was ignored because you've done too many in a minute."
+ if (minute != topiclimiter[ADMINSWARNED_AT]) //only one admin message per-minute. (if they spam the admins can just boot/ban them)
+ topiclimiter[ADMINSWARNED_AT] = minute
+ msg += " Administrators have been informed."
+ log_game("[key_name(src)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute")
+ message_admins("[ADMIN_LOOKUPFLW(usr)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute")
+ to_chat(src, "[msg]")
+ return
+
+ var/stl = 10 // 10 topics a second
+ if (!holder) // Admins are allowed to spam click, deal with it.
+ var/second = round(world.time, 10)
+ if (!topiclimiter)
+ topiclimiter = new(LIMITER_SIZE)
+ if (second != topiclimiter[CURRENT_SECOND])
+ topiclimiter[CURRENT_SECOND] = second
+ topiclimiter[SECOND_COUNT] = 0
+ topiclimiter[SECOND_COUNT] += 1
+ if (topiclimiter[SECOND_COUNT] > stl)
+ to_chat(src, "Your previous action was ignored because you've done too many in a second")
+ return
//search the href for script injection
if( findtext(href,"