diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index 9231f54b606..144e96ab7bc 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -413,3 +413,5 @@
config_entry_value = 40
min_val = 0
integer = FALSE // It is in hours, but just in case one wants to specify minutes.
+
+/datum/config_entry/flag/sdql_spells
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 4343f068741..0bc2e00f98f 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -195,7 +195,8 @@ GLOBAL_PROTECT(admin_verbs_debug)
#endif
/datum/admins/proc/create_or_modify_area,
/client/proc/check_timer_sources,
- /client/proc/toggle_cdn
+ /client/proc/toggle_cdn,
+ /client/proc/cmd_give_sdql_spell
)
GLOBAL_LIST_INIT(admin_verbs_possess, list(/proc/possess, /proc/release))
GLOBAL_PROTECT(admin_verbs_possess)
diff --git a/code/modules/admin/verbs/SDQL2/SDQL_2.dm b/code/modules/admin/verbs/SDQL2/SDQL_2.dm
index acdf6987ced..25026833c56 100644
--- a/code/modules/admin/verbs/SDQL2/SDQL_2.dm
+++ b/code/modules/admin/verbs/SDQL2/SDQL_2.dm
@@ -803,8 +803,8 @@ GLOBAL_DATUM_INIT(sdql2_vv_statobj, /obj/effect/statclick/sdql2_vv_all, new(null
for(var/arg in arguments)
new_args[++new_args.len] = SDQL_expression(source, arg)
if(object == GLOB) // Global proc.
- return superuser? (call("/proc/[procname]")(new_args)) : (WrapAdminProcCall(GLOBAL_PROC, procname, new_args))
- return superuser? (call(object, procname)(new_args)) : (WrapAdminProcCall(object, procname, new_args))
+ return superuser ? (call("/proc/[procname]")(arglist(new_args))) : (WrapAdminProcCall(GLOBAL_PROC, procname, new_args))
+ return superuser ? (call(object, procname)(arglist(new_args))) : (WrapAdminProcCall(object, procname, new_args))
/datum/sdql2_query/proc/SDQL_function_async(datum/object, procname, list/arguments, source)
set waitfor = FALSE
diff --git a/code/modules/admin/verbs/SDQL2/SDQL_spells_and_items.dm b/code/modules/admin/verbs/SDQL2/SDQL_spells_and_items.dm
new file mode 100644
index 00000000000..0d6f9b86ea0
--- /dev/null
+++ b/code/modules/admin/verbs/SDQL2/SDQL_spells_and_items.dm
@@ -0,0 +1,848 @@
+/obj/effect/proc_holder/spell/aimed/sdql
+ name = "Aimed SDQL Spell"
+ desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
+ var/query = "CALL visible_message(\"The spell fizzles!\") ON * IN TARGETS"
+ var/list/scratchpad = list() //Use this to store vars in between queries and casts.
+ projectile_type = /obj/projectile/sdql
+
+/obj/effect/proc_holder/spell/aimed/sdql/ready_projectile(obj/projectile/P, atom/target, mob/user, iteration)
+ var/obj/projectile/sdql/S = P
+ S.linked_spell = src
+ S.query = query
+
+/obj/projectile/sdql
+ name = "\improper SDQL projectile"
+ damage_type = STAMINA
+ nodamage = TRUE
+ damage = 0
+ var/query
+ var/obj/effect/proc_holder/spell/linked_spell
+
+/obj/projectile/sdql/on_hit(atom/target, blocked, pierce_hit)
+ . = ..()
+ var/mob/firer_mob = firer
+ process_spell_query(query, list(target), firer_mob, linked_spell)
+
+/obj/effect/proc_holder/spell/aoe_turf/sdql
+ name = "AoE SDQL Spell"
+ desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
+ var/query = "CALL visible_message(\"The spell fizzles!\") ON * IN TARGETS"
+ var/list/scratchpad = list() //Use this to store vars in between queries and casts.
+
+/obj/effect/proc_holder/spell/aoe_turf/sdql/cast(list/targets, mob/user)
+ process_spell_query(query, targets, user, src)
+
+/obj/effect/proc_holder/spell/cone/sdql
+ name = "Cone SDQL Spell"
+ desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
+ var/query = "CALL visible_message(\"The spell fizzles!\") ON * IN TARGETS"
+ var/list/targets = list()
+ var/list/scratchpad = list() //Use this to store vars in between queries and casts.
+
+/obj/effect/proc_holder/spell/cone/sdql/do_mob_cone_effect(mob/living/target_mob, level)
+ targets |= target_mob
+
+/obj/effect/proc_holder/spell/cone/sdql/do_obj_cone_effect(obj/target_obj, level)
+ targets |= target_obj
+
+/obj/effect/proc_holder/spell/cone/sdql/do_turf_cone_effect(turf/target_turf, level)
+ targets |= target_turf
+
+/obj/effect/proc_holder/spell/cone/sdql/cast(list/targets, mob/user)
+ . = ..()
+ process_spell_query(query, targets, user, src)
+ targets = list()
+
+/obj/effect/proc_holder/spell/cone/staggered/sdql
+ name = "Staggered Cone SDQL Spell"
+ desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
+ var/query = "CALL visible_message(\"The spell fizzles!\") ON * IN TARGETS"
+ var/list/targets = list()
+ var/list/scratchpad = list() //Use this to store vars in between queries and casts.
+
+/obj/effect/proc_holder/spell/cone/staggered/sdql/do_mob_cone_effect(mob/living/target_mob, level)
+ targets |= target_mob
+
+/obj/effect/proc_holder/spell/cone/staggered/sdql/do_obj_cone_effect(obj/target_obj, level)
+ targets |= target_obj
+
+/obj/effect/proc_holder/spell/cone/staggered/sdql/do_turf_cone_effect(turf/target_turf, level)
+ targets |= target_turf
+
+/obj/effect/proc_holder/spell/cone/staggered/sdql/do_cone_effects(list/target_turf_list, level)
+ . = ..()
+ process_spell_query(query, targets, usr, src)
+ targets = list()
+
+/obj/effect/proc_holder/spell/pointed/sdql
+ name = "Pointed SDQL Spell"
+ desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
+ var/query = "CALL visible_message(\"The spell fizzles!\") ON * IN TARGETS"
+ var/list/scratchpad = list() //Use this to store vars in between queries and casts.
+
+/obj/effect/proc_holder/spell/pointed/sdql/cast(list/targets, mob/user)
+ process_spell_query(query, targets, user, src)
+
+/obj/effect/proc_holder/spell/self/sdql
+ name = "Self SDQL Spell"
+ desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
+ var/query = "CALL visible_message(\"The spell fizzles!\") ON * IN TARGETS"
+ var/list/scratchpad = list() //Use this to store vars in between queries and casts.
+
+/obj/effect/proc_holder/spell/self/sdql/cast(list/targets, mob/user)
+ process_spell_query(query, list(user), user, src)
+
+/obj/effect/proc_holder/spell/targeted/sdql
+ name = "Targeted SDQL Spell"
+ desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
+ var/query = "CALL visible_message(\"The spell fizzles!\") ON * IN TARGETS"
+ var/list/scratchpad = list() //Use this to store vars in between queries and casts.
+
+/obj/effect/proc_holder/spell/targeted/sdql/cast(list/targets, mob/user)
+ process_spell_query(query, targets, user, src)
+
+/obj/effect/proc_holder/spell/targeted/touch/sdql
+ name = "Touch SDQL Spell"
+ desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
+ hand_path = /obj/item/melee/touch_attack/sdql
+ var/query = "CALL visible_message(\"The spell fizzles!\") ON * IN TARGETS"
+ var/list/scratchpad = list() //Use this to store vars in between queries and casts.
+ var/list/hand_var_overrides = list() //The touch attack has its vars changed to the ones put in this list.
+
+/obj/effect/proc_holder/spell/targeted/touch/sdql/ChargeHand(mob/living/carbon/user)
+ if(..())
+ for(var/V in hand_var_overrides)
+ if(V in attached_hand.vars)
+ attached_hand.vv_edit_var(V, hand_var_overrides[V])
+ user.update_inv_hands()
+
+
+/obj/item/melee/touch_attack/sdql
+ name = "\improper SDQL touch attack"
+ desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
+ catchphrase = "ADMINS WERE LAZY!!"
+
+/obj/item/melee/touch_attack/sdql/afterattack(atom/target, mob/user, proximity)
+ var/obj/effect/proc_holder/spell/targeted/touch/sdql/spell = attached_spell
+ process_spell_query(spell.query, list(target), user, spell)
+ . = ..()
+
+//Returns the address of x without the square brackets around it.
+#define RAW_ADDRESS(x) copytext("\ref[x]",2,-1)
+
+/proc/process_spell_query(query_text, list/targets, mob/user, source)
+ if(!CONFIG_GET(flag/sdql_spells))
+ return
+ if(!length(query_text))
+ return
+ var/message_query = query_text
+ var/list/targets_and_user_list = targets+user
+ var/targets_and_user_string = ref_list(targets_and_user_list)
+ var/targets_string = ref_list(targets)
+ query_text = replacetextEx_char(query_text, "TARGETS_AND_USER", "[targets_and_user_string]")
+ message_query = replacetext_char(message_query, "TARGETS_AND_USER", (targets_and_user_list.len > 3) ? "\[[targets_and_user_list.len] items]" : targets_and_user_string)
+ query_text = replacetextEx_char(query_text, "USER", "{[RAW_ADDRESS(user)]}")
+ message_query = replacetextEx_char(message_query, "USER", "{[RAW_ADDRESS(user)]}")
+ query_text = replacetextEx_char(query_text, "TARGETS", "[targets_string]")
+ message_query = replacetextEx_char(message_query, "TARGETS", (targets.len > 3) ? "\[[targets.len] items]" : targets_string)
+ query_text = replacetextEx_char(query_text, "SOURCE", "{[RAW_ADDRESS(source)]}")
+ message_query = replacetextEx_char(message_query, "SOURCE", "{[RAW_ADDRESS(source)]}")
+ var/query_message = "[key_name(user)] executed SDQL query(s): \"[message_query]\" using a player query caller."
+ message_admins(query_message)
+ log_game(query_message)
+ var/list/query_list = SDQL2_tokenize(query_text)
+ if(!query_list.len)
+ return
+ var/list/querys = SDQL_parse(query_list)
+ if(!querys.len)
+ return
+
+ var/list/datum/sdql2_query/running = list()
+ var/list/datum/sdql2_query/waiting_queue = list() //Sequential queries queue.
+
+ for(var/list/query_tree in querys)
+ var/datum/sdql2_query/query = new /datum/sdql2_query(query_tree, SU = TRUE, admin_interact = FALSE)
+ if(QDELETED(query))
+ continue
+ waiting_queue += query
+
+ var/datum/sdql2_query/query = popleft(waiting_queue)
+ running += query
+ query.ARun()
+
+ var/finished = FALSE
+ do
+ CHECK_TICK
+ finished = TRUE
+ for(var/i in running)
+ query = i
+ if(QDELETED(query))
+ running -= query
+ continue
+ else if(query.state != SDQL2_STATE_IDLE)
+ finished = FALSE
+ if(query.state == SDQL2_STATE_ERROR)
+ running -= query
+ else
+ if(query.finished)
+ qdel(query)
+ if(waiting_queue.len)
+ finished = FALSE
+ var/datum/sdql2_query/next_query = popleft(waiting_queue)
+ running += next_query
+ next_query.ARun()
+ else
+ running -= query
+ while(!finished)
+
+/proc/ref_list(list/L)
+ if(!L.len)
+ return "\[]"
+ var/ret = "\["
+ for(var/i in 1 to L.len-1)
+ ret += "{[RAW_ADDRESS(L[i])]},"
+ ret += "{[RAW_ADDRESS(L[L.len])]}]"
+ return ret
+
+#undef RAW_ADDRESS
+
+/client/proc/cmd_give_sdql_spell(mob/target in GLOB.mob_list)
+ set category = "Admin.Debug"
+ set name = "Give SDQL spell"
+ if(CONFIG_GET(flag/sdql_spells))
+ var/datum/give_sdql_spell/ui = new(usr, target)
+ ui.ui_interact(usr)
+ else
+ to_chat(usr, "SDQL spells are disabled.")
+
+
+/datum/give_sdql_spell
+ var/client/user
+ var/mob/living/target_mob
+ var/spell_type
+ var/list/saved_vars = list()
+ var/list/list_vars = list("scratchpad" = list())
+ var/alert
+
+ //This list contains all the vars that it should be okay to edit from the menu
+ var/static/list/editable_spell_vars = list(
+ "action_background_icon_state",
+ "action_icon_state",
+ "action_icon",
+ "active_msg",
+ "aim_assist",
+ "antimagic_allowed",
+ "base_icon_state",
+ "centcom_cancast",
+ "charge_max",
+ "charge_type",
+ "clothes_req",
+ "cone_level",
+ "cult_req",
+ "deactive_msg",
+ "desc",
+ "drawmessage",
+ "dropmessage",
+ "hand_var_overrides",
+ "holder_var_amount",
+ "holder_var_type",
+ "human_req",
+ "include_user",
+ "inner_radius",
+ "invocation_emote_self",
+ "invocation_type",
+ "invocation",
+ "max_targets",
+ "message",
+ "name",
+ "nonabstract_req",
+ "overlay_icon_state",
+ "overlay_icon",
+ "overlay_lifespan",
+ "overlay",
+ "phase_allowed",
+ "player_lock",
+ "projectile_amount",
+ "projectile_var_overrides",
+ "projectiles_per_fire",
+ "query",
+ "random_target_priority",
+ "random_target",
+ "range",
+ "ranged_mousepointer",
+ "respect_density",
+ "selection_type",
+ "self_castable",
+ "smoke_amt",
+ "smoke_spread",
+ "sound",
+ "sparks_amt",
+ "sparks_spread",
+ "stat_allowed",
+ "still_recharging_message",
+ "target_ignore_prev",
+ )
+
+ //If a spell creates a datum with vars it overrides, this list should contain an association with the supertype of the created datum
+ var/static/list/special_list_vars = list(
+ "projectile_var_overrides" = list(
+ "supertype" = /obj/projectile,
+ "type" = /obj/projectile/sdql,
+ ),
+ "hand_var_overrides" = list(
+ "supertype" = /obj/item/melee/touch_attack,
+ "type" = /obj/item/melee/touch_attack/sdql,
+ ),
+ )
+
+ var/static/list/static_data
+
+ //base64 representations of any icons that may need to be displayed
+ var/action_icon_base64
+ var/projectile_icon_base64
+ var/hand_icon_base64
+ var/overlay_icon_base64
+ var/mouse_icon_base64
+
+/datum/give_sdql_spell/New(_user, target)
+ if(!CONFIG_GET(flag/sdql_spells))
+ to_chat(_user, "SDQL spells are disabled.")
+ qdel(src)
+ return
+ user = CLIENT_FROM_VAR(_user)
+
+ if(!isliving(target))
+ alert("Invalid mob")
+ return
+ target_mob = target
+
+/datum/give_sdql_spell/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "SDQLSpellMenu", "Give SDQL Spell")
+ ui.open()
+ ui.set_autoupdate(FALSE)
+
+/datum/give_sdql_spell/ui_state(mob/user)
+ return GLOB.admin_state
+
+/datum/give_sdql_spell/ui_status(mob/user, datum/ui_state/state)
+ if(QDELETED(target_mob))
+ return UI_CLOSE
+ return ..()
+
+/datum/give_sdql_spell/ui_close(mob/user)
+ qdel(src)
+
+/datum/give_sdql_spell/ui_data(mob/user, params)
+ var/list/data = list()
+ data["type"] = spell_type
+ data["saved_vars"] = saved_vars
+ data["list_vars"] = list_vars
+ data["action_icon"] = action_icon_base64
+ data["projectile_icon"] = projectile_icon_base64
+ data["hand_icon"] = hand_icon_base64
+ data["overlay_icon"] = overlay_icon_base64
+ data["mouse_icon"] = mouse_icon_base64
+ data["alert"] = alert
+ alert = ""
+ return data
+
+/datum/give_sdql_spell/ui_static_data(mob/user)
+ if(!static_data)
+ static_data = list(
+ "types" = list("aimed", "aoe_turf", "cone", "cone/staggered", "pointed", "self", "targeted", "targeted/touch"),
+ "tooltips" = list(
+ "query" = "The SDQL query that is executed. Certain keywords are specific to SDQL spell queries.\n\
+ $type\n\
+ USER is replaced with a reference to the user of the spell.\n\
+ TARGETS_AND_USER is replaced with the combined references from TARGETS and USER.\n\
+ SOURCE is replaced with a reference to this spell, allowing you to refer to and edit variables within it.\n\
+ You can use the list variable \"scratchpad\" to store variables between individual queries within the same cast or between multiple casts.",
+ "query_aimed" = "TARGETS is replaced with a list containing a reference to the atom hit by the fired projectile.",
+ "query_aoe_turf" = "TARGETS is replaced with a list containing references to every atom in the spell's area of effect.",
+ "query_cone" = "TARGETS is replaced with a list containing references to every atom in the cone produced by the spell.",
+ "query_cone/staggered" = "The query will be executed once for every level of the cone produced by the spell.\n\
+ TARGETS is replaced with a list containing references to every atom in the given level of the cone.",
+ "query_pointed" = "TARGETS is replaced with a list containing a reference to the targeted atom.",
+ "query_self" = "TARGETS is replaced with a list containing a reference to the caster.",
+ "query_targeted" = "TARGETS is replaced with a list containing a reference(s) to the targeted mob(s).",
+ "query_targeted_touch" = "TARGETS is replaced with a list containing a reference to the atom hit with the touch attack.",
+ "charge_type" = "How the spell's charge works. This affects how charge_max is used.\n\
+ When set to \"recharge\", charge_max is the time in deciseconds between casts of the spell.\n\
+ When set to \"charges\", the user can only use the spell a number of times equal to charge_max.\n\
+ When set to \"holder_var\", charge_max is not used. holder_var_type and holder_var_amount are used instead.\n",
+ "holder_var_type" = "When charge_type is set to \"holder_var\", this is the name of the var that is modified each time the spell is cast.\n\
+ If this is set to \"bruteloss\", \"fireloss\", \"toxloss\", or \"oxyloss\", the user will take the corresponding damage.\n\
+ If this is set to \"stun\", \"knockdown\", \"paralyze\", \"immobilize\", or \"unconscious\", the user will suffer the corresponding status effect.\n\
+ If this is set to anything else, the variable with the appropriate name will be modified.",
+ "holder_var_amount" = "The amount of damage taken, the duration of status effect inflicted, or the change made to any other variable.",
+ "clothes_req" = "Whether the user has to be wearing wizard robes to cast the spell.",
+ "cult_req" = "Whether the user has to be wearing cult robes to cast the spell.",
+ "human_req" = "Whether the user has to be a human to cast the spell. Redundant when clothes_req is true.",
+ "nonabstract_req" = "If this is true, the spell cannot be cast by brains and pAIs.",
+ "stat_allowed" = "Whether the spell can be cast if the user is unconscious or dead.",
+ "phase_allowed" = "Whether the spell can be cast while the user is jaunting or bloodcrawling.",
+ "antimagic_allowed" = "Whether the spell can be cast while the user is affected by anti-magic effects.",
+ "invocation_type" = "How the spell is invoked.\n\
+ When set to \"whisper\", the user whispers the invocation, as if with the whisper verb.\n\
+ When set to \"shout\", the user says the invocation, as if with the say verb.\n\
+ When set to \"emote\", a visible message is produced.",
+ "invocation" = "What the user says, whispers, or emotes when using the spell.",
+ "invocation_emote_self" = "What the user sees in their own chat when they use the spell.",
+ "selection_type" = "Whether the spell can target any mob in range, or only visible mobs in range.",
+ "range" = "The spell's range, in tiles.",
+ "message" = "What mobs affected by the spell see in their chat.\n\
+ Keep in mind, just because a mob is affected by the spell doesn't mean the query will have any effect on them.",
+ "player_lock" = "If false, simple mobs can use the spell.",
+ "overlay" = "Whether an overlay is drawn atop atoms affectecd by the spell.\n\
+ Keep in mind, just because an atom is affected by the spell doesn't mean the query will have any effect on it.",
+ "overlay_lifetime" = "The amount of time in deciseconds the overlay will persist.",
+ "sparks_spread" = "Whether the spell produces sparks when cast.",
+ "smoke_spread" = "The kind of smoke, if any, the spell produces when cast.",
+ "centcom_cancast" = "If true, the spell can be cast on the centcom Z-level.",
+ "max_targets" = "The maximum number of mobs the spell can target.",
+ "target_ignore_prev" = "If false, the same mob can be targeted multiple times.",
+ "include_user" = "If true, the user can target themselves with the spell.",
+ "random_target" = "If true, the spell will target a random mob(s) in range.",
+ "random_target_priority" = "Whether the spell will target random mobs in range or the closest mobs in range.",
+ "inner_radius" = "If this is a non-negative number, the spell will not affect atoms within that many tiles of the user.",
+ "ranged_mousepointer" = "The icon used for the mouse when aiming the spell.",
+ "deactive_mesg" = "The message the user sees when canceling the spell.",
+ "active_msg" = "The message the user sees when activating the spell.",
+ "projectile_amount" = "The maximum number of projectiles the user can fire with each cast of the spell.",
+ "projectiles_per_fire" = "The amount of projectiles fired with each click of the mouse.",
+ "projectile_var_overrides" = "The fired projectiles will have the appropriate variables overridden by the corresponding values in this associative list.\n\
+ You should probably set \"name\", \"icon\", and \"icon_state\".\n\
+ Refer to code/modules/projectiles/projectile.dm to see what other vars you can override.",
+ "cone_level" = "How many tiles out the cone will extend.",
+ "respect_density" = "If true, the cone produced by the spell is blocked by walls.",
+ "self_castable" = "If true, the user can cast the spell on themselves.",
+ "aim_assist" = "If true, the spell has turf-based aim assist.",
+ "drawmessage" = "The message the user sees when activating the spell.",
+ "dropmessage" = "The message the user sees when canceling the spell.",
+ "hand_var_overrides" = "The touch attack will have the appropriate variables overridden by the corresponding values in this associative list.\n\
+ You should probably set \"name\", \"desc\", \"catchphrase\", \"on_use_sound\" \"icon\", \"icon_state\", and \"inhand_icon_state\".\n\
+ Refer to code/modules/spells/spell_types/godhand.dm for see what other vars you can override.",
+ "scratchpad" = "This list can be used to store variables between individual queries within the same cast or between casts.\n\
+ You can declare variables from this menu for convenience. To access this list in a query, use the identifier \"SOURCE.scratchpad\".\n\
+ Refer to the _list procs defined in code/modules/admin/verbs/SDQL2/SDQL_2_wrappers.dm for information on how to modify and edit list vars from within a query.",
+ ),
+ )
+ return static_data
+
+#define LIST_VAR_FLAGS_TYPED 1
+#define LIST_VAR_FLAGS_NAMED 2
+
+/datum/give_sdql_spell/ui_act(action, params, datum/tgui/ui)
+ if(..())
+ return
+ . = TRUE
+ switch(action)
+ if("type")
+ spell_type = params["path"]
+ var/path = text2path("/obj/effect/proc_holder/spell/[spell_type]/sdql")
+ var/datum/sample = new path
+ if(spell_type)
+ for(var/V in sample.vars)
+ if(V in editable_spell_vars)
+ if(islist(sample.vars[V]))
+ list_vars += list("[V]" = list())
+ if(V in special_list_vars)
+ var/subpath = special_list_vars[V]["type"]
+ var/datum/subsample = new subpath
+ if("icon" in sample.vars)
+ icon_needs_updating("[V]/icon")
+ qdel(subsample)
+ else
+ saved_vars[V] = sample.vars[V]
+ icon_needs_updating(V)
+ qdel(sample)
+ if("variable")
+ var/V = params["name"]
+ if(V == "holder_var_type")
+ if(!holder_var_validate(params["value"]))
+ return
+ saved_vars[V] = params["value"]
+ icon_needs_updating(V)
+ if("bool_variable")
+ saved_vars[params["name"]] = !saved_vars[params["name"]]
+ if("list_variable_add")
+ if(params["list"] in list_vars)
+ if(params["list"] in special_list_vars)
+ var/superpath = special_list_vars[params["list"]]["supertype"]
+ var/path = special_list_vars[params["list"]]["type"]
+ var/datum/supersample = new superpath
+ var/datum/sample = new path
+ var/list/choosable_vars = map_var_list((sample.vars&supersample.vars)-list_vars[params["list"]], sample)
+ var/chosen_var = input(user, "Select variable to add.", "Add SDQL Spell", null) as null|anything in sortList(choosable_vars)
+ if(chosen_var)
+ if(islist(sample.vars[choosable_vars[chosen_var]]))
+ list_vars[params["list"]] += list("[choosable_vars[chosen_var]]" = list("type" = "list", "value" = null, "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED))
+ list_vars |= list("[params["list"]]/[choosable_vars[chosen_var]]" = list())
+ else if(isnum(sample.vars[choosable_vars[chosen_var]]))
+ list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "num", "value" = sample.vars[choosable_vars[chosen_var]], "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED)
+ else if(ispath(sample.vars[choosable_vars[chosen_var]]))
+ list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "path", "value" = sample.vars[choosable_vars[chosen_var]], "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED)
+ else if(isicon(sample.vars[choosable_vars[chosen_var]]))
+ list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "icon", "value" = sample.vars[choosable_vars[chosen_var]], "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED)
+ else if(istext(sample.vars[choosable_vars[chosen_var]]) || isfile(sample.vars[choosable_vars[chosen_var]]))
+ list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "string", "value" = sample.vars[choosable_vars[chosen_var]], "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED)
+ else if(isnull(sample.vars[choosable_vars[chosen_var]]))
+ list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "num", "value" = 0, "flags" = LIST_VAR_FLAGS_NAMED)
+ alert = "Could not determine the type for [params["list"]]/[choosable_vars[chosen_var]]! Be sure to set it correctly, or you may cause unnecessary runtimes!"
+ else
+ alert = "[params["list"]]/[choosable_vars[chosen_var]] is not of a supported type!"
+ icon_needs_updating("[params["list"]]/[choosable_vars[chosen_var]]")
+ qdel(sample)
+ qdel(supersample)
+ else
+ if(!("new_var" in list_vars[params["list"]]))
+ list_vars[params["list"]] += list("new_var" = list("type" = "num", "value" = 0, "flags" = 0))
+ else
+ alert = "Rename or remove [params["list"]]/new_var before attempting to add another variable to this list!"
+ if("list_variable_remove")
+ remove_list_var(params["list"], params["name"])
+ if("list_variable_rename")
+ rename_list_var(params["list"], params["name"], params["new_name"])
+ if("list_variable_change_type")
+ change_list_var_type(params["list"], params["name"], params["value"])
+ if("list_variable_change_value")
+ set_list_var(params["list"], params["name"], params["value"])
+ icon_needs_updating("[params["list"]]/[params["name"]]")
+ if("list_variable_change_bool")
+ toggle_list_var(params["list"], params["name"])
+ if("save")
+ var/f = file("data/TempSpellUpload")
+ fdel(f)
+ WRITE_FILE(f, json_encode(list("type" = spell_type, "vars" = saved_vars, "list_vars" = list_vars)))
+ user << ftp(f,"[replacetext_char(saved_vars["name"], " ", "_")].json")
+ if("load")
+ var/spell_file = input("Pick spell json file:", "File") as null|file
+ if(!spell_file)
+ return
+ var/filedata = file2text(spell_file)
+ var/json = json_decode(filedata)
+ if(!json)
+ alert = "JSON decode error!"
+ return
+ if(load_from_json(json))
+ icon_needs_updating("everything")
+ else
+ alert = "Malformed/Outdated file!"
+ return
+ if("confirm")
+ give_spell()
+ ui.close()
+
+/datum/give_sdql_spell/proc/load_from_json(json)
+ if(!(("type" in json) && ("vars" in json) && ("list_vars" in json)))
+ return FALSE
+ var/temp_type = json["type"]
+ var/datum/D = text2path("/obj/effect/proc_holder/spell/[temp_type]/sdql")
+ if(!ispath(D))
+ return FALSE
+ if(!islist(json["vars"]))
+ return FALSE
+ if(!islist(json["list_vars"]))
+ return FALSE
+ var/list/temp_vars = json["vars"]
+ var/list/temp_list_vars = json["list_vars"]
+ D = new D
+ . = TRUE
+ for(var/V in temp_vars)
+ if(!istext(V))
+ . = FALSE
+ break
+ if(!(V in editable_spell_vars))
+ . = FALSE
+ break
+ if(!(V in D.vars))
+ . = FALSE
+ break
+ if(islist(D.vars[V]))
+ . = FALSE
+ break
+ if(istext(D.vars[V]) || isicon(D.vars[V]) || ispath(D.vars[V]))
+ if(!istext(temp_vars[V]))
+ . = FALSE
+ break
+ if(isnum(D.vars[V]))
+ if(!isnum(temp_vars[V]))
+ . = FALSE
+ break
+ if(.)
+ for(var/V in temp_list_vars)
+ if(!islist(temp_list_vars[V]))
+ . = FALSE
+ break
+ if((V in special_list_vars) && (V in D.vars))
+ var/datum/sample = special_list_vars[V]["type"]
+ sample = new sample
+ for(var/W in temp_list_vars[V])
+ if(!istext(W))
+ . = FALSE
+ break
+ if(!islist(temp_list_vars[V][W]))
+ . = FALSE
+ break
+ if(!(("type" in temp_list_vars[V][W]) && ("value" in temp_list_vars[V][W]) && ("flags" in temp_list_vars[V][W])))
+ . = FALSE
+ break
+ if(!isnum(temp_list_vars[V][W]["flags"]) || (temp_list_vars[V][W]["flags"] & LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED) == LIST_VAR_FLAGS_TYPED)
+ . = FALSE
+ break
+ if(!istext(temp_list_vars[V][W]["type"]))
+ . = FALSE
+ break
+ if(!(temp_list_vars[V][W]["flags"] & LIST_VAR_FLAGS_TYPED))
+ if(!isnull(sample.vars[W]))
+ . = FALSE
+ break
+ else
+ switch(temp_list_vars[V][W]["type"])
+ if("list")
+ if(!islist(sample.vars[W]))
+ . = FALSE
+ break
+ if(!("[V]/[W]" in temp_list_vars))
+ . = FALSE
+ break
+ if("num")
+ if(isnum(temp_list_vars[V][W]["value"]))
+ if(!(isnum(sample.vars[W])))
+ . = FALSE
+ break
+ else
+ . = FALSE
+ break
+ if("string")
+ if(istext(temp_list_vars[V][W]["value"]))
+ if(!(istext(sample.vars[W]) || isfile(sample.vars[W])))
+ . = FALSE
+ break
+ else
+ . = FALSE
+ break
+ if("path")
+ if(istext(temp_list_vars[V][W]["value"]))
+ if(!(ispath(sample.vars[W])))
+ . = FALSE
+ break
+ else
+ . = FALSE
+ break
+ if("icon")
+ if(istext(temp_list_vars[V][W]["value"]))
+ if(!(isicon(sample.vars[W])))
+ . = FALSE
+ break
+ else
+ . = FALSE
+ break
+ qdel(sample)
+ if(!.)
+ break
+ qdel(D)
+ if(.)
+ spell_type = temp_type
+ saved_vars = temp_vars
+ list_vars = temp_list_vars
+
+#undef LIST_VAR_FLAGS_TYPED
+#undef LIST_VAR_FLAGS_NAMED
+
+/datum/give_sdql_spell/proc/map_var_list(list/L, datum/D)
+ var/list/ret = list()
+ for(var/V in L)
+ if(V in D.vars)
+ ret["[V] = [string_rep(D.vars[V])]"] = V
+ return ret
+
+/datum/give_sdql_spell/proc/string_rep(V)
+ if(istext(V) || isfile(V) || isicon(V))
+ return "\"[V]\""
+ else if(isnull(V))
+ return "null"
+ else
+ return "[V]"
+
+/datum/give_sdql_spell/proc/holder_var_validate(V)
+ switch(V)
+ if("bruteloss", "fireloss", "toxloss", "oxyloss", "stun", "knockdown", "paralyze", "unconscious")
+ return TRUE
+ else
+ if(V in target_mob.vars)
+ if(!isnum(target_mob.vars[V]))
+ alert = "[target_mob.type]/[V] is not a number!"
+ return FALSE
+ else
+ return TRUE
+ else
+ alert = "[target_mob.type] has no such variable [V]!"
+ return FALSE
+
+/datum/give_sdql_spell/proc/icon_needs_updating(var_name)
+ switch(var_name)
+ if("action_icon", "action_icon_state", "action_background_icon_state")
+ var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing")
+ var/image/out_image = image('icons/mob/actions/backgrounds.dmi', null, saved_vars["action_background_icon_state"])
+ var/overlay_icon = icon(saved_vars["action_icon"], saved_vars["action_icon_state"])
+ out_image.overlays += image(overlay_icon)
+ out_icon.Insert(getFlatIcon(out_image, no_anim = TRUE))
+ action_icon_base64 = icon2base64(out_icon)
+
+ if("projectile_var_overrides/icon", "projectile_var_overrides/icon_state")
+ var/atom/A = /obj/projectile/sdql
+ var/icon = initial(A.icon)
+ var/icon_state = initial(A.icon_state)
+ if(list_vars["projectile_var_overrides"]["icon"])
+ icon = list_vars["projectile_var_overrides"]["icon"]["value"]
+ if(list_vars["projectile_var_overrides"]["icon_state"])
+ icon_state = list_vars["projectile_var_overrides"]["icon_state"]["value"]
+ var/icon/out_icon = icon(icon, icon_state, frame = 1)
+ projectile_icon_base64 = icon2base64(out_icon)
+
+ if("hand_var_overrides/icon", "hand_var_overrides/icon_state")
+ var/atom/A = /obj/item/melee/touch_attack/sdql
+ var/icon = initial(A.icon)
+ var/icon_state = initial(A.icon_state)
+ if(list_vars["hand_var_overrides"]["icon"])
+ icon = list_vars["hand_var_overrides"]["icon"]["value"]
+ if(list_vars["hand_var_overrides"]["icon_state"])
+ icon_state = list_vars["hand_var_overrides"]["icon_state"]["value"]
+ var/icon/out_icon = icon(icon, icon_state, frame = 1)
+ hand_icon_base64 = icon2base64(out_icon)
+
+ if("overlay", "overlay_icon", "overlay_icon_state")
+ var/icon/out_icon = icon(saved_vars["overlay_icon"], saved_vars["overlay_icon_state"], frame = 1)
+ overlay_icon_base64 = icon2base64(out_icon)
+
+ if("ranged_mousepointer")
+ var/icon/out_icon = icon(saved_vars["ranged_mousepointer"], frame = 1)
+ mouse_icon_base64 = icon2base64(out_icon)
+
+ if("everything")
+ var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing")
+ var/image/out_image = image('icons/mob/actions/backgrounds.dmi', null, saved_vars["action_background_icon_state"])
+ var/overlay_icon = icon(saved_vars["action_icon"], saved_vars["action_icon_state"])
+ out_image.overlays += image(overlay_icon)
+ out_icon.Insert(getFlatIcon(out_image, no_anim = TRUE))
+ action_icon_base64 = icon2base64(out_icon)
+ if(list_vars["projectile_var_overrides"])
+ var/atom/A = /obj/projectile/sdql
+ var/icon = initial(A.icon)
+ var/icon_state = initial(A.icon_state)
+ if(list_vars["projectile_var_overrides"]["icon"])
+ icon = list_vars["projectile_var_overrides"]["icon"]["value"]
+ if(list_vars["projectile_var_overrides"]["icon_state"])
+ icon_state = list_vars["projectile_var_overrides"]["icon_state"]["value"]
+ out_icon = icon(icon, icon_state, frame = 1)
+ projectile_icon_base64 = icon2base64(out_icon)
+ if(list_vars["hand_var_overrides"])
+ var/atom/A = /obj/item/melee/touch_attack/sdql
+ var/icon = initial(A.icon)
+ var/icon_state = initial(A.icon_state)
+ if(list_vars["hand_var_overrides"]["icon"])
+ icon = list_vars["hand_var_overrides"]["icon"]["value"]
+ if(list_vars["hand_var_overrides"]["icon_state"])
+ icon_state = list_vars["hand_var_overrides"]["icon_state"]["value"]
+ out_icon = icon(icon, icon_state, frame = 1)
+ hand_icon_base64 = icon2base64(out_icon)
+ out_icon = icon(saved_vars["overlay_icon"], saved_vars["overlay_icon_state"], frame = 1)
+ overlay_icon_base64 = icon2base64(out_icon)
+ out_icon = icon(saved_vars["ranged_mousepointer"], frame = 1)
+ mouse_icon_base64 = icon2base64(out_icon)
+
+/datum/give_sdql_spell/proc/toggle_list_var(list_name, list_var)
+ if(list_name in list_vars)
+ if(list_var in list_vars[list_name])
+ list_vars[list_name][list_var]["value"] = !list_vars[list_name][list_var]["value"]
+
+/datum/give_sdql_spell/proc/set_list_var(list_name, list_var, value)
+ if(list_name in list_vars)
+ if(list_var in list_vars[list_name])
+ list_vars[list_name][list_var]["value"] = value
+
+/datum/give_sdql_spell/proc/rename_list_var(list_name, list_var, new_name)
+ if(list_var == new_name)
+ return
+ if(list_name in list_vars)
+ var/list/L = list_vars[list_name]
+ var/ind = L.Find(list_var)
+ if(ind)
+ if(new_name in list_vars[list_name])
+ alert = "There is already a variable named [new_name] in [list_name]!"
+ else
+ list_vars[list_name][ind] = new_name
+
+/datum/give_sdql_spell/proc/change_list_var_type(list_name, list_var, var_type)
+ if(list_name in list_vars)
+ if(list_var in list_vars[list_name])
+ if(list_vars[list_name][list_var]["type"] == "list" && var_type != "list")
+ purge_list_var("[list_name]/[list_var]")
+ list_vars[list_name][list_var]["type"] = var_type
+ switch(var_type)
+ if("string", "path")
+ list_vars[list_name][list_var]["value"] = ""
+ if("bool", "num")
+ list_vars[list_name][list_var]["value"] = 0
+ if("list")
+ list_vars[list_name][list_var]["value"] = null
+ list_vars |= list("[list_name]/[list_var]" = list())
+
+/datum/give_sdql_spell/proc/remove_list_var(list_name, list_var)
+ if(list_name in list_vars)
+ var/list/L = list_vars[list_name]
+ var/ind = L.Find(list_var)
+ if(ind)
+ if(list_vars[list_name][list_var]["type"] == "list")
+ purge_list_var("[list_name]/[list_var]")
+ L.Cut(ind, ind+1)
+ list_vars[list_name] = L
+
+/datum/give_sdql_spell/proc/purge_list_var(list_name)
+ var/ind = list_vars.Find(list_name)
+ if(ind)
+ for(var/V in list_vars[list_name])
+ if(list_vars[list_name][V]["type"] == "list")
+ purge_list_var("[list_name]/[V]")
+ list_vars.Cut(ind, ind+1)
+
+/datum/give_sdql_spell/proc/generate_list_var(list_name)
+ if(!(list_name in list_vars))
+ return null
+ var/list/ret = list()
+ for(var/V in list_vars[list_name])
+ if(list_vars[list_name][V]["type"] == "list")
+ ret[V] = generate_list_var("[list_name]/[V]")
+ else if(list_vars[list_name][V]["type"] == "path")
+ ret[V] = text2path(list_vars[list_name][V]["value"])
+ else if(list_vars[list_name][V]["type"] == "icon")
+ ret[V] = icon(list_vars[list_name][V]["value"])
+ else
+ ret[V] = list_vars[list_name][V]["value"]
+ return ret
+
+/datum/give_sdql_spell/proc/give_spell()
+ var/path = text2path("/obj/effect/proc_holder/spell/[spell_type]/sdql")
+ var/obj/effect/proc_holder/spell/new_spell = new path
+ for(var/V in saved_vars+list_vars)
+ if(V in new_spell.vars)
+ if(islist(new_spell.vars[V]))
+ var/list/list_var = generate_list_var(V)
+ if(list_var)
+ new_spell.vv_edit_var(V, list_var)
+ else if(isicon(new_spell.vars[V]))
+ new_spell.vv_edit_var(V, icon(saved_vars[V]))
+ else
+ new_spell.vv_edit_var(V, saved_vars[V])
+
+ //delete and recreate the action so the overriden vars are respected by the action button
+ qdel(new_spell.action)
+ new_spell.action = new new_spell.base_action(new_spell)
+ if(target_mob.mind)
+ target_mob.mind.AddSpell(new_spell)
+ else
+ target_mob.AddSpell(new_spell)
+ to_chat(user, "Spells given to mindless mobs will not be transferred in mindswap or cloning!")
diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm
index 14509b0e8c7..645ac9ce003 100644
--- a/code/modules/spells/spell.dm
+++ b/code/modules/spells/spell.dm
@@ -1,5 +1,5 @@
-#define TARGET_CLOSEST 1
-#define TARGET_RANDOM 2
+#define TARGET_CLOSEST 0
+#define TARGET_RANDOM 1
/obj/effect/proc_holder
diff --git a/config/game_options.txt b/config/game_options.txt
index e66ffa944ea..de365700443 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -596,3 +596,8 @@ MAXFINE 2000
## How many played hours of DRONE_REQUIRED_ROLE required to be a Maintenance Done
#DRONE_ROLE_PLAYTIME 40
+
+## Uncomment to enable SDQL spells
+## Warning: SDQL is a powerful tool and can break many things or expose security sensitive information.
+## Giving players access to it has major security concerns, be careful and deliberate when using this feature.
+#SDQL_SPELLS
diff --git a/tgstation.dme b/tgstation.dme
index e1e320a69f7..fafe3348b85 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1605,6 +1605,7 @@
#include "code\modules\admin\verbs\SDQL2\SDQL_2.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2_parser.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2_wrappers.dm"
+#include "code\modules\admin\verbs\SDQL2\SDQL_spells_and_items.dm"
#include "code\modules\admin\view_variables\admin_delete.dm"
#include "code\modules\admin\view_variables\debug_variables.dm"
#include "code\modules\admin\view_variables\filterrific.dm"
diff --git a/tgui/packages/tgui/interfaces/SDQLSpellMenu.js b/tgui/packages/tgui/interfaces/SDQLSpellMenu.js
new file mode 100644
index 00000000000..3ee27ba3416
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/SDQLSpellMenu.js
@@ -0,0 +1,778 @@
+import { useBackend } from '../backend';
+import { Box, Button, Collapsible, Dropdown, Input, NumberInput, Section, Stack, Tooltip } from '../components';
+import { Window } from '../layouts';
+
+/**
+ * Gets a list of objects that encode the parameters for the variables relevant
+ * to the passed spell type.
+ * @param type The type of the spell.
+ * @returns A list of objects. Each object contains the name of the variable,
+ * the variable's data type,
+ * what options are valid (if the variable is an enum),
+ * and what the variable's default value should be.
+ */
+const typevars = (type) => {
+ let ret = [
+ { name: 'name', type: 'string', options: null, default_value: '' },
+ { name: 'desc', type: 'string', options: null, default_value: '' },
+ { name: 'query', type: 'string', options: null, default_value: '' },
+ { name: 'action_icon', type: 'string', options: null, default_value: '' },
+ {
+ name: 'action_icon_state',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'action_background_icon_state',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ { name: 'sound', type: 'string', options: null, default_value: '' },
+ {
+ name: 'charge_type',
+ type: 'string_enum',
+ options: ['recharge', 'charges', 'holder_var'],
+ default_value: 'recharge',
+ },
+ { name: 'charge_max', type: 'int', options: null, default_value: 100 },
+ {
+ name: 'still_recharging_message',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'holder_var_type',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'holder_var_amount',
+ type: 'int',
+ options: null,
+ default_value: '',
+ },
+ { name: 'clothes_req', type: 'bool', options: null, default_value: false },
+ { name: 'cult_req', type: 'bool', options: null, default_value: false },
+ { name: 'human_req', type: 'bool', options: null, default_value: false },
+ {
+ name: 'nonabstract_req',
+ type: 'bool',
+ options: null,
+ default_value: false,
+ },
+ { name: 'stat_allowed', type: 'bool', options: null, default_value: false },
+ {
+ name: 'phase_allowed',
+ type: 'bool',
+ options: null,
+ default_value: false,
+ },
+ {
+ name: 'antimagic_allowed',
+ type: 'bool',
+ options: null,
+ default_value: false,
+ },
+ {
+ name: 'invocation_type',
+ type: 'string_enum',
+ options: ['none', 'whisper', 'emote', 'shout'],
+ default_value: 'none',
+ },
+ { name: 'invocation', type: 'string', options: null, default_value: '' },
+ {
+ name: 'invocation_emote_self',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'selection_type',
+ type: 'string_enum',
+ options: ['view', 'range'],
+ default_value: 'view',
+ },
+ { name: 'range', type: 'int', options: null, default_value: 7 },
+ { name: 'message', type: 'string', options: null, default_value: '' },
+ { name: 'player_lock', type: 'bool', options: null, default_value: true },
+ {
+ name: 'sparks_spread',
+ type: 'bool',
+ options: null,
+ default_value: false,
+ },
+ { name: 'sparks_amt', type: 'int', options: null, default_value: 0 },
+ {
+ name: 'smoke_spread',
+ type: 'int_enum',
+ options: ['none', 'harmless', 'harmful', 'sleeping'],
+ default_value: 'none',
+ },
+ { name: 'smoke_amt', type: 'int', options: null, default_value: 0 },
+ {
+ name: 'centcom_cancast',
+ type: 'bool',
+ options: null,
+ default_value: false,
+ },
+ ];
+ switch (type) {
+ case 'targeted':
+ ret.push(
+ { name: 'overlay', type: 'bool', options: null, default_value: false },
+ {
+ name: 'overlay_icon',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'overlay_icon_state',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'overlay_lifespan',
+ type: 'int',
+ options: null,
+ default_value: 0,
+ },
+ {
+ name: 'max_targets',
+ type: 'int',
+ options: null,
+ default_value: false,
+ },
+ {
+ name: 'target_ignore_prev',
+ type: 'bool',
+ options: null,
+ default_value: true,
+ },
+ {
+ name: 'include_user',
+ type: 'bool',
+ options: null,
+ default_value: false,
+ },
+ {
+ name: 'random_target',
+ type: 'bool',
+ options: null,
+ default_value: false,
+ },
+ {
+ name: 'random_target_priority',
+ type: 'int_enum',
+ options: ['closest', 'random'],
+ default_value: 'closest',
+ }
+ );
+ break;
+ case 'aoe_turf':
+ ret = ret.filter((variable) => variable.name !== 'selection_type');
+ ret.push(
+ { name: 'inner_radius', type: 'int', options: null, default_value: -1 },
+ { name: 'overlay', type: 'bool', options: null, default_value: false },
+ {
+ name: 'overlay_icon',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'overlay_icon_state',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'overlay_lifespan',
+ type: 'int',
+ options: null,
+ default_value: 0,
+ }
+ );
+ break;
+ case 'self':
+ ret = ret.filter(
+ (variable) =>
+ variable.name !== 'range' && variable.name !== 'selection_type'
+ );
+ break;
+ case 'aimed':
+ ret.push(
+ {
+ name: 'base_icon_state',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'ranged_mousepointer',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'deactive_msg',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'active_msg',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'projectile_amount',
+ type: 'int',
+ options: null,
+ default_value: 1,
+ },
+ {
+ name: 'projectiles_per_fire',
+ type: 'int',
+ options: null,
+ default_value: 1,
+ },
+ {
+ name: 'projectile_var_overrides',
+ type: 'list',
+ options: null,
+ default_value: [],
+ }
+ );
+ break;
+ case 'cone':
+ case 'cone/staggered':
+ ret = ret.filter(
+ (variable) =>
+ variable.name !== 'range' && variable.name !== 'selection_type'
+ );
+ ret.push(
+ { name: 'cone_level', type: 'int', options: null, default_value: 3 },
+ {
+ name: 'respect_density',
+ type: 'bool',
+ options: null,
+ default_value: false,
+ }
+ );
+ break;
+ case 'pointed':
+ ret.push(
+ { name: 'overlay', type: 'bool', options: null, default_value: false },
+ {
+ name: 'overlay_icon',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'overlay_icon_state',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'overlay_lifespan',
+ type: 'int',
+ options: null,
+ default_value: 0,
+ },
+ {
+ name: 'ranged_mousepointer',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'deactive_msg',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'active_msg',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'self_castable',
+ type: 'bool',
+ options: null,
+ default_value: false,
+ },
+ { name: 'aim_assist', type: 'bool', options: null, default_value: true }
+ );
+ break;
+ case 'targeted/touch':
+ ret = ret.filter(
+ (variable) =>
+ variable.name !== 'range'
+ && variable.name !== 'invocation_type'
+ && variable.name !== 'selection_type'
+ );
+ ret.push(
+ {
+ name: 'drawmessage',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'dropmessage',
+ type: 'string',
+ options: null,
+ default_value: '',
+ },
+ {
+ name: 'hand_var_overrides',
+ type: 'list',
+ options: null,
+ default_value: [],
+ }
+ );
+ break;
+ default:
+ return [];
+ }
+ ret.push({
+ name: 'scratchpad',
+ type: 'list',
+ options: null,
+ default_value: [],
+ });
+ return ret;
+};
+
+export const SDQLSpellMenu = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { type, types, alert } = data;
+
+ return (
+
+
+
+
+
+
+ act('type', { path: value })}
+ />
+
+
+
+
+
+
+
+ act('confirm')}
+ />
+
+
+
+
+ {alert}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+/**
+ * Used to determine whether or not to show a UI element corresponding to a
+ * variable.
+ * @param entry An object, from the list of objects returned by typevars(),
+ * corresponding to the variable to be shown or hidden.
+ * @param saved_vars The list of currently stored variable values.
+ * @returns Whether or not to show the UI element corresponding to the variable
+ * represented by the passed entry.
+ */
+const varCondition = (entry, saved_vars) => {
+ switch (entry.name) {
+ case 'charge_max':
+ return saved_vars['charge_type'] !== 'holder_var';
+ case 'holder_var_type':
+ case 'holder_var_amount':
+ return saved_vars['charge_type'] === 'holder_var';
+ case 'human_req':
+ return !saved_vars['clothes_req'];
+ case 'invocation':
+ return saved_vars['invocation_type'] !== 'none';
+ case 'invocation_emote_self':
+ return saved_vars['invocation_type'] === 'emote';
+ case 'overlay_icon':
+ case 'overlay_icon_state':
+ case 'overlay_lifespan':
+ return !!saved_vars['overlay'];
+ case 'sparks_amt':
+ return !!saved_vars['sparks_spread'];
+ case 'smoke_amt':
+ return !!saved_vars['smoke_spread'];
+ case 'random_target_priority':
+ return !!saved_vars['random_target'];
+ default:
+ return true;
+ }
+};
+
+/**
+ * A React component that wraps its contents in a tooltip object,
+ * if one exists for the variable described by the object passed through the
+ * entry property.
+ *
+ * @param entry An object, from the list of objects returned by typevars(),
+ * corresponding to the variable whose tooltip is to be shown.
+ */
+const WrapInTooltip = (props, context) => {
+ const { data } = useBackend(context);
+ const { entry, children } = props;
+ const { type, tooltips } = data;
+ const tip = tooltips[entry.name]?.replace(
+ '$type',
+ tooltips[entry.name + '_' + type]
+ );
+ // TODO: Uncomment this block when tooltips no longer suck.
+ // if (tip) {
+ // return (
+ //
+ // {children}
+ //
+ // )
+ // }
+ return children;
+};
+
+/**
+ * A React component that contains a list of the meaningfully-editable variables
+ * of the spell being edited.
+ */
+const SDQLSpellOptions = (props, context) => {
+ const { data } = useBackend(context);
+ const { type, saved_vars } = data;
+
+ const vars = typevars(type);
+
+ return (
+
+ {vars
+ .filter((entry) => varCondition(entry, saved_vars))
+ .map((entry) => (
+
+
+
+
+ {entry.name}:
+
+
+
+
+
+
+
+ ))}
+
+ );
+};
+
+/**
+ * A React component that contains the appropriate input element for the
+ * variable described by the object passed through the entry property.
+ * @param entry An object, from the list of objects returned by typevars(),
+ * corresponding to the variable to provide an input element for.
+ */
+const SDQLSpellInput = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { saved_vars } = data;
+ const { entry } = props;
+ const { name, type, options, default_value } = entry;
+ switch (type) {
+ case 'string':
+ return (
+ act('variable', { name, value })}
+ />
+ );
+ case 'int':
+ return (
+ act('variable', { name, value })}
+ />
+ );
+ case 'bool':
+ return (
+ act('bool_variable', { name })}
+ />
+ );
+ case 'string_enum':
+ return (
+ act('variable', { name, value })}
+ />
+ );
+ case 'int_enum':
+ return (
+
+ act('variable', { name, value: options.indexOf(value) })}
+ />
+ );
+ case 'list':
+ return ;
+ }
+};
+
+/**
+ * A React component containing the appropriate input fields for editing a list
+ * variable.
+ * @param {string} list The name of the list to show variables for.
+ */
+const SDQLSpellListEntry = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { list_vars } = data;
+ const { list } = props;
+ return (
+
+ {Object.entries(list_vars[list]).map(([name, { type, value, flags }]) => (
+
+
+ {
+ // Can be renamed?
+ (flags & 2) === 0 ? (
+
+ act('list_variable_rename', { list, name, new_name: value })}
+ />
+ ) : (
+
+ {name}:
+
+ )
+ }
+
+
+ {
+ // Can type be changed?
+ (flags & 1) === 0 && (
+
+ act('list_variable_change_type', { list, name, value })}
+ />
+ )
+ }
+
+
+
+
+
+ ))}
+
+ );
+};
+
+/**
+ * A React component that contains the appropriate input element for the
+ * variable of a given name and type within a list.
+ * @param list The name of the list containing the variable
+ * @param name The name of the variable
+ * @param type The type of the variable
+ * @param value The current value of the variable
+ */
+const SDQLSpellListVarInput = (props, context) => {
+ const { act } = useBackend(context);
+ const { list, name, type, value } = props;
+ switch (type) {
+ case 'num':
+ return (
+
+ act('list_variable_change_value', { list, name, value })}
+ />
+ );
+ case 'bool':
+ return (
+ act('list_variable_change_bool', { list, name })}
+ />
+ );
+ case 'string':
+ case 'path':
+ case 'icon':
+ return (
+
+ act('list_variable_change_value', { list, name, value })}
+ />
+ );
+ case 'list':
+ return ;
+ default:
+ return (
+
+ {"You shouldn't be seeing this!"}
+
+ );
+ }
+};
+
+const SDQLSpellIcons = (props, context) => {
+ const { data } = useBackend(context);
+ const {
+ saved_vars,
+ type,
+ action_icon,
+ hand_icon,
+ projectile_icon,
+ overlay_icon,
+ mouse_icon,
+ } = data;
+
+ const vars = typevars(type);
+
+ return (
+
+
+ {type && (
+
+ )}
+ {type === 'targeted/touch' && (
+
+ )}
+ {type === 'aimed' && (
+
+ )}
+ {type
+ && vars.some((entry) => entry.name === 'ranged_mousepointer')
+ && saved_vars['ranged_mousepointer'] && (
+
+ )}
+ {type && 'overlay' in saved_vars && saved_vars['overlay'] === 1 && (
+
+ )}
+
+
+ );
+};