Merge branch 'master' of https://github.com/Citadel-Station-13/Citadel-Station-13 into Ghommie-cit444
This commit is contained in:
@@ -1,38 +1,38 @@
|
||||
/datum/ntnet_service
|
||||
var/name = "Unidentified Network Service"
|
||||
var/id
|
||||
var/list/networks_by_id = list() //Yes we support multinetwork services!
|
||||
|
||||
/datum/ntnet_service/New()
|
||||
var/datum/component/ntnet_interface/N = AddComponent(/datum/component/ntnet_interface, id, name, FALSE)
|
||||
id = N.hardware_id
|
||||
|
||||
/datum/ntnet_service/Destroy()
|
||||
for(var/i in networks_by_id)
|
||||
var/datum/ntnet/N = i
|
||||
disconnect(N, TRUE)
|
||||
networks_by_id = null
|
||||
return ..()
|
||||
|
||||
/datum/ntnet_service/proc/connect(datum/ntnet/net)
|
||||
if(!istype(net))
|
||||
return FALSE
|
||||
var/datum/component/ntnet_interface/interface = GetComponent(/datum/component/ntnet_interface)
|
||||
if(!interface.register_connection(net))
|
||||
return FALSE
|
||||
if(!net.register_service(src))
|
||||
interface.unregister_connection(net)
|
||||
return FALSE
|
||||
networks_by_id[net.network_id] = net
|
||||
return TRUE
|
||||
|
||||
/datum/ntnet_service/proc/disconnect(datum/ntnet/net, force = FALSE)
|
||||
if(!istype(net) || (!net.unregister_service(src) && !force))
|
||||
return FALSE
|
||||
var/datum/component/ntnet_interface/interface = GetComponent(/datum/component/ntnet_interface)
|
||||
interface.unregister_connection(net)
|
||||
networks_by_id -= net.network_id
|
||||
return TRUE
|
||||
|
||||
/datum/ntnet_service/proc/ntnet_intercept(datum/netdata/data, datum/ntnet/net, datum/component/ntnet_interface/sender)
|
||||
return
|
||||
/datum/ntnet_service
|
||||
var/name = "Unidentified Network Service"
|
||||
var/id
|
||||
var/list/networks_by_id = list() //Yes we support multinetwork services!
|
||||
|
||||
/datum/ntnet_service/New()
|
||||
var/datum/component/ntnet_interface/N = AddComponent(/datum/component/ntnet_interface, id, name, FALSE)
|
||||
id = N.hardware_id
|
||||
|
||||
/datum/ntnet_service/Destroy()
|
||||
for(var/i in networks_by_id)
|
||||
var/datum/ntnet/N = i
|
||||
disconnect(N, TRUE)
|
||||
networks_by_id = null
|
||||
return ..()
|
||||
|
||||
/datum/ntnet_service/proc/connect(datum/ntnet/net)
|
||||
if(!istype(net))
|
||||
return FALSE
|
||||
var/datum/component/ntnet_interface/interface = GetComponent(/datum/component/ntnet_interface)
|
||||
if(!interface.register_connection(net))
|
||||
return FALSE
|
||||
if(!net.register_service(src))
|
||||
interface.unregister_connection(net)
|
||||
return FALSE
|
||||
networks_by_id[net.network_id] = net
|
||||
return TRUE
|
||||
|
||||
/datum/ntnet_service/proc/disconnect(datum/ntnet/net, force = FALSE)
|
||||
if(!istype(net) || (!net.unregister_service(src) && !force))
|
||||
return FALSE
|
||||
var/datum/component/ntnet_interface/interface = GetComponent(/datum/component/ntnet_interface)
|
||||
interface.unregister_connection(net)
|
||||
networks_by_id -= net.network_id
|
||||
return TRUE
|
||||
|
||||
/datum/ntnet_service/proc/ntnet_intercept(datum/netdata/data, datum/ntnet/net, datum/component/ntnet_interface/sender)
|
||||
return
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
|
||||
/datum/action/quit_vr/Trigger() //this merely a trigger for /datum/component/virtual_reality
|
||||
. = ..()
|
||||
if(!.)
|
||||
if(.) //The component was not there to block the trigger.
|
||||
Remove(owner)
|
||||
|
||||
@@ -52,18 +52,16 @@
|
||||
flags_1 = NODECONSTRUCT_1
|
||||
only_current_user_can_interact = TRUE
|
||||
|
||||
/obj/machinery/vr_sleeper/hugbox/emag_act(mob/user)
|
||||
return SEND_SIGNAL(src, COMSIG_ATOM_EMAG_ACT)
|
||||
|
||||
/obj/machinery/vr_sleeper/emag_act(mob/user)
|
||||
. = ..()
|
||||
if(!(obj_flags & EMAGGED))
|
||||
return
|
||||
obj_flags |= EMAGGED
|
||||
you_die_in_the_game_you_die_for_real = TRUE
|
||||
sparks.start()
|
||||
addtimer(CALLBACK(src, .proc/emagNotify), 150)
|
||||
return TRUE
|
||||
if(!only_current_user_can_interact)
|
||||
obj_flags |= EMAGGED
|
||||
you_die_in_the_game_you_die_for_real = TRUE
|
||||
sparks.start()
|
||||
addtimer(CALLBACK(src, .proc/emagNotify), 150)
|
||||
return TRUE
|
||||
|
||||
/obj/machinery/vr_sleeper/update_icon()
|
||||
icon_state = "[initial(icon_state)][state_open ? "-open" : ""]"
|
||||
@@ -76,7 +74,7 @@
|
||||
return ..()
|
||||
|
||||
/obj/machinery/vr_sleeper/MouseDrop_T(mob/target, mob/user)
|
||||
if(user.stat || user.lying || !Adjacent(user) || !user.Adjacent(target) || !iscarbon(target) || !user.IsAdvancedToolUser())
|
||||
if(user.lying || !iscarbon(target) || !Adjacent(target) || !user.canUseTopic(src, BE_CLOSE, TRUE, NO_TK))
|
||||
return
|
||||
close_machine(target)
|
||||
|
||||
@@ -91,26 +89,25 @@
|
||||
return
|
||||
switch(action)
|
||||
if("vr_connect")
|
||||
var/mob/living/carbon/human/human_occupant = occupant
|
||||
if(human_occupant && human_occupant.mind && usr == occupant)
|
||||
|
||||
to_chat(occupant, "<span class='warning'>Transferring to virtual reality...</span>")
|
||||
if(vr_mob && (!istype(vr_mob) || !vr_mob.InCritical()) && !vr_mob.GetComponent(/datum/component/virtual_reality))
|
||||
vr_mob.AddComponent(/datum/component/virtual_reality, human_occupant, src, you_die_in_the_game_you_die_for_real)
|
||||
to_chat(vr_mob, "<span class='notice'>Transfer successful! You are now playing as [vr_mob] in VR!</span>")
|
||||
else
|
||||
var/mob/M = occupant
|
||||
if(M?.mind && M == usr)
|
||||
to_chat(M, "<span class='warning'>Transferring to virtual reality...</span>")
|
||||
var/datum/component/virtual_reality/VR
|
||||
if(vr_mob)
|
||||
VR = vr_mob.GetComponent(/datum/component/virtual_reality)
|
||||
if(!(VR?.connect(M)))
|
||||
if(allow_creating_vr_mobs)
|
||||
to_chat(occupant, "<span class='warning'>Virtual avatar not found, attempting to create one...</span>")
|
||||
to_chat(occupant, "<span class='warning'>Virtual avatar [vr_mob ? "corrupted" : "missing"], attempting to create one...</span>")
|
||||
var/obj/effect/landmark/vr_spawn/V = get_vr_spawnpoint()
|
||||
var/turf/T = get_turf(V)
|
||||
if(T)
|
||||
SStgui.close_user_uis(occupant, src)
|
||||
new_player(occupant, T, V.vr_outfit)
|
||||
to_chat(vr_mob, "<span class='notice'>Transfer successful! You are now playing as [vr_mob] in VR!</span>")
|
||||
else
|
||||
to_chat(occupant, "<span class='warning'>Virtual world misconfigured, aborting transfer</span>")
|
||||
else
|
||||
to_chat(occupant, "<span class='warning'>The virtual world does not support the creation of new virtual avatars, aborting transfer</span>")
|
||||
else
|
||||
to_chat(vr_mob, "<span class='notice'>Transfer successful! You are now playing as [vr_mob] in VR!</span>")
|
||||
. = TRUE
|
||||
if("delete_avatar")
|
||||
if(!occupant || usr == occupant)
|
||||
@@ -157,17 +154,31 @@
|
||||
for(var/obj/effect/landmark/vr_spawn/V in GLOB.landmarks_list)
|
||||
GLOB.vr_spawnpoints[V.vr_category] = V
|
||||
|
||||
/obj/machinery/vr_sleeper/proc/new_player(mob/living/carbon/human/H, location, datum/outfit/outfit, transfer = TRUE)
|
||||
if(!H)
|
||||
/obj/machinery/vr_sleeper/proc/new_player(mob/M, location, datum/outfit/outfit, transfer = TRUE)
|
||||
if(!M)
|
||||
return
|
||||
cleanup_vr_mob()
|
||||
vr_mob = new virtual_mob_type(location)
|
||||
if(vr_mob.build_virtual_character(H, outfit))
|
||||
var/mob/living/carbon/human/vr_H = vr_mob
|
||||
vr_H.updateappearance(TRUE, TRUE, TRUE)
|
||||
if(!transfer || !H.mind)
|
||||
return
|
||||
vr_mob.AddComponent(/datum/component/virtual_reality, H, src, you_die_in_the_game_you_die_for_real)
|
||||
if(vr_mob.build_virtual_character(M, outfit) && iscarbon(vr_mob))
|
||||
var/mob/living/carbon/C = vr_mob
|
||||
C.updateappearance(TRUE, TRUE, TRUE)
|
||||
var/datum/component/virtual_reality/VR = vr_mob.AddComponent(/datum/component/virtual_reality, you_die_in_the_game_you_die_for_real)
|
||||
if(VR.connect(M))
|
||||
RegisterSignal(VR, COMSIG_COMPONENT_UNREGISTER_PARENT, .proc/unset_vr_mob)
|
||||
RegisterSignal(VR, COMSIG_COMPONENT_REGISTER_PARENT, .proc/set_vr_mob)
|
||||
if(!only_current_user_can_interact)
|
||||
VR.RegisterSignal(src, COMSIG_ATOM_EMAG_ACT, /datum/component/virtual_reality.proc/you_only_live_once)
|
||||
VR.RegisterSignal(src, COMSIG_MACHINE_EJECT_OCCUPANT, /datum/component/virtual_reality.proc/revert_to_reality)
|
||||
VR.RegisterSignal(src, COMSIG_PARENT_QDELETING, /datum/component/virtual_reality.proc/machine_destroyed)
|
||||
to_chat(vr_mob, "<span class='notice'>Transfer successful! You are now playing as [vr_mob] in VR!</span>")
|
||||
else
|
||||
to_chat(M, "<span class='notice'>Transfer failed! virtual reality data likely corrupted!</span>")
|
||||
|
||||
/obj/machinery/vr_sleeper/proc/unset_vr_mob(datum/component/virtual_reality/VR)
|
||||
vr_mob = null
|
||||
|
||||
/obj/machinery/vr_sleeper/proc/set_vr_mob(datum/component/virtual_reality/VR)
|
||||
vr_mob = VR.parent
|
||||
|
||||
/obj/machinery/vr_sleeper/proc/cleanup_vr_mob()
|
||||
if(vr_mob)
|
||||
@@ -222,6 +233,7 @@
|
||||
qdel(C)
|
||||
for (var/A in corpse_party)
|
||||
var/mob/M = A
|
||||
if(get_area(M) == vr_area && M.stat == DEAD)
|
||||
if(M && M.stat == DEAD && get_area(M) == vr_area)
|
||||
qdel(M)
|
||||
corpse_party -= M
|
||||
addtimer(CALLBACK(src, .proc/clean_up), 3 MINUTES)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+232
-201
@@ -1,201 +1,232 @@
|
||||
//Blocks an attempt to connect before even creating our client datum thing.
|
||||
|
||||
//How many new ckey matches before we revert the stickyban to it's roundstart state
|
||||
//These are exclusive, so once it goes over one of these numbers, it reverts the ban
|
||||
#define STICKYBAN_MAX_MATCHES 20
|
||||
#define STICKYBAN_MAX_EXISTING_USER_MATCHES 5 //ie, users who were connected before the ban triggered
|
||||
#define STICKYBAN_MAX_ADMIN_MATCHES 2
|
||||
|
||||
/world/IsBanned(key,address,computer_id,type,real_bans_only=FALSE)
|
||||
if (!key || !address || !computer_id)
|
||||
if(real_bans_only)
|
||||
return FALSE
|
||||
log_access("Failed Login (invalid data): [key] [address]-[computer_id]")
|
||||
return list("reason"="invalid login data", "desc"="Error: Could not check ban status, Please try again. Error message: Your computer provided invalid or blank information to the server on connection (byond username, IP, and Computer ID.) Provided information for reference: Username:'[key]' IP:'[address]' Computer ID:'[computer_id]'. (If you continue to get this error, please restart byond or contact byond support.)")
|
||||
|
||||
if (text2num(computer_id) == 2147483647) //this cid causes stickybans to go haywire
|
||||
log_access("Failed Login (invalid cid): [key] [address]-[computer_id]")
|
||||
return list("reason"="invalid login data", "desc"="Error: Could not check ban status, Please try again. Error message: Your computer provided an invalid Computer ID.)")
|
||||
var/admin = 0
|
||||
var/ckey = ckey(key)
|
||||
if(GLOB.admin_datums[ckey] || GLOB.deadmins[ckey])
|
||||
admin = 1
|
||||
|
||||
//Whitelist
|
||||
if(CONFIG_GET(flag/usewhitelist))
|
||||
if(!check_whitelist(ckey))
|
||||
if (admin)
|
||||
log_admin("The admin [key] has been allowed to bypass the whitelist")
|
||||
message_admins("<span class='adminnotice'>The admin [key] has been allowed to bypass the whitelist</span>")
|
||||
addclientmessage(ckey,"<span class='adminnotice'>You have been allowed to bypass the whitelist</span>")
|
||||
else
|
||||
log_access("Failed Login: [key] - Not on whitelist")
|
||||
return list("reason"="whitelist", "desc" = "\nReason: You are not on the white list for this server")
|
||||
|
||||
//Guest Checking
|
||||
if(!real_bans_only && IsGuestKey(key))
|
||||
if (CONFIG_GET(flag/guest_ban))
|
||||
log_access("Failed Login: [key] - Guests not allowed")
|
||||
return list("reason"="guest", "desc"="\nReason: Guests not allowed. Please sign in with a byond account.")
|
||||
if (CONFIG_GET(flag/panic_bunker) && SSdbcore.Connect())
|
||||
log_access("Failed Login: [key] - Guests not allowed during panic bunker")
|
||||
return list("reason"="guest", "desc"="\nReason: Sorry but the server is currently not accepting connections from never before seen players or guests. If you have played on this server with a byond account before, please log in to the byond account you have played from.")
|
||||
|
||||
//Population Cap Checking
|
||||
var/extreme_popcap = CONFIG_GET(number/extreme_popcap)
|
||||
if(!real_bans_only && extreme_popcap && living_player_count() >= extreme_popcap && !admin)
|
||||
log_access("Failed Login: [key] - Population cap reached")
|
||||
return list("reason"="popcap", "desc"= "\nReason: [CONFIG_GET(string/extreme_popcap_message)]")
|
||||
|
||||
if(CONFIG_GET(flag/ban_legacy_system))
|
||||
|
||||
//Ban Checking
|
||||
. = CheckBan(ckey, computer_id, address )
|
||||
if(.)
|
||||
if (admin)
|
||||
log_admin("The admin [key] has been allowed to bypass a matching ban on [.["key"]]")
|
||||
message_admins("<span class='adminnotice'>The admin [key] has been allowed to bypass a matching ban on [.["key"]]</span>")
|
||||
addclientmessage(ckey,"<span class='adminnotice'>You have been allowed to bypass a matching ban on [.["key"]]</span>")
|
||||
else
|
||||
log_access("Failed Login: [key] [computer_id] [address] - Banned [.["reason"]]")
|
||||
return .
|
||||
|
||||
else
|
||||
if(!SSdbcore.Connect())
|
||||
var/msg = "Ban database connection failure. Key [ckey] not checked"
|
||||
log_world(msg)
|
||||
message_admins(msg)
|
||||
return
|
||||
|
||||
var/ipquery = ""
|
||||
var/cidquery = ""
|
||||
if(address)
|
||||
ipquery = " OR ip = INET_ATON('[address]') "
|
||||
|
||||
if(computer_id)
|
||||
cidquery = " OR computerid = '[computer_id]' "
|
||||
|
||||
var/datum/DBQuery/query_ban_check = SSdbcore.NewQuery("SELECT IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE [format_table_name("player")].ckey = [format_table_name("ban")].ckey), ckey), IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE [format_table_name("player")].ckey = [format_table_name("ban")].a_ckey), a_ckey), reason, expiration_time, duration, bantime, bantype, id, round_id FROM [format_table_name("ban")] WHERE (ckey = '[ckey]' [ipquery] [cidquery]) AND (bantype = 'PERMABAN' OR bantype = 'ADMIN_PERMABAN' OR ((bantype = 'TEMPBAN' OR bantype = 'ADMIN_TEMPBAN') AND expiration_time > Now())) AND isnull(unbanned)")
|
||||
if(!query_ban_check.Execute(async = TRUE))
|
||||
qdel(query_ban_check)
|
||||
return
|
||||
while(query_ban_check.NextRow())
|
||||
var/pkey = query_ban_check.item[1]
|
||||
var/akey = query_ban_check.item[2]
|
||||
var/reason = query_ban_check.item[3]
|
||||
var/expiration = query_ban_check.item[4]
|
||||
var/duration = query_ban_check.item[5]
|
||||
var/bantime = query_ban_check.item[6]
|
||||
var/bantype = query_ban_check.item[7]
|
||||
var/banid = query_ban_check.item[8]
|
||||
var/ban_round_id = query_ban_check.item[9]
|
||||
if (bantype == "ADMIN_PERMABAN" || bantype == "ADMIN_TEMPBAN")
|
||||
//admin bans MUST match on ckey to prevent cid-spoofing attacks
|
||||
// as well as dynamic ip abuse
|
||||
if (ckey(pkey) != ckey)
|
||||
continue
|
||||
if (admin)
|
||||
if (bantype == "ADMIN_PERMABAN" || bantype == "ADMIN_TEMPBAN")
|
||||
log_admin("The admin [key] is admin banned (#[banid]), and has been disallowed access")
|
||||
message_admins("<span class='adminnotice'>The admin [key] is admin banned (#[banid]), and has been disallowed access</span>")
|
||||
else
|
||||
log_admin("The admin [key] has been allowed to bypass a matching ban on [pkey] (#[banid])")
|
||||
message_admins("<span class='adminnotice'>The admin [key] has been allowed to bypass a matching ban on [pkey] (#[banid])</span>")
|
||||
addclientmessage(ckey,"<span class='adminnotice'>You have been allowed to bypass a matching ban on [pkey] (#[banid])</span>")
|
||||
continue
|
||||
var/expires = ""
|
||||
if(text2num(duration) > 0)
|
||||
expires = " The ban is for [duration] minutes and expires on [expiration] (server time)."
|
||||
else
|
||||
expires = " The is a permanent ban."
|
||||
|
||||
var/desc = "\nReason: You, or another user of this computer or connection ([pkey]) is banned from playing here. The ban reason is:\n[reason]\nThis ban (BanID #[banid]) was applied by [akey] on [bantime] during round ID [ban_round_id], [expires]"
|
||||
|
||||
. = list("reason"="[bantype]", "desc"="[desc]")
|
||||
|
||||
|
||||
log_access("Failed Login: [key] [computer_id] [address] - Banned (#[banid]) [.["reason"]]")
|
||||
qdel(query_ban_check)
|
||||
return .
|
||||
qdel(query_ban_check)
|
||||
|
||||
var/list/ban = ..() //default pager ban stuff
|
||||
if (ban)
|
||||
var/bannedckey = "ERROR"
|
||||
if (ban["ckey"])
|
||||
bannedckey = ban["ckey"]
|
||||
|
||||
var/newmatch = FALSE
|
||||
var/client/C = GLOB.directory[ckey]
|
||||
var/cachedban = SSstickyban.cache[bannedckey]
|
||||
|
||||
//rogue ban in the process of being reverted.
|
||||
if (cachedban && cachedban["reverting"])
|
||||
return null
|
||||
|
||||
if (cachedban && ckey != bannedckey)
|
||||
newmatch = TRUE
|
||||
if (cachedban["keys"])
|
||||
if (cachedban["keys"][ckey])
|
||||
newmatch = FALSE
|
||||
if (cachedban["matches_this_round"][ckey])
|
||||
newmatch = FALSE
|
||||
|
||||
if (newmatch && cachedban)
|
||||
var/list/newmatches = cachedban["matches_this_round"]
|
||||
var/list/newmatches_connected = cachedban["existing_user_matches_this_round"]
|
||||
var/list/newmatches_admin = cachedban["admin_matches_this_round"]
|
||||
|
||||
newmatches[ckey] = ckey
|
||||
if (C)
|
||||
newmatches_connected[ckey] = ckey
|
||||
if (admin)
|
||||
newmatches_admin[ckey] = ckey
|
||||
|
||||
if (\
|
||||
newmatches.len > STICKYBAN_MAX_MATCHES || \
|
||||
newmatches_connected.len > STICKYBAN_MAX_EXISTING_USER_MATCHES || \
|
||||
newmatches_admin.len > STICKYBAN_MAX_ADMIN_MATCHES \
|
||||
)
|
||||
if (cachedban["reverting"])
|
||||
return null
|
||||
cachedban["reverting"] = TRUE
|
||||
|
||||
world.SetConfig("ban", bannedckey, null)
|
||||
|
||||
log_game("Stickyban on [bannedckey] detected as rogue, reverting to its roundstart state")
|
||||
message_admins("Stickyban on [bannedckey] detected as rogue, reverting to its roundstart state")
|
||||
//do not convert to timer.
|
||||
spawn (5)
|
||||
world.SetConfig("ban", bannedckey, null)
|
||||
sleep(1)
|
||||
world.SetConfig("ban", bannedckey, null)
|
||||
cachedban["matches_this_round"] = list()
|
||||
cachedban["existing_user_matches_this_round"] = list()
|
||||
cachedban["admin_matches_this_round"] = list()
|
||||
cachedban -= "reverting"
|
||||
world.SetConfig("ban", bannedckey, list2stickyban(cachedban))
|
||||
return null
|
||||
|
||||
//byond will not trigger isbanned() for "global" host bans,
|
||||
//ie, ones where the "apply to this game only" checkbox is not checked (defaults to not checked)
|
||||
//So it's safe to let admins walk thru host/sticky bans here
|
||||
if (admin)
|
||||
log_admin("The admin [key] has been allowed to bypass a matching host/sticky ban on [bannedckey]")
|
||||
message_admins("<span class='adminnotice'>The admin [key] has been allowed to bypass a matching host/sticky ban on [bannedckey]</span>")
|
||||
addclientmessage(ckey,"<span class='adminnotice'>You have been allowed to bypass a matching host/sticky ban on [bannedckey]</span>")
|
||||
return null
|
||||
|
||||
if (C) //user is already connected!.
|
||||
to_chat(C, "You are about to get disconnected for matching a sticky ban after you connected. If this turns out to be the ban evasion detection system going haywire, we will automatically detect this and revert the matches. if you feel that this is the case, please wait EXACTLY 6 seconds then reconnect using file -> reconnect to see if the match was reversed.")
|
||||
|
||||
var/desc = "\nReason:(StickyBan) You, or another user of this computer or connection ([bannedckey]) is banned from playing here. The ban reason is:\n[ban["message"]]\nThis ban was applied by [ban["admin"]]\nThis is a BanEvasion Detection System ban, if you think this ban is a mistake, please wait EXACTLY 6 seconds, then try again before filing an appeal.\n"
|
||||
. = list("reason" = "Stickyban", "desc" = desc)
|
||||
log_access("Failed Login: [key] [computer_id] [address] - StickyBanned [ban["message"]] Target Username: [bannedckey] Placed by [ban["admin"]]")
|
||||
|
||||
return .
|
||||
|
||||
|
||||
#undef STICKYBAN_MAX_MATCHES
|
||||
#undef STICKYBAN_MAX_EXISTING_USER_MATCHES
|
||||
#undef STICKYBAN_MAX_ADMIN_MATCHES
|
||||
//Blocks an attempt to connect before even creating our client datum thing.
|
||||
|
||||
//How many new ckey matches before we revert the stickyban to it's roundstart state
|
||||
//These are exclusive, so once it goes over one of these numbers, it reverts the ban
|
||||
#define STICKYBAN_MAX_MATCHES 20
|
||||
#define STICKYBAN_MAX_EXISTING_USER_MATCHES 5 //ie, users who were connected before the ban triggered
|
||||
#define STICKYBAN_MAX_ADMIN_MATCHES 2
|
||||
|
||||
/world/IsBanned(key,address,computer_id,type,real_bans_only=FALSE)
|
||||
var/static/key_cache = list()
|
||||
if(!real_bans_only)
|
||||
if(key_cache[key])
|
||||
return list("reason"="concurrent connection attempts", "desc"="You are attempting to connect too fast. Try again.")
|
||||
key_cache[key] = 1
|
||||
|
||||
if (!key || !address || !computer_id)
|
||||
if(real_bans_only)
|
||||
key_cache[key] = 0
|
||||
return FALSE
|
||||
log_access("Failed Login (invalid data): [key] [address]-[computer_id]")
|
||||
key_cache[key] = 0
|
||||
return list("reason"="invalid login data", "desc"="Error: Could not check ban status, Please try again. Error message: Your computer provided invalid or blank information to the server on connection (byond username, IP, and Computer ID.) Provided information for reference: Username:'[key]' IP:'[address]' Computer ID:'[computer_id]'. (If you continue to get this error, please restart byond or contact byond support.)")
|
||||
|
||||
if (text2num(computer_id) == 2147483647) //this cid causes stickybans to go haywire
|
||||
log_access("Failed Login (invalid cid): [key] [address]-[computer_id]")
|
||||
key_cache[key] = 0
|
||||
return list("reason"="invalid login data", "desc"="Error: Could not check ban status, Please try again. Error message: Your computer provided an invalid Computer ID.)")
|
||||
|
||||
if (type == "world")
|
||||
key_cache[key] = 0
|
||||
return ..() //shunt world topic banchecks to purely to byond's internal ban system
|
||||
|
||||
var/ckey = ckey(key)
|
||||
var/client/C = GLOB.directory[ckey]
|
||||
if (C && ckey == C.ckey && computer_id == C.computer_id && address == C.address)
|
||||
key_cache[key] = 0
|
||||
return //don't recheck connected clients.
|
||||
|
||||
var/admin = FALSE
|
||||
if(GLOB.admin_datums[ckey] || GLOB.deadmins[ckey])
|
||||
admin = 1
|
||||
|
||||
//Whitelist
|
||||
if(CONFIG_GET(flag/usewhitelist))
|
||||
if(!check_whitelist(ckey))
|
||||
if (admin)
|
||||
log_admin("The admin [key] has been allowed to bypass the whitelist")
|
||||
message_admins("<span class='adminnotice'>The admin [key] has been allowed to bypass the whitelist</span>")
|
||||
addclientmessage(ckey,"<span class='adminnotice'>You have been allowed to bypass the whitelist</span>")
|
||||
else
|
||||
log_access("Failed Login: [key] - Not on whitelist")
|
||||
key_cache[key] = 0
|
||||
return list("reason"="whitelist", "desc" = "\nReason: You are not on the white list for this server")
|
||||
|
||||
//Guest Checking
|
||||
if(!real_bans_only && IsGuestKey(key))
|
||||
if (CONFIG_GET(flag/guest_ban))
|
||||
log_access("Failed Login: [key] - Guests not allowed")
|
||||
key_cache[key] = 0
|
||||
return list("reason"="guest", "desc"="\nReason: Guests not allowed. Please sign in with a byond account.")
|
||||
if (CONFIG_GET(flag/panic_bunker) && SSdbcore.Connect())
|
||||
log_access("Failed Login: [key] - Guests not allowed during panic bunker")
|
||||
key_cache[key] = 0
|
||||
return list("reason"="guest", "desc"="\nReason: Sorry but the server is currently not accepting connections from never before seen players or guests. If you have played on this server with a byond account before, please log in to the byond account you have played from.")
|
||||
|
||||
//Population Cap Checking
|
||||
var/extreme_popcap = CONFIG_GET(number/extreme_popcap)
|
||||
if(!real_bans_only && extreme_popcap && living_player_count() >= extreme_popcap && !admin)
|
||||
log_access("Failed Login: [key] - Population cap reached")
|
||||
key_cache[key] = 0
|
||||
return list("reason"="popcap", "desc"= "\nReason: [CONFIG_GET(string/extreme_popcap_message)]")
|
||||
|
||||
if(CONFIG_GET(flag/ban_legacy_system))
|
||||
|
||||
//Ban Checking
|
||||
. = CheckBan(ckey, computer_id, address )
|
||||
if(.)
|
||||
if (admin)
|
||||
log_admin("The admin [key] has been allowed to bypass a matching ban on [.["key"]]")
|
||||
message_admins("<span class='adminnotice'>The admin [key] has been allowed to bypass a matching ban on [.["key"]]</span>")
|
||||
addclientmessage(ckey,"<span class='adminnotice'>You have been allowed to bypass a matching ban on [.["key"]]</span>")
|
||||
else
|
||||
log_access("Failed Login: [key] [computer_id] [address] - Banned [.["reason"]]")
|
||||
key_cache[key] = 0
|
||||
return .
|
||||
|
||||
else
|
||||
if(!SSdbcore.Connect())
|
||||
var/msg = "Ban database connection failure. Key [ckey] not checked"
|
||||
log_world(msg)
|
||||
message_admins(msg)
|
||||
key_cache[key] = 0
|
||||
return
|
||||
|
||||
var/ipquery = ""
|
||||
var/cidquery = ""
|
||||
if(address)
|
||||
ipquery = " OR ip = INET_ATON('[address]') "
|
||||
|
||||
if(computer_id)
|
||||
cidquery = " OR computerid = '[computer_id]' "
|
||||
|
||||
var/datum/DBQuery/query_ban_check = SSdbcore.NewQuery("SELECT IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE [format_table_name("player")].ckey = [format_table_name("ban")].ckey), ckey), IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE [format_table_name("player")].ckey = [format_table_name("ban")].a_ckey), a_ckey), reason, expiration_time, duration, bantime, bantype, id, round_id FROM [format_table_name("ban")] WHERE (ckey = '[ckey]' [ipquery] [cidquery]) AND (bantype = 'PERMABAN' OR bantype = 'ADMIN_PERMABAN' OR ((bantype = 'TEMPBAN' OR bantype = 'ADMIN_TEMPBAN') AND expiration_time > Now())) AND isnull(unbanned)")
|
||||
if(!query_ban_check.Execute(async = TRUE))
|
||||
qdel(query_ban_check)
|
||||
key_cache[key] = 0
|
||||
return
|
||||
while(query_ban_check.NextRow())
|
||||
var/pkey = query_ban_check.item[1]
|
||||
var/akey = query_ban_check.item[2]
|
||||
var/reason = query_ban_check.item[3]
|
||||
var/expiration = query_ban_check.item[4]
|
||||
var/duration = query_ban_check.item[5]
|
||||
var/bantime = query_ban_check.item[6]
|
||||
var/bantype = query_ban_check.item[7]
|
||||
var/banid = query_ban_check.item[8]
|
||||
var/ban_round_id = query_ban_check.item[9]
|
||||
if (bantype == "ADMIN_PERMABAN" || bantype == "ADMIN_TEMPBAN")
|
||||
//admin bans MUST match on ckey to prevent cid-spoofing attacks
|
||||
// as well as dynamic ip abuse
|
||||
if (ckey(pkey) != ckey)
|
||||
continue
|
||||
if (admin)
|
||||
if (bantype == "ADMIN_PERMABAN" || bantype == "ADMIN_TEMPBAN")
|
||||
log_admin("The admin [key] is admin banned (#[banid]), and has been disallowed access")
|
||||
message_admins("<span class='adminnotice'>The admin [key] is admin banned (#[banid]), and has been disallowed access</span>")
|
||||
else
|
||||
log_admin("The admin [key] has been allowed to bypass a matching ban on [pkey] (#[banid])")
|
||||
message_admins("<span class='adminnotice'>The admin [key] has been allowed to bypass a matching ban on [pkey] (#[banid])</span>")
|
||||
addclientmessage(ckey,"<span class='adminnotice'>You have been allowed to bypass a matching ban on [pkey] (#[banid])</span>")
|
||||
continue
|
||||
var/expires = ""
|
||||
if(text2num(duration) > 0)
|
||||
expires = " The ban is for [duration] minutes and expires on [expiration] (server time)."
|
||||
else
|
||||
expires = " The is a permanent ban."
|
||||
|
||||
var/desc = "\nReason: You, or another user of this computer or connection ([pkey]) is banned from playing here. The ban reason is:\n[reason]\nThis ban (BanID #[banid]) was applied by [akey] on [bantime] during round ID [ban_round_id], [expires]"
|
||||
|
||||
. = list("reason"="[bantype]", "desc"="[desc]")
|
||||
|
||||
|
||||
log_access("Failed Login: [key] [computer_id] [address] - Banned (#[banid]) [.["reason"]]")
|
||||
qdel(query_ban_check)
|
||||
key_cache[key] = 0
|
||||
return .
|
||||
qdel(query_ban_check)
|
||||
|
||||
var/list/ban = ..() //default pager ban stuff
|
||||
if (ban)
|
||||
var/bannedckey = "ERROR"
|
||||
if (ban["ckey"])
|
||||
bannedckey = ban["ckey"]
|
||||
|
||||
var/newmatch = FALSE
|
||||
var/cachedban = SSstickyban.cache[bannedckey]
|
||||
|
||||
//rogue ban in the process of being reverted.
|
||||
if (cachedban && cachedban["reverting"])
|
||||
key_cache[key] = 0
|
||||
return null
|
||||
|
||||
if (cachedban && ckey != bannedckey)
|
||||
newmatch = TRUE
|
||||
if (cachedban["keys"])
|
||||
if (cachedban["keys"][ckey])
|
||||
newmatch = FALSE
|
||||
if (cachedban["matches_this_round"][ckey])
|
||||
newmatch = FALSE
|
||||
|
||||
if (newmatch && cachedban)
|
||||
var/list/newmatches = cachedban["matches_this_round"]
|
||||
var/list/newmatches_connected = cachedban["existing_user_matches_this_round"]
|
||||
var/list/newmatches_admin = cachedban["admin_matches_this_round"]
|
||||
|
||||
newmatches[ckey] = ckey
|
||||
if (C)
|
||||
newmatches_connected[ckey] = ckey
|
||||
if (admin)
|
||||
newmatches_admin[ckey] = ckey
|
||||
|
||||
if (\
|
||||
newmatches.len > STICKYBAN_MAX_MATCHES || \
|
||||
newmatches_connected.len > STICKYBAN_MAX_EXISTING_USER_MATCHES || \
|
||||
newmatches_admin.len > STICKYBAN_MAX_ADMIN_MATCHES \
|
||||
)
|
||||
if (cachedban["reverting"])
|
||||
key_cache[key] = 0
|
||||
return null
|
||||
cachedban["reverting"] = TRUE
|
||||
|
||||
world.SetConfig("ban", bannedckey, null)
|
||||
|
||||
log_game("Stickyban on [bannedckey] detected as rogue, reverting to its roundstart state")
|
||||
message_admins("Stickyban on [bannedckey] detected as rogue, reverting to its roundstart state")
|
||||
//do not convert to timer.
|
||||
spawn (5)
|
||||
world.SetConfig("ban", bannedckey, null)
|
||||
sleep(1)
|
||||
world.SetConfig("ban", bannedckey, null)
|
||||
cachedban["matches_this_round"] = list()
|
||||
cachedban["existing_user_matches_this_round"] = list()
|
||||
cachedban["admin_matches_this_round"] = list()
|
||||
cachedban -= "reverting"
|
||||
world.SetConfig("ban", bannedckey, list2stickyban(cachedban))
|
||||
key_cache[key] = 0
|
||||
return null
|
||||
|
||||
//byond will not trigger isbanned() for "global" host bans,
|
||||
//ie, ones where the "apply to this game only" checkbox is not checked (defaults to not checked)
|
||||
//So it's safe to let admins walk thru host/sticky bans here
|
||||
if (admin)
|
||||
log_admin("The admin [key] has been allowed to bypass a matching host/sticky ban on [bannedckey]")
|
||||
message_admins("<span class='adminnotice'>The admin [key] has been allowed to bypass a matching host/sticky ban on [bannedckey]</span>")
|
||||
addclientmessage(ckey,"<span class='adminnotice'>You have been allowed to bypass a matching host/sticky ban on [bannedckey]</span>")
|
||||
key_cache[key] = 0
|
||||
return null
|
||||
|
||||
if (C) //user is already connected!.
|
||||
to_chat(C, "You are about to get disconnected for matching a sticky ban after you connected. If this turns out to be the ban evasion detection system going haywire, we will automatically detect this and revert the matches. if you feel that this is the case, please wait EXACTLY 6 seconds then reconnect using file -> reconnect to see if the match was reversed.")
|
||||
|
||||
var/desc = "\nReason:(StickyBan) You, or another user of this computer or connection ([bannedckey]) is banned from playing here. The ban reason is:\n[ban["message"]]\nThis ban was applied by [ban["admin"]]\nThis is a BanEvasion Detection System ban, if you think this ban is a mistake, please wait EXACTLY 6 seconds, then try again before filing an appeal.\n"
|
||||
. = list("reason" = "Stickyban", "desc" = desc)
|
||||
log_access("Failed Login: [key] [computer_id] [address] - StickyBanned [ban["message"]] Target Username: [bannedckey] Placed by [ban["admin"]]")
|
||||
|
||||
key_cache[key] = 0
|
||||
return .
|
||||
|
||||
|
||||
#undef STICKYBAN_MAX_MATCHES
|
||||
#undef STICKYBAN_MAX_EXISTING_USER_MATCHES
|
||||
#undef STICKYBAN_MAX_ADMIN_MATCHES
|
||||
|
||||
+238
-238
@@ -1,238 +1,238 @@
|
||||
GLOBAL_VAR(CMinutes)
|
||||
GLOBAL_DATUM(Banlist, /savefile)
|
||||
GLOBAL_PROTECT(Banlist)
|
||||
|
||||
|
||||
/proc/CheckBan(ckey, id, address)
|
||||
if(!GLOB.Banlist) // if Banlist cannot be located for some reason
|
||||
LoadBans() // try to load the bans
|
||||
if(!GLOB.Banlist) // uh oh, can't find bans!
|
||||
return 0 // ABORT ABORT ABORT
|
||||
|
||||
. = list()
|
||||
var/appeal
|
||||
var/bran = CONFIG_GET(string/banappeals)
|
||||
if(bran)
|
||||
appeal = "\nFor more information on your ban, or to appeal, head to <a href='[bran]'>[bran]</a>"
|
||||
GLOB.Banlist.cd = "/base"
|
||||
if( "[ckey][id]" in GLOB.Banlist.dir )
|
||||
GLOB.Banlist.cd = "[ckey][id]"
|
||||
if (GLOB.Banlist["temp"])
|
||||
if (!GetExp(GLOB.Banlist["minutes"]))
|
||||
ClearTempbans()
|
||||
return 0
|
||||
else
|
||||
.["desc"] = "\nReason: [GLOB.Banlist["reason"]]\nExpires: [GetExp(GLOB.Banlist["minutes"])]\nBy: [GLOB.Banlist["bannedby"]] during round ID [GLOB.Banlist["roundid"]][appeal]"
|
||||
else
|
||||
GLOB.Banlist.cd = "/base/[ckey][id]"
|
||||
.["desc"] = "\nReason: [GLOB.Banlist["reason"]]\nExpires: <B>PERMANENT</B>\nBy: [GLOB.Banlist["bannedby"]] during round ID [GLOB.Banlist["roundid"]][appeal]"
|
||||
.["reason"] = "ckey/id"
|
||||
return .
|
||||
else
|
||||
for (var/A in GLOB.Banlist.dir)
|
||||
GLOB.Banlist.cd = "/base/[A]"
|
||||
var/matches
|
||||
if( ckey == GLOB.Banlist["key"] )
|
||||
matches += "ckey"
|
||||
if( id == GLOB.Banlist["id"] )
|
||||
if(matches)
|
||||
matches += "/"
|
||||
matches += "id"
|
||||
if( address == GLOB.Banlist["ip"] )
|
||||
if(matches)
|
||||
matches += "/"
|
||||
matches += "ip"
|
||||
|
||||
if(matches)
|
||||
if(GLOB.Banlist["temp"])
|
||||
if (!GetExp(GLOB.Banlist["minutes"]))
|
||||
ClearTempbans()
|
||||
return 0
|
||||
else
|
||||
.["desc"] = "\nReason: [GLOB.Banlist["reason"]]\nExpires: [GetExp(GLOB.Banlist["minutes"])]\nBy: [GLOB.Banlist["bannedby"]] during round ID [GLOB.Banlist["roundid"]][appeal]"
|
||||
else
|
||||
.["desc"] = "\nReason: [GLOB.Banlist["reason"]]\nExpires: <B>PERMENANT</B>\nBy: [GLOB.Banlist["bannedby"]] during round ID [GLOB.Banlist["roundid"]][appeal]"
|
||||
.["reason"] = matches
|
||||
return .
|
||||
return 0
|
||||
|
||||
/proc/UpdateTime() //No idea why i made this a proc.
|
||||
GLOB.CMinutes = (world.realtime / 10) / 60
|
||||
return 1
|
||||
|
||||
/proc/LoadBans()
|
||||
if(!CONFIG_GET(flag/ban_legacy_system))
|
||||
return
|
||||
|
||||
GLOB.Banlist = new("data/banlist.bdb")
|
||||
log_admin("Loading Banlist")
|
||||
|
||||
if (!length(GLOB.Banlist.dir))
|
||||
log_admin("Banlist is empty.")
|
||||
|
||||
if (!GLOB.Banlist.dir.Find("base"))
|
||||
log_admin("Banlist missing base dir.")
|
||||
GLOB.Banlist.dir.Add("base")
|
||||
GLOB.Banlist.cd = "/base"
|
||||
else if (GLOB.Banlist.dir.Find("base"))
|
||||
GLOB.Banlist.cd = "/base"
|
||||
|
||||
ClearTempbans()
|
||||
return 1
|
||||
|
||||
/proc/ClearTempbans()
|
||||
UpdateTime()
|
||||
|
||||
GLOB.Banlist.cd = "/base"
|
||||
for (var/A in GLOB.Banlist.dir)
|
||||
GLOB.Banlist.cd = "/base/[A]"
|
||||
if (!GLOB.Banlist["key"] || !GLOB.Banlist["id"])
|
||||
RemoveBan(A)
|
||||
log_admin("Invalid Ban.")
|
||||
message_admins("Invalid Ban.")
|
||||
continue
|
||||
|
||||
if (!GLOB.Banlist["temp"])
|
||||
continue
|
||||
if (GLOB.CMinutes >= GLOB.Banlist["minutes"])
|
||||
RemoveBan(A)
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
/proc/AddBan(key, computerid, reason, bannedby, temp, minutes, address)
|
||||
|
||||
var/bantimestamp
|
||||
var/ban_ckey = ckey(key)
|
||||
if (temp)
|
||||
UpdateTime()
|
||||
bantimestamp = GLOB.CMinutes + minutes
|
||||
|
||||
GLOB.Banlist.cd = "/base"
|
||||
if ( GLOB.Banlist.dir.Find("[ban_ckey][computerid]") )
|
||||
to_chat(usr, text("<span class='danger'>Ban already exists.</span>"))
|
||||
return 0
|
||||
else
|
||||
GLOB.Banlist.dir.Add("[ban_ckey][computerid]")
|
||||
GLOB.Banlist.cd = "/base/[ban_ckey][computerid]"
|
||||
WRITE_FILE(GLOB.Banlist["key"], ban_ckey)
|
||||
WRITE_FILE(GLOB.Banlist["id"], computerid)
|
||||
WRITE_FILE(GLOB.Banlist["ip"], address)
|
||||
WRITE_FILE(GLOB.Banlist["reason"], reason)
|
||||
WRITE_FILE(GLOB.Banlist["bannedby"], bannedby)
|
||||
WRITE_FILE(GLOB.Banlist["temp"], temp)
|
||||
WRITE_FILE(GLOB.Banlist["roundid"], GLOB.round_id)
|
||||
if (temp)
|
||||
WRITE_FILE(GLOB.Banlist["minutes"], bantimestamp)
|
||||
if(!temp)
|
||||
create_message("note", key, bannedby, "Permanently banned - [reason]", null, null, 0, 0, null, 0, 0)
|
||||
else
|
||||
create_message("note", key, bannedby, "Banned for [minutes] minutes - [reason]", null, null, 0, 0, null, 0, 0)
|
||||
return 1
|
||||
|
||||
/proc/RemoveBan(foldername)
|
||||
var/key
|
||||
var/id
|
||||
|
||||
GLOB.Banlist.cd = "/base/[foldername]"
|
||||
GLOB.Banlist["key"] >> key
|
||||
GLOB.Banlist["id"] >> id
|
||||
GLOB.Banlist.cd = "/base"
|
||||
|
||||
if (!GLOB.Banlist.dir.Remove(foldername))
|
||||
return 0
|
||||
|
||||
if(!usr)
|
||||
log_admin_private("Ban Expired: [key]")
|
||||
message_admins("Ban Expired: [key]")
|
||||
else
|
||||
ban_unban_log_save("[key_name(usr)] unbanned [key]")
|
||||
log_admin_private("[key_name(usr)] unbanned [key]")
|
||||
message_admins("[key_name_admin(usr)] unbanned: [key]")
|
||||
usr.client.holder.DB_ban_unban( ckey(key), BANTYPE_ANY_FULLBAN)
|
||||
for (var/A in GLOB.Banlist.dir)
|
||||
GLOB.Banlist.cd = "/base/[A]"
|
||||
if (key == GLOB.Banlist["key"] /*|| id == Banlist["id"]*/)
|
||||
GLOB.Banlist.cd = "/base"
|
||||
GLOB.Banlist.dir.Remove(A)
|
||||
continue
|
||||
|
||||
return 1
|
||||
|
||||
/proc/GetExp(minutes as num)
|
||||
UpdateTime()
|
||||
var/exp = minutes - GLOB.CMinutes
|
||||
if (exp <= 0)
|
||||
return 0
|
||||
else
|
||||
var/timeleftstring
|
||||
if (exp >= 1440) //1440 = 1 day in minutes
|
||||
timeleftstring = "[round(exp / 1440, 0.1)] Days"
|
||||
else if (exp >= 60) //60 = 1 hour in minutes
|
||||
timeleftstring = "[round(exp / 60, 0.1)] Hours"
|
||||
else
|
||||
timeleftstring = "[exp] Minutes"
|
||||
return timeleftstring
|
||||
|
||||
/datum/admins/proc/unbanpanel()
|
||||
var/count = 0
|
||||
var/dat
|
||||
GLOB.Banlist.cd = "/base"
|
||||
for (var/A in GLOB.Banlist.dir)
|
||||
count++
|
||||
GLOB.Banlist.cd = "/base/[A]"
|
||||
var/ref = "[REF(src)]"
|
||||
var/key = GLOB.Banlist["key"]
|
||||
var/id = GLOB.Banlist["id"]
|
||||
var/ip = GLOB.Banlist["ip"]
|
||||
var/reason = GLOB.Banlist["reason"]
|
||||
var/by = GLOB.Banlist["bannedby"]
|
||||
var/expiry
|
||||
if(GLOB.Banlist["temp"])
|
||||
expiry = GetExp(GLOB.Banlist["minutes"])
|
||||
if(!expiry)
|
||||
expiry = "Removal Pending"
|
||||
else
|
||||
expiry = "Permaban"
|
||||
|
||||
dat += text("<tr><td><A href='?src=[ref];unbanf=[key][id]'>(U)</A><A href='?src=[ref];unbane=[key][id]'>(E)</A> Key: <B>[key]</B></td><td>ComputerID: <B>[id]</B></td><td>IP: <B>[ip]</B></td><td> [expiry]</td><td>(By: [by])</td><td>(Reason: [reason])</td></tr>")
|
||||
|
||||
dat += "</table>"
|
||||
dat = "<HR><B>Bans:</B> <FONT COLOR=blue>(U) = Unban , (E) = Edit Ban</FONT> - <FONT COLOR=green>([count] Bans)</FONT><HR><table border=1 rules=all frame=void cellspacing=0 cellpadding=3 >[dat]"
|
||||
usr << browse(dat, "window=unbanp;size=875x400")
|
||||
|
||||
//////////////////////////////////// DEBUG ////////////////////////////////////
|
||||
|
||||
/proc/CreateBans()
|
||||
|
||||
UpdateTime()
|
||||
|
||||
var/i
|
||||
var/last
|
||||
|
||||
for(i=0, i<1001, i++)
|
||||
var/a = pick(1,0)
|
||||
var/b = pick(1,0)
|
||||
if(b)
|
||||
GLOB.Banlist.cd = "/base"
|
||||
GLOB.Banlist.dir.Add("trash[i]trashid[i]")
|
||||
GLOB.Banlist.cd = "/base/trash[i]trashid[i]"
|
||||
WRITE_FILE(GLOB.Banlist["key"], "trash[i]")
|
||||
else
|
||||
GLOB.Banlist.cd = "/base"
|
||||
GLOB.Banlist.dir.Add("[last]trashid[i]")
|
||||
GLOB.Banlist.cd = "/base/[last]trashid[i]"
|
||||
WRITE_FILE(GLOB.Banlist["key"], last)
|
||||
WRITE_FILE(GLOB.Banlist["id"], "trashid[i]")
|
||||
WRITE_FILE(GLOB.Banlist["reason"], "Trashban[i].")
|
||||
WRITE_FILE(GLOB.Banlist["temp"], a)
|
||||
WRITE_FILE(GLOB.Banlist["minutes"], GLOB.CMinutes + rand(1,2000))
|
||||
WRITE_FILE(GLOB.Banlist["bannedby"], "trashmin")
|
||||
last = "trash[i]"
|
||||
|
||||
GLOB.Banlist.cd = "/base"
|
||||
|
||||
/proc/ClearAllBans()
|
||||
GLOB.Banlist.cd = "/base"
|
||||
for (var/A in GLOB.Banlist.dir)
|
||||
RemoveBan(A)
|
||||
GLOBAL_VAR(CMinutes)
|
||||
GLOBAL_DATUM(Banlist, /savefile)
|
||||
GLOBAL_PROTECT(Banlist)
|
||||
|
||||
|
||||
/proc/CheckBan(ckey, id, address)
|
||||
if(!GLOB.Banlist) // if Banlist cannot be located for some reason
|
||||
LoadBans() // try to load the bans
|
||||
if(!GLOB.Banlist) // uh oh, can't find bans!
|
||||
return 0 // ABORT ABORT ABORT
|
||||
|
||||
. = list()
|
||||
var/appeal
|
||||
var/bran = CONFIG_GET(string/banappeals)
|
||||
if(bran)
|
||||
appeal = "\nFor more information on your ban, or to appeal, head to <a href='[bran]'>[bran]</a>"
|
||||
GLOB.Banlist.cd = "/base"
|
||||
if( "[ckey][id]" in GLOB.Banlist.dir )
|
||||
GLOB.Banlist.cd = "[ckey][id]"
|
||||
if (GLOB.Banlist["temp"])
|
||||
if (!GetExp(GLOB.Banlist["minutes"]))
|
||||
ClearTempbans()
|
||||
return 0
|
||||
else
|
||||
.["desc"] = "\nReason: [GLOB.Banlist["reason"]]\nExpires: [GetExp(GLOB.Banlist["minutes"])]\nBy: [GLOB.Banlist["bannedby"]] during round ID [GLOB.Banlist["roundid"]][appeal]"
|
||||
else
|
||||
GLOB.Banlist.cd = "/base/[ckey][id]"
|
||||
.["desc"] = "\nReason: [GLOB.Banlist["reason"]]\nExpires: <B>PERMANENT</B>\nBy: [GLOB.Banlist["bannedby"]] during round ID [GLOB.Banlist["roundid"]][appeal]"
|
||||
.["reason"] = "ckey/id"
|
||||
return .
|
||||
else
|
||||
for (var/A in GLOB.Banlist.dir)
|
||||
GLOB.Banlist.cd = "/base/[A]"
|
||||
var/matches
|
||||
if( ckey == GLOB.Banlist["key"] )
|
||||
matches += "ckey"
|
||||
if( id == GLOB.Banlist["id"] )
|
||||
if(matches)
|
||||
matches += "/"
|
||||
matches += "id"
|
||||
if( address == GLOB.Banlist["ip"] )
|
||||
if(matches)
|
||||
matches += "/"
|
||||
matches += "ip"
|
||||
|
||||
if(matches)
|
||||
if(GLOB.Banlist["temp"])
|
||||
if (!GetExp(GLOB.Banlist["minutes"]))
|
||||
ClearTempbans()
|
||||
return 0
|
||||
else
|
||||
.["desc"] = "\nReason: [GLOB.Banlist["reason"]]\nExpires: [GetExp(GLOB.Banlist["minutes"])]\nBy: [GLOB.Banlist["bannedby"]] during round ID [GLOB.Banlist["roundid"]][appeal]"
|
||||
else
|
||||
.["desc"] = "\nReason: [GLOB.Banlist["reason"]]\nExpires: <B>PERMENANT</B>\nBy: [GLOB.Banlist["bannedby"]] during round ID [GLOB.Banlist["roundid"]][appeal]"
|
||||
.["reason"] = matches
|
||||
return .
|
||||
return 0
|
||||
|
||||
/proc/UpdateTime() //No idea why i made this a proc.
|
||||
GLOB.CMinutes = (world.realtime / 10) / 60
|
||||
return 1
|
||||
|
||||
/proc/LoadBans()
|
||||
if(!CONFIG_GET(flag/ban_legacy_system))
|
||||
return
|
||||
|
||||
GLOB.Banlist = new("data/banlist.bdb")
|
||||
log_admin("Loading Banlist")
|
||||
|
||||
if (!length(GLOB.Banlist.dir))
|
||||
log_admin("Banlist is empty.")
|
||||
|
||||
if (!GLOB.Banlist.dir.Find("base"))
|
||||
log_admin("Banlist missing base dir.")
|
||||
GLOB.Banlist.dir.Add("base")
|
||||
GLOB.Banlist.cd = "/base"
|
||||
else if (GLOB.Banlist.dir.Find("base"))
|
||||
GLOB.Banlist.cd = "/base"
|
||||
|
||||
ClearTempbans()
|
||||
return 1
|
||||
|
||||
/proc/ClearTempbans()
|
||||
UpdateTime()
|
||||
|
||||
GLOB.Banlist.cd = "/base"
|
||||
for (var/A in GLOB.Banlist.dir)
|
||||
GLOB.Banlist.cd = "/base/[A]"
|
||||
if (!GLOB.Banlist["key"] || !GLOB.Banlist["id"])
|
||||
RemoveBan(A)
|
||||
log_admin("Invalid Ban.")
|
||||
message_admins("Invalid Ban.")
|
||||
continue
|
||||
|
||||
if (!GLOB.Banlist["temp"])
|
||||
continue
|
||||
if (GLOB.CMinutes >= GLOB.Banlist["minutes"])
|
||||
RemoveBan(A)
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
/proc/AddBan(key, computerid, reason, bannedby, temp, minutes, address)
|
||||
|
||||
var/bantimestamp
|
||||
var/ban_ckey = ckey(key)
|
||||
if (temp)
|
||||
UpdateTime()
|
||||
bantimestamp = GLOB.CMinutes + minutes
|
||||
|
||||
GLOB.Banlist.cd = "/base"
|
||||
if ( GLOB.Banlist.dir.Find("[ban_ckey][computerid]") )
|
||||
to_chat(usr, text("<span class='danger'>Ban already exists.</span>"))
|
||||
return 0
|
||||
else
|
||||
GLOB.Banlist.dir.Add("[ban_ckey][computerid]")
|
||||
GLOB.Banlist.cd = "/base/[ban_ckey][computerid]"
|
||||
WRITE_FILE(GLOB.Banlist["key"], ban_ckey)
|
||||
WRITE_FILE(GLOB.Banlist["id"], computerid)
|
||||
WRITE_FILE(GLOB.Banlist["ip"], address)
|
||||
WRITE_FILE(GLOB.Banlist["reason"], reason)
|
||||
WRITE_FILE(GLOB.Banlist["bannedby"], bannedby)
|
||||
WRITE_FILE(GLOB.Banlist["temp"], temp)
|
||||
WRITE_FILE(GLOB.Banlist["roundid"], GLOB.round_id)
|
||||
if (temp)
|
||||
WRITE_FILE(GLOB.Banlist["minutes"], bantimestamp)
|
||||
if(!temp)
|
||||
create_message("note", key, bannedby, "Permanently banned - [reason]", null, null, 0, 0, null, 0, 0)
|
||||
else
|
||||
create_message("note", key, bannedby, "Banned for [minutes] minutes - [reason]", null, null, 0, 0, null, 0, 0)
|
||||
return 1
|
||||
|
||||
/proc/RemoveBan(foldername)
|
||||
var/key
|
||||
var/id
|
||||
|
||||
GLOB.Banlist.cd = "/base/[foldername]"
|
||||
GLOB.Banlist["key"] >> key
|
||||
GLOB.Banlist["id"] >> id
|
||||
GLOB.Banlist.cd = "/base"
|
||||
|
||||
if (!GLOB.Banlist.dir.Remove(foldername))
|
||||
return 0
|
||||
|
||||
if(!usr)
|
||||
log_admin_private("Ban Expired: [key]")
|
||||
message_admins("Ban Expired: [key]")
|
||||
else
|
||||
ban_unban_log_save("[key_name(usr)] unbanned [key]")
|
||||
log_admin_private("[key_name(usr)] unbanned [key]")
|
||||
message_admins("[key_name_admin(usr)] unbanned: [key]")
|
||||
usr.client.holder.DB_ban_unban( ckey(key), BANTYPE_ANY_FULLBAN)
|
||||
for (var/A in GLOB.Banlist.dir)
|
||||
GLOB.Banlist.cd = "/base/[A]"
|
||||
if (key == GLOB.Banlist["key"] /*|| id == Banlist["id"]*/)
|
||||
GLOB.Banlist.cd = "/base"
|
||||
GLOB.Banlist.dir.Remove(A)
|
||||
continue
|
||||
|
||||
return 1
|
||||
|
||||
/proc/GetExp(minutes as num)
|
||||
UpdateTime()
|
||||
var/exp = minutes - GLOB.CMinutes
|
||||
if (exp <= 0)
|
||||
return 0
|
||||
else
|
||||
var/timeleftstring
|
||||
if (exp >= 1440) //1440 = 1 day in minutes
|
||||
timeleftstring = "[round(exp / 1440, 0.1)] Days"
|
||||
else if (exp >= 60) //60 = 1 hour in minutes
|
||||
timeleftstring = "[round(exp / 60, 0.1)] Hours"
|
||||
else
|
||||
timeleftstring = "[exp] Minutes"
|
||||
return timeleftstring
|
||||
|
||||
/datum/admins/proc/unbanpanel()
|
||||
var/count = 0
|
||||
var/dat
|
||||
GLOB.Banlist.cd = "/base"
|
||||
for (var/A in GLOB.Banlist.dir)
|
||||
count++
|
||||
GLOB.Banlist.cd = "/base/[A]"
|
||||
var/ref = "[REF(src)]"
|
||||
var/key = GLOB.Banlist["key"]
|
||||
var/id = GLOB.Banlist["id"]
|
||||
var/ip = GLOB.Banlist["ip"]
|
||||
var/reason = GLOB.Banlist["reason"]
|
||||
var/by = GLOB.Banlist["bannedby"]
|
||||
var/expiry
|
||||
if(GLOB.Banlist["temp"])
|
||||
expiry = GetExp(GLOB.Banlist["minutes"])
|
||||
if(!expiry)
|
||||
expiry = "Removal Pending"
|
||||
else
|
||||
expiry = "Permaban"
|
||||
|
||||
dat += text("<tr><td><A href='?src=[ref];unbanf=[key][id]'>(U)</A><A href='?src=[ref];unbane=[key][id]'>(E)</A> Key: <B>[key]</B></td><td>ComputerID: <B>[id]</B></td><td>IP: <B>[ip]</B></td><td> [expiry]</td><td>(By: [by])</td><td>(Reason: [reason])</td></tr>")
|
||||
|
||||
dat += "</table>"
|
||||
dat = "<HR><B>Bans:</B> <FONT COLOR=blue>(U) = Unban , (E) = Edit Ban</FONT> - <FONT COLOR=green>([count] Bans)</FONT><HR><table border=1 rules=all frame=void cellspacing=0 cellpadding=3 >[dat]"
|
||||
usr << browse(dat, "window=unbanp;size=875x400")
|
||||
|
||||
//////////////////////////////////// DEBUG ////////////////////////////////////
|
||||
|
||||
/proc/CreateBans()
|
||||
|
||||
UpdateTime()
|
||||
|
||||
var/i
|
||||
var/last
|
||||
|
||||
for(i=0, i<1001, i++)
|
||||
var/a = pick(1,0)
|
||||
var/b = pick(1,0)
|
||||
if(b)
|
||||
GLOB.Banlist.cd = "/base"
|
||||
GLOB.Banlist.dir.Add("trash[i]trashid[i]")
|
||||
GLOB.Banlist.cd = "/base/trash[i]trashid[i]"
|
||||
WRITE_FILE(GLOB.Banlist["key"], "trash[i]")
|
||||
else
|
||||
GLOB.Banlist.cd = "/base"
|
||||
GLOB.Banlist.dir.Add("[last]trashid[i]")
|
||||
GLOB.Banlist.cd = "/base/[last]trashid[i]"
|
||||
WRITE_FILE(GLOB.Banlist["key"], last)
|
||||
WRITE_FILE(GLOB.Banlist["id"], "trashid[i]")
|
||||
WRITE_FILE(GLOB.Banlist["reason"], "Trashban[i].")
|
||||
WRITE_FILE(GLOB.Banlist["temp"], a)
|
||||
WRITE_FILE(GLOB.Banlist["minutes"], GLOB.CMinutes + rand(1,2000))
|
||||
WRITE_FILE(GLOB.Banlist["bannedby"], "trashmin")
|
||||
last = "trash[i]"
|
||||
|
||||
GLOB.Banlist.cd = "/base"
|
||||
|
||||
/proc/ClearAllBans()
|
||||
GLOB.Banlist.cd = "/base"
|
||||
for (var/A in GLOB.Banlist.dir)
|
||||
RemoveBan(A)
|
||||
|
||||
+1011
-1011
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,22 @@
|
||||
/atom/proc/investigate_log(message, subject)
|
||||
if(!message || !subject)
|
||||
return
|
||||
var/F = file("[GLOB.log_directory]/[subject].html")
|
||||
WRITE_FILE(F, "<small>[TIME_STAMP("hh:mm:ss", FALSE)] [REF(src)] ([x],[y],[z])</small> || [src] [message]<br>")
|
||||
|
||||
/client/proc/investigate_show(subject in list("notes, memos, watchlist", INVESTIGATE_RCD, INVESTIGATE_RESEARCH, INVESTIGATE_EXONET, INVESTIGATE_PORTAL, INVESTIGATE_SINGULO, INVESTIGATE_WIRES, INVESTIGATE_TELESCI, INVESTIGATE_GRAVITY, INVESTIGATE_RECORDS, INVESTIGATE_CARGO, INVESTIGATE_SUPERMATTER, INVESTIGATE_ATMOS, INVESTIGATE_EXPERIMENTOR, INVESTIGATE_BOTANY, INVESTIGATE_HALLUCINATIONS, INVESTIGATE_RADIATION, INVESTIGATE_CIRCUIT, INVESTIGATE_NANITES) )
|
||||
set name = "Investigate"
|
||||
set category = "Admin"
|
||||
if(!holder)
|
||||
return
|
||||
switch(subject)
|
||||
if("notes, memos, watchlist")
|
||||
if(!check_rights(R_ADMIN))
|
||||
return
|
||||
browse_messages()
|
||||
else
|
||||
var/F = file("[GLOB.log_directory]/[subject].html")
|
||||
if(!fexists(F))
|
||||
to_chat(src, "<span class='danger'>No [subject] logfile was found.</span>")
|
||||
return
|
||||
src << browse(F,"window=investigate[subject];size=800x300")
|
||||
/atom/proc/investigate_log(message, subject)
|
||||
if(!message || !subject)
|
||||
return
|
||||
var/F = file("[GLOB.log_directory]/[subject].html")
|
||||
WRITE_FILE(F, "<small>[TIME_STAMP("hh:mm:ss", FALSE)] [REF(src)] ([x],[y],[z])</small> || [src] [message]<br>")
|
||||
|
||||
/client/proc/investigate_show(subject in list("notes, memos, watchlist", INVESTIGATE_RCD, INVESTIGATE_RESEARCH, INVESTIGATE_EXONET, INVESTIGATE_PORTAL, INVESTIGATE_SINGULO, INVESTIGATE_WIRES, INVESTIGATE_TELESCI, INVESTIGATE_GRAVITY, INVESTIGATE_RECORDS, INVESTIGATE_CARGO, INVESTIGATE_SUPERMATTER, INVESTIGATE_ATMOS, INVESTIGATE_EXPERIMENTOR, INVESTIGATE_BOTANY, INVESTIGATE_HALLUCINATIONS, INVESTIGATE_RADIATION, INVESTIGATE_CIRCUIT, INVESTIGATE_NANITES) )
|
||||
set name = "Investigate"
|
||||
set category = "Admin"
|
||||
if(!holder)
|
||||
return
|
||||
switch(subject)
|
||||
if("notes, memos, watchlist")
|
||||
if(!check_rights(R_ADMIN))
|
||||
return
|
||||
browse_messages()
|
||||
else
|
||||
var/F = file("[GLOB.log_directory]/[subject].html")
|
||||
if(!fexists(F))
|
||||
to_chat(src, "<span class='danger'>No [subject] logfile was found.</span>")
|
||||
return
|
||||
src << browse(F,"window=investigate[subject];size=800x300")
|
||||
|
||||
+306
-306
@@ -1,306 +1,306 @@
|
||||
GLOBAL_LIST_EMPTY(admin_ranks) //list of all admin_rank datums
|
||||
GLOBAL_PROTECT(admin_ranks)
|
||||
|
||||
GLOBAL_LIST_EMPTY(protected_ranks) //admin ranks loaded from txt
|
||||
GLOBAL_PROTECT(protected_ranks)
|
||||
|
||||
/datum/admin_rank
|
||||
var/name = "NoRank"
|
||||
var/rights = R_DEFAULT
|
||||
var/exclude_rights = 0
|
||||
var/include_rights = 0
|
||||
var/can_edit_rights = 0
|
||||
|
||||
/datum/admin_rank/New(init_name, init_rights, init_exclude_rights, init_edit_rights)
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
if (name == "NoRank") //only del if this is a true creation (and not just a New() proc call), other wise trialmins/coders could abuse this to deadmin other admins
|
||||
QDEL_IN(src, 0)
|
||||
CRASH("Admin proc call creation of admin datum")
|
||||
return
|
||||
name = init_name
|
||||
if(!name)
|
||||
qdel(src)
|
||||
throw EXCEPTION("Admin rank created without name.")
|
||||
return
|
||||
if(init_rights)
|
||||
rights = init_rights
|
||||
include_rights = rights
|
||||
if(init_exclude_rights)
|
||||
exclude_rights = init_exclude_rights
|
||||
rights &= ~exclude_rights
|
||||
if(init_edit_rights)
|
||||
can_edit_rights = init_edit_rights
|
||||
|
||||
/datum/admin_rank/Destroy()
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return QDEL_HINT_LETMELIVE
|
||||
. = ..()
|
||||
|
||||
/datum/admin_rank/vv_edit_var(var_name, var_value)
|
||||
return FALSE
|
||||
|
||||
/proc/admin_keyword_to_flag(word, previous_rights=0)
|
||||
var/flag = 0
|
||||
switch(ckey(word))
|
||||
if("buildmode","build")
|
||||
flag = R_BUILDMODE
|
||||
if("admin")
|
||||
flag = R_ADMIN
|
||||
if("ban")
|
||||
flag = R_BAN
|
||||
if("fun")
|
||||
flag = R_FUN
|
||||
if("server")
|
||||
flag = R_SERVER
|
||||
if("debug")
|
||||
flag = R_DEBUG
|
||||
if("permissions","rights")
|
||||
flag = R_PERMISSIONS
|
||||
if("possess")
|
||||
flag = R_POSSESS
|
||||
if("stealth")
|
||||
flag = R_STEALTH
|
||||
if("poll")
|
||||
flag = R_POLL
|
||||
if("varedit")
|
||||
flag = R_VAREDIT
|
||||
if("everything","host","all")
|
||||
flag = R_EVERYTHING
|
||||
if("sound","sounds")
|
||||
flag = R_SOUNDS
|
||||
if("spawn","create")
|
||||
flag = R_SPAWN
|
||||
if("autologin", "autoadmin")
|
||||
flag = R_AUTOLOGIN
|
||||
if("dbranks")
|
||||
flag = R_DBRANKS
|
||||
if("@","prev")
|
||||
flag = previous_rights
|
||||
return flag
|
||||
|
||||
// Adds/removes rights to this admin_rank
|
||||
/datum/admin_rank/proc/process_keyword(word, previous_rights=0)
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return
|
||||
var/flag = admin_keyword_to_flag(word, previous_rights)
|
||||
if(flag)
|
||||
switch(text2ascii(word,1))
|
||||
if(43)
|
||||
rights |= flag //+
|
||||
include_rights |= flag
|
||||
if(45)
|
||||
rights &= ~flag //-
|
||||
exclude_rights |= flag
|
||||
if(42)
|
||||
can_edit_rights |= flag //*
|
||||
|
||||
// Checks for (keyword-formatted) rights on this admin
|
||||
/datum/admins/proc/check_keyword(word)
|
||||
var/flag = admin_keyword_to_flag(word)
|
||||
if(flag)
|
||||
return ((rank.rights & flag) == flag) //true only if right has everything in flag
|
||||
|
||||
/proc/sync_ranks_with_db()
|
||||
set waitfor = FALSE
|
||||
|
||||
if(IsAdminAdvancedProcCall())
|
||||
to_chat(usr, "<span class='admin prefix'>Admin rank DB Sync blocked: Advanced ProcCall detected.</span>")
|
||||
return
|
||||
|
||||
var/list/sql_ranks = list()
|
||||
for(var/datum/admin_rank/R in GLOB.protected_ranks)
|
||||
var/sql_rank = sanitizeSQL(R.name)
|
||||
var/sql_flags = sanitizeSQL(R.include_rights)
|
||||
var/sql_exclude_flags = sanitizeSQL(R.exclude_rights)
|
||||
var/sql_can_edit_flags = sanitizeSQL(R.can_edit_rights)
|
||||
sql_ranks += list(list("rank" = "'[sql_rank]'", "flags" = "[sql_flags]", "exclude_flags" = "[sql_exclude_flags]", "can_edit_flags" = "[sql_can_edit_flags]"))
|
||||
SSdbcore.MassInsert(format_table_name("admin_ranks"), sql_ranks, duplicate_key = TRUE)
|
||||
|
||||
//load our rank - > rights associations
|
||||
/proc/load_admin_ranks(dbfail, no_update)
|
||||
if(IsAdminAdvancedProcCall())
|
||||
to_chat(usr, "<span class='admin prefix'>Admin Reload blocked: Advanced ProcCall detected.</span>")
|
||||
return
|
||||
GLOB.admin_ranks.Cut()
|
||||
GLOB.protected_ranks.Cut()
|
||||
var/previous_rights = 0
|
||||
//load text from file and process each line separately
|
||||
for(var/line in world.file2list("[global.config.directory]/admin_ranks.txt"))
|
||||
if(!line || findtextEx(line,"#",1,2))
|
||||
continue
|
||||
var/next = findtext(line, "=")
|
||||
var/datum/admin_rank/R = new(ckeyEx(copytext(line, 1, next)))
|
||||
if(!R)
|
||||
continue
|
||||
GLOB.admin_ranks += R
|
||||
GLOB.protected_ranks += R
|
||||
var/prev = findchar(line, "+-*", next, 0)
|
||||
while(prev)
|
||||
next = findchar(line, "+-*", prev + 1, 0)
|
||||
R.process_keyword(copytext(line, prev, next), previous_rights)
|
||||
prev = next
|
||||
previous_rights = R.rights
|
||||
if(!CONFIG_GET(flag/admin_legacy_system) || dbfail)
|
||||
if(CONFIG_GET(flag/load_legacy_ranks_only))
|
||||
if(!no_update)
|
||||
sync_ranks_with_db()
|
||||
else
|
||||
var/datum/DBQuery/query_load_admin_ranks = SSdbcore.NewQuery("SELECT rank, flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")]")
|
||||
if(!query_load_admin_ranks.Execute())
|
||||
message_admins("Error loading admin ranks from database. Loading from backup.")
|
||||
log_sql("Error loading admin ranks from database. Loading from backup.")
|
||||
dbfail = 1
|
||||
else
|
||||
while(query_load_admin_ranks.NextRow())
|
||||
var/skip
|
||||
var/rank_name = ckeyEx(query_load_admin_ranks.item[1])
|
||||
for(var/datum/admin_rank/R in GLOB.admin_ranks)
|
||||
if(R.name == rank_name) //this rank was already loaded from txt override
|
||||
skip = 1
|
||||
break
|
||||
if(!skip)
|
||||
var/rank_flags = text2num(query_load_admin_ranks.item[2])
|
||||
var/rank_exclude_flags = text2num(query_load_admin_ranks.item[3])
|
||||
var/rank_can_edit_flags = text2num(query_load_admin_ranks.item[4])
|
||||
var/datum/admin_rank/R = new(rank_name, rank_flags, rank_exclude_flags, rank_can_edit_flags)
|
||||
if(!R)
|
||||
continue
|
||||
GLOB.admin_ranks += R
|
||||
qdel(query_load_admin_ranks)
|
||||
//load ranks from backup file
|
||||
if(dbfail)
|
||||
var/backup_file = file2text("data/admins_backup.json")
|
||||
if(backup_file == null)
|
||||
log_world("Unable to locate admins backup file.")
|
||||
return FALSE
|
||||
var/list/json = json_decode(backup_file)
|
||||
for(var/J in json["ranks"])
|
||||
var/skip
|
||||
for(var/datum/admin_rank/R in GLOB.admin_ranks)
|
||||
if(R.name == "[J]") //this rank was already loaded from txt override
|
||||
skip = TRUE
|
||||
if(skip)
|
||||
continue
|
||||
var/datum/admin_rank/R = new("[J]", json["ranks"]["[J]"]["include rights"], json["ranks"]["[J]"]["exclude rights"], json["ranks"]["[J]"]["can edit rights"])
|
||||
if(!R)
|
||||
continue
|
||||
GLOB.admin_ranks += R
|
||||
return json
|
||||
#ifdef TESTING
|
||||
var/msg = "Permission Sets Built:\n"
|
||||
for(var/datum/admin_rank/R in GLOB.admin_ranks)
|
||||
msg += "\t[R.name]"
|
||||
var/rights = rights2text(R.rights,"\n\t\t")
|
||||
if(rights)
|
||||
msg += "\t\t[rights]\n"
|
||||
testing(msg)
|
||||
#endif
|
||||
|
||||
/proc/load_admins(no_update)
|
||||
var/dbfail
|
||||
if(!CONFIG_GET(flag/admin_legacy_system) && !SSdbcore.Connect())
|
||||
message_admins("Failed to connect to database while loading admins. Loading from backup.")
|
||||
log_sql("Failed to connect to database while loading admins. Loading from backup.")
|
||||
dbfail = 1
|
||||
//clear the datums references
|
||||
GLOB.admin_datums.Cut()
|
||||
for(var/client/C in GLOB.admins)
|
||||
C.remove_admin_verbs()
|
||||
C.holder = null
|
||||
GLOB.admins.Cut()
|
||||
GLOB.protected_admins.Cut()
|
||||
GLOB.deadmins.Cut()
|
||||
var/list/backup_file_json = load_admin_ranks(dbfail, no_update)
|
||||
dbfail = backup_file_json != null
|
||||
//Clear profile access
|
||||
for(var/A in world.GetConfig("admin"))
|
||||
world.SetConfig("APP/admin", A, null)
|
||||
var/list/rank_names = list()
|
||||
for(var/datum/admin_rank/R in GLOB.admin_ranks)
|
||||
rank_names[R.name] = R
|
||||
//ckeys listed in admins.txt are always made admins before sql loading is attempted
|
||||
var/list/lines = world.file2list("[global.config.directory]/admins.txt")
|
||||
for(var/line in lines)
|
||||
if(!length(line) || findtextEx(line, "#", 1, 2))
|
||||
continue
|
||||
var/list/entry = splittext(line, "=")
|
||||
if(entry.len < 2)
|
||||
continue
|
||||
var/ckey = ckey(entry[1])
|
||||
var/rank = ckeyEx(entry[2])
|
||||
if(!ckey || !rank)
|
||||
continue
|
||||
new /datum/admins(rank_names[rank], ckey, 0, 1)
|
||||
if(!CONFIG_GET(flag/admin_legacy_system) || dbfail)
|
||||
var/datum/DBQuery/query_load_admins = SSdbcore.NewQuery("SELECT ckey, rank FROM [format_table_name("admin")] ORDER BY rank")
|
||||
if(!query_load_admins.Execute())
|
||||
message_admins("Error loading admins from database. Loading from backup.")
|
||||
log_sql("Error loading admins from database. Loading from backup.")
|
||||
dbfail = 1
|
||||
else
|
||||
while(query_load_admins.NextRow())
|
||||
var/admin_ckey = ckey(query_load_admins.item[1])
|
||||
var/admin_rank = ckeyEx(query_load_admins.item[2])
|
||||
var/skip
|
||||
if(rank_names[admin_rank] == null)
|
||||
message_admins("[admin_ckey] loaded with invalid admin rank [admin_rank].")
|
||||
skip = 1
|
||||
if(GLOB.admin_datums[admin_ckey] || GLOB.deadmins[admin_ckey])
|
||||
skip = 1
|
||||
if(!skip)
|
||||
new /datum/admins(rank_names[admin_rank], admin_ckey)
|
||||
qdel(query_load_admins)
|
||||
//load admins from backup file
|
||||
if(dbfail)
|
||||
if(!backup_file_json)
|
||||
if(backup_file_json != null)
|
||||
//already tried
|
||||
return
|
||||
var/backup_file = file2text("data/admins_backup.json")
|
||||
if(backup_file == null)
|
||||
log_world("Unable to locate admins backup file.")
|
||||
return
|
||||
backup_file_json = json_decode(backup_file)
|
||||
for(var/J in backup_file_json["admins"])
|
||||
var/skip
|
||||
for(var/A in GLOB.admin_datums + GLOB.deadmins)
|
||||
if(A == "[J]") //this admin was already loaded from txt override
|
||||
skip = TRUE
|
||||
if(skip)
|
||||
continue
|
||||
new /datum/admins(rank_names[ckeyEx(backup_file_json["admins"]["[J]"])], ckey("[J]"))
|
||||
#ifdef TESTING
|
||||
var/msg = "Admins Built:\n"
|
||||
for(var/ckey in GLOB.admin_datums)
|
||||
var/datum/admins/D = GLOB.admin_datums[ckey]
|
||||
msg += "\t[ckey] - [D.rank.name]\n"
|
||||
testing(msg)
|
||||
#endif
|
||||
return dbfail
|
||||
|
||||
#ifdef TESTING
|
||||
/client/verb/changerank(newrank in GLOB.admin_ranks)
|
||||
if(holder)
|
||||
holder.rank = newrank
|
||||
else
|
||||
holder = new /datum/admins(newrank, ckey)
|
||||
remove_admin_verbs()
|
||||
holder.associate(src)
|
||||
|
||||
/client/verb/changerights(newrights as num)
|
||||
if(holder)
|
||||
holder.rank.rights = newrights
|
||||
else
|
||||
holder = new /datum/admins("testing", newrights, ckey)
|
||||
remove_admin_verbs()
|
||||
holder.associate(src)
|
||||
#endif
|
||||
GLOBAL_LIST_EMPTY(admin_ranks) //list of all admin_rank datums
|
||||
GLOBAL_PROTECT(admin_ranks)
|
||||
|
||||
GLOBAL_LIST_EMPTY(protected_ranks) //admin ranks loaded from txt
|
||||
GLOBAL_PROTECT(protected_ranks)
|
||||
|
||||
/datum/admin_rank
|
||||
var/name = "NoRank"
|
||||
var/rights = R_DEFAULT
|
||||
var/exclude_rights = 0
|
||||
var/include_rights = 0
|
||||
var/can_edit_rights = 0
|
||||
|
||||
/datum/admin_rank/New(init_name, init_rights, init_exclude_rights, init_edit_rights)
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
if (name == "NoRank") //only del if this is a true creation (and not just a New() proc call), other wise trialmins/coders could abuse this to deadmin other admins
|
||||
QDEL_IN(src, 0)
|
||||
CRASH("Admin proc call creation of admin datum")
|
||||
return
|
||||
name = init_name
|
||||
if(!name)
|
||||
qdel(src)
|
||||
throw EXCEPTION("Admin rank created without name.")
|
||||
return
|
||||
if(init_rights)
|
||||
rights = init_rights
|
||||
include_rights = rights
|
||||
if(init_exclude_rights)
|
||||
exclude_rights = init_exclude_rights
|
||||
rights &= ~exclude_rights
|
||||
if(init_edit_rights)
|
||||
can_edit_rights = init_edit_rights
|
||||
|
||||
/datum/admin_rank/Destroy()
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return QDEL_HINT_LETMELIVE
|
||||
. = ..()
|
||||
|
||||
/datum/admin_rank/vv_edit_var(var_name, var_value)
|
||||
return FALSE
|
||||
|
||||
/proc/admin_keyword_to_flag(word, previous_rights=0)
|
||||
var/flag = 0
|
||||
switch(ckey(word))
|
||||
if("buildmode","build")
|
||||
flag = R_BUILDMODE
|
||||
if("admin")
|
||||
flag = R_ADMIN
|
||||
if("ban")
|
||||
flag = R_BAN
|
||||
if("fun")
|
||||
flag = R_FUN
|
||||
if("server")
|
||||
flag = R_SERVER
|
||||
if("debug")
|
||||
flag = R_DEBUG
|
||||
if("permissions","rights")
|
||||
flag = R_PERMISSIONS
|
||||
if("possess")
|
||||
flag = R_POSSESS
|
||||
if("stealth")
|
||||
flag = R_STEALTH
|
||||
if("poll")
|
||||
flag = R_POLL
|
||||
if("varedit")
|
||||
flag = R_VAREDIT
|
||||
if("everything","host","all")
|
||||
flag = R_EVERYTHING
|
||||
if("sound","sounds")
|
||||
flag = R_SOUNDS
|
||||
if("spawn","create")
|
||||
flag = R_SPAWN
|
||||
if("autologin", "autoadmin")
|
||||
flag = R_AUTOLOGIN
|
||||
if("dbranks")
|
||||
flag = R_DBRANKS
|
||||
if("@","prev")
|
||||
flag = previous_rights
|
||||
return flag
|
||||
|
||||
// Adds/removes rights to this admin_rank
|
||||
/datum/admin_rank/proc/process_keyword(word, previous_rights=0)
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return
|
||||
var/flag = admin_keyword_to_flag(word, previous_rights)
|
||||
if(flag)
|
||||
switch(text2ascii(word,1))
|
||||
if(43)
|
||||
rights |= flag //+
|
||||
include_rights |= flag
|
||||
if(45)
|
||||
rights &= ~flag //-
|
||||
exclude_rights |= flag
|
||||
if(42)
|
||||
can_edit_rights |= flag //*
|
||||
|
||||
// Checks for (keyword-formatted) rights on this admin
|
||||
/datum/admins/proc/check_keyword(word)
|
||||
var/flag = admin_keyword_to_flag(word)
|
||||
if(flag)
|
||||
return ((rank.rights & flag) == flag) //true only if right has everything in flag
|
||||
|
||||
/proc/sync_ranks_with_db()
|
||||
set waitfor = FALSE
|
||||
|
||||
if(IsAdminAdvancedProcCall())
|
||||
to_chat(usr, "<span class='admin prefix'>Admin rank DB Sync blocked: Advanced ProcCall detected.</span>")
|
||||
return
|
||||
|
||||
var/list/sql_ranks = list()
|
||||
for(var/datum/admin_rank/R in GLOB.protected_ranks)
|
||||
var/sql_rank = sanitizeSQL(R.name)
|
||||
var/sql_flags = sanitizeSQL(R.include_rights)
|
||||
var/sql_exclude_flags = sanitizeSQL(R.exclude_rights)
|
||||
var/sql_can_edit_flags = sanitizeSQL(R.can_edit_rights)
|
||||
sql_ranks += list(list("rank" = "'[sql_rank]'", "flags" = "[sql_flags]", "exclude_flags" = "[sql_exclude_flags]", "can_edit_flags" = "[sql_can_edit_flags]"))
|
||||
SSdbcore.MassInsert(format_table_name("admin_ranks"), sql_ranks, duplicate_key = TRUE)
|
||||
|
||||
//load our rank - > rights associations
|
||||
/proc/load_admin_ranks(dbfail, no_update)
|
||||
if(IsAdminAdvancedProcCall())
|
||||
to_chat(usr, "<span class='admin prefix'>Admin Reload blocked: Advanced ProcCall detected.</span>")
|
||||
return
|
||||
GLOB.admin_ranks.Cut()
|
||||
GLOB.protected_ranks.Cut()
|
||||
var/previous_rights = 0
|
||||
//load text from file and process each line separately
|
||||
for(var/line in world.file2list("[global.config.directory]/admin_ranks.txt"))
|
||||
if(!line || findtextEx(line,"#",1,2))
|
||||
continue
|
||||
var/next = findtext(line, "=")
|
||||
var/datum/admin_rank/R = new(ckeyEx(copytext(line, 1, next)))
|
||||
if(!R)
|
||||
continue
|
||||
GLOB.admin_ranks += R
|
||||
GLOB.protected_ranks += R
|
||||
var/prev = findchar(line, "+-*", next, 0)
|
||||
while(prev)
|
||||
next = findchar(line, "+-*", prev + 1, 0)
|
||||
R.process_keyword(copytext(line, prev, next), previous_rights)
|
||||
prev = next
|
||||
previous_rights = R.rights
|
||||
if(!CONFIG_GET(flag/admin_legacy_system) || dbfail)
|
||||
if(CONFIG_GET(flag/load_legacy_ranks_only))
|
||||
if(!no_update)
|
||||
sync_ranks_with_db()
|
||||
else
|
||||
var/datum/DBQuery/query_load_admin_ranks = SSdbcore.NewQuery("SELECT rank, flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")]")
|
||||
if(!query_load_admin_ranks.Execute())
|
||||
message_admins("Error loading admin ranks from database. Loading from backup.")
|
||||
log_sql("Error loading admin ranks from database. Loading from backup.")
|
||||
dbfail = 1
|
||||
else
|
||||
while(query_load_admin_ranks.NextRow())
|
||||
var/skip
|
||||
var/rank_name = ckeyEx(query_load_admin_ranks.item[1])
|
||||
for(var/datum/admin_rank/R in GLOB.admin_ranks)
|
||||
if(R.name == rank_name) //this rank was already loaded from txt override
|
||||
skip = 1
|
||||
break
|
||||
if(!skip)
|
||||
var/rank_flags = text2num(query_load_admin_ranks.item[2])
|
||||
var/rank_exclude_flags = text2num(query_load_admin_ranks.item[3])
|
||||
var/rank_can_edit_flags = text2num(query_load_admin_ranks.item[4])
|
||||
var/datum/admin_rank/R = new(rank_name, rank_flags, rank_exclude_flags, rank_can_edit_flags)
|
||||
if(!R)
|
||||
continue
|
||||
GLOB.admin_ranks += R
|
||||
qdel(query_load_admin_ranks)
|
||||
//load ranks from backup file
|
||||
if(dbfail)
|
||||
var/backup_file = file2text("data/admins_backup.json")
|
||||
if(backup_file == null)
|
||||
log_world("Unable to locate admins backup file.")
|
||||
return FALSE
|
||||
var/list/json = json_decode(backup_file)
|
||||
for(var/J in json["ranks"])
|
||||
var/skip
|
||||
for(var/datum/admin_rank/R in GLOB.admin_ranks)
|
||||
if(R.name == "[J]") //this rank was already loaded from txt override
|
||||
skip = TRUE
|
||||
if(skip)
|
||||
continue
|
||||
var/datum/admin_rank/R = new("[J]", json["ranks"]["[J]"]["include rights"], json["ranks"]["[J]"]["exclude rights"], json["ranks"]["[J]"]["can edit rights"])
|
||||
if(!R)
|
||||
continue
|
||||
GLOB.admin_ranks += R
|
||||
return json
|
||||
#ifdef TESTING
|
||||
var/msg = "Permission Sets Built:\n"
|
||||
for(var/datum/admin_rank/R in GLOB.admin_ranks)
|
||||
msg += "\t[R.name]"
|
||||
var/rights = rights2text(R.rights,"\n\t\t")
|
||||
if(rights)
|
||||
msg += "\t\t[rights]\n"
|
||||
testing(msg)
|
||||
#endif
|
||||
|
||||
/proc/load_admins(no_update)
|
||||
var/dbfail
|
||||
if(!CONFIG_GET(flag/admin_legacy_system) && !SSdbcore.Connect())
|
||||
message_admins("Failed to connect to database while loading admins. Loading from backup.")
|
||||
log_sql("Failed to connect to database while loading admins. Loading from backup.")
|
||||
dbfail = 1
|
||||
//clear the datums references
|
||||
GLOB.admin_datums.Cut()
|
||||
for(var/client/C in GLOB.admins)
|
||||
C.remove_admin_verbs()
|
||||
C.holder = null
|
||||
GLOB.admins.Cut()
|
||||
GLOB.protected_admins.Cut()
|
||||
GLOB.deadmins.Cut()
|
||||
var/list/backup_file_json = load_admin_ranks(dbfail, no_update)
|
||||
dbfail = backup_file_json != null
|
||||
//Clear profile access
|
||||
for(var/A in world.GetConfig("admin"))
|
||||
world.SetConfig("APP/admin", A, null)
|
||||
var/list/rank_names = list()
|
||||
for(var/datum/admin_rank/R in GLOB.admin_ranks)
|
||||
rank_names[R.name] = R
|
||||
//ckeys listed in admins.txt are always made admins before sql loading is attempted
|
||||
var/list/lines = world.file2list("[global.config.directory]/admins.txt")
|
||||
for(var/line in lines)
|
||||
if(!length(line) || findtextEx(line, "#", 1, 2))
|
||||
continue
|
||||
var/list/entry = splittext(line, "=")
|
||||
if(entry.len < 2)
|
||||
continue
|
||||
var/ckey = ckey(entry[1])
|
||||
var/rank = ckeyEx(entry[2])
|
||||
if(!ckey || !rank)
|
||||
continue
|
||||
new /datum/admins(rank_names[rank], ckey, 0, 1)
|
||||
if(!CONFIG_GET(flag/admin_legacy_system) || dbfail)
|
||||
var/datum/DBQuery/query_load_admins = SSdbcore.NewQuery("SELECT ckey, rank FROM [format_table_name("admin")] ORDER BY rank")
|
||||
if(!query_load_admins.Execute())
|
||||
message_admins("Error loading admins from database. Loading from backup.")
|
||||
log_sql("Error loading admins from database. Loading from backup.")
|
||||
dbfail = 1
|
||||
else
|
||||
while(query_load_admins.NextRow())
|
||||
var/admin_ckey = ckey(query_load_admins.item[1])
|
||||
var/admin_rank = ckeyEx(query_load_admins.item[2])
|
||||
var/skip
|
||||
if(rank_names[admin_rank] == null)
|
||||
message_admins("[admin_ckey] loaded with invalid admin rank [admin_rank].")
|
||||
skip = 1
|
||||
if(GLOB.admin_datums[admin_ckey] || GLOB.deadmins[admin_ckey])
|
||||
skip = 1
|
||||
if(!skip)
|
||||
new /datum/admins(rank_names[admin_rank], admin_ckey)
|
||||
qdel(query_load_admins)
|
||||
//load admins from backup file
|
||||
if(dbfail)
|
||||
if(!backup_file_json)
|
||||
if(backup_file_json != null)
|
||||
//already tried
|
||||
return
|
||||
var/backup_file = file2text("data/admins_backup.json")
|
||||
if(backup_file == null)
|
||||
log_world("Unable to locate admins backup file.")
|
||||
return
|
||||
backup_file_json = json_decode(backup_file)
|
||||
for(var/J in backup_file_json["admins"])
|
||||
var/skip
|
||||
for(var/A in GLOB.admin_datums + GLOB.deadmins)
|
||||
if(A == "[J]") //this admin was already loaded from txt override
|
||||
skip = TRUE
|
||||
if(skip)
|
||||
continue
|
||||
new /datum/admins(rank_names[ckeyEx(backup_file_json["admins"]["[J]"])], ckey("[J]"))
|
||||
#ifdef TESTING
|
||||
var/msg = "Admins Built:\n"
|
||||
for(var/ckey in GLOB.admin_datums)
|
||||
var/datum/admins/D = GLOB.admin_datums[ckey]
|
||||
msg += "\t[ckey] - [D.rank.name]\n"
|
||||
testing(msg)
|
||||
#endif
|
||||
return dbfail
|
||||
|
||||
#ifdef TESTING
|
||||
/client/verb/changerank(newrank in GLOB.admin_ranks)
|
||||
if(holder)
|
||||
holder.rank = newrank
|
||||
else
|
||||
holder = new /datum/admins(newrank, ckey)
|
||||
remove_admin_verbs()
|
||||
holder.associate(src)
|
||||
|
||||
/client/verb/changerights(newrights as num)
|
||||
if(holder)
|
||||
holder.rank.rights = newrights
|
||||
else
|
||||
holder = new /datum/admins("testing", newrights, ckey)
|
||||
remove_admin_verbs()
|
||||
holder.associate(src)
|
||||
#endif
|
||||
|
||||
@@ -438,6 +438,8 @@ GLOBAL_PROTECT(admin_verbs_hideable)
|
||||
set category = "Admin"
|
||||
set name = "Stealth Mode"
|
||||
if(holder)
|
||||
if(!check_rights(R_STEALTH, 0))
|
||||
return
|
||||
if(holder.fakekey)
|
||||
holder.fakekey = null
|
||||
if(isobserver(mob))
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
//returns a reason if M is banned from rank, returns FALSE otherwise
|
||||
/proc/jobban_isbanned(mob/M, rank)
|
||||
if(!M || !istype(M) || !M.ckey)
|
||||
return FALSE
|
||||
|
||||
if(!M.client) //no cache. fallback to a datum/DBQuery
|
||||
var/datum/DBQuery/query_jobban_check_ban = SSdbcore.NewQuery("SELECT reason FROM [format_table_name("ban")] WHERE ckey = '[sanitizeSQL(M.ckey)]' AND (bantype = 'JOB_PERMABAN' OR (bantype = 'JOB_TEMPBAN' AND expiration_time > Now())) AND isnull(unbanned) AND job = '[sanitizeSQL(rank)]'")
|
||||
if(!query_jobban_check_ban.warn_execute())
|
||||
qdel(query_jobban_check_ban)
|
||||
return
|
||||
if(query_jobban_check_ban.NextRow())
|
||||
var/reason = query_jobban_check_ban.item[1]
|
||||
qdel(query_jobban_check_ban)
|
||||
return reason ? reason : TRUE //we don't want to return "" if there is no ban reason, as that would evaluate to false
|
||||
qdel(query_jobban_check_ban)
|
||||
return FALSE
|
||||
|
||||
if(!M.client.jobbancache)
|
||||
jobban_buildcache(M.client)
|
||||
|
||||
if(rank in M.client.jobbancache)
|
||||
var/reason = M.client.jobbancache[rank]
|
||||
return (reason) ? reason : TRUE //see above for why we need to do this
|
||||
return FALSE
|
||||
|
||||
/proc/jobban_buildcache(client/C)
|
||||
if(!SSdbcore.Connect())
|
||||
return
|
||||
if(C && istype(C))
|
||||
C.jobbancache = list()
|
||||
var/datum/DBQuery/query_jobban_build_cache = SSdbcore.NewQuery("SELECT job, reason FROM [format_table_name("ban")] WHERE ckey = '[sanitizeSQL(C.ckey)]' AND (bantype = 'JOB_PERMABAN' OR (bantype = 'JOB_TEMPBAN' AND expiration_time > Now())) AND isnull(unbanned)")
|
||||
if(!query_jobban_build_cache.warn_execute())
|
||||
qdel(query_jobban_build_cache)
|
||||
return
|
||||
while(query_jobban_build_cache.NextRow())
|
||||
C.jobbancache[query_jobban_build_cache.item[1]] = query_jobban_build_cache.item[2]
|
||||
qdel(query_jobban_build_cache)
|
||||
|
||||
/proc/ban_unban_log_save(var/formatted_log)
|
||||
text2file(formatted_log,"data/ban_unban_log.txt")
|
||||
//returns a reason if M is banned from rank, returns FALSE otherwise
|
||||
/proc/jobban_isbanned(mob/M, rank)
|
||||
if(!M || !istype(M) || !M.ckey)
|
||||
return FALSE
|
||||
|
||||
if(!M.client) //no cache. fallback to a datum/DBQuery
|
||||
var/datum/DBQuery/query_jobban_check_ban = SSdbcore.NewQuery("SELECT reason FROM [format_table_name("ban")] WHERE ckey = '[sanitizeSQL(M.ckey)]' AND (bantype = 'JOB_PERMABAN' OR (bantype = 'JOB_TEMPBAN' AND expiration_time > Now())) AND isnull(unbanned) AND job = '[sanitizeSQL(rank)]'")
|
||||
if(!query_jobban_check_ban.warn_execute())
|
||||
qdel(query_jobban_check_ban)
|
||||
return
|
||||
if(query_jobban_check_ban.NextRow())
|
||||
var/reason = query_jobban_check_ban.item[1]
|
||||
qdel(query_jobban_check_ban)
|
||||
return reason ? reason : TRUE //we don't want to return "" if there is no ban reason, as that would evaluate to false
|
||||
qdel(query_jobban_check_ban)
|
||||
return FALSE
|
||||
|
||||
if(!M.client.jobbancache)
|
||||
jobban_buildcache(M.client)
|
||||
|
||||
if(rank in M.client.jobbancache)
|
||||
var/reason = M.client.jobbancache[rank]
|
||||
return (reason) ? reason : TRUE //see above for why we need to do this
|
||||
return FALSE
|
||||
|
||||
/proc/jobban_buildcache(client/C)
|
||||
if(!SSdbcore.Connect())
|
||||
return
|
||||
if(C && istype(C))
|
||||
C.jobbancache = list()
|
||||
var/datum/DBQuery/query_jobban_build_cache = SSdbcore.NewQuery("SELECT job, reason FROM [format_table_name("ban")] WHERE ckey = '[sanitizeSQL(C.ckey)]' AND (bantype = 'JOB_PERMABAN' OR (bantype = 'JOB_TEMPBAN' AND expiration_time > Now())) AND isnull(unbanned)")
|
||||
if(!query_jobban_build_cache.warn_execute())
|
||||
qdel(query_jobban_build_cache)
|
||||
return
|
||||
while(query_jobban_build_cache.NextRow())
|
||||
C.jobbancache[query_jobban_build_cache.item[1]] = query_jobban_build_cache.item[2]
|
||||
qdel(query_jobban_build_cache)
|
||||
|
||||
/proc/ban_unban_log_save(var/formatted_log)
|
||||
text2file(formatted_log,"data/ban_unban_log.txt")
|
||||
|
||||
+143
-143
@@ -1,143 +1,143 @@
|
||||
#define IRC_STATUS_THROTTLE 5
|
||||
|
||||
/datum/tgs_chat_command/ircstatus
|
||||
name = "status"
|
||||
help_text = "Gets the admincount, playercount, gamemode, and true game mode of the server"
|
||||
admin_only = TRUE
|
||||
var/last_irc_status = 0
|
||||
|
||||
/datum/tgs_chat_command/ircstatus/Run(datum/tgs_chat_user/sender, params)
|
||||
var/rtod = REALTIMEOFDAY
|
||||
if(rtod - last_irc_status < IRC_STATUS_THROTTLE)
|
||||
return
|
||||
last_irc_status = rtod
|
||||
var/list/adm = get_admin_counts()
|
||||
var/list/allmins = adm["total"]
|
||||
var/status = "Admins: [allmins.len] (Active: [english_list(adm["present"])] AFK: [english_list(adm["afk"])] Stealth: [english_list(adm["stealth"])] Skipped: [english_list(adm["noflags"])]). "
|
||||
status += "Players: [GLOB.clients.len] (Active: [get_active_player_count(0,1,0)]). Mode: [SSticker.mode ? SSticker.mode.name : "Not started"]."
|
||||
return status
|
||||
|
||||
/datum/tgs_chat_command/irccheck
|
||||
name = "check"
|
||||
help_text = "Gets the playercount, gamemode, and address of the server"
|
||||
var/last_irc_check = 0
|
||||
|
||||
/datum/tgs_chat_command/irccheck/Run(datum/tgs_chat_user/sender, params)
|
||||
var/rtod = REALTIMEOFDAY
|
||||
if(rtod - last_irc_check < IRC_STATUS_THROTTLE)
|
||||
return
|
||||
last_irc_check = rtod
|
||||
var/server = CONFIG_GET(string/server)
|
||||
return "[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [server ? server : "[world.internet_address]:[world.port]"]"
|
||||
//CIT CHANGE obfuscates the gamemode for TGS bot commands on discord by removing Mode:[GLOB.master_mode]
|
||||
/datum/tgs_chat_command/ahelp
|
||||
name = "ahelp"
|
||||
help_text = "<ckey|ticket #> <message|ticket <close|resolve|icissue|reject|reopen <ticket #>|list>>"
|
||||
admin_only = TRUE
|
||||
|
||||
/datum/tgs_chat_command/ahelp/Run(datum/tgs_chat_user/sender, params)
|
||||
var/list/all_params = splittext(params, " ")
|
||||
if(all_params.len < 2)
|
||||
return "Insufficient parameters"
|
||||
var/target = all_params[1]
|
||||
all_params.Cut(1, 2)
|
||||
var/id = text2num(target)
|
||||
if(id != null)
|
||||
var/datum/admin_help/AH = GLOB.ahelp_tickets.TicketByID(id)
|
||||
if(AH)
|
||||
target = AH.initiator_ckey
|
||||
else
|
||||
return "Ticket #[id] not found!"
|
||||
var/res = IrcPm(target, all_params.Join(" "), sender.friendly_name)
|
||||
if(res != "Message Successful")
|
||||
return res
|
||||
|
||||
/datum/tgs_chat_command/namecheck
|
||||
name = "namecheck"
|
||||
help_text = "Returns info on the specified target"
|
||||
admin_only = TRUE
|
||||
|
||||
/datum/tgs_chat_command/namecheck/Run(datum/tgs_chat_user/sender, params)
|
||||
params = trim(params)
|
||||
if(!params)
|
||||
return "Insufficient parameters"
|
||||
log_admin("Chat Name Check: [sender.friendly_name] on [params]")
|
||||
message_admins("Name checking [params] from [sender.friendly_name]")
|
||||
return keywords_lookup(params, 1)
|
||||
|
||||
/datum/tgs_chat_command/adminwho
|
||||
name = "adminwho"
|
||||
help_text = "Lists administrators currently on the server"
|
||||
admin_only = TRUE
|
||||
|
||||
/datum/tgs_chat_command/adminwho/Run(datum/tgs_chat_user/sender, params)
|
||||
return ircadminwho()
|
||||
|
||||
GLOBAL_LIST(round_end_notifiees)
|
||||
|
||||
/datum/tgs_chat_command/notify
|
||||
name = "notify"
|
||||
help_text = "Pings the invoker when the round ends"
|
||||
admin_only = FALSE
|
||||
|
||||
/datum/tgs_chat_command/notify/Run(datum/tgs_chat_user/sender, params)
|
||||
if(!SSticker.IsRoundInProgress() && SSticker.HasRoundStarted())
|
||||
return "[sender.mention], the round has already ended!"
|
||||
LAZYINITLIST(GLOB.round_end_notifiees)
|
||||
GLOB.round_end_notifiees["<@[sender.mention]>"] = TRUE
|
||||
return "I will notify [sender.mention] when the round ends."
|
||||
|
||||
/datum/tgs_chat_command/sdql
|
||||
name = "sdql"
|
||||
help_text = "Runs an SDQL query"
|
||||
admin_only = TRUE
|
||||
|
||||
/datum/tgs_chat_command/sdql/Run(datum/tgs_chat_user/sender, params)
|
||||
if(GLOB.AdminProcCaller)
|
||||
return "Unable to run query, another admin proc call is in progress. Try again later."
|
||||
GLOB.AdminProcCaller = "CHAT_[sender.friendly_name]" //_ won't show up in ckeys so it'll never match with a real admin
|
||||
var/list/results = world.SDQL2_query(params, GLOB.AdminProcCaller, GLOB.AdminProcCaller)
|
||||
GLOB.AdminProcCaller = null
|
||||
if(!results)
|
||||
return "Query produced no output"
|
||||
var/list/text_res = results.Copy(1, 3)
|
||||
var/list/refs = results.len > 3 ? results.Copy(4) : null
|
||||
if(refs)
|
||||
var/list/L = list()
|
||||
for(var/ref in refs)
|
||||
var/atom/A = locate(ref)
|
||||
if(A)
|
||||
L += "[A]"
|
||||
else
|
||||
L += "[ref]"
|
||||
refs = L
|
||||
. = "[text_res.Join("\n")][refs ? "\nRefs: [refs.Join(" ")]" : ""]"
|
||||
|
||||
/datum/tgs_chat_command/reload_admins
|
||||
name = "reload_admins"
|
||||
help_text = "Forces the server to reload admins."
|
||||
admin_only = TRUE
|
||||
|
||||
/datum/tgs_chat_command/reload_admins/Run(datum/tgs_chat_user/sender, params)
|
||||
ReloadAsync()
|
||||
log_admin("[sender.friendly_name] reloaded admins via chat command.")
|
||||
return "Admins reloaded."
|
||||
|
||||
/datum/tgs_chat_command/reload_admins/proc/ReloadAsync()
|
||||
set waitfor = FALSE
|
||||
load_admins()
|
||||
|
||||
/datum/tgs_chat_command/addbunkerbypass
|
||||
name = "whitelist"
|
||||
help_text = "whitelist <ckey>"
|
||||
admin_only = TRUE
|
||||
|
||||
/datum/tgs_chat_command/addbunkerbypass/Run(datum/tgs_chat_user/sender, params)
|
||||
if(!CONFIG_GET(flag/sql_enabled))
|
||||
return "The Database is not enabled!"
|
||||
|
||||
GLOB.bunker_passthrough |= ckey(params)
|
||||
|
||||
log_admin("[sender.friendly_name] has added [params] to the current round's bunker bypass list.")
|
||||
message_admins("[sender.friendly_name] has added [params] to the current round's bunker bypass list.")
|
||||
return "[params] has been added to the current round's bunker bypass list."
|
||||
#define IRC_STATUS_THROTTLE 5
|
||||
|
||||
/datum/tgs_chat_command/ircstatus
|
||||
name = "status"
|
||||
help_text = "Gets the admincount, playercount, gamemode, and true game mode of the server"
|
||||
admin_only = TRUE
|
||||
var/last_irc_status = 0
|
||||
|
||||
/datum/tgs_chat_command/ircstatus/Run(datum/tgs_chat_user/sender, params)
|
||||
var/rtod = REALTIMEOFDAY
|
||||
if(rtod - last_irc_status < IRC_STATUS_THROTTLE)
|
||||
return
|
||||
last_irc_status = rtod
|
||||
var/list/adm = get_admin_counts()
|
||||
var/list/allmins = adm["total"]
|
||||
var/status = "Admins: [allmins.len] (Active: [english_list(adm["present"])] AFK: [english_list(adm["afk"])] Stealth: [english_list(adm["stealth"])] Skipped: [english_list(adm["noflags"])]). "
|
||||
status += "Players: [GLOB.clients.len] (Active: [get_active_player_count(0,1,0)]). Mode: [SSticker.mode ? SSticker.mode.name : "Not started"]."
|
||||
return status
|
||||
|
||||
/datum/tgs_chat_command/irccheck
|
||||
name = "check"
|
||||
help_text = "Gets the playercount, gamemode, and address of the server"
|
||||
var/last_irc_check = 0
|
||||
|
||||
/datum/tgs_chat_command/irccheck/Run(datum/tgs_chat_user/sender, params)
|
||||
var/rtod = REALTIMEOFDAY
|
||||
if(rtod - last_irc_check < IRC_STATUS_THROTTLE)
|
||||
return
|
||||
last_irc_check = rtod
|
||||
var/server = CONFIG_GET(string/server)
|
||||
return "[GLOB.round_id ? "Round #[GLOB.round_id]: " : ""][GLOB.clients.len] players on [SSmapping.config.map_name]; Round [SSticker.HasRoundStarted() ? (SSticker.IsRoundInProgress() ? "Active" : "Finishing") : "Starting"] -- [server ? server : "[world.internet_address]:[world.port]"]"
|
||||
//CIT CHANGE obfuscates the gamemode for TGS bot commands on discord by removing Mode:[GLOB.master_mode]
|
||||
/datum/tgs_chat_command/ahelp
|
||||
name = "ahelp"
|
||||
help_text = "<ckey|ticket #> <message|ticket <close|resolve|icissue|reject|reopen <ticket #>|list>>"
|
||||
admin_only = TRUE
|
||||
|
||||
/datum/tgs_chat_command/ahelp/Run(datum/tgs_chat_user/sender, params)
|
||||
var/list/all_params = splittext(params, " ")
|
||||
if(all_params.len < 2)
|
||||
return "Insufficient parameters"
|
||||
var/target = all_params[1]
|
||||
all_params.Cut(1, 2)
|
||||
var/id = text2num(target)
|
||||
if(id != null)
|
||||
var/datum/admin_help/AH = GLOB.ahelp_tickets.TicketByID(id)
|
||||
if(AH)
|
||||
target = AH.initiator_ckey
|
||||
else
|
||||
return "Ticket #[id] not found!"
|
||||
var/res = IrcPm(target, all_params.Join(" "), sender.friendly_name)
|
||||
if(res != "Message Successful")
|
||||
return res
|
||||
|
||||
/datum/tgs_chat_command/namecheck
|
||||
name = "namecheck"
|
||||
help_text = "Returns info on the specified target"
|
||||
admin_only = TRUE
|
||||
|
||||
/datum/tgs_chat_command/namecheck/Run(datum/tgs_chat_user/sender, params)
|
||||
params = trim(params)
|
||||
if(!params)
|
||||
return "Insufficient parameters"
|
||||
log_admin("Chat Name Check: [sender.friendly_name] on [params]")
|
||||
message_admins("Name checking [params] from [sender.friendly_name]")
|
||||
return keywords_lookup(params, 1)
|
||||
|
||||
/datum/tgs_chat_command/adminwho
|
||||
name = "adminwho"
|
||||
help_text = "Lists administrators currently on the server"
|
||||
admin_only = TRUE
|
||||
|
||||
/datum/tgs_chat_command/adminwho/Run(datum/tgs_chat_user/sender, params)
|
||||
return ircadminwho()
|
||||
|
||||
GLOBAL_LIST(round_end_notifiees)
|
||||
|
||||
/datum/tgs_chat_command/notify
|
||||
name = "notify"
|
||||
help_text = "Pings the invoker when the round ends"
|
||||
admin_only = FALSE
|
||||
|
||||
/datum/tgs_chat_command/notify/Run(datum/tgs_chat_user/sender, params)
|
||||
if(!SSticker.IsRoundInProgress() && SSticker.HasRoundStarted())
|
||||
return "[sender.mention], the round has already ended!"
|
||||
LAZYINITLIST(GLOB.round_end_notifiees)
|
||||
GLOB.round_end_notifiees["<@[sender.mention]>"] = TRUE
|
||||
return "I will notify [sender.mention] when the round ends."
|
||||
|
||||
/datum/tgs_chat_command/sdql
|
||||
name = "sdql"
|
||||
help_text = "Runs an SDQL query"
|
||||
admin_only = TRUE
|
||||
|
||||
/datum/tgs_chat_command/sdql/Run(datum/tgs_chat_user/sender, params)
|
||||
if(GLOB.AdminProcCaller)
|
||||
return "Unable to run query, another admin proc call is in progress. Try again later."
|
||||
GLOB.AdminProcCaller = "CHAT_[sender.friendly_name]" //_ won't show up in ckeys so it'll never match with a real admin
|
||||
var/list/results = world.SDQL2_query(params, GLOB.AdminProcCaller, GLOB.AdminProcCaller)
|
||||
GLOB.AdminProcCaller = null
|
||||
if(!results)
|
||||
return "Query produced no output"
|
||||
var/list/text_res = results.Copy(1, 3)
|
||||
var/list/refs = results.len > 3 ? results.Copy(4) : null
|
||||
if(refs)
|
||||
var/list/L = list()
|
||||
for(var/ref in refs)
|
||||
var/atom/A = locate(ref)
|
||||
if(A)
|
||||
L += "[A]"
|
||||
else
|
||||
L += "[ref]"
|
||||
refs = L
|
||||
. = "[text_res.Join("\n")][refs ? "\nRefs: [refs.Join(" ")]" : ""]"
|
||||
|
||||
/datum/tgs_chat_command/reload_admins
|
||||
name = "reload_admins"
|
||||
help_text = "Forces the server to reload admins."
|
||||
admin_only = TRUE
|
||||
|
||||
/datum/tgs_chat_command/reload_admins/Run(datum/tgs_chat_user/sender, params)
|
||||
ReloadAsync()
|
||||
log_admin("[sender.friendly_name] reloaded admins via chat command.")
|
||||
return "Admins reloaded."
|
||||
|
||||
/datum/tgs_chat_command/reload_admins/proc/ReloadAsync()
|
||||
set waitfor = FALSE
|
||||
load_admins()
|
||||
|
||||
/datum/tgs_chat_command/addbunkerbypass
|
||||
name = "whitelist"
|
||||
help_text = "whitelist <ckey>"
|
||||
admin_only = TRUE
|
||||
|
||||
/datum/tgs_chat_command/addbunkerbypass/Run(datum/tgs_chat_user/sender, params)
|
||||
if(!CONFIG_GET(flag/sql_enabled))
|
||||
return "The Database is not enabled!"
|
||||
|
||||
GLOB.bunker_passthrough |= ckey(params)
|
||||
|
||||
log_admin("[sender.friendly_name] has added [params] to the current round's bunker bypass list.")
|
||||
message_admins("[sender.friendly_name] has added [params] to the current round's bunker bypass list.")
|
||||
return "[params] has been added to the current round's bunker bypass list."
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
|
||||
/datum/admins/proc/create_mob(mob/user)
|
||||
var/static/create_mob_html
|
||||
if (!create_mob_html)
|
||||
var/mobjs = null
|
||||
mobjs = jointext(typesof(/mob), ";")
|
||||
create_mob_html = file2text('html/create_object.html')
|
||||
create_mob_html = replacetext(create_mob_html, "Create Object", "Create Mob")
|
||||
create_mob_html = replacetext(create_mob_html, "null /* object types */", "\"[mobjs]\"")
|
||||
|
||||
user << browse(create_panel_helper(create_mob_html), "window=create_mob;size=425x475")
|
||||
|
||||
/proc/randomize_human(mob/living/carbon/human/H)
|
||||
H.gender = pick(MALE, FEMALE)
|
||||
H.real_name = random_unique_name(H.gender)
|
||||
H.name = H.real_name
|
||||
H.underwear = random_underwear(H.gender)
|
||||
H.undie_color = random_short_color()
|
||||
H.undershirt = random_undershirt(H.gender)
|
||||
H.shirt_color = random_short_color()
|
||||
H.skin_tone = random_skin_tone()
|
||||
H.hair_style = random_hair_style(H.gender)
|
||||
H.facial_hair_style = random_facial_hair_style(H.gender)
|
||||
H.hair_color = random_short_color()
|
||||
H.facial_hair_color = H.hair_color
|
||||
H.eye_color = random_eye_color()
|
||||
H.dna.blood_type = random_blood_type()
|
||||
H.saved_underwear = H.underwear
|
||||
H.saved_undershirt = H.undershirt
|
||||
H.saved_socks = H.socks
|
||||
|
||||
// Mutant randomizing, doesn't affect the mob appearance unless it's the specific mutant.
|
||||
H.dna.features["mcolor"] = random_short_color()
|
||||
H.dna.features["tail_lizard"] = pick(GLOB.tails_list_lizard)
|
||||
H.dna.features["snout"] = pick(GLOB.snouts_list)
|
||||
H.dna.features["horns"] = pick(GLOB.horns_list)
|
||||
H.dna.features["frills"] = pick(GLOB.frills_list)
|
||||
H.dna.features["spines"] = pick(GLOB.spines_list)
|
||||
H.dna.features["body_markings"] = pick(GLOB.body_markings_list)
|
||||
H.dna.features["insect_wings"] = pick(GLOB.insect_wings_list)
|
||||
H.dna.features["deco_wings"] = pick(GLOB.deco_wings_list)
|
||||
H.dna.features["insect_fluff"] = pick(GLOB.insect_fluffs_list)
|
||||
|
||||
H.update_body()
|
||||
H.update_hair()
|
||||
H.update_body_parts()
|
||||
|
||||
/datum/admins/proc/create_mob(mob/user)
|
||||
var/static/create_mob_html
|
||||
if (!create_mob_html)
|
||||
var/mobjs = null
|
||||
mobjs = jointext(typesof(/mob), ";")
|
||||
create_mob_html = file2text('html/create_object.html')
|
||||
create_mob_html = replacetext(create_mob_html, "Create Object", "Create Mob")
|
||||
create_mob_html = replacetext(create_mob_html, "null /* object types */", "\"[mobjs]\"")
|
||||
|
||||
user << browse(create_panel_helper(create_mob_html), "window=create_mob;size=425x475")
|
||||
|
||||
/proc/randomize_human(mob/living/carbon/human/H)
|
||||
H.gender = pick(MALE, FEMALE)
|
||||
H.real_name = random_unique_name(H.gender)
|
||||
H.name = H.real_name
|
||||
H.underwear = random_underwear(H.gender)
|
||||
H.undie_color = random_short_color()
|
||||
H.undershirt = random_undershirt(H.gender)
|
||||
H.shirt_color = random_short_color()
|
||||
H.skin_tone = random_skin_tone()
|
||||
H.hair_style = random_hair_style(H.gender)
|
||||
H.facial_hair_style = random_facial_hair_style(H.gender)
|
||||
H.hair_color = random_short_color()
|
||||
H.facial_hair_color = H.hair_color
|
||||
H.eye_color = random_eye_color()
|
||||
H.dna.blood_type = random_blood_type()
|
||||
H.saved_underwear = H.underwear
|
||||
H.saved_undershirt = H.undershirt
|
||||
H.saved_socks = H.socks
|
||||
|
||||
// Mutant randomizing, doesn't affect the mob appearance unless it's the specific mutant.
|
||||
H.dna.features["mcolor"] = random_short_color()
|
||||
H.dna.features["tail_lizard"] = pick(GLOB.tails_list_lizard)
|
||||
H.dna.features["snout"] = pick(GLOB.snouts_list)
|
||||
H.dna.features["horns"] = pick(GLOB.horns_list)
|
||||
H.dna.features["frills"] = pick(GLOB.frills_list)
|
||||
H.dna.features["spines"] = pick(GLOB.spines_list)
|
||||
H.dna.features["body_markings"] = pick(GLOB.body_markings_list)
|
||||
H.dna.features["insect_wings"] = pick(GLOB.insect_wings_list)
|
||||
H.dna.features["deco_wings"] = pick(GLOB.deco_wings_list)
|
||||
H.dna.features["insect_fluff"] = pick(GLOB.insect_fluffs_list)
|
||||
|
||||
H.update_body()
|
||||
H.update_hair()
|
||||
H.update_body_parts()
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
/datum/admins/proc/create_panel_helper(template)
|
||||
var/final_html = replacetext(template, "/* ref src */", "[REF(src)];[HrefToken()]")
|
||||
final_html = replacetext(final_html,"/* hreftokenfield */","[HrefTokenFormField()]")
|
||||
return final_html
|
||||
|
||||
/datum/admins/proc/create_object(mob/user)
|
||||
var/static/create_object_html = null
|
||||
if (!create_object_html)
|
||||
var/objectjs = null
|
||||
objectjs = jointext(typesof(/obj), ";")
|
||||
create_object_html = file2text('html/create_object.html')
|
||||
create_object_html = replacetext(create_object_html, "null /* object types */", "\"[objectjs]\"")
|
||||
|
||||
user << browse(create_panel_helper(create_object_html), "window=create_object;size=425x475")
|
||||
|
||||
/datum/admins/proc/quick_create_object(mob/user)
|
||||
var/static/list/create_object_forms = list(
|
||||
/obj, /obj/structure, /obj/machinery, /obj/effect,
|
||||
/obj/item, /obj/item/clothing, /obj/item/stack, /obj/item,
|
||||
/obj/item/reagent_containers, /obj/item/gun)
|
||||
|
||||
var/path = input("Select the path of the object you wish to create.", "Path", /obj) in create_object_forms
|
||||
var/html_form = create_object_forms[path]
|
||||
|
||||
if (!html_form)
|
||||
var/objectjs = jointext(typesof(path), ";")
|
||||
html_form = file2text('html/create_object.html')
|
||||
html_form = replacetext(html_form, "Create Object", "Create [path]")
|
||||
html_form = replacetext(html_form, "null /* object types */", "\"[objectjs]\"")
|
||||
create_object_forms[path] = html_form
|
||||
|
||||
user << browse(create_panel_helper(html_form), "window=qco[path];size=425x475")
|
||||
/datum/admins/proc/create_panel_helper(template)
|
||||
var/final_html = replacetext(template, "/* ref src */", "[REF(src)];[HrefToken()]")
|
||||
final_html = replacetext(final_html,"/* hreftokenfield */","[HrefTokenFormField()]")
|
||||
return final_html
|
||||
|
||||
/datum/admins/proc/create_object(mob/user)
|
||||
var/static/create_object_html = null
|
||||
if (!create_object_html)
|
||||
var/objectjs = null
|
||||
objectjs = jointext(typesof(/obj), ";")
|
||||
create_object_html = file2text('html/create_object.html')
|
||||
create_object_html = replacetext(create_object_html, "null /* object types */", "\"[objectjs]\"")
|
||||
|
||||
user << browse(create_panel_helper(create_object_html), "window=create_object;size=425x475")
|
||||
|
||||
/datum/admins/proc/quick_create_object(mob/user)
|
||||
var/static/list/create_object_forms = list(
|
||||
/obj, /obj/structure, /obj/machinery, /obj/effect,
|
||||
/obj/item, /obj/item/clothing, /obj/item/stack, /obj/item,
|
||||
/obj/item/reagent_containers, /obj/item/gun)
|
||||
|
||||
var/path = input("Select the path of the object you wish to create.", "Path", /obj) in create_object_forms
|
||||
var/html_form = create_object_forms[path]
|
||||
|
||||
if (!html_form)
|
||||
var/objectjs = jointext(typesof(path), ";")
|
||||
html_form = file2text('html/create_object.html')
|
||||
html_form = replacetext(html_form, "Create Object", "Create [path]")
|
||||
html_form = replacetext(html_form, "null /* object types */", "\"[objectjs]\"")
|
||||
create_object_forms[path] = html_form
|
||||
|
||||
user << browse(create_panel_helper(html_form), "window=qco[path];size=425x475")
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/datum/admins/proc/create_turf(mob/user)
|
||||
var/static/create_turf_html
|
||||
if (!create_turf_html)
|
||||
var/turfjs = null
|
||||
turfjs = jointext(typesof(/turf), ";")
|
||||
create_turf_html = file2text('html/create_object.html')
|
||||
create_turf_html = replacetext(create_turf_html, "Create Object", "Create Turf")
|
||||
create_turf_html = replacetext(create_turf_html, "null /* object types */", "\"[turfjs]\"")
|
||||
|
||||
user << browse(create_panel_helper(create_turf_html), "window=create_turf;size=425x475")
|
||||
/datum/admins/proc/create_turf(mob/user)
|
||||
var/static/create_turf_html
|
||||
if (!create_turf_html)
|
||||
var/turfjs = null
|
||||
turfjs = jointext(typesof(/turf), ";")
|
||||
create_turf_html = file2text('html/create_object.html')
|
||||
create_turf_html = replacetext(create_turf_html, "Create Object", "Create Turf")
|
||||
create_turf_html = replacetext(create_turf_html, "null /* object types */", "\"[turfjs]\"")
|
||||
|
||||
user << browse(create_panel_helper(create_turf_html), "window=create_turf;size=425x475")
|
||||
|
||||
+210
-210
@@ -1,210 +1,210 @@
|
||||
GLOBAL_LIST_EMPTY(admin_datums)
|
||||
GLOBAL_PROTECT(admin_datums)
|
||||
GLOBAL_LIST_EMPTY(protected_admins)
|
||||
GLOBAL_PROTECT(protected_admins)
|
||||
|
||||
GLOBAL_VAR_INIT(href_token, GenerateToken())
|
||||
GLOBAL_PROTECT(href_token)
|
||||
|
||||
/datum/admins
|
||||
var/datum/admin_rank/rank
|
||||
|
||||
var/target
|
||||
var/name = "nobody's admin datum (no rank)" //Makes for better runtimes
|
||||
var/client/owner = null
|
||||
var/fakekey = null
|
||||
|
||||
var/datum/marked_datum
|
||||
|
||||
var/spamcooldown = 0
|
||||
|
||||
var/admincaster_screen = 0 //TODO: remove all these 5 variables, they are completly unacceptable
|
||||
var/datum/newscaster/feed_message/admincaster_feed_message = new /datum/newscaster/feed_message
|
||||
var/datum/newscaster/wanted_message/admincaster_wanted_message = new /datum/newscaster/wanted_message
|
||||
var/datum/newscaster/feed_channel/admincaster_feed_channel = new /datum/newscaster/feed_channel
|
||||
var/admin_signature
|
||||
|
||||
var/href_token
|
||||
|
||||
var/deadmined
|
||||
|
||||
/datum/admins/New(datum/admin_rank/R, ckey, force_active = FALSE, protected)
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
if (!target) //only del if this is a true creation (and not just a New() proc call), other wise trialmins/coders could abuse this to deadmin other admins
|
||||
QDEL_IN(src, 0)
|
||||
CRASH("Admin proc call creation of admin datum")
|
||||
return
|
||||
if(!ckey)
|
||||
QDEL_IN(src, 0)
|
||||
throw EXCEPTION("Admin datum created without a ckey")
|
||||
return
|
||||
if(!istype(R))
|
||||
QDEL_IN(src, 0)
|
||||
throw EXCEPTION("Admin datum created without a rank")
|
||||
return
|
||||
target = ckey
|
||||
name = "[ckey]'s admin datum ([R])"
|
||||
rank = R
|
||||
admin_signature = "Nanotrasen Officer #[rand(0,9)][rand(0,9)][rand(0,9)]"
|
||||
href_token = GenerateToken()
|
||||
if(R.rights & R_DEBUG) //grant profile access
|
||||
world.SetConfig("APP/admin", ckey, "role=admin")
|
||||
//only admins with +ADMIN start admined
|
||||
if(protected)
|
||||
GLOB.protected_admins[target] = src
|
||||
if (force_active || (R.rights & R_AUTOLOGIN))
|
||||
activate()
|
||||
else
|
||||
deactivate()
|
||||
|
||||
/datum/admins/Destroy()
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return QDEL_HINT_LETMELIVE
|
||||
. = ..()
|
||||
|
||||
/datum/admins/proc/activate()
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return
|
||||
GLOB.deadmins -= target
|
||||
GLOB.admin_datums[target] = src
|
||||
deadmined = FALSE
|
||||
if (GLOB.directory[target])
|
||||
associate(GLOB.directory[target]) //find the client for a ckey if they are connected and associate them with us
|
||||
|
||||
|
||||
/datum/admins/proc/deactivate()
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return
|
||||
GLOB.deadmins[target] = src
|
||||
GLOB.admin_datums -= target
|
||||
deadmined = TRUE
|
||||
var/client/C
|
||||
if ((C = owner) || (C = GLOB.directory[target]))
|
||||
disassociate()
|
||||
C.verbs += /client/proc/readmin
|
||||
|
||||
/datum/admins/proc/associate(client/C)
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return
|
||||
|
||||
if(istype(C))
|
||||
if(C.ckey != target)
|
||||
var/msg = " has attempted to associate with [target]'s admin datum"
|
||||
message_admins("[key_name_admin(C)][msg]")
|
||||
log_admin("[key_name(C)][msg]")
|
||||
return
|
||||
if (deadmined)
|
||||
activate()
|
||||
owner = C
|
||||
owner.holder = src
|
||||
owner.add_admin_verbs() //TODO <--- todo what? the proc clearly exists and works since its the backbone to our entire admin system
|
||||
owner.verbs -= /client/proc/readmin
|
||||
GLOB.admins |= C
|
||||
|
||||
/datum/admins/proc/disassociate()
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return
|
||||
if(owner)
|
||||
GLOB.admins -= owner
|
||||
owner.remove_admin_verbs()
|
||||
owner.holder = null
|
||||
owner = null
|
||||
|
||||
/datum/admins/proc/check_for_rights(rights_required)
|
||||
if(rights_required && !(rights_required & rank.rights))
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
/datum/admins/proc/check_if_greater_rights_than_holder(datum/admins/other)
|
||||
if(!other)
|
||||
return 1 //they have no rights
|
||||
if(rank.rights == R_EVERYTHING)
|
||||
return 1 //we have all the rights
|
||||
if(src == other)
|
||||
return 1 //you always have more rights than yourself
|
||||
if(rank.rights != other.rank.rights)
|
||||
if( (rank.rights & other.rank.rights) == other.rank.rights )
|
||||
return 1 //we have all the rights they have and more
|
||||
return 0
|
||||
|
||||
/datum/admins/vv_edit_var(var_name, var_value)
|
||||
return FALSE //nice try trialmin
|
||||
|
||||
/*
|
||||
checks if usr is an admin with at least ONE of the flags in rights_required. (Note, they don't need all the flags)
|
||||
if rights_required == 0, then it simply checks if they are an admin.
|
||||
if it doesn't return 1 and show_msg=1 it will prints a message explaining why the check has failed
|
||||
generally it would be used like so:
|
||||
|
||||
/proc/admin_proc()
|
||||
if(!check_rights(R_ADMIN))
|
||||
return
|
||||
to_chat(world, "you have enough rights!")
|
||||
|
||||
NOTE: it checks usr! not src! So if you're checking somebody's rank in a proc which they did not call
|
||||
you will have to do something like if(client.rights & R_ADMIN) yourself.
|
||||
*/
|
||||
/proc/check_rights(rights_required, show_msg=1)
|
||||
if(usr && usr.client)
|
||||
if (check_rights_for(usr.client, rights_required))
|
||||
return 1
|
||||
else
|
||||
if(show_msg)
|
||||
to_chat(usr, "<font color='red'>Error: You do not have sufficient rights to do that. You require one of the following flags:[rights2text(rights_required," ")].</font>")
|
||||
return 0
|
||||
|
||||
//probably a bit iffy - will hopefully figure out a better solution
|
||||
/proc/check_if_greater_rights_than(client/other)
|
||||
if(usr && usr.client)
|
||||
if(usr.client.holder)
|
||||
if(!other || !other.holder)
|
||||
return 1
|
||||
return usr.client.holder.check_if_greater_rights_than_holder(other.holder)
|
||||
return 0
|
||||
|
||||
//This proc checks whether subject has at least ONE of the rights specified in rights_required.
|
||||
/proc/check_rights_for(client/subject, rights_required)
|
||||
if(subject && subject.holder)
|
||||
return subject.holder.check_for_rights(rights_required)
|
||||
return 0
|
||||
|
||||
/proc/GenerateToken()
|
||||
. = ""
|
||||
for(var/I in 1 to 32)
|
||||
. += "[rand(10)]"
|
||||
|
||||
/proc/RawHrefToken(forceGlobal = FALSE)
|
||||
var/tok = GLOB.href_token
|
||||
if(!forceGlobal && usr)
|
||||
var/client/C = usr.client
|
||||
if(!C)
|
||||
CRASH("No client for HrefToken()!")
|
||||
var/datum/admins/holder = C.holder
|
||||
if(holder)
|
||||
tok = holder.href_token
|
||||
return tok
|
||||
|
||||
/proc/HrefToken(forceGlobal = FALSE)
|
||||
return "admin_token=[RawHrefToken(forceGlobal)]"
|
||||
|
||||
/proc/HrefTokenFormField(forceGlobal = FALSE)
|
||||
return "<input type='hidden' name='admin_token' value='[RawHrefToken(forceGlobal)]'>"
|
||||
GLOBAL_LIST_EMPTY(admin_datums)
|
||||
GLOBAL_PROTECT(admin_datums)
|
||||
GLOBAL_LIST_EMPTY(protected_admins)
|
||||
GLOBAL_PROTECT(protected_admins)
|
||||
|
||||
GLOBAL_VAR_INIT(href_token, GenerateToken())
|
||||
GLOBAL_PROTECT(href_token)
|
||||
|
||||
/datum/admins
|
||||
var/datum/admin_rank/rank
|
||||
|
||||
var/target
|
||||
var/name = "nobody's admin datum (no rank)" //Makes for better runtimes
|
||||
var/client/owner = null
|
||||
var/fakekey = null
|
||||
|
||||
var/datum/marked_datum
|
||||
|
||||
var/spamcooldown = 0
|
||||
|
||||
var/admincaster_screen = 0 //TODO: remove all these 5 variables, they are completly unacceptable
|
||||
var/datum/newscaster/feed_message/admincaster_feed_message = new /datum/newscaster/feed_message
|
||||
var/datum/newscaster/wanted_message/admincaster_wanted_message = new /datum/newscaster/wanted_message
|
||||
var/datum/newscaster/feed_channel/admincaster_feed_channel = new /datum/newscaster/feed_channel
|
||||
var/admin_signature
|
||||
|
||||
var/href_token
|
||||
|
||||
var/deadmined
|
||||
|
||||
/datum/admins/New(datum/admin_rank/R, ckey, force_active = FALSE, protected)
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
if (!target) //only del if this is a true creation (and not just a New() proc call), other wise trialmins/coders could abuse this to deadmin other admins
|
||||
QDEL_IN(src, 0)
|
||||
CRASH("Admin proc call creation of admin datum")
|
||||
return
|
||||
if(!ckey)
|
||||
QDEL_IN(src, 0)
|
||||
throw EXCEPTION("Admin datum created without a ckey")
|
||||
return
|
||||
if(!istype(R))
|
||||
QDEL_IN(src, 0)
|
||||
throw EXCEPTION("Admin datum created without a rank")
|
||||
return
|
||||
target = ckey
|
||||
name = "[ckey]'s admin datum ([R])"
|
||||
rank = R
|
||||
admin_signature = "Nanotrasen Officer #[rand(0,9)][rand(0,9)][rand(0,9)]"
|
||||
href_token = GenerateToken()
|
||||
if(R.rights & R_DEBUG) //grant profile access
|
||||
world.SetConfig("APP/admin", ckey, "role=admin")
|
||||
//only admins with +ADMIN start admined
|
||||
if(protected)
|
||||
GLOB.protected_admins[target] = src
|
||||
if (force_active || (R.rights & R_AUTOLOGIN))
|
||||
activate()
|
||||
else
|
||||
deactivate()
|
||||
|
||||
/datum/admins/Destroy()
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return QDEL_HINT_LETMELIVE
|
||||
. = ..()
|
||||
|
||||
/datum/admins/proc/activate()
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return
|
||||
GLOB.deadmins -= target
|
||||
GLOB.admin_datums[target] = src
|
||||
deadmined = FALSE
|
||||
if (GLOB.directory[target])
|
||||
associate(GLOB.directory[target]) //find the client for a ckey if they are connected and associate them with us
|
||||
|
||||
|
||||
/datum/admins/proc/deactivate()
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return
|
||||
GLOB.deadmins[target] = src
|
||||
GLOB.admin_datums -= target
|
||||
deadmined = TRUE
|
||||
var/client/C
|
||||
if ((C = owner) || (C = GLOB.directory[target]))
|
||||
disassociate()
|
||||
C.verbs += /client/proc/readmin
|
||||
|
||||
/datum/admins/proc/associate(client/C)
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return
|
||||
|
||||
if(istype(C))
|
||||
if(C.ckey != target)
|
||||
var/msg = " has attempted to associate with [target]'s admin datum"
|
||||
message_admins("[key_name_admin(C)][msg]")
|
||||
log_admin("[key_name(C)][msg]")
|
||||
return
|
||||
if (deadmined)
|
||||
activate()
|
||||
owner = C
|
||||
owner.holder = src
|
||||
owner.add_admin_verbs() //TODO <--- todo what? the proc clearly exists and works since its the backbone to our entire admin system
|
||||
owner.verbs -= /client/proc/readmin
|
||||
GLOB.admins |= C
|
||||
|
||||
/datum/admins/proc/disassociate()
|
||||
if(IsAdminAdvancedProcCall())
|
||||
var/msg = " has tried to elevate permissions!"
|
||||
message_admins("[key_name_admin(usr)][msg]")
|
||||
log_admin("[key_name(usr)][msg]")
|
||||
return
|
||||
if(owner)
|
||||
GLOB.admins -= owner
|
||||
owner.remove_admin_verbs()
|
||||
owner.holder = null
|
||||
owner = null
|
||||
|
||||
/datum/admins/proc/check_for_rights(rights_required)
|
||||
if(rights_required && !(rights_required & rank.rights))
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
/datum/admins/proc/check_if_greater_rights_than_holder(datum/admins/other)
|
||||
if(!other)
|
||||
return 1 //they have no rights
|
||||
if(rank.rights == R_EVERYTHING)
|
||||
return 1 //we have all the rights
|
||||
if(src == other)
|
||||
return 1 //you always have more rights than yourself
|
||||
if(rank.rights != other.rank.rights)
|
||||
if( (rank.rights & other.rank.rights) == other.rank.rights )
|
||||
return 1 //we have all the rights they have and more
|
||||
return 0
|
||||
|
||||
/datum/admins/vv_edit_var(var_name, var_value)
|
||||
return FALSE //nice try trialmin
|
||||
|
||||
/*
|
||||
checks if usr is an admin with at least ONE of the flags in rights_required. (Note, they don't need all the flags)
|
||||
if rights_required == 0, then it simply checks if they are an admin.
|
||||
if it doesn't return 1 and show_msg=1 it will prints a message explaining why the check has failed
|
||||
generally it would be used like so:
|
||||
|
||||
/proc/admin_proc()
|
||||
if(!check_rights(R_ADMIN))
|
||||
return
|
||||
to_chat(world, "you have enough rights!")
|
||||
|
||||
NOTE: it checks usr! not src! So if you're checking somebody's rank in a proc which they did not call
|
||||
you will have to do something like if(client.rights & R_ADMIN) yourself.
|
||||
*/
|
||||
/proc/check_rights(rights_required, show_msg=1)
|
||||
if(usr && usr.client)
|
||||
if (check_rights_for(usr.client, rights_required))
|
||||
return 1
|
||||
else
|
||||
if(show_msg)
|
||||
to_chat(usr, "<font color='red'>Error: You do not have sufficient rights to do that. You require one of the following flags:[rights2text(rights_required," ")].</font>")
|
||||
return 0
|
||||
|
||||
//probably a bit iffy - will hopefully figure out a better solution
|
||||
/proc/check_if_greater_rights_than(client/other)
|
||||
if(usr && usr.client)
|
||||
if(usr.client.holder)
|
||||
if(!other || !other.holder)
|
||||
return 1
|
||||
return usr.client.holder.check_if_greater_rights_than_holder(other.holder)
|
||||
return 0
|
||||
|
||||
//This proc checks whether subject has at least ONE of the rights specified in rights_required.
|
||||
/proc/check_rights_for(client/subject, rights_required)
|
||||
if(subject && subject.holder)
|
||||
return subject.holder.check_for_rights(rights_required)
|
||||
return 0
|
||||
|
||||
/proc/GenerateToken()
|
||||
. = ""
|
||||
for(var/I in 1 to 32)
|
||||
. += "[rand(10)]"
|
||||
|
||||
/proc/RawHrefToken(forceGlobal = FALSE)
|
||||
var/tok = GLOB.href_token
|
||||
if(!forceGlobal && usr)
|
||||
var/client/C = usr.client
|
||||
if(!C)
|
||||
CRASH("No client for HrefToken()!")
|
||||
var/datum/admins/holder = C.holder
|
||||
if(holder)
|
||||
tok = holder.href_token
|
||||
return tok
|
||||
|
||||
/proc/HrefToken(forceGlobal = FALSE)
|
||||
return "admin_token=[RawHrefToken(forceGlobal)]"
|
||||
|
||||
/proc/HrefTokenFormField(forceGlobal = FALSE)
|
||||
return "<input type='hidden' name='admin_token' value='[RawHrefToken(forceGlobal)]'>"
|
||||
|
||||
+313
-313
@@ -1,313 +1,313 @@
|
||||
/datum/admins/proc/player_panel_new()//The new one
|
||||
if(!check_rights())
|
||||
return
|
||||
log_admin("[key_name(usr)] checked the player panel.")
|
||||
var/dat = "<html><head><title>Player Panel</title></head>"
|
||||
|
||||
//javascript, the part that does most of the work~
|
||||
dat += {"
|
||||
|
||||
<head>
|
||||
<script type='text/javascript'>
|
||||
|
||||
var locked_tabs = new Array();
|
||||
|
||||
function updateSearch(){
|
||||
|
||||
|
||||
var filter_text = document.getElementById('filter');
|
||||
var filter = filter_text.value.toLowerCase();
|
||||
|
||||
if(complete_list != null && complete_list != ""){
|
||||
var mtbl = document.getElementById("maintable_data_archive");
|
||||
mtbl.innerHTML = complete_list;
|
||||
}
|
||||
|
||||
if(filter.value == ""){
|
||||
return;
|
||||
}else{
|
||||
|
||||
var maintable_data = document.getElementById('maintable_data');
|
||||
var ltr = maintable_data.getElementsByTagName("tr");
|
||||
for ( var i = 0; i < ltr.length; ++i )
|
||||
{
|
||||
try{
|
||||
var tr = ltr\[i\];
|
||||
if(tr.getAttribute("id").indexOf("data") != 0){
|
||||
continue;
|
||||
}
|
||||
var ltd = tr.getElementsByTagName("td");
|
||||
var td = ltd\[0\];
|
||||
var lsearch = td.getElementsByTagName("b");
|
||||
var search = lsearch\[0\];
|
||||
//var inner_span = li.getElementsByTagName("span")\[1\] //Should only ever contain one element.
|
||||
//document.write("<p>"+search.innerText+"<br>"+filter+"<br>"+search.innerText.indexOf(filter))
|
||||
if ( search.innerText.toLowerCase().indexOf(filter) == -1 )
|
||||
{
|
||||
//document.write("a");
|
||||
//ltr.removeChild(tr);
|
||||
td.innerHTML = "";
|
||||
i--;
|
||||
}
|
||||
}catch(err) { }
|
||||
}
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
var index = -1;
|
||||
var debug = document.getElementById("debug");
|
||||
|
||||
locked_tabs = new Array();
|
||||
|
||||
}
|
||||
|
||||
function expand(id,job,name,real_name,image,key,ip,antagonist,ref){
|
||||
|
||||
clearAll();
|
||||
|
||||
var span = document.getElementById(id);
|
||||
var ckey = key.toLowerCase().replace(/\[^a-z@0-9\]+/g,"");
|
||||
|
||||
body = "<table><tr><td>";
|
||||
|
||||
body += "</td><td align='center'>";
|
||||
|
||||
body += "<font size='2'><b>"+job+" "+name+"</b><br><b>Real name "+real_name+"</b><br><b>Played by "+key+" ("+ip+")</b></font>"
|
||||
|
||||
body += "</td><td align='center'>";
|
||||
|
||||
body += "<a href='?_src_=holder;[HrefToken()];adminplayeropts="+ref+"'>PP</a> - "
|
||||
body += "<a href='?_src_=holder;[HrefToken()];showmessageckey="+ckey+"'>N</a> - "
|
||||
body += "<a href='?_src_=vars;[HrefToken()];Vars="+ref+"'>VV</a> - "
|
||||
body += "<a href='?_src_=holder;[HrefToken()];traitor="+ref+"'>TP</a> - "
|
||||
if (job == "Cyborg")
|
||||
body += "<a href='?_src_=holder;[HrefToken()];borgpanel="+ref+"'>BP</a> - "
|
||||
body += "<a href='?priv_msg="+ckey+"'>PM</a> - "
|
||||
body += "<a href='?_src_=holder;[HrefToken()];subtlemessage="+ref+"'>SM</a> - "
|
||||
body += "<a href='?_src_=holder;[HrefToken()];adminplayerobservefollow="+ref+"'>FLW</a> - "
|
||||
body += "<a href='?_src_=holder;[HrefToken()];individuallog="+ref+"'>LOGS</a><br>"
|
||||
if(antagonist > 0)
|
||||
body += "<font size='2'><a href='?_src_=holder;[HrefToken()];secrets=check_antagonist'><font color='red'><b>Antagonist</b></font></a></font>";
|
||||
|
||||
body += "</td></tr></table>";
|
||||
|
||||
|
||||
span.innerHTML = body
|
||||
}
|
||||
|
||||
function clearAll(){
|
||||
var spans = document.getElementsByTagName('span');
|
||||
for(var i = 0; i < spans.length; i++){
|
||||
var span = spans\[i\];
|
||||
|
||||
var id = span.getAttribute("id");
|
||||
|
||||
if(!(id.indexOf("item")==0))
|
||||
continue;
|
||||
|
||||
var pass = 1;
|
||||
|
||||
for(var j = 0; j < locked_tabs.length; j++){
|
||||
if(locked_tabs\[j\]==id){
|
||||
pass = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(pass != 1)
|
||||
continue;
|
||||
|
||||
|
||||
|
||||
|
||||
span.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
function addToLocked(id,link_id,notice_span_id){
|
||||
var link = document.getElementById(link_id);
|
||||
var decision = link.getAttribute("name");
|
||||
if(decision == "1"){
|
||||
link.setAttribute("name","2");
|
||||
}else{
|
||||
link.setAttribute("name","1");
|
||||
removeFromLocked(id,link_id,notice_span_id);
|
||||
return;
|
||||
}
|
||||
|
||||
var pass = 1;
|
||||
for(var j = 0; j < locked_tabs.length; j++){
|
||||
if(locked_tabs\[j\]==id){
|
||||
pass = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!pass)
|
||||
return;
|
||||
locked_tabs.push(id);
|
||||
var notice_span = document.getElementById(notice_span_id);
|
||||
notice_span.innerHTML = "<font color='red'>Locked</font> ";
|
||||
//link.setAttribute("onClick","attempt('"+id+"','"+link_id+"','"+notice_span_id+"');");
|
||||
//document.write("removeFromLocked('"+id+"','"+link_id+"','"+notice_span_id+"')");
|
||||
//document.write("aa - "+link.getAttribute("onClick"));
|
||||
}
|
||||
|
||||
function attempt(ab){
|
||||
return ab;
|
||||
}
|
||||
|
||||
function removeFromLocked(id,link_id,notice_span_id){
|
||||
//document.write("a");
|
||||
var index = 0;
|
||||
var pass = 0;
|
||||
for(var j = 0; j < locked_tabs.length; j++){
|
||||
if(locked_tabs\[j\]==id){
|
||||
pass = 1;
|
||||
index = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!pass)
|
||||
return;
|
||||
locked_tabs\[index\] = "";
|
||||
var notice_span = document.getElementById(notice_span_id);
|
||||
notice_span.innerHTML = "";
|
||||
//var link = document.getElementById(link_id);
|
||||
//link.setAttribute("onClick","addToLocked('"+id+"','"+link_id+"','"+notice_span_id+"')");
|
||||
}
|
||||
|
||||
function selectTextField(){
|
||||
var filter_text = document.getElementById('filter');
|
||||
filter_text.focus();
|
||||
filter_text.select();
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
|
||||
"}
|
||||
|
||||
//body tag start + onload and onkeypress (onkeyup) javascript event calls
|
||||
dat += "<body onload='selectTextField(); updateSearch();' onkeyup='updateSearch();'>"
|
||||
|
||||
//title + search bar
|
||||
dat += {"
|
||||
|
||||
<table width='560' align='center' cellspacing='0' cellpadding='5' id='maintable'>
|
||||
<tr id='title_tr'>
|
||||
<td align='center'>
|
||||
<font size='5'><b>Player panel</b></font><br>
|
||||
Hover over a line to see more information - <a href='?_src_=holder;[HrefToken()];check_antagonist=1'>Check antagonists</a> - Kick <a href='?_src_=holder;[HrefToken()];kick_all_from_lobby=1;afkonly=0'>everyone</a>/<a href='?_src_=holder;[HrefToken()];kick_all_from_lobby=1;afkonly=1'>AFKers</a> in lobby
|
||||
<p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id='search_tr'>
|
||||
<td align='center'>
|
||||
<b>Search:</b> <input type='text' id='filter' value='' style='width:300px;'>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
"}
|
||||
|
||||
//player table header
|
||||
dat += {"
|
||||
<span id='maintable_data_archive'>
|
||||
<table width='560' align='center' cellspacing='0' cellpadding='5' id='maintable_data'>"}
|
||||
|
||||
var/list/mobs = sortmobs()
|
||||
var/i = 1
|
||||
for(var/mob/M in mobs)
|
||||
if(M.ckey)
|
||||
|
||||
var/color = "#e6e6e6"
|
||||
if(i%2 == 0)
|
||||
color = "#f2f2f2"
|
||||
var/is_antagonist = is_special_character(M)
|
||||
|
||||
var/M_job = ""
|
||||
|
||||
if(isliving(M))
|
||||
|
||||
if(iscarbon(M)) //Carbon stuff
|
||||
if(ishuman(M))
|
||||
M_job = M.job
|
||||
else if(ismonkey(M))
|
||||
M_job = "Monkey"
|
||||
else if(isalien(M)) //aliens
|
||||
if(islarva(M))
|
||||
M_job = "Alien larva"
|
||||
else
|
||||
M_job = ROLE_ALIEN
|
||||
else
|
||||
M_job = "Carbon-based"
|
||||
|
||||
else if(issilicon(M)) //silicon
|
||||
if(isAI(M))
|
||||
M_job = "AI"
|
||||
else if(ispAI(M))
|
||||
M_job = ROLE_PAI
|
||||
else if(iscyborg(M))
|
||||
M_job = "Cyborg"
|
||||
else
|
||||
M_job = "Silicon-based"
|
||||
|
||||
else if(isanimal(M)) //simple animals
|
||||
if(iscorgi(M))
|
||||
M_job = "Corgi"
|
||||
else if(isslime(M))
|
||||
M_job = "slime"
|
||||
else
|
||||
M_job = "Animal"
|
||||
|
||||
else
|
||||
M_job = "Living"
|
||||
|
||||
else if(isnewplayer(M))
|
||||
M_job = "New player"
|
||||
|
||||
else if(isobserver(M))
|
||||
var/mob/dead/observer/O = M
|
||||
if(O.started_as_observer)//Did they get BTFO or are they just not trying?
|
||||
M_job = "Observer"
|
||||
else
|
||||
M_job = "Ghost"
|
||||
|
||||
var/M_name = html_encode(M.name)
|
||||
var/M_rname = html_encode(M.real_name)
|
||||
var/M_key = html_encode(M.key)
|
||||
|
||||
//output for each mob
|
||||
dat += {"
|
||||
|
||||
<tr id='data[i]' name='[i]' onClick="addToLocked('item[i]','data[i]','notice_span[i]')">
|
||||
<td align='center' bgcolor='[color]'>
|
||||
<span id='notice_span[i]'></span>
|
||||
<a id='link[i]'
|
||||
onmouseover='expand("item[i]","[M_job]","[M_name]","[M_rname]","--unused--","[M_key]","[M.lastKnownIP]",[is_antagonist],"[REF(M)]")'
|
||||
>
|
||||
<b id='search[i]'>[M_name] - [M_rname] - [M_key] ([M_job])</b>
|
||||
</a>
|
||||
<br><span id='item[i]'></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
"}
|
||||
|
||||
i++
|
||||
|
||||
|
||||
//player table ending
|
||||
dat += {"
|
||||
</table>
|
||||
</span>
|
||||
|
||||
<script type='text/javascript'>
|
||||
var maintable = document.getElementById("maintable_data_archive");
|
||||
var complete_list = maintable.innerHTML;
|
||||
</script>
|
||||
</body></html>
|
||||
"}
|
||||
|
||||
usr << browse(dat, "window=players;size=600x480")
|
||||
/datum/admins/proc/player_panel_new()//The new one
|
||||
if(!check_rights())
|
||||
return
|
||||
log_admin("[key_name(usr)] checked the player panel.")
|
||||
var/dat = "<html><head><title>Player Panel</title></head>"
|
||||
|
||||
//javascript, the part that does most of the work~
|
||||
dat += {"
|
||||
|
||||
<head>
|
||||
<script type='text/javascript'>
|
||||
|
||||
var locked_tabs = new Array();
|
||||
|
||||
function updateSearch(){
|
||||
|
||||
|
||||
var filter_text = document.getElementById('filter');
|
||||
var filter = filter_text.value.toLowerCase();
|
||||
|
||||
if(complete_list != null && complete_list != ""){
|
||||
var mtbl = document.getElementById("maintable_data_archive");
|
||||
mtbl.innerHTML = complete_list;
|
||||
}
|
||||
|
||||
if(filter.value == ""){
|
||||
return;
|
||||
}else{
|
||||
|
||||
var maintable_data = document.getElementById('maintable_data');
|
||||
var ltr = maintable_data.getElementsByTagName("tr");
|
||||
for ( var i = 0; i < ltr.length; ++i )
|
||||
{
|
||||
try{
|
||||
var tr = ltr\[i\];
|
||||
if(tr.getAttribute("id").indexOf("data") != 0){
|
||||
continue;
|
||||
}
|
||||
var ltd = tr.getElementsByTagName("td");
|
||||
var td = ltd\[0\];
|
||||
var lsearch = td.getElementsByTagName("b");
|
||||
var search = lsearch\[0\];
|
||||
//var inner_span = li.getElementsByTagName("span")\[1\] //Should only ever contain one element.
|
||||
//document.write("<p>"+search.innerText+"<br>"+filter+"<br>"+search.innerText.indexOf(filter))
|
||||
if ( search.innerText.toLowerCase().indexOf(filter) == -1 )
|
||||
{
|
||||
//document.write("a");
|
||||
//ltr.removeChild(tr);
|
||||
td.innerHTML = "";
|
||||
i--;
|
||||
}
|
||||
}catch(err) { }
|
||||
}
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
var index = -1;
|
||||
var debug = document.getElementById("debug");
|
||||
|
||||
locked_tabs = new Array();
|
||||
|
||||
}
|
||||
|
||||
function expand(id,job,name,real_name,image,key,ip,antagonist,ref){
|
||||
|
||||
clearAll();
|
||||
|
||||
var span = document.getElementById(id);
|
||||
var ckey = key.toLowerCase().replace(/\[^a-z@0-9\]+/g,"");
|
||||
|
||||
body = "<table><tr><td>";
|
||||
|
||||
body += "</td><td align='center'>";
|
||||
|
||||
body += "<font size='2'><b>"+job+" "+name+"</b><br><b>Real name "+real_name+"</b><br><b>Played by "+key+" ("+ip+")</b></font>"
|
||||
|
||||
body += "</td><td align='center'>";
|
||||
|
||||
body += "<a href='?_src_=holder;[HrefToken()];adminplayeropts="+ref+"'>PP</a> - "
|
||||
body += "<a href='?_src_=holder;[HrefToken()];showmessageckey="+ckey+"'>N</a> - "
|
||||
body += "<a href='?_src_=vars;[HrefToken()];Vars="+ref+"'>VV</a> - "
|
||||
body += "<a href='?_src_=holder;[HrefToken()];traitor="+ref+"'>TP</a> - "
|
||||
if (job == "Cyborg")
|
||||
body += "<a href='?_src_=holder;[HrefToken()];borgpanel="+ref+"'>BP</a> - "
|
||||
body += "<a href='?priv_msg="+ckey+"'>PM</a> - "
|
||||
body += "<a href='?_src_=holder;[HrefToken()];subtlemessage="+ref+"'>SM</a> - "
|
||||
body += "<a href='?_src_=holder;[HrefToken()];adminplayerobservefollow="+ref+"'>FLW</a> - "
|
||||
body += "<a href='?_src_=holder;[HrefToken()];individuallog="+ref+"'>LOGS</a><br>"
|
||||
if(antagonist > 0)
|
||||
body += "<font size='2'><a href='?_src_=holder;[HrefToken()];secrets=check_antagonist'><font color='red'><b>Antagonist</b></font></a></font>";
|
||||
|
||||
body += "</td></tr></table>";
|
||||
|
||||
|
||||
span.innerHTML = body
|
||||
}
|
||||
|
||||
function clearAll(){
|
||||
var spans = document.getElementsByTagName('span');
|
||||
for(var i = 0; i < spans.length; i++){
|
||||
var span = spans\[i\];
|
||||
|
||||
var id = span.getAttribute("id");
|
||||
|
||||
if(!(id.indexOf("item")==0))
|
||||
continue;
|
||||
|
||||
var pass = 1;
|
||||
|
||||
for(var j = 0; j < locked_tabs.length; j++){
|
||||
if(locked_tabs\[j\]==id){
|
||||
pass = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(pass != 1)
|
||||
continue;
|
||||
|
||||
|
||||
|
||||
|
||||
span.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
function addToLocked(id,link_id,notice_span_id){
|
||||
var link = document.getElementById(link_id);
|
||||
var decision = link.getAttribute("name");
|
||||
if(decision == "1"){
|
||||
link.setAttribute("name","2");
|
||||
}else{
|
||||
link.setAttribute("name","1");
|
||||
removeFromLocked(id,link_id,notice_span_id);
|
||||
return;
|
||||
}
|
||||
|
||||
var pass = 1;
|
||||
for(var j = 0; j < locked_tabs.length; j++){
|
||||
if(locked_tabs\[j\]==id){
|
||||
pass = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!pass)
|
||||
return;
|
||||
locked_tabs.push(id);
|
||||
var notice_span = document.getElementById(notice_span_id);
|
||||
notice_span.innerHTML = "<font color='red'>Locked</font> ";
|
||||
//link.setAttribute("onClick","attempt('"+id+"','"+link_id+"','"+notice_span_id+"');");
|
||||
//document.write("removeFromLocked('"+id+"','"+link_id+"','"+notice_span_id+"')");
|
||||
//document.write("aa - "+link.getAttribute("onClick"));
|
||||
}
|
||||
|
||||
function attempt(ab){
|
||||
return ab;
|
||||
}
|
||||
|
||||
function removeFromLocked(id,link_id,notice_span_id){
|
||||
//document.write("a");
|
||||
var index = 0;
|
||||
var pass = 0;
|
||||
for(var j = 0; j < locked_tabs.length; j++){
|
||||
if(locked_tabs\[j\]==id){
|
||||
pass = 1;
|
||||
index = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!pass)
|
||||
return;
|
||||
locked_tabs\[index\] = "";
|
||||
var notice_span = document.getElementById(notice_span_id);
|
||||
notice_span.innerHTML = "";
|
||||
//var link = document.getElementById(link_id);
|
||||
//link.setAttribute("onClick","addToLocked('"+id+"','"+link_id+"','"+notice_span_id+"')");
|
||||
}
|
||||
|
||||
function selectTextField(){
|
||||
var filter_text = document.getElementById('filter');
|
||||
filter_text.focus();
|
||||
filter_text.select();
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
|
||||
"}
|
||||
|
||||
//body tag start + onload and onkeypress (onkeyup) javascript event calls
|
||||
dat += "<body onload='selectTextField(); updateSearch();' onkeyup='updateSearch();'>"
|
||||
|
||||
//title + search bar
|
||||
dat += {"
|
||||
|
||||
<table width='560' align='center' cellspacing='0' cellpadding='5' id='maintable'>
|
||||
<tr id='title_tr'>
|
||||
<td align='center'>
|
||||
<font size='5'><b>Player panel</b></font><br>
|
||||
Hover over a line to see more information - <a href='?_src_=holder;[HrefToken()];check_antagonist=1'>Check antagonists</a> - Kick <a href='?_src_=holder;[HrefToken()];kick_all_from_lobby=1;afkonly=0'>everyone</a>/<a href='?_src_=holder;[HrefToken()];kick_all_from_lobby=1;afkonly=1'>AFKers</a> in lobby
|
||||
<p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id='search_tr'>
|
||||
<td align='center'>
|
||||
<b>Search:</b> <input type='text' id='filter' value='' style='width:300px;'>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
"}
|
||||
|
||||
//player table header
|
||||
dat += {"
|
||||
<span id='maintable_data_archive'>
|
||||
<table width='560' align='center' cellspacing='0' cellpadding='5' id='maintable_data'>"}
|
||||
|
||||
var/list/mobs = sortmobs()
|
||||
var/i = 1
|
||||
for(var/mob/M in mobs)
|
||||
if(M.ckey)
|
||||
|
||||
var/color = "#e6e6e6"
|
||||
if(i%2 == 0)
|
||||
color = "#f2f2f2"
|
||||
var/is_antagonist = is_special_character(M)
|
||||
|
||||
var/M_job = ""
|
||||
|
||||
if(isliving(M))
|
||||
|
||||
if(iscarbon(M)) //Carbon stuff
|
||||
if(ishuman(M))
|
||||
M_job = M.job
|
||||
else if(ismonkey(M))
|
||||
M_job = "Monkey"
|
||||
else if(isalien(M)) //aliens
|
||||
if(islarva(M))
|
||||
M_job = "Alien larva"
|
||||
else
|
||||
M_job = ROLE_ALIEN
|
||||
else
|
||||
M_job = "Carbon-based"
|
||||
|
||||
else if(issilicon(M)) //silicon
|
||||
if(isAI(M))
|
||||
M_job = "AI"
|
||||
else if(ispAI(M))
|
||||
M_job = ROLE_PAI
|
||||
else if(iscyborg(M))
|
||||
M_job = "Cyborg"
|
||||
else
|
||||
M_job = "Silicon-based"
|
||||
|
||||
else if(isanimal(M)) //simple animals
|
||||
if(iscorgi(M))
|
||||
M_job = "Corgi"
|
||||
else if(isslime(M))
|
||||
M_job = "slime"
|
||||
else
|
||||
M_job = "Animal"
|
||||
|
||||
else
|
||||
M_job = "Living"
|
||||
|
||||
else if(isnewplayer(M))
|
||||
M_job = "New player"
|
||||
|
||||
else if(isobserver(M))
|
||||
var/mob/dead/observer/O = M
|
||||
if(O.started_as_observer)//Did they get BTFO or are they just not trying?
|
||||
M_job = "Observer"
|
||||
else
|
||||
M_job = "Ghost"
|
||||
|
||||
var/M_name = html_encode(M.name)
|
||||
var/M_rname = html_encode(M.real_name)
|
||||
var/M_key = html_encode(M.key)
|
||||
|
||||
//output for each mob
|
||||
dat += {"
|
||||
|
||||
<tr id='data[i]' name='[i]' onClick="addToLocked('item[i]','data[i]','notice_span[i]')">
|
||||
<td align='center' bgcolor='[color]'>
|
||||
<span id='notice_span[i]'></span>
|
||||
<a id='link[i]'
|
||||
onmouseover='expand("item[i]","[M_job]","[M_name]","[M_rname]","--unused--","[M_key]","[M.lastKnownIP]",[is_antagonist],"[REF(M)]")'
|
||||
>
|
||||
<b id='search[i]'>[M_name] - [M_rname] - [M_key] ([M_job])</b>
|
||||
</a>
|
||||
<br><span id='item[i]'></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
"}
|
||||
|
||||
i++
|
||||
|
||||
|
||||
//player table ending
|
||||
dat += {"
|
||||
</table>
|
||||
</span>
|
||||
|
||||
<script type='text/javascript'>
|
||||
var maintable = document.getElementById("maintable_data_archive");
|
||||
var complete_list = maintable.innerHTML;
|
||||
</script>
|
||||
</body></html>
|
||||
"}
|
||||
|
||||
usr << browse(dat, "window=players;size=600x480")
|
||||
|
||||
@@ -52,9 +52,11 @@
|
||||
edit_emitter(user)
|
||||
|
||||
/obj/effect/sound_emitter/AltClick(mob/user)
|
||||
. = ..()
|
||||
if(check_rights_for(user.client, R_SOUNDS))
|
||||
activate(user)
|
||||
to_chat(user, "<span class='notice'>Sound emitter activated.</span>")
|
||||
return TRUE
|
||||
|
||||
/obj/effect/sound_emitter/proc/edit_emitter(mob/user)
|
||||
var/dat = ""
|
||||
|
||||
+2848
-2848
File diff suppressed because it is too large
Load Diff
@@ -95,6 +95,10 @@
|
||||
|
||||
Don't crash the server, OK?
|
||||
|
||||
"UPDATE /mob/living/carbon/monkey SET #null = forceMove(usr.loc)"
|
||||
|
||||
Writing "#null" in front of the "=" will call the proc and discard the return value.
|
||||
|
||||
A quick recommendation: before you run something like a DELETE or another query.. Run it through SELECT
|
||||
first.
|
||||
You'd rather not gib every player on accident.
|
||||
@@ -740,6 +744,9 @@ GLOBAL_DATUM_INIT(sdql2_vv_statobj, /obj/effect/statclick/SDQL2_VV_all, new(null
|
||||
var/datum/temp = d
|
||||
var/i = 0
|
||||
for(var/v in sets)
|
||||
if(v == "#null")
|
||||
SDQL_expression(d, set_list[sets])
|
||||
break
|
||||
if(++i == sets.len)
|
||||
if(superuser)
|
||||
if(temp.vars.Find(v))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,157 +1,157 @@
|
||||
/client/proc/jumptoarea(area/A in GLOB.sortedAreas)
|
||||
set name = "Jump to Area"
|
||||
set desc = "Area to jump to"
|
||||
set category = "Admin"
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
if(!A)
|
||||
return
|
||||
|
||||
var/list/turfs = list()
|
||||
for(var/turf/T in A)
|
||||
if(T.density)
|
||||
continue
|
||||
turfs.Add(T)
|
||||
|
||||
var/turf/T = safepick(turfs)
|
||||
if(!T)
|
||||
to_chat(src, "Nowhere to jump to!")
|
||||
return
|
||||
usr.forceMove(T)
|
||||
log_admin("[key_name(usr)] jumped to [AREACOORD(A)]")
|
||||
message_admins("[key_name_admin(usr)] jumped to [AREACOORD(A)]")
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Jump To Area") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/jumptoturf(turf/T in world)
|
||||
set name = "Jump to Turf"
|
||||
set category = "Admin"
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
log_admin("[key_name(usr)] jumped to [AREACOORD(T)]")
|
||||
message_admins("[key_name_admin(usr)] jumped to [AREACOORD(T)]")
|
||||
usr.forceMove(T)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Jump To Turf") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
return
|
||||
|
||||
/client/proc/jumptomob(mob/M in GLOB.mob_list)
|
||||
set category = "Admin"
|
||||
set name = "Jump to Mob"
|
||||
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
log_admin("[key_name(usr)] jumped to [key_name(M)]")
|
||||
message_admins("[key_name_admin(usr)] jumped to [ADMIN_LOOKUPFLW(M)] at [AREACOORD(M)]")
|
||||
if(src.mob)
|
||||
var/mob/A = src.mob
|
||||
var/turf/T = get_turf(M)
|
||||
if(T && isturf(T))
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Jump To Mob") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
A.forceMove(M.loc)
|
||||
else
|
||||
to_chat(A, "This mob is not located in the game world.")
|
||||
|
||||
/client/proc/jumptocoord(tx as num, ty as num, tz as num)
|
||||
set category = "Admin"
|
||||
set name = "Jump to Coordinate"
|
||||
|
||||
if (!holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
if(src.mob)
|
||||
var/mob/A = src.mob
|
||||
var/turf/T = locate(tx,ty,tz)
|
||||
A.forceMove(T)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Jump To Coordiate") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
message_admins("[key_name_admin(usr)] jumped to coordinates [tx], [ty], [tz]")
|
||||
|
||||
/client/proc/jumptokey()
|
||||
set category = "Admin"
|
||||
set name = "Jump to Key"
|
||||
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
var/list/keys = list()
|
||||
for(var/mob/M in GLOB.player_list)
|
||||
keys += M.client
|
||||
var/client/selection = input("Please, select a player!", "Admin Jumping", null, null) as null|anything in sortKey(keys)
|
||||
if(!selection)
|
||||
to_chat(src, "No keys found.")
|
||||
return
|
||||
var/mob/M = selection.mob
|
||||
log_admin("[key_name(usr)] jumped to [key_name(M)]")
|
||||
message_admins("[key_name_admin(usr)] jumped to [ADMIN_LOOKUPFLW(M)]")
|
||||
|
||||
usr.forceMove(M.loc)
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Jump To Key") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/Getmob(mob/M in GLOB.mob_list - GLOB.dummy_mob_list)
|
||||
set category = "Admin"
|
||||
set name = "Get Mob"
|
||||
set desc = "Mob to teleport"
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
var/atom/loc = get_turf(usr)
|
||||
log_admin("[key_name(usr)] teleported [key_name(M)] to [AREACOORD(loc)]")
|
||||
var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(M)] to [ADMIN_VERBOSEJMP(loc)]"
|
||||
message_admins(msg)
|
||||
admin_ticket_log(M, msg)
|
||||
M.forceMove(loc)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Get Mob") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/Getkey()
|
||||
set category = "Admin"
|
||||
set name = "Get Key"
|
||||
set desc = "Key to teleport"
|
||||
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
var/list/keys = list()
|
||||
for(var/mob/M in GLOB.player_list)
|
||||
keys += M.client
|
||||
var/client/selection = input("Please, select a player!", "Admin Jumping", null, null) as null|anything in sortKey(keys)
|
||||
if(!selection)
|
||||
return
|
||||
var/mob/M = selection.mob
|
||||
|
||||
if(!M)
|
||||
return
|
||||
log_admin("[key_name(usr)] teleported [key_name(M)]")
|
||||
var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(M)]"
|
||||
message_admins(msg)
|
||||
admin_ticket_log(M, msg)
|
||||
if(M)
|
||||
M.forceMove(get_turf(usr))
|
||||
usr.forceMove(M.loc)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Get Key") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/sendmob(mob/M in sortmobs())
|
||||
set category = "Admin"
|
||||
set name = "Send Mob"
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
var/area/A = input(usr, "Pick an area.", "Pick an area") in GLOB.sortedAreas|null
|
||||
if(A && istype(A))
|
||||
if(M.forceMove(safepick(get_area_turfs(A))))
|
||||
|
||||
log_admin("[key_name(usr)] teleported [key_name(M)] to [AREACOORD(A)]")
|
||||
var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(M)] to [AREACOORD(A)]"
|
||||
message_admins(msg)
|
||||
admin_ticket_log(M, msg)
|
||||
else
|
||||
to_chat(src, "Failed to move mob to a valid location.")
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Send Mob") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
/client/proc/jumptoarea(area/A in GLOB.sortedAreas)
|
||||
set name = "Jump to Area"
|
||||
set desc = "Area to jump to"
|
||||
set category = "Admin"
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
if(!A)
|
||||
return
|
||||
|
||||
var/list/turfs = list()
|
||||
for(var/turf/T in A)
|
||||
if(T.density)
|
||||
continue
|
||||
turfs.Add(T)
|
||||
|
||||
var/turf/T = safepick(turfs)
|
||||
if(!T)
|
||||
to_chat(src, "Nowhere to jump to!")
|
||||
return
|
||||
usr.forceMove(T)
|
||||
log_admin("[key_name(usr)] jumped to [AREACOORD(A)]")
|
||||
message_admins("[key_name_admin(usr)] jumped to [AREACOORD(A)]")
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Jump To Area") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/jumptoturf(turf/T in world)
|
||||
set name = "Jump to Turf"
|
||||
set category = "Admin"
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
log_admin("[key_name(usr)] jumped to [AREACOORD(T)]")
|
||||
message_admins("[key_name_admin(usr)] jumped to [AREACOORD(T)]")
|
||||
usr.forceMove(T)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Jump To Turf") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
return
|
||||
|
||||
/client/proc/jumptomob(mob/M in GLOB.mob_list)
|
||||
set category = "Admin"
|
||||
set name = "Jump to Mob"
|
||||
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
log_admin("[key_name(usr)] jumped to [key_name(M)]")
|
||||
message_admins("[key_name_admin(usr)] jumped to [ADMIN_LOOKUPFLW(M)] at [AREACOORD(M)]")
|
||||
if(src.mob)
|
||||
var/mob/A = src.mob
|
||||
var/turf/T = get_turf(M)
|
||||
if(T && isturf(T))
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Jump To Mob") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
A.forceMove(M.loc)
|
||||
else
|
||||
to_chat(A, "This mob is not located in the game world.")
|
||||
|
||||
/client/proc/jumptocoord(tx as num, ty as num, tz as num)
|
||||
set category = "Admin"
|
||||
set name = "Jump to Coordinate"
|
||||
|
||||
if (!holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
if(src.mob)
|
||||
var/mob/A = src.mob
|
||||
var/turf/T = locate(tx,ty,tz)
|
||||
A.forceMove(T)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Jump To Coordiate") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
message_admins("[key_name_admin(usr)] jumped to coordinates [tx], [ty], [tz]")
|
||||
|
||||
/client/proc/jumptokey()
|
||||
set category = "Admin"
|
||||
set name = "Jump to Key"
|
||||
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
var/list/keys = list()
|
||||
for(var/mob/M in GLOB.player_list)
|
||||
keys += M.client
|
||||
var/client/selection = input("Please, select a player!", "Admin Jumping", null, null) as null|anything in sortKey(keys)
|
||||
if(!selection)
|
||||
to_chat(src, "No keys found.")
|
||||
return
|
||||
var/mob/M = selection.mob
|
||||
log_admin("[key_name(usr)] jumped to [key_name(M)]")
|
||||
message_admins("[key_name_admin(usr)] jumped to [ADMIN_LOOKUPFLW(M)]")
|
||||
|
||||
usr.forceMove(M.loc)
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Jump To Key") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/Getmob(mob/M in GLOB.mob_list - GLOB.dummy_mob_list)
|
||||
set category = "Admin"
|
||||
set name = "Get Mob"
|
||||
set desc = "Mob to teleport"
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
var/atom/loc = get_turf(usr)
|
||||
log_admin("[key_name(usr)] teleported [key_name(M)] to [AREACOORD(loc)]")
|
||||
var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(M)] to [ADMIN_VERBOSEJMP(loc)]"
|
||||
message_admins(msg)
|
||||
admin_ticket_log(M, msg)
|
||||
M.forceMove(loc)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Get Mob") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/Getkey()
|
||||
set category = "Admin"
|
||||
set name = "Get Key"
|
||||
set desc = "Key to teleport"
|
||||
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
|
||||
var/list/keys = list()
|
||||
for(var/mob/M in GLOB.player_list)
|
||||
keys += M.client
|
||||
var/client/selection = input("Please, select a player!", "Admin Jumping", null, null) as null|anything in sortKey(keys)
|
||||
if(!selection)
|
||||
return
|
||||
var/mob/M = selection.mob
|
||||
|
||||
if(!M)
|
||||
return
|
||||
log_admin("[key_name(usr)] teleported [key_name(M)]")
|
||||
var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(M)]"
|
||||
message_admins(msg)
|
||||
admin_ticket_log(M, msg)
|
||||
if(M)
|
||||
M.forceMove(get_turf(usr))
|
||||
usr.forceMove(M.loc)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Get Key") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/sendmob(mob/M in sortmobs())
|
||||
set category = "Admin"
|
||||
set name = "Send Mob"
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
var/area/A = input(usr, "Pick an area.", "Pick an area") in GLOB.sortedAreas|null
|
||||
if(A && istype(A))
|
||||
if(M.forceMove(safepick(get_area_turfs(A))))
|
||||
|
||||
log_admin("[key_name(usr)] teleported [key_name(M)] to [AREACOORD(A)]")
|
||||
var/msg = "[key_name_admin(usr)] teleported [ADMIN_LOOKUPFLW(M)] to [AREACOORD(A)]"
|
||||
message_admins(msg)
|
||||
admin_ticket_log(M, msg)
|
||||
else
|
||||
to_chat(src, "Failed to move mob to a valid location.")
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Send Mob") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
+325
-325
@@ -1,325 +1,325 @@
|
||||
#define IRCREPLYCOUNT 2
|
||||
|
||||
|
||||
//allows right clicking mobs to send an admin PM to their client, forwards the selected mob's client to cmd_admin_pm
|
||||
/client/proc/cmd_admin_pm_context(mob/M in GLOB.mob_list)
|
||||
set category = null
|
||||
set name = "Admin PM Mob"
|
||||
if(!holder)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM-Context: Only administrators may use this command.</span>")
|
||||
return
|
||||
if( !ismob(M) || !M.client )
|
||||
return
|
||||
cmd_admin_pm(M.client,null)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Admin PM Mob") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
//shows a list of clients we could send PMs to, then forwards our choice to cmd_admin_pm
|
||||
/client/proc/cmd_admin_pm_panel()
|
||||
set category = "Admin"
|
||||
set name = "Admin PM"
|
||||
if(!holder)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM-Panel: Only administrators may use this command.</span>")
|
||||
return
|
||||
var/list/client/targets[0]
|
||||
for(var/client/T)
|
||||
if(T.mob)
|
||||
if(isnewplayer(T.mob))
|
||||
targets["(New Player) - [T]"] = T
|
||||
else if(isobserver(T.mob))
|
||||
targets["[T.mob.name](Ghost) - [T]"] = T
|
||||
else
|
||||
targets["[T.mob.real_name](as [T.mob.name]) - [T]"] = T
|
||||
else
|
||||
targets["(No Mob) - [T]"] = T
|
||||
var/target = input(src,"To whom shall we send a message?","Admin PM",null) as null|anything in sortList(targets)
|
||||
cmd_admin_pm(targets[target],null)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Admin PM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/cmd_ahelp_reply(whom)
|
||||
if(prefs.muted & MUTE_ADMINHELP)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: You are unable to use admin PM-s (muted).</span>")
|
||||
return
|
||||
var/client/C
|
||||
if(istext(whom))
|
||||
if(cmptext(copytext(whom,1,2),"@"))
|
||||
whom = findStealthKey(whom)
|
||||
C = GLOB.directory[whom]
|
||||
else if(istype(whom, /client))
|
||||
C = whom
|
||||
if(!C)
|
||||
if(holder)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: Client not found.</span>")
|
||||
return
|
||||
|
||||
var/datum/admin_help/AH = C.current_ticket
|
||||
|
||||
if(AH)
|
||||
message_admins("[key_name_admin(src)] has started replying to [key_name(C, 0, 0)]'s admin help.")
|
||||
var/msg = input(src,"Message:", "Private message to [key_name(C, 0, 0)]") as message|null
|
||||
if (!msg)
|
||||
message_admins("[key_name_admin(src)] has cancelled their reply to [key_name(C, 0, 0)]'s admin help.")
|
||||
return
|
||||
cmd_admin_pm(whom, msg)
|
||||
|
||||
//takes input from cmd_admin_pm_context, cmd_admin_pm_panel or /client/Topic and sends them a PM.
|
||||
//Fetching a message if needed. src is the sender and C is the target client
|
||||
/client/proc/cmd_admin_pm(whom, msg)
|
||||
if(prefs.muted & MUTE_ADMINHELP)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: You are unable to use admin PM-s (muted).</span>")
|
||||
return
|
||||
|
||||
if(!holder && !current_ticket) //no ticket? https://www.youtube.com/watch?v=iHSPf6x1Fdo
|
||||
to_chat(src, "<span class='danger'>You can no longer reply to this ticket, please open another one by using the Adminhelp verb if need be.</span>")
|
||||
to_chat(src, "<span class='notice'>Message: [msg]</span>")
|
||||
return
|
||||
|
||||
var/client/recipient
|
||||
var/irc = 0
|
||||
if(istext(whom))
|
||||
if(cmptext(copytext(whom,1,2),"@"))
|
||||
whom = findStealthKey(whom)
|
||||
if(whom == "IRCKEY")
|
||||
irc = 1
|
||||
else
|
||||
recipient = GLOB.directory[whom]
|
||||
else if(istype(whom, /client))
|
||||
recipient = whom
|
||||
|
||||
|
||||
if(irc)
|
||||
if(!ircreplyamount) //to prevent people from spamming irc
|
||||
return
|
||||
if(!msg)
|
||||
msg = input(src,"Message:", "Private message to Administrator") as text|null
|
||||
|
||||
if(!msg)
|
||||
return
|
||||
if(holder)
|
||||
to_chat(src, "<span class='danger'>Error: Use the admin IRC channel, nerd.</span>")
|
||||
return
|
||||
|
||||
|
||||
else
|
||||
if(!recipient)
|
||||
if(holder)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: Client not found.</span>")
|
||||
if(msg)
|
||||
to_chat(src, msg)
|
||||
return
|
||||
else if(msg) // you want to continue if there's no message instead of returning now
|
||||
current_ticket.MessageNoRecipient(msg)
|
||||
return
|
||||
|
||||
//get message text, limit it's length.and clean/escape html
|
||||
if(!msg)
|
||||
msg = input(src,"Message:", "Private message to [key_name(recipient, 0, 0)]") as message|null
|
||||
msg = trim(msg)
|
||||
if(!msg)
|
||||
return
|
||||
|
||||
if(prefs.muted & MUTE_ADMINHELP)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: You are unable to use admin PM-s (muted).</span>")
|
||||
return
|
||||
|
||||
if(!recipient)
|
||||
if(holder)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: Client not found.</span>")
|
||||
else
|
||||
current_ticket.MessageNoRecipient(msg)
|
||||
return
|
||||
|
||||
if (src.handle_spam_prevention(msg,MUTE_ADMINHELP))
|
||||
return
|
||||
|
||||
//clean the message if it's not sent by a high-rank admin
|
||||
if(!check_rights(R_SERVER|R_DEBUG,0)||irc)//no sending html to the poor bots
|
||||
msg = trim(sanitize(copytext(msg,1,MAX_MESSAGE_LEN)))
|
||||
if(!msg)
|
||||
return
|
||||
|
||||
var/rawmsg = msg
|
||||
|
||||
if(holder)
|
||||
msg = emoji_parse(msg)
|
||||
|
||||
var/keywordparsedmsg = keywords_lookup(msg)
|
||||
|
||||
if(irc)
|
||||
to_chat(src, "<span class='notice'>PM to-<b>Admins</b>: <span class='linkify'>[rawmsg]</span></span>")
|
||||
var/datum/admin_help/AH = admin_ticket_log(src, "<font color='red'>Reply PM from-<b>[key_name(src, TRUE, TRUE)] to <i>IRC</i>: [keywordparsedmsg]</font>")
|
||||
ircreplyamount--
|
||||
send2irc("[AH ? "#[AH.id] " : ""]Reply: [ckey]", rawmsg)
|
||||
else
|
||||
if(recipient.holder)
|
||||
if(holder) //both are admins
|
||||
to_chat(recipient, "<span class='danger'>Admin PM from-<b>[key_name(src, recipient, 1)]</b>: <span class='linkify'>[keywordparsedmsg]</span></span>")
|
||||
to_chat(src, "<span class='notice'>Admin PM to-<b>[key_name(recipient, src, 1)]</b>: <span class='linkify'>[keywordparsedmsg]</span></span>")
|
||||
|
||||
//omg this is dumb, just fill in both their tickets
|
||||
var/interaction_message = "<font color='purple'>PM from-<b>[key_name(src, recipient, 1)]</b> to-<b>[key_name(recipient, src, 1)]</b>: [keywordparsedmsg]</font>"
|
||||
admin_ticket_log(src, interaction_message)
|
||||
if(recipient != src) //reeee
|
||||
admin_ticket_log(recipient, interaction_message)
|
||||
|
||||
else //recipient is an admin but sender is not
|
||||
var/replymsg = "Reply PM from-<b>[key_name(src, recipient, 1)]</b>: <span class='linkify'>[keywordparsedmsg]</span>"
|
||||
admin_ticket_log(src, "<font color='red'>[replymsg]</font>")
|
||||
to_chat(recipient, "<span class='danger'>[replymsg]</span>")
|
||||
to_chat(src, "<span class='notice'>PM to-<b>Admins</b>: <span class='linkify'>[msg]</span></span>")
|
||||
|
||||
//play the receiving admin the adminhelp sound (if they have them enabled)
|
||||
if(recipient.prefs.toggles & SOUND_ADMINHELP)
|
||||
SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg'))
|
||||
|
||||
else
|
||||
if(holder) //sender is an admin but recipient is not. Do BIG RED TEXT
|
||||
if(!recipient.current_ticket)
|
||||
new /datum/admin_help(msg, recipient, TRUE)
|
||||
|
||||
to_chat(recipient, "<font color='red' size='4'><b>-- Administrator private message --</b></font>")
|
||||
to_chat(recipient, "<span class='danger'>Admin PM from-<b>[key_name(src, recipient, 0)]</b>: <span class='linkify'>[msg]</span></span>")
|
||||
to_chat(recipient, "<span class='danger'><i>Click on the administrator's name to reply.</i></span>")
|
||||
to_chat(src, "<span class='notice'>Admin PM to-<b>[key_name(recipient, src, 1)]</b>: <span class='linkify'>[msg]</span></span>")
|
||||
|
||||
admin_ticket_log(recipient, "<font color='purple'>PM From [key_name_admin(src)]: [keywordparsedmsg]</font>")
|
||||
|
||||
//always play non-admin recipients the adminhelp sound
|
||||
SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg'))
|
||||
|
||||
//AdminPM popup for ApocStation and anybody else who wants to use it. Set it with POPUP_ADMIN_PM in config.txt ~Carn
|
||||
if(CONFIG_GET(flag/popup_admin_pm))
|
||||
spawn() //so we don't hold the caller proc up
|
||||
var/sender = src
|
||||
var/sendername = key
|
||||
var/reply = input(recipient, msg,"Admin PM from-[sendername]", "") as text|null //show message and await a reply
|
||||
if(recipient && reply)
|
||||
if(sender)
|
||||
recipient.cmd_admin_pm(sender,reply) //sender is still about, let's reply to them
|
||||
else
|
||||
adminhelp(reply) //sender has left, adminhelp instead
|
||||
return
|
||||
|
||||
else //neither are admins
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: Non-admin to non-admin PM communication is forbidden.</span>")
|
||||
return
|
||||
|
||||
if(irc)
|
||||
log_admin_private("PM: [key_name(src)]->IRC: [rawmsg]")
|
||||
for(var/client/X in GLOB.admins)
|
||||
to_chat(X, "<span class='notice'><B>PM: [key_name(src, X, 0)]->IRC:</B> [keywordparsedmsg]</span>")
|
||||
else
|
||||
window_flash(recipient, ignorepref = TRUE)
|
||||
log_admin_private("PM: [key_name(src)]->[key_name(recipient)]: [rawmsg]")
|
||||
//we don't use message_admins here because the sender/receiver might get it too
|
||||
for(var/client/X in GLOB.admins)
|
||||
if(X.key!=key && X.key!=recipient.key) //check client/X is an admin and isn't the sender or recipient
|
||||
to_chat(X, "<span class='notice'><B>PM: [key_name(src, X, 0)]->[key_name(recipient, X, 0)]:</B> [keywordparsedmsg]</span>" )
|
||||
|
||||
|
||||
|
||||
#define IRC_AHELP_USAGE "Usage: ticket <close|resolve|icissue|reject|reopen \[ticket #\]|list>"
|
||||
/proc/IrcPm(target,msg,sender)
|
||||
target = ckey(target)
|
||||
var/client/C = GLOB.directory[target]
|
||||
|
||||
var/datum/admin_help/ticket = C ? C.current_ticket : GLOB.ahelp_tickets.CKey2ActiveTicket(target)
|
||||
var/compliant_msg = trim(lowertext(msg))
|
||||
var/irc_tagged = "[sender](IRC)"
|
||||
var/list/splits = splittext(compliant_msg, " ")
|
||||
if(splits.len && splits[1] == "ticket")
|
||||
if(splits.len < 2)
|
||||
return IRC_AHELP_USAGE
|
||||
switch(splits[2])
|
||||
if("close")
|
||||
if(ticket)
|
||||
ticket.Close(irc_tagged)
|
||||
return "Ticket #[ticket.id] successfully closed"
|
||||
if("resolve")
|
||||
if(ticket)
|
||||
ticket.Resolve(irc_tagged)
|
||||
return "Ticket #[ticket.id] successfully resolved"
|
||||
if("icissue")
|
||||
if(ticket)
|
||||
ticket.ICIssue(irc_tagged)
|
||||
return "Ticket #[ticket.id] successfully marked as IC issue"
|
||||
if("reject")
|
||||
if(ticket)
|
||||
ticket.Reject(irc_tagged)
|
||||
return "Ticket #[ticket.id] successfully rejected"
|
||||
if("reopen")
|
||||
if(ticket)
|
||||
return "Error: [target] already has ticket #[ticket.id] open"
|
||||
var/fail = splits.len < 3 ? null : -1
|
||||
if(!isnull(fail))
|
||||
fail = text2num(splits[3])
|
||||
if(isnull(fail))
|
||||
return "Error: No/Invalid ticket id specified. [IRC_AHELP_USAGE]"
|
||||
var/datum/admin_help/AH = GLOB.ahelp_tickets.TicketByID(fail)
|
||||
if(!AH)
|
||||
return "Error: Ticket #[fail] not found"
|
||||
if(AH.initiator_ckey != target)
|
||||
return "Error: Ticket #[fail] belongs to [AH.initiator_ckey]"
|
||||
AH.Reopen()
|
||||
return "Ticket #[ticket.id] successfully reopened"
|
||||
if("list")
|
||||
var/list/tickets = GLOB.ahelp_tickets.TicketsByCKey(target)
|
||||
if(!tickets.len)
|
||||
return "None"
|
||||
. = ""
|
||||
for(var/I in tickets)
|
||||
var/datum/admin_help/AH = I
|
||||
if(.)
|
||||
. += ", "
|
||||
if(AH == ticket)
|
||||
. += "Active: "
|
||||
. += "#[AH.id]"
|
||||
return
|
||||
else
|
||||
return IRC_AHELP_USAGE
|
||||
return "Error: Ticket could not be found"
|
||||
|
||||
var/static/stealthkey
|
||||
var/adminname = CONFIG_GET(flag/show_irc_name) ? irc_tagged : "Administrator"
|
||||
|
||||
if(!C)
|
||||
return "Error: No client"
|
||||
|
||||
if(!stealthkey)
|
||||
stealthkey = GenIrcStealthKey()
|
||||
|
||||
msg = sanitize(copytext(msg,1,MAX_MESSAGE_LEN))
|
||||
if(!msg)
|
||||
return "Error: No message"
|
||||
|
||||
message_admins("IRC message from [sender] to [key_name_admin(C)] : [msg]")
|
||||
log_admin_private("IRC PM: [sender] -> [key_name(C)] : [msg]")
|
||||
msg = emoji_parse(msg)
|
||||
|
||||
to_chat(C, "<font color='red' size='4'><b>-- Administrator private message --</b></font>")
|
||||
to_chat(C, "<span class='adminsay'>Admin PM from-<b><a href='?priv_msg=[stealthkey]'>[adminname]</A></b>: [msg]</span>")
|
||||
to_chat(C, "<span class='adminsay'><i>Click on the administrator's name to reply.</i></span>")
|
||||
|
||||
admin_ticket_log(C, "<font color='purple'>PM From [irc_tagged]: [msg]</font>")
|
||||
|
||||
window_flash(C, ignorepref = TRUE)
|
||||
//always play non-admin recipients the adminhelp sound
|
||||
SEND_SOUND(C, 'sound/effects/adminhelp.ogg')
|
||||
|
||||
C.ircreplyamount = IRCREPLYCOUNT
|
||||
|
||||
return "Message Successful"
|
||||
|
||||
/proc/GenIrcStealthKey()
|
||||
var/num = (rand(0,1000))
|
||||
var/i = 0
|
||||
while(i == 0)
|
||||
i = 1
|
||||
for(var/P in GLOB.stealthminID)
|
||||
if(num == GLOB.stealthminID[P])
|
||||
num++
|
||||
i = 0
|
||||
var/stealth = "@[num2text(num)]"
|
||||
GLOB.stealthminID["IRCKEY"] = stealth
|
||||
return stealth
|
||||
|
||||
#undef IRCREPLYCOUNT
|
||||
#define IRCREPLYCOUNT 2
|
||||
|
||||
|
||||
//allows right clicking mobs to send an admin PM to their client, forwards the selected mob's client to cmd_admin_pm
|
||||
/client/proc/cmd_admin_pm_context(mob/M in GLOB.mob_list)
|
||||
set category = null
|
||||
set name = "Admin PM Mob"
|
||||
if(!holder)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM-Context: Only administrators may use this command.</span>")
|
||||
return
|
||||
if( !ismob(M) || !M.client )
|
||||
return
|
||||
cmd_admin_pm(M.client,null)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Admin PM Mob") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
//shows a list of clients we could send PMs to, then forwards our choice to cmd_admin_pm
|
||||
/client/proc/cmd_admin_pm_panel()
|
||||
set category = "Admin"
|
||||
set name = "Admin PM"
|
||||
if(!holder)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM-Panel: Only administrators may use this command.</span>")
|
||||
return
|
||||
var/list/client/targets[0]
|
||||
for(var/client/T)
|
||||
if(T.mob)
|
||||
if(isnewplayer(T.mob))
|
||||
targets["(New Player) - [T]"] = T
|
||||
else if(isobserver(T.mob))
|
||||
targets["[T.mob.name](Ghost) - [T]"] = T
|
||||
else
|
||||
targets["[T.mob.real_name](as [T.mob.name]) - [T]"] = T
|
||||
else
|
||||
targets["(No Mob) - [T]"] = T
|
||||
var/target = input(src,"To whom shall we send a message?","Admin PM",null) as null|anything in sortList(targets)
|
||||
cmd_admin_pm(targets[target],null)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Admin PM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/cmd_ahelp_reply(whom)
|
||||
if(prefs.muted & MUTE_ADMINHELP)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: You are unable to use admin PM-s (muted).</span>")
|
||||
return
|
||||
var/client/C
|
||||
if(istext(whom))
|
||||
if(cmptext(copytext(whom,1,2),"@"))
|
||||
whom = findStealthKey(whom)
|
||||
C = GLOB.directory[whom]
|
||||
else if(istype(whom, /client))
|
||||
C = whom
|
||||
if(!C)
|
||||
if(holder)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: Client not found.</span>")
|
||||
return
|
||||
|
||||
var/datum/admin_help/AH = C.current_ticket
|
||||
|
||||
if(AH)
|
||||
message_admins("[key_name_admin(src)] has started replying to [key_name(C, 0, 0)]'s admin help.")
|
||||
var/msg = input(src,"Message:", "Private message to [key_name(C, 0, 0)]") as message|null
|
||||
if (!msg)
|
||||
message_admins("[key_name_admin(src)] has cancelled their reply to [key_name(C, 0, 0)]'s admin help.")
|
||||
return
|
||||
cmd_admin_pm(whom, msg)
|
||||
|
||||
//takes input from cmd_admin_pm_context, cmd_admin_pm_panel or /client/Topic and sends them a PM.
|
||||
//Fetching a message if needed. src is the sender and C is the target client
|
||||
/client/proc/cmd_admin_pm(whom, msg)
|
||||
if(prefs.muted & MUTE_ADMINHELP)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: You are unable to use admin PM-s (muted).</span>")
|
||||
return
|
||||
|
||||
if(!holder && !current_ticket) //no ticket? https://www.youtube.com/watch?v=iHSPf6x1Fdo
|
||||
to_chat(src, "<span class='danger'>You can no longer reply to this ticket, please open another one by using the Adminhelp verb if need be.</span>")
|
||||
to_chat(src, "<span class='notice'>Message: [msg]</span>")
|
||||
return
|
||||
|
||||
var/client/recipient
|
||||
var/irc = 0
|
||||
if(istext(whom))
|
||||
if(cmptext(copytext(whom,1,2),"@"))
|
||||
whom = findStealthKey(whom)
|
||||
if(whom == "IRCKEY")
|
||||
irc = 1
|
||||
else
|
||||
recipient = GLOB.directory[whom]
|
||||
else if(istype(whom, /client))
|
||||
recipient = whom
|
||||
|
||||
|
||||
if(irc)
|
||||
if(!ircreplyamount) //to prevent people from spamming irc
|
||||
return
|
||||
if(!msg)
|
||||
msg = input(src,"Message:", "Private message to Administrator") as text|null
|
||||
|
||||
if(!msg)
|
||||
return
|
||||
if(holder)
|
||||
to_chat(src, "<span class='danger'>Error: Use the admin IRC channel, nerd.</span>")
|
||||
return
|
||||
|
||||
|
||||
else
|
||||
if(!recipient)
|
||||
if(holder)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: Client not found.</span>")
|
||||
if(msg)
|
||||
to_chat(src, msg)
|
||||
return
|
||||
else if(msg) // you want to continue if there's no message instead of returning now
|
||||
current_ticket.MessageNoRecipient(msg)
|
||||
return
|
||||
|
||||
//get message text, limit it's length.and clean/escape html
|
||||
if(!msg)
|
||||
msg = input(src,"Message:", "Private message to [key_name(recipient, 0, 0)]") as message|null
|
||||
msg = trim(msg)
|
||||
if(!msg)
|
||||
return
|
||||
|
||||
if(prefs.muted & MUTE_ADMINHELP)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: You are unable to use admin PM-s (muted).</span>")
|
||||
return
|
||||
|
||||
if(!recipient)
|
||||
if(holder)
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: Client not found.</span>")
|
||||
else
|
||||
current_ticket.MessageNoRecipient(msg)
|
||||
return
|
||||
|
||||
if (src.handle_spam_prevention(msg,MUTE_ADMINHELP))
|
||||
return
|
||||
|
||||
//clean the message if it's not sent by a high-rank admin
|
||||
if(!check_rights(R_SERVER|R_DEBUG,0)||irc)//no sending html to the poor bots
|
||||
msg = trim(sanitize(copytext(msg,1,MAX_MESSAGE_LEN)))
|
||||
if(!msg)
|
||||
return
|
||||
|
||||
var/rawmsg = msg
|
||||
|
||||
if(holder)
|
||||
msg = emoji_parse(msg)
|
||||
|
||||
var/keywordparsedmsg = keywords_lookup(msg)
|
||||
|
||||
if(irc)
|
||||
to_chat(src, "<span class='notice'>PM to-<b>Admins</b>: <span class='linkify'>[rawmsg]</span></span>")
|
||||
var/datum/admin_help/AH = admin_ticket_log(src, "<font color='red'>Reply PM from-<b>[key_name(src, TRUE, TRUE)] to <i>IRC</i>: [keywordparsedmsg]</font>")
|
||||
ircreplyamount--
|
||||
send2irc("[AH ? "#[AH.id] " : ""]Reply: [ckey]", rawmsg)
|
||||
else
|
||||
if(recipient.holder)
|
||||
if(holder) //both are admins
|
||||
to_chat(recipient, "<span class='danger'>Admin PM from-<b>[key_name(src, recipient, 1)]</b>: <span class='linkify'>[keywordparsedmsg]</span></span>")
|
||||
to_chat(src, "<span class='notice'>Admin PM to-<b>[key_name(recipient, src, 1)]</b>: <span class='linkify'>[keywordparsedmsg]</span></span>")
|
||||
|
||||
//omg this is dumb, just fill in both their tickets
|
||||
var/interaction_message = "<font color='purple'>PM from-<b>[key_name(src, recipient, 1)]</b> to-<b>[key_name(recipient, src, 1)]</b>: [keywordparsedmsg]</font>"
|
||||
admin_ticket_log(src, interaction_message)
|
||||
if(recipient != src) //reeee
|
||||
admin_ticket_log(recipient, interaction_message)
|
||||
|
||||
else //recipient is an admin but sender is not
|
||||
var/replymsg = "Reply PM from-<b>[key_name(src, recipient, 1)]</b>: <span class='linkify'>[keywordparsedmsg]</span>"
|
||||
admin_ticket_log(src, "<font color='red'>[replymsg]</font>")
|
||||
to_chat(recipient, "<span class='danger'>[replymsg]</span>")
|
||||
to_chat(src, "<span class='notice'>PM to-<b>Admins</b>: <span class='linkify'>[msg]</span></span>")
|
||||
|
||||
//play the receiving admin the adminhelp sound (if they have them enabled)
|
||||
if(recipient.prefs.toggles & SOUND_ADMINHELP)
|
||||
SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg'))
|
||||
|
||||
else
|
||||
if(holder) //sender is an admin but recipient is not. Do BIG RED TEXT
|
||||
if(!recipient.current_ticket)
|
||||
new /datum/admin_help(msg, recipient, TRUE)
|
||||
|
||||
to_chat(recipient, "<font color='red' size='4'><b>-- Administrator private message --</b></font>")
|
||||
to_chat(recipient, "<span class='danger'>Admin PM from-<b>[key_name(src, recipient, 0)]</b>: <span class='linkify'>[msg]</span></span>")
|
||||
to_chat(recipient, "<span class='danger'><i>Click on the administrator's name to reply.</i></span>")
|
||||
to_chat(src, "<span class='notice'>Admin PM to-<b>[key_name(recipient, src, 1)]</b>: <span class='linkify'>[msg]</span></span>")
|
||||
|
||||
admin_ticket_log(recipient, "<font color='purple'>PM From [key_name_admin(src)]: [keywordparsedmsg]</font>")
|
||||
|
||||
//always play non-admin recipients the adminhelp sound
|
||||
SEND_SOUND(recipient, sound('sound/effects/adminhelp.ogg'))
|
||||
|
||||
//AdminPM popup for ApocStation and anybody else who wants to use it. Set it with POPUP_ADMIN_PM in config.txt ~Carn
|
||||
if(CONFIG_GET(flag/popup_admin_pm))
|
||||
spawn() //so we don't hold the caller proc up
|
||||
var/sender = src
|
||||
var/sendername = key
|
||||
var/reply = input(recipient, msg,"Admin PM from-[sendername]", "") as text|null //show message and await a reply
|
||||
if(recipient && reply)
|
||||
if(sender)
|
||||
recipient.cmd_admin_pm(sender,reply) //sender is still about, let's reply to them
|
||||
else
|
||||
adminhelp(reply) //sender has left, adminhelp instead
|
||||
return
|
||||
|
||||
else //neither are admins
|
||||
to_chat(src, "<span class='danger'>Error: Admin-PM: Non-admin to non-admin PM communication is forbidden.</span>")
|
||||
return
|
||||
|
||||
if(irc)
|
||||
log_admin_private("PM: [key_name(src)]->IRC: [rawmsg]")
|
||||
for(var/client/X in GLOB.admins)
|
||||
to_chat(X, "<span class='notice'><B>PM: [key_name(src, X, 0)]->IRC:</B> [keywordparsedmsg]</span>")
|
||||
else
|
||||
window_flash(recipient, ignorepref = TRUE)
|
||||
log_admin_private("PM: [key_name(src)]->[key_name(recipient)]: [rawmsg]")
|
||||
//we don't use message_admins here because the sender/receiver might get it too
|
||||
for(var/client/X in GLOB.admins)
|
||||
if(X.key!=key && X.key!=recipient.key) //check client/X is an admin and isn't the sender or recipient
|
||||
to_chat(X, "<span class='notice'><B>PM: [key_name(src, X, 0)]->[key_name(recipient, X, 0)]:</B> [keywordparsedmsg]</span>" )
|
||||
|
||||
|
||||
|
||||
#define IRC_AHELP_USAGE "Usage: ticket <close|resolve|icissue|reject|reopen \[ticket #\]|list>"
|
||||
/proc/IrcPm(target,msg,sender)
|
||||
target = ckey(target)
|
||||
var/client/C = GLOB.directory[target]
|
||||
|
||||
var/datum/admin_help/ticket = C ? C.current_ticket : GLOB.ahelp_tickets.CKey2ActiveTicket(target)
|
||||
var/compliant_msg = trim(lowertext(msg))
|
||||
var/irc_tagged = "[sender](IRC)"
|
||||
var/list/splits = splittext(compliant_msg, " ")
|
||||
if(splits.len && splits[1] == "ticket")
|
||||
if(splits.len < 2)
|
||||
return IRC_AHELP_USAGE
|
||||
switch(splits[2])
|
||||
if("close")
|
||||
if(ticket)
|
||||
ticket.Close(irc_tagged)
|
||||
return "Ticket #[ticket.id] successfully closed"
|
||||
if("resolve")
|
||||
if(ticket)
|
||||
ticket.Resolve(irc_tagged)
|
||||
return "Ticket #[ticket.id] successfully resolved"
|
||||
if("icissue")
|
||||
if(ticket)
|
||||
ticket.ICIssue(irc_tagged)
|
||||
return "Ticket #[ticket.id] successfully marked as IC issue"
|
||||
if("reject")
|
||||
if(ticket)
|
||||
ticket.Reject(irc_tagged)
|
||||
return "Ticket #[ticket.id] successfully rejected"
|
||||
if("reopen")
|
||||
if(ticket)
|
||||
return "Error: [target] already has ticket #[ticket.id] open"
|
||||
var/fail = splits.len < 3 ? null : -1
|
||||
if(!isnull(fail))
|
||||
fail = text2num(splits[3])
|
||||
if(isnull(fail))
|
||||
return "Error: No/Invalid ticket id specified. [IRC_AHELP_USAGE]"
|
||||
var/datum/admin_help/AH = GLOB.ahelp_tickets.TicketByID(fail)
|
||||
if(!AH)
|
||||
return "Error: Ticket #[fail] not found"
|
||||
if(AH.initiator_ckey != target)
|
||||
return "Error: Ticket #[fail] belongs to [AH.initiator_ckey]"
|
||||
AH.Reopen()
|
||||
return "Ticket #[ticket.id] successfully reopened"
|
||||
if("list")
|
||||
var/list/tickets = GLOB.ahelp_tickets.TicketsByCKey(target)
|
||||
if(!tickets.len)
|
||||
return "None"
|
||||
. = ""
|
||||
for(var/I in tickets)
|
||||
var/datum/admin_help/AH = I
|
||||
if(.)
|
||||
. += ", "
|
||||
if(AH == ticket)
|
||||
. += "Active: "
|
||||
. += "#[AH.id]"
|
||||
return
|
||||
else
|
||||
return IRC_AHELP_USAGE
|
||||
return "Error: Ticket could not be found"
|
||||
|
||||
var/static/stealthkey
|
||||
var/adminname = CONFIG_GET(flag/show_irc_name) ? irc_tagged : "Administrator"
|
||||
|
||||
if(!C)
|
||||
return "Error: No client"
|
||||
|
||||
if(!stealthkey)
|
||||
stealthkey = GenIrcStealthKey()
|
||||
|
||||
msg = sanitize(copytext(msg,1,MAX_MESSAGE_LEN))
|
||||
if(!msg)
|
||||
return "Error: No message"
|
||||
|
||||
message_admins("IRC message from [sender] to [key_name_admin(C)] : [msg]")
|
||||
log_admin_private("IRC PM: [sender] -> [key_name(C)] : [msg]")
|
||||
msg = emoji_parse(msg)
|
||||
|
||||
to_chat(C, "<font color='red' size='4'><b>-- Administrator private message --</b></font>")
|
||||
to_chat(C, "<span class='adminsay'>Admin PM from-<b><a href='?priv_msg=[stealthkey]'>[adminname]</A></b>: [msg]</span>")
|
||||
to_chat(C, "<span class='adminsay'><i>Click on the administrator's name to reply.</i></span>")
|
||||
|
||||
admin_ticket_log(C, "<font color='purple'>PM From [irc_tagged]: [msg]</font>")
|
||||
|
||||
window_flash(C, ignorepref = TRUE)
|
||||
//always play non-admin recipients the adminhelp sound
|
||||
SEND_SOUND(C, 'sound/effects/adminhelp.ogg')
|
||||
|
||||
C.ircreplyamount = IRCREPLYCOUNT
|
||||
|
||||
return "Message Successful"
|
||||
|
||||
/proc/GenIrcStealthKey()
|
||||
var/num = (rand(0,1000))
|
||||
var/i = 0
|
||||
while(i == 0)
|
||||
i = 1
|
||||
for(var/P in GLOB.stealthminID)
|
||||
if(num == GLOB.stealthminID[P])
|
||||
num++
|
||||
i = 0
|
||||
var/stealth = "@[num2text(num)]"
|
||||
GLOB.stealthminID["IRCKEY"] = stealth
|
||||
return stealth
|
||||
|
||||
#undef IRCREPLYCOUNT
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
/client/proc/cmd_admin_say(msg as text)
|
||||
set category = "Special Verbs"
|
||||
set name = "Asay" //Gave this shit a shorter name so you only have to time out "asay" rather than "admin say" to use it --NeoFite
|
||||
set hidden = 1
|
||||
if(!check_rights(0))
|
||||
return
|
||||
|
||||
msg = copytext(sanitize(msg), 1, MAX_MESSAGE_LEN)
|
||||
if(!msg)
|
||||
return
|
||||
msg = emoji_parse(msg)
|
||||
mob.log_talk(msg, LOG_ASAY)
|
||||
|
||||
msg = keywords_lookup(msg)
|
||||
msg = "<span class='adminsay'><span class='prefix'>ADMIN:</span> <EM>[key_name(usr, 1)]</EM> [ADMIN_FLW(mob)]: <span class='message linkify'>[msg]</span></span>"
|
||||
to_chat(GLOB.admins, msg)
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Asay") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/get_admin_say()
|
||||
var/msg = input(src, null, "asay \"text\"") as text
|
||||
cmd_admin_say(msg)
|
||||
/client/proc/cmd_admin_say(msg as text)
|
||||
set category = "Special Verbs"
|
||||
set name = "Asay" //Gave this shit a shorter name so you only have to time out "asay" rather than "admin say" to use it --NeoFite
|
||||
set hidden = 1
|
||||
if(!check_rights(0))
|
||||
return
|
||||
|
||||
msg = copytext(sanitize(msg), 1, MAX_MESSAGE_LEN)
|
||||
if(!msg)
|
||||
return
|
||||
msg = emoji_parse(msg)
|
||||
mob.log_talk(msg, LOG_ASAY)
|
||||
|
||||
msg = keywords_lookup(msg)
|
||||
msg = "<span class='adminsay'><span class='prefix'>ADMIN:</span> <EM>[key_name(usr, 1)]</EM> [ADMIN_FLW(mob)]: <span class='message linkify'>[msg]</span></span>"
|
||||
to_chat(GLOB.admins, msg)
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Asay") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/get_admin_say()
|
||||
var/msg = input(src, null, "asay \"text\"") as text
|
||||
cmd_admin_say(msg)
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
/client/proc/atmosscan()
|
||||
set category = "Mapping"
|
||||
set name = "Check Plumbing"
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Check Plumbing") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
//all plumbing - yes, some things might get stated twice, doesn't matter.
|
||||
for (var/obj/machinery/atmospherics/plumbing in GLOB.machines)
|
||||
if (plumbing.nodealert)
|
||||
to_chat(usr, "Unconnected [plumbing.name] located at [ADMIN_VERBOSEJMP(plumbing)]")
|
||||
|
||||
//Manifolds
|
||||
for (var/obj/machinery/atmospherics/pipe/manifold/pipe in GLOB.machines)
|
||||
if (!pipe.nodes[1] || !pipe.nodes[2] || !pipe.nodes[3])
|
||||
to_chat(usr, "Unconnected [pipe.name] located at [ADMIN_VERBOSEJMP(pipe)]")
|
||||
|
||||
//Pipes
|
||||
for (var/obj/machinery/atmospherics/pipe/simple/pipe in GLOB.machines)
|
||||
if (!pipe.nodes[1] || !pipe.nodes[2])
|
||||
to_chat(usr, "Unconnected [pipe.name] located at [ADMIN_VERBOSEJMP(pipe)]")
|
||||
|
||||
/client/proc/powerdebug()
|
||||
set category = "Mapping"
|
||||
set name = "Check Power"
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Check Power") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
for (var/datum/powernet/PN in GLOB.powernets)
|
||||
if (!PN.nodes || !PN.nodes.len)
|
||||
if(PN.cables && (PN.cables.len > 1))
|
||||
var/obj/structure/cable/C = PN.cables[1]
|
||||
to_chat(usr, "Powernet with no nodes! (number [PN.number]) - example cable at [ADMIN_VERBOSEJMP(C)]")
|
||||
|
||||
if (!PN.cables || (PN.cables.len < 10))
|
||||
if(PN.cables && (PN.cables.len > 1))
|
||||
var/obj/structure/cable/C = PN.cables[1]
|
||||
to_chat(usr, "Powernet with fewer than 10 cables! (number [PN.number]) - example cable at [ADMIN_VERBOSEJMP(C)]")
|
||||
/client/proc/atmosscan()
|
||||
set category = "Mapping"
|
||||
set name = "Check Plumbing"
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Check Plumbing") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
//all plumbing - yes, some things might get stated twice, doesn't matter.
|
||||
for (var/obj/machinery/atmospherics/plumbing in GLOB.machines)
|
||||
if (plumbing.nodealert)
|
||||
to_chat(usr, "Unconnected [plumbing.name] located at [ADMIN_VERBOSEJMP(plumbing)]")
|
||||
|
||||
//Manifolds
|
||||
for (var/obj/machinery/atmospherics/pipe/manifold/pipe in GLOB.machines)
|
||||
if (!pipe.nodes[1] || !pipe.nodes[2] || !pipe.nodes[3])
|
||||
to_chat(usr, "Unconnected [pipe.name] located at [ADMIN_VERBOSEJMP(pipe)]")
|
||||
|
||||
//Pipes
|
||||
for (var/obj/machinery/atmospherics/pipe/simple/pipe in GLOB.machines)
|
||||
if (!pipe.nodes[1] || !pipe.nodes[2])
|
||||
to_chat(usr, "Unconnected [pipe.name] located at [ADMIN_VERBOSEJMP(pipe)]")
|
||||
|
||||
/client/proc/powerdebug()
|
||||
set category = "Mapping"
|
||||
set name = "Check Power"
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Check Power") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
for (var/datum/powernet/PN in GLOB.powernets)
|
||||
if (!PN.nodes || !PN.nodes.len)
|
||||
if(PN.cables && (PN.cables.len > 1))
|
||||
var/obj/structure/cable/C = PN.cables[1]
|
||||
to_chat(usr, "Powernet with no nodes! (number [PN.number]) - example cable at [ADMIN_VERBOSEJMP(C)]")
|
||||
|
||||
if (!PN.cables || (PN.cables.len < 10))
|
||||
if(PN.cables && (PN.cables.len > 1))
|
||||
var/obj/structure/cable/C = PN.cables[1]
|
||||
to_chat(usr, "Powernet with fewer than 10 cables! (number [PN.number]) - example cable at [ADMIN_VERBOSEJMP(C)]")
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/client/proc/cinematic()
|
||||
set name = "cinematic"
|
||||
set category = "Fun"
|
||||
set desc = "Shows a cinematic." // Intended for testing but I thought it might be nice for events on the rare occasion Feel free to comment it out if it's not wanted.
|
||||
set hidden = 1
|
||||
if(!SSticker)
|
||||
return
|
||||
|
||||
var/datum/cinematic/choice = input(src,"Cinematic","Choose",null) as anything in subtypesof(/datum/cinematic)
|
||||
if(choice)
|
||||
/client/proc/cinematic()
|
||||
set name = "cinematic"
|
||||
set category = "Fun"
|
||||
set desc = "Shows a cinematic." // Intended for testing but I thought it might be nice for events on the rare occasion Feel free to comment it out if it's not wanted.
|
||||
set hidden = 1
|
||||
if(!SSticker)
|
||||
return
|
||||
|
||||
var/datum/cinematic/choice = input(src,"Cinematic","Choose",null) as anything in subtypesof(/datum/cinematic)
|
||||
if(choice)
|
||||
Cinematic(initial(choice.id),world,null)
|
||||
@@ -1,36 +1,36 @@
|
||||
/client/proc/dsay(msg as text)
|
||||
set category = "Special Verbs"
|
||||
set name = "Dsay"
|
||||
set hidden = 1
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
if(!src.mob)
|
||||
return
|
||||
if(prefs.muted & MUTE_DEADCHAT)
|
||||
to_chat(src, "<span class='danger'>You cannot send DSAY messages (muted).</span>")
|
||||
return
|
||||
|
||||
if (src.handle_spam_prevention(msg,MUTE_DEADCHAT))
|
||||
return
|
||||
|
||||
msg = copytext(sanitize(msg), 1, MAX_MESSAGE_LEN)
|
||||
mob.log_talk(msg, LOG_DSAY)
|
||||
|
||||
if (!msg)
|
||||
return
|
||||
var/static/nicknames = world.file2list("[global.config.directory]/admin_nicknames.txt")
|
||||
|
||||
var/rendered = "<span class='game deadsay'><span class='prefix'>DEAD:</span> <span class='name'>[uppertext(holder.rank)]([src.holder.fakekey ? pick(nicknames) : src.key])</span> says, <span class='message'>\"[emoji_parse(msg)]\"</span></span>"
|
||||
|
||||
for (var/mob/M in GLOB.player_list)
|
||||
if(isnewplayer(M))
|
||||
continue
|
||||
if (M.stat == DEAD || (M.client && M.client.holder && (M.client.prefs.chat_toggles & CHAT_DEAD))) //admins can toggle deadchat on and off. This is a proc in admin.dm and is only give to Administrators and above
|
||||
to_chat(M, rendered)
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Dsay") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/get_dead_say()
|
||||
var/msg = input(src, null, "dsay \"text\"") as text
|
||||
dsay(msg)
|
||||
/client/proc/dsay(msg as text)
|
||||
set category = "Special Verbs"
|
||||
set name = "Dsay"
|
||||
set hidden = 1
|
||||
if(!src.holder)
|
||||
to_chat(src, "Only administrators may use this command.")
|
||||
return
|
||||
if(!src.mob)
|
||||
return
|
||||
if(prefs.muted & MUTE_DEADCHAT)
|
||||
to_chat(src, "<span class='danger'>You cannot send DSAY messages (muted).</span>")
|
||||
return
|
||||
|
||||
if (src.handle_spam_prevention(msg,MUTE_DEADCHAT))
|
||||
return
|
||||
|
||||
msg = copytext(sanitize(msg), 1, MAX_MESSAGE_LEN)
|
||||
mob.log_talk(msg, LOG_DSAY)
|
||||
|
||||
if (!msg)
|
||||
return
|
||||
var/static/nicknames = world.file2list("[global.config.directory]/admin_nicknames.txt")
|
||||
|
||||
var/rendered = "<span class='game deadsay'><span class='prefix'>DEAD:</span> <span class='name'>[uppertext(holder.rank)]([src.holder.fakekey ? pick(nicknames) : src.key])</span> says, <span class='message'>\"[emoji_parse(msg)]\"</span></span>"
|
||||
|
||||
for (var/mob/M in GLOB.player_list)
|
||||
if(isnewplayer(M))
|
||||
continue
|
||||
if (M.stat == DEAD || (M.client && M.client.holder && (M.client.prefs.chat_toggles & CHAT_DEAD))) //admins can toggle deadchat on and off. This is a proc in admin.dm and is only give to Administrators and above
|
||||
to_chat(M, rendered)
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Dsay") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/get_dead_say()
|
||||
var/msg = input(src, null, "dsay \"text\"") as text
|
||||
dsay(msg)
|
||||
|
||||
@@ -492,7 +492,7 @@ GLOBAL_PROTECT(AdminProcCallSpamPrevention)
|
||||
message_admins("<span class='adminnotice'>[key_name_admin(usr)] assumed direct control of [M].</span>")
|
||||
log_admin("[key_name(usr)] assumed direct control of [M].")
|
||||
var/mob/adminmob = src.mob
|
||||
M.ckey = src.ckey
|
||||
adminmob.transfer_ckey(M, send_signal = FALSE)
|
||||
if( isobserver(adminmob) )
|
||||
qdel(adminmob)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Assume Direct Control") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
//replaces the old Ticklag verb, fps is easier to understand
|
||||
/client/proc/set_server_fps()
|
||||
set category = "Debug"
|
||||
set name = "Set Server FPS"
|
||||
set desc = "Sets game speed in frames-per-second. Can potentially break the game"
|
||||
|
||||
if(!check_rights(R_DEBUG))
|
||||
return
|
||||
|
||||
var/cfg_fps = CONFIG_GET(number/fps)
|
||||
var/new_fps = round(input("Sets game frames-per-second. Can potentially break the game (default: [cfg_fps])","FPS", world.fps) as num|null)
|
||||
|
||||
if(new_fps <= 0)
|
||||
to_chat(src, "<span class='danger'>Error: set_server_fps(): Invalid world.fps value. No changes made.</span>")
|
||||
return
|
||||
if(new_fps > cfg_fps * 1.5)
|
||||
if(alert(src, "You are setting fps to a high value:\n\t[new_fps] frames-per-second\n\tconfig.fps = [cfg_fps]","Warning!","Confirm","ABORT-ABORT-ABORT") != "Confirm")
|
||||
return
|
||||
|
||||
var/msg = "[key_name(src)] has modified world.fps to [new_fps]"
|
||||
log_admin(msg, 0)
|
||||
message_admins(msg, 0)
|
||||
SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Set Server FPS", "[new_fps]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
CONFIG_SET(number/fps, new_fps)
|
||||
world.fps = new_fps
|
||||
//replaces the old Ticklag verb, fps is easier to understand
|
||||
/client/proc/set_server_fps()
|
||||
set category = "Debug"
|
||||
set name = "Set Server FPS"
|
||||
set desc = "Sets game speed in frames-per-second. Can potentially break the game"
|
||||
|
||||
if(!check_rights(R_DEBUG))
|
||||
return
|
||||
|
||||
var/cfg_fps = CONFIG_GET(number/fps)
|
||||
var/new_fps = round(input("Sets game frames-per-second. Can potentially break the game (default: [cfg_fps])","FPS", world.fps) as num|null)
|
||||
|
||||
if(new_fps <= 0)
|
||||
to_chat(src, "<span class='danger'>Error: set_server_fps(): Invalid world.fps value. No changes made.</span>")
|
||||
return
|
||||
if(new_fps > cfg_fps * 1.5)
|
||||
if(alert(src, "You are setting fps to a high value:\n\t[new_fps] frames-per-second\n\tconfig.fps = [cfg_fps]","Warning!","Confirm","ABORT-ABORT-ABORT") != "Confirm")
|
||||
return
|
||||
|
||||
var/msg = "[key_name(src)] has modified world.fps to [new_fps]"
|
||||
log_admin(msg, 0)
|
||||
message_admins(msg, 0)
|
||||
SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Set Server FPS", "[new_fps]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
CONFIG_SET(number/fps, new_fps)
|
||||
world.fps = new_fps
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
//This proc allows download of past server logs saved within the data/logs/ folder.
|
||||
/client/proc/getserverlogs()
|
||||
set name = "Get Server Logs"
|
||||
set desc = "View/retrieve logfiles."
|
||||
set category = "Admin"
|
||||
|
||||
browseserverlogs()
|
||||
|
||||
/client/proc/getcurrentlogs()
|
||||
set name = "Get Current Logs"
|
||||
set desc = "View/retrieve logfiles for the current round."
|
||||
set category = "Admin"
|
||||
|
||||
browseserverlogs("[GLOB.log_directory]/")
|
||||
|
||||
/client/proc/browseserverlogs(path = "data/logs/")
|
||||
path = browse_files(path)
|
||||
if(!path)
|
||||
return
|
||||
|
||||
if(file_spam_check())
|
||||
return
|
||||
|
||||
message_admins("[key_name_admin(src)] accessed file: [path]")
|
||||
switch(alert("View (in game), Open (in your system's text editor), or Download?", path, "View", "Open", "Download"))
|
||||
if ("View")
|
||||
src << browse("<pre style='word-wrap: break-word;'>[html_encode(file2text(file(path)))]</pre>", list2params(list("window" = "viewfile.[path]")))
|
||||
if ("Open")
|
||||
src << run(file(path))
|
||||
if ("Download")
|
||||
src << ftp(file(path))
|
||||
else
|
||||
return
|
||||
to_chat(src, "Attempting to send [path], this may take a fair few minutes if the file is very large.")
|
||||
//This proc allows download of past server logs saved within the data/logs/ folder.
|
||||
/client/proc/getserverlogs()
|
||||
set name = "Get Server Logs"
|
||||
set desc = "View/retrieve logfiles."
|
||||
set category = "Admin"
|
||||
|
||||
browseserverlogs()
|
||||
|
||||
/client/proc/getcurrentlogs()
|
||||
set name = "Get Current Logs"
|
||||
set desc = "View/retrieve logfiles for the current round."
|
||||
set category = "Admin"
|
||||
|
||||
browseserverlogs("[GLOB.log_directory]/")
|
||||
|
||||
/client/proc/browseserverlogs(path = "data/logs/")
|
||||
path = browse_files(path)
|
||||
if(!path)
|
||||
return
|
||||
|
||||
if(file_spam_check())
|
||||
return
|
||||
|
||||
message_admins("[key_name_admin(src)] accessed file: [path]")
|
||||
switch(alert("View (in game), Open (in your system's text editor), or Download?", path, "View", "Open", "Download"))
|
||||
if ("View")
|
||||
src << browse("<pre style='word-wrap: break-word;'>[html_encode(file2text(file(path)))]</pre>", list2params(list("window" = "viewfile.[path]")))
|
||||
if ("Open")
|
||||
src << run(file(path))
|
||||
if ("Download")
|
||||
src << ftp(file(path))
|
||||
else
|
||||
return
|
||||
to_chat(src, "Attempting to send [path], this may take a fair few minutes if the file is very large.")
|
||||
return
|
||||
+377
-358
@@ -1,358 +1,377 @@
|
||||
//- Are all the floors with or without air, as they should be? (regular or airless)
|
||||
//- Does the area have an APC?
|
||||
//- Does the area have an Air Alarm?
|
||||
//- Does the area have a Request Console?
|
||||
//- Does the area have lights?
|
||||
//- Does the area have a light switch?
|
||||
//- Does the area have enough intercoms?
|
||||
//- Does the area have enough security cameras? (Use the 'Camera Range Display' verb under Debug)
|
||||
//- Is the area connected to the scrubbers air loop?
|
||||
//- Is the area connected to the vent air loop? (vent pumps)
|
||||
//- Is everything wired properly?
|
||||
//- Does the area have a fire alarm and firedoors?
|
||||
//- Do all pod doors work properly?
|
||||
//- Are accesses set properly on doors, pod buttons, etc.
|
||||
//- Are all items placed properly? (not below vents, scrubbers, tables)
|
||||
//- Does the disposal system work properly from all the disposal units in this room and all the units, the pipes of which pass through this room?
|
||||
//- Check for any misplaced or stacked piece of pipe (air and disposal)
|
||||
//- Check for any misplaced or stacked piece of wire
|
||||
//- Identify how hard it is to break into the area and where the weak points are
|
||||
//- Check if the area has too much empty space. If so, make it smaller and replace the rest with maintenance tunnels.
|
||||
|
||||
GLOBAL_LIST_INIT(admin_verbs_debug_mapping, list(
|
||||
/client/proc/camera_view, //-errorage
|
||||
/client/proc/sec_camera_report, //-errorage
|
||||
/client/proc/intercom_view, //-errorage
|
||||
/client/proc/air_status, //Air things
|
||||
/client/proc/Cell, //More air things
|
||||
/client/proc/atmosscan, //check plumbing
|
||||
/client/proc/powerdebug, //check power
|
||||
/client/proc/count_objects_on_z_level,
|
||||
/client/proc/count_objects_all,
|
||||
/client/proc/cmd_assume_direct_control, //-errorage
|
||||
/client/proc/startSinglo,
|
||||
/client/proc/set_server_fps, //allows you to set the ticklag.
|
||||
/client/proc/cmd_admin_grantfullaccess,
|
||||
/client/proc/cmd_admin_areatest_all,
|
||||
/client/proc/cmd_admin_areatest_station,
|
||||
/client/proc/cmd_admin_test_atmos_controllers,
|
||||
/client/proc/cmd_admin_rejuvenate,
|
||||
/datum/admins/proc/show_traitor_panel,
|
||||
/client/proc/disable_communication,
|
||||
/client/proc/cmd_show_at_list,
|
||||
/client/proc/cmd_show_at_markers,
|
||||
/client/proc/manipulate_organs,
|
||||
/client/proc/start_line_profiling,
|
||||
/client/proc/stop_line_profiling,
|
||||
/client/proc/show_line_profiling,
|
||||
/client/proc/create_mapping_job_icons,
|
||||
/client/proc/debug_z_levels,
|
||||
/client/proc/place_ruin
|
||||
))
|
||||
GLOBAL_PROTECT(admin_verbs_debug_mapping)
|
||||
|
||||
/obj/effect/debugging/mapfix_marker
|
||||
name = "map fix marker"
|
||||
icon = 'icons/mob/screen_gen.dmi'
|
||||
icon_state = "mapfixmarker"
|
||||
desc = "I am a mappers mistake."
|
||||
|
||||
/obj/effect/debugging/marker
|
||||
icon = 'icons/turf/areas.dmi'
|
||||
icon_state = "yellow"
|
||||
|
||||
/obj/effect/debugging/marker/Move()
|
||||
return 0
|
||||
|
||||
/client/proc/camera_view()
|
||||
set category = "Mapping"
|
||||
set name = "Camera Range Display"
|
||||
|
||||
var/on = FALSE
|
||||
for(var/turf/T in world)
|
||||
if(T.maptext)
|
||||
on = TRUE
|
||||
T.maptext = null
|
||||
|
||||
if(!on)
|
||||
var/list/seen = list()
|
||||
for(var/obj/machinery/camera/C in GLOB.cameranet.cameras)
|
||||
for(var/turf/T in C.can_see())
|
||||
seen[T]++
|
||||
for(var/turf/T in seen)
|
||||
T.maptext = "[seen[T]]"
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Camera Range") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Camera Range")
|
||||
|
||||
|
||||
|
||||
/client/proc/sec_camera_report()
|
||||
set category = "Mapping"
|
||||
set name = "Camera Report"
|
||||
|
||||
if(!Master)
|
||||
alert(usr,"Master_controller not found.","Sec Camera Report")
|
||||
return 0
|
||||
|
||||
var/list/obj/machinery/camera/CL = list()
|
||||
|
||||
for(var/obj/machinery/camera/C in GLOB.cameranet.cameras)
|
||||
CL += C
|
||||
|
||||
var/output = {"<B>Camera Abnormalities Report</B><HR>
|
||||
<B>The following abnormalities have been detected. The ones in red need immediate attention: Some of those in black may be intentional.</B><BR><ul>"}
|
||||
|
||||
for(var/obj/machinery/camera/C1 in CL)
|
||||
for(var/obj/machinery/camera/C2 in CL)
|
||||
if(C1 != C2)
|
||||
if(C1.c_tag == C2.c_tag)
|
||||
output += "<li><font color='red'>c_tag match for cameras at [ADMIN_VERBOSEJMP(C1)] and [ADMIN_VERBOSEJMP(C2)] - c_tag is [C1.c_tag]</font></li>"
|
||||
if(C1.loc == C2.loc && C1.dir == C2.dir && C1.pixel_x == C2.pixel_x && C1.pixel_y == C2.pixel_y)
|
||||
output += "<li><font color='red'>FULLY overlapping cameras at [ADMIN_VERBOSEJMP(C1)] Networks: [json_encode(C1.network)] and [json_encode(C2.network)]</font></li>"
|
||||
if(C1.loc == C2.loc)
|
||||
output += "<li>Overlapping cameras at [ADMIN_VERBOSEJMP(C1)] Networks: [json_encode(C1.network)] and [json_encode(C2.network)]</li>"
|
||||
var/turf/T = get_step(C1,turn(C1.dir,180))
|
||||
if(!T || !isturf(T) || !T.density )
|
||||
if(!(locate(/obj/structure/grille) in T))
|
||||
var/window_check = 0
|
||||
for(var/obj/structure/window/W in T)
|
||||
if (W.dir == turn(C1.dir,180) || W.dir in list(5,6,9,10) )
|
||||
window_check = 1
|
||||
break
|
||||
if(!window_check)
|
||||
output += "<li><font color='red'>Camera not connected to wall at [ADMIN_VERBOSEJMP(C1)] Network: [json_encode(C1.network)]</font></li>"
|
||||
|
||||
output += "</ul>"
|
||||
usr << browse(output,"window=airreport;size=1000x500")
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Camera Report") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/intercom_view()
|
||||
set category = "Mapping"
|
||||
set name = "Intercom Range Display"
|
||||
|
||||
var/static/intercom_range_display_status = FALSE
|
||||
intercom_range_display_status = !intercom_range_display_status //blame cyberboss if this breaks something
|
||||
|
||||
for(var/obj/effect/debugging/marker/M in world)
|
||||
qdel(M)
|
||||
|
||||
if(intercom_range_display_status)
|
||||
for(var/obj/item/radio/intercom/I in world)
|
||||
for(var/turf/T in orange(7,I))
|
||||
var/obj/effect/debugging/marker/F = new/obj/effect/debugging/marker(T)
|
||||
if (!(F in view(7,I.loc)))
|
||||
qdel(F)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Intercom Range") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/cmd_show_at_list()
|
||||
set category = "Mapping"
|
||||
set name = "Show roundstart AT list"
|
||||
set desc = "Displays a list of active turfs coordinates at roundstart"
|
||||
|
||||
var/dat = {"<b>Coordinate list of Active Turfs at Roundstart</b>
|
||||
<br>Real-time Active Turfs list you can see in Air Subsystem at active_turfs var<br>"}
|
||||
|
||||
for(var/t in GLOB.active_turfs_startlist)
|
||||
var/turf/T = t
|
||||
dat += "[ADMIN_VERBOSEJMP(T)]\n"
|
||||
dat += "<br>"
|
||||
|
||||
usr << browse(dat, "window=at_list")
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Roundstart Active Turfs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/cmd_show_at_markers()
|
||||
set category = "Mapping"
|
||||
set name = "Show roundstart AT markers"
|
||||
set desc = "Places a marker on all active-at-roundstart turfs"
|
||||
|
||||
var/count = 0
|
||||
for(var/obj/effect/abstract/marker/at/AT in GLOB.all_abstract_markers)
|
||||
qdel(AT)
|
||||
count++
|
||||
|
||||
if(count)
|
||||
to_chat(usr, "[count] AT markers removed.")
|
||||
else
|
||||
for(var/t in GLOB.active_turfs_startlist)
|
||||
new /obj/effect/abstract/marker/at(t)
|
||||
count++
|
||||
to_chat(usr, "[count] AT markers placed.")
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Roundstart Active Turf Markers")
|
||||
|
||||
/client/proc/enable_debug_verbs()
|
||||
set category = "Debug"
|
||||
set name = "Debug verbs - Enable"
|
||||
if(!check_rights(R_DEBUG))
|
||||
return
|
||||
verbs -= /client/proc/enable_debug_verbs
|
||||
verbs.Add(/client/proc/disable_debug_verbs, GLOB.admin_verbs_debug_mapping)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Enable Debug Verbs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/disable_debug_verbs()
|
||||
set category = "Debug"
|
||||
set name = "Debug verbs - Disable"
|
||||
verbs.Remove(/client/proc/disable_debug_verbs, GLOB.admin_verbs_debug_mapping)
|
||||
verbs += /client/proc/enable_debug_verbs
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Disable Debug Verbs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/count_objects_on_z_level()
|
||||
set category = "Mapping"
|
||||
set name = "Count Objects On Level"
|
||||
var/level = input("Which z-level?","Level?") as text
|
||||
if(!level)
|
||||
return
|
||||
var/num_level = text2num(level)
|
||||
if(!num_level)
|
||||
return
|
||||
if(!isnum(num_level))
|
||||
return
|
||||
|
||||
var/type_text = input("Which type path?","Path?") as text
|
||||
if(!type_text)
|
||||
return
|
||||
var/type_path = text2path(type_text)
|
||||
if(!type_path)
|
||||
return
|
||||
|
||||
var/count = 0
|
||||
|
||||
var/list/atom/atom_list = list()
|
||||
|
||||
for(var/atom/A in world)
|
||||
if(istype(A,type_path))
|
||||
var/atom/B = A
|
||||
while(!(isturf(B.loc)))
|
||||
if(B && B.loc)
|
||||
B = B.loc
|
||||
else
|
||||
break
|
||||
if(B)
|
||||
if(B.z == num_level)
|
||||
count++
|
||||
atom_list += A
|
||||
|
||||
to_chat(world, "There are [count] objects of type [type_path] on z-level [num_level]")
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Count Objects Zlevel") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/count_objects_all()
|
||||
set category = "Mapping"
|
||||
set name = "Count Objects All"
|
||||
|
||||
var/type_text = input("Which type path?","") as text
|
||||
if(!type_text)
|
||||
return
|
||||
var/type_path = text2path(type_text)
|
||||
if(!type_path)
|
||||
return
|
||||
|
||||
var/count = 0
|
||||
|
||||
for(var/atom/A in world)
|
||||
if(istype(A,type_path))
|
||||
count++
|
||||
|
||||
to_chat(world, "There are [count] objects of type [type_path] in the game world")
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Count Objects All") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
|
||||
//This proc is intended to detect lag problems relating to communication procs
|
||||
GLOBAL_VAR_INIT(say_disabled, FALSE)
|
||||
/client/proc/disable_communication()
|
||||
set category = "Mapping"
|
||||
set name = "Disable all communication verbs"
|
||||
|
||||
GLOB.say_disabled = !GLOB.say_disabled
|
||||
if(GLOB.say_disabled)
|
||||
message_admins("[key] used 'Disable all communication verbs', killing all communication methods.")
|
||||
else
|
||||
message_admins("[key] used 'Disable all communication verbs', restoring all communication methods.")
|
||||
|
||||
//This generates the icon states for job starting location landmarks.
|
||||
/client/proc/create_mapping_job_icons()
|
||||
set name = "Generate job landmarks icons"
|
||||
set category = "Mapping"
|
||||
var/icon/final = icon()
|
||||
var/mob/living/carbon/human/dummy/D = new(locate(1,1,1)) //spawn on 1,1,1 so we don't have runtimes when items are deleted
|
||||
D.setDir(SOUTH)
|
||||
for(var/job in subtypesof(/datum/job))
|
||||
var/datum/job/JB = new job
|
||||
switch(JB.title)
|
||||
if("AI")
|
||||
final.Insert(icon('icons/mob/ai.dmi', "ai", SOUTH, 1), "AI")
|
||||
if("Cyborg")
|
||||
final.Insert(icon('icons/mob/robots.dmi', "robot", SOUTH, 1), "Cyborg")
|
||||
else
|
||||
for(var/obj/item/I in D)
|
||||
qdel(I)
|
||||
randomize_human(D)
|
||||
JB.equip(D, TRUE, FALSE)
|
||||
COMPILE_OVERLAYS(D)
|
||||
var/icon/I = icon(getFlatIcon(D), frame = 1)
|
||||
final.Insert(I, JB.title)
|
||||
qdel(D)
|
||||
//Also add the x
|
||||
for(var/x_number in 1 to 4)
|
||||
final.Insert(icon('icons/mob/screen_gen.dmi', "x[x_number == 1 ? "" : x_number]"), "x[x_number == 1 ? "" : x_number]")
|
||||
fcopy(final, "icons/mob/landmarks.dmi")
|
||||
|
||||
/client/proc/debug_z_levels()
|
||||
set name = "Debug Z-Levels"
|
||||
set category = "Mapping"
|
||||
|
||||
var/list/z_list = SSmapping.z_list
|
||||
var/list/messages = list()
|
||||
messages += "<b>World</b>: [world.maxx] x [world.maxy] x [world.maxz]<br>"
|
||||
|
||||
var/list/linked_levels = list()
|
||||
var/min_x = INFINITY
|
||||
var/min_y = INFINITY
|
||||
var/max_x = -INFINITY
|
||||
var/max_y = -INFINITY
|
||||
|
||||
for(var/z in 1 to max(world.maxz, z_list.len))
|
||||
if (z > z_list.len)
|
||||
messages += "<b>[z]</b>: Unmanaged (out of bounds)<br>"
|
||||
continue
|
||||
var/datum/space_level/S = z_list[z]
|
||||
if (!S)
|
||||
messages += "<b>[z]</b>: Unmanaged (null)<br>"
|
||||
continue
|
||||
var/linkage
|
||||
switch (S.linkage)
|
||||
if (UNAFFECTED)
|
||||
linkage = "no linkage"
|
||||
if (SELFLOOPING)
|
||||
linkage = "self-looping"
|
||||
if (CROSSLINKED)
|
||||
linkage = "linked at ([S.xi], [S.yi])"
|
||||
linked_levels += S
|
||||
min_x = min(min_x, S.xi)
|
||||
min_y = min(min_y, S.yi)
|
||||
max_x = max(max_x, S.xi)
|
||||
max_y = max(max_y, S.yi)
|
||||
else
|
||||
linkage = "unknown linkage '[S.linkage]'"
|
||||
|
||||
messages += "<b>[z]</b>: [S.name], [linkage], traits: [json_encode(S.traits)]<br>"
|
||||
if (S.z_value != z)
|
||||
messages += "-- z_value is [S.z_value], should be [z]<br>"
|
||||
if (S.name == initial(S.name))
|
||||
messages += "-- name not set<br>"
|
||||
if (z > world.maxz)
|
||||
messages += "-- exceeds max z"
|
||||
|
||||
var/grid[max_x - min_x + 1][max_y - min_y + 1]
|
||||
for(var/datum/space_level/S in linked_levels)
|
||||
grid[S.xi - min_x + 1][S.yi - min_y + 1] = S.z_value
|
||||
|
||||
messages += "<table border='1'>"
|
||||
for(var/y in max_y to min_y step -1)
|
||||
var/list/part = list()
|
||||
for(var/x in min_x to max_x)
|
||||
part += "[grid[x - min_x + 1][y - min_y + 1]]"
|
||||
messages += "<tr><td>[part.Join("</td><td>")]</td></tr>"
|
||||
messages += "</table>"
|
||||
|
||||
to_chat(src, messages.Join(""))
|
||||
//- Are all the floors with or without air, as they should be? (regular or airless)
|
||||
//- Does the area have an APC?
|
||||
//- Does the area have an Air Alarm?
|
||||
//- Does the area have a Request Console?
|
||||
//- Does the area have lights?
|
||||
//- Does the area have a light switch?
|
||||
//- Does the area have enough intercoms?
|
||||
//- Does the area have enough security cameras? (Use the 'Camera Range Display' verb under Debug)
|
||||
//- Is the area connected to the scrubbers air loop?
|
||||
//- Is the area connected to the vent air loop? (vent pumps)
|
||||
//- Is everything wired properly?
|
||||
//- Does the area have a fire alarm and firedoors?
|
||||
//- Do all pod doors work properly?
|
||||
//- Are accesses set properly on doors, pod buttons, etc.
|
||||
//- Are all items placed properly? (not below vents, scrubbers, tables)
|
||||
//- Does the disposal system work properly from all the disposal units in this room and all the units, the pipes of which pass through this room?
|
||||
//- Check for any misplaced or stacked piece of pipe (air and disposal)
|
||||
//- Check for any misplaced or stacked piece of wire
|
||||
//- Identify how hard it is to break into the area and where the weak points are
|
||||
//- Check if the area has too much empty space. If so, make it smaller and replace the rest with maintenance tunnels.
|
||||
|
||||
GLOBAL_LIST_INIT(admin_verbs_debug_mapping, list(
|
||||
/client/proc/camera_view, //-errorage
|
||||
/client/proc/sec_camera_report, //-errorage
|
||||
/client/proc/intercom_view, //-errorage
|
||||
/client/proc/air_status, //Air things
|
||||
/client/proc/Cell, //More air things
|
||||
/client/proc/atmosscan, //check plumbing
|
||||
/client/proc/powerdebug, //check power
|
||||
/client/proc/count_objects_on_z_level,
|
||||
/client/proc/count_objects_all,
|
||||
/client/proc/cmd_assume_direct_control, //-errorage
|
||||
/client/proc/startSinglo,
|
||||
/client/proc/set_server_fps, //allows you to set the ticklag.
|
||||
/client/proc/cmd_admin_grantfullaccess,
|
||||
/client/proc/cmd_admin_areatest_all,
|
||||
/client/proc/cmd_admin_areatest_station,
|
||||
#ifdef TESTING
|
||||
/client/proc/see_dirty_varedits,
|
||||
#endif
|
||||
/client/proc/cmd_admin_test_atmos_controllers,
|
||||
/client/proc/cmd_admin_rejuvenate,
|
||||
/datum/admins/proc/show_traitor_panel,
|
||||
/client/proc/disable_communication,
|
||||
/client/proc/cmd_show_at_list,
|
||||
/client/proc/cmd_show_at_markers,
|
||||
/client/proc/manipulate_organs,
|
||||
/client/proc/start_line_profiling,
|
||||
/client/proc/stop_line_profiling,
|
||||
/client/proc/show_line_profiling,
|
||||
/client/proc/create_mapping_job_icons,
|
||||
/client/proc/debug_z_levels,
|
||||
/client/proc/place_ruin
|
||||
))
|
||||
GLOBAL_PROTECT(admin_verbs_debug_mapping)
|
||||
|
||||
/obj/effect/debugging/mapfix_marker
|
||||
name = "map fix marker"
|
||||
icon = 'icons/mob/screen_gen.dmi'
|
||||
icon_state = "mapfixmarker"
|
||||
desc = "I am a mappers mistake."
|
||||
|
||||
/obj/effect/debugging/marker
|
||||
icon = 'icons/turf/areas.dmi'
|
||||
icon_state = "yellow"
|
||||
|
||||
/obj/effect/debugging/marker/Move()
|
||||
return 0
|
||||
|
||||
/client/proc/camera_view()
|
||||
set category = "Mapping"
|
||||
set name = "Camera Range Display"
|
||||
|
||||
var/on = FALSE
|
||||
for(var/turf/T in world)
|
||||
if(T.maptext)
|
||||
on = TRUE
|
||||
T.maptext = null
|
||||
|
||||
if(!on)
|
||||
var/list/seen = list()
|
||||
for(var/obj/machinery/camera/C in GLOB.cameranet.cameras)
|
||||
for(var/turf/T in C.can_see())
|
||||
seen[T]++
|
||||
for(var/turf/T in seen)
|
||||
T.maptext = "[seen[T]]"
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Camera Range") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Camera Range")
|
||||
|
||||
#ifdef TESTING
|
||||
GLOBAL_LIST_EMPTY(dirty_vars)
|
||||
|
||||
|
||||
/client/proc/see_dirty_varedits()
|
||||
set category = "Mapping"
|
||||
set name = "Dirty Varedits"
|
||||
|
||||
var/list/dat = list()
|
||||
dat += "<h3>Abandon all hope ye who enter here</h3><br><br>"
|
||||
for(var/thing in GLOB.dirty_vars)
|
||||
dat += "[thing]<br>"
|
||||
CHECK_TICK
|
||||
var/datum/browser/popup = new(usr, "dirty_vars", "Dirty Varedits", 900, 750)
|
||||
popup.set_content(dat.Join())
|
||||
popup.open()
|
||||
#endif
|
||||
|
||||
/client/proc/sec_camera_report()
|
||||
set category = "Mapping"
|
||||
set name = "Camera Report"
|
||||
|
||||
if(!Master)
|
||||
alert(usr,"Master_controller not found.","Sec Camera Report")
|
||||
return 0
|
||||
|
||||
var/list/obj/machinery/camera/CL = list()
|
||||
|
||||
for(var/obj/machinery/camera/C in GLOB.cameranet.cameras)
|
||||
CL += C
|
||||
|
||||
var/output = {"<B>Camera Abnormalities Report</B><HR>
|
||||
<B>The following abnormalities have been detected. The ones in red need immediate attention: Some of those in black may be intentional.</B><BR><ul>"}
|
||||
|
||||
for(var/obj/machinery/camera/C1 in CL)
|
||||
for(var/obj/machinery/camera/C2 in CL)
|
||||
if(C1 != C2)
|
||||
if(C1.c_tag == C2.c_tag)
|
||||
output += "<li><font color='red'>c_tag match for cameras at [ADMIN_VERBOSEJMP(C1)] and [ADMIN_VERBOSEJMP(C2)] - c_tag is [C1.c_tag]</font></li>"
|
||||
if(C1.loc == C2.loc && C1.dir == C2.dir && C1.pixel_x == C2.pixel_x && C1.pixel_y == C2.pixel_y)
|
||||
output += "<li><font color='red'>FULLY overlapping cameras at [ADMIN_VERBOSEJMP(C1)] Networks: [json_encode(C1.network)] and [json_encode(C2.network)]</font></li>"
|
||||
if(C1.loc == C2.loc)
|
||||
output += "<li>Overlapping cameras at [ADMIN_VERBOSEJMP(C1)] Networks: [json_encode(C1.network)] and [json_encode(C2.network)]</li>"
|
||||
var/turf/T = get_step(C1,turn(C1.dir,180))
|
||||
if(!T || !isturf(T) || !T.density )
|
||||
if(!(locate(/obj/structure/grille) in T))
|
||||
var/window_check = 0
|
||||
for(var/obj/structure/window/W in T)
|
||||
if (W.dir == turn(C1.dir,180) || W.dir in list(5,6,9,10) )
|
||||
window_check = 1
|
||||
break
|
||||
if(!window_check)
|
||||
output += "<li><font color='red'>Camera not connected to wall at [ADMIN_VERBOSEJMP(C1)] Network: [json_encode(C1.network)]</font></li>"
|
||||
|
||||
output += "</ul>"
|
||||
usr << browse(output,"window=airreport;size=1000x500")
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Camera Report") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/intercom_view()
|
||||
set category = "Mapping"
|
||||
set name = "Intercom Range Display"
|
||||
|
||||
var/static/intercom_range_display_status = FALSE
|
||||
intercom_range_display_status = !intercom_range_display_status //blame cyberboss if this breaks something
|
||||
|
||||
for(var/obj/effect/debugging/marker/M in world)
|
||||
qdel(M)
|
||||
|
||||
if(intercom_range_display_status)
|
||||
for(var/obj/item/radio/intercom/I in world)
|
||||
for(var/turf/T in orange(7,I))
|
||||
var/obj/effect/debugging/marker/F = new/obj/effect/debugging/marker(T)
|
||||
if (!(F in view(7,I.loc)))
|
||||
qdel(F)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Intercom Range") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/cmd_show_at_list()
|
||||
set category = "Mapping"
|
||||
set name = "Show roundstart AT list"
|
||||
set desc = "Displays a list of active turfs coordinates at roundstart"
|
||||
|
||||
var/dat = {"<b>Coordinate list of Active Turfs at Roundstart</b>
|
||||
<br>Real-time Active Turfs list you can see in Air Subsystem at active_turfs var<br>"}
|
||||
|
||||
for(var/t in GLOB.active_turfs_startlist)
|
||||
var/turf/T = t
|
||||
dat += "[ADMIN_VERBOSEJMP(T)]\n"
|
||||
dat += "<br>"
|
||||
|
||||
usr << browse(dat, "window=at_list")
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Roundstart Active Turfs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/cmd_show_at_markers()
|
||||
set category = "Mapping"
|
||||
set name = "Show roundstart AT markers"
|
||||
set desc = "Places a marker on all active-at-roundstart turfs"
|
||||
|
||||
var/count = 0
|
||||
for(var/obj/effect/abstract/marker/at/AT in GLOB.all_abstract_markers)
|
||||
qdel(AT)
|
||||
count++
|
||||
|
||||
if(count)
|
||||
to_chat(usr, "[count] AT markers removed.")
|
||||
else
|
||||
for(var/t in GLOB.active_turfs_startlist)
|
||||
new /obj/effect/abstract/marker/at(t)
|
||||
count++
|
||||
to_chat(usr, "[count] AT markers placed.")
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Roundstart Active Turf Markers")
|
||||
|
||||
/client/proc/enable_debug_verbs()
|
||||
set category = "Debug"
|
||||
set name = "Debug verbs - Enable"
|
||||
if(!check_rights(R_DEBUG))
|
||||
return
|
||||
verbs -= /client/proc/enable_debug_verbs
|
||||
verbs.Add(/client/proc/disable_debug_verbs, GLOB.admin_verbs_debug_mapping)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Enable Debug Verbs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/disable_debug_verbs()
|
||||
set category = "Debug"
|
||||
set name = "Debug verbs - Disable"
|
||||
verbs.Remove(/client/proc/disable_debug_verbs, GLOB.admin_verbs_debug_mapping)
|
||||
verbs += /client/proc/enable_debug_verbs
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Disable Debug Verbs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/count_objects_on_z_level()
|
||||
set category = "Mapping"
|
||||
set name = "Count Objects On Level"
|
||||
var/level = input("Which z-level?","Level?") as text
|
||||
if(!level)
|
||||
return
|
||||
var/num_level = text2num(level)
|
||||
if(!num_level)
|
||||
return
|
||||
if(!isnum(num_level))
|
||||
return
|
||||
|
||||
var/type_text = input("Which type path?","Path?") as text
|
||||
if(!type_text)
|
||||
return
|
||||
var/type_path = text2path(type_text)
|
||||
if(!type_path)
|
||||
return
|
||||
|
||||
var/count = 0
|
||||
|
||||
var/list/atom/atom_list = list()
|
||||
|
||||
for(var/atom/A in world)
|
||||
if(istype(A,type_path))
|
||||
var/atom/B = A
|
||||
while(!(isturf(B.loc)))
|
||||
if(B && B.loc)
|
||||
B = B.loc
|
||||
else
|
||||
break
|
||||
if(B)
|
||||
if(B.z == num_level)
|
||||
count++
|
||||
atom_list += A
|
||||
|
||||
to_chat(world, "There are [count] objects of type [type_path] on z-level [num_level]")
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Count Objects Zlevel") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/count_objects_all()
|
||||
set category = "Mapping"
|
||||
set name = "Count Objects All"
|
||||
|
||||
var/type_text = input("Which type path?","") as text
|
||||
if(!type_text)
|
||||
return
|
||||
var/type_path = text2path(type_text)
|
||||
if(!type_path)
|
||||
return
|
||||
|
||||
var/count = 0
|
||||
|
||||
for(var/atom/A in world)
|
||||
if(istype(A,type_path))
|
||||
count++
|
||||
|
||||
to_chat(world, "There are [count] objects of type [type_path] in the game world")
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Count Objects All") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
|
||||
//This proc is intended to detect lag problems relating to communication procs
|
||||
GLOBAL_VAR_INIT(say_disabled, FALSE)
|
||||
/client/proc/disable_communication()
|
||||
set category = "Mapping"
|
||||
set name = "Disable all communication verbs"
|
||||
|
||||
GLOB.say_disabled = !GLOB.say_disabled
|
||||
if(GLOB.say_disabled)
|
||||
message_admins("[key] used 'Disable all communication verbs', killing all communication methods.")
|
||||
else
|
||||
message_admins("[key] used 'Disable all communication verbs', restoring all communication methods.")
|
||||
|
||||
//This generates the icon states for job starting location landmarks.
|
||||
/client/proc/create_mapping_job_icons()
|
||||
set name = "Generate job landmarks icons"
|
||||
set category = "Mapping"
|
||||
var/icon/final = icon()
|
||||
var/mob/living/carbon/human/dummy/D = new(locate(1,1,1)) //spawn on 1,1,1 so we don't have runtimes when items are deleted
|
||||
D.setDir(SOUTH)
|
||||
for(var/job in subtypesof(/datum/job))
|
||||
var/datum/job/JB = new job
|
||||
switch(JB.title)
|
||||
if("AI")
|
||||
final.Insert(icon('icons/mob/ai.dmi', "ai", SOUTH, 1), "AI")
|
||||
if("Cyborg")
|
||||
final.Insert(icon('icons/mob/robots.dmi', "robot", SOUTH, 1), "Cyborg")
|
||||
else
|
||||
for(var/obj/item/I in D)
|
||||
qdel(I)
|
||||
randomize_human(D)
|
||||
JB.equip(D, TRUE, FALSE)
|
||||
COMPILE_OVERLAYS(D)
|
||||
var/icon/I = icon(getFlatIcon(D), frame = 1)
|
||||
final.Insert(I, JB.title)
|
||||
qdel(D)
|
||||
//Also add the x
|
||||
for(var/x_number in 1 to 4)
|
||||
final.Insert(icon('icons/mob/screen_gen.dmi', "x[x_number == 1 ? "" : x_number]"), "x[x_number == 1 ? "" : x_number]")
|
||||
fcopy(final, "icons/mob/landmarks.dmi")
|
||||
|
||||
/client/proc/debug_z_levels()
|
||||
set name = "Debug Z-Levels"
|
||||
set category = "Mapping"
|
||||
|
||||
var/list/z_list = SSmapping.z_list
|
||||
var/list/messages = list()
|
||||
messages += "<b>World</b>: [world.maxx] x [world.maxy] x [world.maxz]<br>"
|
||||
|
||||
var/list/linked_levels = list()
|
||||
var/min_x = INFINITY
|
||||
var/min_y = INFINITY
|
||||
var/max_x = -INFINITY
|
||||
var/max_y = -INFINITY
|
||||
|
||||
for(var/z in 1 to max(world.maxz, z_list.len))
|
||||
if (z > z_list.len)
|
||||
messages += "<b>[z]</b>: Unmanaged (out of bounds)<br>"
|
||||
continue
|
||||
var/datum/space_level/S = z_list[z]
|
||||
if (!S)
|
||||
messages += "<b>[z]</b>: Unmanaged (null)<br>"
|
||||
continue
|
||||
var/linkage
|
||||
switch (S.linkage)
|
||||
if (UNAFFECTED)
|
||||
linkage = "no linkage"
|
||||
if (SELFLOOPING)
|
||||
linkage = "self-looping"
|
||||
if (CROSSLINKED)
|
||||
linkage = "linked at ([S.xi], [S.yi])"
|
||||
linked_levels += S
|
||||
min_x = min(min_x, S.xi)
|
||||
min_y = min(min_y, S.yi)
|
||||
max_x = max(max_x, S.xi)
|
||||
max_y = max(max_y, S.yi)
|
||||
else
|
||||
linkage = "unknown linkage '[S.linkage]'"
|
||||
|
||||
messages += "<b>[z]</b>: [S.name], [linkage], traits: [json_encode(S.traits)]<br>"
|
||||
if (S.z_value != z)
|
||||
messages += "-- z_value is [S.z_value], should be [z]<br>"
|
||||
if (S.name == initial(S.name))
|
||||
messages += "-- name not set<br>"
|
||||
if (z > world.maxz)
|
||||
messages += "-- exceeds max z"
|
||||
|
||||
var/grid[max_x - min_x + 1][max_y - min_y + 1]
|
||||
for(var/datum/space_level/S in linked_levels)
|
||||
grid[S.xi - min_x + 1][S.yi - min_y + 1] = S.z_value
|
||||
|
||||
messages += "<table border='1'>"
|
||||
for(var/y in max_y to min_y step -1)
|
||||
var/list/part = list()
|
||||
for(var/x in min_x to max_x)
|
||||
part += "[grid[x - min_x + 1][y - min_y + 1]]"
|
||||
messages += "<tr><td>[part.Join("</td><td>")]</td></tr>"
|
||||
messages += "</table>"
|
||||
|
||||
to_chat(src, messages.Join(""))
|
||||
|
||||
@@ -1,265 +1,265 @@
|
||||
/client/proc/cmd_mass_modify_object_variables(atom/A, var_name)
|
||||
set category = "Debug"
|
||||
set name = "Mass Edit Variables"
|
||||
set desc="(target) Edit all instances of a target item's variables"
|
||||
|
||||
var/method = 0 //0 means strict type detection while 1 means this type and all subtypes (IE: /obj/item with this set to 1 will set it to ALL items)
|
||||
|
||||
if(!check_rights(R_VAREDIT))
|
||||
return
|
||||
|
||||
if(A && A.type)
|
||||
method = vv_subtype_prompt(A.type)
|
||||
|
||||
src.massmodify_variables(A, var_name, method)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Mass Edit Variables") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/massmodify_variables(datum/O, var_name = "", method = 0)
|
||||
if(!check_rights(R_VAREDIT))
|
||||
return
|
||||
if(!istype(O))
|
||||
return
|
||||
|
||||
var/variable = ""
|
||||
if(!var_name)
|
||||
var/list/names = list()
|
||||
for (var/V in O.vars)
|
||||
names += V
|
||||
|
||||
names = sortList(names)
|
||||
|
||||
variable = input("Which var?", "Var") as null|anything in names
|
||||
else
|
||||
variable = var_name
|
||||
|
||||
if(!variable || !O.can_vv_get(variable))
|
||||
return
|
||||
var/default
|
||||
var/var_value = O.vars[variable]
|
||||
|
||||
if(variable in GLOB.VVckey_edit)
|
||||
to_chat(src, "It's forbidden to mass-modify ckeys. It'll crash everyone's client you dummy.")
|
||||
return
|
||||
if(variable in GLOB.VVlocked)
|
||||
if(!check_rights(R_DEBUG))
|
||||
return
|
||||
if(variable in GLOB.VVicon_edit_lock)
|
||||
if(!check_rights(R_FUN|R_DEBUG))
|
||||
return
|
||||
if(variable in GLOB.VVpixelmovement)
|
||||
if(!check_rights(R_DEBUG))
|
||||
return
|
||||
var/prompt = alert(src, "Editing this var may irreparably break tile gliding for the rest of the round. THIS CAN'T BE UNDONE", "DANGER", "ABORT ", "Continue", " ABORT")
|
||||
if (prompt != "Continue")
|
||||
return
|
||||
|
||||
default = vv_get_class(variable, var_value)
|
||||
|
||||
if(isnull(default))
|
||||
to_chat(src, "Unable to determine variable type.")
|
||||
else
|
||||
to_chat(src, "Variable appears to be <b>[uppertext(default)]</b>.")
|
||||
|
||||
to_chat(src, "Variable contains: [var_value]")
|
||||
|
||||
if(default == VV_NUM)
|
||||
var/dir_text = ""
|
||||
if(var_value > 0 && var_value < 16)
|
||||
if(var_value & 1)
|
||||
dir_text += "NORTH"
|
||||
if(var_value & 2)
|
||||
dir_text += "SOUTH"
|
||||
if(var_value & 4)
|
||||
dir_text += "EAST"
|
||||
if(var_value & 8)
|
||||
dir_text += "WEST"
|
||||
|
||||
if(dir_text)
|
||||
to_chat(src, "If a direction, direction is: [dir_text]")
|
||||
|
||||
var/value = vv_get_value(default_class = default)
|
||||
var/new_value = value["value"]
|
||||
var/class = value["class"]
|
||||
|
||||
if(!class || !new_value == null && class != VV_NULL)
|
||||
return
|
||||
|
||||
if (class == VV_MESSAGE)
|
||||
class = VV_TEXT
|
||||
|
||||
if (value["type"])
|
||||
class = VV_NEW_TYPE
|
||||
|
||||
var/original_name = "[O]"
|
||||
|
||||
var/rejected = 0
|
||||
var/accepted = 0
|
||||
|
||||
switch(class)
|
||||
if(VV_RESTORE_DEFAULT)
|
||||
to_chat(src, "Finding items...")
|
||||
var/list/items = get_all_of_type(O.type, method)
|
||||
to_chat(src, "Changing [items.len] items...")
|
||||
for(var/thing in items)
|
||||
if (!thing)
|
||||
continue
|
||||
var/datum/D = thing
|
||||
if (D.vv_edit_var(variable, initial(D.vars[variable])) != FALSE)
|
||||
accepted++
|
||||
else
|
||||
rejected++
|
||||
CHECK_TICK
|
||||
|
||||
if(VV_TEXT)
|
||||
var/list/varsvars = vv_parse_text(O, new_value)
|
||||
var/pre_processing = new_value
|
||||
var/unique
|
||||
if (varsvars && varsvars.len)
|
||||
unique = alert(usr, "Process vars unique to each instance, or same for all?", "Variable Association", "Unique", "Same")
|
||||
if(unique == "Unique")
|
||||
unique = TRUE
|
||||
else
|
||||
unique = FALSE
|
||||
for(var/V in varsvars)
|
||||
new_value = replacetext(new_value,"\[[V]]","[O.vars[V]]")
|
||||
|
||||
to_chat(src, "Finding items...")
|
||||
var/list/items = get_all_of_type(O.type, method)
|
||||
to_chat(src, "Changing [items.len] items...")
|
||||
for(var/thing in items)
|
||||
if (!thing)
|
||||
continue
|
||||
var/datum/D = thing
|
||||
if(unique)
|
||||
new_value = pre_processing
|
||||
for(var/V in varsvars)
|
||||
new_value = replacetext(new_value,"\[[V]]","[D.vars[V]]")
|
||||
|
||||
if (D.vv_edit_var(variable, new_value) != FALSE)
|
||||
accepted++
|
||||
else
|
||||
rejected++
|
||||
CHECK_TICK
|
||||
|
||||
if (VV_NEW_TYPE)
|
||||
var/many = alert(src, "Create only one [value["type"]] and assign each or a new one for each thing", "How Many", "One", "Many", "Cancel")
|
||||
if (many == "Cancel")
|
||||
return
|
||||
if (many == "Many")
|
||||
many = TRUE
|
||||
else
|
||||
many = FALSE
|
||||
|
||||
var/type = value["type"]
|
||||
to_chat(src, "Finding items...")
|
||||
var/list/items = get_all_of_type(O.type, method)
|
||||
to_chat(src, "Changing [items.len] items...")
|
||||
for(var/thing in items)
|
||||
if (!thing)
|
||||
continue
|
||||
var/datum/D = thing
|
||||
if(many && !new_value)
|
||||
new_value = new type()
|
||||
|
||||
if (D.vv_edit_var(variable, new_value) != FALSE)
|
||||
accepted++
|
||||
else
|
||||
rejected++
|
||||
new_value = null
|
||||
CHECK_TICK
|
||||
|
||||
else
|
||||
to_chat(src, "Finding items...")
|
||||
var/list/items = get_all_of_type(O.type, method)
|
||||
to_chat(src, "Changing [items.len] items...")
|
||||
for(var/thing in items)
|
||||
if (!thing)
|
||||
continue
|
||||
var/datum/D = thing
|
||||
if (D.vv_edit_var(variable, new_value) != FALSE)
|
||||
accepted++
|
||||
else
|
||||
rejected++
|
||||
CHECK_TICK
|
||||
|
||||
|
||||
var/count = rejected+accepted
|
||||
if (!count)
|
||||
to_chat(src, "No objects found")
|
||||
return
|
||||
if (!accepted)
|
||||
to_chat(src, "Every object rejected your edit")
|
||||
return
|
||||
if (rejected)
|
||||
to_chat(src, "[rejected] out of [count] objects rejected your edit")
|
||||
|
||||
log_world("### MassVarEdit by [src]: [O.type] (A/R [accepted]/[rejected]) [variable]=[html_encode("[O.vars[variable]]")]([list2params(value)])")
|
||||
log_admin("[key_name(src)] mass modified [original_name]'s [variable] to [O.vars[variable]] ([accepted] objects modified)")
|
||||
message_admins("[key_name_admin(src)] mass modified [original_name]'s [variable] to [O.vars[variable]] ([accepted] objects modified)")
|
||||
|
||||
|
||||
/proc/get_all_of_type(var/T, subtypes = TRUE)
|
||||
var/list/typecache = list()
|
||||
typecache[T] = 1
|
||||
if (subtypes)
|
||||
typecache = typecacheof(typecache)
|
||||
. = list()
|
||||
if (ispath(T, /mob))
|
||||
for(var/mob/thing in GLOB.mob_list)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /obj/machinery/door))
|
||||
for(var/obj/machinery/door/thing in GLOB.airlocks)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /obj/machinery))
|
||||
for(var/obj/machinery/thing in GLOB.machines)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /obj))
|
||||
for(var/obj/thing in world)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /atom/movable))
|
||||
for(var/atom/movable/thing in world)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /turf))
|
||||
for(var/turf/thing in world)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /atom))
|
||||
for(var/atom/thing in world)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /client))
|
||||
for(var/client/thing in GLOB.clients)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /datum))
|
||||
for(var/datum/thing)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else
|
||||
for(var/datum/thing in world)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
/client/proc/cmd_mass_modify_object_variables(atom/A, var_name)
|
||||
set category = "Debug"
|
||||
set name = "Mass Edit Variables"
|
||||
set desc="(target) Edit all instances of a target item's variables"
|
||||
|
||||
var/method = 0 //0 means strict type detection while 1 means this type and all subtypes (IE: /obj/item with this set to 1 will set it to ALL items)
|
||||
|
||||
if(!check_rights(R_VAREDIT))
|
||||
return
|
||||
|
||||
if(A && A.type)
|
||||
method = vv_subtype_prompt(A.type)
|
||||
|
||||
src.massmodify_variables(A, var_name, method)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Mass Edit Variables") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/massmodify_variables(datum/O, var_name = "", method = 0)
|
||||
if(!check_rights(R_VAREDIT))
|
||||
return
|
||||
if(!istype(O))
|
||||
return
|
||||
|
||||
var/variable = ""
|
||||
if(!var_name)
|
||||
var/list/names = list()
|
||||
for (var/V in O.vars)
|
||||
names += V
|
||||
|
||||
names = sortList(names)
|
||||
|
||||
variable = input("Which var?", "Var") as null|anything in names
|
||||
else
|
||||
variable = var_name
|
||||
|
||||
if(!variable || !O.can_vv_get(variable))
|
||||
return
|
||||
var/default
|
||||
var/var_value = O.vars[variable]
|
||||
|
||||
if(variable in GLOB.VVckey_edit)
|
||||
to_chat(src, "It's forbidden to mass-modify ckeys. It'll crash everyone's client you dummy.")
|
||||
return
|
||||
if(variable in GLOB.VVlocked)
|
||||
if(!check_rights(R_DEBUG))
|
||||
return
|
||||
if(variable in GLOB.VVicon_edit_lock)
|
||||
if(!check_rights(R_FUN|R_DEBUG))
|
||||
return
|
||||
if(variable in GLOB.VVpixelmovement)
|
||||
if(!check_rights(R_DEBUG))
|
||||
return
|
||||
var/prompt = alert(src, "Editing this var may irreparably break tile gliding for the rest of the round. THIS CAN'T BE UNDONE", "DANGER", "ABORT ", "Continue", " ABORT")
|
||||
if (prompt != "Continue")
|
||||
return
|
||||
|
||||
default = vv_get_class(variable, var_value)
|
||||
|
||||
if(isnull(default))
|
||||
to_chat(src, "Unable to determine variable type.")
|
||||
else
|
||||
to_chat(src, "Variable appears to be <b>[uppertext(default)]</b>.")
|
||||
|
||||
to_chat(src, "Variable contains: [var_value]")
|
||||
|
||||
if(default == VV_NUM)
|
||||
var/dir_text = ""
|
||||
if(var_value > 0 && var_value < 16)
|
||||
if(var_value & 1)
|
||||
dir_text += "NORTH"
|
||||
if(var_value & 2)
|
||||
dir_text += "SOUTH"
|
||||
if(var_value & 4)
|
||||
dir_text += "EAST"
|
||||
if(var_value & 8)
|
||||
dir_text += "WEST"
|
||||
|
||||
if(dir_text)
|
||||
to_chat(src, "If a direction, direction is: [dir_text]")
|
||||
|
||||
var/value = vv_get_value(default_class = default)
|
||||
var/new_value = value["value"]
|
||||
var/class = value["class"]
|
||||
|
||||
if(!class || !new_value == null && class != VV_NULL)
|
||||
return
|
||||
|
||||
if (class == VV_MESSAGE)
|
||||
class = VV_TEXT
|
||||
|
||||
if (value["type"])
|
||||
class = VV_NEW_TYPE
|
||||
|
||||
var/original_name = "[O]"
|
||||
|
||||
var/rejected = 0
|
||||
var/accepted = 0
|
||||
|
||||
switch(class)
|
||||
if(VV_RESTORE_DEFAULT)
|
||||
to_chat(src, "Finding items...")
|
||||
var/list/items = get_all_of_type(O.type, method)
|
||||
to_chat(src, "Changing [items.len] items...")
|
||||
for(var/thing in items)
|
||||
if (!thing)
|
||||
continue
|
||||
var/datum/D = thing
|
||||
if (D.vv_edit_var(variable, initial(D.vars[variable])) != FALSE)
|
||||
accepted++
|
||||
else
|
||||
rejected++
|
||||
CHECK_TICK
|
||||
|
||||
if(VV_TEXT)
|
||||
var/list/varsvars = vv_parse_text(O, new_value)
|
||||
var/pre_processing = new_value
|
||||
var/unique
|
||||
if (varsvars && varsvars.len)
|
||||
unique = alert(usr, "Process vars unique to each instance, or same for all?", "Variable Association", "Unique", "Same")
|
||||
if(unique == "Unique")
|
||||
unique = TRUE
|
||||
else
|
||||
unique = FALSE
|
||||
for(var/V in varsvars)
|
||||
new_value = replacetext(new_value,"\[[V]]","[O.vars[V]]")
|
||||
|
||||
to_chat(src, "Finding items...")
|
||||
var/list/items = get_all_of_type(O.type, method)
|
||||
to_chat(src, "Changing [items.len] items...")
|
||||
for(var/thing in items)
|
||||
if (!thing)
|
||||
continue
|
||||
var/datum/D = thing
|
||||
if(unique)
|
||||
new_value = pre_processing
|
||||
for(var/V in varsvars)
|
||||
new_value = replacetext(new_value,"\[[V]]","[D.vars[V]]")
|
||||
|
||||
if (D.vv_edit_var(variable, new_value) != FALSE)
|
||||
accepted++
|
||||
else
|
||||
rejected++
|
||||
CHECK_TICK
|
||||
|
||||
if (VV_NEW_TYPE)
|
||||
var/many = alert(src, "Create only one [value["type"]] and assign each or a new one for each thing", "How Many", "One", "Many", "Cancel")
|
||||
if (many == "Cancel")
|
||||
return
|
||||
if (many == "Many")
|
||||
many = TRUE
|
||||
else
|
||||
many = FALSE
|
||||
|
||||
var/type = value["type"]
|
||||
to_chat(src, "Finding items...")
|
||||
var/list/items = get_all_of_type(O.type, method)
|
||||
to_chat(src, "Changing [items.len] items...")
|
||||
for(var/thing in items)
|
||||
if (!thing)
|
||||
continue
|
||||
var/datum/D = thing
|
||||
if(many && !new_value)
|
||||
new_value = new type()
|
||||
|
||||
if (D.vv_edit_var(variable, new_value) != FALSE)
|
||||
accepted++
|
||||
else
|
||||
rejected++
|
||||
new_value = null
|
||||
CHECK_TICK
|
||||
|
||||
else
|
||||
to_chat(src, "Finding items...")
|
||||
var/list/items = get_all_of_type(O.type, method)
|
||||
to_chat(src, "Changing [items.len] items...")
|
||||
for(var/thing in items)
|
||||
if (!thing)
|
||||
continue
|
||||
var/datum/D = thing
|
||||
if (D.vv_edit_var(variable, new_value) != FALSE)
|
||||
accepted++
|
||||
else
|
||||
rejected++
|
||||
CHECK_TICK
|
||||
|
||||
|
||||
var/count = rejected+accepted
|
||||
if (!count)
|
||||
to_chat(src, "No objects found")
|
||||
return
|
||||
if (!accepted)
|
||||
to_chat(src, "Every object rejected your edit")
|
||||
return
|
||||
if (rejected)
|
||||
to_chat(src, "[rejected] out of [count] objects rejected your edit")
|
||||
|
||||
log_world("### MassVarEdit by [src]: [O.type] (A/R [accepted]/[rejected]) [variable]=[html_encode("[O.vars[variable]]")]([list2params(value)])")
|
||||
log_admin("[key_name(src)] mass modified [original_name]'s [variable] to [O.vars[variable]] ([accepted] objects modified)")
|
||||
message_admins("[key_name_admin(src)] mass modified [original_name]'s [variable] to [O.vars[variable]] ([accepted] objects modified)")
|
||||
|
||||
|
||||
/proc/get_all_of_type(var/T, subtypes = TRUE)
|
||||
var/list/typecache = list()
|
||||
typecache[T] = 1
|
||||
if (subtypes)
|
||||
typecache = typecacheof(typecache)
|
||||
. = list()
|
||||
if (ispath(T, /mob))
|
||||
for(var/mob/thing in GLOB.mob_list)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /obj/machinery/door))
|
||||
for(var/obj/machinery/door/thing in GLOB.airlocks)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /obj/machinery))
|
||||
for(var/obj/machinery/thing in GLOB.machines)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /obj))
|
||||
for(var/obj/thing in world)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /atom/movable))
|
||||
for(var/atom/movable/thing in world)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /turf))
|
||||
for(var/turf/thing in world)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /atom))
|
||||
for(var/atom/thing in world)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /client))
|
||||
for(var/client/thing in GLOB.clients)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else if (ispath(T, /datum))
|
||||
for(var/datum/thing)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
else
|
||||
for(var/datum/thing in world)
|
||||
if (typecache[thing.type])
|
||||
. += thing
|
||||
CHECK_TICK
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -137,9 +137,9 @@
|
||||
|
||||
/datum/admins/proc/makeWizard()
|
||||
|
||||
var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you wish to be considered for the position of a Wizard Foundation 'diplomat'?", ROLE_WIZARD, null)
|
||||
var/list/mob/candidates = pollGhostCandidates("Do you wish to be considered for the position of a Wizard Foundation 'diplomat'?", ROLE_WIZARD, null)
|
||||
|
||||
var/mob/dead/observer/selected = pick_n_take(candidates)
|
||||
var/mob/selected = pick_n_take(candidates)
|
||||
|
||||
var/mob/living/carbon/human/new_character = makeBody(selected)
|
||||
new_character.mind.make_Wizard()
|
||||
@@ -214,9 +214,9 @@
|
||||
|
||||
/datum/admins/proc/makeNukeTeam()
|
||||
var/datum/game_mode/nuclear/temp = new
|
||||
var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you wish to be considered for a nuke team being sent in?", ROLE_OPERATIVE, temp)
|
||||
var/list/mob/dead/observer/chosen = list()
|
||||
var/mob/dead/observer/theghost = null
|
||||
var/list/mob/candidates = pollGhostCandidates("Do you wish to be considered for a nuke team being sent in?", ROLE_OPERATIVE, temp)
|
||||
var/list/mob/chosen = list()
|
||||
var/mob/theghost = null
|
||||
|
||||
if(candidates.len)
|
||||
var/numagents = 5
|
||||
@@ -378,7 +378,7 @@
|
||||
ertemplate.enforce_human = prefs["enforce_human"]["value"] == "Yes" ? TRUE : FALSE
|
||||
ertemplate.opendoors = prefs["open_armory"]["value"] == "Yes" ? TRUE : FALSE
|
||||
|
||||
var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you wish to be considered for [ertemplate.polldesc] ?", "deathsquad", null)
|
||||
var/list/mob/candidates = pollGhostCandidates("Do you wish to be considered for [ertemplate.polldesc] ?", "deathsquad", null)
|
||||
var/teamSpawned = FALSE
|
||||
|
||||
if(candidates.len > 0)
|
||||
@@ -404,7 +404,7 @@
|
||||
numagents--
|
||||
continue // This guy's unlucky, not enough spawn points, we skip him.
|
||||
var/spawnloc = spawnpoints[numagents]
|
||||
var/mob/dead/observer/chosen_candidate = pick(candidates)
|
||||
var/mob/chosen_candidate = pick(candidates)
|
||||
candidates -= chosen_candidate
|
||||
if(!chosen_candidate.key)
|
||||
continue
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
GLOBAL_VAR_INIT(highlander, FALSE)
|
||||
/client/proc/only_one() //Gives everyone kilts, berets, claymores, and pinpointers, with the objective to hijack the emergency shuttle.
|
||||
if(!SSticker.HasRoundStarted())
|
||||
alert("The game hasn't started yet!")
|
||||
return
|
||||
GLOB.highlander = TRUE
|
||||
|
||||
send_to_playing_players("<span class='boldannounce'><font size=6>THERE CAN BE ONLY ONE</font></span>")
|
||||
|
||||
for(var/obj/item/disk/nuclear/N in GLOB.poi_list)
|
||||
var/datum/component/stationloving/component = N.GetComponent(/datum/component/stationloving)
|
||||
if (component)
|
||||
component.relocate() //Gets it out of bags and such
|
||||
|
||||
for(var/mob/living/carbon/human/H in GLOB.player_list)
|
||||
if(H.stat == DEAD || !(H.client))
|
||||
continue
|
||||
H.make_scottish()
|
||||
|
||||
message_admins("<span class='adminnotice'>[key_name_admin(usr)] used THERE CAN BE ONLY ONE!</span>")
|
||||
log_admin("[key_name(usr)] used THERE CAN BE ONLY ONE.")
|
||||
addtimer(CALLBACK(SSshuttle.emergency, /obj/docking_port/mobile/emergency.proc/request, null, 1), 50)
|
||||
|
||||
/client/proc/only_one_delayed()
|
||||
send_to_playing_players("<span class='userdanger'>Bagpipes begin to blare. You feel Scottish pride coming over you.</span>")
|
||||
message_admins("<span class='adminnotice'>[key_name_admin(usr)] used (delayed) THERE CAN BE ONLY ONE!</span>")
|
||||
log_admin("[key_name(usr)] used delayed THERE CAN BE ONLY ONE.")
|
||||
addtimer(CALLBACK(src, .proc/only_one), 420)
|
||||
|
||||
/mob/living/carbon/human/proc/make_scottish()
|
||||
GLOBAL_VAR_INIT(highlander, FALSE)
|
||||
/client/proc/only_one() //Gives everyone kilts, berets, claymores, and pinpointers, with the objective to hijack the emergency shuttle.
|
||||
if(!SSticker.HasRoundStarted())
|
||||
alert("The game hasn't started yet!")
|
||||
return
|
||||
GLOB.highlander = TRUE
|
||||
|
||||
send_to_playing_players("<span class='boldannounce'><font size=6>THERE CAN BE ONLY ONE</font></span>")
|
||||
|
||||
for(var/obj/item/disk/nuclear/N in GLOB.poi_list)
|
||||
var/datum/component/stationloving/component = N.GetComponent(/datum/component/stationloving)
|
||||
if (component)
|
||||
component.relocate() //Gets it out of bags and such
|
||||
|
||||
for(var/mob/living/carbon/human/H in GLOB.player_list)
|
||||
if(H.stat == DEAD || !(H.client))
|
||||
continue
|
||||
H.make_scottish()
|
||||
|
||||
message_admins("<span class='adminnotice'>[key_name_admin(usr)] used THERE CAN BE ONLY ONE!</span>")
|
||||
log_admin("[key_name(usr)] used THERE CAN BE ONLY ONE.")
|
||||
addtimer(CALLBACK(SSshuttle.emergency, /obj/docking_port/mobile/emergency.proc/request, null, 1), 50)
|
||||
|
||||
/client/proc/only_one_delayed()
|
||||
send_to_playing_players("<span class='userdanger'>Bagpipes begin to blare. You feel Scottish pride coming over you.</span>")
|
||||
message_admins("<span class='adminnotice'>[key_name_admin(usr)] used (delayed) THERE CAN BE ONLY ONE!</span>")
|
||||
log_admin("[key_name(usr)] used delayed THERE CAN BE ONLY ONE.")
|
||||
addtimer(CALLBACK(src, .proc/only_one), 420)
|
||||
|
||||
/mob/living/carbon/human/proc/make_scottish()
|
||||
mind.add_antag_datum(/datum/antagonist/highlander)
|
||||
@@ -1,203 +1,203 @@
|
||||
/client/proc/play_sound(S as sound)
|
||||
set category = "Fun"
|
||||
set name = "Play Global Sound"
|
||||
if(!check_rights(R_SOUNDS))
|
||||
return
|
||||
|
||||
var/vol = input(usr, "What volume would you like the sound to play at?",, 100) as null|num
|
||||
if(!vol)
|
||||
return
|
||||
var/freq = input(usr, "What frequency would you like the sound to play at?",, 1) as null|num
|
||||
if(!freq)
|
||||
freq = 1
|
||||
vol = CLAMP(vol, 1, 100)
|
||||
|
||||
var/sound/admin_sound = new()
|
||||
admin_sound.file = S
|
||||
admin_sound.priority = 250
|
||||
admin_sound.channel = CHANNEL_ADMIN
|
||||
admin_sound.frequency = freq
|
||||
admin_sound.wait = 1
|
||||
admin_sound.repeat = 0
|
||||
admin_sound.status = SOUND_STREAM
|
||||
admin_sound.volume = vol
|
||||
|
||||
var/res = alert(usr, "Show the title of this song to the players?",, "Yes","No", "Cancel")
|
||||
switch(res)
|
||||
if("Yes")
|
||||
to_chat(world, "<span class='boldannounce'>An admin played: [S]</span>")
|
||||
if("Cancel")
|
||||
return
|
||||
|
||||
log_admin("[key_name(src)] played sound [S]")
|
||||
message_admins("[key_name_admin(src)] played sound [S]")
|
||||
|
||||
for(var/mob/M in GLOB.player_list)
|
||||
if(M.client.prefs.toggles & SOUND_MIDI)
|
||||
var/user_vol = M.client.chatOutput.adminMusicVolume
|
||||
if(user_vol)
|
||||
admin_sound.volume = vol * (user_vol / 100)
|
||||
SEND_SOUND(M, admin_sound)
|
||||
admin_sound.volume = vol
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Play Global Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
|
||||
/client/proc/play_local_sound(S as sound)
|
||||
set category = "Fun"
|
||||
set name = "Play Local Sound"
|
||||
if(!check_rights(R_SOUNDS))
|
||||
return
|
||||
|
||||
log_admin("[key_name(src)] played a local sound [S]")
|
||||
message_admins("[key_name_admin(src)] played a local sound [S]")
|
||||
playsound(get_turf(src.mob), S, 50, 0, 0)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Play Local Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/play_web_sound()
|
||||
set category = "Fun"
|
||||
set name = "Play Internet Sound"
|
||||
if(!check_rights(R_SOUNDS))
|
||||
return
|
||||
|
||||
var/ytdl = CONFIG_GET(string/invoke_youtubedl)
|
||||
if(!ytdl)
|
||||
to_chat(src, "<span class='boldwarning'>Youtube-dl was not configured, action unavailable</span>") //Check config.txt for the INVOKE_YOUTUBEDL value
|
||||
return
|
||||
|
||||
var/web_sound_input = input("Enter content URL (supported sites only, leave blank to stop playing)", "Play Internet Sound via youtube-dl") as text|null
|
||||
if(istext(web_sound_input))
|
||||
var/web_sound_url = ""
|
||||
var/stop_web_sounds = FALSE
|
||||
var/pitch
|
||||
if(length(web_sound_input))
|
||||
|
||||
web_sound_input = trim(web_sound_input)
|
||||
if(findtext(web_sound_input, ":") && !findtext(web_sound_input, GLOB.is_http_protocol))
|
||||
to_chat(src, "<span class='boldwarning'>Non-http(s) URIs are not allowed.</span>")
|
||||
to_chat(src, "<span class='warning'>For youtube-dl shortcuts like ytsearch: please use the appropriate full url from the website.</span>")
|
||||
return
|
||||
var/shell_scrubbed_input = shell_url_scrub(web_sound_input)
|
||||
var/list/output = world.shelleo("[ytdl] --format \"bestaudio\[ext=mp3]/best\[ext=mp4]\[height<=360]/bestaudio\[ext=m4a]/bestaudio\[ext=aac]\" --dump-single-json --no-playlist -- \"[shell_scrubbed_input]\"")
|
||||
var/errorlevel = output[SHELLEO_ERRORLEVEL]
|
||||
var/stdout = output[SHELLEO_STDOUT]
|
||||
var/stderr = output[SHELLEO_STDERR]
|
||||
if(!errorlevel)
|
||||
var/list/data
|
||||
try
|
||||
data = json_decode(stdout)
|
||||
catch(var/exception/e)
|
||||
to_chat(src, "<span class='boldwarning'>Youtube-dl JSON parsing FAILED:</span>")
|
||||
to_chat(src, "<span class='warning'>[e]: [stdout]</span>")
|
||||
return
|
||||
|
||||
if (data["url"])
|
||||
web_sound_url = data["url"]
|
||||
var/title = "[data["title"]]"
|
||||
var/webpage_url = title
|
||||
if (data["webpage_url"])
|
||||
webpage_url = "<a href=\"[data["webpage_url"]]\">[title]</a>"
|
||||
|
||||
var/freq = input(usr, "What frequency would you like the sound to play at?",, 1) as null|num
|
||||
if(!freq)
|
||||
freq = 1
|
||||
pitch = freq
|
||||
|
||||
var/res = alert(usr, "Show the title of and link to this song to the players?\n[title]",, "No", "Yes", "Cancel")
|
||||
switch(res)
|
||||
if("Yes")
|
||||
to_chat(world, "<span class='boldannounce'>An admin played: [webpage_url]</span>")
|
||||
if("Cancel")
|
||||
return
|
||||
SSblackbox.record_feedback("nested tally", "played_url", 1, list("[ckey]", "[web_sound_input]"))
|
||||
log_admin("[key_name(src)] played web sound: [web_sound_input]")
|
||||
message_admins("[key_name(src)] played web sound: [web_sound_input]")
|
||||
else
|
||||
to_chat(src, "<span class='boldwarning'>Youtube-dl URL retrieval FAILED:</span>")
|
||||
to_chat(src, "<span class='warning'>[stderr]</span>")
|
||||
|
||||
else //pressed ok with blank
|
||||
log_admin("[key_name(src)] stopped web sound")
|
||||
message_admins("[key_name(src)] stopped web sound")
|
||||
web_sound_url = null
|
||||
stop_web_sounds = TRUE
|
||||
|
||||
if(web_sound_url && !findtext(web_sound_url, GLOB.is_http_protocol))
|
||||
to_chat(src, "<span class='boldwarning'>BLOCKED: Content URL not using http(s) protocol</span>")
|
||||
to_chat(src, "<span class='warning'>The media provider returned a content URL that isn't using the HTTP or HTTPS protocol</span>")
|
||||
return
|
||||
if(web_sound_url || stop_web_sounds)
|
||||
for(var/m in GLOB.player_list)
|
||||
var/mob/M = m
|
||||
var/client/C = M.client
|
||||
if((C.prefs.toggles & SOUND_MIDI) && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
|
||||
if(!stop_web_sounds)
|
||||
C.chatOutput.sendMusic(web_sound_url, pitch)
|
||||
else
|
||||
C.chatOutput.stopMusic()
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Play Internet Sound")
|
||||
|
||||
/client/proc/set_round_end_sound(S as sound)
|
||||
set category = "Fun"
|
||||
set name = "Set Round End Sound"
|
||||
if(!check_rights(R_SOUNDS))
|
||||
return
|
||||
|
||||
SSticker.SetRoundEndSound(S)
|
||||
|
||||
log_admin("[key_name(src)] set the round end sound to [S]")
|
||||
message_admins("[key_name_admin(src)] set the round end sound to [S]")
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Set Round End Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/play_web_sound_manual()
|
||||
set category = "Fun"
|
||||
set name = "Manual Play Internet Sound"
|
||||
if(!check_rights(R_SOUNDS))
|
||||
return
|
||||
|
||||
var/web_sound_input = input("Enter youtube-dl fetched content URL (supported sites only, leave blank to stop playing)", "Send youtube-dl media link") as text|null
|
||||
if(!istext(web_sound_input))
|
||||
return
|
||||
web_sound_input = trim(web_sound_input)
|
||||
if(!length(web_sound_input))
|
||||
log_admin("[key_name(src)] stopped web sound")
|
||||
message_admins("[key_name(src)] stopped web sound")
|
||||
for(var/m in GLOB.player_list)
|
||||
var/mob/M = m
|
||||
var/client/C = M.client
|
||||
if((C.prefs.toggles & SOUND_MIDI) && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
|
||||
C.chatOutput.stopMusic()
|
||||
return
|
||||
var/freq = input(usr, "What frequency would you like the sound to play at?",, 1) as null|num
|
||||
if(!freq)
|
||||
return
|
||||
if(web_sound_input && !findtext(web_sound_input, GLOB.is_http_protocol))
|
||||
to_chat(src, "<span class='boldwarning'>BLOCKED: Content URL not using http(s) protocol</span>")
|
||||
to_chat(src, "<span class='warning'>The media provider returned a content URL that isn't using the HTTP or HTTPS protocol</span>")
|
||||
return
|
||||
|
||||
SSblackbox.record_feedback("nested tally", "played_url_manual", 1, list("[ckey]", "[web_sound_input]"))
|
||||
log_admin("[key_name(src)] manually played web sound: [web_sound_input]")
|
||||
message_admins("[key_name(src)] manually played web sound: <a href='web_sound_input'>HREF</a>")
|
||||
for(var/m in GLOB.player_list)
|
||||
var/mob/M = m
|
||||
var/client/C = M.client
|
||||
if((C.prefs.toggles & SOUND_MIDI) && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
|
||||
C.chatOutput.sendMusic(web_sound_input, freq)
|
||||
|
||||
/client/proc/stop_sounds()
|
||||
set category = "Debug"
|
||||
set name = "Stop All Playing Sounds"
|
||||
if(!src.holder)
|
||||
return
|
||||
|
||||
log_admin("[key_name(src)] stopped all currently playing sounds.")
|
||||
message_admins("[key_name_admin(src)] stopped all currently playing sounds.")
|
||||
for(var/mob/M in GLOB.player_list)
|
||||
if(M.client)
|
||||
SEND_SOUND(M, sound(null))
|
||||
var/client/C = M.client
|
||||
if(C && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
|
||||
C.chatOutput.stopMusic()
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Stop All Playing Sounds") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
/client/proc/play_sound(S as sound)
|
||||
set category = "Fun"
|
||||
set name = "Play Global Sound"
|
||||
if(!check_rights(R_SOUNDS))
|
||||
return
|
||||
|
||||
var/vol = input(usr, "What volume would you like the sound to play at?",, 100) as null|num
|
||||
if(!vol)
|
||||
return
|
||||
var/freq = input(usr, "What frequency would you like the sound to play at?",, 1) as null|num
|
||||
if(!freq)
|
||||
freq = 1
|
||||
vol = CLAMP(vol, 1, 100)
|
||||
|
||||
var/sound/admin_sound = new()
|
||||
admin_sound.file = S
|
||||
admin_sound.priority = 250
|
||||
admin_sound.channel = CHANNEL_ADMIN
|
||||
admin_sound.frequency = freq
|
||||
admin_sound.wait = 1
|
||||
admin_sound.repeat = 0
|
||||
admin_sound.status = SOUND_STREAM
|
||||
admin_sound.volume = vol
|
||||
|
||||
var/res = alert(usr, "Show the title of this song to the players?",, "Yes","No", "Cancel")
|
||||
switch(res)
|
||||
if("Yes")
|
||||
to_chat(world, "<span class='boldannounce'>An admin played: [S]</span>")
|
||||
if("Cancel")
|
||||
return
|
||||
|
||||
log_admin("[key_name(src)] played sound [S]")
|
||||
message_admins("[key_name_admin(src)] played sound [S]")
|
||||
|
||||
for(var/mob/M in GLOB.player_list)
|
||||
if(M.client.prefs.toggles & SOUND_MIDI)
|
||||
var/user_vol = M.client.chatOutput.adminMusicVolume
|
||||
if(user_vol)
|
||||
admin_sound.volume = vol * (user_vol / 100)
|
||||
SEND_SOUND(M, admin_sound)
|
||||
admin_sound.volume = vol
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Play Global Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
|
||||
/client/proc/play_local_sound(S as sound)
|
||||
set category = "Fun"
|
||||
set name = "Play Local Sound"
|
||||
if(!check_rights(R_SOUNDS))
|
||||
return
|
||||
|
||||
log_admin("[key_name(src)] played a local sound [S]")
|
||||
message_admins("[key_name_admin(src)] played a local sound [S]")
|
||||
playsound(get_turf(src.mob), S, 50, 0, 0)
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Play Local Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/play_web_sound()
|
||||
set category = "Fun"
|
||||
set name = "Play Internet Sound"
|
||||
if(!check_rights(R_SOUNDS))
|
||||
return
|
||||
|
||||
var/ytdl = CONFIG_GET(string/invoke_youtubedl)
|
||||
if(!ytdl)
|
||||
to_chat(src, "<span class='boldwarning'>Youtube-dl was not configured, action unavailable</span>") //Check config.txt for the INVOKE_YOUTUBEDL value
|
||||
return
|
||||
|
||||
var/web_sound_input = input("Enter content URL (supported sites only, leave blank to stop playing)", "Play Internet Sound via youtube-dl") as text|null
|
||||
if(istext(web_sound_input))
|
||||
var/web_sound_url = ""
|
||||
var/stop_web_sounds = FALSE
|
||||
var/pitch
|
||||
if(length(web_sound_input))
|
||||
|
||||
web_sound_input = trim(web_sound_input)
|
||||
if(findtext(web_sound_input, ":") && !findtext(web_sound_input, GLOB.is_http_protocol))
|
||||
to_chat(src, "<span class='boldwarning'>Non-http(s) URIs are not allowed.</span>")
|
||||
to_chat(src, "<span class='warning'>For youtube-dl shortcuts like ytsearch: please use the appropriate full url from the website.</span>")
|
||||
return
|
||||
var/shell_scrubbed_input = shell_url_scrub(web_sound_input)
|
||||
var/list/output = world.shelleo("[ytdl] --format \"bestaudio\[ext=mp3]/best\[ext=mp4]\[height<=360]/bestaudio\[ext=m4a]/bestaudio\[ext=aac]\" --dump-single-json --no-playlist -- \"[shell_scrubbed_input]\"")
|
||||
var/errorlevel = output[SHELLEO_ERRORLEVEL]
|
||||
var/stdout = output[SHELLEO_STDOUT]
|
||||
var/stderr = output[SHELLEO_STDERR]
|
||||
if(!errorlevel)
|
||||
var/list/data
|
||||
try
|
||||
data = json_decode(stdout)
|
||||
catch(var/exception/e)
|
||||
to_chat(src, "<span class='boldwarning'>Youtube-dl JSON parsing FAILED:</span>")
|
||||
to_chat(src, "<span class='warning'>[e]: [stdout]</span>")
|
||||
return
|
||||
|
||||
if (data["url"])
|
||||
web_sound_url = data["url"]
|
||||
var/title = "[data["title"]]"
|
||||
var/webpage_url = title
|
||||
if (data["webpage_url"])
|
||||
webpage_url = "<a href=\"[data["webpage_url"]]\">[title]</a>"
|
||||
|
||||
var/freq = input(usr, "What frequency would you like the sound to play at?",, 1) as null|num
|
||||
if(!freq)
|
||||
freq = 1
|
||||
pitch = freq
|
||||
|
||||
var/res = alert(usr, "Show the title of and link to this song to the players?\n[title]",, "No", "Yes", "Cancel")
|
||||
switch(res)
|
||||
if("Yes")
|
||||
to_chat(world, "<span class='boldannounce'>An admin played: [webpage_url]</span>")
|
||||
if("Cancel")
|
||||
return
|
||||
SSblackbox.record_feedback("nested tally", "played_url", 1, list("[ckey]", "[web_sound_input]"))
|
||||
log_admin("[key_name(src)] played web sound: [web_sound_input]")
|
||||
message_admins("[key_name(src)] played web sound: [web_sound_input]")
|
||||
else
|
||||
to_chat(src, "<span class='boldwarning'>Youtube-dl URL retrieval FAILED:</span>")
|
||||
to_chat(src, "<span class='warning'>[stderr]</span>")
|
||||
|
||||
else //pressed ok with blank
|
||||
log_admin("[key_name(src)] stopped web sound")
|
||||
message_admins("[key_name(src)] stopped web sound")
|
||||
web_sound_url = null
|
||||
stop_web_sounds = TRUE
|
||||
|
||||
if(web_sound_url && !findtext(web_sound_url, GLOB.is_http_protocol))
|
||||
to_chat(src, "<span class='boldwarning'>BLOCKED: Content URL not using http(s) protocol</span>")
|
||||
to_chat(src, "<span class='warning'>The media provider returned a content URL that isn't using the HTTP or HTTPS protocol</span>")
|
||||
return
|
||||
if(web_sound_url || stop_web_sounds)
|
||||
for(var/m in GLOB.player_list)
|
||||
var/mob/M = m
|
||||
var/client/C = M.client
|
||||
if((C.prefs.toggles & SOUND_MIDI) && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
|
||||
if(!stop_web_sounds)
|
||||
C.chatOutput.sendMusic(web_sound_url, pitch)
|
||||
else
|
||||
C.chatOutput.stopMusic()
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Play Internet Sound")
|
||||
|
||||
/client/proc/set_round_end_sound(S as sound)
|
||||
set category = "Fun"
|
||||
set name = "Set Round End Sound"
|
||||
if(!check_rights(R_SOUNDS))
|
||||
return
|
||||
|
||||
SSticker.SetRoundEndSound(S)
|
||||
|
||||
log_admin("[key_name(src)] set the round end sound to [S]")
|
||||
message_admins("[key_name_admin(src)] set the round end sound to [S]")
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Set Round End Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/play_web_sound_manual()
|
||||
set category = "Fun"
|
||||
set name = "Manual Play Internet Sound"
|
||||
if(!check_rights(R_SOUNDS))
|
||||
return
|
||||
|
||||
var/web_sound_input = input("Enter youtube-dl fetched content URL (supported sites only, leave blank to stop playing)", "Send youtube-dl media link") as text|null
|
||||
if(!istext(web_sound_input))
|
||||
return
|
||||
web_sound_input = trim(web_sound_input)
|
||||
if(!length(web_sound_input))
|
||||
log_admin("[key_name(src)] stopped web sound")
|
||||
message_admins("[key_name(src)] stopped web sound")
|
||||
for(var/m in GLOB.player_list)
|
||||
var/mob/M = m
|
||||
var/client/C = M.client
|
||||
if((C.prefs.toggles & SOUND_MIDI) && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
|
||||
C.chatOutput.stopMusic()
|
||||
return
|
||||
var/freq = input(usr, "What frequency would you like the sound to play at?",, 1) as null|num
|
||||
if(!freq)
|
||||
return
|
||||
if(web_sound_input && !findtext(web_sound_input, GLOB.is_http_protocol))
|
||||
to_chat(src, "<span class='boldwarning'>BLOCKED: Content URL not using http(s) protocol</span>")
|
||||
to_chat(src, "<span class='warning'>The media provider returned a content URL that isn't using the HTTP or HTTPS protocol</span>")
|
||||
return
|
||||
|
||||
SSblackbox.record_feedback("nested tally", "played_url_manual", 1, list("[ckey]", "[web_sound_input]"))
|
||||
log_admin("[key_name(src)] manually played web sound: [web_sound_input]")
|
||||
message_admins("[key_name(src)] manually played web sound: <a href='web_sound_input'>HREF</a>")
|
||||
for(var/m in GLOB.player_list)
|
||||
var/mob/M = m
|
||||
var/client/C = M.client
|
||||
if((C.prefs.toggles & SOUND_MIDI) && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
|
||||
C.chatOutput.sendMusic(web_sound_input, freq)
|
||||
|
||||
/client/proc/stop_sounds()
|
||||
set category = "Debug"
|
||||
set name = "Stop All Playing Sounds"
|
||||
if(!src.holder)
|
||||
return
|
||||
|
||||
log_admin("[key_name(src)] stopped all currently playing sounds.")
|
||||
message_admins("[key_name_admin(src)] stopped all currently playing sounds.")
|
||||
for(var/mob/M in GLOB.player_list)
|
||||
if(M.client)
|
||||
SEND_SOUND(M, sound(null))
|
||||
var/client/C = M.client
|
||||
if(C && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
|
||||
C.chatOutput.stopMusic()
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Stop All Playing Sounds") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
/proc/possess(obj/O in world)
|
||||
set name = "Possess Obj"
|
||||
set category = "Object"
|
||||
|
||||
if((O.obj_flags & DANGEROUS_POSSESSION) && CONFIG_GET(flag/forbid_singulo_possession))
|
||||
to_chat(usr, "[O] is too powerful for you to possess.")
|
||||
return
|
||||
|
||||
var/turf/T = get_turf(O)
|
||||
|
||||
if(T)
|
||||
log_admin("[key_name(usr)] has possessed [O] ([O.type]) at [AREACOORD(T)]")
|
||||
message_admins("[key_name(usr)] has possessed [O] ([O.type]) at [AREACOORD(T)]")
|
||||
else
|
||||
log_admin("[key_name(usr)] has possessed [O] ([O.type]) at an unknown location")
|
||||
message_admins("[key_name(usr)] has possessed [O] ([O.type]) at an unknown location")
|
||||
|
||||
if(!usr.control_object) //If you're not already possessing something...
|
||||
usr.name_archive = usr.real_name
|
||||
|
||||
usr.loc = O
|
||||
usr.real_name = O.name
|
||||
usr.name = O.name
|
||||
usr.reset_perspective(O)
|
||||
usr.control_object = O
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Possess Object") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/proc/release()
|
||||
set name = "Release Obj"
|
||||
set category = "Object"
|
||||
//usr.loc = get_turf(usr)
|
||||
|
||||
if(usr.control_object && usr.name_archive) //if you have a name archived and if you are actually relassing an object
|
||||
usr.real_name = usr.name_archive
|
||||
usr.name_archive = ""
|
||||
usr.name = usr.real_name
|
||||
if(ishuman(usr))
|
||||
var/mob/living/carbon/human/H = usr
|
||||
H.name = H.get_visible_name()
|
||||
|
||||
|
||||
usr.loc = get_turf(usr.control_object)
|
||||
usr.reset_perspective()
|
||||
usr.control_object = null
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Release Object") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/proc/givetestverbs(mob/M in GLOB.mob_list)
|
||||
set desc = "Give this guy possess/release verbs"
|
||||
set category = "Debug"
|
||||
set name = "Give Possessing Verbs"
|
||||
M.verbs += /proc/possess
|
||||
M.verbs += /proc/release
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Give Possessing Verbs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
/proc/possess(obj/O in world)
|
||||
set name = "Possess Obj"
|
||||
set category = "Object"
|
||||
|
||||
if((O.obj_flags & DANGEROUS_POSSESSION) && CONFIG_GET(flag/forbid_singulo_possession))
|
||||
to_chat(usr, "[O] is too powerful for you to possess.")
|
||||
return
|
||||
|
||||
var/turf/T = get_turf(O)
|
||||
|
||||
if(T)
|
||||
log_admin("[key_name(usr)] has possessed [O] ([O.type]) at [AREACOORD(T)]")
|
||||
message_admins("[key_name(usr)] has possessed [O] ([O.type]) at [AREACOORD(T)]")
|
||||
else
|
||||
log_admin("[key_name(usr)] has possessed [O] ([O.type]) at an unknown location")
|
||||
message_admins("[key_name(usr)] has possessed [O] ([O.type]) at an unknown location")
|
||||
|
||||
if(!usr.control_object) //If you're not already possessing something...
|
||||
usr.name_archive = usr.real_name
|
||||
|
||||
usr.loc = O
|
||||
usr.real_name = O.name
|
||||
usr.name = O.name
|
||||
usr.reset_perspective(O)
|
||||
usr.control_object = O
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Possess Object") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/proc/release()
|
||||
set name = "Release Obj"
|
||||
set category = "Object"
|
||||
//usr.loc = get_turf(usr)
|
||||
|
||||
if(usr.control_object && usr.name_archive) //if you have a name archived and if you are actually relassing an object
|
||||
usr.real_name = usr.name_archive
|
||||
usr.name_archive = ""
|
||||
usr.name = usr.real_name
|
||||
if(ishuman(usr))
|
||||
var/mob/living/carbon/human/H = usr
|
||||
H.name = H.get_visible_name()
|
||||
|
||||
|
||||
usr.loc = get_turf(usr.control_object)
|
||||
usr.reset_perspective()
|
||||
usr.control_object = null
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Release Object") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/proc/givetestverbs(mob/M in GLOB.mob_list)
|
||||
set desc = "Give this guy possess/release verbs"
|
||||
set category = "Debug"
|
||||
set name = "Give Possessing Verbs"
|
||||
M.verbs += /proc/possess
|
||||
M.verbs += /proc/release
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Give Possessing Verbs") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
/mob/verb/pray(msg as text)
|
||||
set category = "IC"
|
||||
set name = "Pray"
|
||||
|
||||
if(GLOB.say_disabled) //This is here to try to identify lag problems
|
||||
to_chat(usr, "<span class='danger'>Speech is currently admin-disabled.</span>")
|
||||
return
|
||||
|
||||
msg = copytext(sanitize(msg), 1, MAX_MESSAGE_LEN)
|
||||
if(!msg)
|
||||
return
|
||||
log_prayer("[src.key]/([src.name]): [msg]")
|
||||
if(usr.client)
|
||||
if(usr.client.prefs.muted & MUTE_PRAY)
|
||||
to_chat(usr, "<span class='danger'>You cannot pray (muted).</span>")
|
||||
return
|
||||
if(src.client.handle_spam_prevention(msg,MUTE_PRAY))
|
||||
return
|
||||
|
||||
var/mutable_appearance/cross = mutable_appearance('icons/obj/storage.dmi', "bible")
|
||||
var/font_color = "purple"
|
||||
var/prayer_type = "PRAYER"
|
||||
var/deity
|
||||
if(usr.job == "Chaplain")
|
||||
cross.icon_state = "kingyellow"
|
||||
font_color = "blue"
|
||||
prayer_type = "CHAPLAIN PRAYER"
|
||||
if(GLOB.deity)
|
||||
deity = GLOB.deity
|
||||
else if(iscultist(usr))
|
||||
cross.icon_state = "tome"
|
||||
font_color = "red"
|
||||
prayer_type = "CULTIST PRAYER"
|
||||
deity = "Nar'Sie"
|
||||
else if(isliving(usr))
|
||||
var/mob/living/L = usr
|
||||
if(HAS_TRAIT(L, TRAIT_SPIRITUAL))
|
||||
cross.icon_state = "holylight"
|
||||
font_color = "blue"
|
||||
prayer_type = "SPIRITUAL PRAYER"
|
||||
|
||||
var/msg_tmp = msg
|
||||
msg = "<span class='adminnotice'>[icon2html(cross, GLOB.admins)]<b><font color=[font_color]>[prayer_type][deity ? " (to [deity])" : ""]: </font>[ADMIN_FULLMONTY(src)] [ADMIN_SC(src)]:</b> <span class='linkify'>[msg]</span></span>"
|
||||
|
||||
for(var/client/C in GLOB.admins)
|
||||
if(C.prefs.chat_toggles & CHAT_PRAYER)
|
||||
to_chat(C, msg)
|
||||
if(C.prefs.toggles & SOUND_PRAYERS)
|
||||
if(usr.job == "Chaplain")
|
||||
SEND_SOUND(C, sound('sound/effects/pray.ogg'))
|
||||
to_chat(usr, "<span class='info'>You pray to the gods: \"[msg_tmp]\"</span>")
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Prayer") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
//log_admin("HELP: [key_name(src)]: [msg]")
|
||||
|
||||
/proc/CentCom_announce(text , mob/Sender)
|
||||
var/msg = copytext(sanitize(text), 1, MAX_MESSAGE_LEN)
|
||||
msg = "<span class='adminnotice'><b><font color=orange>CENTCOM:</font>[ADMIN_FULLMONTY(Sender)] [ADMIN_CENTCOM_REPLY(Sender)]:</b> [msg]</span>"
|
||||
to_chat(GLOB.admins, msg)
|
||||
for(var/obj/machinery/computer/communications/C in GLOB.machines)
|
||||
C.overrideCooldown()
|
||||
|
||||
/proc/Syndicate_announce(text , mob/Sender)
|
||||
var/msg = copytext(sanitize(text), 1, MAX_MESSAGE_LEN)
|
||||
msg = "<span class='adminnotice'><b><font color=crimson>SYNDICATE:</font>[ADMIN_FULLMONTY(Sender)] [ADMIN_SYNDICATE_REPLY(Sender)]:</b> [msg]</span>"
|
||||
to_chat(GLOB.admins, msg)
|
||||
for(var/obj/machinery/computer/communications/C in GLOB.machines)
|
||||
C.overrideCooldown()
|
||||
|
||||
/proc/Nuke_request(text , mob/Sender)
|
||||
var/msg = copytext(sanitize(text), 1, MAX_MESSAGE_LEN)
|
||||
msg = "<span class='adminnotice'><b><font color=orange>NUKE CODE REQUEST:</font>[ADMIN_FULLMONTY(Sender)] [ADMIN_CENTCOM_REPLY(Sender)] [ADMIN_SET_SD_CODE]:</b> [msg]</span>"
|
||||
to_chat(GLOB.admins, msg)
|
||||
for(var/obj/machinery/computer/communications/C in GLOB.machines)
|
||||
C.overrideCooldown()
|
||||
/mob/verb/pray(msg as text)
|
||||
set category = "IC"
|
||||
set name = "Pray"
|
||||
|
||||
if(GLOB.say_disabled) //This is here to try to identify lag problems
|
||||
to_chat(usr, "<span class='danger'>Speech is currently admin-disabled.</span>")
|
||||
return
|
||||
|
||||
msg = copytext(sanitize(msg), 1, MAX_MESSAGE_LEN)
|
||||
if(!msg)
|
||||
return
|
||||
log_prayer("[src.key]/([src.name]): [msg]")
|
||||
if(usr.client)
|
||||
if(usr.client.prefs.muted & MUTE_PRAY)
|
||||
to_chat(usr, "<span class='danger'>You cannot pray (muted).</span>")
|
||||
return
|
||||
if(src.client.handle_spam_prevention(msg,MUTE_PRAY))
|
||||
return
|
||||
|
||||
var/mutable_appearance/cross = mutable_appearance('icons/obj/storage.dmi', "bible")
|
||||
var/font_color = "purple"
|
||||
var/prayer_type = "PRAYER"
|
||||
var/deity
|
||||
if(usr.job == "Chaplain")
|
||||
cross.icon_state = "kingyellow"
|
||||
font_color = "blue"
|
||||
prayer_type = "CHAPLAIN PRAYER"
|
||||
if(GLOB.deity)
|
||||
deity = GLOB.deity
|
||||
else if(iscultist(usr))
|
||||
cross.icon_state = "tome"
|
||||
font_color = "red"
|
||||
prayer_type = "CULTIST PRAYER"
|
||||
deity = "Nar'Sie"
|
||||
else if(isliving(usr))
|
||||
var/mob/living/L = usr
|
||||
if(HAS_TRAIT(L, TRAIT_SPIRITUAL))
|
||||
cross.icon_state = "holylight"
|
||||
font_color = "blue"
|
||||
prayer_type = "SPIRITUAL PRAYER"
|
||||
|
||||
var/msg_tmp = msg
|
||||
msg = "<span class='adminnotice'>[icon2html(cross, GLOB.admins)]<b><font color=[font_color]>[prayer_type][deity ? " (to [deity])" : ""]: </font>[ADMIN_FULLMONTY(src)] [ADMIN_SC(src)]:</b> <span class='linkify'>[msg]</span></span>"
|
||||
|
||||
for(var/client/C in GLOB.admins)
|
||||
if(C.prefs.chat_toggles & CHAT_PRAYER)
|
||||
to_chat(C, msg)
|
||||
if(C.prefs.toggles & SOUND_PRAYERS)
|
||||
if(usr.job == "Chaplain")
|
||||
SEND_SOUND(C, sound('sound/effects/pray.ogg'))
|
||||
to_chat(usr, "<span class='info'>You pray to the gods: \"[msg_tmp]\"</span>")
|
||||
|
||||
SSblackbox.record_feedback("tally", "admin_verb", 1, "Prayer") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
//log_admin("HELP: [key_name(src)]: [msg]")
|
||||
|
||||
/proc/CentCom_announce(text , mob/Sender)
|
||||
var/msg = copytext(sanitize(text), 1, MAX_MESSAGE_LEN)
|
||||
msg = "<span class='adminnotice'><b><font color=orange>CENTCOM:</font>[ADMIN_FULLMONTY(Sender)] [ADMIN_CENTCOM_REPLY(Sender)]:</b> [msg]</span>"
|
||||
to_chat(GLOB.admins, msg)
|
||||
for(var/obj/machinery/computer/communications/C in GLOB.machines)
|
||||
C.overrideCooldown()
|
||||
|
||||
/proc/Syndicate_announce(text , mob/Sender)
|
||||
var/msg = copytext(sanitize(text), 1, MAX_MESSAGE_LEN)
|
||||
msg = "<span class='adminnotice'><b><font color=crimson>SYNDICATE:</font>[ADMIN_FULLMONTY(Sender)] [ADMIN_SYNDICATE_REPLY(Sender)]:</b> [msg]</span>"
|
||||
to_chat(GLOB.admins, msg)
|
||||
for(var/obj/machinery/computer/communications/C in GLOB.machines)
|
||||
C.overrideCooldown()
|
||||
|
||||
/proc/Nuke_request(text , mob/Sender)
|
||||
var/msg = copytext(sanitize(text), 1, MAX_MESSAGE_LEN)
|
||||
msg = "<span class='adminnotice'><b><font color=orange>NUKE CODE REQUEST:</font>[ADMIN_FULLMONTY(Sender)] [ADMIN_CENTCOM_REPLY(Sender)] [ADMIN_SET_SD_CODE]:</b> [msg]</span>"
|
||||
to_chat(GLOB.admins, msg)
|
||||
for(var/obj/machinery/computer/communications/C in GLOB.machines)
|
||||
C.overrideCooldown()
|
||||
|
||||
+1393
-1393
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,23 @@
|
||||
#define WHITELISTFILE "[global.config.directory]/whitelist.txt"
|
||||
|
||||
GLOBAL_LIST(whitelist)
|
||||
GLOBAL_PROTECT(whitelist)
|
||||
|
||||
/proc/load_whitelist()
|
||||
GLOB.whitelist = list()
|
||||
for(var/line in world.file2list(WHITELISTFILE))
|
||||
if(!line)
|
||||
continue
|
||||
if(findtextEx(line,"#",1,2))
|
||||
continue
|
||||
GLOB.whitelist += ckey(line)
|
||||
|
||||
if(!GLOB.whitelist.len)
|
||||
GLOB.whitelist = null
|
||||
|
||||
/proc/check_whitelist(var/ckey)
|
||||
if(!GLOB.whitelist)
|
||||
return FALSE
|
||||
. = (ckey in GLOB.whitelist)
|
||||
|
||||
#undef WHITELISTFILE
|
||||
#define WHITELISTFILE "[global.config.directory]/whitelist.txt"
|
||||
|
||||
GLOBAL_LIST(whitelist)
|
||||
GLOBAL_PROTECT(whitelist)
|
||||
|
||||
/proc/load_whitelist()
|
||||
GLOB.whitelist = list()
|
||||
for(var/line in world.file2list(WHITELISTFILE))
|
||||
if(!line)
|
||||
continue
|
||||
if(findtextEx(line,"#",1,2))
|
||||
continue
|
||||
GLOB.whitelist += ckey(line)
|
||||
|
||||
if(!GLOB.whitelist.len)
|
||||
GLOB.whitelist = null
|
||||
|
||||
/proc/check_whitelist(var/ckey)
|
||||
if(!GLOB.whitelist)
|
||||
return FALSE
|
||||
. = (ckey in GLOB.whitelist)
|
||||
|
||||
#undef WHITELISTFILE
|
||||
|
||||
@@ -1,280 +1,282 @@
|
||||
/obj/item/antag_spawner
|
||||
throw_speed = 1
|
||||
throw_range = 5
|
||||
w_class = WEIGHT_CLASS_TINY
|
||||
var/used = FALSE
|
||||
|
||||
/obj/item/antag_spawner/proc/spawn_antag(client/C, turf/T, kind = "", datum/mind/user)
|
||||
return
|
||||
|
||||
/obj/item/antag_spawner/proc/equip_antag(mob/target)
|
||||
return
|
||||
|
||||
|
||||
///////////WIZARD
|
||||
|
||||
/obj/item/antag_spawner/contract
|
||||
name = "contract"
|
||||
desc = "A magic contract previously signed by an apprentice. In exchange for instruction in the magical arts, they are bound to answer your call for aid."
|
||||
icon = 'icons/obj/wizard.dmi'
|
||||
icon_state ="scroll2"
|
||||
|
||||
/obj/item/antag_spawner/contract/attack_self(mob/user)
|
||||
user.set_machine(src)
|
||||
var/dat
|
||||
if(used)
|
||||
dat = "<B>You have already summoned your apprentice.</B><BR>"
|
||||
else
|
||||
dat = "<B>Contract of Apprenticeship:</B><BR>"
|
||||
dat += "<I>Using this contract, you may summon an apprentice to aid you on your mission.</I><BR>"
|
||||
dat += "<I>If you are unable to establish contact with your apprentice, you can feed the contract back to the spellbook to refund your points.</I><BR>"
|
||||
dat += "<B>Which school of magic is your apprentice studying?:</B><BR>"
|
||||
dat += "<A href='byond://?src=[REF(src)];school=[APPRENTICE_DESTRUCTION]'>Destruction</A><BR>"
|
||||
dat += "<I>Your apprentice is skilled in offensive magic. They know Magic Missile and Fireball.</I><BR>"
|
||||
dat += "<A href='byond://?src=[REF(src)];school=[APPRENTICE_BLUESPACE]'>Bluespace Manipulation</A><BR>"
|
||||
dat += "<I>Your apprentice is able to defy physics, melting through solid objects and travelling great distances in the blink of an eye. They know Teleport and Ethereal Jaunt.</I><BR>"
|
||||
dat += "<A href='byond://?src=[REF(src)];school=[APPRENTICE_HEALING]'>Healing</A><BR>"
|
||||
dat += "<I>Your apprentice is training to cast spells that will aid your survival. They know Forcewall and Charge and come with a Staff of Healing.</I><BR>"
|
||||
dat += "<A href='byond://?src=[REF(src)];school=[APPRENTICE_ROBELESS]'>Robeless</A><BR>"
|
||||
dat += "<I>Your apprentice is training to cast spells without their robes. They know Knock and Mindswap.</I><BR>"
|
||||
user << browse(dat, "window=radio")
|
||||
onclose(user, "radio")
|
||||
return
|
||||
|
||||
/obj/item/antag_spawner/contract/Topic(href, href_list)
|
||||
..()
|
||||
var/mob/living/carbon/human/H = usr
|
||||
|
||||
if(H.stat || H.restrained())
|
||||
return
|
||||
if(!ishuman(H))
|
||||
return 1
|
||||
|
||||
if(loc == H || (in_range(src, H) && isturf(loc)))
|
||||
H.set_machine(src)
|
||||
if(href_list["school"])
|
||||
if(used)
|
||||
to_chat(H, "You already used this contract!")
|
||||
return
|
||||
var/list/candidates = pollCandidatesForMob("Do you want to play as a wizard's [href_list["school"]] apprentice?", ROLE_WIZARD, null, ROLE_WIZARD, 150, src, ignore_category = POLL_IGNORE_WIZARD)
|
||||
if(LAZYLEN(candidates))
|
||||
if(QDELETED(src))
|
||||
return
|
||||
if(used)
|
||||
to_chat(H, "You already used this contract!")
|
||||
return
|
||||
used = TRUE
|
||||
var/mob/dead/observer/C = pick(candidates)
|
||||
spawn_antag(C.client, get_turf(src), href_list["school"],H.mind)
|
||||
else
|
||||
to_chat(H, "Unable to reach your apprentice! You can either attack the spellbook with the contract to refund your points, or wait and try again later.")
|
||||
|
||||
/obj/item/antag_spawner/contract/spawn_antag(client/C, turf/T, kind ,datum/mind/user)
|
||||
new /obj/effect/particle_effect/smoke(T)
|
||||
var/mob/living/carbon/human/M = new/mob/living/carbon/human(T)
|
||||
C.prefs.copy_to(M)
|
||||
M.key = C.key
|
||||
var/datum/mind/app_mind = M.mind
|
||||
|
||||
var/datum/antagonist/wizard/apprentice/app = new()
|
||||
app.master = user
|
||||
app.school = kind
|
||||
|
||||
var/datum/antagonist/wizard/master_wizard = user.has_antag_datum(/datum/antagonist/wizard)
|
||||
if(master_wizard)
|
||||
if(!master_wizard.wiz_team)
|
||||
master_wizard.create_wiz_team()
|
||||
app.wiz_team = master_wizard.wiz_team
|
||||
master_wizard.wiz_team.add_member(app_mind)
|
||||
app_mind.add_antag_datum(app)
|
||||
//TODO Kill these if possible
|
||||
app_mind.assigned_role = "Apprentice"
|
||||
app_mind.special_role = "apprentice"
|
||||
//
|
||||
SEND_SOUND(M, sound('sound/effects/magic.ogg'))
|
||||
|
||||
///////////BORGS AND OPERATIVES
|
||||
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops
|
||||
name = "syndicate operative teleporter"
|
||||
desc = "A single-use teleporter designed to quickly reinforce operatives in the field."
|
||||
icon = 'icons/obj/device.dmi'
|
||||
icon_state = "locator"
|
||||
var/borg_to_spawn
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/proc/check_usability(mob/user)
|
||||
if(used)
|
||||
to_chat(user, "<span class='warning'>[src] is out of power!</span>")
|
||||
return FALSE
|
||||
if(!user.mind.has_antag_datum(/datum/antagonist/nukeop,TRUE))
|
||||
to_chat(user, "<span class='danger'>AUTHENTICATION FAILURE. ACCESS DENIED.</span>")
|
||||
return FALSE
|
||||
if(!user.onSyndieBase())
|
||||
to_chat(user, "<span class='warning'>[src] is out of range! It can only be used at your base!</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/attack_self(mob/user)
|
||||
if(!(check_usability(user)))
|
||||
return
|
||||
|
||||
to_chat(user, "<span class='notice'>You activate [src] and wait for confirmation.</span>")
|
||||
var/list/nuke_candidates = pollGhostCandidates("Do you want to play as a syndicate [borg_to_spawn ? "[lowertext(borg_to_spawn)] cyborg":"operative"]?", ROLE_OPERATIVE, null, ROLE_OPERATIVE, 150, POLL_IGNORE_SYNDICATE)
|
||||
if(LAZYLEN(nuke_candidates))
|
||||
if(QDELETED(src) || !check_usability(user))
|
||||
return
|
||||
used = TRUE
|
||||
var/mob/dead/observer/G = pick(nuke_candidates)
|
||||
spawn_antag(G.client, get_turf(src), "syndieborg", user.mind)
|
||||
do_sparks(4, TRUE, src)
|
||||
qdel(src)
|
||||
else
|
||||
to_chat(user, "<span class='warning'>Unable to connect to Syndicate command. Please wait and try again later or use the teleporter on your uplink to get your points refunded.</span>")
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/spawn_antag(client/C, turf/T, kind, datum/mind/user)
|
||||
var/mob/living/carbon/human/M = new/mob/living/carbon/human(T)
|
||||
C.prefs.copy_to(M)
|
||||
M.key = C.key
|
||||
|
||||
var/datum/antagonist/nukeop/new_op = new()
|
||||
new_op.send_to_spawnpoint = FALSE
|
||||
new_op.nukeop_outfit = /datum/outfit/syndicate/no_crystals
|
||||
|
||||
var/datum/antagonist/nukeop/creator_op = user.has_antag_datum(/datum/antagonist/nukeop,TRUE)
|
||||
if(creator_op)
|
||||
M.mind.add_antag_datum(new_op,creator_op.nuke_team)
|
||||
M.mind.special_role = "Nuclear Operative"
|
||||
|
||||
//////CLOWN OP
|
||||
/obj/item/antag_spawner/nuke_ops/clown
|
||||
name = "clown operative teleporter"
|
||||
desc = "A single-use teleporter designed to quickly reinforce clown operatives in the field."
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/clown/spawn_antag(client/C, turf/T, kind, datum/mind/user)
|
||||
var/mob/living/carbon/human/M = new/mob/living/carbon/human(T)
|
||||
C.prefs.copy_to(M)
|
||||
M.key = C.key
|
||||
|
||||
var/datum/antagonist/nukeop/clownop/new_op = new /datum/antagonist/nukeop/clownop()
|
||||
new_op.send_to_spawnpoint = FALSE
|
||||
new_op.nukeop_outfit = /datum/outfit/syndicate/clownop/no_crystals
|
||||
|
||||
var/datum/antagonist/nukeop/creator_op = user.has_antag_datum(/datum/antagonist/nukeop/clownop,TRUE)
|
||||
if(creator_op)
|
||||
M.mind.add_antag_datum(new_op, creator_op.nuke_team)
|
||||
M.mind.special_role = "Clown Operative"
|
||||
|
||||
|
||||
//////SYNDICATE BORG
|
||||
/obj/item/antag_spawner/nuke_ops/borg_tele
|
||||
name = "syndicate cyborg teleporter"
|
||||
desc = "A single-use teleporter designed to quickly reinforce operatives in the field."
|
||||
icon = 'icons/obj/device.dmi'
|
||||
icon_state = "locator"
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/borg_tele/assault
|
||||
name = "syndicate assault cyborg teleporter"
|
||||
borg_to_spawn = "Assault"
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/borg_tele/medical
|
||||
name = "syndicate medical teleporter"
|
||||
borg_to_spawn = "Medical"
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/borg_tele/saboteur
|
||||
name = "syndicate saboteur teleporter"
|
||||
borg_to_spawn = "Saboteur"
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/borg_tele/spawn_antag(client/C, turf/T, kind, datum/mind/user)
|
||||
var/mob/living/silicon/robot/R
|
||||
var/datum/antagonist/nukeop/creator_op = user.has_antag_datum(/datum/antagonist/nukeop,TRUE)
|
||||
if(!creator_op)
|
||||
return
|
||||
|
||||
switch(borg_to_spawn)
|
||||
if("Medical")
|
||||
R = new /mob/living/silicon/robot/modules/syndicate/medical(T)
|
||||
if("Saboteur")
|
||||
R = new /mob/living/silicon/robot/modules/syndicate/saboteur(T)
|
||||
else
|
||||
R = new /mob/living/silicon/robot/modules/syndicate(T) //Assault borg by default
|
||||
|
||||
var/brainfirstname = pick(GLOB.first_names_male)
|
||||
if(prob(50))
|
||||
brainfirstname = pick(GLOB.first_names_female)
|
||||
var/brainopslastname = pick(GLOB.last_names)
|
||||
if(creator_op.nuke_team.syndicate_name) //the brain inside the syndiborg has the same last name as the other ops.
|
||||
brainopslastname = creator_op.nuke_team.syndicate_name
|
||||
var/brainopsname = "[brainfirstname] [brainopslastname]"
|
||||
|
||||
R.mmi.name = "Man-Machine Interface: [brainopsname]"
|
||||
R.mmi.brain.name = "[brainopsname]'s brain"
|
||||
R.mmi.brainmob.real_name = brainopsname
|
||||
R.mmi.brainmob.name = brainopsname
|
||||
R.real_name = R.name
|
||||
|
||||
R.key = C.key
|
||||
|
||||
var/datum/antagonist/nukeop/new_borg = new()
|
||||
new_borg.send_to_spawnpoint = FALSE
|
||||
R.mind.add_antag_datum(new_borg,creator_op.nuke_team)
|
||||
R.mind.special_role = "Syndicate Cyborg"
|
||||
|
||||
///////////SLAUGHTER DEMON
|
||||
|
||||
/obj/item/antag_spawner/slaughter_demon //Warning edgiest item in the game
|
||||
name = "vial of blood"
|
||||
desc = "A magically infused bottle of blood, distilled from countless murder victims. Used in unholy rituals to attract horrifying creatures."
|
||||
icon = 'icons/obj/wizard.dmi'
|
||||
icon_state = "vial"
|
||||
|
||||
var/shatter_msg = "<span class='notice'>You shatter the bottle, no turning back now!</span>"
|
||||
var/veil_msg = "<span class='warning'>You sense a dark presence lurking just beyond the veil...</span>"
|
||||
var/mob/living/demon_type = /mob/living/simple_animal/slaughter
|
||||
var/antag_type = /datum/antagonist/slaughter
|
||||
|
||||
|
||||
/obj/item/antag_spawner/slaughter_demon/attack_self(mob/user)
|
||||
if(!is_station_level(user.z))
|
||||
to_chat(user, "<span class='notice'>You should probably wait until you reach the station.</span>")
|
||||
return
|
||||
if(used)
|
||||
return
|
||||
var/list/candidates = pollCandidatesForMob("Do you want to play as a [initial(demon_type.name)]?", ROLE_ALIEN, null, ROLE_ALIEN, 50, src, ignore_category = POLL_IGNORE_DEMON)
|
||||
if(LAZYLEN(candidates))
|
||||
if(used || QDELETED(src))
|
||||
return
|
||||
used = TRUE
|
||||
var/mob/dead/observer/C = pick(candidates)
|
||||
spawn_antag(C.client, get_turf(src), initial(demon_type.name),user.mind)
|
||||
to_chat(user, shatter_msg)
|
||||
to_chat(user, veil_msg)
|
||||
playsound(user.loc, 'sound/effects/glassbr1.ogg', 100, 1)
|
||||
qdel(src)
|
||||
else
|
||||
to_chat(user, "<span class='notice'>You can't seem to work up the nerve to shatter the bottle. Perhaps you should try again later.</span>")
|
||||
|
||||
|
||||
/obj/item/antag_spawner/slaughter_demon/spawn_antag(client/C, turf/T, kind = "", datum/mind/user)
|
||||
var/obj/effect/dummy/phased_mob/slaughter/holder = new /obj/effect/dummy/phased_mob/slaughter(T)
|
||||
var/mob/living/simple_animal/slaughter/S = new demon_type(holder)
|
||||
S.holder = holder
|
||||
S.key = C.key
|
||||
S.mind.assigned_role = S.name
|
||||
S.mind.special_role = S.name
|
||||
S.mind.add_antag_datum(antag_type)
|
||||
to_chat(S, S.playstyle_string)
|
||||
to_chat(S, "<B>You are currently not currently in the same plane of existence as the station. \
|
||||
Ctrl+Click a blood pool to manifest.</B>")
|
||||
|
||||
/obj/item/antag_spawner/slaughter_demon/laughter
|
||||
name = "vial of tickles"
|
||||
desc = "A magically infused bottle of clown love, distilled from countless hugging attacks. Used in funny rituals to attract adorable creatures."
|
||||
icon = 'icons/obj/wizard.dmi'
|
||||
icon_state = "vial"
|
||||
color = "#FF69B4" // HOT PINK
|
||||
|
||||
veil_msg = "<span class='warning'>You sense an adorable presence lurking just beyond the veil...</span>"
|
||||
demon_type = /mob/living/simple_animal/slaughter/laughter
|
||||
antag_type = /datum/antagonist/slaughter/laughter
|
||||
/obj/item/antag_spawner
|
||||
throw_speed = 1
|
||||
throw_range = 5
|
||||
w_class = WEIGHT_CLASS_TINY
|
||||
var/used = FALSE
|
||||
|
||||
/obj/item/antag_spawner/proc/spawn_antag(client/C, turf/T, kind = "", datum/mind/user)
|
||||
return
|
||||
|
||||
/obj/item/antag_spawner/proc/equip_antag(mob/target)
|
||||
return
|
||||
|
||||
|
||||
///////////WIZARD
|
||||
|
||||
/obj/item/antag_spawner/contract
|
||||
name = "contract"
|
||||
desc = "A magic contract previously signed by an apprentice. In exchange for instruction in the magical arts, they are bound to answer your call for aid."
|
||||
icon = 'icons/obj/wizard.dmi'
|
||||
icon_state ="scroll2"
|
||||
|
||||
/obj/item/antag_spawner/contract/attack_self(mob/user)
|
||||
user.set_machine(src)
|
||||
var/dat
|
||||
if(used)
|
||||
dat = "<B>You have already summoned your apprentice.</B><BR>"
|
||||
else
|
||||
dat = "<B>Contract of Apprenticeship:</B><BR>"
|
||||
dat += "<I>Using this contract, you may summon an apprentice to aid you on your mission.</I><BR>"
|
||||
dat += "<I>If you are unable to establish contact with your apprentice, you can feed the contract back to the spellbook to refund your points.</I><BR>"
|
||||
dat += "<B>Which school of magic is your apprentice studying?:</B><BR>"
|
||||
dat += "<A href='byond://?src=[REF(src)];school=[APPRENTICE_DESTRUCTION]'>Destruction</A><BR>"
|
||||
dat += "<I>Your apprentice is skilled in offensive magic. They know Magic Missile and Fireball.</I><BR>"
|
||||
dat += "<A href='byond://?src=[REF(src)];school=[APPRENTICE_BLUESPACE]'>Bluespace Manipulation</A><BR>"
|
||||
dat += "<I>Your apprentice is able to defy physics, melting through solid objects and travelling great distances in the blink of an eye. They know Teleport and Ethereal Jaunt.</I><BR>"
|
||||
dat += "<A href='byond://?src=[REF(src)];school=[APPRENTICE_HEALING]'>Healing</A><BR>"
|
||||
dat += "<I>Your apprentice is training to cast spells that will aid your survival. They know Forcewall and Charge and come with a Staff of Healing.</I><BR>"
|
||||
dat += "<A href='byond://?src=[REF(src)];school=[APPRENTICE_ROBELESS]'>Robeless</A><BR>"
|
||||
dat += "<I>Your apprentice is training to cast spells without their robes. They know Knock and Mindswap.</I><BR>"
|
||||
dat += "<A href='byond://?src=[REF(src)];school=[APPRENTICE_MARTIAL]'>Martial Artist</a><BR>"
|
||||
dat += "<I>Your apprentice is training in ancient martial arts. They know the Plasmafist and Nuclear Fist.</I><BR>"
|
||||
user << browse(dat, "window=radio")
|
||||
onclose(user, "radio")
|
||||
return
|
||||
|
||||
/obj/item/antag_spawner/contract/Topic(href, href_list)
|
||||
..()
|
||||
var/mob/living/carbon/human/H = usr
|
||||
|
||||
if(H.stat || H.restrained())
|
||||
return
|
||||
if(!ishuman(H))
|
||||
return 1
|
||||
|
||||
if(loc == H || (in_range(src, H) && isturf(loc)))
|
||||
H.set_machine(src)
|
||||
if(href_list["school"])
|
||||
if(used)
|
||||
to_chat(H, "You already used this contract!")
|
||||
return
|
||||
var/list/candidates = pollCandidatesForMob("Do you want to play as a wizard's [href_list["school"]] apprentice?", ROLE_WIZARD, null, ROLE_WIZARD, 150, src, ignore_category = POLL_IGNORE_WIZARD)
|
||||
if(LAZYLEN(candidates))
|
||||
if(QDELETED(src))
|
||||
return
|
||||
if(used)
|
||||
to_chat(H, "You already used this contract!")
|
||||
return
|
||||
used = TRUE
|
||||
var/mob/dead/observer/C = pick(candidates)
|
||||
spawn_antag(C.client, get_turf(src), href_list["school"],H.mind)
|
||||
else
|
||||
to_chat(H, "Unable to reach your apprentice! You can either attack the spellbook with the contract to refund your points, or wait and try again later.")
|
||||
|
||||
/obj/item/antag_spawner/contract/spawn_antag(client/C, turf/T, kind ,datum/mind/user)
|
||||
new /obj/effect/particle_effect/smoke(T)
|
||||
var/mob/living/carbon/human/M = new/mob/living/carbon/human(T)
|
||||
C.prefs.copy_to(M)
|
||||
M.key = C.key
|
||||
var/datum/mind/app_mind = M.mind
|
||||
|
||||
var/datum/antagonist/wizard/apprentice/app = new()
|
||||
app.master = user
|
||||
app.school = kind
|
||||
|
||||
var/datum/antagonist/wizard/master_wizard = user.has_antag_datum(/datum/antagonist/wizard)
|
||||
if(master_wizard)
|
||||
if(!master_wizard.wiz_team)
|
||||
master_wizard.create_wiz_team()
|
||||
app.wiz_team = master_wizard.wiz_team
|
||||
master_wizard.wiz_team.add_member(app_mind)
|
||||
app_mind.add_antag_datum(app)
|
||||
//TODO Kill these if possible
|
||||
app_mind.assigned_role = "Apprentice"
|
||||
app_mind.special_role = "apprentice"
|
||||
//
|
||||
SEND_SOUND(M, sound('sound/effects/magic.ogg'))
|
||||
|
||||
///////////BORGS AND OPERATIVES
|
||||
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops
|
||||
name = "syndicate operative teleporter"
|
||||
desc = "A single-use teleporter designed to quickly reinforce operatives in the field."
|
||||
icon = 'icons/obj/device.dmi'
|
||||
icon_state = "locator"
|
||||
var/borg_to_spawn
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/proc/check_usability(mob/user)
|
||||
if(used)
|
||||
to_chat(user, "<span class='warning'>[src] is out of power!</span>")
|
||||
return FALSE
|
||||
if(!user.mind.has_antag_datum(/datum/antagonist/nukeop,TRUE))
|
||||
to_chat(user, "<span class='danger'>AUTHENTICATION FAILURE. ACCESS DENIED.</span>")
|
||||
return FALSE
|
||||
if(!user.onSyndieBase())
|
||||
to_chat(user, "<span class='warning'>[src] is out of range! It can only be used at your base!</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/attack_self(mob/user)
|
||||
if(!(check_usability(user)))
|
||||
return
|
||||
|
||||
to_chat(user, "<span class='notice'>You activate [src] and wait for confirmation.</span>")
|
||||
var/list/nuke_candidates = pollGhostCandidates("Do you want to play as a syndicate [borg_to_spawn ? "[lowertext(borg_to_spawn)] cyborg":"operative"]?", ROLE_OPERATIVE, null, ROLE_OPERATIVE, 150, POLL_IGNORE_SYNDICATE)
|
||||
if(LAZYLEN(nuke_candidates))
|
||||
if(QDELETED(src) || !check_usability(user))
|
||||
return
|
||||
used = TRUE
|
||||
var/mob/dead/observer/G = pick(nuke_candidates)
|
||||
spawn_antag(G.client, get_turf(src), "syndieborg", user.mind)
|
||||
do_sparks(4, TRUE, src)
|
||||
qdel(src)
|
||||
else
|
||||
to_chat(user, "<span class='warning'>Unable to connect to Syndicate command. Please wait and try again later or use the teleporter on your uplink to get your points refunded.</span>")
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/spawn_antag(client/C, turf/T, kind, datum/mind/user)
|
||||
var/mob/living/carbon/human/M = new/mob/living/carbon/human(T)
|
||||
C.prefs.copy_to(M)
|
||||
M.key = C.key
|
||||
|
||||
var/datum/antagonist/nukeop/new_op = new()
|
||||
new_op.send_to_spawnpoint = FALSE
|
||||
new_op.nukeop_outfit = /datum/outfit/syndicate/no_crystals
|
||||
|
||||
var/datum/antagonist/nukeop/creator_op = user.has_antag_datum(/datum/antagonist/nukeop,TRUE)
|
||||
if(creator_op)
|
||||
M.mind.add_antag_datum(new_op,creator_op.nuke_team)
|
||||
M.mind.special_role = "Nuclear Operative"
|
||||
|
||||
//////CLOWN OP
|
||||
/obj/item/antag_spawner/nuke_ops/clown
|
||||
name = "clown operative teleporter"
|
||||
desc = "A single-use teleporter designed to quickly reinforce clown operatives in the field."
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/clown/spawn_antag(client/C, turf/T, kind, datum/mind/user)
|
||||
var/mob/living/carbon/human/M = new/mob/living/carbon/human(T)
|
||||
C.prefs.copy_to(M)
|
||||
M.key = C.key
|
||||
|
||||
var/datum/antagonist/nukeop/clownop/new_op = new /datum/antagonist/nukeop/clownop()
|
||||
new_op.send_to_spawnpoint = FALSE
|
||||
new_op.nukeop_outfit = /datum/outfit/syndicate/clownop/no_crystals
|
||||
|
||||
var/datum/antagonist/nukeop/creator_op = user.has_antag_datum(/datum/antagonist/nukeop/clownop,TRUE)
|
||||
if(creator_op)
|
||||
M.mind.add_antag_datum(new_op, creator_op.nuke_team)
|
||||
M.mind.special_role = "Clown Operative"
|
||||
|
||||
|
||||
//////SYNDICATE BORG
|
||||
/obj/item/antag_spawner/nuke_ops/borg_tele
|
||||
name = "syndicate cyborg teleporter"
|
||||
desc = "A single-use teleporter designed to quickly reinforce operatives in the field."
|
||||
icon = 'icons/obj/device.dmi'
|
||||
icon_state = "locator"
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/borg_tele/assault
|
||||
name = "syndicate assault cyborg teleporter"
|
||||
borg_to_spawn = "Assault"
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/borg_tele/medical
|
||||
name = "syndicate medical teleporter"
|
||||
borg_to_spawn = "Medical"
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/borg_tele/saboteur
|
||||
name = "syndicate saboteur teleporter"
|
||||
borg_to_spawn = "Saboteur"
|
||||
|
||||
/obj/item/antag_spawner/nuke_ops/borg_tele/spawn_antag(client/C, turf/T, kind, datum/mind/user)
|
||||
var/mob/living/silicon/robot/R
|
||||
var/datum/antagonist/nukeop/creator_op = user.has_antag_datum(/datum/antagonist/nukeop,TRUE)
|
||||
if(!creator_op)
|
||||
return
|
||||
|
||||
switch(borg_to_spawn)
|
||||
if("Medical")
|
||||
R = new /mob/living/silicon/robot/modules/syndicate/medical(T)
|
||||
if("Saboteur")
|
||||
R = new /mob/living/silicon/robot/modules/syndicate/saboteur(T)
|
||||
else
|
||||
R = new /mob/living/silicon/robot/modules/syndicate(T) //Assault borg by default
|
||||
|
||||
var/brainfirstname = pick(GLOB.first_names_male)
|
||||
if(prob(50))
|
||||
brainfirstname = pick(GLOB.first_names_female)
|
||||
var/brainopslastname = pick(GLOB.last_names)
|
||||
if(creator_op.nuke_team.syndicate_name) //the brain inside the syndiborg has the same last name as the other ops.
|
||||
brainopslastname = creator_op.nuke_team.syndicate_name
|
||||
var/brainopsname = "[brainfirstname] [brainopslastname]"
|
||||
|
||||
R.mmi.name = "Man-Machine Interface: [brainopsname]"
|
||||
R.mmi.brain.name = "[brainopsname]'s brain"
|
||||
R.mmi.brainmob.real_name = brainopsname
|
||||
R.mmi.brainmob.name = brainopsname
|
||||
R.real_name = R.name
|
||||
|
||||
R.key = C.key
|
||||
|
||||
var/datum/antagonist/nukeop/new_borg = new()
|
||||
new_borg.send_to_spawnpoint = FALSE
|
||||
R.mind.add_antag_datum(new_borg,creator_op.nuke_team)
|
||||
R.mind.special_role = "Syndicate Cyborg"
|
||||
|
||||
///////////SLAUGHTER DEMON
|
||||
|
||||
/obj/item/antag_spawner/slaughter_demon //Warning edgiest item in the game
|
||||
name = "vial of blood"
|
||||
desc = "A magically infused bottle of blood, distilled from countless murder victims. Used in unholy rituals to attract horrifying creatures."
|
||||
icon = 'icons/obj/wizard.dmi'
|
||||
icon_state = "vial"
|
||||
|
||||
var/shatter_msg = "<span class='notice'>You shatter the bottle, no turning back now!</span>"
|
||||
var/veil_msg = "<span class='warning'>You sense a dark presence lurking just beyond the veil...</span>"
|
||||
var/mob/living/demon_type = /mob/living/simple_animal/slaughter
|
||||
var/antag_type = /datum/antagonist/slaughter
|
||||
|
||||
|
||||
/obj/item/antag_spawner/slaughter_demon/attack_self(mob/user)
|
||||
if(!is_station_level(user.z))
|
||||
to_chat(user, "<span class='notice'>You should probably wait until you reach the station.</span>")
|
||||
return
|
||||
if(used)
|
||||
return
|
||||
var/list/candidates = pollCandidatesForMob("Do you want to play as a [initial(demon_type.name)]?", ROLE_ALIEN, null, ROLE_ALIEN, 50, src, ignore_category = POLL_IGNORE_DEMON)
|
||||
if(LAZYLEN(candidates))
|
||||
if(used || QDELETED(src))
|
||||
return
|
||||
used = TRUE
|
||||
var/mob/dead/observer/C = pick(candidates)
|
||||
spawn_antag(C.client, get_turf(src), initial(demon_type.name),user.mind)
|
||||
to_chat(user, shatter_msg)
|
||||
to_chat(user, veil_msg)
|
||||
playsound(user.loc, 'sound/effects/glassbr1.ogg', 100, 1)
|
||||
qdel(src)
|
||||
else
|
||||
to_chat(user, "<span class='notice'>You can't seem to work up the nerve to shatter the bottle. Perhaps you should try again later.</span>")
|
||||
|
||||
|
||||
/obj/item/antag_spawner/slaughter_demon/spawn_antag(client/C, turf/T, kind = "", datum/mind/user)
|
||||
var/obj/effect/dummy/phased_mob/slaughter/holder = new /obj/effect/dummy/phased_mob/slaughter(T)
|
||||
var/mob/living/simple_animal/slaughter/S = new demon_type(holder)
|
||||
S.holder = holder
|
||||
S.key = C.key
|
||||
S.mind.assigned_role = S.name
|
||||
S.mind.special_role = S.name
|
||||
S.mind.add_antag_datum(antag_type)
|
||||
to_chat(S, S.playstyle_string)
|
||||
to_chat(S, "<B>You are currently not currently in the same plane of existence as the station. \
|
||||
Ctrl+Click a blood pool to manifest.</B>")
|
||||
|
||||
/obj/item/antag_spawner/slaughter_demon/laughter
|
||||
name = "vial of tickles"
|
||||
desc = "A magically infused bottle of clown love, distilled from countless hugging attacks. Used in funny rituals to attract adorable creatures."
|
||||
icon = 'icons/obj/wizard.dmi'
|
||||
icon_state = "vial"
|
||||
color = "#FF69B4" // HOT PINK
|
||||
|
||||
veil_msg = "<span class='warning'>You sense an adorable presence lurking just beyond the veil...</span>"
|
||||
demon_type = /mob/living/simple_animal/slaughter/laughter
|
||||
antag_type = /datum/antagonist/slaughter/laughter
|
||||
|
||||
@@ -65,6 +65,8 @@
|
||||
//Equip
|
||||
var/mob/living/carbon/human/H = owner.current
|
||||
H.set_species(/datum/species/abductor)
|
||||
var/obj/item/organ/tongue/abductor/T = H.getorganslot(ORGAN_SLOT_TONGUE)
|
||||
T.mothership = "[team.name]"
|
||||
|
||||
H.real_name = "[team.name] [sub_role]"
|
||||
H.equipOutfit(outfit)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
actions_types = list(/datum/action/item_action/hands_free/activate)
|
||||
allowed = list(
|
||||
/obj/item/abductor,
|
||||
/obj/item/abductor_baton,
|
||||
/obj/item/abductor/baton,
|
||||
/obj/item/melee/baton,
|
||||
/obj/item/gun/energy,
|
||||
/obj/item/restraints/handcuffs
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
/obj/item/clothing/suit/armor/abductor/vest/item_action_slot_check(slot, mob/user, datum/action/A)
|
||||
if(slot == SLOT_WEAR_SUIT) //we only give the mob the ability to activate the vest if he's actually wearing it.
|
||||
return 1
|
||||
return TRUE
|
||||
|
||||
/obj/item/clothing/suit/armor/abductor/vest/proc/SetDisguise(datum/icon_snapshot/entry)
|
||||
disguise = entry
|
||||
@@ -89,11 +89,9 @@
|
||||
|
||||
/obj/item/clothing/suit/armor/abductor/vest/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK)
|
||||
DeactivateStealth()
|
||||
return 0
|
||||
|
||||
/obj/item/clothing/suit/armor/abductor/vest/IsReflect()
|
||||
DeactivateStealth()
|
||||
return 0
|
||||
|
||||
/obj/item/clothing/suit/armor/abductor/vest/ui_action_click()
|
||||
switch(mode)
|
||||
@@ -111,7 +109,10 @@
|
||||
to_chat(loc, "<span class='warning'>Combat injection is still recharging.</span>")
|
||||
return
|
||||
var/mob/living/carbon/human/M = loc
|
||||
M.do_adrenaline(150, FALSE, 0, 0, TRUE, list("inaprovaline" = 3, "synaptizine" = 10, "omnizine" = 10), "<span class='boldnotice'>You feel a sudden surge of energy!</span>")
|
||||
M.adjustStaminaLoss(-75)
|
||||
M.SetUnconscious(0)
|
||||
M.SetStun(0)
|
||||
M.SetKnockdown(0)
|
||||
combat_cooldown = 0
|
||||
START_PROCESSING(SSobj, src)
|
||||
|
||||
@@ -131,9 +132,11 @@
|
||||
|
||||
/obj/item/abductor
|
||||
icon = 'icons/obj/abductor.dmi'
|
||||
lefthand_file = 'icons/mob/inhands/antag/abductor_lefthand.dmi'
|
||||
righthand_file = 'icons/mob/inhands/antag/abductor_righthand.dmi'
|
||||
|
||||
/obj/item/abductor/proc/AbductorCheck(mob/user)
|
||||
if(HAS_TRAIT(user, TRAIT_ABDUCTOR_TRAINING))
|
||||
if (HAS_TRAIT(user, TRAIT_ABDUCTOR_TRAINING))
|
||||
return TRUE
|
||||
if (istype(user) && user.mind && HAS_TRAIT(user.mind, TRAIT_ABDUCTOR_TRAINING))
|
||||
return TRUE
|
||||
@@ -158,8 +161,6 @@
|
||||
desc = "A dual-mode tool for retrieving specimens and scanning appearances. Scanning can be done through cameras."
|
||||
icon_state = "gizmo_scan"
|
||||
item_state = "silencer"
|
||||
lefthand_file = 'icons/mob/inhands/antag/abductor_lefthand.dmi'
|
||||
righthand_file = 'icons/mob/inhands/antag/abductor_righthand.dmi'
|
||||
var/mode = GIZMO_SCAN
|
||||
var/mob/living/marked = null
|
||||
var/obj/machinery/abductor/console/console
|
||||
@@ -218,12 +219,9 @@
|
||||
if(marked == target)
|
||||
to_chat(user, "<span class='warning'>This specimen is already marked!</span>")
|
||||
return
|
||||
if(ishuman(target))
|
||||
if(isabductor(target))
|
||||
marked = target
|
||||
to_chat(user, "<span class='notice'>You mark [target] for future retrieval.</span>")
|
||||
else
|
||||
prepare(target,user)
|
||||
if(isabductor(target) || iscow(target))
|
||||
marked = target
|
||||
to_chat(user, "<span class='notice'>You mark [target] for future retrieval.</span>")
|
||||
else
|
||||
prepare(target,user)
|
||||
|
||||
@@ -247,8 +245,6 @@
|
||||
desc = "A compact device used to shut down communications equipment."
|
||||
icon_state = "silencer"
|
||||
item_state = "gizmo"
|
||||
lefthand_file = 'icons/mob/inhands/antag/abductor_lefthand.dmi'
|
||||
righthand_file = 'icons/mob/inhands/antag/abductor_righthand.dmi'
|
||||
|
||||
/obj/item/abductor/silencer/attack(mob/living/M, mob/user)
|
||||
if(!AbductorCheck(user))
|
||||
@@ -292,8 +288,6 @@
|
||||
or to send a command to a test subject with a charged gland."
|
||||
icon_state = "mind_device_message"
|
||||
item_state = "silencer"
|
||||
lefthand_file = 'icons/mob/inhands/antag/abductor_lefthand.dmi'
|
||||
righthand_file = 'icons/mob/inhands/antag/abductor_righthand.dmi'
|
||||
var/mode = MIND_DEVICE_MESSAGE
|
||||
|
||||
/obj/item/abductor/mind_device/attack_self(mob/user)
|
||||
@@ -389,6 +383,17 @@
|
||||
item_state = "alienpistol"
|
||||
trigger_guard = TRIGGER_GUARD_ALLOW_ALL
|
||||
|
||||
/obj/item/gun/energy/shrink_ray
|
||||
name = "shrink ray blaster"
|
||||
desc = "This is a piece of frightening alien tech that enhances the magnetic pull of atoms in a localized space to temporarily make an object shrink. \
|
||||
That or it's just space magic. Either way, it shrinks stuff."
|
||||
ammo_type = list(/obj/item/ammo_casing/energy/shrink)
|
||||
item_state = "shrink_ray"
|
||||
icon_state = "shrink_ray"
|
||||
fire_delay = 30
|
||||
selfcharge = 1//shot costs 200 energy, has a max capacity of 1000 for 5 shots. self charge returns 25 energy every couple ticks, so about 1 shot charged every 12~ seconds
|
||||
trigger_guard = TRIGGER_GUARD_ALLOW_ALL// variable-size trigger, get it? (abductors need this to be set so the gun is usable for them)
|
||||
|
||||
/obj/item/paper/guides/antag/abductor
|
||||
name = "Dissection Guide"
|
||||
icon_state = "alienpaper_words"
|
||||
@@ -422,21 +427,18 @@
|
||||
#define BATON_PROBE 3
|
||||
#define BATON_MODES 4
|
||||
|
||||
/obj/item/abductor_baton
|
||||
/obj/item/abductor/baton
|
||||
name = "advanced baton"
|
||||
desc = "A quad-mode baton used for incapacitation and restraining of specimens."
|
||||
var/mode = BATON_STUN
|
||||
icon = 'icons/obj/abductor.dmi'
|
||||
icon_state = "wonderprodStun"
|
||||
item_state = "wonderprod"
|
||||
lefthand_file = 'icons/mob/inhands/antag/abductor_lefthand.dmi'
|
||||
righthand_file = 'icons/mob/inhands/antag/abductor_righthand.dmi'
|
||||
slot_flags = ITEM_SLOT_BELT
|
||||
force = 7
|
||||
w_class = WEIGHT_CLASS_NORMAL
|
||||
actions_types = list(/datum/action/item_action/toggle_mode)
|
||||
|
||||
/obj/item/abductor_baton/proc/toggle(mob/living/user=usr)
|
||||
/obj/item/abductor/baton/proc/toggle(mob/living/user=usr)
|
||||
mode = (mode+1)%BATON_MODES
|
||||
var/txt
|
||||
switch(mode)
|
||||
@@ -452,7 +454,7 @@
|
||||
to_chat(usr, "<span class='notice'>You switch the baton to [txt] mode.</span>")
|
||||
update_icon()
|
||||
|
||||
/obj/item/abductor_baton/update_icon()
|
||||
/obj/item/abductor/baton/update_icon()
|
||||
switch(mode)
|
||||
if(BATON_STUN)
|
||||
icon_state = "wonderprodStun"
|
||||
@@ -467,8 +469,8 @@
|
||||
icon_state = "wonderprodProbe"
|
||||
item_state = "wonderprodProbe"
|
||||
|
||||
/obj/item/abductor_baton/attack(mob/target, mob/living/user)
|
||||
if(!isabductor(user))
|
||||
/obj/item/abductor/baton/attack(mob/target, mob/living/user)
|
||||
if(!AbductorCheck(user))
|
||||
return
|
||||
|
||||
if(iscyborg(target))
|
||||
@@ -482,11 +484,9 @@
|
||||
|
||||
user.do_attack_animation(L)
|
||||
|
||||
if(ishuman(L))
|
||||
var/mob/living/carbon/human/H = L
|
||||
if(H.check_shields(src, 0, "[user]'s [name]", MELEE_ATTACK))
|
||||
playsound(L, 'sound/weapons/genhit.ogg', 50, 1)
|
||||
return 0
|
||||
if(L.check_shields(src, 0, "[user]'s [name]", MELEE_ATTACK))
|
||||
playsound(L, 'sound/weapons/genhit.ogg', 50, TRUE)
|
||||
return FALSE
|
||||
|
||||
switch (mode)
|
||||
if(BATON_STUN)
|
||||
@@ -498,10 +498,10 @@
|
||||
if(BATON_PROBE)
|
||||
ProbeAttack(L,user)
|
||||
|
||||
/obj/item/abductor_baton/attack_self(mob/living/user)
|
||||
/obj/item/abductor/baton/attack_self(mob/living/user)
|
||||
toggle(user)
|
||||
|
||||
/obj/item/abductor_baton/proc/StunAttack(mob/living/L,mob/living/user)
|
||||
/obj/item/abductor/baton/proc/StunAttack(mob/living/L,mob/living/user)
|
||||
|
||||
L.lastattacker = user.real_name
|
||||
L.lastattackerckey = user.ckey
|
||||
@@ -513,7 +513,7 @@
|
||||
|
||||
L.visible_message("<span class='danger'>[user] has stunned [L] with [src]!</span>", \
|
||||
"<span class='userdanger'>[user] has stunned you with [src]!</span>")
|
||||
playsound(loc, 'sound/weapons/egloves.ogg', 50, 1, -1)
|
||||
playsound(src, 'sound/weapons/egloves.ogg', 50, TRUE, -1)
|
||||
|
||||
if(ishuman(L))
|
||||
var/mob/living/carbon/human/H = L
|
||||
@@ -521,7 +521,7 @@
|
||||
|
||||
log_combat(user, L, "stunned")
|
||||
|
||||
/obj/item/abductor_baton/proc/SleepAttack(mob/living/L,mob/living/user)
|
||||
/obj/item/abductor/baton/proc/SleepAttack(mob/living/L,mob/living/user)
|
||||
if(L.incapacitated(TRUE, TRUE))
|
||||
if(L.anti_magic_check(FALSE, FALSE, TRUE, 0))
|
||||
to_chat(user, "<span class='warning'>The specimen's tinfoil protection is interfering with the sleep inducement!</span>")
|
||||
@@ -531,7 +531,7 @@
|
||||
return
|
||||
L.visible_message("<span class='danger'>[user] has induced sleep in [L] with [src]!</span>", \
|
||||
"<span class='userdanger'>You suddenly feel very drowsy!</span>")
|
||||
playsound(loc, 'sound/weapons/egloves.ogg', 50, 1, -1)
|
||||
playsound(src, 'sound/weapons/egloves.ogg', 50, TRUE, -1)
|
||||
L.Sleeping(1200)
|
||||
log_combat(user, L, "put to sleep")
|
||||
else
|
||||
@@ -545,13 +545,13 @@
|
||||
L.visible_message("<span class='danger'>[user] tried to induce sleep in [L] with [src]!</span>", \
|
||||
"<span class='userdanger'>You suddenly feel drowsy!</span>")
|
||||
|
||||
/obj/item/abductor_baton/proc/CuffAttack(mob/living/L,mob/living/user)
|
||||
/obj/item/abductor/baton/proc/CuffAttack(mob/living/L,mob/living/user)
|
||||
if(!iscarbon(L))
|
||||
return
|
||||
var/mob/living/carbon/C = L
|
||||
if(!C.handcuffed)
|
||||
if(C.get_num_arms(FALSE) >= 2 || C.get_arm_ignore())
|
||||
playsound(loc, 'sound/weapons/cablecuff.ogg', 30, 1, -2)
|
||||
playsound(src, 'sound/weapons/cablecuff.ogg', 30, TRUE, -2)
|
||||
C.visible_message("<span class='danger'>[user] begins restraining [C] with [src]!</span>", \
|
||||
"<span class='userdanger'>[user] begins shaping an energy field around your hands!</span>")
|
||||
if(do_mob(user, C, 30) && (C.get_num_arms(FALSE) >= 2 || C.get_arm_ignore()))
|
||||
@@ -565,7 +565,7 @@
|
||||
else
|
||||
to_chat(user, "<span class='warning'>[C] doesn't have two hands...</span>")
|
||||
|
||||
/obj/item/abductor_baton/proc/ProbeAttack(mob/living/L,mob/living/user)
|
||||
/obj/item/abductor/baton/proc/ProbeAttack(mob/living/L,mob/living/user)
|
||||
L.visible_message("<span class='danger'>[user] probes [L] with [src]!</span>", \
|
||||
"<span class='userdanger'>[user] probes you!</span>")
|
||||
|
||||
@@ -610,7 +610,7 @@
|
||||
S.start()
|
||||
. = ..()
|
||||
|
||||
/obj/item/abductor_baton/examine(mob/user)
|
||||
/obj/item/abductor/baton/examine(mob/user)
|
||||
. = ..()
|
||||
switch(mode)
|
||||
if(BATON_STUN)
|
||||
@@ -639,10 +639,44 @@
|
||||
AddComponent(/datum/component/wearertargeting/earprotection, list(SLOT_EARS))
|
||||
|
||||
/obj/item/radio/headset/abductor/attackby(obj/item/W, mob/user, params)
|
||||
if(istype(W, /obj/item/screwdriver))
|
||||
if(W.tool_behaviour == TOOL_SCREWDRIVER)
|
||||
return // Stops humans from disassembling abductor headsets.
|
||||
return ..()
|
||||
|
||||
/obj/item/abductor_machine_beacon
|
||||
name = "machine beacon"
|
||||
desc = "A beacon designed to instantly tele-construct abductor machinery."
|
||||
icon = 'icons/obj/abductor.dmi'
|
||||
icon_state = "beacon"
|
||||
w_class = WEIGHT_CLASS_TINY
|
||||
var/obj/machinery/spawned_machine
|
||||
|
||||
/obj/item/abductor_machine_beacon/attack_self(mob/user)
|
||||
..()
|
||||
user.visible_message("<span class='notice'>[user] places down [src] and activates it.</span>", "<span class='notice'>You place down [src] and activate it.</span>")
|
||||
user.dropItemToGround(src)
|
||||
playsound(src, 'sound/machines/terminal_alert.ogg', 50)
|
||||
addtimer(CALLBACK(src, .proc/try_spawn_machine), 30)
|
||||
|
||||
/obj/item/abductor_machine_beacon/proc/try_spawn_machine()
|
||||
var/viable = FALSE
|
||||
if(isfloorturf(loc))
|
||||
var/turf/T = loc
|
||||
viable = TRUE
|
||||
for(var/obj/thing in T.contents)
|
||||
if(thing.density || ismachinery(thing) || isstructure(thing))
|
||||
viable = FALSE
|
||||
if(viable)
|
||||
playsound(src, 'sound/effects/phasein.ogg', 50, TRUE)
|
||||
var/new_machine = new spawned_machine(loc)
|
||||
visible_message("<span class='notice'>[new_machine] warps on top of the beacon!</span>")
|
||||
qdel(src)
|
||||
else
|
||||
playsound(src, 'sound/machines/buzz-two.ogg', 50)
|
||||
|
||||
/obj/item/abductor_machine_beacon/chem_dispenser
|
||||
name = "beacon - Reagent Synthesizer"
|
||||
spawned_machine = /obj/machinery/chem_dispenser/abductor
|
||||
|
||||
/obj/item/scalpel/alien
|
||||
name = "alien scalpel"
|
||||
@@ -706,11 +740,11 @@
|
||||
framestackamount = 1
|
||||
|
||||
/obj/structure/table_frame/abductor/attackby(obj/item/I, mob/user, params)
|
||||
if(istype(I, /obj/item/wrench))
|
||||
if(I.tool_behaviour == TOOL_WRENCH)
|
||||
to_chat(user, "<span class='notice'>You start disassembling [src]...</span>")
|
||||
I.play_tool_sound(src)
|
||||
if(I.use_tool(src, user, 30))
|
||||
playsound(src.loc, 'sound/items/deconstruct.ogg', 50, 1)
|
||||
playsound(src, 'sound/items/deconstruct.ogg', 50, TRUE)
|
||||
for(var/i = 1, i <= framestackamount, i++)
|
||||
new framestack(get_turf(src))
|
||||
qdel(src)
|
||||
@@ -760,9 +794,8 @@
|
||||
icon = 'icons/obj/abductor.dmi'
|
||||
icon_state = "bed"
|
||||
can_buckle = 1
|
||||
buckle_lying = 1
|
||||
|
||||
var/static/list/injected_reagents = list("corazone")
|
||||
var/static/list/injected_reagents = list(/datum/reagent/medicine/corazone)
|
||||
|
||||
/obj/structure/table/optable/abductor/Crossed(atom/movable/AM)
|
||||
. = ..()
|
||||
@@ -798,3 +831,11 @@
|
||||
airlock_type = /obj/machinery/door/airlock/abductor
|
||||
material_type = /obj/item/stack/sheet/mineral/abductor
|
||||
noglass = TRUE
|
||||
|
||||
/obj/item/clothing/under/abductor
|
||||
desc = "The most advanced form of jumpsuit known to reality, looks uncomfortable."
|
||||
name = "alien jumpsuit"
|
||||
icon_state = "abductor"
|
||||
item_state = "bl_suit"
|
||||
armor = list(melee = 0, bullet = 0, laser = 0, energy = 0, bomb = 10, bio = 10, rad = 0, fire = 0, acid = 0)
|
||||
can_adjust = 0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/datum/outfit/abductor
|
||||
name = "Abductor Basic"
|
||||
uniform = /obj/item/clothing/under/color/grey //they're greys gettit
|
||||
uniform = /obj/item/clothing/under/abductor
|
||||
shoes = /obj/item/clothing/shoes/combat
|
||||
back = /obj/item/storage/backpack
|
||||
ears = /obj/item/radio/headset/abductor
|
||||
@@ -34,7 +34,7 @@
|
||||
name = "Abductor Agent"
|
||||
head = /obj/item/clothing/head/helmet/abductor
|
||||
suit = /obj/item/clothing/suit/armor/abductor/vest
|
||||
suit_store = /obj/item/abductor_baton
|
||||
suit_store = /obj/item/abductor/baton
|
||||
belt = /obj/item/storage/belt/military/abductor/full
|
||||
|
||||
backpack_contents = list(
|
||||
|
||||
@@ -5,18 +5,23 @@
|
||||
icon_state = "gland"
|
||||
status = ORGAN_ROBOTIC
|
||||
beating = TRUE
|
||||
organ_flags = ORGAN_NO_SPOIL
|
||||
var/true_name = "baseline placebo referencer"
|
||||
var/cooldown_low = 300
|
||||
var/cooldown_high = 300
|
||||
var/next_activation = 0
|
||||
var/uses // -1 For infinite
|
||||
var/human_only = 0
|
||||
var/active = 0
|
||||
var/human_only = FALSE
|
||||
var/active = FALSE
|
||||
|
||||
var/mind_control_uses = 1
|
||||
var/mind_control_duration = 1800
|
||||
var/active_mind_control = FALSE
|
||||
|
||||
/obj/item/organ/heart/gland/Initialize()
|
||||
. = ..()
|
||||
icon_state = pick(list("health", "spider", "slime", "emp", "species", "egg", "vent", "mindshock", "viral"))
|
||||
|
||||
/obj/item/organ/heart/gland/examine(mob/user)
|
||||
. = ..()
|
||||
if((user.mind && HAS_TRAIT(user.mind, TRAIT_ABDUCTOR_SCIENTIST_TRAINING)) || isobserver(user))
|
||||
@@ -55,14 +60,18 @@
|
||||
active_mind_control = TRUE
|
||||
message_admins("[key_name(user)] sent an abductor mind control message to [key_name(owner)]: [command]")
|
||||
update_gland_hud()
|
||||
|
||||
var/obj/screen/alert/mind_control/mind_alert = owner.throw_alert("mind_control", /obj/screen/alert/mind_control)
|
||||
mind_alert.command = command
|
||||
addtimer(CALLBACK(src, .proc/clear_mind_control), mind_control_duration)
|
||||
return TRUE
|
||||
|
||||
/obj/item/organ/heart/gland/proc/clear_mind_control()
|
||||
if(!ownerCheck() || !active_mind_control)
|
||||
return FALSE
|
||||
to_chat(owner, "<span class='userdanger'>You feel the compulsion fade, and you completely forget about your previous orders.</span>")
|
||||
to_chat(owner, "<span class='userdanger'>You feel the compulsion fade, and you <i>completely forget</i> about your previous orders.</span>")
|
||||
owner.clear_alert("mind_control")
|
||||
active_mind_control = FALSE
|
||||
return TRUE
|
||||
|
||||
/obj/item/organ/heart/gland/Remove(mob/living/carbon/M, special = 0)
|
||||
active = 0
|
||||
@@ -98,257 +107,4 @@
|
||||
active = 0
|
||||
|
||||
/obj/item/organ/heart/gland/proc/activate()
|
||||
return
|
||||
|
||||
/obj/item/organ/heart/gland/heals
|
||||
true_name = "coherency harmonizer"
|
||||
cooldown_low = 200
|
||||
cooldown_high = 400
|
||||
uses = -1
|
||||
icon_state = "health"
|
||||
mind_control_uses = 3
|
||||
mind_control_duration = 3000
|
||||
|
||||
/obj/item/organ/heart/gland/heals/activate()
|
||||
to_chat(owner, "<span class='notice'>You feel curiously revitalized.</span>")
|
||||
owner.adjustToxLoss(-20, FALSE, TRUE)
|
||||
owner.heal_bodypart_damage(20, 20, 0, TRUE)
|
||||
owner.adjustOxyLoss(-20)
|
||||
|
||||
/obj/item/organ/heart/gland/slime
|
||||
true_name = "gastric animation galvanizer"
|
||||
cooldown_low = 600
|
||||
cooldown_high = 1200
|
||||
uses = -1
|
||||
icon_state = "slime"
|
||||
mind_control_uses = 1
|
||||
mind_control_duration = 2400
|
||||
|
||||
/obj/item/organ/heart/gland/slime/Insert(mob/living/carbon/M, special = 0, drop_if_replaced = TRUE)
|
||||
..()
|
||||
owner.faction |= "slime"
|
||||
owner.grant_language(/datum/language/slime)
|
||||
|
||||
/obj/item/organ/heart/gland/slime/activate()
|
||||
to_chat(owner, "<span class='warning'>You feel nauseated!</span>")
|
||||
owner.vomit(20)
|
||||
|
||||
var/mob/living/simple_animal/slime/Slime = new(get_turf(owner), "grey")
|
||||
Slime.Friends = list(owner)
|
||||
Slime.Leader = owner
|
||||
|
||||
/obj/item/organ/heart/gland/mindshock
|
||||
true_name = "neural crosstalk uninhibitor"
|
||||
cooldown_low = 400
|
||||
cooldown_high = 700
|
||||
uses = -1
|
||||
icon_state = "mindshock"
|
||||
mind_control_uses = 1
|
||||
mind_control_duration = 6000
|
||||
|
||||
/obj/item/organ/heart/gland/mindshock/activate()
|
||||
to_chat(owner, "<span class='notice'>You get a headache.</span>")
|
||||
|
||||
var/turf/T = get_turf(owner)
|
||||
for(var/mob/living/carbon/H in orange(4,T))
|
||||
if(H == owner)
|
||||
continue
|
||||
switch(pick(1,3))
|
||||
if(1)
|
||||
to_chat(H, "<span class='userdanger'>You hear a loud buzz in your head, silencing your thoughts!</span>")
|
||||
H.Stun(50)
|
||||
if(2)
|
||||
to_chat(H, "<span class='warning'>You hear an annoying buzz in your head.</span>")
|
||||
H.confused += 15
|
||||
H.adjustOrganLoss(ORGAN_SLOT_BRAIN, 10, 160)
|
||||
if(3)
|
||||
H.hallucination += 60
|
||||
|
||||
/obj/item/organ/heart/gland/pop
|
||||
true_name = "anthropmorphic translocator"
|
||||
cooldown_low = 900
|
||||
cooldown_high = 1800
|
||||
uses = -1
|
||||
human_only = TRUE
|
||||
icon_state = "species"
|
||||
mind_control_uses = 5
|
||||
mind_control_duration = 300
|
||||
|
||||
/obj/item/organ/heart/gland/pop/activate()
|
||||
to_chat(owner, "<span class='notice'>You feel unlike yourself.</span>")
|
||||
randomize_human(owner)
|
||||
var/species = pick(list(/datum/species/human, /datum/species/lizard, /datum/species/insect, /datum/species/fly))
|
||||
owner.set_species(species)
|
||||
|
||||
/obj/item/organ/heart/gland/ventcrawling
|
||||
true_name = "pliant cartilage enabler"
|
||||
cooldown_low = 1800
|
||||
cooldown_high = 2400
|
||||
uses = 1
|
||||
icon_state = "vent"
|
||||
mind_control_uses = 4
|
||||
mind_control_duration = 1800
|
||||
|
||||
/obj/item/organ/heart/gland/ventcrawling/activate()
|
||||
to_chat(owner, "<span class='notice'>You feel very stretchy.</span>")
|
||||
owner.ventcrawler = VENTCRAWLER_ALWAYS
|
||||
|
||||
/obj/item/organ/heart/gland/viral
|
||||
true_name = "contamination incubator"
|
||||
cooldown_low = 1800
|
||||
cooldown_high = 2400
|
||||
uses = 1
|
||||
icon_state = "viral"
|
||||
mind_control_uses = 1
|
||||
mind_control_duration = 1800
|
||||
|
||||
/obj/item/organ/heart/gland/viral/activate()
|
||||
to_chat(owner, "<span class='warning'>You feel sick.</span>")
|
||||
var/datum/disease/advance/A = random_virus(pick(2,6),6)
|
||||
A.carrier = TRUE
|
||||
owner.ForceContractDisease(A, FALSE, TRUE)
|
||||
|
||||
/obj/item/organ/heart/gland/viral/proc/random_virus(max_symptoms, max_level)
|
||||
if(max_symptoms > VIRUS_SYMPTOM_LIMIT)
|
||||
max_symptoms = VIRUS_SYMPTOM_LIMIT
|
||||
var/datum/disease/advance/A = new /datum/disease/advance()
|
||||
var/list/datum/symptom/possible_symptoms = list()
|
||||
for(var/symptom in subtypesof(/datum/symptom))
|
||||
var/datum/symptom/S = symptom
|
||||
if(initial(S.level) > max_level)
|
||||
continue
|
||||
if(initial(S.level) <= 0) //unobtainable symptoms
|
||||
continue
|
||||
possible_symptoms += S
|
||||
for(var/i in 1 to max_symptoms)
|
||||
var/datum/symptom/chosen_symptom = pick_n_take(possible_symptoms)
|
||||
if(chosen_symptom)
|
||||
var/datum/symptom/S = new chosen_symptom
|
||||
A.symptoms += S
|
||||
A.Refresh() //just in case someone already made and named the same disease
|
||||
return A
|
||||
|
||||
/obj/item/organ/heart/gland/trauma
|
||||
true_name = "white matter randomiser"
|
||||
cooldown_low = 800
|
||||
cooldown_high = 1200
|
||||
uses = 5
|
||||
icon_state = "emp"
|
||||
mind_control_uses = 3
|
||||
mind_control_duration = 1800
|
||||
|
||||
/obj/item/organ/heart/gland/trauma/activate()
|
||||
to_chat(owner, "<span class='warning'>You feel a spike of pain in your head.</span>")
|
||||
if(prob(33))
|
||||
owner.gain_trauma_type(BRAIN_TRAUMA_SPECIAL, rand(TRAUMA_RESILIENCE_BASIC, TRAUMA_RESILIENCE_LOBOTOMY))
|
||||
else
|
||||
if(prob(20))
|
||||
owner.gain_trauma_type(BRAIN_TRAUMA_SEVERE, rand(TRAUMA_RESILIENCE_BASIC, TRAUMA_RESILIENCE_LOBOTOMY))
|
||||
else
|
||||
owner.gain_trauma_type(BRAIN_TRAUMA_MILD, rand(TRAUMA_RESILIENCE_BASIC, TRAUMA_RESILIENCE_LOBOTOMY))
|
||||
|
||||
/obj/item/organ/heart/gland/spiderman
|
||||
true_name = "araneae cloister accelerator"
|
||||
cooldown_low = 450
|
||||
cooldown_high = 900
|
||||
uses = -1
|
||||
icon_state = "spider"
|
||||
mind_control_uses = 2
|
||||
mind_control_duration = 2400
|
||||
|
||||
/obj/item/organ/heart/gland/spiderman/activate()
|
||||
to_chat(owner, "<span class='warning'>You feel something crawling in your skin.</span>")
|
||||
owner.faction |= "spiders"
|
||||
var/obj/structure/spider/spiderling/S = new(owner.drop_location())
|
||||
S.directive = "Protect your nest inside [owner.real_name]."
|
||||
|
||||
/obj/item/organ/heart/gland/egg
|
||||
true_name = "roe/enzymatic synthesizer"
|
||||
cooldown_low = 300
|
||||
cooldown_high = 400
|
||||
uses = -1
|
||||
icon_state = "egg"
|
||||
lefthand_file = 'icons/mob/inhands/misc/food_lefthand.dmi'
|
||||
righthand_file = 'icons/mob/inhands/misc/food_righthand.dmi'
|
||||
mind_control_uses = 2
|
||||
mind_control_duration = 1800
|
||||
|
||||
/obj/item/organ/heart/gland/egg/activate()
|
||||
owner.visible_message("<span class='alertalien'>[owner] [pick(EGG_LAYING_MESSAGES)]</span>")
|
||||
var/turf/T = owner.drop_location()
|
||||
new /obj/item/reagent_containers/food/snacks/egg/gland(T)
|
||||
|
||||
/obj/item/organ/heart/gland/electric
|
||||
true_name = "electron accumulator/discharger"
|
||||
cooldown_low = 800
|
||||
cooldown_high = 1200
|
||||
uses = -1
|
||||
mind_control_uses = 2
|
||||
mind_control_duration = 900
|
||||
|
||||
/obj/item/organ/heart/gland/electric/Insert(mob/living/carbon/M, special = 0, drop_if_replaced = TRUE)
|
||||
..()
|
||||
ADD_TRAIT(owner, TRAIT_SHOCKIMMUNE, ORGAN_TRAIT)
|
||||
|
||||
/obj/item/organ/heart/gland/electric/Remove(mob/living/carbon/M, special = 0)
|
||||
REMOVE_TRAIT(owner, TRAIT_SHOCKIMMUNE, ORGAN_TRAIT)
|
||||
..()
|
||||
|
||||
/obj/item/organ/heart/gland/electric/activate()
|
||||
owner.visible_message("<span class='danger'>[owner]'s skin starts emitting electric arcs!</span>",\
|
||||
"<span class='warning'>You feel electric energy building up inside you!</span>")
|
||||
playsound(get_turf(owner), "sparks", 100, 1, -1)
|
||||
addtimer(CALLBACK(src, .proc/zap), rand(30, 100))
|
||||
|
||||
/obj/item/organ/heart/gland/electric/proc/zap()
|
||||
tesla_zap(owner, 4, 8000, TESLA_MOB_DAMAGE | TESLA_OBJ_DAMAGE | TESLA_MOB_STUN)
|
||||
playsound(get_turf(owner), 'sound/magic/lightningshock.ogg', 50, 1)
|
||||
|
||||
/obj/item/organ/heart/gland/chem
|
||||
true_name = "intrinsic pharma-provider"
|
||||
cooldown_low = 50
|
||||
cooldown_high = 50
|
||||
uses = -1
|
||||
mind_control_uses = 3
|
||||
mind_control_duration = 1200
|
||||
var/list/possible_reagents = list()
|
||||
|
||||
/obj/item/organ/heart/gland/chem/Initialize()
|
||||
..()
|
||||
for(var/X in subtypesof(/datum/reagent/drug))
|
||||
var/datum/reagent/R = X
|
||||
possible_reagents += initial(R.id)
|
||||
for(var/X in subtypesof(/datum/reagent/medicine))
|
||||
var/datum/reagent/R = X
|
||||
possible_reagents += initial(R.id)
|
||||
for(var/X in typesof(/datum/reagent/toxin))
|
||||
var/datum/reagent/R = X
|
||||
possible_reagents += initial(R.id)
|
||||
|
||||
/obj/item/organ/heart/gland/chem/activate()
|
||||
var/chem_to_add = pick(possible_reagents)
|
||||
owner.reagents.add_reagent(chem_to_add, 2)
|
||||
owner.adjustToxLoss(-2, TRUE, TRUE)
|
||||
..()
|
||||
|
||||
/obj/item/organ/heart/gland/plasma
|
||||
true_name = "effluvium sanguine-synonym emitter"
|
||||
cooldown_low = 1200
|
||||
cooldown_high = 1800
|
||||
uses = -1
|
||||
mind_control_uses = 1
|
||||
mind_control_duration = 800
|
||||
|
||||
/obj/item/organ/heart/gland/plasma/activate()
|
||||
to_chat(owner, "<span class='warning'>You feel bloated.</span>")
|
||||
addtimer(CALLBACK(GLOBAL_PROC, .proc/to_chat, owner, "<span class='userdanger'>A massive stomachache overcomes you.</span>"), 150)
|
||||
addtimer(CALLBACK(src, .proc/vomit_plasma), 200)
|
||||
|
||||
/obj/item/organ/heart/gland/plasma/proc/vomit_plasma()
|
||||
if(!owner)
|
||||
return
|
||||
owner.visible_message("<span class='danger'>[owner] vomits a cloud of plasma!</span>")
|
||||
var/turf/open/T = get_turf(owner)
|
||||
if(istype(T))
|
||||
T.atmos_spawn_air("plasma=50;TEMP=[T20C]")
|
||||
owner.vomit()
|
||||
return
|
||||
@@ -0,0 +1,19 @@
|
||||
/obj/item/organ/heart/gland/access
|
||||
true_name = "anagraphic electro-scrambler"
|
||||
cooldown_low = 600
|
||||
cooldown_high = 1200
|
||||
uses = 1
|
||||
icon_state = "mindshock"
|
||||
mind_control_uses = 3
|
||||
mind_control_duration = 900
|
||||
|
||||
/obj/item/organ/heart/gland/access/activate()
|
||||
to_chat(owner, "<span class='notice'>You feel like a VIP for some reason.</span>")
|
||||
RegisterSignal(owner, COMSIG_MOB_ALLOWED, .proc/free_access)
|
||||
|
||||
/obj/item/organ/heart/gland/access/proc/free_access(datum/source, obj/O)
|
||||
return TRUE
|
||||
|
||||
/obj/item/organ/heart/gland/access/Remove(mob/living/carbon/M, special = 0)
|
||||
UnregisterSignal(owner, COMSIG_MOB_ALLOWED)
|
||||
..()
|
||||
@@ -0,0 +1,18 @@
|
||||
/obj/item/organ/heart/gland/blood
|
||||
true_name = "pseudonuclear hemo-destabilizer"
|
||||
cooldown_low = 1200
|
||||
cooldown_high = 1800
|
||||
uses = -1
|
||||
icon_state = "egg"
|
||||
lefthand_file = 'icons/mob/inhands/misc/food_lefthand.dmi'
|
||||
righthand_file = 'icons/mob/inhands/misc/food_righthand.dmi'
|
||||
mind_control_uses = 3
|
||||
mind_control_duration = 1500
|
||||
|
||||
/obj/item/organ/heart/gland/blood/activate()
|
||||
if(!ishuman(owner) || !owner.dna.species)
|
||||
return
|
||||
var/mob/living/carbon/human/H = owner
|
||||
var/datum/species/species = H.dna.species
|
||||
to_chat(H, "<span class='warning'>You feel your blood heat up for a moment.</span>")
|
||||
species.exotic_blood = get_random_reagent_id()
|
||||
@@ -0,0 +1,20 @@
|
||||
/obj/item/organ/heart/gland/chem
|
||||
true_name = "intrinsic pharma-provider"
|
||||
cooldown_low = 50
|
||||
cooldown_high = 50
|
||||
uses = -1
|
||||
icon_state = "viral"
|
||||
mind_control_uses = 3
|
||||
mind_control_duration = 1200
|
||||
var/list/possible_reagents = list()
|
||||
|
||||
/obj/item/organ/heart/gland/chem/Initialize()
|
||||
. = ..()
|
||||
for(var/R in subtypesof(/datum/reagent/drug) + subtypesof(/datum/reagent/medicine) + typesof(/datum/reagent/toxin))
|
||||
possible_reagents += R
|
||||
|
||||
/obj/item/organ/heart/gland/chem/activate()
|
||||
var/chem_to_add = pick(possible_reagents)
|
||||
owner.reagents.add_reagent(chem_to_add, 2)
|
||||
owner.adjustToxLoss(-5, TRUE, TRUE)
|
||||
..()
|
||||
@@ -0,0 +1,15 @@
|
||||
/obj/item/organ/heart/gland/egg
|
||||
true_name = "roe/enzymatic synthesizer"
|
||||
cooldown_low = 300
|
||||
cooldown_high = 400
|
||||
uses = -1
|
||||
icon_state = "egg"
|
||||
lefthand_file = 'icons/mob/inhands/misc/food_lefthand.dmi'
|
||||
righthand_file = 'icons/mob/inhands/misc/food_righthand.dmi'
|
||||
mind_control_uses = 2
|
||||
mind_control_duration = 1800
|
||||
|
||||
/obj/item/organ/heart/gland/egg/activate()
|
||||
owner.visible_message("<span class='alertalien'>[owner] [pick(EGG_LAYING_MESSAGES)]</span>")
|
||||
var/turf/T = owner.drop_location()
|
||||
new /obj/item/reagent_containers/food/snacks/egg/gland(T)
|
||||
@@ -0,0 +1,26 @@
|
||||
/obj/item/organ/heart/gland/electric
|
||||
true_name = "electron accumulator/discharger"
|
||||
cooldown_low = 800
|
||||
cooldown_high = 1200
|
||||
icon_state = "species"
|
||||
uses = -1
|
||||
mind_control_uses = 2
|
||||
mind_control_duration = 900
|
||||
|
||||
/obj/item/organ/heart/gland/electric/Insert(mob/living/carbon/M, special = 0, drop_if_replaced = TRUE)
|
||||
..()
|
||||
ADD_TRAIT(owner, TRAIT_SHOCKIMMUNE, "abductor_gland")
|
||||
|
||||
/obj/item/organ/heart/gland/electric/Remove(mob/living/carbon/M, special = 0)
|
||||
REMOVE_TRAIT(owner, TRAIT_SHOCKIMMUNE, "abductor_gland")
|
||||
..()
|
||||
|
||||
/obj/item/organ/heart/gland/electric/activate()
|
||||
owner.visible_message("<span class='danger'>[owner]'s skin starts emitting electric arcs!</span>",\
|
||||
"<span class='warning'>You feel electric energy building up inside you!</span>")
|
||||
playsound(get_turf(owner), "sparks", 100, TRUE, -1)
|
||||
addtimer(CALLBACK(src, .proc/zap), rand(30, 100))
|
||||
|
||||
/obj/item/organ/heart/gland/electric/proc/zap()
|
||||
tesla_zap(owner, 4, 8000, TESLA_MOB_DAMAGE | TESLA_OBJ_DAMAGE | TESLA_MOB_STUN)
|
||||
playsound(get_turf(owner), 'sound/magic/lightningshock.ogg', 50, TRUE)
|
||||
@@ -0,0 +1,178 @@
|
||||
/obj/item/organ/heart/gland/heal
|
||||
true_name = "organic replicator"
|
||||
cooldown_low = 200
|
||||
cooldown_high = 400
|
||||
uses = -1
|
||||
human_only = TRUE
|
||||
icon_state = "health"
|
||||
mind_control_uses = 3
|
||||
mind_control_duration = 3000
|
||||
|
||||
/obj/item/organ/heart/gland/heal/activate()
|
||||
if(!(owner.mob_biotypes & MOB_ORGANIC))
|
||||
return
|
||||
|
||||
for(var/organ in owner.internal_organs)
|
||||
if(istype(organ, /obj/item/organ/cyberimp))
|
||||
reject_implant(organ)
|
||||
return
|
||||
|
||||
var/obj/item/organ/liver/liver = owner.getorganslot(ORGAN_SLOT_LIVER)
|
||||
if((!liver/* && !HAS_TRAIT(owner, TRAIT_NOMETABOLISM)*/) || (liver && ((liver.damage > (liver.maxHealth / 2)) || (istype(liver, /obj/item/organ/liver/cybernetic)))))
|
||||
replace_liver(liver)
|
||||
return
|
||||
|
||||
var/obj/item/organ/lungs/lungs = owner.getorganslot(ORGAN_SLOT_LUNGS)
|
||||
if((!lungs && !HAS_TRAIT(owner, TRAIT_NOBREATH)) || (lungs && (istype(lungs, /obj/item/organ/lungs/cybernetic))))
|
||||
replace_lungs(lungs)
|
||||
return
|
||||
|
||||
var/obj/item/organ/eyes/eyes = owner.getorganslot(ORGAN_SLOT_EYES)
|
||||
if(!eyes || (eyes && ((HAS_TRAIT_FROM(owner, TRAIT_NEARSIGHT, EYE_DAMAGE)) || (HAS_TRAIT_FROM(owner, TRAIT_BLIND, EYE_DAMAGE)) || (istype(eyes, /obj/item/organ/eyes/robotic)))))
|
||||
replace_eyes(eyes)
|
||||
return
|
||||
|
||||
var/obj/item/bodypart/limb
|
||||
var/list/limb_list = list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
|
||||
for(var/zone in limb_list)
|
||||
limb = owner.get_bodypart(zone)
|
||||
if(!limb)
|
||||
replace_limb(zone)
|
||||
return
|
||||
if((limb.get_damage() >= (limb.max_damage / 2)) || (limb.status == BODYPART_ROBOTIC))
|
||||
replace_limb(zone, limb)
|
||||
return
|
||||
|
||||
if(owner.getToxLoss() > 40)
|
||||
replace_blood()
|
||||
return
|
||||
var/tox_amount = 0
|
||||
for(var/datum/reagent/toxin/T in owner.reagents.reagent_list)
|
||||
tox_amount += owner.reagents.get_reagent_amount(T.type)
|
||||
if(tox_amount > 10)
|
||||
replace_blood()
|
||||
return
|
||||
if(owner.blood_volume < BLOOD_VOLUME_OKAY)
|
||||
owner.blood_volume = BLOOD_VOLUME_NORMAL
|
||||
to_chat(owner, "<span class='warning'>You feel your blood pulsing within you.</span>")
|
||||
return
|
||||
|
||||
var/obj/item/bodypart/chest/chest = owner.get_bodypart(BODY_ZONE_CHEST)
|
||||
if((chest.get_damage() >= (chest.max_damage / 4)) || (chest.status == BODYPART_ROBOTIC))
|
||||
replace_chest(chest)
|
||||
return
|
||||
|
||||
/obj/item/organ/heart/gland/heal/proc/reject_implant(obj/item/organ/cyberimp/implant)
|
||||
owner.visible_message("<span class='warning'>[owner] vomits up his [implant.name]!</span>", "<span class='userdanger'>You suddenly vomit up your [implant.name]!</span>")
|
||||
owner.vomit(0, TRUE, TRUE, 1, FALSE, FALSE, FALSE, TRUE)
|
||||
implant.Remove(owner)
|
||||
implant.forceMove(owner.drop_location())
|
||||
|
||||
/obj/item/organ/heart/gland/heal/proc/replace_liver(obj/item/organ/liver/liver)
|
||||
if(liver)
|
||||
owner.visible_message("<span class='warning'>[owner] vomits up his [liver.name]!</span>", "<span class='userdanger'>You suddenly vomit up your [liver.name]!</span>")
|
||||
owner.vomit(0, TRUE, TRUE, 1, FALSE, FALSE, FALSE, TRUE)
|
||||
liver.Remove(owner)
|
||||
liver.forceMove(owner.drop_location())
|
||||
else
|
||||
to_chat(owner, "<span class='warning'>You feel a weird rumble in your bowels...</span>")
|
||||
|
||||
var/liver_type = /obj/item/organ/liver
|
||||
if(owner?.dna?.species?.mutantliver)
|
||||
liver_type = owner.dna.species.mutantliver
|
||||
var/obj/item/organ/liver/new_liver = new liver_type()
|
||||
new_liver.Insert(owner)
|
||||
|
||||
/obj/item/organ/heart/gland/heal/proc/replace_lungs(obj/item/organ/lungs/lungs)
|
||||
if(lungs)
|
||||
owner.visible_message("<span class='warning'>[owner] vomits up his [lungs.name]!</span>", "<span class='userdanger'>You suddenly vomit up your [lungs.name]!</span>")
|
||||
owner.vomit(0, TRUE, TRUE, 1, FALSE, FALSE, FALSE, TRUE)
|
||||
lungs.Remove(owner)
|
||||
lungs.forceMove(owner.drop_location())
|
||||
else
|
||||
to_chat(owner, "<span class='warning'>You feel a weird rumble inside your chest...</span>")
|
||||
|
||||
var/lung_type = /obj/item/organ/lungs
|
||||
if(owner.dna.species && owner.dna.species.mutantlungs)
|
||||
lung_type = owner.dna.species.mutantlungs
|
||||
var/obj/item/organ/lungs/new_lungs = new lung_type()
|
||||
new_lungs.Insert(owner)
|
||||
|
||||
/obj/item/organ/heart/gland/heal/proc/replace_eyes(obj/item/organ/eyes/eyes)
|
||||
if(eyes)
|
||||
owner.visible_message("<span class='warning'>[owner]'s [eyes.name] fall out of their sockets!</span>", "<span class='userdanger'>Your [eyes.name] fall out of their sockets!</span>")
|
||||
playsound(owner, 'sound/effects/splat.ogg', 50, TRUE)
|
||||
eyes.Remove(owner)
|
||||
eyes.forceMove(owner.drop_location())
|
||||
else
|
||||
to_chat(owner, "<span class='warning'>You feel a weird rumble behind your eye sockets...</span>")
|
||||
|
||||
addtimer(CALLBACK(src, .proc/finish_replace_eyes), rand(100, 200))
|
||||
|
||||
/obj/item/organ/heart/gland/heal/proc/finish_replace_eyes()
|
||||
var/eye_type = /obj/item/organ/eyes
|
||||
if(owner.dna.species && owner.dna.species.mutanteyes)
|
||||
eye_type = owner.dna.species.mutanteyes
|
||||
var/obj/item/organ/eyes/new_eyes = new eye_type()
|
||||
new_eyes.Insert(owner)
|
||||
owner.visible_message("<span class='warning'>A pair of new eyes suddenly inflates into [owner]'s eye sockets!</span>", "<span class='userdanger'>A pair of new eyes suddenly inflates into your eye sockets!</span>")
|
||||
|
||||
/obj/item/organ/heart/gland/heal/proc/replace_limb(body_zone, obj/item/bodypart/limb)
|
||||
if(limb)
|
||||
owner.visible_message("<span class='warning'>[owner]'s [limb.name] suddenly detaches from [owner.p_their()] body!</span>", "<span class='userdanger'>Your [limb.name] suddenly detaches from your body!</span>")
|
||||
playsound(owner, "desceration", 50, TRUE, -1)
|
||||
limb.drop_limb()
|
||||
else
|
||||
to_chat(owner, "<span class='warning'>You feel a weird tingle in your [parse_zone(body_zone)]... even if you don't have one.</span>")
|
||||
|
||||
addtimer(CALLBACK(src, .proc/finish_replace_limb, body_zone), rand(150, 300))
|
||||
|
||||
/obj/item/organ/heart/gland/heal/proc/finish_replace_limb(body_zone)
|
||||
owner.visible_message("<span class='warning'>With a loud snap, [owner]'s [parse_zone(body_zone)] rapidly grows back from [owner.p_their()] body!</span>",
|
||||
"<span class='userdanger'>With a loud snap, your [parse_zone(body_zone)] rapidly grows back from your body!</span>",
|
||||
"<span class='warning'>Your hear a loud snap.</span>")
|
||||
playsound(owner, 'sound/magic/demon_consume.ogg', 50, TRUE)
|
||||
owner.regenerate_limb(body_zone)
|
||||
|
||||
/obj/item/organ/heart/gland/heal/proc/replace_blood()
|
||||
owner.visible_message("<span class='warning'>[owner] starts vomiting huge amounts of blood!</span>", "<span class='userdanger'>You suddenly start vomiting huge amounts of blood!</span>")
|
||||
keep_replacing_blood()
|
||||
|
||||
/obj/item/organ/heart/gland/heal/proc/keep_replacing_blood()
|
||||
var/keep_going = FALSE
|
||||
owner.vomit(0, TRUE, FALSE, 3, FALSE, FALSE, FALSE, TRUE)
|
||||
owner.Stun(15)
|
||||
owner.adjustToxLoss(-15, TRUE, TRUE)
|
||||
|
||||
owner.blood_volume = min(BLOOD_VOLUME_NORMAL, owner.blood_volume + 20)
|
||||
if(owner.blood_volume < BLOOD_VOLUME_NORMAL)
|
||||
keep_going = TRUE
|
||||
|
||||
if(owner.getToxLoss())
|
||||
keep_going = TRUE
|
||||
for(var/datum/reagent/toxin/R in owner.reagents.reagent_list)
|
||||
owner.reagents.remove_reagent(R.type, 4)
|
||||
if(owner.reagents.has_reagent(R.type))
|
||||
keep_going = TRUE
|
||||
if(keep_going)
|
||||
addtimer(CALLBACK(src, .proc/keep_replacing_blood), 30)
|
||||
|
||||
/obj/item/organ/heart/gland/heal/proc/replace_chest(obj/item/bodypart/chest/chest)
|
||||
if(chest.status == BODYPART_ROBOTIC)
|
||||
owner.visible_message("<span class='warning'>[owner]'s [chest.name] rapidly expels its mechanical components, replacing them with flesh!</span>", "<span class='userdanger'>Your [chest.name] rapidly expels its mechanical components, replacing them with flesh!</span>")
|
||||
playsound(owner, 'sound/magic/clockwork/anima_fragment_attack.ogg', 50, TRUE)
|
||||
var/list/dirs = GLOB.alldirs.Copy()
|
||||
for(var/i in 1 to 3)
|
||||
var/obj/effect/decal/cleanable/robot_debris/debris = new(get_turf(owner))
|
||||
debris.streak(dirs)
|
||||
else
|
||||
owner.visible_message("<span class='warning'>[owner]'s [chest.name] sheds off its damaged flesh, rapidly replacing it!</span>", "<span class='warning'>Your [chest.name] sheds off its damaged flesh, rapidly replacing it!</span>")
|
||||
playsound(owner, 'sound/effects/splat.ogg', 50, TRUE)
|
||||
var/list/dirs = GLOB.alldirs.Copy()
|
||||
for(var/i in 1 to 3)
|
||||
var/obj/effect/decal/cleanable/blood/gibs/gibs = new(get_turf(owner))
|
||||
gibs.streak(dirs)
|
||||
|
||||
var/obj/item/bodypart/chest/new_chest = new(null)
|
||||
new_chest.replace_limb(owner, TRUE)
|
||||
qdel(chest)
|
||||
@@ -0,0 +1,64 @@
|
||||
/obj/item/organ/heart/gland/mindshock
|
||||
true_name = "neural crosstalk uninhibitor"
|
||||
cooldown_low = 400
|
||||
cooldown_high = 700
|
||||
uses = -1
|
||||
icon_state = "mindshock"
|
||||
mind_control_uses = 1
|
||||
mind_control_duration = 6000
|
||||
var/list/mob/living/carbon/human/broadcasted_mobs = list()
|
||||
|
||||
/obj/item/organ/heart/gland/mindshock/activate()
|
||||
to_chat(owner, "<span class='notice'>You get a headache.</span>")
|
||||
|
||||
var/turf/T = get_turf(owner)
|
||||
for(var/mob/living/carbon/H in orange(4,T))
|
||||
if(H == owner)
|
||||
continue
|
||||
switch(pick(1,3))
|
||||
if(1)
|
||||
to_chat(H, "<span class='userdanger'>You hear a loud buzz in your head, silencing your thoughts!</span>")
|
||||
H.Stun(50)
|
||||
if(2)
|
||||
to_chat(H, "<span class='warning'>You hear an annoying buzz in your head.</span>")
|
||||
H.confused += 15
|
||||
H.adjustOrganLoss(ORGAN_SLOT_BRAIN, 10, 160)
|
||||
if(3)
|
||||
H.hallucination += 60
|
||||
|
||||
/obj/item/organ/heart/gland/mindshock/mind_control(command, mob/living/user)
|
||||
if(!ownerCheck() || !mind_control_uses || active_mind_control)
|
||||
return FALSE
|
||||
mind_control_uses--
|
||||
for(var/mob/M in oview(7, owner))
|
||||
if(!ishuman(M))
|
||||
continue
|
||||
var/mob/living/carbon/human/H = M
|
||||
if(H.stat)
|
||||
continue
|
||||
|
||||
broadcasted_mobs += H
|
||||
to_chat(H, "<span class='userdanger'>You suddenly feel an irresistible compulsion to follow an order...</span>")
|
||||
to_chat(H, "<span class='mind_control'>[command]</span>")
|
||||
|
||||
message_admins("[key_name(user)] broadcasted an abductor mind control message from [key_name(owner)] to [key_name(H)]: [command]")
|
||||
|
||||
var/obj/screen/alert/mind_control/mind_alert = H.throw_alert("mind_control", /obj/screen/alert/mind_control)
|
||||
mind_alert.command = command
|
||||
|
||||
if(LAZYLEN(broadcasted_mobs))
|
||||
active_mind_control = TRUE
|
||||
addtimer(CALLBACK(src, .proc/clear_mind_control), mind_control_duration)
|
||||
|
||||
update_gland_hud()
|
||||
return TRUE
|
||||
|
||||
/obj/item/organ/heart/gland/mindshock/clear_mind_control()
|
||||
if(!active_mind_control || !LAZYLEN(broadcasted_mobs))
|
||||
return FALSE
|
||||
for(var/M in broadcasted_mobs)
|
||||
var/mob/living/carbon/human/H = M
|
||||
to_chat(H, "<span class='userdanger'>You feel the compulsion fade, and you <i>completely forget</i> about your previous orders.</span>")
|
||||
H.clear_alert("mind_control")
|
||||
active_mind_control = FALSE
|
||||
return TRUE
|
||||
@@ -0,0 +1,22 @@
|
||||
/obj/item/organ/heart/gland/plasma
|
||||
true_name = "effluvium sanguine-synonym emitter"
|
||||
cooldown_low = 1200
|
||||
cooldown_high = 1800
|
||||
icon_state = "slime"
|
||||
uses = -1
|
||||
mind_control_uses = 1
|
||||
mind_control_duration = 800
|
||||
|
||||
/obj/item/organ/heart/gland/plasma/activate()
|
||||
to_chat(owner, "<span class='warning'>You feel bloated.</span>")
|
||||
addtimer(CALLBACK(GLOBAL_PROC, .proc/to_chat, owner, "<span class='userdanger'>A massive stomachache overcomes you.</span>"), 150)
|
||||
addtimer(CALLBACK(src, .proc/vomit_plasma), 200)
|
||||
|
||||
/obj/item/organ/heart/gland/plasma/proc/vomit_plasma()
|
||||
if(!owner)
|
||||
return
|
||||
owner.visible_message("<span class='danger'>[owner] vomits a cloud of plasma!</span>")
|
||||
var/turf/open/T = get_turf(owner)
|
||||
if(istype(T))
|
||||
T.atmos_spawn_air("plasma=50;TEMP=[T20C]")
|
||||
owner.vomit()
|
||||
@@ -0,0 +1,47 @@
|
||||
/obj/item/organ/heart/gland/quantum
|
||||
true_name = "quantic de-observation matrix"
|
||||
cooldown_low = 150
|
||||
cooldown_high = 150
|
||||
uses = -1
|
||||
icon_state = "emp"
|
||||
mind_control_uses = 2
|
||||
mind_control_duration = 1200
|
||||
var/mob/living/carbon/entangled_mob
|
||||
|
||||
/obj/item/organ/heart/gland/quantum/activate()
|
||||
if(entangled_mob)
|
||||
return
|
||||
for(var/mob/M in oview(owner, 7))
|
||||
if(!iscarbon(M))
|
||||
continue
|
||||
entangled_mob = M
|
||||
addtimer(CALLBACK(src, .proc/quantum_swap), rand(600, 2400))
|
||||
return
|
||||
|
||||
/obj/item/organ/heart/gland/quantum/proc/quantum_swap()
|
||||
if(QDELETED(entangled_mob))
|
||||
entangled_mob = null
|
||||
return
|
||||
var/turf/T = get_turf(owner)
|
||||
do_teleport(owner, get_turf(entangled_mob),null,TRUE,channel = TELEPORT_CHANNEL_QUANTUM)
|
||||
do_teleport(entangled_mob, T,null,TRUE,channel = TELEPORT_CHANNEL_QUANTUM)
|
||||
to_chat(owner, "<span class='warning'>You suddenly find yourself somewhere else!</span>")
|
||||
to_chat(entangled_mob, "<span class='warning'>You suddenly find yourself somewhere else!</span>")
|
||||
if(!active_mind_control) //Do not reset entangled mob while mind control is active
|
||||
entangled_mob = null
|
||||
|
||||
/obj/item/organ/heart/gland/quantum/mind_control(command, mob/living/user)
|
||||
if(..())
|
||||
if(entangled_mob && ishuman(entangled_mob) && (entangled_mob.stat < DEAD))
|
||||
to_chat(entangled_mob, "<span class='userdanger'>You suddenly feel an irresistible compulsion to follow an order...</span>")
|
||||
to_chat(entangled_mob, "<span class='mind_control'>[command]</span>")
|
||||
var/obj/screen/alert/mind_control/mind_alert = entangled_mob.throw_alert("mind_control", /obj/screen/alert/mind_control)
|
||||
mind_alert.command = command
|
||||
message_admins("[key_name(owner)] mirrored an abductor mind control message to [key_name(entangled_mob)]: [command]")
|
||||
update_gland_hud()
|
||||
|
||||
/obj/item/organ/heart/gland/quantum/clear_mind_control()
|
||||
if(active_mind_control)
|
||||
to_chat(entangled_mob, "<span class='userdanger'>You feel the compulsion fade, and you completely forget about your previous orders.</span>")
|
||||
entangled_mob.clear_alert("mind_control")
|
||||
..()
|
||||
@@ -0,0 +1,21 @@
|
||||
/obj/item/organ/heart/gland/slime
|
||||
true_name = "gastric animation galvanizer"
|
||||
cooldown_low = 600
|
||||
cooldown_high = 1200
|
||||
uses = -1
|
||||
icon_state = "slime"
|
||||
mind_control_uses = 1
|
||||
mind_control_duration = 2400
|
||||
|
||||
/obj/item/organ/heart/gland/slime/Insert(mob/living/carbon/M, special = 0, drop_if_replaced = TRUE)
|
||||
..()
|
||||
owner.faction |= "slime"
|
||||
owner.grant_language(/datum/language/slime)
|
||||
|
||||
/obj/item/organ/heart/gland/slime/activate()
|
||||
to_chat(owner, "<span class='warning'>You feel nauseated!</span>")
|
||||
owner.vomit(20)
|
||||
|
||||
var/mob/living/simple_animal/slime/Slime = new(get_turf(owner), "grey")
|
||||
Slime.Friends = list(owner)
|
||||
Slime.Leader = owner
|
||||
@@ -0,0 +1,14 @@
|
||||
/obj/item/organ/heart/gland/spiderman
|
||||
true_name = "araneae cloister accelerator"
|
||||
cooldown_low = 450
|
||||
cooldown_high = 900
|
||||
uses = -1
|
||||
icon_state = "spider"
|
||||
mind_control_uses = 2
|
||||
mind_control_duration = 2400
|
||||
|
||||
/obj/item/organ/heart/gland/spiderman/activate()
|
||||
to_chat(owner, "<span class='warning'>You feel something crawling in your skin.</span>")
|
||||
owner.faction |= "spiders"
|
||||
var/obj/structure/spider/spiderling/S = new(owner.drop_location())
|
||||
S.directive = "Protect your nest inside [owner.real_name]."
|
||||
@@ -0,0 +1,15 @@
|
||||
/obj/item/organ/heart/gland/transform
|
||||
true_name = "anthropmorphic transmorphosizer"
|
||||
cooldown_low = 900
|
||||
cooldown_high = 1800
|
||||
uses = -1
|
||||
human_only = TRUE
|
||||
icon_state = "species"
|
||||
mind_control_uses = 7
|
||||
mind_control_duration = 300
|
||||
|
||||
/obj/item/organ/heart/gland/transform/activate()
|
||||
to_chat(owner, "<span class='notice'>You feel unlike yourself.</span>")
|
||||
randomize_human(owner)
|
||||
var/species = pick(list(/datum/species/human, /datum/species/lizard, /datum/species/insect, /datum/species/fly))
|
||||
owner.set_species(species)
|
||||
@@ -0,0 +1,18 @@
|
||||
/obj/item/organ/heart/gland/trauma
|
||||
true_name = "white matter randomiser"
|
||||
cooldown_low = 800
|
||||
cooldown_high = 1200
|
||||
uses = 5
|
||||
icon_state = "emp"
|
||||
mind_control_uses = 3
|
||||
mind_control_duration = 1800
|
||||
|
||||
/obj/item/organ/heart/gland/trauma/activate()
|
||||
to_chat(owner, "<span class='warning'>You feel a spike of pain in your head.</span>")
|
||||
if(prob(33))
|
||||
owner.gain_trauma_type(BRAIN_TRAUMA_SPECIAL, rand(TRAUMA_RESILIENCE_BASIC, TRAUMA_RESILIENCE_LOBOTOMY))
|
||||
else
|
||||
if(prob(20))
|
||||
owner.gain_trauma_type(BRAIN_TRAUMA_SEVERE, rand(TRAUMA_RESILIENCE_BASIC, TRAUMA_RESILIENCE_LOBOTOMY))
|
||||
else
|
||||
owner.gain_trauma_type(BRAIN_TRAUMA_MILD, rand(TRAUMA_RESILIENCE_BASIC, TRAUMA_RESILIENCE_LOBOTOMY))
|
||||
@@ -0,0 +1,12 @@
|
||||
/obj/item/organ/heart/gland/ventcrawling
|
||||
true_name = "pliant cartilage enabler"
|
||||
cooldown_low = 1800
|
||||
cooldown_high = 2400
|
||||
uses = 1
|
||||
icon_state = "vent"
|
||||
mind_control_uses = 4
|
||||
mind_control_duration = 1800
|
||||
|
||||
/obj/item/organ/heart/gland/ventcrawling/activate()
|
||||
to_chat(owner, "<span class='notice'>You feel very stretchy.</span>")
|
||||
owner.ventcrawler = VENTCRAWLER_ALWAYS
|
||||
@@ -0,0 +1,34 @@
|
||||
/obj/item/organ/heart/gland/viral
|
||||
true_name = "contamination incubator"
|
||||
cooldown_low = 1800
|
||||
cooldown_high = 2400
|
||||
uses = 1
|
||||
icon_state = "viral"
|
||||
mind_control_uses = 1
|
||||
mind_control_duration = 1800
|
||||
|
||||
/obj/item/organ/heart/gland/viral/activate()
|
||||
to_chat(owner, "<span class='warning'>You feel sick.</span>")
|
||||
var/datum/disease/advance/A = random_virus(pick(2,6),6)
|
||||
A.carrier = TRUE
|
||||
owner.ForceContractDisease(A, FALSE, TRUE)
|
||||
|
||||
/obj/item/organ/heart/gland/viral/proc/random_virus(max_symptoms, max_level)
|
||||
if(max_symptoms > VIRUS_SYMPTOM_LIMIT)
|
||||
max_symptoms = VIRUS_SYMPTOM_LIMIT
|
||||
var/datum/disease/advance/A = new /datum/disease/advance()
|
||||
var/list/datum/symptom/possible_symptoms = list()
|
||||
for(var/symptom in subtypesof(/datum/symptom))
|
||||
var/datum/symptom/S = symptom
|
||||
if(initial(S.level) > max_level)
|
||||
continue
|
||||
if(initial(S.level) <= 0) //unobtainable symptoms
|
||||
continue
|
||||
possible_symptoms += S
|
||||
for(var/i in 1 to max_symptoms)
|
||||
var/datum/symptom/chosen_symptom = pick_n_take(possible_symptoms)
|
||||
if(chosen_symptom)
|
||||
var/datum/symptom/S = new chosen_symptom
|
||||
A.symptoms += S
|
||||
A.Refresh() //just in case someone already made and named the same disease
|
||||
return A
|
||||
@@ -43,12 +43,15 @@
|
||||
dat += "Collected Samples : [points] <br>"
|
||||
dat += "Gear Credits: [credits] <br>"
|
||||
dat += "<b>Transfer data in exchange for supplies:</b><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=baton'>Advanced Baton</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=helmet'>Agent Helmet</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=vest'>Agent Vest</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=silencer'>Radio Silencer</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=tool'>Science Tool</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=mind_device'>Mental Interface Device</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=baton'>Advanced Baton (2 Credits)</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=mind_device'>Mental Interface Device (2 Credits)</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=chem_dispenser'>Reagent Synthesizer (2 Credits)</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=shrink_ray'>Shrink Ray Blaster (2 Credits)</a><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=helmet'>Agent Helmet (1 Credit)</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=vest'>Agent Vest (1 Credit)</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=silencer'>Radio Silencer (1 Credit)</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=tool'>Science Tool (1 Credit)</A><br>"
|
||||
dat += "<a href='?src=[REF(src)];dispense=tongue'>Superlingual Matrix (1 Credit)</a><br>"
|
||||
else
|
||||
dat += "<span class='bad'>NO EXPERIMENT MACHINE DETECTED</span> <br>"
|
||||
|
||||
@@ -101,7 +104,7 @@
|
||||
else if(href_list["dispense"])
|
||||
switch(href_list["dispense"])
|
||||
if("baton")
|
||||
Dispense(/obj/item/abductor_baton,cost=2)
|
||||
Dispense(/obj/item/abductor/baton,cost=2)
|
||||
if("helmet")
|
||||
Dispense(/obj/item/clothing/head/helmet/abductor)
|
||||
if("silencer")
|
||||
@@ -112,6 +115,12 @@
|
||||
Dispense(/obj/item/clothing/suit/armor/abductor/vest)
|
||||
if("mind_device")
|
||||
Dispense(/obj/item/abductor/mind_device,cost=2)
|
||||
if("chem_dispenser")
|
||||
Dispense(/obj/item/abductor_machine_beacon/chem_dispenser,cost=2)
|
||||
if("tongue")
|
||||
Dispense(/obj/item/organ/tongue/abductor)
|
||||
if("shrink_ray")
|
||||
Dispense(/obj/item/gun/energy/shrink_ray,cost=2)
|
||||
updateUsrDialog()
|
||||
|
||||
/obj/machinery/abductor/console/proc/TeleporterRetrieve()
|
||||
@@ -136,9 +145,9 @@
|
||||
|
||||
var/entry_name
|
||||
if(remote)
|
||||
entry_name = show_radial_menu(usr, camera.eyeobj, disguises2)
|
||||
entry_name = show_radial_menu(usr, camera.eyeobj, disguises2, tooltips = TRUE)
|
||||
else
|
||||
entry_name = show_radial_menu(usr, src, disguises2)
|
||||
entry_name = show_radial_menu(usr, src, disguises2, require_near = TRUE, tooltips = TRUE)
|
||||
|
||||
var/datum/icon_snapshot/chosen = disguises[entry_name]
|
||||
if(chosen && vest && (remote || in_range(usr,src)))
|
||||
@@ -236,4 +245,4 @@
|
||||
new item(drop_location)
|
||||
|
||||
else
|
||||
say("Insufficent data!")
|
||||
say("Insufficent data!")
|
||||
@@ -143,9 +143,9 @@
|
||||
create_reagents(10)
|
||||
|
||||
if(overmind && overmind.blob_reagent_datum)
|
||||
reagents.add_reagent(overmind.blob_reagent_datum.id, 10)
|
||||
reagents.add_reagent(overmind.blob_reagent_datum.type, 10)
|
||||
else
|
||||
reagents.add_reagent("spore", 10)
|
||||
reagents.add_reagent(/datum/reagent/toxin/spore, 10)
|
||||
|
||||
// Attach the smoke spreader and setup/start it.
|
||||
S.attach(location)
|
||||
|
||||
@@ -1,40 +1,64 @@
|
||||
/obj/structure/blob/shield
|
||||
name = "strong blob"
|
||||
icon = 'icons/mob/blob.dmi'
|
||||
icon_state = "blob_shield"
|
||||
desc = "A solid wall of slightly twitching tendrils."
|
||||
max_integrity = 150
|
||||
brute_resist = 0.25
|
||||
explosion_block = 3
|
||||
point_return = 4
|
||||
atmosblock = TRUE
|
||||
armor = list("melee" = 25, "bullet" = 25, "laser" = 15, "energy" = 10, "bomb" = 20, "bio" = 0, "rad" = 0, "fire" = 90, "acid" = 90)
|
||||
var/weakened
|
||||
|
||||
/obj/structure/blob/shield/scannerreport()
|
||||
if(atmosblock)
|
||||
return "Will prevent the spread of atmospheric changes."
|
||||
return "N/A"
|
||||
|
||||
/obj/structure/blob/shield/core
|
||||
point_return = 0
|
||||
|
||||
/obj/structure/blob/shield/update_icon()
|
||||
..()
|
||||
if(obj_integrity <= 70)
|
||||
icon_state = "blob_shield_damaged"
|
||||
name = "weakened strong blob"
|
||||
desc = "A wall of twitching tendrils."
|
||||
atmosblock = FALSE
|
||||
if(!weakened)
|
||||
armor = armor.setRating("melee" = 15, "bullet" = 15, "laser" = 5, "energy" = 0, "bomb" = 10, "bio" = 0, "rad" = 0, "fire" = 90, "acid" = 90)
|
||||
weakened = TRUE
|
||||
else
|
||||
icon_state = initial(icon_state)
|
||||
name = initial(name)
|
||||
desc = initial(desc)
|
||||
atmosblock = TRUE
|
||||
if(weakened)
|
||||
armor = armor.setRating("melee" = 25, "bullet" = 25, "laser" = 15, "energy" = 10, "bomb" = 20, "bio" = 0, "rad" = 0, "fire" = 90, "acid" = 90)
|
||||
weakened = FALSE
|
||||
air_update_turf(1)
|
||||
/obj/structure/blob/shield
|
||||
name = "strong blob"
|
||||
icon = 'icons/mob/blob.dmi'
|
||||
icon_state = "blob_shield"
|
||||
desc = "A solid wall of slightly twitching tendrils."
|
||||
var/damaged_desc = "A wall of twitching tendrils."
|
||||
max_integrity = 150
|
||||
brute_resist = 0.25
|
||||
explosion_block = 3
|
||||
point_return = 4
|
||||
atmosblock = TRUE
|
||||
armor = list("melee" = 25, "bullet" = 25, "laser" = 15, "energy" = 10, "bomb" = 20, "bio" = 0, "rad" = 0, "fire" = 90, "acid" = 90)
|
||||
var/weakened
|
||||
|
||||
/obj/structure/blob/shield/scannerreport()
|
||||
if(atmosblock)
|
||||
return "Will prevent the spread of atmospheric changes."
|
||||
return "N/A"
|
||||
|
||||
/obj/structure/blob/shield/core
|
||||
point_return = 0
|
||||
|
||||
/obj/structure/blob/shield/update_icon()
|
||||
..()
|
||||
if(obj_integrity < max_integrity * 0.5)
|
||||
icon_state = "[initial(icon_state)]_damaged"
|
||||
name = "weakened [initial(name)]"
|
||||
desc = "[damaged_desc]"
|
||||
atmosblock = FALSE
|
||||
if(!weakened)
|
||||
armor = armor.setRating("melee" = 15, "bullet" = 15, "laser" = 5, "energy" = 0, "bomb" = 10, "bio" = 0, "rad" = 0, "fire" = 90, "acid" = 90)
|
||||
weakened = TRUE
|
||||
else
|
||||
icon_state = initial(icon_state)
|
||||
name = initial(name)
|
||||
desc = initial(desc)
|
||||
atmosblock = TRUE
|
||||
if(weakened)
|
||||
armor = armor.setRating("melee" = 25, "bullet" = 25, "laser" = 15, "energy" = 10, "bomb" = 20, "bio" = 0, "rad" = 0, "fire" = 90, "acid" = 90)
|
||||
weakened = FALSE
|
||||
air_update_turf(1)
|
||||
|
||||
/obj/structure/blob/shield/reflective
|
||||
name = "reflective blob"
|
||||
desc = "A solid wall of slightly twitching tendrils with a reflective glow."
|
||||
damaged_desc = "A wall of twitching tendrils with a reflective glow."
|
||||
icon_state = "blob_glow"
|
||||
flags_1 = CHECK_RICOCHET_1
|
||||
point_return = 8
|
||||
max_integrity = 50
|
||||
brute_resist = 1
|
||||
explosion_block = 2
|
||||
|
||||
/obj/structure/blob/shield/reflective/handle_ricochet(obj/item/projectile/P)
|
||||
var/turf/p_turf = get_turf(P)
|
||||
var/face_direction = get_dir(src, p_turf)
|
||||
var/face_angle = dir2angle(face_direction)
|
||||
var/incidence_s = GET_ANGLE_OF_INCIDENCE(face_angle, (P.Angle + 180))
|
||||
if(abs(incidence_s) > 90 && abs(incidence_s) < 270)
|
||||
return FALSE
|
||||
var/new_angle_s = SIMPLIFY_DEGREES(face_angle + incidence_s)
|
||||
P.setAngle(new_angle_s)
|
||||
visible_message("<span class='warning'>[P] reflects off [src]!</span>")
|
||||
return TRUE
|
||||
@@ -113,12 +113,22 @@
|
||||
|
||||
/mob/camera/blob/verb/create_shield_power()
|
||||
set category = "Blob"
|
||||
set name = "Create Shield Blob (15)"
|
||||
set desc = "Create a shield blob, which will block fire and is hard to kill."
|
||||
set name = "Create/Upgrade Shield Blob (15)"
|
||||
set desc = "Create a shield blob, which will block fire and is hard to kill. Using this on an existing shield blob turns it into a reflective blob, capable of reflecting most projectiles but making it much weaker than usual to brute attacks."
|
||||
create_shield()
|
||||
|
||||
/mob/camera/blob/proc/create_shield(turf/T)
|
||||
createSpecial(15, /obj/structure/blob/shield, 0, 0, T)
|
||||
var/obj/structure/blob/shield/S = locate(/obj/structure/blob/shield) in T
|
||||
if(S)
|
||||
if(!can_buy(15))
|
||||
return
|
||||
if(S.obj_integrity < S.max_integrity * 0.5)
|
||||
to_chat(src, "<span class='warning'>This shield blob is too damaged to be modified properly!</span>")
|
||||
return
|
||||
to_chat(src, "<span class='warning'>You secrete a reflective ooze over the shield blob, allowing it to reflect projectiles at the cost of reduced intregrity.</span>")
|
||||
S.change_to(/obj/structure/blob/shield/reflective, src)
|
||||
else
|
||||
createSpecial(15, /obj/structure/blob/shield, 0, 0, T)
|
||||
|
||||
/mob/camera/blob/verb/create_resource()
|
||||
set category = "Blob"
|
||||
@@ -156,7 +166,7 @@
|
||||
if(!can_buy(40))
|
||||
return
|
||||
|
||||
var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you want to play as a [blob_reagent_datum.name] blobbernaut?", ROLE_BLOB, null, ROLE_BLOB, 50) //players must answer rapidly
|
||||
var/list/mob/candidates = pollGhostCandidates("Do you want to play as a [blob_reagent_datum.name] blobbernaut?", ROLE_BLOB, null, ROLE_BLOB, 50) //players must answer rapidly
|
||||
if(LAZYLEN(candidates)) //if we got at least one candidate, they're a blobbernaut now.
|
||||
B.max_integrity = initial(B.max_integrity) * 0.25 //factories that produced a blobbernaut have much lower health
|
||||
B.obj_integrity = min(B.obj_integrity, B.max_integrity)
|
||||
@@ -171,7 +181,7 @@
|
||||
blobber.update_icons()
|
||||
blobber.adjustHealth(blobber.maxHealth * 0.5)
|
||||
blob_mobs += blobber
|
||||
var/mob/dead/observer/C = pick(candidates)
|
||||
var/mob/C = pick(candidates)
|
||||
C.transfer_ckey(blobber)
|
||||
SEND_SOUND(blobber, sound('sound/effects/blobattack.ogg'))
|
||||
SEND_SOUND(blobber, sound('sound/effects/attackblob.ogg'))
|
||||
@@ -359,7 +369,7 @@
|
||||
to_chat(src, "<b>You can expand, which will attack people, damage objects, or place a Normal Blob if the tile is clear.</b>")
|
||||
to_chat(src, "<i>Normal Blobs</i> will expand your reach and can be upgraded into special blobs that perform certain functions.")
|
||||
to_chat(src, "<b>You can upgrade normal blobs into the following types of blob:</b>")
|
||||
to_chat(src, "<i>Shield Blobs</i> are strong and expensive blobs which take more damage. In additon, they are fireproof and can block air, use these to protect yourself from station fires.")
|
||||
to_chat(src, "<i>Shield Blobs</i> are strong and expensive blobs which take more damage. In additon, they are fireproof and can block air, use these to protect yourself from station fires. Upgrading them again will result in a reflective blob, capable of reflecting most projectiles at the cost of the strong blob's extra health.")
|
||||
to_chat(src, "<i>Resource Blobs</i> are blobs which produce more resources for you, build as many of these as possible to consume the station. This type of blob must be placed near node blobs or your core to work.")
|
||||
to_chat(src, "<i>Factory Blobs</i> are blobs that spawn blob spores which will attack nearby enemies. This type of blob must be placed near node blobs or your core to work.")
|
||||
to_chat(src, "<i>Blobbernauts</i> can be produced from factories for a cost, and are hard to kill, powerful, and moderately smart. The factory used to create one will become fragile and briefly unable to produce spores.")
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
// Getting Flaws:
|
||||
//
|
||||
// Killing crew
|
||||
//
|
||||
// Gaining ranks
|
||||
|
||||
|
||||
|
||||
// * COMPULSION * Things you must do
|
||||
//
|
||||
// SELECTIVE: -Gender/BloodType/Job sustains you, but others give you less.
|
||||
//
|
||||
|
||||
|
||||
|
||||
|
||||
// * WEAKNESSES * Things that may harm you
|
||||
//
|
||||
// LIGHTS: -Bright light nullifies the Examine benefits of Masquerade.
|
||||
// -Bright lights disable your healing (including in Torpor)
|
||||
//
|
||||
// STAKES: -Stakes kill you immediately.
|
||||
//
|
||||
// PAINFUL: -Your feed victims scream, despite being unconscious.
|
||||
//
|
||||
// FIRE: -You only need your max health (not x2) in fire damage to die.
|
||||
//
|
||||
// CORPSE: -Your Masquerade turns off when unconscious or crit.
|
||||
//
|
||||
// FERAL: -
|
||||
//
|
||||
// CRAVEN
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// BANES //
|
||||
|
||||
// These are basically small weaknesses that affect your character in certain circumstances.
|
||||
// As a rule, they should be specific as to when they happen, or have only some certain
|
||||
// drawback.
|
||||
|
||||
// (core ideas)
|
||||
// SENSITIVE: You are slightly blinded by bright lights.
|
||||
// DARKFRIEND: Your automatic healing is at a crawl when in bright light.
|
||||
// TRADITIONAL: Every five minutes spent outside a coffin lowers your rate of automatic healing.
|
||||
// CONSUMED: Every five minutes spent outside a coffin increases the rate at which your blood ticks down.
|
||||
// GOURMAND: Animals and blood bags offer you no nourishment when feeding.
|
||||
// DEATHMASK: You no longer fake having a heartbeat, and always show up as pale when examined.
|
||||
// BESTIAL: When your blood is low, you will twitch involuntarily.
|
||||
|
||||
// (alternate ideas)
|
||||
// STERILE: There is a high chance that turning corpses to Bloodsuckers will fail, and further attempts on them by you are impossible.
|
||||
// FERAL: You're a threat to Vampire-kind: New Bloodsuckers may have an Objective to destroy you.
|
||||
// UNHOLY: The Chapel, the Bible, and Holy Water set you on fire.
|
||||
// PARANOID: Only your own claimed coffin counts for healing and banes.
|
||||
|
||||
|
||||
// ON LEVEL-UP:
|
||||
// Burn Damage increases
|
||||
// Regen Rate increases
|
||||
// Max Punch Damage increase
|
||||
// Reset Level Timer
|
||||
// Select Bane
|
||||
|
||||
|
||||
// How to Burn Vamps:
|
||||
// C.adjustFireLoss(20)
|
||||
// C.adjust_fire_stacks(6)
|
||||
// C.IgniteMob()
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/AssignRandomBane()
|
||||
return
|
||||
@@ -0,0 +1,148 @@
|
||||
// INTEGRATION: Adding Procs and Datums to existing "classes"
|
||||
/mob/living/proc/AmBloodsucker(falseIfInDisguise=FALSE)
|
||||
// No Datum
|
||||
if(!mind || !mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER))
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
|
||||
/mob/living/proc/HaveBloodsuckerBodyparts(var/displaymessage="") // displaymessage can be something such as "rising from death" for Torpid Sleep. givewarningto is the person receiving messages.
|
||||
if(!getorganslot(ORGAN_SLOT_HEART))
|
||||
if(displaymessage != "")
|
||||
to_chat(src, "<span class='warning'>Without a heart, you are incapable of [displaymessage].</span>")
|
||||
return FALSE
|
||||
if(!get_bodypart(BODY_ZONE_HEAD))
|
||||
if(displaymessage != "")
|
||||
to_chat(src, "<span class='warning'>Without a head, you are incapable of [displaymessage].</span>")
|
||||
return FALSE
|
||||
if(!getorgan(/obj/item/organ/brain)) // NOTE: This is mostly just here so we can do one scan for all needed parts when creating a vamp. You probably won't be trying to use powers w/out a brain.
|
||||
if(displaymessage != "")
|
||||
to_chat(src, "<span class='warning'>Without a brain, you are incapable of [displaymessage].</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
|
||||
|
||||
// GET DAMAGE
|
||||
|
||||
|
||||
// Do NOT count the damage on prosthetics for this.
|
||||
/mob/living/proc/getBruteLoss_nonProsthetic()
|
||||
return getBruteLoss()
|
||||
|
||||
/mob/living/proc/getFireLoss_nonProsthetic()
|
||||
return getFireLoss()
|
||||
|
||||
/mob/living/carbon/getBruteLoss_nonProsthetic()
|
||||
var/amount = 0
|
||||
for(var/obj/item/bodypart/BP in bodyparts)
|
||||
if(BP.status < 2)
|
||||
amount += BP.brute_dam
|
||||
return amount
|
||||
|
||||
/mob/living/carbon/getFireLoss_nonProsthetic()
|
||||
var/amount = 0
|
||||
for(var/obj/item/bodypart/BP in bodyparts)
|
||||
if(BP.status < 2)
|
||||
amount += BP.burn_dam
|
||||
return amount
|
||||
|
||||
/mob/living/carbon
|
||||
// EXAMINING
|
||||
/mob/living/carbon/human/proc/ReturnVampExamine(var/mob/viewer)
|
||||
if(!mind || !viewer.mind)
|
||||
return ""
|
||||
// Target must be a Vamp
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(!bloodsuckerdatum)
|
||||
return ""
|
||||
// Viewer is Target's Vassal?
|
||||
if(viewer.mind.has_antag_datum(ANTAG_DATUM_VASSAL) in bloodsuckerdatum.vassals)
|
||||
var/returnString = "\[<span class='warning'><EM>This is your Master!</EM></span>\]"
|
||||
var/returnIcon = "[icon2html('icons/misc/language.dmi', world, "bloodsucker")]"
|
||||
returnString += "\n"
|
||||
return returnIcon + returnString
|
||||
// Viewer not a Vamp AND not the target's vassal?
|
||||
if(!viewer.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) && !(viewer in bloodsuckerdatum.vassals))
|
||||
return ""
|
||||
// Default String
|
||||
var/returnString = "\[<span class='warning'><EM>[bloodsuckerdatum.ReturnFullName(1)]</EM></span>\]"
|
||||
var/returnIcon = "[icon2html('icons/misc/language.dmi', world, "bloodsucker")]"
|
||||
|
||||
// In Disguise (Veil)?
|
||||
//if (name_override != null)
|
||||
// returnString += "<span class='suicide'> ([real_name] in disguise!) </span>"
|
||||
|
||||
//returnString += "\n" Don't need spacers. Using . += "" in examine.dm does this on its own.
|
||||
return returnIcon + returnString
|
||||
|
||||
|
||||
/mob/living/carbon/human/proc/ReturnVassalExamine(var/mob/viewer)
|
||||
if(!mind || !viewer.mind)
|
||||
return ""
|
||||
// Am I not even a Vassal? Then I am not marked.
|
||||
var/datum/antagonist/vassal/vassaldatum = mind.has_antag_datum(ANTAG_DATUM_VASSAL)
|
||||
if(!vassaldatum)
|
||||
return ""
|
||||
// Only Vassals and Bloodsuckers can recognize marks.
|
||||
if(!viewer.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) && !viewer.mind.has_antag_datum(ANTAG_DATUM_VASSAL))
|
||||
return ""
|
||||
|
||||
// Default String
|
||||
var/returnString = "\[<span class='warning'>"
|
||||
var/returnIcon = ""
|
||||
// Am I Viewer's Vassal?
|
||||
if(vassaldatum.master.owner == viewer.mind)
|
||||
returnString += "This [dna.species.name] bears YOUR mark!"
|
||||
returnIcon = "[icon2html('icons/misc/mark_icons.dmi', world, "vassal")]"
|
||||
// Am I someone ELSE'S Vassal?
|
||||
else if(viewer.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER))
|
||||
returnString += "This [dna.species.name] bears the mark of <span class='boldwarning'>[vassaldatum.master.ReturnFullName(vassaldatum.master.owner.current,1)]</span>"
|
||||
returnIcon = "[icon2html('icons/misc/mark_icons.dmi', world, "vassal_grey")]"
|
||||
// Are you serving the same master as I am?
|
||||
else if(viewer.mind.has_antag_datum(ANTAG_DATUM_VASSAL) in vassaldatum.master.vassals)
|
||||
returnString += "[p_they(TRUE)] bears the mark of your Master"
|
||||
returnIcon = "[icon2html('icons/misc/mark_icons.dmi', world, "vassal")]"
|
||||
// You serve a different Master than I do.
|
||||
else
|
||||
returnString += "[p_they(TRUE)] bears the mark of another Bloodsucker"
|
||||
returnIcon = "[icon2html('icons/misc/mark_icons.dmi', world, "vassal_grey")]"
|
||||
|
||||
returnString += "</span>\]" // \n" Don't need spacers. Using . += "" in examine.dm does this on its own.
|
||||
return returnIcon + returnString
|
||||
|
||||
|
||||
// Am I "pale" when examined? Bloodsuckers can trick this.
|
||||
/mob/living/carbon/proc/ShowAsPaleExamine()
|
||||
|
||||
// Normal Creatures:
|
||||
if(!mind || !mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER))
|
||||
return blood_volume < (BLOOD_VOLUME_SAFE * blood_ratio)
|
||||
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(bloodsuckerdatum.poweron_masquerade)
|
||||
return FALSE
|
||||
|
||||
// If a Bloodsucker is malnourished, AND if his temperature matches his surroundings (aka he hasn't fed recently and looks COLD)...
|
||||
return blood_volume < (BLOOD_VOLUME_OKAY * blood_ratio) // && !(bodytemperature <= get_temperature() + 2)
|
||||
|
||||
/mob/living/carbon/human/ShowAsPaleExamine()
|
||||
// Check for albino, as per human/examine.dm's check.
|
||||
if(dna.species.use_skintones && skin_tone == "albino")
|
||||
return TRUE
|
||||
|
||||
return ..() // Return vamp check
|
||||
|
||||
/mob/living/carbon/proc/scan_blood_volume()
|
||||
// Vamps don't show up normally to scanners unless Masquerade power is on ----> scanner.dm
|
||||
if(mind)
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if (istype(bloodsuckerdatum) && bloodsuckerdatum.poweron_masquerade)
|
||||
return BLOOD_VOLUME_NORMAL
|
||||
return blood_volume
|
||||
|
||||
/mob/living/proc/IsFrenzied()
|
||||
return FALSE
|
||||
|
||||
/mob/living/proc/StartFrenzy(inTime = 120)
|
||||
set waitfor = FALSE
|
||||
@@ -0,0 +1,363 @@
|
||||
|
||||
|
||||
// TO PLUG INTO LIFE:
|
||||
|
||||
// Cancel BLOOD life
|
||||
// Cancel METABOLISM life (or find a way to control what gets digested)
|
||||
// Create COLDBLOODED trait (thermal homeostasis)
|
||||
|
||||
// EXAMINE
|
||||
//
|
||||
// Show as dead when...
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/LifeTick()// Should probably run from life.dm, same as handle_changeling, but will be an utter pain to move
|
||||
set waitfor = FALSE // Don't make on_gain() wait for this function to finish. This lets this code run on the side.
|
||||
var/notice_healing = FALSE
|
||||
while(owner && !AmFinalDeath()) // owner.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) == src
|
||||
if(owner.current.stat == CONSCIOUS && !poweron_feed && !HAS_TRAIT(owner.current, TRAIT_DEATHCOMA)) // Deduct Blood
|
||||
AddBloodVolume(-0.1) // -.15 (before tick went from 10 to 30, but we also charge more for faking life now)
|
||||
if(HandleHealing(1)) // Heal
|
||||
if(notice_healing == FALSE && owner.current.blood_volume > 0)
|
||||
to_chat(owner, "<span class='notice'>The power of your blood begins knitting your wounds...</span>")
|
||||
notice_healing = TRUE
|
||||
else if(notice_healing == TRUE)
|
||||
notice_healing = FALSE // Apply Low Blood Effects
|
||||
HandleStarving() // Death
|
||||
HandleDeath() // Standard Update
|
||||
update_hud()// Daytime Sleep in Coffin
|
||||
if (SSticker.mode.is_daylight() && !HAS_TRAIT_FROM(owner.current, TRAIT_DEATHCOMA, "bloodsucker"))
|
||||
if(istype(owner.current.loc, /obj/structure/closet/crate/coffin))
|
||||
Torpor_Begin()
|
||||
// Wait before next pass
|
||||
sleep(10)
|
||||
FreeAllVassals() // Free my Vassals! (if I haven't yet)
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// BLOOD
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/AddBloodVolume(value)
|
||||
owner.current.blood_volume = CLAMP(owner.current.blood_volume + value, 0, maxBloodVolume)
|
||||
update_hud()
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/HandleFeeding(mob/living/carbon/target, mult=1)
|
||||
// mult: SILENT feed is 1/3 the amount
|
||||
var/blood_taken = min(feedAmount, target.blood_volume) * mult // Starts at 15 (now 8 since we doubled the Feed time)
|
||||
target.blood_volume -= blood_taken
|
||||
// Simple Animals lose a LOT of blood, and take damage. This is to keep cats, cows, and so forth from giving you insane amounts of blood.
|
||||
if(!ishuman(target))
|
||||
target.blood_volume -= (blood_taken / max(target.mob_size, 0.1)) * 3.5 // max() to prevent divide-by-zero
|
||||
target.apply_damage_type(blood_taken / 3.5) // Don't do too much damage, or else they die and provide no blood nourishment.
|
||||
if(target.blood_volume <= 0)
|
||||
target.blood_volume = 0
|
||||
target.death(0)
|
||||
///////////
|
||||
// Shift Body Temp (toward Target's temp, by volume taken)
|
||||
owner.current.bodytemperature = ((owner.current.blood_volume * owner.current.bodytemperature) + (blood_taken * target.bodytemperature)) / (owner.current.blood_volume + blood_taken)
|
||||
// our volume * temp, + their volume * temp, / total volume
|
||||
///////////
|
||||
// Reduce Value Quantity
|
||||
if(target.stat == DEAD) // Penalty for Dead Blood
|
||||
blood_taken /= 3
|
||||
if(!ishuman(target)) // Penalty for Non-Human Blood
|
||||
blood_taken /= 2
|
||||
//if (!iscarbon(target)) // Penalty for Animals (they're junk food)
|
||||
// Apply to Volume
|
||||
AddBloodVolume(blood_taken)
|
||||
// Reagents (NOT Blood!)
|
||||
if(target.reagents && target.reagents.total_volume)
|
||||
target.reagents.reaction(owner.current, INGEST, 1) // Run Reaction: what happens when what they have mixes with what I have?
|
||||
target.reagents.trans_to(owner.current, 1) // Run transfer of 1 unit of reagent from them to me.
|
||||
// Blood Gulp Sound
|
||||
owner.current.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head.
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// HEALING
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/HandleHealing(mult = 1)
|
||||
// NOTE: Mult of 0 is just a TEST to see if we are injured and need to go into Torpor!
|
||||
//It is called from your coffin on close (by you only)
|
||||
if(poweron_masquerade == TRUE || owner.current.AmStaked())
|
||||
return FALSE
|
||||
owner.current.adjustStaminaLoss(-2 + (regenRate * -8) * mult, 0) // Humans lose stamina damage really quickly. Vamps should heal more.
|
||||
owner.current.adjustCloneLoss(-0.1 * (regenRate * 2) * mult, 0)
|
||||
owner.current.adjustOrganLoss(ORGAN_SLOT_BRAIN, -1 * (regenRate * 4) * mult) //adjustBrainLoss(-1 * (regenRate * 4) * mult, 0)
|
||||
// No Bleeding
|
||||
if(ishuman(owner.current)) //NOTE Current bleeding is horrible, not to count the amount of blood ballistics delete.
|
||||
var/mob/living/carbon/human/H = owner.current
|
||||
H.bleed_rate = 0
|
||||
if(iscarbon(owner.current)) // Damage Heal: Do I have damage to ANY bodypart?
|
||||
var/mob/living/carbon/C = owner.current
|
||||
var/costMult = 1 // Coffin makes it cheaper
|
||||
var/fireheal = 0 // BURN: Heal in Coffin while Fakedeath, or when damage above maxhealth (you can never fully heal fire)
|
||||
var/amInCoffinWhileTorpor = istype(C.loc, /obj/structure/closet/crate/coffin) && (mult == 0 || HAS_TRAIT(C, TRAIT_DEATHCOMA)) // Check for mult 0 OR death coma. (mult 0 means we're testing from coffin)
|
||||
if(amInCoffinWhileTorpor)
|
||||
mult *= 4 // Increase multiplier if we're sleeping in a coffin.
|
||||
fireheal = min(C.getFireLoss_nonProsthetic(), regenRate) // NOTE: Burn damage ONLY heals in torpor.
|
||||
costMult = 0.25
|
||||
C.ExtinguishMob()
|
||||
CureDisabilities() // Extinguish Fire
|
||||
C.remove_all_embedded_objects() // Remove Embedded!
|
||||
owner.current.regenerate_organs() // Heal Organs (will respawn original eyes etc. but we replace right away, next)
|
||||
CheckVampOrgans() // Heart, Eyes
|
||||
else
|
||||
if(owner.current.blood_volume <= 0) // No Blood? Lower Mult
|
||||
mult = 0.25
|
||||
// Crit from burn? Lower damage to maximum allowed.
|
||||
//if (C.getFireLoss() > owner.current.getMaxHealth())
|
||||
// fireheal = regenRate / 2
|
||||
// BRUTE: Always Heal
|
||||
var/bruteheal = min(C.getBruteLoss_nonProsthetic(), regenRate)
|
||||
var/toxinheal = min(C.getToxLoss(), regenRate)
|
||||
// Heal if Damaged
|
||||
if(bruteheal + fireheal + toxinheal > 0) // Just a check? Don't heal/spend, and return.
|
||||
if(mult == 0)
|
||||
return TRUE
|
||||
if(owner.current.stat >= UNCONSCIOUS) //Faster regeneration while unconcious, so you dont have to wait all day
|
||||
mult *= 2
|
||||
// We have damage. Let's heal (one time)
|
||||
C.adjustBruteLoss(-bruteheal * mult, forced = TRUE)// Heal BRUTE / BURN in random portions throughout the body.
|
||||
C.adjustFireLoss(-fireheal * mult, forced = TRUE)
|
||||
C.adjustToxLoss(-toxinheal * mult * 2, forced = TRUE) //Toxin healing because vamps arent immune
|
||||
//C.heal_overall_damage(bruteheal * mult, fireheal * mult) // REMOVED: We need to FORCE this, because otherwise, vamps won't heal EVER. Swapped to above.
|
||||
AddBloodVolume((bruteheal * -0.5 + fireheal * -1) / mult * costMult) // Costs blood to heal
|
||||
return TRUE // Healed! Done for this tick.
|
||||
if(amInCoffinWhileTorpor) // Limbs? (And I have no other healing)
|
||||
var/list/missing = owner.current.get_missing_limbs() // Heal Missing
|
||||
if (missing.len) // Cycle through ALL limbs and regen them!
|
||||
for (var/targetLimbZone in missing) // 1) Find ONE Limb and regenerate it.
|
||||
owner.current.regenerate_limb(targetLimbZone, 0) // regenerate_limbs() <--- If you want to EXCLUDE certain parts, do it like this ----> regenerate_limbs(0, list("head"))
|
||||
var/obj/item/bodypart/L = owner.current.get_bodypart( targetLimbZone ) // 2) Limb returns Damaged
|
||||
AddBloodVolume(50 * costMult) // Costs blood to heal
|
||||
L.brute_dam = 60
|
||||
to_chat(owner.current, "<span class='notice'>Your flesh knits as it regrows [L]!</span>")
|
||||
playsound(owner.current, 'sound/magic/demon_consume.ogg', 50, 1)
|
||||
// DONE! After regenerating ANY number of limbs, we stop here.
|
||||
return TRUE
|
||||
/*else // REMOVED: For now, let's just leave prosthetics on. Maybe you WANT to be a robovamp.
|
||||
// Remove Prosthetic/False Limb
|
||||
for(var/obj/item/bodypart/BP in C.bodyparts)
|
||||
message_admins("T1: [BP] ")
|
||||
if (istype(BP) && BP.status == 2)
|
||||
message_admins("T2: [BP] ")
|
||||
BP.drop_limb()
|
||||
return TRUE */
|
||||
// NOTE: Limbs have a "status", like their hosts "stat". 2 is dead (aka Prosthetic). 1 seems to be idle/alive.*/
|
||||
return FALSE
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/CureDisabilities()
|
||||
var/mob/living/carbon/C = owner.current
|
||||
C.cure_blind(list(EYE_DAMAGE))//()
|
||||
C.cure_nearsighted(EYE_DAMAGE)
|
||||
C.set_blindness(0) // Added 9/2/19
|
||||
C.set_blurriness(0) // Added 9/2/19
|
||||
C.update_tint() // Added 9/2/19
|
||||
C.update_sight() // Added 9/2/19
|
||||
for(var/O in C.internal_organs) //owner.current.adjust_eye_damage(-100) // This was removed by TG
|
||||
var/obj/item/organ/organ = O
|
||||
organ.setOrganDamage(0)
|
||||
owner.current.cure_husk()
|
||||
|
||||
// I am thirsty for blud!
|
||||
/datum/antagonist/bloodsucker/proc/HandleStarving()
|
||||
|
||||
// High: Faster Healing
|
||||
// Med: Pale
|
||||
// Low: Twitch
|
||||
// V.Low: Blur Vision
|
||||
// EMPTY: Frenzy!
|
||||
// BLOOD_VOLUME_GOOD: [336] Pale (handled in bloodsucker_integration.dm
|
||||
// BLOOD_VOLUME_BAD: [224] Jitter
|
||||
if(owner.current.blood_volume < BLOOD_VOLUME_BAD && !prob(0.5))
|
||||
owner.current.Jitter(3)
|
||||
// BLOOD_VOLUME_SURVIVE: [122] Blur Vision
|
||||
if(owner.current.blood_volume < BLOOD_VOLUME_BAD / 2)
|
||||
owner.current.blur_eyes(8 - 8 * (owner.current.blood_volume / BLOOD_VOLUME_BAD))
|
||||
// Nutrition
|
||||
owner.current.nutrition = min(owner.current.blood_volume, NUTRITION_LEVEL_FED) // <-- 350 //NUTRITION_LEVEL_FULL
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// DEATH
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/HandleDeath()
|
||||
// FINAL DEATH
|
||||
// Fire Damage? (above double health)
|
||||
if(owner.current.getFireLoss_nonProsthetic() >= owner.current.getMaxHealth() * 1.5)
|
||||
FinalDeath()
|
||||
return
|
||||
// Staked while "Temp Death" or Asleep
|
||||
if(owner.current.StakeCanKillMe() && owner.current.AmStaked())
|
||||
FinalDeath()
|
||||
return
|
||||
// Not "Alive"?
|
||||
if(!owner.current || !isliving(owner.current) || isbrain(owner.current) || !get_turf(owner.current))
|
||||
FinalDeath()
|
||||
return
|
||||
// Missing Brain or Heart?
|
||||
if(!owner.current.HaveBloodsuckerBodyparts())
|
||||
FinalDeath()
|
||||
return
|
||||
// Disable Powers: Masquerade * NOTE * This should happen as a FLAW!
|
||||
//if (stat >= UNCONSCIOUS)
|
||||
// for (var/datum/action/bloodsucker/masquerade/P in powers)
|
||||
// P.Deactivate()
|
||||
// TEMP DEATH
|
||||
var/total_brute = owner.current.getBruteLoss_nonProsthetic()
|
||||
var/total_burn = owner.current.getFireLoss_nonProsthetic()
|
||||
var/total_toxloss = owner.current.getToxLoss() //This is neater than just putting it in total_damage
|
||||
var/total_damage = total_brute + total_burn + total_toxloss
|
||||
// Died? Convert to Torpor (fake death)
|
||||
if(owner.current.stat >= DEAD)
|
||||
Torpor_Begin()
|
||||
to_chat(owner, "<span class='danger'>Your immortal body will not yet relinquish your soul to the abyss. You enter Torpor.</span>")
|
||||
sleep(30) //To avoid spam
|
||||
if (poweron_masquerade == TRUE)
|
||||
to_chat(owner, "<span class='warning'>Your wounds will not heal until you disable the <span class='boldnotice'>Masquerade</span> power.</span>")
|
||||
// End Torpor:
|
||||
else // No damage, OR toxin healed AND brute healed and NOT in coffin (since you cannot heal burn)
|
||||
if(total_damage <= 0 || total_toxloss <= 0 && total_brute <= 0 && !istype(owner.current.loc, /obj/structure/closet/crate/coffin))
|
||||
// Not Daytime, Not in Torpor
|
||||
if(!SSticker.mode.is_daylight() && HAS_TRAIT_FROM(owner.current, TRAIT_DEATHCOMA, "bloodsucker"))
|
||||
Torpor_End()
|
||||
// Fake Unconscious
|
||||
if(poweron_masquerade == TRUE && total_damage >= owner.current.getMaxHealth() - HEALTH_THRESHOLD_FULLCRIT)
|
||||
owner.current.Unconscious(20,1)
|
||||
//HEALTH_THRESHOLD_CRIT 0
|
||||
//HEALTH_THRESHOLD_FULLCRIT -30
|
||||
//HEALTH_THRESHOLD_DEAD -100
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/Torpor_Begin(amInCoffin=FALSE)
|
||||
owner.current.stat = UNCONSCIOUS
|
||||
owner.current.fakedeath("bloodsucker") // Come after UNCONSCIOUS or else it fails
|
||||
ADD_TRAIT(owner.current, TRAIT_NODEATH, "bloodsucker") // Without this, you'll just keep dying while you recover.
|
||||
ADD_TRAIT(owner.current, TRAIT_RESISTHIGHPRESSURE, "bloodsucker") // So you can heal in 0 G. otherwise you just...heal forever.
|
||||
ADD_TRAIT(owner.current, TRAIT_RESISTLOWPRESSURE, "bloodsucker") // So you can heal in 0 G. otherwise you just...heal forever.
|
||||
// Visuals
|
||||
owner.current.update_sight()
|
||||
owner.current.reload_fullscreen()
|
||||
// Disable ALL Powers
|
||||
for(var/datum/action/bloodsucker/power in powers)
|
||||
if(power.active && !power.can_use_in_torpor)
|
||||
power.DeactivatePower()
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/Torpor_End()
|
||||
owner.current.stat = SOFT_CRIT
|
||||
owner.current.cure_fakedeath("bloodsucker") // Come after SOFT_CRIT or else it fails
|
||||
REMOVE_TRAIT(owner.current, TRAIT_NODEATH, "bloodsucker")
|
||||
REMOVE_TRAIT(owner.current, TRAIT_RESISTHIGHPRESSURE, "bloodsucker")
|
||||
REMOVE_TRAIT(owner.current, TRAIT_RESISTLOWPRESSURE, "bloodsucker")
|
||||
to_chat(owner, "<span class='warning'>You have recovered from Torpor.</span>")
|
||||
|
||||
|
||||
/datum/antagonist/proc/AmFinalDeath()
|
||||
// Standard Antags can be dead OR final death
|
||||
return owner && (owner.current && owner.current.stat >= DEAD || owner.AmFinalDeath())
|
||||
|
||||
/datum/antagonist/bloodsucker/AmFinalDeath()
|
||||
return owner && owner.AmFinalDeath()
|
||||
/datum/antagonist/changeling/AmFinalDeath()
|
||||
return owner && owner.AmFinalDeath()
|
||||
|
||||
/datum/mind/proc/AmFinalDeath()
|
||||
return !current || QDELETED(current) || !isliving(current) || isbrain(current) || !get_turf(current) // NOTE: "isliving()" is not the same as STAT == CONSCIOUS. This is to make sure you're not a BORG (aka silicon)
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/FinalDeath()
|
||||
if(!iscarbon(owner.current)) //Check for non carbons.
|
||||
owner.current.gib()
|
||||
return
|
||||
playsound(get_turf(owner.current), 'sound/effects/tendril_destroyed.ogg', 60, 1)
|
||||
owner.current.drop_all_held_items()
|
||||
owner.current.unequip_everything()
|
||||
var/mob/living/carbon/C = owner.current
|
||||
C.remove_all_embedded_objects()
|
||||
// Make me UN-CLONEABLE
|
||||
owner.current.hellbound = TRUE // This was done during creation, but let's do it again one more time...to make SURE this guy stays dead, but they dont stay dead because brains can be cloned!
|
||||
// Free my Vassals!
|
||||
FreeAllVassals()
|
||||
// Elders get Dusted
|
||||
if(vamplevel >= 4) // (vamptitle)
|
||||
owner.current.visible_message("<span class='warning'>[owner.current]'s skin crackles and dries, their skin and bones withering to dust. A hollow cry whips from what is now a sandy pile of remains.</span>", \
|
||||
"<span class='userdanger'>Your soul escapes your withering body as the abyss welcomes you to your Final Death.</span>", \
|
||||
"<span class='italics'>You hear a dry, crackling sound.</span>")
|
||||
owner.current.dust()
|
||||
// Fledglings get Gibbed
|
||||
else
|
||||
owner.current.visible_message("<span class='warning'>[owner.current]'s skin bursts forth in a spray of gore and detritus. A horrible cry echoes from what is now a wet pile of decaying meat.</span>", \
|
||||
"<span class='userdanger'>Your soul escapes your withering body as the abyss welcomes you to your Final Death.</span>", \
|
||||
"<span class='italics'>You hear a wet, bursting sound.</span>")
|
||||
owner.current.gib(TRUE, FALSE, FALSE)//Brain cloning is wierd and allows hellbounds. Lets destroy the brain for safety.
|
||||
playsound(owner.current.loc, 'sound/effects/tendril_destroyed.ogg', 40, 1)
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// HUMAN FOOD
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/mob/proc/CheckBloodsuckerEatFood(var/food_nutrition)
|
||||
if (!isliving(src))
|
||||
return
|
||||
var/mob/living/L = src
|
||||
if(!L.AmBloodsucker())
|
||||
return
|
||||
// We're a vamp? Try to eat food...
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
bloodsuckerdatum.handle_eat_human_food(food_nutrition)
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/handle_eat_human_food(var/food_nutrition) // Called from snacks.dm and drinks.dm
|
||||
set waitfor = FALSE
|
||||
if(!owner.current || !iscarbon(owner.current))
|
||||
return
|
||||
var/mob/living/carbon/C = owner.current
|
||||
// Remove Nutrition, Give Bad Food
|
||||
C.nutrition -= food_nutrition
|
||||
foodInGut += food_nutrition
|
||||
// Already ate some bad clams? Then we can back out, because we're already sick from it.
|
||||
if (foodInGut != food_nutrition)
|
||||
return
|
||||
// Haven't eaten, but I'm in a Human Disguise.
|
||||
else if (poweron_masquerade)
|
||||
to_chat(C, "<span class='notice'>Your stomach turns, but your \"human disguise\" keeps the food down...for now.</span>")
|
||||
// Keep looping until we purge. If we have activated our Human Disguise, we ignore the food. But it'll come up eventually...
|
||||
var/sickphase = 0
|
||||
while (foodInGut)
|
||||
sleep(50)
|
||||
C.adjust_disgust(10 * sickphase)
|
||||
// Wait an interval...
|
||||
sleep(50 + 50 * sickphase) // At intervals of 100, 150, and 200. (10 seconds, 15 seconds, and 20 seconds)
|
||||
// Died? Cancel
|
||||
if(C.stat == DEAD)
|
||||
return
|
||||
// Put up disguise? Then hold off the vomit.
|
||||
if(poweron_masquerade)
|
||||
if(sickphase > 0)
|
||||
to_chat(C, "<span class='notice'>Your stomach settles temporarily. You regain your composure...for now.</span>")
|
||||
sickphase = 0
|
||||
continue
|
||||
switch(sickphase)
|
||||
if (1)
|
||||
to_chat(C, "<span class='warning'>You feel unwell. You can taste ash on your tongue.</span>")
|
||||
C.Stun(10)
|
||||
if (2)
|
||||
to_chat(C, "<span class='warning'>Your stomach turns. Whatever you ate tastes of grave dirt and brimstone.</span>")
|
||||
C.Dizzy(15)
|
||||
C.Stun(13)
|
||||
if (3)
|
||||
to_chat(C, "<span class='warning'>You purge the food of the living from your viscera! You've never felt worse.</span>")
|
||||
C.vomit(foodInGut * 4, foodInGut * 2, 0) // (var/lost_nutrition = 10, var/blood = 0, var/stun = 1, var/distance = 0, var/message = 1, var/toxic = 0)
|
||||
C.blood_volume = max(0, C.blood_volume - foodInGut * 2)
|
||||
C.Stun(30)
|
||||
//C.Dizzy(50)
|
||||
foodInGut = 0
|
||||
sickphase ++
|
||||
@@ -0,0 +1,351 @@
|
||||
|
||||
|
||||
// Hide a random object somewhere on the station:
|
||||
// var/turf/targetturf = get_random_station_turf()
|
||||
// var/turf/targetturf = get_safe_random_station_turf()
|
||||
|
||||
|
||||
|
||||
|
||||
/datum/objective/bloodsucker
|
||||
martyr_compatible = TRUE
|
||||
|
||||
// GENERATE!
|
||||
/datum/objective/bloodsucker/proc/generate_objective()
|
||||
update_explanation_text()
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// // PROCS // //
|
||||
|
||||
|
||||
/datum/objective/bloodsucker/proc/return_possible_targets()
|
||||
var/list/possible_targets = list()
|
||||
|
||||
// Look at all crew members, and for/loop through.
|
||||
for(var/datum/mind/possible_target in get_crewmember_minds())
|
||||
// Check One: Default Valid User
|
||||
if(possible_target != owner && ishuman(possible_target.current) && possible_target.current.stat != DEAD)// && is_unique_objective(possible_target))
|
||||
// Check Two: Am Bloodsucker? OR in Bloodsucker list?
|
||||
if (possible_target.has_antag_datum(ANTAG_DATUM_BLOODSUCKER) || (possible_target in SSticker.mode.bloodsuckers))
|
||||
continue
|
||||
else
|
||||
possible_targets += possible_target
|
||||
|
||||
return possible_targets
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/datum/objective/bloodsucker/lair
|
||||
|
||||
// EXPLANATION
|
||||
/datum/objective/bloodsucker/lair/update_explanation_text()
|
||||
explanation_text = "Create a lair by claiming a coffin, and protect it until the end of the shift"// Make sure to keep it safe!"
|
||||
|
||||
// WIN CONDITIONS?
|
||||
/datum/objective/bloodsucker/lair/check_completion()
|
||||
var/datum/antagonist/bloodsucker/antagdatum = owner.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if (antagdatum && antagdatum.coffin && antagdatum.lair)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
// Space_Station_13_areas.dm <--- all the areas
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Vassal becomes a Head, or part of a department
|
||||
/datum/objective/bloodsucker/protege
|
||||
|
||||
// LOOKUP: /datum/crewmonitor/proc/update_data(z) for .assignment to see how to get a person's PDA.
|
||||
var/list/roles = list(
|
||||
"Captain",
|
||||
"Head of Personnel",
|
||||
"Research Director",
|
||||
"Chief Engineer",
|
||||
"Chief Medical Officer",
|
||||
"Quartermaster"
|
||||
)
|
||||
var/list/departs = list(
|
||||
"Research Director",
|
||||
"Chief Engineer",
|
||||
"Chief Medical Officer",
|
||||
"Quartermaster"
|
||||
)
|
||||
|
||||
|
||||
var/target_role // Equals "HEAD" when it's not a department role.
|
||||
var/department_string
|
||||
|
||||
// GENERATE!
|
||||
/datum/objective/bloodsucker/protege/generate_objective()
|
||||
target_role = rand(0,2) == 0 ? "HEAD" : pick(departs)
|
||||
|
||||
// Heads?
|
||||
if (target_role == "HEAD")
|
||||
target_amount = rand(1, round(SSticker.mode.num_players() / 20))
|
||||
target_amount = CLAMP(target_amount,1,3)
|
||||
// Department?
|
||||
else
|
||||
switch(target_role)
|
||||
if("Research Director")
|
||||
department_string = "Science"
|
||||
if("Chief Engineer")
|
||||
department_string = "Engineering"
|
||||
if("Chief Medical Officer")
|
||||
department_string = "Medical"
|
||||
if("Quartermaster")
|
||||
department_string = "Cargo"
|
||||
target_amount = rand(round(SSticker.mode.num_players() / 20), round(SSticker.mode.num_players() / 10))
|
||||
target_amount = CLAMP(target_amount, 2, 4)
|
||||
..()
|
||||
|
||||
// EXPLANATION
|
||||
/datum/objective/bloodsucker/protege/update_explanation_text()
|
||||
if (target_role == "HEAD")
|
||||
if (target_amount == 1)
|
||||
explanation_text = "Guarantee a Vassal ends up as a Department Head or in a Leadership role."
|
||||
else
|
||||
explanation_text = "Guarantee [target_amount] Vassals end up as different Leadership or Department Heads."
|
||||
else
|
||||
explanation_text = "Have [target_amount] Vassal[target_amount==1?"":"s"] in the [department_string] department."
|
||||
|
||||
// WIN CONDITIONS?
|
||||
/datum/objective/bloodsucker/protege/check_completion()
|
||||
|
||||
var/datum/antagonist/bloodsucker/antagdatum = owner.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if (!antagdatum || antagdatum.vassals.len == 0)
|
||||
return FALSE
|
||||
|
||||
// Get list of all jobs that are qualified (for HEAD, this is already done)
|
||||
var/list/valid_jobs
|
||||
if (target_role == "HEAD")
|
||||
valid_jobs = roles
|
||||
else
|
||||
valid_jobs = list()
|
||||
var/list/alljobs = subtypesof(/datum/job) // This is just a list of TYPES, not the actual variables!
|
||||
for(var/T in alljobs)
|
||||
var/datum/job/J = SSjob.GetJobType(T) //
|
||||
if (!istype(J))
|
||||
continue
|
||||
// Found a job whose Dept Head matches either list of heads, or this job IS the head
|
||||
if ((target_role in J.department_head) || target_role == J.title)
|
||||
valid_jobs += J.title
|
||||
|
||||
|
||||
// Check Vassals, and see if they match
|
||||
var/objcount = 0
|
||||
var/list/counted_roles = list() // So you can't have more than one Captain count.
|
||||
for(var/datum/antagonist/vassal/V in antagdatum.vassals)
|
||||
if (!V || !V.owner) // Must exist somewhere, and as a vassal.
|
||||
continue
|
||||
|
||||
var/thisRole = "none"
|
||||
|
||||
// Mind Assigned
|
||||
if ((V.owner.assigned_role in valid_jobs) && !(V.owner.assigned_role in counted_roles))
|
||||
//to_chat(owner, "<span class='userdanger'>PROTEGE OBJECTIVE: (MIND ROLE)</span>")
|
||||
thisRole = V.owner.assigned_role
|
||||
// Mob Assigned
|
||||
else if ((V.owner.current.job in valid_jobs) && !(V.owner.current.job in counted_roles))
|
||||
//to_chat(owner, "<span class='userdanger'>PROTEGE OBJECTIVE: (MOB JOB)</span>")
|
||||
thisRole = V.owner.current.job
|
||||
// PDA Assigned
|
||||
else if (V.owner.current && ishuman(V.owner.current))
|
||||
var/mob/living/carbon/human/H = V.owner.current
|
||||
var/obj/item/card/id/I = H.wear_id ? H.wear_id.GetID() : null
|
||||
if (I && (I.assignment in valid_jobs) && !(I.assignment in counted_roles))
|
||||
//to_chat(owner, "<span class='userdanger'>PROTEGE OBJECTIVE: (GET ID)</span>")
|
||||
thisRole = I.assignment
|
||||
|
||||
// NO MATCH
|
||||
if (thisRole == "none")
|
||||
continue
|
||||
|
||||
// SUCCESS!
|
||||
objcount ++
|
||||
if (target_role == "HEAD")
|
||||
counted_roles += thisRole // Add to list so we don't count it again (but only if it's a Head)
|
||||
|
||||
// NOTE!!!!!!!!!!!
|
||||
|
||||
// Look for jobs value on mobs! This is assigned at start, but COULD be assigned from HoP?
|
||||
//
|
||||
// ALSO - Search through all jobs (look for prefs earlier that look for all jobs, and search through all jobs to see if their head matches the head listed, or it IS the head)
|
||||
//
|
||||
// ALSO - registered_account in _vending.dm for banks, and assigning new ones.
|
||||
|
||||
//to_chat(antagdatum.owner, "<span class='userdanger'>PROTEGE OBJECTIVE: Final Count: [objcount] of [antagdatum.vassals.len] vassals</span>")
|
||||
return objcount >= target_amount
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Eat blood from a lot of people
|
||||
/datum/objective/bloodsucker/gourmand
|
||||
|
||||
// HOW: Track each feed (if human). Count victory.
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Convert a crewmate
|
||||
/datum/objective/bloodsucker/embrace
|
||||
|
||||
// HOW: Find crewmate. Check if person is a bloodsucker
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Defile a facility with blood
|
||||
/datum/objective/bloodsucker/desecrate
|
||||
|
||||
// Space_Station_13_areas.dm <--- all the areas
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Destroy the Solar Arrays
|
||||
/datum/objective/bloodsucker/solars
|
||||
|
||||
// Space_Station_13_areas.dm <--- all the areas
|
||||
/datum/objective/bloodsucker/solars/update_explanation_text()
|
||||
explanation_text = "Prevent all solar arrays on the station from functioning."
|
||||
|
||||
/datum/objective/bloodsucker/solars/check_completion()
|
||||
// Sort through all /obj/machinery/power/solar_control in the station ONLY, and check that they are functioning.
|
||||
// Make sure that lastgen is 0 or connected_panels.len is 0. Doesnt matter if it's tracking.
|
||||
for (var/obj/machinery/power/solar_control/SC in SSsun.solars)
|
||||
// Check On Station:
|
||||
var/turf/T = get_turf(SC)
|
||||
if(!T || !is_station_level(T.z)) // <------ Taken from NukeOp
|
||||
//message_admins("DEBUG A: [SC] not on station!")
|
||||
continue // Not on station! We don't care about this.
|
||||
if (SC && SC.lastgen > 0 && SC.connected_panels.len > 0 && SC.connected_tracker)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Steal hearts. You just really wanna have some hearts.
|
||||
/datum/objective/bloodsucker/heartthief
|
||||
// NOTE: Look up /steal in objective.dm for inspiration.
|
||||
|
||||
// GENERATE!
|
||||
/datum/objective/bloodsucker/heartthief/generate_objective()
|
||||
target_amount = rand(2,3)
|
||||
|
||||
update_explanation_text()
|
||||
//dangerrating += target_amount * 2
|
||||
|
||||
// EXPLANATION
|
||||
/datum/objective/bloodsucker/heartthief/update_explanation_text()
|
||||
explanation_text = "Steal and keep [target_amount] heart[target_amount == 1 ? "" : "s"]." // TO DO: Limit them to Human Only!
|
||||
|
||||
// WIN CONDITIONS?
|
||||
/datum/objective/bloodsucker/heartthief/check_completion()
|
||||
// -Must have a body.
|
||||
if (!owner.current)
|
||||
return FALSE
|
||||
// Taken from /steal in objective.dm
|
||||
var/list/all_items = owner.current.GetAllContents() // Includes items inside other items.
|
||||
var/itemcount = FALSE
|
||||
for(var/obj/I in all_items) //Check for items
|
||||
if(I == /obj/item/organ/heart)
|
||||
itemcount ++
|
||||
if (itemcount >= target_amount) // Got the right amount?
|
||||
return TRUE
|
||||
|
||||
return FALSE
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/datum/objective/bloodsucker/survive
|
||||
martyr_compatible = FALSE
|
||||
|
||||
|
||||
// EXPLANATION
|
||||
/datum/objective/bloodsucker/survive/update_explanation_text()
|
||||
explanation_text = "Survive the entire shift without succumbing to Final Death."
|
||||
|
||||
// WIN CONDITIONS?
|
||||
/datum/objective/bloodsucker/survive/check_completion()
|
||||
// -Must have a body.
|
||||
if (!owner.current || !isliving(owner.current))
|
||||
return FALSE
|
||||
// Dead, without a head or heart? Cya
|
||||
return owner.current.stat != DEAD// || owner.current.HaveBloodsuckerBodyparts()
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/datum/objective/bloodsucker/vamphunter
|
||||
|
||||
// GENERATE!
|
||||
/datum/objective/bloodsucker/vamphunter/generate_objective()
|
||||
update_explanation_text()
|
||||
|
||||
// EXPLANATION
|
||||
/datum/objective/bloodsucker/vamphunter/update_explanation_text()
|
||||
explanation_text = "Destroy all Bloodsuckers on [station_name()]."
|
||||
|
||||
// WIN CONDITIONS?
|
||||
/datum/objective/bloodsucker/vamphunter/check_completion()
|
||||
for (var/datum/mind/M in SSticker.mode.bloodsuckers)
|
||||
if (M && M.current && M.current.stat != DEAD && get_turf(M.current))
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/datum/objective/bloodsucker/monsterhunter
|
||||
|
||||
// GENERATE!
|
||||
/datum/objective/bloodsucker/monsterhunter/generate_objective()
|
||||
update_explanation_text()
|
||||
|
||||
// EXPLANATION
|
||||
/datum/objective/bloodsucker/monsterhunter/update_explanation_text()
|
||||
explanation_text = "Destroy all monsters on [station_name()]."
|
||||
|
||||
// WIN CONDITIONS?
|
||||
/datum/objective/bloodsucker/monsterhunter/check_completion()
|
||||
var/list/datum/mind/monsters = list()
|
||||
monsters += SSticker.mode.bloodsuckers
|
||||
monsters += SSticker.mode.devils
|
||||
monsters += SSticker.mode.cult
|
||||
monsters += SSticker.mode.wizards
|
||||
monsters += SSticker.mode.apprentices
|
||||
//monsters += SSticker.mode.servants_of_ratvar
|
||||
//monsters += SSticker.mode.changelings disabled anyways
|
||||
|
||||
for (var/datum/mind/M in monsters)
|
||||
if (M && M != owner && M.current && M.current.stat != DEAD && get_turf(M.current))
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/datum/objective/bloodsucker/vassal
|
||||
|
||||
// GENERATE!
|
||||
/datum/objective/bloodsucker/vassal/generate_objective()
|
||||
update_explanation_text()
|
||||
|
||||
// EXPLANATION
|
||||
/datum/objective/bloodsucker/vassal/update_explanation_text()
|
||||
explanation_text = "Guarantee the success of your Master's mission!"
|
||||
|
||||
// WIN CONDITIONS?
|
||||
/datum/objective/bloodsucker/vassal/check_completion()
|
||||
var/datum/antagonist/vassal/antag_datum = owner.has_antag_datum(ANTAG_DATUM_VASSAL)
|
||||
return antag_datum.master && antag_datum.master.owner && antag_datum.master.owner.current && antag_datum.master.owner.current.stat != DEAD
|
||||
@@ -0,0 +1,284 @@
|
||||
/datum/action/bloodsucker
|
||||
name = "Vampiric Gift"
|
||||
desc = "A vampiric gift."
|
||||
button_icon = 'icons/mob/actions/bloodsucker.dmi' //This is the file for the BACKGROUND icon
|
||||
background_icon_state = "vamp_power_off" //And this is the state for the background icon
|
||||
var/background_icon_state_on = "vamp_power_on" // FULP: Our "ON" icon alternative.
|
||||
var/background_icon_state_off = "vamp_power_off" // FULP: Our "OFF" icon alternative.
|
||||
icon_icon = 'icons/mob/actions/bloodsucker.dmi' //This is the file for the ACTION icon
|
||||
button_icon_state = "power_feed" //And this is the state for the action icon
|
||||
buttontooltipstyle = "cult"
|
||||
|
||||
// Action-Related
|
||||
//var/amPassive = FALSE // REMOVED: Just made it its own kind. // Am I just "on" at all times? (aka NO ICON)
|
||||
var/amTargetted = FALSE // Am I asked to choose a target when enabled? (Shows as toggled ON when armed)
|
||||
var/amToggle = FALSE // Can I be actively turned on and off?
|
||||
var/amSingleUse = FALSE // Am I removed after a single use?
|
||||
var/active = FALSE
|
||||
var/cooldown = 20 // 10 ticks, 1 second.
|
||||
var/cooldownUntil = 0 // From action.dm: next_use_time = world.time + cooldown_time
|
||||
// Power-Related
|
||||
var/level_current = 0 // Can increase to yield new abilities. Each power goes up in strength each Rank.
|
||||
//var/level_max = 1 //
|
||||
var/bloodcost = 10
|
||||
var/needs_button = TRUE // Taken from Changeling - for passive abilities that dont need a button
|
||||
var/bloodsucker_can_buy = FALSE // Must be a bloodsucker to use this power.
|
||||
var/warn_constant_cost = FALSE // Some powers charge you for staying on. Masquerade, Cloak, Veil, etc.
|
||||
var/can_use_in_torpor = FALSE // Most powers don't function if you're in torpor.
|
||||
var/must_be_capacitated = FALSE // Some powers require you to be standing and ready.
|
||||
var/can_be_immobilized = FALSE // Brawn can be used when incapacitated/laying if it's because you're being immobilized. NOTE: If must_be_capacitated is FALSE, this is irrelevant.
|
||||
var/can_be_staked = FALSE // Only Feed can happen with a stake in you.
|
||||
var/cooldown_static = FALSE // Feed, Masquerade, and One-Shot powers don't improve their cooldown.
|
||||
//var/not_bloodsucker = FALSE // This goes to Vassals or Hunters, but NOT bloodsuckers.
|
||||
|
||||
/datum/action/bloodsucker/New()
|
||||
if (bloodcost > 0)
|
||||
desc += "<br><br><b>COST:</b> [bloodcost] Blood" // Modify description to add cost.
|
||||
if (warn_constant_cost)
|
||||
desc += "<br><br><i>Your over-time blood consumption increases while [name] is active.</i>"
|
||||
if (amSingleUse)
|
||||
desc += "<br><br><i>Useable once per night.</i>"
|
||||
..()
|
||||
|
||||
// NOTES
|
||||
//
|
||||
// click.dm <--- Where we can take over mouse clicks
|
||||
// spells.dm /add_ranged_ability() <--- How we take over the mouse click to use a power on a target.
|
||||
|
||||
/datum/action/bloodsucker/Trigger()
|
||||
// Active? DEACTIVATE AND END!
|
||||
if (active && CheckCanDeactivate(TRUE))
|
||||
DeactivatePower()
|
||||
return
|
||||
if (!CheckCanPayCost(TRUE) || !CheckCanUse(TRUE))
|
||||
return
|
||||
PayCost()
|
||||
if (amToggle)
|
||||
active = !active
|
||||
UpdateButtonIcon()
|
||||
if (!amToggle || !active)
|
||||
StartCooldown() // Must come AFTER UpdateButton(), otherwise icon will revert.
|
||||
ActivatePower() // NOTE: ActivatePower() freezes this power in place until it ends.
|
||||
if (active) // Did we not manually disable? Handle it here.
|
||||
DeactivatePower()
|
||||
if (amSingleUse)
|
||||
RemoveAfterUse()
|
||||
|
||||
/datum/action/bloodsucker/proc/CheckCanPayCost(display_error)
|
||||
if(!owner || !owner.mind)
|
||||
return FALSE
|
||||
// Cooldown?
|
||||
if (cooldownUntil > world.time)
|
||||
if (display_error)
|
||||
to_chat(owner, "[src] is unavailable. Wait [(cooldownUntil - world.time) / 10] seconds.")
|
||||
return FALSE
|
||||
// Have enough blood?
|
||||
var/mob/living/L = owner
|
||||
if (L.blood_volume < bloodcost)
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>You need at least [bloodcost] blood to activate [name]</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/proc/CheckCanUse(display_error) // These checks can be scanned every frame while a ranged power is on.
|
||||
if(!owner || !owner.mind)
|
||||
return FALSE
|
||||
// Torpor?
|
||||
if(!can_use_in_torpor && HAS_TRAIT(owner, TRAIT_DEATHCOMA))
|
||||
if(display_error)
|
||||
to_chat(owner, "<span class='warning'>Not while you're in Torpor.</span>")
|
||||
return FALSE
|
||||
// Stake?
|
||||
if(!can_be_staked && owner.AmStaked())
|
||||
if(display_error)
|
||||
to_chat(owner, "<span class='warning'>You have a stake in your chest! Your powers are useless.</span>")
|
||||
return FALSE
|
||||
// Incap?
|
||||
if(must_be_capacitated)
|
||||
var/mob/living/L = owner
|
||||
if (L.incapacitated(TRUE, TRUE) || L.resting && !can_be_immobilized)
|
||||
if(display_error)
|
||||
to_chat(owner, "<span class='warning'>Not while you're incapacitated!</span>")
|
||||
return FALSE
|
||||
// Constant Cost (out of blood)
|
||||
if(warn_constant_cost)
|
||||
var/mob/living/L = owner
|
||||
if(L.blood_volume <= 0)
|
||||
if(display_error)
|
||||
to_chat(owner, "<span class='warning'>You don't have the blood to upkeep [src].</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/proc/StartCooldown()
|
||||
set waitfor = FALSE
|
||||
// Alpha Out
|
||||
button.color = rgb(128,0,0,128)
|
||||
button.alpha = 100
|
||||
// Calculate Cooldown (by power's level)
|
||||
var/this_cooldown = (cooldown_static || amSingleUse) ? cooldown : max(cooldown / 2, cooldown - (cooldown / 16 * (level_current-1)))
|
||||
// NOTE: With this formula, you'll hit half cooldown at level 8 for that power.
|
||||
|
||||
// Wait for cooldown
|
||||
cooldownUntil = world.time + this_cooldown
|
||||
spawn(this_cooldown)
|
||||
// Alpha In
|
||||
button.color = rgb(255,255,255,255)
|
||||
button.alpha = 255
|
||||
|
||||
/datum/action/bloodsucker/proc/CheckCanDeactivate(display_error)
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/UpdateButtonIcon(force = FALSE)
|
||||
background_icon_state = active? background_icon_state_on : background_icon_state_off
|
||||
..()//UpdateButtonIcon()
|
||||
|
||||
|
||||
/datum/action/bloodsucker/proc/PayCost()
|
||||
// owner for actions is the mob, not mind.
|
||||
var/mob/living/L = owner
|
||||
L.blood_volume -= bloodcost
|
||||
|
||||
|
||||
/datum/action/bloodsucker/proc/ActivatePower()
|
||||
|
||||
|
||||
/datum/action/bloodsucker/proc/DeactivatePower(mob/living/user = owner, mob/living/target)
|
||||
active = FALSE
|
||||
UpdateButtonIcon()
|
||||
StartCooldown()
|
||||
|
||||
/datum/action/bloodsucker/proc/ContinueActive(mob/living/user, mob/living/target) // Used by loops to make sure this power can stay active.
|
||||
return active && user && (!warn_constant_cost || user.blood_volume > 0)
|
||||
|
||||
/datum/action/bloodsucker/proc/RemoveAfterUse()
|
||||
// Un-Learn Me! (GO HOME
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if (istype(bloodsuckerdatum))
|
||||
bloodsuckerdatum.powers -= src
|
||||
Remove(owner)
|
||||
|
||||
/datum/action/bloodsucker/proc/Upgrade()
|
||||
level_current ++
|
||||
|
||||
/////////////////////////////////// PASSIVE POWERS ///////////////////////////////////
|
||||
|
||||
// New Type: Passive (Always on, no button)
|
||||
/datum/action/bloodsucker/passive
|
||||
|
||||
/datum/action/bloodsucker/passive/New()
|
||||
// REMOVED: DO NOTHBING!
|
||||
..()
|
||||
// Don't Display Button! (it doesn't do anything anyhow)
|
||||
button.screen_loc = DEFAULT_BLOODSPELLS
|
||||
button.moved = DEFAULT_BLOODSPELLS
|
||||
button.ordered = FALSE
|
||||
/datum/action/bloodsucker/passive/Destroy()
|
||||
if(owner)
|
||||
Remove(owner)
|
||||
target = null
|
||||
|
||||
/////////////////////////////////// TARGETTED POWERS ///////////////////////////////////
|
||||
|
||||
/datum/action/bloodsucker/targeted
|
||||
// NOTE: All Targeted spells are Toggles! We just don't bother checking here.
|
||||
var/target_range = 99
|
||||
var/message_Trigger = "Select a target."
|
||||
var/obj/effect/proc_holder/bloodsucker/bs_proc_holder
|
||||
var/power_activates_immediately = TRUE // Most powers happen the moment you click. Some, like Mesmerize, require time and shouldn't cost you if they fail.
|
||||
|
||||
var/power_in_use = FALSE // Is this power LOCKED due to being used?
|
||||
|
||||
/datum/action/bloodsucker/targeted/New(Target)
|
||||
desc += "<br>\[<i>Targeted Power</i>\]" // Modify description to add notice that this is aimed.
|
||||
..()
|
||||
// Create Proc Holder for intercepting clicks
|
||||
bs_proc_holder = new ()
|
||||
bs_proc_holder.linked_power = src
|
||||
|
||||
// Click power: Begin Aim
|
||||
/datum/action/bloodsucker/targeted/Trigger()
|
||||
if(active && CheckCanDeactivate(TRUE))
|
||||
DeactivateRangedAbility()
|
||||
DeactivatePower()
|
||||
return
|
||||
if(!CheckCanPayCost(TRUE) || !CheckCanUse(TRUE))
|
||||
return
|
||||
active = !active
|
||||
UpdateButtonIcon()
|
||||
// Create & Link Targeting Proc
|
||||
var/mob/living/L = owner
|
||||
if(L.ranged_ability)
|
||||
L.ranged_ability.remove_ranged_ability()
|
||||
bs_proc_holder.add_ranged_ability(L)
|
||||
|
||||
if(message_Trigger != "")
|
||||
to_chat(owner, "<span class='announce'>[message_Trigger]</span>")
|
||||
|
||||
/datum/action/bloodsucker/targeted/CheckCanUse(display_error)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
if(!owner.client) // <--- We don't allow non client usage so that using powers like mesmerize will FAIL if you try to use them as ghost. Why? because ranged_abvility in spell.dm
|
||||
return FALSE // doesn't let you remove powers if you're not there. So, let's just cancel the power entirely.
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/targeted/DeactivatePower(mob/living/user = owner, mob/living/target)
|
||||
// Don't run ..(), we don't want to engage the cooldown until we USE this power!
|
||||
active = FALSE
|
||||
UpdateButtonIcon()
|
||||
|
||||
/datum/action/bloodsucker/targeted/proc/DeactivateRangedAbility()
|
||||
// Only Turned off when CLICK is disabled...aka, when you successfully clicked (or
|
||||
bs_proc_holder.remove_ranged_ability()
|
||||
|
||||
// Check if target is VALID (wall, turf, or character?)
|
||||
/datum/action/bloodsucker/targeted/proc/CheckValidTarget(atom/A)
|
||||
return FALSE // FALSE targets nothing.
|
||||
|
||||
// Check if valid target meets conditions
|
||||
/datum/action/bloodsucker/targeted/proc/CheckCanTarget(atom/A, display_error)
|
||||
// Out of Range
|
||||
if(!(A in view(target_range, owner)))
|
||||
if(display_error && target_range > 1) // Only warn for range if it's greater than 1. Brawn doesn't need to announce itself.
|
||||
to_chat(owner, "<span class='warning'>Your target is out of range.</span>")
|
||||
return FALSE
|
||||
return istype(A)
|
||||
|
||||
// Click Target
|
||||
/datum/action/bloodsucker/targeted/proc/ClickWithPower(atom/A)
|
||||
// CANCEL RANGED TARGET check
|
||||
if(power_in_use || !CheckValidTarget(A))
|
||||
return FALSE
|
||||
// Valid? (return true means DON'T cancel power!)
|
||||
if(!CheckCanPayCost(TRUE) || !CheckCanUse(TRUE) || !CheckCanTarget(A, TRUE))
|
||||
return TRUE
|
||||
// Skip this part so we can return TRUE right away.
|
||||
if(power_activates_immediately)
|
||||
PowerActivatedSuccessfully() // Mesmerize pays only after success.
|
||||
power_in_use = TRUE // Lock us into this ability until it successfully fires off. Otherwise, we pay the blood even if we fail.
|
||||
FireTargetedPower(A) // We use this instead of ActivatePower(), which has no input
|
||||
power_in_use = FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/targeted/proc/FireTargetedPower(atom/A)
|
||||
// Like ActivatePower, but specific to Targeted (and takes an atom input). We don't use ActivatePower for targeted.
|
||||
|
||||
/datum/action/bloodsucker/targeted/proc/PowerActivatedSuccessfully()
|
||||
// The power went off! We now pay the cost of the power.
|
||||
PayCost()
|
||||
DeactivateRangedAbility()
|
||||
DeactivatePower()
|
||||
StartCooldown() // Do AFTER UpdateIcon() inside of DeactivatePower. Otherwise icon just gets wiped.
|
||||
|
||||
/datum/action/bloodsucker/targeted/ContinueActive(mob/living/user, mob/living/target) // Used by loops to make sure this power can stay active.
|
||||
return ..()
|
||||
// Target Proc Holder
|
||||
/obj/effect/proc_holder/bloodsucker
|
||||
var/datum/action/bloodsucker/targeted/linked_power
|
||||
|
||||
/obj/effect/proc_holder/bloodsucker/remove_ranged_ability(msg)
|
||||
..()
|
||||
linked_power.DeactivatePower()
|
||||
|
||||
/obj/effect/proc_holder/bloodsucker/InterceptClickOn(mob/living/caller, params, atom/A)
|
||||
return linked_power.ClickWithPower(A)
|
||||
@@ -0,0 +1,195 @@
|
||||
#define TIME_BLOODSUCKER_NIGHT 900 // 15 minutes
|
||||
#define TIME_BLOODSUCKER_DAY_WARN 90 // 1.5 minutes
|
||||
#define TIME_BLOODSUCKER_DAY_FINAL_WARN 25 // 25 sec
|
||||
#define TIME_BLOODSUCKER_DAY 60 // 1.5 minutes // 10 is a second, 600 is a minute.
|
||||
#define TIME_BLOODSUCKER_BURN_INTERVAL 40 // 4 sec
|
||||
|
||||
|
||||
// Over Time, tick down toward a "Solar Flare" of UV buffeting the station. This period is harmful to vamps.
|
||||
/obj/effect/sunlight
|
||||
//var/amDay = FALSE
|
||||
var/cancel_me = FALSE
|
||||
var/amDay = FALSE
|
||||
var/time_til_cycle = 0
|
||||
|
||||
/obj/effect/sunlight/Initialize()
|
||||
countdown()
|
||||
hud_tick()
|
||||
|
||||
/obj/effect/sunlight/proc/countdown()
|
||||
set waitfor = FALSE
|
||||
|
||||
while(!cancel_me)
|
||||
|
||||
time_til_cycle = TIME_BLOODSUCKER_NIGHT
|
||||
|
||||
// Part 1: Night (all is well)
|
||||
while(time_til_cycle > TIME_BLOODSUCKER_DAY_WARN)
|
||||
sleep(10)
|
||||
if(cancel_me)
|
||||
return
|
||||
//sleep(TIME_BLOODSUCKER_NIGHT - TIME_BLOODSUCKER_DAY_WARN)
|
||||
warn_daylight(1,"<span class = 'danger'>Solar Flares will bombard the station with dangerous UV in [TIME_BLOODSUCKER_DAY_WARN / 60] minutes. <b>Prepare to seek cover in a coffin or closet.</b></span>") // time2text <-- use Help On
|
||||
give_home_power() // Give VANISHING ACT power to all vamps with a lair!
|
||||
|
||||
// Part 2: Night Ending
|
||||
while(time_til_cycle > TIME_BLOODSUCKER_DAY_FINAL_WARN)
|
||||
sleep(10)
|
||||
if(cancel_me)
|
||||
return
|
||||
//sleep(TIME_BLOODSUCKER_DAY_WARN - TIME_BLOODSUCKER_DAY_FINAL_WARN)
|
||||
message_admins("BLOODSUCKER NOTICE: Daylight beginning in [TIME_BLOODSUCKER_DAY_FINAL_WARN] seconds.)")
|
||||
warn_daylight(2,"<span class = 'userdanger'>Solar Flares are about to bombard the station! You have [TIME_BLOODSUCKER_DAY_FINAL_WARN] seconds to find cover!</span>",\
|
||||
"<span class = 'danger'>In [TIME_BLOODSUCKER_DAY_FINAL_WARN / 10], your master will be at risk of a Solar Flare. Make sure they find cover!</span>")
|
||||
|
||||
// (FINAL LIL WARNING)
|
||||
while(time_til_cycle > 5)
|
||||
sleep(10)
|
||||
if (cancel_me)
|
||||
return
|
||||
//sleep(TIME_BLOODSUCKER_DAY_FINAL_WARN - 50)
|
||||
warn_daylight(3,"<span class = 'userdanger'>Seek cover, for Sol rises!</span>")
|
||||
|
||||
// Part 3: Night Ending
|
||||
while (time_til_cycle > 0)
|
||||
sleep(10)
|
||||
if (cancel_me)
|
||||
return
|
||||
//sleep(50)
|
||||
warn_daylight(4,"<span class = 'userdanger'>Solar flares bombard the station with deadly UV light!</span><br><span class = ''>Stay in cover for the next [TIME_BLOODSUCKER_DAY / 60] minutes or risk Final Death!</span>",\
|
||||
"<span class = 'danger'>Solar flares bombard the station with UV light!</span>")
|
||||
|
||||
// Part 4: Day
|
||||
amDay = TRUE
|
||||
message_admins("BLOODSUCKER NOTICE: Daylight Beginning (Lasts for [TIME_BLOODSUCKER_DAY / 60] minutes.)")
|
||||
time_til_cycle = TIME_BLOODSUCKER_DAY
|
||||
sleep(10) // One second grace period.
|
||||
//var/daylight_time = TIME_BLOODSUCKER_DAY
|
||||
var/issued_XP = FALSE
|
||||
while(time_til_cycle > 0)
|
||||
punish_vamps()
|
||||
sleep(TIME_BLOODSUCKER_BURN_INTERVAL)
|
||||
if (cancel_me)
|
||||
return
|
||||
//daylight_time -= TIME_BLOODSUCKER_BURN_INTERVAL
|
||||
// Issue Level Up!
|
||||
if(!issued_XP && time_til_cycle <= 15)
|
||||
issued_XP = TRUE
|
||||
vamps_rank_up()
|
||||
|
||||
warn_daylight(5,"<span class = 'announce'>The solar flare has ended, and the daylight danger has passed...for now.</span>",\
|
||||
"<span class = 'announce'>The solar flare has ended, and the daylight danger has passed...for now.</span>")
|
||||
amDay = FALSE
|
||||
day_end() // Remove VANISHING ACT power from all vamps who have it! Clear Warnings (sunlight, locker protection)
|
||||
message_admins("BLOODSUCKER NOTICE: Daylight Ended. Resetting to Night (Lasts for [TIME_BLOODSUCKER_NIGHT / 60] minutes.)")
|
||||
|
||||
|
||||
/obj/effect/sunlight/proc/hud_tick()
|
||||
set waitfor = FALSE
|
||||
while(!cancel_me)
|
||||
// Update all Bloodsucker sunlight huds
|
||||
for(var/datum/mind/M in SSticker.mode.bloodsuckers)
|
||||
if(!istype(M) || !istype(M.current))
|
||||
continue
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(istype(bloodsuckerdatum))
|
||||
bloodsuckerdatum.update_sunlight(max(0, time_til_cycle), amDay) // This pings all HUDs
|
||||
sleep(10)
|
||||
time_til_cycle --
|
||||
|
||||
/obj/effect/sunlight/proc/warn_daylight(danger_level=0, vampwarn = "", vassalwarn = "")
|
||||
for(var/datum/mind/M in SSticker.mode.bloodsuckers)
|
||||
if(!istype(M))
|
||||
continue
|
||||
to_chat(M,vampwarn)
|
||||
if(M.current)
|
||||
if(danger_level == 1)
|
||||
M.current.playsound_local(null, 'sound/chatter/griffin_3.ogg', 50 + danger_level, 1)
|
||||
else if(danger_level == 2)
|
||||
M.current.playsound_local(null, 'sound/chatter/griffin_5.ogg', 50 + danger_level, 1)
|
||||
else if(danger_level == 3)
|
||||
M.current.playsound_local(null, 'sound/effects/alert.ogg', 75, 1)
|
||||
else if(danger_level == 4)
|
||||
M.current.playsound_local(null, 'sound/ambience/ambimystery.ogg', 100, 1)
|
||||
else if(danger_level == 5)
|
||||
M.current.playsound_local(null, 'sound/spookoween/ghosty_wind.ogg', 90, 1)
|
||||
|
||||
if(vassalwarn != "")
|
||||
for(var/datum/mind/M in SSticker.mode.vassals)
|
||||
if(!istype(M))
|
||||
continue
|
||||
to_chat(M,vassalwarn)
|
||||
|
||||
|
||||
/obj/effect/sunlight/proc/punish_vamps()
|
||||
// Cycle through all vamp antags and check if they're inside a closet.
|
||||
for(var/datum/mind/M in SSticker.mode.bloodsuckers)
|
||||
if(!istype(M) || !istype(M.current))
|
||||
continue
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(!istype(bloodsuckerdatum))
|
||||
continue
|
||||
// Closets offer SOME protection
|
||||
if(istype(M.current.loc, /obj/structure))
|
||||
// Coffins offer the BEST protection
|
||||
if(istype(M.current.loc, /obj/structure/closet/crate/coffin))
|
||||
SEND_SIGNAL(M.current, COMSIG_ADD_MOOD_EVENT, "vampsleep", /datum/mood_event/coffinsleep)
|
||||
continue
|
||||
else
|
||||
if(!bloodsuckerdatum.warn_sun_locker)
|
||||
to_chat(M, "<span class='warning'>Your skin sizzles. The [M.current.loc] doesn't protect well against UV bombardment.</span>")
|
||||
bloodsuckerdatum.warn_sun_locker = TRUE
|
||||
M.current.adjustFireLoss(0.5 + bloodsuckerdatum.vamplevel / 2) // M.current.fireloss += 0.5 + bloodsuckerdatum.vamplevel / 2 // Do DIRECT damage. Being spaced was causing this to not occur. setFireLoss(bloodsuckerdatum.vamplevel)
|
||||
M.current.updatehealth()
|
||||
SEND_SIGNAL(M.current, COMSIG_ADD_MOOD_EVENT, "vampsleep", /datum/mood_event/daylight_1)
|
||||
// Out in the Open? Buh Bye
|
||||
else
|
||||
if(!bloodsuckerdatum.warn_sun_burn)
|
||||
if(bloodsuckerdatum.vamplevel > 0)
|
||||
to_chat(M, "<span class='userdanger'>The solar flare sets your skin ablaze!</span>")
|
||||
else
|
||||
to_chat(M, "<span class='userdanger'>The solar flare scalds your neophyte skin!</span>")
|
||||
bloodsuckerdatum.warn_sun_burn = TRUE
|
||||
if(M.current.fire_stacks <= 0)
|
||||
M.current.fire_stacks = 0
|
||||
if(bloodsuckerdatum.vamplevel > 0)
|
||||
M.current.adjust_fire_stacks(0.2 + bloodsuckerdatum.vamplevel / 10)
|
||||
M.current.IgniteMob()
|
||||
M.current.adjustFireLoss(2 + bloodsuckerdatum.vamplevel) // M.current.fireloss += 2 + bloodsuckerdatum.vamplevel // Do DIRECT damage. Being spaced was causing this to not occur. //setFireLoss(2 + bloodsuckerdatum.vamplevel)
|
||||
M.current.updatehealth()
|
||||
SEND_SIGNAL(M.current, COMSIG_ADD_MOOD_EVENT, "vampsleep", /datum/mood_event/daylight_2)
|
||||
|
||||
/obj/effect/sunlight/proc/day_end()
|
||||
for(var/datum/mind/M in SSticker.mode.bloodsuckers)
|
||||
if(!istype(M) || !istype(M.current))
|
||||
continue
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(!istype(bloodsuckerdatum))
|
||||
continue
|
||||
// Reset Warnings
|
||||
bloodsuckerdatum.warn_sun_locker = FALSE
|
||||
bloodsuckerdatum.warn_sun_burn = FALSE
|
||||
// Remove Dawn Powers
|
||||
for(var/datum/action/bloodsucker/P in bloodsuckerdatum.powers)
|
||||
if(istype(P, /datum/action/bloodsucker/gohome))
|
||||
bloodsuckerdatum.powers -= P
|
||||
P.Remove(M.current)
|
||||
|
||||
/obj/effect/sunlight/proc/vamps_rank_up()
|
||||
set waitfor = FALSE
|
||||
// Cycle through all vamp antags and check if they're inside a closet.
|
||||
for(var/datum/mind/M in SSticker.mode.bloodsuckers)
|
||||
if(!istype(M) || !istype(M.current))
|
||||
continue
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(istype(bloodsuckerdatum))
|
||||
bloodsuckerdatum.RankUp() // Rank up! Must still be in a coffin to level!
|
||||
|
||||
/obj/effect/sunlight/proc/give_home_power()
|
||||
// It's late...! Give the "Vanishing Act" gohome power to bloodsuckers.
|
||||
for(var/datum/mind/M in SSticker.mode.bloodsuckers)
|
||||
if(!istype(M) || !istype(M.current))
|
||||
continue
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(istype(bloodsuckerdatum) && bloodsuckerdatum.lair && !(locate(/datum/action/bloodsucker/gohome) in bloodsuckerdatum.powers))
|
||||
bloodsuckerdatum.BuyPower(new /datum/action/bloodsucker/gohome)
|
||||
@@ -0,0 +1,116 @@
|
||||
|
||||
|
||||
// For all things visual, such as leveling up
|
||||
|
||||
|
||||
// Look up: _vending.dm proc/ui_interact()
|
||||
// Malf_Modules.dm proc/use()
|
||||
|
||||
/*
|
||||
/datum/antagonist/bloodsucker/proc/LevelUpMenu()
|
||||
var/list/dat = list()
|
||||
|
||||
dat += "<h3>You have become more ancient.<BR>Direct the path of your blood</h3>"
|
||||
dat += "<HR>"
|
||||
|
||||
// Step One: Decide powers you CAN buy.
|
||||
for(var/pickedpower in typesof(/datum/action/bloodsucker))
|
||||
var/obj/effect/proc_holder/spell/bloodsucker/power = pickedpower
|
||||
// NAME
|
||||
dat += "<A href='byond://?src=[REF(src)];[module.mod_pick_name]=1'>[power.name]</A>"
|
||||
// COST
|
||||
dat += "<td align='right'><b>[power.name] </b><a href='byond://?src=[REF(src)];vend=[REF(R)]'>Vend</a></td>"
|
||||
dat == "<BR>"
|
||||
|
||||
var/datum/browser/popup = new(owner.current, "bloodsuckerrank", "Bloodsucker Rank Up")
|
||||
popup.set_content(dat.Join())
|
||||
popup.open()
|
||||
|
||||
/datum/antagonist/bloodsucker/Topic(href, href_list)
|
||||
if(..())
|
||||
return
|
||||
*/
|
||||
|
||||
|
||||
// From browser.dm: /datum/browser/New(nuser, nwindow_id, ntitle = 0, nwidth = 0, nheight = 0, var/atom/nref = null)
|
||||
|
||||
|
||||
/*
|
||||
var/list/dat = list()
|
||||
dat += "<B>Select use of processing time: (currently #[processing_time] left.)</B><BR>"
|
||||
dat += "<HR>"
|
||||
dat += "<B>Install Module:</B><BR>"
|
||||
dat += "<I>The number afterwards is the amount of processing time it consumes.</I><BR>"
|
||||
for(var/datum/AI_Module/large/module in possible_modules)
|
||||
dat += "<A href='byond://?src=[REF(src)];[module.mod_pick_name]=1'>[module.module_name]</A><A href='byond://?src=[REF(src)];showdesc=[module.mod_pick_name]'>\[?\]</A> ([module.cost])<BR>"
|
||||
for(var/datum/AI_Module/small/module in possible_modules)
|
||||
dat += "<A href='byond://?src=[REF(src)];[module.mod_pick_name]=1'>[module.module_name]</A><A href='byond://?src=[REF(src)];showdesc=[module.mod_pick_name]'>\[?\]</A> ([module.cost])<BR>"
|
||||
dat += "<HR>"
|
||||
if(temp)
|
||||
dat += "[temp]"
|
||||
var/datum/browser/popup = new(user, "modpicker", "Malf Module Menu")
|
||||
popup.set_content(dat.Join())
|
||||
popup.open()
|
||||
*/
|
||||
|
||||
/*
|
||||
var/dat = ""
|
||||
var/datum/bank_account/account
|
||||
var/mob/living/carbon/human/H
|
||||
var/obj/item/card/id/C
|
||||
if(ishuman(user))
|
||||
H = user
|
||||
C = H.get_idcard(TRUE)
|
||||
|
||||
if(!C)
|
||||
dat += "<font color = 'red'><h3>No ID Card detected!</h3></font>"
|
||||
else if (!C.registered_account)
|
||||
dat += "<font color = 'red'><h3>No account on registered ID card!</h3></font>"
|
||||
if(onstation && C && C.registered_account)
|
||||
account = C.registered_account
|
||||
dat += "<h3>Select an item</h3>"
|
||||
dat += "<div class='statusDisplay'>"
|
||||
if(!product_records.len)
|
||||
dat += "<font color = 'red'>No product loaded!</font>"
|
||||
else
|
||||
var/list/display_records = product_records + coin_records
|
||||
if(extended_inventory)
|
||||
display_records = product_records + coin_records + hidden_records
|
||||
dat += "<table>"
|
||||
for (var/datum/data/vending_product/R in display_records)
|
||||
var/price_listed = "$[default_price]"
|
||||
var/is_hidden = hidden_records.Find(R)
|
||||
if(is_hidden && !extended_inventory)
|
||||
continue
|
||||
if(R.custom_price)
|
||||
price_listed = "$[R.custom_price]"
|
||||
if(!onstation || account && account.account_job && account.account_job.paycheck_department == payment_department)
|
||||
price_listed = "FREE"
|
||||
if(coin_records.Find(R) || is_hidden)
|
||||
price_listed = "$[R.custom_premium_price ? R.custom_premium_price : extra_price]"
|
||||
dat += "<tr><td><img src='data:image/jpeg;base64,[GetIconForProduct(R)]'/></td>"
|
||||
dat += "<td style=\"width: 100%\"><b>[sanitize(R.name)] ([price_listed])</b></td>"
|
||||
if(R.amount > 0 && ((C && C.registered_account && onstation) || (!onstation && isliving(user))))
|
||||
dat += "<td align='right'><b>[R.amount] </b><a href='byond://?src=[REF(src)];vend=[REF(R)]'>Vend</a></td>"
|
||||
else
|
||||
dat += "<td align='right'><span class='linkOff'>Not Available</span></td>"
|
||||
dat += "</tr>"
|
||||
dat += "</table>"
|
||||
dat += "</div>"
|
||||
if(onstation && C && C.registered_account)
|
||||
dat += "<b>Balance: $[account.account_balance]</b>"
|
||||
if(istype(src, /obj/machinery/vending/snack))
|
||||
dat += "<h3>Chef's Food Selection</h3>"
|
||||
dat += "<div class='statusDisplay'>"
|
||||
for (var/O in dish_quants)
|
||||
if(dish_quants[O] > 0)
|
||||
var/N = dish_quants[O]
|
||||
dat += "<a href='byond://?src=[REF(src)];dispense=[sanitize(O)]'>Dispense</A> "
|
||||
dat += "<B>[capitalize(O)] ($[default_price]): [N]</B><br>"
|
||||
dat += "</div>"
|
||||
|
||||
var/datum/browser/popup = new(user, "vending", (name))
|
||||
popup.set_content(dat)
|
||||
popup.set_title_image(user.browse_rsc_icon(icon, icon_state))
|
||||
popup.open()
|
||||
*/
|
||||
@@ -0,0 +1,763 @@
|
||||
/datum/team/vampireclan
|
||||
name = "Clan" // Teravanni,
|
||||
|
||||
/datum/antagonist/bloodsucker
|
||||
name = "Bloodsucker"
|
||||
roundend_category = "bloodsuckers"
|
||||
antagpanel_category = "Bloodsucker"
|
||||
job_rank = ROLE_BLOODSUCKER
|
||||
|
||||
// NAME
|
||||
var/vampname // My Dracula name
|
||||
var/vamptitle // My Dracula title
|
||||
var/vampreputation // My "Surname" or description of my deeds
|
||||
// CLAN
|
||||
var/datum/team/vampireclan/clan
|
||||
var/list/datum/antagonist/vassal/vassals = list()// Vassals under my control. Periodically remove the dead ones.
|
||||
var/datum/mind/creator // Who made me? For both Vassals AND Bloodsuckers (though Master Vamps won't have one)
|
||||
// POWERS
|
||||
var/list/datum/action/powers = list()// Purchased powers
|
||||
var/poweron_feed = FALSE // Am I feeding?
|
||||
var/poweron_masquerade = FALSE
|
||||
// STATS
|
||||
var/vamplevel = 0
|
||||
var/vamplevel_unspent = 1
|
||||
var/regenRate = 0.3 // How many points of Brute do I heal per tick?
|
||||
var/feedAmount = 15 // Amount of blood drawn from a target per tick.
|
||||
var/maxBloodVolume = 600 // Maximum blood a Vamp can hold via feeding. // BLOOD_VOLUME_NORMAL 550 // BLOOD_VOLUME_SAFE 475 //BLOOD_VOLUME_OKAY 336 //BLOOD_VOLUME_BAD 224 // BLOOD_VOLUME_SURVIVE 122
|
||||
// OBJECTIVES
|
||||
var/list/datum/objective/objectives_given = list() // For removal if needed.
|
||||
var/area/lair
|
||||
var/obj/structure/closet/crate/coffin
|
||||
// TRACKING
|
||||
var/foodInGut = 0 // How much food to throw up later. You shouldn't have eaten that.
|
||||
var/warn_sun_locker = FALSE // So we only get the locker burn message once per day.
|
||||
var/warn_sun_burn = FALSE // So we only get the sun burn message once per day.
|
||||
var/had_toxlover = FALSE
|
||||
// LISTS
|
||||
var/static/list/defaultTraits = list (TRAIT_STABLEHEART, TRAIT_NOBREATH, TRAIT_SLEEPIMMUNE, TRAIT_NOCRITDAMAGE, TRAIT_RESISTCOLD, TRAIT_RADIMMUNE, TRAIT_NIGHT_VISION, \
|
||||
TRAIT_NOSOFTCRIT, TRAIT_NOHARDCRIT, TRAIT_AGEUSIA, TRAIT_COLDBLOODED, TRAIT_NONATURALHEAL, TRAIT_NOMARROW, TRAIT_NOPULSE, TRAIT_VIRUSIMMUNE)
|
||||
// NOTES: TRAIT_AGEUSIA <-- Doesn't like flavors.
|
||||
// REMOVED: TRAIT_NODEATH
|
||||
// TO ADD:
|
||||
//var/static/list/defaultOrgans = list (/obj/item/organ/heart/vampheart,/obj/item/organ/heart/vampeyes)
|
||||
|
||||
/datum/antagonist/bloodsucker/on_gain()
|
||||
SSticker.mode.bloodsuckers |= owner // Add if not already in here (and you might be, if you were picked at round start)
|
||||
SSticker.mode.check_start_sunlight()// Start Sunlight? (if first Vamp)
|
||||
SelectFirstName()// Name & Title
|
||||
SelectTitle(am_fledgling=TRUE) // If I have a creator, then set as Fledgling.
|
||||
SelectReputation(am_fledgling=TRUE)
|
||||
AssignStarterPowersAndStats()// Give Powers & Stats
|
||||
forge_bloodsucker_objectives()// Objectives & Team
|
||||
update_bloodsucker_icons_added(owner.current, "bloodsucker") // Add Antag HUD
|
||||
LifeTick() // Run Life Function
|
||||
. = ..()
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/on_removal()
|
||||
SSticker.mode.bloodsuckers -= owner
|
||||
SSticker.mode.check_cancel_sunlight()// End Sunlight? (if last Vamp)
|
||||
ClearAllPowersAndStats()// Clear Powers & Stats
|
||||
clear_bloodsucker_objectives() // Objectives
|
||||
update_bloodsucker_icons_removed(owner.current)// Clear Antag HUD
|
||||
. = ..()
|
||||
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/greet()
|
||||
var/fullname = ReturnFullName(TRUE)
|
||||
to_chat(owner, "<span class='userdanger'>You are [fullname], a bloodsucking vampire!</span><br>")
|
||||
owner.announce_objectives()
|
||||
to_chat(owner, "<span class='boldannounce'>* You regenerate your health slowly, you're weak to fire, and you depend on blood to survive. Allow your stolen blood to run too low, and you will find yourself at \
|
||||
risk of being discovered!</span><br>")
|
||||
//to_chat(owner, "<span class='boldannounce'>As an immortal, your power is linked to your age. The older you grow, the more abilities you will have access to.<span>")
|
||||
var/vamp_greet
|
||||
vamp_greet += "<span class='boldannounce'>* Other Bloodsuckers are not necessarily your friends, but your survival may depend on cooperation. Betray them at your own discretion and peril.</span><br>"
|
||||
vamp_greet += "<span class='boldannounce'><i>* Use \",b\" to speak your ancient Bloodsucker language.</span><br>"
|
||||
vamp_greet += "<span class='announce'>Bloodsucker Tip: Rest in a <i>Coffin</i> to claim it, and that area, as your lair.</span><br>"
|
||||
vamp_greet += "<span class='announce'>Bloodsucker Tip: Fear the daylight! Solar flares will bombard the station periodically, and only your coffin can guarantee your safety.</span><br>"
|
||||
vamp_greet += "<span class='announce'>Bloodsucker Tip: You wont loose blood if you are unconcious or sleeping. Use this to your advantage to conserve blood.</span><br>"
|
||||
to_chat(owner, vamp_greet)
|
||||
|
||||
owner.current.playsound_local(null, 'sound/bloodsucker/BloodsuckerAlert.ogg', 100, FALSE, pressure_affected = FALSE)
|
||||
antag_memory += "Although you were born a mortal, in un-death you earned the name <b>[fullname]</b>.<br>"
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/farewell()
|
||||
owner.current.visible_message("[owner.current]'s skin flushes with color, their eyes growing glossier. They look...alive.",\
|
||||
"<span class='userdanger'><FONT size = 3>With a snap, your curse has ended. You are no longer a Bloodsucker. You live once more!</FONT></span>")
|
||||
// Refill with Blood
|
||||
owner.current.blood_volume = max(owner.current.blood_volume,BLOOD_VOLUME_SAFE)
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/SelectFirstName()
|
||||
// Names (EVERYONE gets one))
|
||||
if (owner.current.gender == MALE)
|
||||
vampname = pick("Desmond","Rudolph","Dracul","Vlad","Pyotr","Gregor","Cristian","Christoff","Marcu","Andrei","Constantin","Gheorghe","Grigore","Ilie","Iacob","Luca","Mihail","Pavel","Vasile","Octavian","Sorin", \
|
||||
"Sveyn","Aurel","Alexe","Iustin","Theodor","Dimitrie","Octav","Damien","Magnus","Caine","Abel", // Romanian/Ancient
|
||||
"Lucius","Gaius","Otho","Balbinus","Arcadius","Romanos","Alexios","Vitellius", // Latin
|
||||
"Melanthus","Teuthras","Orchamus","Amyntor","Axion", // Greek
|
||||
"Thoth","Thutmose","Osorkon,","Nofret","Minmotu","Khafra", // Egyptian
|
||||
"Dio")
|
||||
|
||||
else
|
||||
vampname = pick("Islana","Tyrra","Greganna","Pytra","Hilda","Andra","Crina","Viorela","Viorica","Anemona","Camelia","Narcisa","Sorina","Alessia","Sophia","Gladda","Arcana","Morgan","Lasarra","Ioana","Elena", \
|
||||
"Alina","Rodica","Teodora","Denisa","Mihaela","Svetla","Stefania","Diyana","Kelssa","Lilith", // Romanian/Ancient
|
||||
"Alexia","Athanasia","Callista","Karena","Nephele","Scylla","Ursa", // Latin
|
||||
"Alcestis","Damaris","Elisavet","Khthonia","Teodora", // Greek
|
||||
"Nefret","Ankhesenpep") // Egyptian
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/SelectTitle(am_fledgling = 0, forced = FALSE)
|
||||
// Already have Title
|
||||
if (!forced && vamptitle != null)
|
||||
return
|
||||
// Titles [Master]
|
||||
if (!am_fledgling)
|
||||
if (owner.current.gender == MALE)
|
||||
vamptitle = pick ("Count","Baron","Viscount","Prince","Duke","Tzar","Dreadlord","Lord","Master")
|
||||
else
|
||||
vamptitle = pick ("Countess","Baroness","Viscountess","Princess","Duchess","Tzarina","Dreadlady","Lady","Mistress")
|
||||
to_chat(owner, "<span class='announce'>You have earned a title! You are now known as <i>[ReturnFullName(TRUE)]</i>!</span>")
|
||||
// Titles [Fledgling]
|
||||
else
|
||||
vamptitle = null
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/SelectReputation(am_fledgling = 0, forced=FALSE)
|
||||
// Already have Reputation
|
||||
if (!forced && vampreputation != null)
|
||||
return
|
||||
// Reputations [Master]
|
||||
if (!am_fledgling)
|
||||
vampreputation = pick("Butcher","Blood Fiend","Crimson","Red","Black","Terror","Nightman","Feared","Ravenous","Fiend","Malevolent","Wicked","Ancient","Plaguebringer","Sinister","Forgotten","Wretched","Baleful", \
|
||||
"Inqisitor","Harvester","Reviled","Robust","Betrayer","Destructor","Damned","Accursed","Terrible","Vicious","Profane","Vile","Depraved","Foul","Slayer","Manslayer","Sovereign","Slaughterer", \
|
||||
"Forsaken","Mad","Dragon","Savage","Villainous","Nefarious","Inquisitor","Marauder","Horrible","Immortal","Undying","Overlord","Corrupt","Hellspawn","Tyrant","Sanguineous")
|
||||
if (owner.current.gender == MALE)
|
||||
if (prob(10)) // Gender override
|
||||
vampreputation = pick("King of the Damned", "Blood King", "Emperor of Blades", "Sinlord", "God-King")
|
||||
else
|
||||
if (prob(10)) // Gender override
|
||||
vampreputation = pick("Queen of the Damned", "Blood Queen", "Empress of Blades", "Sinlady", "God-Queen")
|
||||
|
||||
to_chat(owner, "<span class='announce'>You have earned a reputation! You are now known as <i>[ReturnFullName(TRUE)]</i>!</span>")
|
||||
|
||||
// Reputations [Fledgling]
|
||||
else
|
||||
vampreputation = pick ("Crude","Callow","Unlearned","Neophyte","Novice","Unseasoned","Fledgling","Young","Neonate","Scrapling","Untested","Unproven","Unknown","Newly Risen","Born","Scavenger","Unknowing",\
|
||||
"Unspoiled","Disgraced","Defrocked","Shamed","Meek","Timid","Broken")//,"Fresh")
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/AmFledgling()
|
||||
return !vamptitle
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/ReturnFullName(var/include_rep=0)
|
||||
|
||||
var/fullname
|
||||
// Name First
|
||||
fullname = (vampname ? vampname : owner.current.name)
|
||||
// Title
|
||||
if(vamptitle)
|
||||
fullname = vamptitle + " " + fullname
|
||||
// Rep
|
||||
if(include_rep && vampreputation)
|
||||
fullname = fullname + " the " + vampreputation
|
||||
|
||||
return fullname
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/BuyPower(datum/action/bloodsucker/power)//(obj/effect/proc_holder/spell/power)
|
||||
powers += power
|
||||
power.Grant(owner.current)// owner.AddSpell(power)
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/AssignStarterPowersAndStats()
|
||||
// Blood/Rank Counter
|
||||
add_hud()
|
||||
update_hud(TRUE) // Set blood value, current rank
|
||||
// Powers
|
||||
BuyPower(new /datum/action/bloodsucker/feed)
|
||||
BuyPower(new /datum/action/bloodsucker/masquerade)
|
||||
BuyPower(new /datum/action/bloodsucker/veil)
|
||||
// Traits
|
||||
for(var/T in defaultTraits)
|
||||
ADD_TRAIT(owner.current, T, "bloodsucker")
|
||||
if(HAS_TRAIT(owner.current, TRAIT_TOXINLOVER)) //No slime bonuses here, no thank you
|
||||
had_toxlover = TRUE
|
||||
REMOVE_TRAIT(owner.current, TRAIT_TOXINLOVER, "species")
|
||||
// Traits: Species
|
||||
if(ishuman(owner.current))
|
||||
var/mob/living/carbon/human/H = owner.current
|
||||
var/datum/species/S = H.dna.species
|
||||
S.species_traits |= DRINKSBLOOD
|
||||
// Clear Addictions
|
||||
owner.current.reagents.addiction_list = list() // Start over from scratch. Lucky you! At least you're not addicted to blood anymore (if you were)
|
||||
// Stats
|
||||
if(ishuman(owner.current))
|
||||
var/mob/living/carbon/human/H = owner.current
|
||||
var/datum/species/S = H.dna.species
|
||||
// Make Changes
|
||||
H.physiology.brute_mod *= 0.8 // <-------------------- Start small, but burn mod increases based on rank!
|
||||
H.physiology.cold_mod = 0
|
||||
H.physiology.stun_mod *= 0.35
|
||||
H.physiology.siemens_coeff *= 0.75 //base electrocution coefficient 1
|
||||
//S.heatmod += 0.5 // Heat shouldn't affect. Only Fire.
|
||||
//S.punchstunthreshold = 8 //damage at which punches from this race will stun 9
|
||||
S.punchdamagelow += 1 //lowest possible punch damage 0
|
||||
S.punchdamagehigh += 1 //highest possible punch damage 9
|
||||
// Clown
|
||||
if(istype(H) && owner.assigned_role == "Clown")
|
||||
H.dna.remove_mutation(CLOWNMUT)
|
||||
to_chat(H, "As a vampiric clown, you are no longer a danger to yourself. Your nature is subdued.")
|
||||
// Physiology
|
||||
CheckVampOrgans() // Heart, Eyes
|
||||
// Language
|
||||
owner.current.grant_language(/datum/language/vampiric)
|
||||
// Soul
|
||||
//owner.current.hellbound = TRUE Causes wierd stuff
|
||||
owner.hasSoul = FALSE // If false, renders the character unable to sell their soul.
|
||||
owner.isholy = FALSE // is this person a chaplain or admin role allowed to use bibles
|
||||
// Disabilities
|
||||
CureDisabilities()
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/ClearAllPowersAndStats()
|
||||
// Blood/Rank Counter
|
||||
remove_hud()
|
||||
// Powers
|
||||
while(powers.len)
|
||||
var/datum/action/bloodsucker/power = pick(powers)
|
||||
powers -= power
|
||||
power.Remove(owner.current)
|
||||
// owner.RemoveSpell(power)
|
||||
// Traits
|
||||
for(var/T in defaultTraits)
|
||||
REMOVE_TRAIT(owner.current, T, "bloodsucker")
|
||||
if(had_toxlover == TRUE)
|
||||
ADD_TRAIT(owner.current, TRAIT_TOXINLOVER, "species")
|
||||
|
||||
// Traits: Species
|
||||
if(ishuman(owner.current))
|
||||
var/mob/living/carbon/human/H = owner.current
|
||||
H.set_species(H.dna.species.type)
|
||||
// Stats
|
||||
if(ishuman(owner.current))
|
||||
var/mob/living/carbon/human/H = owner.current
|
||||
H.set_species(H.dna.species.type)
|
||||
// Clown
|
||||
if(istype(H) && owner.assigned_role == "Clown")
|
||||
H.dna.add_mutation(CLOWNMUT)
|
||||
// NOTE: Use initial() to return things to default!
|
||||
// Physiology
|
||||
owner.current.regenerate_organs()
|
||||
// Update Health
|
||||
owner.current.setMaxHealth(100)
|
||||
// Language
|
||||
owner.current.remove_language(/datum/language/vampiric)
|
||||
// Soul
|
||||
if (owner.soulOwner == owner) // Return soul, if *I* own it.
|
||||
owner.hasSoul = TRUE
|
||||
//owner.current.hellbound = FALSE
|
||||
|
||||
datum/antagonist/bloodsucker/proc/RankUp()
|
||||
set waitfor = FALSE
|
||||
if(!owner || !owner.current)
|
||||
return
|
||||
vamplevel_unspent ++
|
||||
// Spend Rank Immediately?
|
||||
if(istype(owner.current.loc, /obj/structure/closet/crate/coffin))
|
||||
SpendRank()
|
||||
else
|
||||
to_chat(owner, "<EM><span class='notice'>You have grown more ancient! Sleep in a coffin that you have claimed to thicken your blood and become more powerful.</span></EM>")
|
||||
if(vamplevel_unspent >= 2)
|
||||
to_chat(owner, "<span class='announce'>Bloodsucker Tip: If you cannot find or steal a coffin to use, they can be built from wooden planks.</span><br>")
|
||||
|
||||
datum/antagonist/bloodsucker/proc/LevelUpPowers()
|
||||
for(var/datum/action/bloodsucker/power in powers)
|
||||
power.level_current ++
|
||||
|
||||
datum/antagonist/bloodsucker/proc/SpendRank()
|
||||
set waitfor = FALSE
|
||||
if (vamplevel_unspent <= 0 || !owner || !owner.current || !owner.current.client)
|
||||
return
|
||||
/////////
|
||||
// Powers
|
||||
//TODO: Make this into a radial
|
||||
// Purchase Power Prompt
|
||||
var/list/options = list() // Taken from gasmask.dm, for Clown Masks.
|
||||
for(var/pickedpower in typesof(/datum/action/bloodsucker))
|
||||
var/datum/action/bloodsucker/power = pickedpower
|
||||
// If I don't own it, and I'm allowed to buy it.
|
||||
if(!(locate(power) in powers) && initial(power.bloodsucker_can_buy))
|
||||
options[initial(power.name)] = power // TESTING: After working with TGUI, it seems you can use initial() to view the variables inside a path?
|
||||
options["\[ Not Now \]"] = null
|
||||
// Abort?
|
||||
if(options.len > 1)
|
||||
var/choice = input(owner.current, "You have the opportunity to grow more ancient. Select a power to advance your Rank.", "Your Blood Thickens...") in options
|
||||
// Cheat-Safety: Can't keep opening/closing coffin to spam levels
|
||||
if(vamplevel_unspent <= 0) // Already spent all your points, and tried opening/closing your coffin, pal.
|
||||
return
|
||||
if(!istype(owner.current.loc, /obj/structure/closet/crate/coffin))
|
||||
to_chat(owner.current, "<span class='warning'>Return to your coffin to advance your Rank.</span>")
|
||||
return
|
||||
if(!choice || !options[choice] || (locate(options[choice]) in powers)) // ADDED: Check to see if you already have this power, due to window stacking.
|
||||
to_chat(owner.current, "<span class='notice'>You prevent your blood from thickening just yet, but you may try again later.</span>")
|
||||
return
|
||||
// Buy New Powers
|
||||
var/datum/action/bloodsucker/P = options[choice]
|
||||
BuyPower(new P)
|
||||
to_chat(owner.current, "<span class='notice'>You have learned [initial(P.name)]!</span>")
|
||||
else
|
||||
to_chat(owner.current, "<span class='notice'>You grow more ancient by the night!</span>")
|
||||
/////////
|
||||
// Advance Powers (including new)
|
||||
LevelUpPowers()
|
||||
////////
|
||||
// Advance Stats
|
||||
if(ishuman(owner.current))
|
||||
var/mob/living/carbon/human/H = owner.current
|
||||
var/datum/species/S = H.dna.species
|
||||
S.punchdamagelow += 0.5
|
||||
S.punchdamagehigh += 0.5 // NOTE: This affects the hitting power of Brawn.
|
||||
// More Health
|
||||
owner.current.setMaxHealth(owner.current.maxHealth + 10)
|
||||
// Vamp Stats
|
||||
regenRate += 0.05 // Points of brute healed (starts at 0.3)
|
||||
feedAmount += 2 // Increase how quickly I munch down vics (15)
|
||||
maxBloodVolume += 50 // Increase my max blood (600)
|
||||
/////////
|
||||
vamplevel ++
|
||||
vamplevel_unspent --
|
||||
|
||||
// Assign True Reputation
|
||||
if(vamplevel == 4)
|
||||
SelectReputation(am_fledgling = FALSE, forced = TRUE)
|
||||
to_chat(owner.current, "<span class='notice'>You are now a rank [vamplevel] Bloodsucker. Your strength, health, feed rate, regen rate, and maximum blood have all increased!</span>")
|
||||
to_chat(owner.current, "<span class='notice'>Your existing powers have all ranked up as well!</span>")
|
||||
update_hud(TRUE)
|
||||
owner.current.playsound_local(null, 'sound/effects/pope_entry.ogg', 25, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head.
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//This handles the application of antag huds/special abilities
|
||||
/datum/antagonist/bloodsucker/apply_innate_effects(mob/living/mob_override)
|
||||
return
|
||||
|
||||
//This handles the removal of antag huds/special abilities
|
||||
/datum/antagonist/bloodsucker/remove_innate_effects(mob/living/mob_override)
|
||||
return
|
||||
|
||||
//Assign default team and creates one for one of a kind team antagonists
|
||||
/datum/antagonist/bloodsucker/create_team(datum/team/team)
|
||||
return
|
||||
|
||||
// Create Objectives
|
||||
/datum/antagonist/bloodsucker/proc/forge_bloodsucker_objectives() // Fledgling vampires can have different objectives.
|
||||
|
||||
// TEAM
|
||||
//clan = new /datum/team/vampireclan(owner)
|
||||
|
||||
|
||||
// Lair Objective: Create a Lair
|
||||
var/datum/objective/bloodsucker/lair/lair_objective = new
|
||||
lair_objective.owner = owner
|
||||
lair_objective.generate_objective()
|
||||
add_objective(lair_objective)
|
||||
|
||||
// Protege Objective
|
||||
var/datum/objective/bloodsucker/protege/protege_objective = new
|
||||
protege_objective.owner = owner
|
||||
protege_objective.generate_objective()
|
||||
add_objective(protege_objective)
|
||||
|
||||
if (rand(0,1) == 0)
|
||||
// Heart Thief Objective
|
||||
var/datum/objective/bloodsucker/heartthief/heartthief_objective = new
|
||||
heartthief_objective.owner = owner
|
||||
heartthief_objective.generate_objective()
|
||||
add_objective(heartthief_objective)
|
||||
|
||||
else
|
||||
// Solars Objective
|
||||
var/datum/objective/bloodsucker/solars/solars_objective = new
|
||||
solars_objective.owner = owner
|
||||
solars_objective.generate_objective()
|
||||
add_objective(solars_objective)
|
||||
|
||||
// Survive Objective
|
||||
var/datum/objective/bloodsucker/survive/survive_objective = new
|
||||
survive_objective.owner = owner
|
||||
survive_objective.generate_objective()
|
||||
add_objective(survive_objective)
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/add_objective(var/datum/objective/O)
|
||||
objectives += O
|
||||
objectives_given += O
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/clear_bloodsucker_objectives()
|
||||
|
||||
var/datum/team/team = get_team()
|
||||
if(team)
|
||||
team.remove_member(owner)
|
||||
|
||||
for(var/O in objectives_given)
|
||||
objectives -= O
|
||||
qdel(O)
|
||||
objectives_given = list() // Traitors had this, so I added it. Not sure why.
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/get_team()
|
||||
return clan
|
||||
|
||||
//Name shown on antag list
|
||||
/datum/antagonist/bloodsucker/antag_listing_name()
|
||||
return ..() + "([ReturnFullName(TRUE)])"
|
||||
|
||||
//Whatever interesting things happened to the antag admins should know about
|
||||
//Include additional information about antag in this part
|
||||
/datum/antagonist/bloodsucker/antag_listing_status()
|
||||
if (owner && owner.AmFinalDeath())
|
||||
return "<font color=red>Final Death</font>"
|
||||
return ..()
|
||||
|
||||
|
||||
|
||||
//Individual roundend report
|
||||
/datum/antagonist/bloodsucker/roundend_report()
|
||||
// Get the default Objectives
|
||||
var/list/report = list()
|
||||
|
||||
// Vamp Name
|
||||
report += "<br><span class='header'><b>\[[ReturnFullName(TRUE)]\]</b></span>"
|
||||
|
||||
// Default Report
|
||||
report += ..()
|
||||
|
||||
// Now list their vassals
|
||||
if (vassals.len > 0)
|
||||
report += "<span class='header'>Their Vassals were...</span>"
|
||||
for (var/datum/antagonist/vassal/V in vassals)
|
||||
if (V.owner)
|
||||
var/jobname = V.owner.assigned_role ? "the [V.owner.assigned_role]" : ""
|
||||
report += "<b>[V.owner.name]</b> [jobname]"
|
||||
|
||||
return report.Join("<br>")
|
||||
|
||||
//Displayed at the start of roundend_category section, default to roundend_category header
|
||||
/datum/antagonist/bloodsucker/roundend_report_header()
|
||||
return "<span class='header'>Lurking in the darkness, the Bloodsuckers were:</span><br>"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 2019 Breakdown of Bloodsuckers:
|
||||
|
||||
// G A M E P L A Y
|
||||
//
|
||||
// Bloodsuckers should be inherrently powerful: they never stay dead, and they can hide in plain sight
|
||||
// better than any other antagonist aboard the station.
|
||||
//
|
||||
// However, only elder Bloodsuckers are the powerful creatures of legend. Ranking up as a Bloodsucker
|
||||
// should impart slight strength and health benefits, as well as powers that can grow over time. But
|
||||
// their weaknesses should grow as well, and not just to fire.
|
||||
|
||||
|
||||
// A B I L I T I E S
|
||||
//
|
||||
// * Bloodsuckers can FEIGN LIFE + DEATH.
|
||||
// Feigning LIFE:
|
||||
// - Warms up the body
|
||||
// - Creates a heartbeat
|
||||
// - Fake blood amount (550)
|
||||
// Feign DEATH: Not yet done
|
||||
// - When lying down or sitting, you appear "dead and lifeless"
|
||||
|
||||
// * Bloodsuckers REGENERATE
|
||||
// - Brute damage heals rather rapidly. Burn damage heals slowly.
|
||||
// - Healing is reduced when hungry or starved.
|
||||
// - Burn does not heal when starved. A starved vampire remains "dead" until burns can heal.
|
||||
// - Bodyparts and organs regrow in Torpor (except for the Heart and Brain).
|
||||
//
|
||||
// * Bloodsuckers are IMMORTAL
|
||||
// - Brute damage cannot destroy them (and it caps very low, so they don't stack too much)
|
||||
// - Burn damage can only kill them at very high amounts.
|
||||
// - Removing the head kills the vamp forever.
|
||||
// - Removing the heart kills the vamp until replaced.
|
||||
//
|
||||
// * Bloodsuckers are DEAD
|
||||
// - They do not breathe.
|
||||
// - Cold affects them less.
|
||||
// - They are immune to disease (but can spread it)
|
||||
// - Food is useless and cause sickness.
|
||||
// - Nothing can heal the vamp other than his own blood.
|
||||
//
|
||||
// * Bloodsuckers are PREDATORS
|
||||
// - They detect life/heartbeats nearby.
|
||||
// - They know other predators instantly (Vamps, Werewolves, and alien types) regardless of disguise.
|
||||
//
|
||||
//
|
||||
//
|
||||
// * Bloodsuckers enter Torpor when DEAD or RESTING in coffin
|
||||
// - Torpid vampires regenerate their health. Coffins negate cost and speed up the process.
|
||||
// ** To rest in a coffin, either SLEEP or CLOSE THE LID while you're in it. You will be given a prompt to sleep until healed. Healing in a coffin costs NO blood!
|
||||
//
|
||||
|
||||
|
||||
|
||||
// O B J E C T I V E S
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
// 1) GROOM AN HEIR: Find a person with appropriate traits (hair, blood type, gender) to be turned as a Vampire. Before they rise, they must be properly trained. Raise them to great power after their change.
|
||||
//
|
||||
// 2) BIBLIOPHILE: Research objects of interest, study items looking for clues of ancient secrets, and hunt down the clues to a Vampiric artifact of horrible power.
|
||||
//
|
||||
// 3) CRYPT LORD: Build a terrifying sepulcher to your evil, with servants to lavish upon you in undeath. The trappings of a true crypt lord come at grave cost.
|
||||
//
|
||||
// 4) GOURMOND: Oh, to taste all the delicacies the station has to offer! DRINK ## BLOOD FROM VICTIMS WHO LIVE, EAT ## ORGANS FROM VICTIMS WHO LIVE
|
||||
|
||||
|
||||
// Vassals
|
||||
//
|
||||
// - Loyal to (and In Love With) Master
|
||||
// - Master can speak to, summon, or punish his Vassals, even while asleep or torpid.
|
||||
// - Master may have as many Vassals as Rank
|
||||
// - Vassals see their Master's speech emboldened!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Dev Notes
|
||||
//
|
||||
// HEALING: Maybe Vamps metabolize specially? Like, they slowly drip their own blood into their system?
|
||||
// - Give Vamps their own metabolization proc, perhaps?
|
||||
// ** shadowpeople.dm has rules for healing.
|
||||
//
|
||||
// KILLING: It's almost impossible to track who someone has directly killed. But an Admin could be given
|
||||
// an easy way to whip a Bloodsucker for cruel behavior, as a RP mechanic but not a punishment.
|
||||
// **
|
||||
//
|
||||
// HUNGER: Just keep adjusting mob's nutrition to Blood Hunger level. No need to cancel nutrition from eating.
|
||||
// ** mob.dm /set_nutrition()
|
||||
// ** snacks.dm / attack() <-- Stop food from doing anything?
|
||||
|
||||
// ORGANS: Liver
|
||||
// ** life.dm /handle_liver()
|
||||
//
|
||||
// CORPSE: Most of these effects likely go away when using "Masquerade" to appear alive.
|
||||
// ** status_procs.dm /adjust_bodytemperature()
|
||||
// ** traits.dm /TRAIT_NOBREATH /TRAIT_SLEEPIMMUNE /TRAIT_RESISTCOLD /TRAIT_RADIMMUNE /TRAIT_VIRUSIMMUNE
|
||||
// * MASQUERADE ON/OFF: /TRAIT_FAKEDEATH (M)
|
||||
// * /TRAIT_NIGHT_VISION
|
||||
// * /TRAIT_DEATHCOMA <-- This basically makes you immobile. When using status_procs /fakedeath(), make sure to remove Coma unless we're in Torpor!
|
||||
// * /TRAIT_NODEATH <--- ???
|
||||
// ** species /NOZOMBIE
|
||||
// * ADD: TRAIT_COLDBLOODED <-- add to carbon/life.dm /natural_bodytemperature_stabilization()
|
||||
//
|
||||
// MASQUERADE Appear as human!
|
||||
// ** examine.dm /examine() <-- Change "blood_volume < BLOOD_VOLUME_SAFE" to a new examine
|
||||
//
|
||||
// NOSFERATU ** human.add_trait(TRAIT_DISFIGURED, "insert_vamp_datum_here") <-- Makes you UNKNOWN unless your ID says otherwise.
|
||||
// STEALTH ** human_helpers.dm /get_visible_name() ** shadowpeople.dm has rules for Light.
|
||||
//
|
||||
// FRENZY ** living.dm /update_mobility() (USED TO be update_canmove)
|
||||
//
|
||||
// PREDATOR See other Vamps!
|
||||
// * examine.dm /examine()
|
||||
//
|
||||
// WEAKNESSES: -Poor mood in Chapel or near Chaplain. -Stamina damage from Bible
|
||||
|
||||
|
||||
|
||||
//message_admins("DEBUG3: attempt_cast() [name] / [user_C.handcuffed] ")
|
||||
|
||||
|
||||
// TODO:
|
||||
//
|
||||
// Death (fire, heart, brain, head)
|
||||
// Disable Life: BLOOD
|
||||
// Body Temp
|
||||
// Spend blood over time (more if imitating life) (none if sleeping in coffin)
|
||||
// Auto-Heal (brute to 0, fire to 99) (toxin/o2 always 0)
|
||||
//
|
||||
// Hud Icons
|
||||
// UI Blood Counter
|
||||
// Examine Name (+Masquerade, only "Dead and lifeless" if not standing?)
|
||||
//
|
||||
//
|
||||
// Turn vamps
|
||||
// Create vassals
|
||||
//
|
||||
|
||||
|
||||
// FIX LIST
|
||||
//
|
||||
|
||||
|
||||
/////////////////////////////////////
|
||||
|
||||
// HUD! //
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/update_bloodsucker_icons_added(datum/mind/m)
|
||||
var/datum/atom_hud/antag/vamphud = GLOB.huds[ANTAG_HUD_BLOODSUCKER]
|
||||
vamphud.join_hud(owner.current)
|
||||
set_antag_hud(owner.current, "bloodsucker") // "bloodsucker"
|
||||
owner.current.hud_list[ANTAG_HUD].icon = image('icons/mob/hud.dmi', owner.current, "bloodsucker") //Check prepare_huds in mob.dm to see why.
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/update_bloodsucker_icons_removed(datum/mind/m)
|
||||
var/datum/atom_hud/antag/vamphud = GLOB.huds[ANTAG_HUD_BLOODSUCKER]
|
||||
vamphud.leave_hud(owner.current)
|
||||
set_antag_hud(owner.current, null)
|
||||
|
||||
/datum/atom_hud/antag/bloodsucker // from hud.dm in /datums/ Also see data_huds.dm + antag_hud.dm
|
||||
|
||||
/datum/atom_hud/antag/bloodsucker/add_to_single_hud(mob/M, atom/A)
|
||||
if (!check_valid_hud_user(M,A)) // FULP: This checks if the Mob is a Vassal, and if the Atom is his master OR on his team.
|
||||
return
|
||||
..()
|
||||
|
||||
/datum/atom_hud/antag/bloodsucker/proc/check_valid_hud_user(mob/M, atom/A) // Remember: A is being added to M's hud. Because M's hud is a /antag/vassal hud, this means M is the vassal here.
|
||||
// Ghost Admins always see Bloodsuckers/Vassals
|
||||
if (isobserver(M))
|
||||
return TRUE
|
||||
// GOAL: Vassals see their Master and his other Vassals.
|
||||
// GOAL: Vassals can BE seen by their Bloodsucker and his other Vassals.
|
||||
// GOAL: Bloodsuckers can see each other.
|
||||
if (!M || !A || !ismob(A) || !M.mind)// || !A.mind)
|
||||
return FALSE
|
||||
var/mob/A_mob = A
|
||||
if (!A_mob.mind)
|
||||
return FALSE
|
||||
// Find Datums: Bloodsucker
|
||||
var/datum/antagonist/bloodsucker/atom_B = A_mob.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
var/datum/antagonist/bloodsucker/mob_B = M.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
// Check 1) Are we both Bloodsuckers?
|
||||
if (atom_B && mob_B)
|
||||
return TRUE
|
||||
// Find Datums: Vassal
|
||||
var/datum/antagonist/vassal/atom_V = A_mob.mind.has_antag_datum(ANTAG_DATUM_VASSAL)
|
||||
var/datum/antagonist/vassal/mob_V = M.mind.has_antag_datum(ANTAG_DATUM_VASSAL)
|
||||
// Check 2) If they are a BLOODSUCKER, then are they my Master?
|
||||
if (mob_V && atom_B == mob_V.master)
|
||||
return TRUE // SUCCESS!
|
||||
// Check 3) If I am a BLOODSUCKER, then are they my Vassal?
|
||||
if (mob_B && atom_V && (atom_V in mob_B.vassals))
|
||||
return TRUE // SUCCESS!
|
||||
// Check 4) If we are both VASSAL, then do we have the same master?
|
||||
if (atom_V && mob_V && atom_V.master == mob_V.master)
|
||||
return TRUE // SUCCESS!
|
||||
return FALSE
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////
|
||||
|
||||
|
||||
// BLOOD COUNTER & RANK MARKER ! //
|
||||
|
||||
#define ui_sunlight_display "WEST:6,CENTER-0:0" // 6 pixels to the right, zero tiles & 5 pixels DOWN.
|
||||
#define ui_blood_display "WEST:6,CENTER-1:0" // 1 tile down
|
||||
#define ui_vamprank_display "WEST:6,CENTER-2:-5" // 2 tiles down
|
||||
|
||||
/datum/hud
|
||||
var/obj/screen/bloodsucker/blood_counter/blood_display
|
||||
var/obj/screen/bloodsucker/rank_counter/vamprank_display
|
||||
var/obj/screen/bloodsucker/sunlight_counter/sunlight_display
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/add_hud()
|
||||
return
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/remove_hud()
|
||||
// No Hud? Get out.
|
||||
if (!owner.current.hud_used)
|
||||
return
|
||||
owner.current.hud_used.blood_display.invisibility = INVISIBILITY_ABSTRACT
|
||||
owner.current.hud_used.vamprank_display.invisibility = INVISIBILITY_ABSTRACT
|
||||
owner.current.hud_used.sunlight_display.invisibility = INVISIBILITY_ABSTRACT
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/update_hud(updateRank=FALSE)
|
||||
// No Hud? Get out.
|
||||
if(!owner.current.hud_used)
|
||||
return
|
||||
// Update Blood Counter
|
||||
if (owner.current.hud_used.blood_display)
|
||||
var/valuecolor = "#FF6666"
|
||||
if(owner.current.blood_volume > BLOOD_VOLUME_SAFE)
|
||||
valuecolor = "#FFDDDD"
|
||||
else if(owner.current.blood_volume > BLOOD_VOLUME_BAD)
|
||||
valuecolor = "#FFAAAA"
|
||||
owner.current.hud_used.blood_display.update_counter(owner.current.blood_volume, valuecolor)
|
||||
|
||||
// Update Rank Counter
|
||||
if(owner.current.hud_used.vamprank_display)
|
||||
var/valuecolor = vamplevel_unspent ? "#FFFF00" : "#FF0000"
|
||||
owner.current.hud_used.vamprank_display.update_counter(vamplevel, valuecolor)
|
||||
if(updateRank) // Only change icon on special request.
|
||||
owner.current.hud_used.vamprank_display.icon_state = (vamplevel_unspent > 0) ? "rank_up" : "rank"
|
||||
|
||||
|
||||
/obj/screen/bloodsucker
|
||||
invisibility = INVISIBILITY_ABSTRACT
|
||||
|
||||
/obj/screen/bloodsucker/proc/clear()
|
||||
invisibility = INVISIBILITY_ABSTRACT
|
||||
|
||||
/obj/screen/bloodsucker/proc/update_counter(value, valuecolor)
|
||||
invisibility = 0 // Make Visible
|
||||
|
||||
/obj/screen/bloodsucker/blood_counter // NOTE: Look up /obj/screen/devil/soul_counter in _onclick / hud / human.dm
|
||||
icon = 'icons/mob/actions/bloodsucker.dmi'//'icons/mob/screen_gen.dmi'
|
||||
name = "Blood Consumed"
|
||||
icon_state = "blood_display"//"power_display"
|
||||
screen_loc = ui_blood_display
|
||||
|
||||
/obj/screen/bloodsucker/blood_counter/update_counter(value, valuecolor)
|
||||
..()
|
||||
maptext = "<div align='center' valign='middle' style='position:relative; top:0px; left:6px'><font color='[valuecolor]'>[round(value,1)]</font></div>"
|
||||
|
||||
/obj/screen/bloodsucker/rank_counter
|
||||
name = "Bloodsucker Rank"
|
||||
icon = 'icons/mob/actions/bloodsucker.dmi'
|
||||
icon_state = "rank"
|
||||
screen_loc = ui_vamprank_display
|
||||
|
||||
/obj/screen/bloodsucker/rank_counter/update_counter(value, valuecolor)
|
||||
..()
|
||||
maptext = "<div align='center' valign='middle' style='position:relative; top:0px; left:6px'><font color='[valuecolor]'>[round(value,1)]</font></div>"
|
||||
|
||||
/obj/screen/bloodsucker/sunlight_counter
|
||||
icon = 'icons/mob/actions/bloodsucker.dmi'
|
||||
name = "Solar Flare Timer"
|
||||
icon_state = "sunlight_night"
|
||||
screen_loc = ui_sunlight_display
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/update_sunlight(value, amDay = FALSE)
|
||||
// No Hud? Get out.
|
||||
if (!owner.current.hud_used)
|
||||
return
|
||||
// Update Sun Time
|
||||
if (owner.current.hud_used.sunlight_display)
|
||||
var/valuecolor = "#BBBBFF"
|
||||
if (amDay)
|
||||
valuecolor = "#FF5555"
|
||||
else if(value <= 25)
|
||||
valuecolor = "#FFCCCC"
|
||||
else if(value < 10)
|
||||
valuecolor = "#FF5555"
|
||||
var/value_string = (value >= 60) ? "[round(value / 60, 1)] m" : "[round(value,1)] s"
|
||||
owner.current.hud_used.sunlight_display.update_counter( value_string, valuecolor )
|
||||
owner.current.hud_used.sunlight_display.icon_state = "sunlight_" + (amDay ? "day":"night")
|
||||
|
||||
|
||||
/obj/screen/bloodsucker/sunlight_counter/update_counter(value, valuecolor)
|
||||
..()
|
||||
maptext = "<div align='center' valign='bottom' style='position:relative; top:0px; left:6px'><font color='[valuecolor]'>[value]</font></div>"
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
#define HUNTER_SCAN_MIN_DISTANCE 8
|
||||
#define HUNTER_SCAN_MAX_DISTANCE 35
|
||||
#define HUNTER_SCAN_PING_TIME 20 //5s update time.
|
||||
|
||||
|
||||
/datum/antagonist/vamphunter
|
||||
name = "Hunter"
|
||||
roundend_category = "hunters"
|
||||
antagpanel_category = "Monster Hunter"
|
||||
job_rank = ROLE_MONSTERHUNTER
|
||||
var/list/datum/action/powers = list()// Purchased powers
|
||||
var/list/datum/objective/objectives_given = list() // For removal if needed.
|
||||
var/datum/martial_art/my_kungfu // Hunters know a lil kung fu.
|
||||
var/bad_dude = FALSE // Every first hunter spawned is a SHIT LORD.
|
||||
|
||||
|
||||
/datum/antagonist/vamphunter/on_gain()
|
||||
|
||||
SSticker.mode.vamphunters |= owner // Add if not already in here (and you might be, if you were picked at round start)
|
||||
|
||||
// Hunter Pinpointer
|
||||
//owner.current.apply_status_effect(/datum/status_effect/agent_pinpointer/hunter_edition)
|
||||
|
||||
// Give Hunter Power
|
||||
var/datum/action/P = new /datum/action/bloodsucker/trackvamp
|
||||
P.Grant(owner.current)
|
||||
|
||||
// Give Hunter Martial Arts
|
||||
//if (rand(1,3) == 1)
|
||||
// var/datum/martial_art/pick_type = pick (/datum/martial_art/cqc, /datum/martial_art/krav_maga, /datum/martial_art/cqc, /datum/martial_art/krav_maga, /datum/martial_art/wrestling) // /datum/martial_art/boxing <--- doesn't include grabbing, so don't use!
|
||||
// my_kungfu = new pick_type //pick (/datum/martial_art/boxing, /datum/martial_art/cqc) // ick_type
|
||||
// my_kungfu.teach(owner.current, 0)
|
||||
|
||||
// Give Hunter Objective
|
||||
var/datum/objective/bloodsucker/monsterhunter/monsterhunter_objective = new
|
||||
monsterhunter_objective.owner = owner
|
||||
monsterhunter_objective.generate_objective()
|
||||
objectives += monsterhunter_objective
|
||||
objectives_given += monsterhunter_objective
|
||||
// Badguy Hunter? (Give him BADGUY objectives)
|
||||
if (bad_dude)
|
||||
// Stolen DIRECTLY from datum_traitor.dm
|
||||
if(prob(15) && !(locate(/datum/objective/download) in objectives) && !(owner.assigned_role in list("Research Director", "Scientist", "Roboticist")))
|
||||
var/datum/objective/download/download_objective = new
|
||||
download_objective.owner = owner
|
||||
download_objective.gen_amount_goal()
|
||||
objectives += download_objective
|
||||
objectives_given += download_objective
|
||||
else
|
||||
var/datum/objective/steal/steal_objective = new
|
||||
steal_objective.owner = owner
|
||||
steal_objective.find_target()
|
||||
objectives += steal_objective
|
||||
objectives_given += steal_objective
|
||||
|
||||
|
||||
. = ..()
|
||||
|
||||
/datum/antagonist/vamphunter/on_removal()
|
||||
|
||||
SSticker.mode.vamphunters -= owner // Add if not already in here (and you might be, if you were picked at round start)
|
||||
|
||||
// Master Pinpointer
|
||||
//owner.current.remove_status_effect(/datum/status_effect/agent_pinpointer/hunter_edition)
|
||||
|
||||
// Take Hunter Power
|
||||
if (owner.current)
|
||||
for (var/datum/action/bloodsucker/P in owner.current.actions)
|
||||
P.Remove(owner.current)
|
||||
|
||||
// Take Hunter Martial Arts
|
||||
//my_kungfu.remove(owner.current)
|
||||
|
||||
// Remove Hunter Objectives
|
||||
for(var/O in objectives_given)
|
||||
objectives -= O
|
||||
qdel(O)
|
||||
objectives_given = list()
|
||||
|
||||
. = ..()
|
||||
|
||||
/datum/antagonist/vamphunter/greet()
|
||||
var/vamp_hunter_greet = "<span class='userdanger'>You are a fearless Monster Hunter!</span>"
|
||||
vamp_hunter_greet += "<span class='boldannounce'>You know there's one or more filthy creature onboard the station, though their identities elude you.<span>"
|
||||
vamp_hunter_greet += "<span class='boldannounce'>It's your job to root them out, destroy their nests, and save the crew.<span>"
|
||||
vamp_hunter_greet += "<span class='boldannounce'>Use <b>WHATEVER MEANS NECESSARY</b> to find these creatures...no matter who gets hurt or what you have to destroy to do it.</span>"
|
||||
vamp_hunter_greet += "There are greater stakes at hand than the safety of the station!<span>"
|
||||
vamp_hunter_greet += "<span class='boldannounce'>However, security may detain you if they discover your mission...<span>"
|
||||
antag_memory += "You remember your training:<br>"
|
||||
antag_memory += " -Bloodsuckers are weak to fire, or a stake to the heart. Removing their head or heart will also destroy them permanently.<br>"
|
||||
antag_memory += " -Wooden stakes can be made from planks, and hardened by a welding tool. Your recipes list has ways of making them even stronger.<br>"
|
||||
antag_memory += " -Changelings return to life unless their body is destroyed. Not even decapitation can stop them for long.<br>"
|
||||
antag_memory += " -Cultists are weak to the Chaplain's holy water.<br>"
|
||||
antag_memory += " -Wizards are notoriously hard to outmatch. Rob or steal whatever weapons you need to destroy them, and shoot before asking questions.<br><br>"
|
||||
if (my_kungfu != null)
|
||||
vamp_hunter_greet += "<span class='announce'>Hunter Tip: Use your [my_kungfu.name] techniques to give you an advantage over the enemy.</span><br>"
|
||||
antag_memory += "You remember your training: You are skilled in the [my_kungfu.name] style of combat.<br>"
|
||||
to_chat(owner, vamp_hunter_greet)
|
||||
|
||||
/datum/antagonist/vamphunter/farewell()
|
||||
to_chat(owner, "<span class='userdanger'>Your hunt has ended: you are no longer a monster hunter!</span>")
|
||||
|
||||
|
||||
// TAKEN FROM: /datum/action/changeling/pheromone_receptors // pheromone_receptors.dm for a version of tracking that Changelings have!
|
||||
|
||||
|
||||
/datum/status_effect/agent_pinpointer/hunter_edition
|
||||
alert_type = /obj/screen/alert/status_effect/agent_pinpointer/hunter_edition
|
||||
minimum_range = HUNTER_SCAN_MIN_DISTANCE
|
||||
tick_interval = HUNTER_SCAN_PING_TIME
|
||||
duration = 160 // Lasts 10s
|
||||
range_fuzz_factor = 5//PINPOINTER_EXTRA_RANDOM_RANGE
|
||||
|
||||
/obj/screen/alert/status_effect/agent_pinpointer/hunter_edition
|
||||
name = "Monster Tracking"
|
||||
desc = "You always know where the hellspawn are."
|
||||
|
||||
|
||||
/datum/status_effect/agent_pinpointer/hunter_edition/on_creation(mob/living/new_owner, ...)
|
||||
..()
|
||||
|
||||
// Pick target
|
||||
var/turf/my_loc = get_turf(owner)
|
||||
var/list/mob/living/carbon/vamps = list()
|
||||
|
||||
for(var/datum/mind/M in SSticker.mode.bloodsuckers)
|
||||
if (!M.current || M.current == owner || !get_turf(M.current) || !get_turf(new_owner))
|
||||
continue
|
||||
var/datum/antagonist/bloodsucker/antag_datum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(!istype(antag_datum))
|
||||
continue
|
||||
var/their_loc = get_turf(M.current)
|
||||
var/distance = get_dist_euclidian(my_loc, their_loc)
|
||||
if (distance < HUNTER_SCAN_MAX_DISTANCE)
|
||||
vamps[M.current] = (HUNTER_SCAN_MAX_DISTANCE ** 2) - (distance ** 2)
|
||||
// Found one!
|
||||
if(vamps.len)
|
||||
scan_target = pickweight(vamps) //Point at a 'random' vamp, biasing heavily towards closer ones.
|
||||
to_chat(owner, "<span class='warning'>You detect signs of monsters to the <b>[dir2text(get_dir(my_loc,get_turf(scan_target)))]!</b></span>")
|
||||
// Will yield a "?"
|
||||
else
|
||||
to_chat(owner, "<span class='notice'>There are no monsters nearby.</span>")
|
||||
// Force Point-To Immediately
|
||||
point_to_target()
|
||||
|
||||
/datum/status_effect/agent_pinpointer/hunter_edition/scan_for_target()
|
||||
// Lose target? Done. Otherwise, scan for target's current position.
|
||||
if (!scan_target && owner)
|
||||
owner.remove_status_effect(/datum/status_effect/agent_pinpointer/hunter_edition)
|
||||
|
||||
// NOTE: Do NOT run ..(), or else we'll remove our target.
|
||||
|
||||
|
||||
/datum/status_effect/agent_pinpointer/hunter_edition/Destroy()
|
||||
if (scan_target)
|
||||
to_chat(owner, "<span class='notice'>You've lost the trail.</span>")
|
||||
..()
|
||||
*/
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
|
||||
|
||||
/datum/action/bloodsucker/trackvamp/
|
||||
name = "Track Monster"//"Cellular Emporium"
|
||||
desc = "Take a moment to look for clues of any nearby monsters.<br>These creatures are slippery, and often look like the crew."
|
||||
button_icon = 'icons/mob/actions/bloodsucker.dmi' //This is the file for the BACKGROUND icon
|
||||
background_icon_state = "vamp_power_off" //And this is the state for the background icon
|
||||
icon_icon = 'icons/mob/actions/bloodsucker.dmi' //This is the file for the ACTION icon
|
||||
button_icon_state = "power_hunter" //And this is the state for the action icon
|
||||
amToggle = FALSE // Action-Related
|
||||
cooldown = 300 // 10 ticks, 1 second.
|
||||
bloodcost = 0
|
||||
|
||||
/datum/action/bloodsucker/trackvamp/ActivatePower()
|
||||
|
||||
var/mob/living/user = owner
|
||||
to_chat(user, "<span class='notice'>You look around, scanning your environment and discerning signs of any filthy, wretched affronts to the natural order.</span>")
|
||||
|
||||
if (!do_mob(user,owner,80))
|
||||
return
|
||||
// Add Power
|
||||
// REMOVED //user.apply_status_effect(/datum/status_effect/agent_pinpointer/hunter_edition)
|
||||
// We don't track direction anymore!
|
||||
// Return text indicating direction
|
||||
display_proximity()
|
||||
// NOTE: DON'T DEACTIVATE!
|
||||
//DeactivatePower()
|
||||
|
||||
/datum/action/bloodsucker/trackvamp/proc/display_proximity()
|
||||
// Pick target
|
||||
var/turf/my_loc = get_turf(owner)
|
||||
//var/list/mob/living/carbon/vamps = list()
|
||||
var/best_dist = 9999
|
||||
var/mob/living/best_vamp
|
||||
|
||||
// Track ALL MONSTERS in Game Mode
|
||||
var/list/datum/mind/monsters = list()
|
||||
monsters += SSticker.mode.bloodsuckers
|
||||
monsters += SSticker.mode.devils
|
||||
//monsters += SSticker.mode.cult
|
||||
monsters += SSticker.mode.wizards
|
||||
monsters += SSticker.mode.apprentices
|
||||
monsters += SSticker.mode.servants_of_ratvar
|
||||
//monsters += SSticker.mode.changelings Disabled anyways
|
||||
//
|
||||
for(var/datum/mind/M in monsters)
|
||||
if (!M.current || M.current == owner)// || !get_turf(M.current) || !get_turf(owner))
|
||||
continue
|
||||
for(var/a in M.antag_datums)
|
||||
var/datum/antagonist/antag_datum = a // var/datum/antagonist/antag_datum = M.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(!istype(antag_datum) || antag_datum.AmFinalDeath())
|
||||
continue
|
||||
var/their_loc = get_turf(M.current)
|
||||
var/distance = get_dist_euclidian(my_loc, their_loc)
|
||||
// Found One: Closer than previous/max distance
|
||||
if (distance < best_dist && distance <= HUNTER_SCAN_MAX_DISTANCE)
|
||||
best_dist = distance
|
||||
best_vamp = M.current
|
||||
break // Stop searching through my antag datums and go to the next guy
|
||||
|
||||
// Found one!
|
||||
if(best_vamp)
|
||||
var/distString = best_dist <= HUNTER_SCAN_MAX_DISTANCE / 2 ? "<b>somewhere closeby!</b>" : "somewhere in the distance."
|
||||
//to_chat(owner, "<span class='warning'>You detect signs of Bloodsuckers to the <b>[dir2text(get_dir(my_loc,get_turf(targetVamp)))]!</b></span>")
|
||||
to_chat(owner, "<span class='warning'>You detect signs of monsters [distString]</span>")
|
||||
|
||||
// Will yield a "?"
|
||||
else
|
||||
to_chat(owner, "<span class='notice'>There are no monsters nearby.</span>")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// From martial.dm
|
||||
|
||||
/*
|
||||
/datum/martial_art/hunter
|
||||
name = "Hunter-Fu"
|
||||
id = "MARTIALART_HUNTER" //ID, used by mind/has_martialart
|
||||
//streak = ""
|
||||
//max_streak_length = 6
|
||||
//current_target
|
||||
//datum/martial_art/base // The permanent style. This will be null unless the martial art is temporary
|
||||
//deflection_chance = 0 //Chance to deflect projectiles
|
||||
//reroute_deflection = FALSE //Delete the bullet, or actually deflect it in some direction?
|
||||
//block_chance = 0 //Chance to block melee attacks using items while on throw mode.
|
||||
//restraining = 0 //used in cqc's disarm_act to check if the disarmed is being restrained and so whether they should be put in a chokehold or not
|
||||
//help_verb
|
||||
//no_guns = FALSE
|
||||
//allow_temp_override = TRUE //if this martial art can be overridden by temporary martial arts
|
||||
|
||||
/datum/martial_art/hunter/disarm_act(mob/living/carbon/human/A, mob/living/carbon/human/D)
|
||||
return FALSE
|
||||
|
||||
/datum/martial_art/hunter/harm_act(mob/living/carbon/human/A, mob/living/carbon/human/D)
|
||||
return FALSE
|
||||
|
||||
/datum/martial_art/hunter/grab_act(mob/living/carbon/human/A, mob/living/carbon/human/D)
|
||||
return FALSE
|
||||
|
||||
/datum/martial_art/hunter/can_use(mob/living/carbon/human/H)
|
||||
return TRUE
|
||||
|
||||
|
||||
/datum/martial_art/hunter/add_to_streak(element,mob/living/carbon/human/D)
|
||||
if(D != current_target)
|
||||
current_target = D
|
||||
streak = ""
|
||||
restraining = 0
|
||||
streak = streak+element
|
||||
if(length(streak) > max_streak_length)
|
||||
streak = copytext(streak,2)
|
||||
return
|
||||
|
||||
|
||||
/datum/martial_art/hunter/basic_hit(mob/living/carbon/human/A,mob/living/carbon/human/D)
|
||||
|
||||
var/damage = rand(A.dna.species.punchdamagelow, A.dna.species.punchdamagehigh)
|
||||
*/
|
||||
*/
|
||||
@@ -0,0 +1,146 @@
|
||||
#define VASSAL_SCAN_MIN_DISTANCE 5
|
||||
#define VASSAL_SCAN_MAX_DISTANCE 500
|
||||
#define VASSAL_SCAN_PING_TIME 20 //2s update time.
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/attempt_turn_vassal(mob/living/carbon/C)
|
||||
C.silent = 0
|
||||
return SSticker.mode.make_vassal(C,owner)
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/FreeAllVassals()
|
||||
for (var/datum/antagonist/vassal/V in vassals)
|
||||
SSticker.mode.remove_vassal(V.owner)
|
||||
|
||||
/datum/antagonist/vassal
|
||||
name = "Vassal"//WARNING: DO NOT SELECT" // "Vassal"
|
||||
roundend_category = "vassals"
|
||||
antagpanel_category = "Bloodsucker"
|
||||
job_rank = ROLE_BLOODSUCKER
|
||||
var/datum/antagonist/bloodsucker/master // Who made me?
|
||||
var/list/datum/action/powers = list()// Purchased powers
|
||||
var/list/datum/objective/objectives_given = list() // For removal if needed.
|
||||
|
||||
/datum/antagonist/vassal/can_be_owned(datum/mind/new_owner)
|
||||
// If we weren't created by a bloodsucker, then we cannot be a vassal (assigned from antag panel)
|
||||
if (!master)
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
/datum/antagonist/vassal/on_gain()
|
||||
SSticker.mode.vassals |= owner // Add if not already in here (and you might be, if you were picked at round start)
|
||||
// Mindslave Add
|
||||
if(master)
|
||||
var/datum/antagonist/bloodsucker/B = master.owner.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(B)
|
||||
B.vassals |= src
|
||||
owner.enslave_mind_to_creator(master.owner.current)
|
||||
// Master Pinpointer
|
||||
owner.current.apply_status_effect(/datum/status_effect/agent_pinpointer/vassal_edition)
|
||||
// Powers
|
||||
var/datum/action/bloodsucker/vassal/recuperate/new_Recuperate = new ()
|
||||
new_Recuperate.Grant(owner.current)
|
||||
powers += new_Recuperate
|
||||
// Give Vassal Objective
|
||||
var/datum/objective/bloodsucker/vassal/vassal_objective = new
|
||||
vassal_objective.owner = owner
|
||||
vassal_objective.generate_objective()
|
||||
objectives += vassal_objective
|
||||
objectives_given += vassal_objective
|
||||
give_thrall_eyes()
|
||||
owner.current.grant_language(/datum/language/vampiric)
|
||||
// Add Antag HUD
|
||||
update_vassal_icons_added(owner.current, "vassal")
|
||||
. = ..()
|
||||
|
||||
/datum/antagonist/vassal/proc/give_thrall_eyes()
|
||||
var/obj/item/organ/eyes/vassal/E = new
|
||||
E.Insert(owner.current)
|
||||
|
||||
/datum/antagonist/vassal/proc/remove_thrall_eyes()
|
||||
var/obj/item/organ/eyes/E = new
|
||||
E.Insert(owner.current)
|
||||
|
||||
/datum/antagonist/vassal/on_removal()
|
||||
SSticker.mode.vassals -= owner // Add if not already in here (and you might be, if you were picked at round start)
|
||||
// Mindslave Remove
|
||||
if (master && master.owner)
|
||||
master.vassals -= src
|
||||
if (owner.enslaved_to == master.owner.current)
|
||||
owner.enslaved_to = null
|
||||
// Master Pinpointer
|
||||
owner.current.remove_status_effect(/datum/status_effect/agent_pinpointer/vassal_edition)
|
||||
// Powers
|
||||
while(powers.len)
|
||||
var/datum/action/power = pick(powers)
|
||||
powers -= power
|
||||
power.Remove(owner.current)
|
||||
// Remove Hunter Objectives
|
||||
for(var/O in objectives_given)
|
||||
objectives -= O
|
||||
qdel(O)
|
||||
objectives_given = list()
|
||||
remove_thrall_eyes()
|
||||
owner.current.remove_language(/datum/language/vampiric)
|
||||
// Clear Antag HUD
|
||||
update_vassal_icons_removed(owner.current)
|
||||
|
||||
. = ..()
|
||||
|
||||
/datum/antagonist/vassal/greet()
|
||||
to_chat(owner, "<span class='userdanger'>You are now the mortal servant of [master.owner.current], a bloodsucking vampire!</span>")
|
||||
to_chat(owner, "<span class='boldannounce'>The power of [master.owner.current.p_their()] immortal blood compells you to obey [master.owner.current.p_them()] in all things, even offering your own life to prolong theirs.<br>\
|
||||
You are not required to obey any other Bloodsucker, for only [master.owner.current] is your master. The laws of Nanotransen do not apply to you now; only your vampiric master's word must be obeyed.<span>")
|
||||
// Effects...
|
||||
owner.current.playsound_local(null, 'sound/magic/mutate.ogg', 100, FALSE, pressure_affected = FALSE)
|
||||
//owner.store_memory("You became the mortal servant of [master.owner.current], a bloodsucking vampire!")
|
||||
antag_memory += "You became the mortal servant of <b>[master.owner.current]</b>, a bloodsucking vampire!<br>"
|
||||
// And to your new Master...
|
||||
to_chat(master.owner, "<span class='userdanger'>[owner.current] has become addicted to your immortal blood. [owner.current.p_they(TRUE)] [owner.current.p_are()] now your undying servant!</span>")
|
||||
master.owner.current.playsound_local(null, 'sound/magic/mutate.ogg', 100, FALSE, pressure_affected = FALSE)
|
||||
|
||||
/datum/antagonist/vassal/farewell()
|
||||
owner.current.visible_message("[owner.current]'s eyes dart feverishly from side to side, and then stop. [owner.current.p_they(TRUE)] seem[owner.current.p_s()] calm, \
|
||||
like [owner.current.p_they()] [owner.current.p_have()] regained some lost part of [owner.current.p_them()]self.",\
|
||||
"<span class='userdanger'><FONT size = 3>With a snap, you are no longer enslaved to [master.owner]! You breathe in heavily, having regained your free will.</FONT></span>")
|
||||
owner.current.playsound_local(null, 'sound/magic/mutate.ogg', 100, FALSE, pressure_affected = FALSE)
|
||||
// And to your former Master...
|
||||
//if (master && master.owner)
|
||||
// to_chat(master.owner, "<span class='userdanger'>You feel the bond with your vassal [owner.current] has somehow been broken!</span>")
|
||||
|
||||
/datum/status_effect/agent_pinpointer/vassal_edition
|
||||
id = "agent_pinpointer"
|
||||
alert_type = /obj/screen/alert/status_effect/agent_pinpointer/vassal_edition
|
||||
minimum_range = VASSAL_SCAN_MIN_DISTANCE
|
||||
tick_interval = VASSAL_SCAN_PING_TIME
|
||||
duration = -1 // runs out fast
|
||||
range_fuzz_factor = 0
|
||||
|
||||
/obj/screen/alert/status_effect/agent_pinpointer/vassal_edition
|
||||
name = "Blood Bond"
|
||||
desc = "You always know where your master is."
|
||||
//icon = 'icons/obj/device.dmi'
|
||||
//icon_state = "pinon"
|
||||
|
||||
/datum/status_effect/agent_pinpointer/vassal_edition/on_creation(mob/living/new_owner, ...)
|
||||
..()
|
||||
|
||||
var/datum/antagonist/vassal/antag_datum = new_owner.mind.has_antag_datum(ANTAG_DATUM_VASSAL)
|
||||
scan_target = antag_datum?.master?.owner?.current
|
||||
|
||||
/datum/status_effect/agent_pinpointer/vassal_edition/scan_for_target()
|
||||
// DO NOTHING. We already have our target, and don't wanna do anything from agent_pinpointer
|
||||
|
||||
/datum/antagonist/vassal/proc/update_vassal_icons_added(mob/living/vassal, icontype="vassal")
|
||||
var/datum/atom_hud/antag/bloodsucker/hud = GLOB.huds[ANTAG_HUD_BLOODSUCKER]
|
||||
hud.join_hud(vassal)
|
||||
set_antag_hud(vassal, icontype) // Located in icons/mob/hud.dmi
|
||||
owner.current.hud_list[ANTAG_HUD].icon = image('icons/mob/hud.dmi', owner.current, "bloodsucker")
|
||||
|
||||
/datum/antagonist/vassal/proc/update_vassal_icons_removed(mob/living/vassal)
|
||||
var/datum/atom_hud/antag/hud = GLOB.huds[ANTAG_HUD_BLOODSUCKER]
|
||||
hud.leave_hud(vassal)
|
||||
set_antag_hud(vassal, null)
|
||||
|
||||
//Displayed at the start of roundend_category section, default to roundend_category header
|
||||
/*/datum/antagonist/vassal/roundend_report_header()
|
||||
return "<span class='header'>Loyal to their bloodsucking masters, the Vassals were:</span><br><br>"*/
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/CheckVampOrgans()
|
||||
// Do I have any parts that need replacing?
|
||||
var/obj/item/organ/O
|
||||
// Heart
|
||||
O = owner.current.getorganslot(ORGAN_SLOT_HEART)
|
||||
if(!istype(O, /obj/item/organ/heart/vampheart))
|
||||
qdel(O)
|
||||
var/obj/item/organ/heart/vampheart/H = new
|
||||
H.Insert(owner.current)
|
||||
H.Stop() // Now...stop beating!
|
||||
// Eyes
|
||||
O = owner.current.getorganslot(ORGAN_SLOT_EYES)
|
||||
if(!istype(O, /obj/item/organ/eyes/vassal/bloodsucker))
|
||||
qdel(O)
|
||||
var/obj/item/organ/eyes/vassal/bloodsucker/E = new
|
||||
E.Insert(owner.current)
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/RemoveVampOrgans()
|
||||
// Heart
|
||||
var/obj/item/organ/heart/H = new
|
||||
H.Insert(owner.current)
|
||||
// Eyes
|
||||
var/obj/item/organ/eyes/E = new
|
||||
E.Insert(owner.current)
|
||||
// HEART: OVERWRITE //
|
||||
// HEART //
|
||||
/obj/item/organ/heart/vampheart
|
||||
beating = 0
|
||||
var/fakingit = 0
|
||||
|
||||
/obj/item/organ/heart/vampheart/prepare_eat()
|
||||
..()
|
||||
// Do cool stuff for eating vamp heart?
|
||||
|
||||
/obj/item/organ/heart/vampheart/Restart()
|
||||
beating = 0 // DONT run ..(). We don't want to start beating again.
|
||||
return 0
|
||||
|
||||
/obj/item/organ/heart/vampheart/Stop()
|
||||
fakingit = 0
|
||||
return ..()
|
||||
|
||||
/obj/item/organ/heart/vampheart/proc/FakeStart()
|
||||
fakingit = 1 // We're pretending to beat, to fool people.
|
||||
|
||||
/obj/item/organ/heart/vampheart/HeartStrengthMessage()
|
||||
if(fakingit)
|
||||
return "a healthy"
|
||||
return "<span class='danger'>no</span>" // Bloodsuckers don't have a heartbeat at all when stopped (default is "an unstable")
|
||||
// EYES //
|
||||
|
||||
/obj/item/organ/eyes/vassal/
|
||||
lighting_alpha = 180 // LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE <--- This is too low a value at 128. We need to SEE what the darkness is so we can hide in it.
|
||||
see_in_dark = 12
|
||||
flash_protect = -1 //These eyes are weaker to flashes, but let you see in the dark
|
||||
|
||||
/obj/item/organ/eyes/vassal/bloodsucker
|
||||
flash_protect = 2 //Eye healing isnt working properly
|
||||
sight_flags = SEE_MOBS // Taken from augmented_eyesight.dm
|
||||
|
||||
/*
|
||||
// LIVER //
|
||||
/obj/item/organ/liver/vampliver
|
||||
// Livers run on_life(), which calls reagents.metabolize() in holder.dm, which calls on_mob_life.dm in the cheam (medicine_reagents.dm)
|
||||
// Holder also calls reagents.reaction_mob for the moment it happens
|
||||
|
||||
/obj/item/organ/liver/vampliver/on_life()
|
||||
var/mob/living/carbon/C = owner
|
||||
|
||||
if(!istype(C))
|
||||
return
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,176 @@
|
||||
|
||||
|
||||
// organ_internal.dm -- /obj/item/organ
|
||||
|
||||
|
||||
|
||||
// Do I have a stake in my heart?
|
||||
/mob/living/AmStaked()
|
||||
var/obj/item/bodypart/BP = get_bodypart("chest")
|
||||
if (!BP)
|
||||
return FALSE
|
||||
for(var/obj/item/I in BP.embedded_objects)
|
||||
if (istype(I,/obj/item/stake/))
|
||||
return TRUE
|
||||
return FALSE
|
||||
/mob/proc/AmStaked()
|
||||
return FALSE
|
||||
|
||||
|
||||
/mob/living/proc/StakeCanKillMe()
|
||||
return IsSleeping() || stat >= UNCONSCIOUS || blood_volume <= 0 || HAS_TRAIT(src, TRAIT_DEATHCOMA) // NOTE: You can't go to sleep in a coffin with a stake in you.
|
||||
|
||||
|
||||
///obj/item/weapon/melee/stake
|
||||
/obj/item/stake/
|
||||
name = "wooden stake"
|
||||
desc = "A simple wooden stake carved to a sharp point."
|
||||
icon = 'icons/obj/items_and_weapons.dmi'
|
||||
icon_state = "wood" // Inventory Icon
|
||||
item_state = "wood" // In-hand Icon
|
||||
lefthand_file = 'icons/mob/inhands/weapons/melee_lefthand.dmi' // File for in-hand icon
|
||||
righthand_file = 'icons/mob/inhands/weapons/melee_righthand.dmi'
|
||||
attack_verb = list("staked")
|
||||
slot_flags = ITEM_SLOT_BELT
|
||||
w_class = WEIGHT_CLASS_SMALL
|
||||
hitsound = 'sound/weapons/bladeslice.ogg'
|
||||
force = 6
|
||||
throwforce = 10
|
||||
embedding = list("embed_chance" = 25, "embedded_fall_chance" = 0.5) // UPDATE 2/10/18 embedding_behavior.dm is how this is handled
|
||||
//embed_chance = 25 // Look up "is_pointed" to see where we set stakes able to do this.
|
||||
//embedded_fall_chance = 0.5 // Chance it will fall out.
|
||||
obj_integrity = 30
|
||||
max_integrity = 30
|
||||
//embedded_fall_pain_multiplier
|
||||
var/staketime = 120 // Time it takes to embed the stake into someone's chest.
|
||||
|
||||
/obj/item/stake/basic
|
||||
name = "wooden stake"
|
||||
// This exists so Hardened/Silver Stake can't have a welding torch used on them.
|
||||
|
||||
/obj/item/stake/basic/attackby(obj/item/W, mob/user, params)
|
||||
if(istype(W, /obj/item/weldingtool))
|
||||
//if (amWelded)
|
||||
// to_chat(user, "<span class='warning'>This stake has already been treated with fire.</span>")
|
||||
// return
|
||||
//amWelded = TRUE
|
||||
// Weld it
|
||||
var/obj/item/weldingtool/WT = W
|
||||
if(WT.use(0))//remove_fuel(0,user))
|
||||
user.visible_message("[user.name] scorched the pointy end of [src] with the welding tool.", \
|
||||
"<span class='notice'>You scorch the pointy end of [src] with the welding tool.</span>", \
|
||||
"<span class='italics'>You hear welding.</span>")
|
||||
// 8 Second Timer
|
||||
if(!do_mob(user, src, 80))
|
||||
return
|
||||
// Create the Stake
|
||||
qdel(src)
|
||||
var/obj/item/stake/hardened/new_item = new(usr.loc)
|
||||
user.put_in_hands(new_item)
|
||||
else
|
||||
return ..()
|
||||
|
||||
/obj/item/stake/afterattack(atom/target, mob/user, proximity)
|
||||
//to_chat(world, "<span class='notice'>DEBUG: Staking </span>")
|
||||
// Invalid Target, or not targetting chest with HARM intent?
|
||||
if(!iscarbon(target) || check_zone(user.zone_selected) != "chest" || user.a_intent != INTENT_HARM)
|
||||
return
|
||||
var/mob/living/carbon/C = target
|
||||
// Needs to be Down/Slipped in some way to Stake.
|
||||
if(!C.can_be_staked() || target == user)
|
||||
to_chat(user, "<span class='danger'>You can't stake [target] when they are moving about! They have to be laying down or grabbed by the neck!</span>")
|
||||
return
|
||||
// Oops! Can't.
|
||||
if(HAS_TRAIT(C, TRAIT_PIERCEIMMUNE))
|
||||
to_chat(user, "<span class='danger'>[target]'s chest resists the stake. It won't go in.</span>")
|
||||
return
|
||||
// Make Attempt...
|
||||
to_chat(user, "<span class='notice'>You put all your weight into embedding the stake into [target]'s chest...</span>")
|
||||
playsound(user, 'sound/magic/Demon_consume.ogg', 50, 1)
|
||||
if(!do_mob(user, C, staketime, 0, 1, extra_checks=CALLBACK(C, /mob/living/carbon/proc/can_be_staked))) // user / target / time / uninterruptable / show progress bar / extra checks
|
||||
return
|
||||
// Drop & Embed Stake
|
||||
user.visible_message("<span class='danger'>[user.name] drives the [src] into [target]'s chest!</span>", \
|
||||
"<span class='danger'>You drive the [src] into [target]'s chest!</span>")
|
||||
playsound(get_turf(target), 'sound/effects/splat.ogg', 40, 1)
|
||||
user.dropItemToGround(src, TRUE) //user.drop_item() // "drop item" doesn't seem to exist anymore. New proc is user.dropItemToGround() but it doesn't seem like it's needed now?
|
||||
var/obj/item/bodypart/B = C.get_bodypart("chest") // This was all taken from hitby() in human_defense.dm
|
||||
B.embedded_objects |= src
|
||||
add_mob_blood(target)//Place blood on the stake
|
||||
loc = C // Put INSIDE the character
|
||||
B.receive_damage(w_class * embedding.embedded_impact_pain_multiplier)
|
||||
if(C.mind)
|
||||
var/datum/antagonist/bloodsucker/bloodsucker = C.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(bloodsucker)
|
||||
// If DEAD or TORPID...kill vamp!
|
||||
if(C.StakeCanKillMe()) // NOTE: This is the ONLY time a staked Torpid vamp dies.
|
||||
bloodsucker.FinalDeath()
|
||||
return
|
||||
else
|
||||
to_chat(target, "<span class='userdanger'>You have been staked! Your powers are useless, your death forever, while it remains in place.</span>")
|
||||
to_chat(user, "<span class='warning'>You missed [C.p_their(TRUE)]'s heart! It would be easier if [C.p_they(TRUE)] weren't struggling so much.</span>")
|
||||
|
||||
// Can this target be staked? If someone stands up before this is complete, it fails. Best used on someone stationary.
|
||||
/mob/living/carbon/proc/can_be_staked()
|
||||
//return resting || IsKnockdown() || IsUnconscious() || (stat && (stat != SOFT_CRIT || pulledby)) || (has_trait(TRAIT_FAKEDEATH)) || resting || IsStun() || IsFrozen() || (pulledby && pulledby.grab_state >= GRAB_NECK)
|
||||
return (resting || lying || IsUnconscious() || pulledby && pulledby.grab_state >= GRAB_NECK)
|
||||
// ABOVE: Taken from update_mobility() in living.dm
|
||||
|
||||
/obj/item/stake/hardened
|
||||
// Created by welding and acid-treating a simple stake.
|
||||
name = "hardened stake"
|
||||
desc = "A hardened wooden stake carved to a sharp point and scorched at the end."
|
||||
icon_state = "hardened" // Inventory Icon
|
||||
force = 8
|
||||
throwforce = 12
|
||||
armour_penetration = 10
|
||||
embedding = list("embed_chance" = 50, "embedded_fall_chance" = 0) // UPDATE 2/10/18 embedding_behavior.dm is how this is handled
|
||||
obj_integrity = 120
|
||||
max_integrity = 120
|
||||
|
||||
staketime = 80
|
||||
|
||||
/obj/item/stake/hardened/silver
|
||||
name = "silver stake"
|
||||
desc = "Polished and sharp at the end. For when some mofo is always trying to iceskate uphill."
|
||||
icon_state = "silver" // Inventory Icon
|
||||
item_state = "silver" // In-hand Icon
|
||||
siemens_coefficient = 1 //flags = CONDUCT // var/siemens_coefficient = 1 // for electrical admittance/conductance (electrocution checks and shit)
|
||||
force = 9
|
||||
armour_penetration = 25
|
||||
embedding = list("embed_chance" = 65) // UPDATE 2/10/18 embedding_behavior.dm is how this is handled
|
||||
obj_integrity = 300
|
||||
max_integrity = 300
|
||||
|
||||
staketime = 60
|
||||
|
||||
// Convert back to Silver
|
||||
/obj/item/stake/hardened/silver/attackby(obj/item/I, mob/user, params)
|
||||
if(istype(I, /obj/item/weldingtool))
|
||||
var/obj/item/weldingtool/WT = I
|
||||
if(WT.use(0))//remove_fuel(0, user))
|
||||
var/obj/item/stack/sheet/mineral/silver/newsheet = new (user.loc)
|
||||
for(var/obj/item/stack/sheet/mineral/silver/S in user.loc)
|
||||
if(S == newsheet)
|
||||
continue
|
||||
if(S.amount >= S.max_amount)
|
||||
continue
|
||||
S.attackby(newsheet, user)
|
||||
to_chat(user, "<span class='notice'>You melt down the stake and add it to the stack. It now contains [newsheet.amount] sheet\s.</span>")
|
||||
qdel(src)
|
||||
else
|
||||
return ..()
|
||||
|
||||
|
||||
// Look up recipes.dm OR pneumaticCannon.dm
|
||||
/datum/crafting_recipe/silver_stake
|
||||
name = "Silver Stake"
|
||||
result = /obj/item/stake/hardened/silver
|
||||
tools = list(/obj/item/weldingtool)
|
||||
reqs = list(/obj/item/stack/sheet/mineral/silver = 1,
|
||||
/obj/item/stake/hardened = 1)
|
||||
///obj/item/stack/packageWrap = 8,
|
||||
///obj/item/pipe = 2)
|
||||
time = 80
|
||||
category = CAT_WEAPONRY
|
||||
subcategory = CAT_WEAPON
|
||||
@@ -0,0 +1,242 @@
|
||||
|
||||
|
||||
// TRAIT_DEATHCOMA - Activate this when you're in your coffin to simulate sleep/death.
|
||||
|
||||
|
||||
// Coffins...
|
||||
// -heal all wounds, and quickly.
|
||||
// -restore limbs & organs
|
||||
//
|
||||
|
||||
// Without Coffins...
|
||||
// -
|
||||
// -limbs stay lost
|
||||
|
||||
|
||||
|
||||
// To put to sleep: use owner.current.fakedeath("bloodsucker") but change name to "bloodsucker_coffin" so you continue to stay fakedeath despite healing in the main thread!
|
||||
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/ClaimCoffin(obj/structure/closet/crate/claimed) // NOTE: This can be any "closet" that you are resting AND inside of.
|
||||
// ALREADY CLAIMED
|
||||
if(claimed.resident)
|
||||
if(claimed.resident == owner.current)
|
||||
to_chat(owner, "This is your [src].")
|
||||
else
|
||||
to_chat(owner, "This [src] has already been claimed by another.")
|
||||
return FALSE
|
||||
// Bloodsucker Learns new Recipes!
|
||||
owner.teach_crafting_recipe(/datum/crafting_recipe/bloodsucker/vassalrack)
|
||||
owner.teach_crafting_recipe(/datum/crafting_recipe/bloodsucker/candelabrum)
|
||||
// This is my Lair
|
||||
coffin = claimed
|
||||
lair = get_area(claimed)
|
||||
// DONE
|
||||
to_chat(owner, "<span class='userdanger'>You have claimed the [claimed] as your place of immortal rest! Your lair is now [lair].</span>")
|
||||
to_chat(owner, "<span class='danger'>You have learned new construction recipes to improve your lair.</span>")
|
||||
to_chat(owner, "<span class='announce'>Bloodsucker Tip: Find new lair recipes in the misc tab of the <i>Crafting Menu</i> at the bottom of the screen, including the <i>Persuasion Rack</i> for converting crew into Vassals.</span><br><br>")
|
||||
RunLair() // Start
|
||||
return TRUE
|
||||
|
||||
// crate.dm
|
||||
/obj/structure/closet/crate
|
||||
var/mob/living/resident // This lets bloodsuckers claim any "closet" as a Coffin, so long as they could get into it and close it. This locks it in place, too.
|
||||
|
||||
/obj/structure/closet/crate/coffin
|
||||
var/pryLidTimer = 250
|
||||
can_weld_shut = FALSE
|
||||
breakout_time = 200
|
||||
|
||||
|
||||
/obj/structure/closet/crate/coffin/blackcoffin
|
||||
name = "black coffin"
|
||||
desc = "For those departed who are not so dear."
|
||||
icon_state = "coffin"
|
||||
icon = 'icons/obj/vamp_obj.dmi'
|
||||
can_weld_shut = FALSE
|
||||
resistance_flags = 0 // Start off with no bonuses.
|
||||
open_sound = 'sound/bloodsucker/coffin_open.ogg'
|
||||
close_sound = 'sound/bloodsucker/coffin_close.ogg'
|
||||
breakout_time = 600
|
||||
pryLidTimer = 400
|
||||
resistance_flags = NONE
|
||||
|
||||
/obj/structure/closet/crate/coffin/meatcoffin
|
||||
name = "meat coffin"
|
||||
desc = "When you're ready to meat your maker, the steaks can never be too high."
|
||||
icon_state = "meatcoffin"
|
||||
icon = 'icons/obj/vamp_obj.dmi'
|
||||
can_weld_shut = FALSE
|
||||
resistance_flags = 0 // Start off with no bonuses.
|
||||
open_sound = 'sound/effects/footstep/slime1.ogg'
|
||||
close_sound = 'sound/effects/footstep/slime1.ogg'
|
||||
breakout_time = 200
|
||||
pryLidTimer = 200
|
||||
resistance_flags = NONE
|
||||
material_drop = /obj/item/reagent_containers/food/snacks/meat/slab
|
||||
material_drop_amount = 3
|
||||
|
||||
/obj/structure/closet/crate/coffin/metalcoffin
|
||||
name = "metal coffin"
|
||||
desc = "A big metal sardine can inside of another big metal sardine can, in space."
|
||||
icon_state = "metalcoffin"
|
||||
icon = 'icons/obj/vamp_obj.dmi'
|
||||
can_weld_shut = FALSE
|
||||
resistance_flags = FIRE_PROOF | LAVA_PROOF
|
||||
open_sound = 'sound/effects/pressureplate.ogg'
|
||||
close_sound = 'sound/effects/pressureplate.ogg'
|
||||
breakout_time = 300
|
||||
pryLidTimer = 200
|
||||
resistance_flags = NONE
|
||||
material_drop = /obj/item/stack/sheet/metal
|
||||
material_drop_amount = 5
|
||||
|
||||
//////////////////////////////////////////////
|
||||
|
||||
/obj/structure/closet/crate/proc/ClaimCoffin(mob/living/claimant) // NOTE: This can be any "closet" that you are resting AND inside of.
|
||||
// Bloodsucker Claim
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = claimant.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(bloodsuckerdatum)
|
||||
// Vamp Successfuly Claims Me?
|
||||
if(bloodsuckerdatum.ClaimCoffin(src))
|
||||
resident = claimant
|
||||
anchored = 1 // No moving this
|
||||
|
||||
/obj/structure/closet/crate/coffin/Destroy()
|
||||
UnclaimCoffin()
|
||||
return ..()
|
||||
|
||||
/obj/structure/closet/crate/proc/UnclaimCoffin()
|
||||
if (resident)
|
||||
// Vamp Un-Claim
|
||||
if (resident.mind)
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = resident.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if (bloodsuckerdatum && bloodsuckerdatum.coffin == src)
|
||||
bloodsuckerdatum.coffin = null
|
||||
bloodsuckerdatum.lair = null
|
||||
to_chat(resident, "<span class='danger'><span class='italics'>You sense that the link with your coffin, your sacred place of rest, has been brokem! You will need to seek another.</span></span>")
|
||||
resident = null // Remove resident. Because this object isnt removed from the game immediately (GC?) we need to give them a way to see they don't have a home anymore.
|
||||
|
||||
/obj/structure/closet/crate/coffin/can_open(mob/living/user)
|
||||
// You cannot lock in/out a coffin's owner. SORRY.
|
||||
if (locked)
|
||||
if(user == resident)
|
||||
if (welded)
|
||||
welded = FALSE
|
||||
update_icon()
|
||||
//to_chat(user, "<span class='notice'>You flip a secret latch and unlock [src].</span>") // Don't bother. We know it's unlocked.
|
||||
locked = FALSE
|
||||
return 1
|
||||
else
|
||||
playsound(get_turf(src), 'sound/machines/door_locked.ogg', 20, 1)
|
||||
to_chat(user, "<span class='notice'>[src] is locked tight from the inside.</span>")
|
||||
return ..()
|
||||
|
||||
/obj/structure/closet/crate/coffin/close(mob/living/user)
|
||||
var/turf/Turf = get_turf(src)
|
||||
var/area/A = get_area(src)
|
||||
if (!..())
|
||||
return FALSE
|
||||
// Only the User can put themself into Torpor (if you're already in it, you'll start to heal)
|
||||
if((user in src))
|
||||
// Bloodsucker Only
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if(bloodsuckerdatum)
|
||||
LockMe(user)
|
||||
Turf = get_turf(user) //we may have moved. adjust as needed...
|
||||
A = get_area(src)
|
||||
// Claim?
|
||||
if(!bloodsuckerdatum.coffin && !resident && (is_station_level(Turf.z) || !A.map_name == "Space"))
|
||||
switch(alert(user,"Do you wish to claim this as your coffin? [get_area(src)] will be your lair.","Claim Lair","Yes", "No"))
|
||||
if("Yes")
|
||||
ClaimCoffin(user)
|
||||
if (user.AmStaked()) // Stake? No Heal!
|
||||
to_chat(bloodsuckerdatum.owner.current, "<span class='userdanger'>You are staked! Remove the offending weapon from your heart before sleeping.</span>")
|
||||
return
|
||||
// Heal
|
||||
if(bloodsuckerdatum.HandleHealing(0)) // Healing Mult 0 <--- We only want to check if healing is valid!
|
||||
to_chat(bloodsuckerdatum.owner.current, "<span class='notice'>You enter the horrible slumber of deathless Torpor. You will heal until you are renewed.</span>")
|
||||
bloodsuckerdatum.Torpor_Begin()
|
||||
// Level Up?
|
||||
bloodsuckerdatum.SpendRank() // Auto-Fails if not appropriate
|
||||
return TRUE
|
||||
|
||||
/obj/structure/closet/crate/coffin/attackby(obj/item/W, mob/user, params)
|
||||
// You cannot weld or deconstruct an owned coffin. STILL NOT SORRY.
|
||||
if (resident != null && user != resident) // Owner can destroy their own coffin.
|
||||
if(opened)
|
||||
if(istype(W, cutting_tool))
|
||||
to_chat(user, "<span class='notice'>This is a much more complex mechanical structure than you thought. You don't know where to begin cutting [src].</span>")
|
||||
return
|
||||
else if(anchored && istype(W, /obj/item/wrench)) // Can't unanchor unless owner.
|
||||
to_chat(user, "<span class='danger'>The coffin won't come unanchored from the floor.</span>")
|
||||
return
|
||||
|
||||
if(locked && istype(W, /obj/item/crowbar))
|
||||
var/pry_time = pryLidTimer * W.toolspeed // Pry speed must be affected by the speed of the tool.
|
||||
user.visible_message("<span class='notice'>[user] tries to pry the lid off of [src] with [W].</span>", \
|
||||
"<span class='notice'>You begin prying the lid off of [src] with [W]. This should take about [DisplayTimeText(pry_time)].</span>")
|
||||
if (!do_mob(user,src,pry_time))
|
||||
return
|
||||
bust_open()
|
||||
user.visible_message("<span class='notice'>[user] snaps the door of [src] wide open.</span>", \
|
||||
"<span class='notice'>The door of [src] snaps open.</span>")
|
||||
return
|
||||
..()
|
||||
|
||||
|
||||
|
||||
/obj/structure/closet/crate/coffin/AltClick(mob/user)
|
||||
// Distance Check (Inside Of)
|
||||
if (user in src) // user.Adjacent(src)
|
||||
LockMe(user, !locked)
|
||||
|
||||
/obj/structure/closet/crate/proc/LockMe(mob/user, inLocked = TRUE)
|
||||
// Lock
|
||||
if (user == resident)
|
||||
if (!broken)
|
||||
locked = inLocked
|
||||
to_chat(user, "<span class='notice'>You flip a secret latch and [locked?"":"un"]lock yourself inside [src].</span>")
|
||||
else
|
||||
to_chat(resident, "<span class='notice'>The secret latch to lock [src] from the inside is broken. You set it back into place...</span>")
|
||||
if (do_mob(resident, src, 50))//sleep(10)
|
||||
if (broken) // Spam Safety
|
||||
to_chat(resident, "<span class='notice'>You fix the mechanism and lock it.</span>")
|
||||
broken = FALSE
|
||||
locked = TRUE
|
||||
|
||||
// Look up recipes.dm OR pneumaticCannon.dm
|
||||
/datum/crafting_recipe/bloodsucker/blackcoffin
|
||||
name = "Black Coffin"
|
||||
result = /obj/structure/closet/crate/coffin/blackcoffin
|
||||
tools = list(/obj/item/weldingtool,
|
||||
/obj/item/screwdriver)
|
||||
reqs = list(/obj/item/stack/sheet/cloth = 1,
|
||||
/obj/item/stack/sheet/mineral/wood = 5,
|
||||
/obj/item/stack/sheet/metal = 1)
|
||||
///obj/item/stack/packageWrap = 8,
|
||||
///obj/item/pipe = 2)
|
||||
time = 150
|
||||
category = CAT_MISC
|
||||
always_availible = TRUE
|
||||
|
||||
/datum/crafting_recipe/bloodsucker/meatcoffin
|
||||
name = "Meat Coffin"
|
||||
result =/obj/structure/closet/crate/coffin/meatcoffin
|
||||
tools = list(/obj/item/kitchen/knife,
|
||||
/obj/item/kitchen/rollingpin)
|
||||
reqs = list(/obj/item/reagent_containers/food/snacks/meat/slab = 5,
|
||||
/obj/item/restraints/handcuffs/cable = 1)
|
||||
time = 150
|
||||
category = CAT_MISC
|
||||
always_availible = TRUE
|
||||
|
||||
/datum/crafting_recipe/bloodsucker/metalcoffin
|
||||
name = "Metal Coffin"
|
||||
result =/obj/structure/closet/crate/coffin/metalcoffin
|
||||
tools = list(/obj/item/weldingtool,
|
||||
/obj/item/screwdriver)
|
||||
reqs = list(/obj/item/stack/sheet/metal = 5)
|
||||
time = 100
|
||||
category = CAT_MISC
|
||||
always_availible = TRUE
|
||||
@@ -0,0 +1,494 @@
|
||||
|
||||
|
||||
|
||||
// IDEAS --
|
||||
// An object that disguises your coffin while you're in it!
|
||||
//
|
||||
// An object that lets your lair itself protect you from sunlight, like a coffin would (no healing tho)
|
||||
|
||||
|
||||
|
||||
// Hide a random object somewhere on the station:
|
||||
// var/turf/targetturf = get_random_station_turf()
|
||||
// var/turf/targetturf = get_safe_random_station_turf()
|
||||
|
||||
|
||||
|
||||
|
||||
// CRYPT OBJECTS
|
||||
//
|
||||
//
|
||||
// PODIUM Stores your Relics
|
||||
//
|
||||
// ALTAR Transmute items into sacred items.
|
||||
//
|
||||
// PORTRAIT Gaze into your past to: restore mood boost?
|
||||
//
|
||||
// BOOKSHELF Discover secrets about crew and locations. Learn languages. Learn marial arts.
|
||||
//
|
||||
// BRAZER Burn rare ingredients to gleen insights.
|
||||
//
|
||||
// RUG Ornate, and creaks when stepped upon by any humanoid other than yourself and your vassals.
|
||||
//
|
||||
// X COFFIN (Handled elsewhere)
|
||||
//
|
||||
// X CANDELABRA (Handled elsewhere)
|
||||
//
|
||||
// THRONE Your mental powers work at any range on anyone inside your crypt.
|
||||
//
|
||||
// MIRROR Find any person
|
||||
//
|
||||
// BUST/STATUE Create terror, but looks just like you (maybe just in Examine?)
|
||||
|
||||
|
||||
// RELICS
|
||||
//
|
||||
// RITUAL DAGGER
|
||||
//
|
||||
// SKULL
|
||||
//
|
||||
// VAMPIRIC SCROLL
|
||||
//
|
||||
// SAINTS BONES
|
||||
//
|
||||
// GRIMOIRE
|
||||
|
||||
|
||||
// RARE INGREDIENTS
|
||||
// Ore
|
||||
// Books (Manuals)
|
||||
|
||||
|
||||
// NOTE: Look up AI and Sentient Disease to see how the game handles the selector logo that only one player is allowed to see. We could add hud for vamps to that?
|
||||
// ALTERNATIVELY, use the Vamp Huds on relics to mark them, but only show to relevant vamps?
|
||||
|
||||
|
||||
/obj/structure/bloodsucker
|
||||
var/mob/living/owner
|
||||
|
||||
/*
|
||||
/obj/structure/bloodsucker/bloodthrone
|
||||
name = "wicked throne"
|
||||
desc = "Twisted metal shards jut from the arm rests. Very uncomfortable looking. It would take a sadistic sort to sit on this jagged piece of furniture."
|
||||
|
||||
/obj/structure/bloodsucker/bloodaltar
|
||||
name = "bloody altar"
|
||||
desc = "It is marble, lined with basalt, and radiates an unnerving chill that puts your skin on edge."
|
||||
|
||||
/obj/structure/bloodsucker/bloodstatue
|
||||
name = "bloody countenance"
|
||||
desc = "It looks upsettingly familiar..."
|
||||
|
||||
/obj/structure/bloodsucker/bloodportrait
|
||||
name = "oil portrait"
|
||||
desc = "A disturbingly familiar face stares back at you. On second thought, the reds don't seem to be painted in oil..."
|
||||
|
||||
/obj/structure/bloodsucker/bloodbrazer
|
||||
name = "lit brazer"
|
||||
desc = "It burns slowly, but doesn't radiate any heat."
|
||||
|
||||
/obj/structure/bloodsucker/bloodmirror
|
||||
name = "faded mirror"
|
||||
desc = "You get the sense that the foggy reflection looking back at you has an alien intelligence to it."
|
||||
*/
|
||||
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack
|
||||
name = "persuasion rack"
|
||||
desc = "If this wasn't meant for torture, then someone has some fairly horrifying hobbies."
|
||||
icon = 'icons/obj/vamp_obj.dmi'
|
||||
icon_state = "vassalrack"
|
||||
buckle_lying = FALSE
|
||||
anchored = FALSE
|
||||
density = TRUE // Start dense. Once fixed in place, go non-dense.
|
||||
can_buckle = TRUE
|
||||
var/useLock = FALSE // So we can't just keep dragging ppl on here.
|
||||
var/mob/buckled
|
||||
var/convert_progress = 3 // Resets on each new character to be added to the chair. Some effects should lower it...
|
||||
var/disloyalty_confirm = FALSE // Command & Antags need to CONFIRM they are willing to lose their role (and will only do it if the Vassal'ing succeeds)
|
||||
var/disloyalty_offered = FALSE // Has the popup been issued? Don't spam them.
|
||||
var/convert_cost = 100
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/deconstruct(disassembled = TRUE)
|
||||
new /obj/item/stack/sheet/metal(src.loc, 4)
|
||||
new /obj/item/stack/rods(loc, 4)
|
||||
qdel(src)
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/examine(mob/user)
|
||||
. = ..()
|
||||
if((user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)) || isobserver(user))
|
||||
. += {"<span class='cult'>This is the vassal rack, which allows you to thrall crewmembers into loyal minions in your service.</span>"}
|
||||
. += {"<span class='cult'>You need to first secure the vassal rack by clicking on it while it is in your lair.</span>"}
|
||||
. += {"<span class='cult'>Simply click and hold on a victim, and then drag their sprite on the vassal rack.</span>"}
|
||||
. += {"<span class='cult'>Make sure that the victim is handcuffed, or else they can simply run away or resist, as the process is not instant.</span>"}
|
||||
. += {"<span class='cult'>To convert the victim, simply click on the vassal rack itself. Sharp weapons work faster than other tools.</span>"}
|
||||
/* if(user.mind.has_antag_datum(ANTAG_DATUM_VASSAL)
|
||||
. += {"<span class='cult'>This is the vassal rack, which allows your master to thrall crewmembers into his minions.\n
|
||||
Aid your master in bringing their victims here and keeping them secure.\n
|
||||
You can secure victims to the vassal rack by click dragging the victim onto the rack while it is secured</span>"} */
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/MouseDrop_T(atom/movable/O, mob/user)
|
||||
if(!O.Adjacent(src) || O == user || !isliving(O) || !isliving(user) || useLock || has_buckled_mobs() || user.incapacitated())
|
||||
return
|
||||
if(!anchored && isvamp(user))
|
||||
to_chat(user, "<span class='danger'>Until this rack is secured in place, it cannot serve its purpose.</span>")
|
||||
return
|
||||
// PULL TARGET: Remember if I was pullin this guy, so we can restore this
|
||||
var/waspulling = (O == owner.pulling)
|
||||
var/wasgrabstate = owner.grab_state
|
||||
// * MOVE! *
|
||||
O.forceMove(drop_location())
|
||||
// PULL TARGET: Restore?
|
||||
if(waspulling)
|
||||
owner.start_pulling(O, wasgrabstate, TRUE)
|
||||
// NOTE: in bs_lunge.dm, we use [target.grabbedby(owner)], which simulates doing a grab action. We don't want that though...we're cutting directly back to where we were in a grab.
|
||||
// Do Action!
|
||||
useLock = TRUE
|
||||
if(do_mob(user, O, 50))
|
||||
attach_victim(O,user)
|
||||
useLock = FALSE
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/AltClick(mob/user)
|
||||
if(!has_buckled_mobs() || !isliving(user) || useLock)
|
||||
return
|
||||
// Attempt Release (Owner vs Non Owner)
|
||||
var/mob/living/carbon/C = pick(buckled_mobs)
|
||||
if(C)
|
||||
if(user == owner)
|
||||
unbuckle_mob(C)
|
||||
else
|
||||
user_unbuckle_mob(C,user)
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/proc/attach_victim(mob/living/M, mob/living/user)
|
||||
// Standard Buckle Check
|
||||
if(!buckle_mob(M)) // force=TRUE))
|
||||
return
|
||||
// Attempt Buckle
|
||||
user.visible_message("<span class='notice'>[user] straps [M] into the rack, immobilizing them.</span>", \
|
||||
"<span class='boldnotice'>You secure [M] tightly in place. They won't escape you now.</span>")
|
||||
|
||||
playsound(src.loc, 'sound/effects/pop_expl.ogg', 25, 1)
|
||||
//M.forceMove(drop_location()) <--- CANT DO! This cancels the buckle_mob() we JUST did (even if we foced the move)
|
||||
M.setDir(2)
|
||||
density = TRUE
|
||||
var/matrix/m180 = matrix(M.transform)
|
||||
m180.Turn(180)//90)//180
|
||||
animate(M, transform = m180, time = 2)
|
||||
M.pixel_y = -2 //M.get_standard_pixel_y_offset(120)//180)
|
||||
update_icon()
|
||||
// Torture Stuff
|
||||
convert_progress = 2 // Goes down unless you start over.
|
||||
disloyalty_confirm = FALSE // New guy gets the chance to say NO if he's special.
|
||||
disloyalty_offered = FALSE // Prevents spamming torture window.
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/user_unbuckle_mob(mob/living/M, mob/user)
|
||||
// Attempt Unbuckle
|
||||
if(!isvamp(user))
|
||||
if(M == user)
|
||||
M.visible_message("<span class='danger'>[user] tries to release themself from the rack!</span>",\
|
||||
"<span class='danger'>You attempt to release yourself from the rack!</span>") // For sound if not seen --> "<span class='italics'>You hear a squishy wet noise.</span>")
|
||||
else
|
||||
M.visible_message("<span class='danger'>[user] tries to pull [M] rack!</span>",\
|
||||
"<span class='danger'>[user] attempts to release you from the rack!</span>") // For sound if not seen --> "<span class='italics'>You hear a squishy wet noise.</span>")
|
||||
if(!do_mob(user, M, 100))
|
||||
return
|
||||
// Did the time. Now try to do it.
|
||||
..()
|
||||
unbuckle_mob(M)
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/unbuckle_mob(mob/living/buckled_mob, force = FALSE)
|
||||
if(!..())
|
||||
return
|
||||
var/matrix/m180 = matrix(buckled_mob.transform)
|
||||
m180.Turn(180)//-90)//180
|
||||
animate(buckled_mob, transform = m180, time = 2)
|
||||
buckled_mob.pixel_y = buckled_mob.get_standard_pixel_y_offset(180)
|
||||
src.visible_message(text("<span class='danger'>[buckled_mob][buckled_mob.stat==DEAD?"'s corpse":""] slides off of the rack.</span>"))
|
||||
density = FALSE
|
||||
buckled_mob.AdjustKnockdown(30)
|
||||
update_icon()
|
||||
useLock = FALSE // Failsafe
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/attackby(obj/item/W, mob/user, params)
|
||||
if(has_buckled_mobs()) // Attack w/weapon vs guy standing there? Don't do an attack.
|
||||
attack_hand(user)
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/attack_hand(mob/user)
|
||||
//. = ..() // Taken from sacrificial altar in divine.dm
|
||||
//if(.)
|
||||
// return
|
||||
// Go away. Torturing.
|
||||
if(useLock)
|
||||
return
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
// CHECK ONE: Am I claiming this? Is it in the right place?
|
||||
if(istype(bloodsuckerdatum) && !owner)
|
||||
if(!bloodsuckerdatum.lair)
|
||||
to_chat(user, "<span class='danger'>You don't have a lair. Claim a coffin to make that location your lair.</span>")
|
||||
if(bloodsuckerdatum.lair != get_area(src))
|
||||
to_chat(user, "<span class='danger'>You may only activate this structure in your lair: [bloodsuckerdatum.lair].</span>")
|
||||
return
|
||||
switch(alert(user,"Do you wish to afix this structure here? Be aware you wont be able to unsecure it anymore","Secure [src]","Yes", "No"))
|
||||
if("Yes")
|
||||
owner = user
|
||||
density = FALSE
|
||||
anchored = TRUE
|
||||
return //No, you cant move this ever again
|
||||
// No One Home
|
||||
if(!has_buckled_mobs())
|
||||
return
|
||||
// CHECK TWO: Am I a non-bloodsucker?
|
||||
var/mob/living/carbon/C = pick(buckled_mobs)
|
||||
if(!istype(bloodsuckerdatum))
|
||||
// Try to release this guy
|
||||
user_unbuckle_mob(C, user)
|
||||
return
|
||||
// Bloodsucker Owner! Let the boy go.
|
||||
if(C.mind)
|
||||
var/datum/antagonist/vassal/vassaldatum = C.mind.has_antag_datum(ANTAG_DATUM_VASSAL)
|
||||
if (istype(vassaldatum) && vassaldatum.master == bloodsuckerdatum || C.stat >= DEAD)
|
||||
unbuckle_mob(C)
|
||||
useLock = FALSE // Failsafe
|
||||
return
|
||||
// Just torture the boy
|
||||
torture_victim(user, C)
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/proc/torture_victim(mob/living/user, mob/living/target)
|
||||
// Check Bloodmob/living/M, force = FALSE, check_loc = TRUE
|
||||
if(user.blood_volume < convert_cost + 5)
|
||||
to_chat(user, "<span class='notice'>You don't have enough blood to initiate the Dark Communion with [target].</span>")
|
||||
return
|
||||
// Prep...
|
||||
useLock = TRUE
|
||||
// Step One: Tick Down Conversion from 3 to 0
|
||||
// Step Two: Break mindshielding/antag (on approve)
|
||||
// Step Three: Blood Ritual
|
||||
// Conversion Process
|
||||
if(convert_progress > 0)
|
||||
to_chat(user, "<span class='notice'>You prepare to initiate [target] into your service.</span>")
|
||||
if(!do_torture(user,target))
|
||||
to_chat(user, "<span class='danger'><i>The ritual has been interrupted!</i></span>")
|
||||
else
|
||||
convert_progress -- // Ouch. Stop. Don't.
|
||||
// All done!
|
||||
if(convert_progress <= 0)
|
||||
// FAIL: Can't be Vassal
|
||||
if(!SSticker.mode.can_make_vassal(target, user, display_warning=FALSE) || HAS_TRAIT(target, TRAIT_MINDSHIELD)) // If I'm an unconvertable Antag ONLY
|
||||
to_chat(user, "<span class='danger'>[target] doesn't respond to your persuasion. It doesn't appear they can be converted to follow you, they either have a mindshield or their external loyalties are too difficult for you to break.<i>\[ALT+click to release\]</span>")
|
||||
convert_progress ++ // Pop it back up some. Avoids wasting Blood on a lost cause.
|
||||
// SUCCESS: All done!
|
||||
else
|
||||
if(RequireDisloyalty(target))
|
||||
to_chat(user, "<span class='boldwarning'>[target] has external loyalties! [target.p_they(TRUE)] will require more <i>persuasion</i> to break [target.p_them()] to your will!</span>")
|
||||
else
|
||||
to_chat(user, "<span class='notice'>[target] looks ready for the <b>Dark Communion</b>.</span>")
|
||||
// Still Need More Persuasion...
|
||||
else
|
||||
to_chat(user, "<span class='notice'>[target] could use [convert_progress == 1?"a little":"some"] more <i>persuasion</i>.</span>")
|
||||
useLock = FALSE
|
||||
return
|
||||
// Check: Mindshield & Antag
|
||||
if(!disloyalty_confirm && RequireDisloyalty(target))
|
||||
if(!do_disloyalty(user,target))
|
||||
to_chat(user, "<span class='danger'><i>The ritual has been interrupted!</i></span>")
|
||||
else if (!disloyalty_confirm)
|
||||
to_chat(user, "<span class='danger'>[target] refuses to give into your persuasion. Perhaps a little more?</span>")
|
||||
else
|
||||
to_chat(user, "<span class='notice'>[target] looks ready for the <b>Dark Communion</b>.</span>")
|
||||
useLock = FALSE
|
||||
return
|
||||
// Check: Blood
|
||||
if(user.blood_volume < convert_cost)
|
||||
to_chat(user, "<span class='notice'>You don't have enough blood to initiate the Dark Communion with [target].</span>")
|
||||
useLock = FALSE
|
||||
return
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
bloodsuckerdatum.AddBloodVolume(-convert_cost)
|
||||
target.add_mob_blood(user)
|
||||
user.visible_message("<span class='notice'>[user] marks a bloody smear on [target]'s forehead and puts a wrist up to [target.p_their()] mouth!</span>", \
|
||||
"<span class='notice'>You paint a bloody marking across [target]'s forehead, place your wrist to [target.p_their()] mouth, and subject [target.p_them()] to the Dark Communion.</span>")
|
||||
if(!do_mob(user, src, 50))
|
||||
to_chat(user, "<span class='danger'><i>The ritual has been interrupted!</i></span>")
|
||||
useLock = FALSE
|
||||
return
|
||||
// Convert to Vassal!
|
||||
if(bloodsuckerdatum && bloodsuckerdatum.attempt_turn_vassal(target))
|
||||
//remove_loyalties(target) // In case of Mindshield, or appropriate Antag (Traitor, Internal, etc)
|
||||
//if (!target.buckled)
|
||||
// to_chat(user, "<span class='danger'><i>The ritual has been interrupted!</i></span>")
|
||||
// useLock = FALSE
|
||||
// return
|
||||
user.playsound_local(null, 'sound/effects/explosion_distant.ogg', 40, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head.
|
||||
target.playsound_local(null, 'sound/effects/explosion_distant.ogg', 40, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head.
|
||||
target.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head.
|
||||
target.Jitter(25)
|
||||
target.emote("laugh")
|
||||
//remove_victim(target) // Remove on CLICK ONLY!
|
||||
useLock = FALSE
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/proc/do_torture(mob/living/user, mob/living/target, mult=1)
|
||||
var/torture_time = 15 // Fifteen seconds if you aren't using anything. Shorter with weapons and such.
|
||||
var/torture_dmg_brute = 2
|
||||
var/torture_dmg_burn = 0
|
||||
// Get Bodypart
|
||||
var/target_string = ""
|
||||
var/obj/item/bodypart/BP = null
|
||||
if(iscarbon(target))
|
||||
var/mob/living/carbon/C = target
|
||||
BP = pick(C.bodyparts)
|
||||
if(BP)
|
||||
target_string += BP.name
|
||||
// Get Weapon
|
||||
var/obj/item/I = user.get_active_held_item()
|
||||
if(!istype(I))
|
||||
I = user.get_inactive_held_item()
|
||||
// Create Strings
|
||||
var/method_string = I?.attack_verb?.len ? pick(I.attack_verb) : pick("harmed","tortured","wrenched","twisted","scoured","beaten","lashed","scathed")
|
||||
var/weapon_string = I ? I.name : pick("bare hands","hands","fingers","fists")
|
||||
// Weapon Bonus + SFX
|
||||
if(I)
|
||||
torture_time -= I.force / 4
|
||||
torture_dmg_brute += I.force / 4
|
||||
//torture_dmg_burn += I.
|
||||
if(I.sharpness == IS_SHARP)
|
||||
torture_time -= 1
|
||||
else if(I.sharpness == IS_SHARP_ACCURATE)
|
||||
torture_time -= 2
|
||||
if(istype(I, /obj/item/weldingtool))
|
||||
var/obj/item/weldingtool/welder = I
|
||||
welder.welding = TRUE
|
||||
torture_time -= 5
|
||||
torture_dmg_burn += 5
|
||||
I.play_tool_sound(target)
|
||||
torture_time = max(50, torture_time * 10) // Minimum 5 seconds.
|
||||
// Now run process.
|
||||
if(!do_mob(user, target, torture_time * mult))
|
||||
return FALSE
|
||||
// SUCCESS
|
||||
if(I)
|
||||
playsound(loc, I.hitsound, 30, 1, -1)
|
||||
I.play_tool_sound(target)
|
||||
target.visible_message("<span class='danger'>[user] has [method_string] [target]'s [target_string] with [user.p_their()] [weapon_string]!</span>", \
|
||||
"<span class='userdanger'>[user] has [method_string] your [target_string] with [user.p_their()] [weapon_string]!</span>")
|
||||
if(!target.is_muzzled())
|
||||
target.emote("scream")
|
||||
target.Jitter(5)
|
||||
target.apply_damages(brute = torture_dmg_brute, burn = torture_dmg_burn, def_zone = (BP ? BP.body_zone : null)) // take_overall_damage(6,0)
|
||||
return TRUE
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/proc/do_disloyalty(mob/living/user, mob/living/target)
|
||||
|
||||
// OFFER YES/NO NOW!
|
||||
spawn(10)
|
||||
if(useLock && target && target.client) // Are we still torturing? Did we cancel? Are they still here?
|
||||
to_chat(user, "<span class='notice'>[target] has been given the opportunity for servitude. You await their decision...</span>")
|
||||
var/alert_text = "You are being tortured! Do you want to give in and pledge your undying loyalty to [user]?"
|
||||
/* if(HAS_TRAIT(target, TRAIT_MINDSHIELD))
|
||||
alert_text += "\n\nYou will no longer be loyal to the station!"
|
||||
if(SSticker.mode.AmValidAntag(target.mind)) */
|
||||
alert_text += "\n\nYou will not lose your current objectives, but they come second to the will of your new master!"
|
||||
switch(alert(target, alert_text,"THE HORRIBLE PAIN! WHEN WILL IT END?!","Yes, Master!", "NEVER!"))
|
||||
if("Yes, Master!")
|
||||
disloyalty_accept(target)
|
||||
else
|
||||
disloyalty_refuse(target)
|
||||
if(!do_torture(user,target, 2))
|
||||
return FALSE
|
||||
|
||||
// NOTE: We only remove loyalties when we're CONVERTED!
|
||||
return TRUE
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/proc/RequireDisloyalty(mob/living/target)
|
||||
return SSticker.mode.AmValidAntag(target.mind) //|| HAS_TRAIT(target, TRAIT_MINDSHIELD)
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/proc/disloyalty_accept(mob/living/target)
|
||||
// FAILSAFE: Still on the rack?
|
||||
if(!(locate(target) in buckled_mobs))
|
||||
return
|
||||
// NOTE: You can say YES after torture. It'll apply to next time.
|
||||
disloyalty_confirm = TRUE
|
||||
/*if(HAS_TRAIT(target, TRAIT_MINDSHIELD))
|
||||
to_chat(target, "<span class='boldnotice'>You give in to the will of your torturer. If they are successful, you will no longer be loyal to the station!</span>")
|
||||
*/
|
||||
/obj/structure/bloodsucker/vassalrack/proc/disloyalty_refuse(mob/living/target)
|
||||
// FAILSAFE: Still on the rack?
|
||||
if(!(locate(target) in buckled_mobs))
|
||||
return
|
||||
// Failsafe: You already said YES.
|
||||
if(disloyalty_confirm)
|
||||
return
|
||||
to_chat(target, "<span class='notice'>You refuse to give in! You <i>will not</i> break!</span>")
|
||||
|
||||
|
||||
/obj/structure/bloodsucker/vassalrack/proc/remove_loyalties(mob/living/target)
|
||||
// Find Mind Implant & Destroy
|
||||
if(HAS_TRAIT(target, TRAIT_MINDSHIELD))
|
||||
for(var/obj/item/implant/I in target.implants)
|
||||
if(I.type == /obj/item/implant/mindshield)
|
||||
I.removed(target,silent=TRUE)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/obj/structure/bloodsucker/candelabrum
|
||||
name = "candelabrum"
|
||||
desc = "It burns slowly, but doesn't radiate any heat."
|
||||
icon = 'icons/obj/vamp_obj.dmi'
|
||||
icon_state = "candelabrum"
|
||||
light_color = "#66FFFF"//LIGHT_COLOR_BLUEGREEN // lighting.dm
|
||||
light_power = 3
|
||||
light_range = 0 // to 2
|
||||
density = FALSE
|
||||
anchored = FALSE
|
||||
var/lit = FALSE
|
||||
///obj/structure/bloodsucker/candelabrum/is_hot() // candle.dm
|
||||
//return FALSE
|
||||
|
||||
/obj/structure/bloodsucker/candelabrum/Destroy()
|
||||
STOP_PROCESSING(SSobj, src)
|
||||
|
||||
/obj/structure/bloodsucker/candelabrum/update_icon()
|
||||
icon_state = "candelabrum[lit ? "_lit" : ""]"
|
||||
|
||||
/obj/structure/bloodsucker/candelabrum/examine(mob/user)
|
||||
. = ..()
|
||||
if((isvamp()) || isobserver(user))
|
||||
. += {"<span class='cult'>This is a magical candle which drains at the sanity of mortals who are not under your command while it is active.</span>"}
|
||||
. += {"<span class='cult'>You can alt click on it from any range to turn it on remotely, or simply be next to it and click on it to turn it on and off normally.</span>"}
|
||||
/* if(user.mind.has_antag_datum(ANTAG_DATUM_VASSAL)
|
||||
. += {"<span class='cult'>This is a magical candle which drains at the sanity of the fools who havent yet accepted your master, as long as it is active.\n
|
||||
You can turn it on and off by clicking on it while you are next to it</span>"} */
|
||||
|
||||
/obj/structure/bloodsucker/candelabrum/attack_hand(mob/user)
|
||||
var/datum/antagonist/vassal/T = user.mind.has_antag_datum(ANTAG_DATUM_VASSAL)
|
||||
if(isvamp(user) || istype(T))
|
||||
toggle()
|
||||
|
||||
/obj/structure/bloodsucker/candelabrum/AltClick(mob/user)
|
||||
// Bloodsuckers can turn their candles on from a distance. SPOOOOKY.
|
||||
if(isvamp(user))
|
||||
toggle()
|
||||
|
||||
/obj/structure/bloodsucker/candelabrum/proc/toggle(mob/user)
|
||||
lit = !lit
|
||||
if(lit)
|
||||
set_light(2, 3, "#66FFFF")
|
||||
START_PROCESSING(SSobj, src)
|
||||
else
|
||||
set_light(0)
|
||||
STOP_PROCESSING(SSobj, src)
|
||||
update_icon()
|
||||
|
||||
/obj/structure/bloodsucker/candelabrum/process()
|
||||
if(lit)
|
||||
for(var/mob/living/carbon/human/H in viewers(7, src))
|
||||
var/datum/antagonist/vassal/T = H.mind.has_antag_datum(ANTAG_DATUM_VASSAL)
|
||||
if(isvamp(H) || T) //We dont want vassals or vampires affected by this
|
||||
return
|
||||
H.hallucination = 20
|
||||
SEND_SIGNAL(H, COMSIG_ADD_MOOD_EVENT, "vampcandle", /datum/mood_event/vampcandle)
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// OTHER THINGS TO USE: HUMAN BLOOD. /obj/effect/decal/cleanable/blood
|
||||
|
||||
/obj/item/restraints/legcuffs/beartrap/bloodsucker
|
||||
@@ -0,0 +1,88 @@
|
||||
|
||||
|
||||
// Created by claiming a Coffin.
|
||||
|
||||
|
||||
|
||||
// THINGS TO SPAWN:
|
||||
//
|
||||
// /obj/effect/decal/cleanable/cobweb && /obj/effect/decal/cleanable/cobweb/cobweb2
|
||||
// /obj/effect/decal/cleanable/generic
|
||||
// /obj/effect/decal/cleanable/dirt/dust <-- Pretty cool, just stains the tile itself.
|
||||
// /obj/effect/decal/cleanable/blood/old
|
||||
|
||||
/*
|
||||
/area/
|
||||
// All coffins assigned to this area
|
||||
var/list/obj/structure/closet/crate/laircoffins = new list()
|
||||
|
||||
// Called by Coffin when an area is claimed as a vamp's lair
|
||||
/area/proc/ClaimAsLair(/obj/structure/closet/crate/inClaimant)
|
||||
set waitfor = FALSE // Don't make on_gain() wait for this function to finish. This lets this code run on the side.
|
||||
|
||||
laircoffins += laircoffins
|
||||
sleep()
|
||||
|
||||
// Cancel!
|
||||
if (laircoffins.len == 0)
|
||||
return
|
||||
*/
|
||||
|
||||
/datum/antagonist/bloodsucker/proc/RunLair()
|
||||
set waitfor = FALSE // Don't make on_gain() wait for this function to finish. This lets this code run on the side.
|
||||
while(!AmFinalDeath() && coffin && lair)
|
||||
// WAit 2 min and Repeat
|
||||
sleep(120)
|
||||
// Coffin Moved SOMEHOW?
|
||||
if(lair != get_area(coffin))
|
||||
if(coffin)
|
||||
coffin.UnclaimCoffin()
|
||||
//lair = get_area(coffin)
|
||||
break // DONE
|
||||
var/list/turf/area_turfs = get_area_turfs(lair)
|
||||
// Create Dirt etc.
|
||||
var/turf/T_Dirty = pick(area_turfs)
|
||||
if(T_Dirty && !T_Dirty.density)
|
||||
// Default: Dirt
|
||||
// CHECK: Cobweb already there?
|
||||
//if (!locate(var/obj/effect/decal/cleanable/cobweb) in T_Dirty) // REMOVED! Cleanables don't stack.
|
||||
// STEP ONE: COBWEBS
|
||||
// CHECK: Wall to North?
|
||||
var/turf/check_N = get_step(T_Dirty, NORTH)
|
||||
if(istype(check_N, /turf/closed/wall))
|
||||
// CHECK: Wall to West?
|
||||
var/turf/check_W = get_step(T_Dirty, WEST)
|
||||
if(istype(check_W, /turf/closed/wall))
|
||||
new /obj/effect/decal/cleanable/cobweb (T_Dirty)
|
||||
// CHECK: Wall to East?
|
||||
var/turf/check_E = get_step(T_Dirty, EAST)
|
||||
if(istype(check_E, /turf/closed/wall))
|
||||
new /obj/effect/decal/cleanable/cobweb/cobweb2 (T_Dirty)
|
||||
// STEP TWO: DIRT
|
||||
new /obj/effect/decal/cleanable/dirt (T_Dirty)
|
||||
// Find Animals in Area
|
||||
/* if(rand(0,2) == 0)
|
||||
var/mobCount = 0
|
||||
var/mobMax = CLAMP(area_turfs.len / 25, 1, 4)
|
||||
for (var/turf/T in area_turfs)
|
||||
if(!T) continue
|
||||
var/mob/living/simple_animal/SA = locate() in T
|
||||
if(SA)
|
||||
mobCount ++
|
||||
if (mobCount >= mobMax) // Already at max
|
||||
break
|
||||
Spawn One
|
||||
if(mobCount < mobMax)
|
||||
Seek Out Location
|
||||
while(area_turfs.len > 0)
|
||||
var/turf/T = pick(area_turfs) // We use while&pick instead of a for/loop so it's random, rather than from the top of the list.
|
||||
if(T && !T.density)
|
||||
var/mob/living/simple_animal/SA = /mob/living/simple_animal/mouse // pick(/mob/living/simple_animal/mouse,/mob/living/simple_animal/mouse,/mob/living/simple_animal/mouse, /mob/living/simple_animal/hostile/retaliate/bat) //prob(300) /mob/living/simple_animal/mouse,
|
||||
new SA (T)
|
||||
break
|
||||
area_turfs -= T*/
|
||||
// NOTE: area_turfs is now cleared out!
|
||||
if(coffin)
|
||||
coffin.UnclaimCoffin()
|
||||
// Done (somehow)
|
||||
lair = null
|
||||
@@ -0,0 +1,174 @@
|
||||
|
||||
|
||||
/datum/action/bloodsucker/targeted/brawn
|
||||
name = "Brawn"//"Cellular Emporium"
|
||||
desc = "Snap restraints with ease, or deal terrible damage with your bare hands."
|
||||
button_icon_state = "power_strength"
|
||||
bloodcost = 10
|
||||
cooldown = 130
|
||||
target_range = 1
|
||||
power_activates_immediately = TRUE
|
||||
message_Trigger = ""//"Whom will you subvert to your will?"
|
||||
must_be_capacitated = TRUE
|
||||
can_be_immobilized = TRUE
|
||||
bloodsucker_can_buy = TRUE
|
||||
// Level Up
|
||||
var/upgrade_canLocker = FALSE
|
||||
var/upgrade_canDoor = FALSE
|
||||
|
||||
/datum/action/bloodsucker/targeted/brawn/CheckCanUse(display_error)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
. = TRUE
|
||||
// Break Out of Restraints! (And then cancel)
|
||||
if(CheckBreakRestraints())
|
||||
//PowerActivatedSuccessfully() // PAY COST! BEGIN COOLDOWN!DEACTIVATE!
|
||||
. = FALSE //return FALSE
|
||||
// Throw Off Attacker! (And then cancel)
|
||||
if(CheckEscapePuller())
|
||||
//PowerActivatedSuccessfully() // PAY COST! BEGIN COOLDOWN!DEACTIVATE!
|
||||
. = FALSE //return FALSE
|
||||
/*if(CheckBreakLocker())
|
||||
.= FALSE */
|
||||
// Did we successfuly use power to BREAK CUFFS and/or ESCAPE PULLER and/or escape from a locker?
|
||||
// Then PAY COST!
|
||||
if(. == FALSE)
|
||||
PowerActivatedSuccessfully() // PAY COST! BEGIN COOLDOWN!DEACTIVATE!
|
||||
|
||||
// NOTE: We use . = FALSE so that we can break cuffs AND throw off our attacker in one use!
|
||||
//return TRUE
|
||||
/datum/action/bloodsucker/targeted/brawn/CheckValidTarget(atom/A)
|
||||
return isliving(A) || istype(A, /obj/machinery/door)
|
||||
|
||||
/datum/action/bloodsucker/targeted/brawn/CheckCanTarget(atom/A, display_error)
|
||||
// DEFAULT CHECKS (Distance)
|
||||
if(!..()) // Disable range notice for Brawn.
|
||||
return FALSE
|
||||
// Must outside Closet to target anyone!
|
||||
if(!isturf(owner.loc))
|
||||
return FALSE
|
||||
// Check: Self
|
||||
if(A == owner)
|
||||
return FALSE
|
||||
// Target Type: Living
|
||||
if(isliving(A))
|
||||
return TRUE
|
||||
// Target Type: Door
|
||||
else if(istype(A, /obj/machinery/door))
|
||||
return TRUE
|
||||
return ..() // yes, FALSE! You failed if you got here! BAD TARGET
|
||||
|
||||
/datum/action/bloodsucker/targeted/brawn/FireTargetedPower(atom/A)
|
||||
// set waitfor = FALSE <---- DONT DO THIS!We WANT this power to hold up ClickWithPower(), so that we can unlock the power when it's done.
|
||||
var/mob/living/carbon/target = A
|
||||
var/mob/living/user = owner
|
||||
// Target Type: Mob
|
||||
if(isliving(target))
|
||||
var/mob/living/carbon/user_C = user
|
||||
var/hitStrength = user_C.dna.species.punchdamagehigh * 1.3 + 5
|
||||
// Knockdown!
|
||||
var/powerlevel = min(5, 1 + level_current)
|
||||
if(rand(5 + powerlevel) >= 5)
|
||||
target.visible_message("<span class='danger'>[user] lands a vicious punch, sending [target] away!</span>", \
|
||||
"<span class='userdanger'>[user] has landed a horrifying punch on you, sending you flying!!</span>", null, COMBAT_MESSAGE_RANGE)
|
||||
target.Knockdown(min(5, rand(10, 10 * powerlevel)) )
|
||||
// Chance of KO
|
||||
if(rand(6 + powerlevel) >= 6 && target.stat <= UNCONSCIOUS)
|
||||
target.Unconscious(40)
|
||||
// Attack!
|
||||
playsound(get_turf(target), 'sound/weapons/punch4.ogg', 60, 1, -1)
|
||||
user.do_attack_animation(target, ATTACK_EFFECT_SMASH)
|
||||
var/obj/item/bodypart/affecting = target.get_bodypart(ran_zone(target.zone_selected))
|
||||
target.apply_damage(hitStrength, BRUTE, affecting)
|
||||
// Knockback
|
||||
var/send_dir = get_dir(owner, target)
|
||||
var/turf/T = get_ranged_target_turf(target, send_dir, powerlevel)
|
||||
owner.newtonian_move(send_dir) // Bounce back in 0 G
|
||||
target.throw_at(T, powerlevel, TRUE, owner) //new /datum/forced_movement(target, get_ranged_target_turf(target, send_dir, (hitStrength / 4)), 1, FALSE)
|
||||
// Target Type: Door
|
||||
else if(istype(target, /obj/machinery/door))
|
||||
var/obj/machinery/door/D = target
|
||||
playsound(get_turf(usr), 'sound/machines/airlock_alien_prying.ogg', 40, 1, -1)
|
||||
to_chat(user, "<span class='notice'>You prepare to tear open [D].</span>")
|
||||
if(do_mob(usr,target,25))
|
||||
if (D.Adjacent(user))
|
||||
to_chat(user, "<span class='notice'>You tear open the [D].</span>")
|
||||
user.Stun(10)
|
||||
user.do_attack_animation(D, ATTACK_EFFECT_SMASH)
|
||||
playsound(get_turf(D), 'sound/effects/bang.ogg', 30, 1, -1)
|
||||
D.open(2) // open(2) is like a crowbar or jaws of life.
|
||||
// Target Type: Closet
|
||||
|
||||
/datum/action/bloodsucker/targeted/brawn/proc/CheckBreakRestraints()
|
||||
if(!iscarbon(owner)) // || !owner.restrained()
|
||||
return FALSE
|
||||
// (NOTE: Just like biodegrade.dm, we only remove one thing per use //
|
||||
// Destroy Cuffs
|
||||
var/mob/living/carbon/user_C = owner
|
||||
//message_admins("DEBUG3: attempt_cast() [name] / [user_C.handcuffed] ")
|
||||
if(user_C.handcuffed)
|
||||
var/obj/O = user_C.get_item_by_slot(SLOT_HANDCUFFED)
|
||||
if(istype(O))
|
||||
//user_C.visible_message("<span class='warning'>[user_C] attempts to remove [O]!</span>", \
|
||||
// "<span class='warning'>You snap [O] like it's nothing!</span>")
|
||||
user_C.clear_cuffs(O,TRUE)
|
||||
playsound(get_turf(usr), 'sound/effects/grillehit.ogg', 80, 1, -1)
|
||||
return TRUE
|
||||
/* Doesnt work
|
||||
// Destroy Straightjacket
|
||||
if(ishuman(owner))
|
||||
var/mob/living/carbon/human/user_H = owner
|
||||
if(user_H.wear_suit && user_H.wear_suit.breakouttime)
|
||||
var/obj/item/clothing/suit/straight_jacket/S = user_H.get_item_by_slot(ITEM_SLOT_ICLOTHING)
|
||||
if(istype(S))
|
||||
user_C.visible_message("<span class='warning'>[user_C] attempts to remove [S]!</span>", \
|
||||
"<span class='warning'>You rip through [S] like it's nothing!</span>")
|
||||
user_C.clear_cuffs(S,TRUE)
|
||||
playsound(get_turf(usr), 'sound/effects/grillehit.ogg', 80, 1, -1)
|
||||
return TRUE */
|
||||
// Destroy Leg Cuffs
|
||||
if(user_C.legcuffed)
|
||||
var/obj/O = user_C.get_item_by_slot(SLOT_LEGCUFFED)
|
||||
if(istype(O))
|
||||
//user_C.visible_message("<span class='warning'>[user_C] attempts to remove [O]!</span>", \
|
||||
// "<span class='warning'>You snap [O] like it's nothing!</span>")
|
||||
user_C.clear_cuffs(O,TRUE)
|
||||
playsound(get_turf(usr), 'sound/effects/grillehit.ogg', 80, 1, -1)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/action/bloodsucker/targeted/brawn/proc/CheckEscapePuller()
|
||||
if(!owner.pulledby)// || owner.pulledby.grab_state <= GRAB_PASSIVE)
|
||||
return FALSE
|
||||
var/mob/M = owner.pulledby
|
||||
var/pull_power = M.grab_state
|
||||
playsound(get_turf(M), 'sound/effects/woodhit.ogg', 75, 1, -1)
|
||||
// Knock Down (if Living)
|
||||
if (isliving(M))
|
||||
var/mob/living/L = M
|
||||
L.Knockdown(pull_power * 10 + 20)
|
||||
// Knock Back (before Knockdown, which probably cancels pull)
|
||||
var/send_dir = get_dir(owner, M)
|
||||
var/turf/T = get_ranged_target_turf(M, send_dir, pull_power)
|
||||
owner.newtonian_move(send_dir) // Bounce back in 0 G
|
||||
M.throw_at(T, pull_power, TRUE, owner, FALSE) // Throw distance based on grab state! Harder grabs punished more aggressively.
|
||||
// /proc/log_combat(atom/user, atom/target, what_done, atom/object=null, addition=null)
|
||||
log_combat(owner, M, "used Brawn power")
|
||||
owner.visible_message("<span class='warning'>[owner] tears free of [M]'s grasp!</span>", \
|
||||
"<span class='warning'>You shrug off [M]'s grasp!</span>")
|
||||
owner.pulledby = null // It's already done, but JUST IN CASE.
|
||||
return TRUE
|
||||
/* Doesnt work
|
||||
/datum/action/bloodsucker/targeted/brawn/proc/CheckBreakLocker()
|
||||
if(!istype(owner.loc, /obj/structure/closet))
|
||||
return FALSE
|
||||
playsound(get_turf(owner), 'sound/machines/airlock_alien_prying.ogg', 40, 1, -1)
|
||||
if(do_mob(owner ,target, 25))
|
||||
var/obj/structure/closet/C = owner.loc
|
||||
to_chat(owner, "<span class='notice'>You prepare to tear open the [C].</span>")
|
||||
owner.do_attack_animation(C, ATTACK_EFFECT_SMASH)
|
||||
playsound(get_turf(C), 'sound/effects/bang.ogg', 30, 1, -1)
|
||||
C.bust_open()
|
||||
return TRUE
|
||||
*/
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
|
||||
/datum/action/bloodsucker/cloak
|
||||
name = "Cloak of Darkness"
|
||||
desc = "Blend into the shadows and become invisible to the untrained eye."
|
||||
button_icon_state = "power_cloak"
|
||||
bloodcost = 5
|
||||
cooldown = 50
|
||||
bloodsucker_can_buy = TRUE
|
||||
amToggle = TRUE
|
||||
warn_constant_cost = TRUE
|
||||
var/was_running
|
||||
|
||||
var/light_min = 0.2 // If lum is above this, no good.
|
||||
|
||||
/datum/action/bloodsucker/cloak/CheckCanUse(display_error)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
// Must be Dark
|
||||
var/turf/T = owner.loc
|
||||
if(istype(T) && T.get_lumcount() > light_min)
|
||||
to_chat(owner, "<span class='warning'>This area is not dark enough to blend in</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/cloak/ActivatePower()
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
var/mob/living/user = owner
|
||||
was_running = (user.m_intent == MOVE_INTENT_RUN)
|
||||
if(was_running)
|
||||
user.toggle_move_intent()
|
||||
ADD_TRAIT(user, TRAIT_NORUNNING, "cloak of darkness")
|
||||
while(bloodsuckerdatum && ContinueActive(user) || user.m_intent == MOVE_INTENT_RUN)
|
||||
// Pay Blood Toll (if awake)
|
||||
owner.alpha = max(20, owner.alpha - min(75, 10 + 5 * level_current))
|
||||
bloodsuckerdatum.AddBloodVolume(-0.2)
|
||||
sleep(5) // Check every few ticks that we haven't disabled this power
|
||||
// Return to Running (if you were before)
|
||||
|
||||
/datum/action/bloodsucker/cloak/ContinueActive(mob/living/user, mob/living/target)
|
||||
if (!..())
|
||||
return FALSE
|
||||
if(user.stat == !CONSCIOUS) // Must be CONSCIOUS
|
||||
to_chat(owner, "<span class='warning'>Your cloak failed due to you falling unconcious! </span>")
|
||||
return FALSE
|
||||
var/turf/T = owner.loc // Must be DARK
|
||||
if(istype(T) && T.get_lumcount() > light_min)
|
||||
to_chat(owner, "<span class='warning'>Your cloak failed due to there being too much light!</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/cloak/DeactivatePower(mob/living/user = owner, mob/living/target)
|
||||
..()
|
||||
REMOVE_TRAIT(user, TRAIT_NORUNNING, "cloak of darkness")
|
||||
user.alpha = 255
|
||||
if(was_running && user.m_intent != MOVE_INTENT_RUN)
|
||||
user.toggle_move_intent()
|
||||
@@ -0,0 +1,324 @@
|
||||
|
||||
|
||||
/datum/action/bloodsucker/feed
|
||||
name = "Feed"
|
||||
desc = "Draw the heartsblood of living victims in your grasp.<br><b>None/Passive:</b> Feed silently and unnoticed by your victim.<br><b>Aggressive: </b>Subdue your target quickly."
|
||||
button_icon_state = "power_feed"
|
||||
|
||||
bloodcost = 0
|
||||
cooldown = 30
|
||||
amToggle = TRUE
|
||||
bloodsucker_can_buy = TRUE
|
||||
can_be_staked = TRUE
|
||||
cooldown_static = TRUE
|
||||
|
||||
var/notice_range = 2 // Distance before silent feeding is noticed.
|
||||
var/mob/living/feed_target // So we can validate more than just the guy we're grappling.
|
||||
var/target_grappled = FALSE // If you started grappled, then ending it will end your Feed.
|
||||
|
||||
/datum/action/bloodsucker/feed/CheckCanUse(display_error)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
// Wearing mask
|
||||
var/mob/living/L = owner
|
||||
if (L.is_mouth_covered())
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>You cannot feed with your mouth covered! Remove your mask.</span>")
|
||||
return FALSE
|
||||
// Find my Target!
|
||||
if (!FindMyTarget(display_error)) // Sets feed_target within after Validating
|
||||
return FALSE
|
||||
// Not in correct state
|
||||
// DONE!
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/feed/proc/ValidateTarget(mob/living/target, display_error) // Called twice: validating a subtle victim, or validating your grapple victim.
|
||||
// Bloodsuckers + Animals MUST be grabbed aggressively!
|
||||
if (!owner.pulling || target == owner.pulling && owner.grab_state < GRAB_AGGRESSIVE)
|
||||
// NOTE: It's OKAY that we are checking if(!target) below, AFTER animals here. We want passive check vs animal to warn you first, THEN the standard warning.
|
||||
// Animals:
|
||||
if (isliving(target) && !iscarbon(target))
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>Lesser beings require a tighter grip.</span>")
|
||||
return FALSE
|
||||
// Bloodsuckers:
|
||||
else if (iscarbon(target) && target.mind && target.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER))
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>Other Bloodsuckers will not fall for your subtle approach.</span>")
|
||||
return FALSE
|
||||
// Must have Target
|
||||
if (!target) // || !ismob(target)
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>You must be next to or grabbing a victim to feed from them.</span>")
|
||||
return FALSE
|
||||
// Not even living!
|
||||
if (!isliving(target) || issilicon(target))
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>You may only feed from living beings.</span>")
|
||||
return FALSE
|
||||
if (target.blood_volume <= 0)
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>Your victim has no blood to take.</span>")
|
||||
return FALSE
|
||||
if (ishuman(target))
|
||||
var/mob/living/carbon/human/H = target
|
||||
if(NOBLOOD in H.dna.species.species_traits)// || owner.get_blood_id() != target.get_blood_id())
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>Your victim's blood is not suitable for you to take.</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
// If I'm not grabbing someone, find me someone nearby.
|
||||
/datum/action/bloodsucker/feed/proc/FindMyTarget(display_error)
|
||||
// Default
|
||||
feed_target = null
|
||||
target_grappled = FALSE
|
||||
// If you are pulling a mob, that's your target. If you don't like it, then release them.
|
||||
if (owner.pulling && ismob(owner.pulling))
|
||||
// Check grapple target Valid
|
||||
if (!ValidateTarget(owner.pulling, display_error)) // Grabbed targets display error.
|
||||
return FALSE
|
||||
target_grappled = TRUE
|
||||
feed_target = owner.pulling
|
||||
return TRUE
|
||||
// Find Targets
|
||||
var/list/mob/living/seen_targets = view(1, owner)
|
||||
var/list/mob/living/seen_mobs = list()
|
||||
for(var/mob/living/M in seen_targets)
|
||||
if (isliving(M) && M != owner)
|
||||
seen_mobs += M
|
||||
// None Seen!
|
||||
if (seen_mobs.len == 0)
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>You must be next to or grabbing a victim to feed from them.</span>")
|
||||
return FALSE
|
||||
// Check Valids...
|
||||
var/list/targets_valid = list()
|
||||
var/list/targets_dead = list()
|
||||
for(var/mob/living/M in seen_mobs)
|
||||
// Check adjecent Valid target
|
||||
if (M != owner && ValidateTarget(M, display_error = FALSE)) // Do NOT display errors. We'll be doing this again in CheckCanUse(), which will rule out grabbed targets.
|
||||
// Prioritize living, but remember dead as backup
|
||||
if (M.stat < DEAD)
|
||||
targets_valid += M
|
||||
else
|
||||
targets_dead += M
|
||||
// No Living? Try dead.
|
||||
if (targets_valid.len == 0 && targets_dead.len > 0)
|
||||
targets_valid = targets_dead
|
||||
// No Targets
|
||||
if (targets_valid.len == 0)
|
||||
// Did I see targets? Then display at least one error
|
||||
if (seen_mobs.len > 1)
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>None of these are valid targets to feed from subtly.</span>")
|
||||
else
|
||||
ValidateTarget(seen_mobs[1], display_error)
|
||||
return FALSE
|
||||
// Too Many Targets
|
||||
//else if (targets.len > 1)
|
||||
// if (display_error)
|
||||
// to_chat(owner, "<span class='warning'>You are adjecent to too many witnesses. Either grab your victim or move away.</span>")
|
||||
// return FALSE
|
||||
// One Target!
|
||||
else
|
||||
feed_target = pick(targets_valid)//targets[1]
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/feed/ActivatePower()
|
||||
// set waitfor = FALSE <---- DONT DO THIS!We WANT this power to hold up Activate(), so Deactivate() can happen after.
|
||||
var/mob/living/target = feed_target // Stored during CheckCanUse(). Can be a grabbed OR adjecent character.
|
||||
var/mob/living/user = owner
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
// Am I SECRET or LOUD? It stays this way the whole time! I must END IT to try it the other way.
|
||||
var/amSilent = (!target_grappled || owner.grab_state <= GRAB_PASSIVE) // && iscarbon(target) // Non-carbons (animals) not passive. They go straight into aggressive.
|
||||
// Initial Wait
|
||||
var/feed_time = (amSilent ? 45 : 25) - (2.5 * level_current)
|
||||
feed_time = max(15, feed_time)
|
||||
if (amSilent)
|
||||
to_chat(user, "<span class='notice'>You lean quietly toward [target] and secretly draw out your fangs...</span>")
|
||||
else
|
||||
to_chat(user, "<span class='warning'>You pull [target] close to you and draw out your fangs...</span>")
|
||||
if (!do_mob(user, target, feed_time,0,1,extra_checks=CALLBACK(src, .proc/ContinueActive, user, target)))//sleep(10)
|
||||
to_chat(user, "<span class='warning'>Your feeding was interrupted.</span>")
|
||||
//DeactivatePower(user,target)
|
||||
return
|
||||
// Put target to Sleep (Bloodsuckers are immune to their own bite's sleep effect)
|
||||
if (!amSilent)
|
||||
ApplyVictimEffects(target) // Sleep, paralysis, immobile, unconscious, and mute
|
||||
if(target.stat <= UNCONSCIOUS)
|
||||
sleep(1)
|
||||
// Wait, then Cancel if Invalid
|
||||
if (!ContinueActive(user,target)) // Cancel. They're gone.
|
||||
//DeactivatePower(user,target)
|
||||
return
|
||||
// Pull Target Close
|
||||
if (!target.density) // Pull target to you if they don't take up space.
|
||||
target.Move(user.loc)
|
||||
// Broadcast Message
|
||||
if (amSilent)
|
||||
//if (!iscarbon(target))
|
||||
// user.visible_message("<span class='notice'>[user] shifts [target] closer to [user.p_their()] mouth.</span>", \
|
||||
// "<span class='notice'>You secretly slip your fangs into [target]'s flesh.</span>", \
|
||||
// vision_distance = 2, ignored_mobs=target) // Only people who AREN'T the target will notice this action.
|
||||
//else
|
||||
var/deadmessage = target.stat == DEAD ? "" : " <i>[target.p_they(TRUE)] looks dazed, and will not remember this.</i>"
|
||||
user.visible_message("<span class='notice'>[user] puts [target]'s wrist up to [user.p_their()] mouth.</span>", \
|
||||
"<span class='notice'>You secretly slip your fangs into [target]'s wrist.[deadmessage]</span>", \
|
||||
vision_distance = notice_range, ignored_mobs=target) // Only people who AREN'T the target will notice this action.
|
||||
// Warn Feeder about Witnesses...
|
||||
var/was_unnoticed = TRUE
|
||||
for(var/mob/living/M in viewers(notice_range, owner))
|
||||
if(M != owner && M != target && iscarbon(M) && M.mind && !M.has_unlimited_silicon_privilege && !M.eye_blind && !M.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER))
|
||||
was_unnoticed = FALSE
|
||||
break
|
||||
if (was_unnoticed)
|
||||
to_chat(user, "<span class='notice'>You think no one saw you...</span>")
|
||||
else
|
||||
to_chat(user, "<span class='warning'>Someone may have noticed...</span>")
|
||||
|
||||
else // /atom/proc/visible_message(message, self_message, blind_message, vision_distance, ignored_mobs)
|
||||
user.visible_message("<span class='warning'>[user] closes [user.p_their()] mouth around [target]'s neck!</span>", \
|
||||
"<span class='warning'>You sink your fangs into [target]'s neck.</span>")
|
||||
// My mouth is full!
|
||||
ADD_TRAIT(user, TRAIT_MUTE, "bloodsucker_feed")
|
||||
|
||||
// Begin Feed Loop
|
||||
var/warning_target_inhuman = FALSE
|
||||
var/warning_target_dead = FALSE
|
||||
var/warning_full = FALSE
|
||||
var/warning_target_bloodvol = 99999
|
||||
var/amount_taken = 0
|
||||
var/blood_take_mult = amSilent ? 0.3 : 1 // Quantity to take per tick, based on Silent or not.
|
||||
var/was_alive = target.stat < DEAD && ishuman(target)
|
||||
// Activate Effects
|
||||
//target.add_trait(TRAIT_MUTE, "bloodsucker_victim") // <----- Make mute a power you buy?
|
||||
|
||||
// FEEEEEEEEED!!! //
|
||||
bloodsuckerdatum.poweron_feed = TRUE
|
||||
while (bloodsuckerdatum && target && active)
|
||||
//user.mobility_flags &= ~MOBILITY_MOVE // user.canmove = 0 // Prevents spilling blood accidentally.
|
||||
|
||||
// Abort? A bloody mistake.
|
||||
if (!do_mob(user, target, 20, 0, 0, extra_checks=CALLBACK(src, .proc/ContinueActive, user, target)))
|
||||
// May have disabled Feed during do_mob
|
||||
if (!active || !ContinueActive(user, target))
|
||||
break
|
||||
|
||||
if (amSilent)
|
||||
to_chat(user, "<span class='warning'>Your feeding has been interrupted...but [target.p_they()] didn't seem to notice you.<span>")
|
||||
else
|
||||
to_chat(user, "<span class='warning'>Your feeding has been interrupted!</span>")
|
||||
user.visible_message("<span class='danger'>[user] is ripped from [target]'s throat. [target.p_their(TRUE)] blood sprays everywhere!</span>", \
|
||||
"<span class='userdanger'>Your teeth are ripped from [target]'s throat. [target.p_their(TRUE)] blood sprays everywhere!</span>")
|
||||
|
||||
// Deal Damage to Target (should have been more careful!)
|
||||
if (iscarbon(target))
|
||||
var/mob/living/carbon/C = target
|
||||
C.bleed(15)
|
||||
playsound(get_turf(target), 'sound/effects/splat.ogg', 40, 1)
|
||||
if (ishuman(target))
|
||||
var/mob/living/carbon/human/H = target
|
||||
H.bleed_rate += 5
|
||||
target.add_splatter_floor(get_turf(target))
|
||||
user.add_mob_blood(target) // Put target's blood on us. The donor goes in the ( )
|
||||
target.add_mob_blood(target)
|
||||
target.take_overall_damage(10,0)
|
||||
target.emote("scream")
|
||||
|
||||
// Killed Target?
|
||||
if (was_alive)
|
||||
CheckKilledTarget(user,target)
|
||||
|
||||
return
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// Handle Feeding! User & Victim Effects (per tick)
|
||||
bloodsuckerdatum.HandleFeeding(target, blood_take_mult)
|
||||
amount_taken += amSilent ? 0.3 : 1
|
||||
if (!amSilent)
|
||||
ApplyVictimEffects(target) // Sleep, paralysis, immobile, unconscious, and mute
|
||||
if (amount_taken > 5 && target.stat < DEAD && ishuman(target))
|
||||
SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "drankblood", /datum/mood_event/drankblood) // GOOD // in bloodsucker_life.dm
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// Not Human?
|
||||
if (!ishuman(target))
|
||||
SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "drankblood", /datum/mood_event/drankblood_bad) // BAD // in bloodsucker_life.dm
|
||||
if (!warning_target_inhuman)
|
||||
to_chat(user, "<span class='notice'>You recoil at the taste of a lesser lifeform.</span>")
|
||||
warning_target_inhuman = TRUE
|
||||
// Dead Blood?
|
||||
if (target.stat >= DEAD)
|
||||
if (ishuman(target))
|
||||
SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "drankblood", /datum/mood_event/drankblood_dead) // BAD // in bloodsucker_life.dm
|
||||
if (!warning_target_dead)
|
||||
to_chat(user, "<span class='notice'>Your victim is dead. [target.p_their(TRUE)] blood barely nourishes you.</span>")
|
||||
warning_target_dead = TRUE
|
||||
// Full?
|
||||
if (!warning_full && user.blood_volume >= bloodsuckerdatum.maxBloodVolume)
|
||||
to_chat(user, "<span class='notice'>You are full. Further blood will be wasted.</span>")
|
||||
warning_full = TRUE
|
||||
// Blood Remaining? (Carbons/Humans only)
|
||||
if (iscarbon(target) && !target.AmBloodsucker(1))
|
||||
if (target.blood_volume <= BLOOD_VOLUME_BAD && warning_target_bloodvol > BLOOD_VOLUME_BAD)
|
||||
to_chat(user, "<span class='warning'>Your victim's blood volume is fatally low!</span>")
|
||||
else if (target.blood_volume <= BLOOD_VOLUME_OKAY && warning_target_bloodvol > BLOOD_VOLUME_OKAY)
|
||||
to_chat(user, "<span class='warning'>Your victim's blood volume is dangerously low.</span>")
|
||||
else if (target.blood_volume <= BLOOD_VOLUME_SAFE && warning_target_bloodvol > BLOOD_VOLUME_SAFE)
|
||||
to_chat(user, "<span class='notice'>Your victim's blood is at an unsafe level.</span>")
|
||||
warning_target_bloodvol = target.blood_volume // If we had a warning to give, it's been given by now.
|
||||
// Done?
|
||||
if (target.blood_volume <= 0)
|
||||
to_chat(user, "<span class='notice'>You have bled your victim dry.</span>")
|
||||
break
|
||||
|
||||
// Blood Gulp Sound
|
||||
owner.playsound_local(null, 'sound/effects/singlebeat.ogg', 40, 1) // Play THIS sound for user only. The "null" is where turf would go if a location was needed. Null puts it right in their head.
|
||||
|
||||
// DONE!
|
||||
//DeactivatePower(user,target)
|
||||
if (amSilent)
|
||||
to_chat(user, "<span class='notice'>You slowly release [target]'s wrist." + (target.stat == 0 ? " [target.p_their(TRUE)] face lacks expression, like you've already been forgotten.</span>" : ""))
|
||||
else
|
||||
user.visible_message("<span class='warning'>[user] unclenches their teeth from [target]'s neck.</span>", \
|
||||
"<span class='warning'>You retract your fangs and release [target] from your bite.</span>")
|
||||
|
||||
// /proc/log_combat(atom/user, atom/target, what_done, atom/object=null, addition=null)
|
||||
log_combat(owner, target, "fed on blood", addition="(and took [amount_taken] blood)")
|
||||
|
||||
// Killed Target?
|
||||
if (was_alive)
|
||||
CheckKilledTarget(user,target)
|
||||
|
||||
|
||||
/datum/action/bloodsucker/feed/proc/CheckKilledTarget(mob/living/user, mob/living/target)
|
||||
// Bad Vampire. You shouldn't do that.
|
||||
if (target && target.stat >= DEAD && ishuman(target))
|
||||
SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "drankkilled", /datum/mood_event/drankkilled) // BAD // in bloodsucker_life.dm
|
||||
|
||||
/datum/action/bloodsucker/feed/ContinueActive(mob/living/user, mob/living/target)
|
||||
return ..() && target && (!target_grappled || user.pulling == target)// Active, and still Antag,
|
||||
// NOTE: We only care about pulling if target started off that way. Mostly only important for Aggressive feed.
|
||||
|
||||
/datum/action/bloodsucker/feed/proc/ApplyVictimEffects(mob/living/target)
|
||||
// Bloodsuckers not affected by "the Kiss" of another vampire
|
||||
if (!target.mind || !target.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER))
|
||||
target.Unconscious(50,0)
|
||||
target.Knockdown(40 + 5 * level_current,1)
|
||||
// NOTE: THis is based on level of power!
|
||||
if (ishuman(target))
|
||||
target.adjustStaminaLoss(5, forced = TRUE)// Base Stamina Damage
|
||||
|
||||
/datum/action/bloodsucker/feed/DeactivatePower(mob/living/user = owner, mob/living/target)
|
||||
..() // activate = FALSE
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
// No longer Feeding
|
||||
if (bloodsuckerdatum)
|
||||
bloodsuckerdatum.poweron_feed = FALSE
|
||||
feed_target = null
|
||||
// My mouth is no longer full
|
||||
REMOVE_TRAIT(owner, TRAIT_MUTE, "bloodsucker_feed")
|
||||
// Let me move immediately
|
||||
user.update_canmove()
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
|
||||
|
||||
|
||||
/datum/action/bloodsucker/fortitude
|
||||
name = "Fortitude"//"Cellular Emporium"
|
||||
desc = "Withstand egregious physical wounds and walk away from attacks that would stun, pierce, and dismember lesser beings. You cannot run while active."
|
||||
button_icon_state = "power_fortitude"
|
||||
bloodcost = 5
|
||||
cooldown = 80
|
||||
bloodsucker_can_buy = TRUE
|
||||
amToggle = TRUE
|
||||
warn_constant_cost = TRUE
|
||||
|
||||
var/this_resist // So we can raise and lower your brute resist based on what your level_current WAS.
|
||||
|
||||
/datum/action/bloodsucker/fortitude/ActivatePower()
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
var/mob/living/user = owner
|
||||
to_chat(user, "<span class='notice'>Your flesh, skin, and muscles become as steel.</span>")
|
||||
// Traits & Effects
|
||||
ADD_TRAIT(user, TRAIT_PIERCEIMMUNE, "fortitude")
|
||||
ADD_TRAIT(user, TRAIT_NODISMEMBER, "fortitude")
|
||||
ADD_TRAIT(user, TRAIT_STUNIMMUNE, "fortitude")
|
||||
ADD_TRAIT(user, TRAIT_NORUNNING, "fortitude")
|
||||
if (ishuman(owner))
|
||||
var/mob/living/carbon/human/H = owner
|
||||
this_resist = max(0.3, 0.7 - level_current * 0.1)
|
||||
H.physiology.brute_mod *= this_resist//0.5
|
||||
H.physiology.burn_mod *= this_resist//0.5
|
||||
// Stop Running (Taken from /datum/quirk/nyctophobia in negative.dm)
|
||||
var/was_running = (user.m_intent == MOVE_INTENT_RUN)
|
||||
if(was_running)
|
||||
user.toggle_move_intent()
|
||||
while(bloodsuckerdatum && ContinueActive(user) || user.m_intent == MOVE_INTENT_RUN)
|
||||
// Pay Blood Toll (if awake)
|
||||
if (user.stat == CONSCIOUS)
|
||||
bloodsuckerdatum.AddBloodVolume(-0.5) // Used to be 0.3 blood per 2 seconds, but we're making it more expensive to keep on.
|
||||
sleep(20) // Check every few ticks that we haven't disabled this power
|
||||
// Return to Running (if you were before)
|
||||
if(was_running && user.m_intent != MOVE_INTENT_RUN)
|
||||
user.toggle_move_intent()
|
||||
|
||||
/datum/action/bloodsucker/fortitude/DeactivatePower(mob/living/user = owner, mob/living/target)
|
||||
..()
|
||||
// Restore Traits & Effects
|
||||
REMOVE_TRAIT(user, TRAIT_PIERCEIMMUNE, "fortitude")
|
||||
REMOVE_TRAIT(user, TRAIT_NODISMEMBER, "fortitude")
|
||||
REMOVE_TRAIT(user, TRAIT_STUNIMMUNE, "fortitude")
|
||||
REMOVE_TRAIT(user, TRAIT_NORUNNING, "fortitude")
|
||||
if (ishuman(owner))
|
||||
var/mob/living/carbon/human/H = owner
|
||||
H.physiology.brute_mod /= this_resist//0.5
|
||||
H.physiology.burn_mod /= this_resist//0.5
|
||||
@@ -0,0 +1,115 @@
|
||||
|
||||
|
||||
/datum/action/bloodsucker/gohome
|
||||
name = "Vanishing Act"
|
||||
desc = "As dawn aproaches, disperse into mist and return directly to your Lair.<br><b>WARNING:</b> You will drop <b>ALL</b> of your possessions if observed by mortals."
|
||||
button_icon_state = "power_gohome"
|
||||
background_icon_state_on = "vamp_power_off_oneshot" // Even though this never goes off.
|
||||
background_icon_state_off = "vamp_power_off_oneshot"
|
||||
|
||||
bloodcost = 25
|
||||
cooldown = 99999 // It'll never come back.
|
||||
amToggle = FALSE
|
||||
amSingleUse = TRUE
|
||||
|
||||
bloodsucker_can_buy = FALSE // You only get this if you've claimed a lair, and only just before sunrise.
|
||||
can_use_in_torpor = TRUE
|
||||
must_be_capacitated = TRUE
|
||||
can_be_immobilized = TRUE
|
||||
|
||||
/datum/action/bloodsucker/gohome/CheckCanUse(display_error)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
// Have No Lair (NOTE: You only got this power if you had a lair, so this means it's destroyed)
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
if (!istype(bloodsuckerdatum) || !bloodsuckerdatum.coffin)
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>Your coffin has been destroyed!</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/gohome/ActivatePower()
|
||||
var/mob/living/carbon/user = owner
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
// IMPORTANT: Check for lair at every step! It might get destroyed.
|
||||
to_chat(user, "<span class='notice'>You focus on separating your consciousness from your physical form...</span>")
|
||||
// STEP ONE: Flicker Lights
|
||||
for(var/obj/machinery/light/L in view(3, get_turf(owner))) // /obj/machinery/light/proc/flicker(var/amount = rand(10, 20))
|
||||
L.flicker(5)
|
||||
playsound(get_turf(owner), 'sound/effects/singlebeat.ogg', 20, 1)
|
||||
sleep(50)
|
||||
for(var/obj/machinery/light/L in view(3, get_turf(owner))) // /obj/machinery/light/proc/flicker(var/amount = rand(10, 20))
|
||||
L.flicker(5)
|
||||
playsound(get_turf(owner), 'sound/effects/singlebeat.ogg', 40, 1)
|
||||
sleep(50)
|
||||
for(var/obj/machinery/light/L in view(6, get_turf(owner))) // /obj/machinery/light/proc/flicker(var/amount = rand(10, 20))
|
||||
L.flicker(5)
|
||||
playsound(get_turf(owner), 'sound/effects/singlebeat.ogg', 60, 1)
|
||||
// ( STEP TWO: Lights OFF? )
|
||||
// CHECK: Still have Coffin?
|
||||
if (!istype(bloodsuckerdatum) || !bloodsuckerdatum.coffin)
|
||||
to_chat(user, "<span class='warning'>Your coffin has been destroyed! You no longer have a destination.</span>")
|
||||
return FALSE
|
||||
if (!owner)
|
||||
return
|
||||
// SEEN?: (effects ONLY if there are witnesses! Otherwise you just POOF)
|
||||
// NOTE: Stolen directly from statue.dm, thanks guys!
|
||||
var/am_seen = FALSE // Do Effects (seen by anyone)
|
||||
var/drop_item = FALSE // Drop Stuff (seen by non-vamp)
|
||||
if (isturf(owner.loc)) // Only check if I'm not in a Locker or something.
|
||||
// A) Check for Darkness (we can just leave)
|
||||
var/turf/T = get_turf(user)
|
||||
if(T && T.lighting_object && T.get_lumcount()>= 0.1)
|
||||
// B) Check for Viewers
|
||||
for(var/mob/living/M in viewers(owner))
|
||||
if(M != owner && isliving(M) && M.mind && !M.has_unlimited_silicon_privilege && !M.eye_blind) // M.client <--- add this in after testing!
|
||||
am_seen = TRUE
|
||||
if (!M.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER))
|
||||
drop_item = TRUE
|
||||
break
|
||||
// LOSE CUFFS
|
||||
if(user.handcuffed)
|
||||
var/obj/O = user.handcuffed
|
||||
user.dropItemToGround(O)
|
||||
if(user.legcuffed)
|
||||
var/obj/O = user.legcuffed
|
||||
user.dropItemToGround(O)
|
||||
// SEEN!
|
||||
if (drop_item)
|
||||
// DROP: Clothes, held items, and cuffs etc
|
||||
// NOTE: Taken from unequip_everything() in inventory.dm. We need to
|
||||
// *force* all items to drop, so we had to just gut the code out of it.
|
||||
var/list/items = list()
|
||||
items |= user.get_equipped_items()
|
||||
for(var/I in items)
|
||||
user.dropItemToGround(I,TRUE)
|
||||
for(var/obj/item/I in owner.held_items) // drop_all_held_items()
|
||||
user.dropItemToGround(I, TRUE)
|
||||
if (am_seen)
|
||||
// POOF EFFECTS
|
||||
playsound(get_turf(owner), 'sound/magic/summon_karp.ogg', 60, 1)
|
||||
var/datum/effect_system/steam_spread/puff = new /datum/effect_system/steam_spread/()
|
||||
puff.effect_type = /obj/effect/particle_effect/smoke/vampsmoke
|
||||
puff.set_up(3, 0, get_turf(owner))
|
||||
puff.start()
|
||||
// TELEPORT: Move to Coffin & Close it!
|
||||
do_teleport(owner, bloodsuckerdatum.coffin, no_effects = TRUE, forced = TRUE, channel = TELEPORT_CHANNEL_QUANTUM) // in teleport.dm?
|
||||
// SLEEP
|
||||
user.resting = TRUE
|
||||
//user.Unconscious(30,0)
|
||||
user.Stun(30,1)
|
||||
// CLOSE LID: If fail, force me in.
|
||||
if (!bloodsuckerdatum.coffin.close(owner))
|
||||
bloodsuckerdatum.coffin.insert(owner) // Puts me inside.
|
||||
// The following was taken from close() proc in closets.dm
|
||||
// (but we had to do it this way because there is no way to force entry)
|
||||
playsound(bloodsuckerdatum.coffin.loc, bloodsuckerdatum.coffin.close_sound, 15, 1, -3)
|
||||
bloodsuckerdatum.coffin.opened = FALSE
|
||||
bloodsuckerdatum.coffin.density = TRUE
|
||||
bloodsuckerdatum.coffin.update_icon()
|
||||
// Lock Coffin
|
||||
bloodsuckerdatum.coffin.LockMe(owner)
|
||||
// ( STEP FIVE: Create animal at prev location? )
|
||||
//var/mob/living/simple_animal/SA = /mob/living/simple_animal/hostile/retaliate/bat // pick(/mob/living/simple_animal/mouse,/mob/living/simple_animal/mouse,/mob/living/simple_animal/mouse, /mob/living/simple_animal/hostile/retaliate/bat) //prob(300) /mob/living/simple_animal/mouse,
|
||||
//new SA (owner.loc)
|
||||
@@ -0,0 +1,85 @@
|
||||
|
||||
// Level 1: Speed to location
|
||||
// Level 2: Dodge Bullets
|
||||
// Level 3: Stun People Passed
|
||||
|
||||
/datum/action/bloodsucker/targeted/haste
|
||||
name = "Immortal Haste"
|
||||
desc = "Dash somewhere with supernatural speed. Those nearby may be knocked away, stunned, or left empty-handed."
|
||||
button_icon_state = "power_speed"
|
||||
bloodcost = 6
|
||||
cooldown = 30
|
||||
target_range = 15
|
||||
power_activates_immediately = TRUE
|
||||
message_Trigger = ""//"Whom will you subvert to your will?"
|
||||
bloodsucker_can_buy = TRUE
|
||||
must_be_capacitated = TRUE
|
||||
|
||||
/datum/action/bloodsucker/targeted/haste/CheckCanUse(display_error)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
// Being Grabbed
|
||||
if(owner.pulledby && owner.pulledby.grab_state >= GRAB_AGGRESSIVE)
|
||||
if(display_error)
|
||||
to_chat(owner, "<span class='warning'>You're being grabbed!</span>")
|
||||
return FALSE
|
||||
if(!owner.has_gravity(owner.loc)) //We dont want people to be able to use this to fly around in space
|
||||
if(display_error)
|
||||
to_chat(owner, "<span class='warning'>You cant dash while floating!</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/targeted/haste/CheckValidTarget(atom/A)
|
||||
return isturf(A) || A.loc != owner.loc // Anything will do, if it's not me or my square
|
||||
|
||||
/datum/action/bloodsucker/targeted/haste/CheckCanTarget(atom/A, display_error)
|
||||
// DEFAULT CHECKS (Distance)
|
||||
if (!..())
|
||||
return FALSE
|
||||
// Check: Range
|
||||
//if (!(A in view(target_range, get_turf(owner))))
|
||||
// return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/targeted/haste/FireTargetedPower(atom/A)
|
||||
// set waitfor = FALSE <---- DONT DO THIS!We WANT this power to hold up ClickWithPower(), so that we can unlock the power when it's done.
|
||||
var/mob/living/user = owner
|
||||
var/turf/T = isturf(A) ? A : get_turf(A)
|
||||
// Pulled? Not anymore.
|
||||
owner.pulledby = null
|
||||
// Step One: Heatseek toward Target's Turf
|
||||
walk_to(owner, T, 0, 0.01, 20) // NOTE: this runs in the background! to cancel it, you need to use walk(owner.current,0), or give them a new path.
|
||||
playsound(get_turf(owner), 'sound/weapons/punchmiss.ogg', 25, 1, -1)
|
||||
var/safety = 20
|
||||
while(get_turf(owner) != T && safety > 0 && !(isliving(target) && target.Adjacent(owner)))
|
||||
user.canmove = FALSE //Dont move while doing the thing, or itll break
|
||||
safety --
|
||||
// Did I get knocked down?
|
||||
if(owner && owner.incapacitated(ignore_restraints=TRUE, ignore_grab=TRUE))// owner.incapacitated())
|
||||
// We're gonna cancel. But am I on the ground? Spin me!
|
||||
if(user.resting)
|
||||
var/send_dir = get_dir(owner, T)
|
||||
new /datum/forced_movement(owner, get_ranged_target_turf(owner, send_dir, 1), 1, FALSE)
|
||||
owner.spin(10)
|
||||
break
|
||||
// Spin/Stun people we pass.
|
||||
//var/mob/living/newtarget = locate(/mob/living) in oview(1, owner)
|
||||
var/list/mob/living/foundtargets = list()
|
||||
for(var/mob/living/newtarget in oview(1, owner))
|
||||
if (newtarget && newtarget != target && !(newtarget in foundtargets))//!newtarget.IsKnockdown())
|
||||
if (rand(0, 5) < level_current)
|
||||
playsound(get_turf(newtarget), "sound/weapons/punch[rand(1,4)].ogg", 15, 1, -1)
|
||||
newtarget.Knockdown(10 + level_current * 5)
|
||||
if(newtarget.IsStun())
|
||||
newtarget.spin(10,1)
|
||||
if (rand(0,4))
|
||||
newtarget.drop_all_held_items()
|
||||
foundtargets += newtarget
|
||||
sleep(1)
|
||||
if(user)
|
||||
user.update_canmove() //Let the poor guy move again
|
||||
|
||||
/datum/action/bloodsucker/targeted/haste/DeactivatePower(mob/living/user = owner, mob/living/target)
|
||||
..() // activate = FALSE
|
||||
user.update_canmove()
|
||||
@@ -0,0 +1,83 @@
|
||||
// Level 1: Grapple level 2
|
||||
// Level 2: Grapple 3 from Behind
|
||||
// Level 3: Grapple 3 from Shadows
|
||||
/datum/action/bloodsucker/targeted/lunge
|
||||
name = "Predatory Lunge"
|
||||
desc = "Spring at your target and aggressively grapple them without warning. Attacks from concealment or the rear may even knock them down."
|
||||
button_icon_state = "power_lunge"
|
||||
bloodcost = 10
|
||||
cooldown = 100
|
||||
target_range = 3
|
||||
power_activates_immediately = TRUE
|
||||
message_Trigger = ""//"Whom will you subvert to your will?"
|
||||
must_be_capacitated = TRUE
|
||||
bloodsucker_can_buy = TRUE
|
||||
|
||||
/datum/action/bloodsucker/targeted/lunge/CheckCanUse(display_error)
|
||||
if(!..(display_error))// DEFAULT CHECKS
|
||||
return FALSE
|
||||
// Being Grabbed
|
||||
if(owner.pulledby && owner.pulledby.grab_state >= GRAB_AGGRESSIVE)
|
||||
if(display_error)
|
||||
to_chat(owner, "<span class='warning'>You're being grabbed!</span>")
|
||||
return FALSE
|
||||
if(!owner.has_gravity(owner.loc))//TODO figure out how to check if theyre able to move while in nograv
|
||||
if(display_error)
|
||||
to_chat(owner, "<span class='warning'>You cant lunge while floating!</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/targeted/lunge/CheckValidTarget(atom/A)
|
||||
return isliving(A)
|
||||
|
||||
/datum/action/bloodsucker/targeted/lunge/CheckCanTarget(atom/A, display_error)
|
||||
// Check: Self
|
||||
if(target == owner)
|
||||
return FALSE
|
||||
// Check: Range
|
||||
//if (!(target in view(target_range, get_turf(owner))))
|
||||
// if (display_error)
|
||||
// to_chat(owner, "<span class='warning'>Your victim is too far away.</span>")
|
||||
// return FALSE
|
||||
// DEFAULT CHECKS (Distance)
|
||||
if(!..())
|
||||
return FALSE
|
||||
// Check: Turf
|
||||
var/mob/living/L = A
|
||||
if(!isturf(L.loc))
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/targeted/lunge/FireTargetedPower(atom/A)
|
||||
// set waitfor = FALSE <---- DONT DO THIS!We WANT this power to hold up ClickWithPower(), so that we can unlock the power when it's done.
|
||||
var/mob/living/carbon/target = A
|
||||
var/turf/T = get_turf(target)
|
||||
// Clear Vars
|
||||
owner.pulling = null
|
||||
// Will we Knock them Down?
|
||||
var/do_knockdown = !is_A_facing_B(target,owner) || owner.alpha <= 0 || istype(owner.loc, /obj/structure/closet)
|
||||
// CAUSES: Target has their back to me, I'm invisible, or I'm in a Closet
|
||||
// Step One: Heatseek toward Target's Turf
|
||||
|
||||
walk_towards(owner, T, 0.1, 10) // NOTE: this runs in the background! to cancel it, you need to use walk(owner.current,0), or give them a new path.
|
||||
addtimer(CALLBACK(owner, .proc/_walk, 0), 2 SECONDS)
|
||||
if(get_turf(owner) != T && !(isliving(target) && target.Adjacent(owner)) && owner.incapacitated() && owner.resting)
|
||||
var/send_dir = get_dir(owner, T)
|
||||
new /datum/forced_movement(owner, get_ranged_target_turf(owner, send_dir, 1), 1, FALSE)
|
||||
owner.spin(10)
|
||||
// Step Two: Check if I'm at/adjectent to Target's CURRENT turf (not original...that was just a destination)
|
||||
sleep(1)
|
||||
if(target.Adjacent(owner))
|
||||
// LEVEL 2: If behind target, mute or unconscious!
|
||||
if(do_knockdown) // && level_current >= 1)
|
||||
target.Knockdown(15 + 10 * level_current,1)
|
||||
target.adjustStaminaLoss(40 + 10 * level_current)
|
||||
// Cancel Walk (we were close enough to contact them)
|
||||
walk(owner, 0)
|
||||
target.Stun(10,1) //Without this the victim can just walk away
|
||||
target.grabbedby(owner) // Taken from mutations.dm under changelings
|
||||
target.grippedby(owner, instant = TRUE) //instant aggro grab
|
||||
|
||||
/datum/action/bloodsucker/targeted/lunge/DeactivatePower(mob/living/user = owner, mob/living/target)
|
||||
..() // activate = FALSE
|
||||
user.update_canmove()
|
||||
@@ -0,0 +1,104 @@
|
||||
|
||||
|
||||
|
||||
// WITHOUT THIS POWER:
|
||||
//
|
||||
// - Mid-Blood: SHOW AS PALE
|
||||
// - Low-Blood: SHOW AS DEAD
|
||||
// - No Heartbeat
|
||||
// - Examine shows actual blood
|
||||
// - Thermal homeostasis (ColdBlooded)
|
||||
|
||||
|
||||
|
||||
// WITH THIS POWER:
|
||||
// - Normal body temp -- remove Cold Blooded (return on deactivate)
|
||||
// -
|
||||
|
||||
|
||||
/datum/action/bloodsucker/masquerade
|
||||
name = "Masquerade"
|
||||
desc = "Feign the vital signs of a mortal, and escape both casual and medical notice as the monster you truly are."
|
||||
button_icon_state = "power_human"
|
||||
bloodcost = 10
|
||||
cooldown = 50
|
||||
amToggle = TRUE
|
||||
bloodsucker_can_buy = TRUE
|
||||
warn_constant_cost = TRUE
|
||||
can_use_in_torpor = TRUE // Masquerade is maybe the only one that can do this. It stops your healing.
|
||||
cooldown_static = TRUE
|
||||
|
||||
// NOTE: Firing off vulgar powers disables your Masquerade!
|
||||
|
||||
/*/datum/action/bloodsucker/masquerade/CheckCanUse(display_error)
|
||||
if(!..(display_error))// DEFAULT CHECKS
|
||||
return FALSE
|
||||
// DONE!
|
||||
return TRUE
|
||||
*/
|
||||
|
||||
|
||||
/datum/action/bloodsucker/masquerade/ActivatePower()
|
||||
|
||||
var/mob/living/user = owner
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
|
||||
to_chat(user, "<span class='notice'>Your heart beats falsely within your lifeless chest. You may yet pass for a mortal.</span>")
|
||||
to_chat(user, "<span class='warning'>Your vampiric healing is halted while imitating life.</span>")
|
||||
|
||||
|
||||
// Remove ColdBlooded & Hard/SoftCrit
|
||||
REMOVE_TRAIT(user, TRAIT_COLDBLOODED, "bloodsucker")
|
||||
REMOVE_TRAIT(user, TRAIT_NOHARDCRIT, "bloodsucker")
|
||||
REMOVE_TRAIT(user, TRAIT_NOSOFTCRIT, "bloodsucker")
|
||||
REMOVE_TRAIT(user, TRAIT_VIRUSIMMUNE, "bloodsucker")
|
||||
var/obj/item/organ/heart/vampheart/H = user.getorganslot(ORGAN_SLOT_HEART)
|
||||
var/obj/item/organ/eyes/vassal/bloodsucker/E = user.getorganslot(ORGAN_SLOT_EYES)
|
||||
E.flash_protect = 0
|
||||
|
||||
// WE ARE ALIVE! //
|
||||
bloodsuckerdatum.poweron_masquerade = TRUE
|
||||
while(bloodsuckerdatum && ContinueActive(user))
|
||||
|
||||
// HEART
|
||||
if(istype(H))
|
||||
H.FakeStart()
|
||||
|
||||
// PASSIVE (done from LIFE)
|
||||
// Don't Show Pale/Dead on low blood
|
||||
// Don't vomit food
|
||||
// Don't Heal
|
||||
|
||||
// Pay Blood Toll (if awake)
|
||||
if(user.stat == CONSCIOUS)
|
||||
bloodsuckerdatum.AddBloodVolume(-0.2)
|
||||
|
||||
sleep(20) // Check every few ticks that we haven't disabled this power
|
||||
|
||||
|
||||
/datum/action/bloodsucker/masquerade/ContinueActive(mob/living/user)
|
||||
// Disable if unable to use power anymore.
|
||||
//if (user.stat == DEAD || user.blood_volume <= 0) // not conscious or soft critor uncon, just dead
|
||||
// return FALSE
|
||||
return ..() // Active, and still Antag
|
||||
|
||||
|
||||
/datum/action/bloodsucker/masquerade/DeactivatePower(mob/living/user = owner, mob/living/target)
|
||||
..() // activate = FALSE
|
||||
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = user.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
bloodsuckerdatum.poweron_masquerade = FALSE
|
||||
|
||||
ADD_TRAIT(user, TRAIT_COLDBLOODED, "bloodsucker")
|
||||
ADD_TRAIT(user, TRAIT_NOHARDCRIT, "bloodsucker")
|
||||
ADD_TRAIT(user, TRAIT_NOSOFTCRIT, "bloodsucker")
|
||||
ADD_TRAIT(user, TRAIT_VIRUSIMMUNE, "bloodsucker")
|
||||
|
||||
// HEART
|
||||
var/obj/item/organ/heart/H = user.getorganslot(ORGAN_SLOT_HEART)
|
||||
var/obj/item/organ/eyes/vassal/bloodsucker/E = user.getorganslot(ORGAN_SLOT_EYES)
|
||||
H.Stop()
|
||||
|
||||
E.flash_protect = 2
|
||||
|
||||
to_chat(user, "<span class='notice'>Your heart beats one final time, while your skin dries out and your icy pallor returns.</span>")
|
||||
@@ -0,0 +1,111 @@
|
||||
|
||||
// * MEZMERIZE
|
||||
// LOVE: Target falls in love with you. Being harmed directly causes them harm if they see it?
|
||||
// STAY: Target will do everything they can to stand in the same place.
|
||||
// FOLLOW: Target follows you, spouting random phrases from their history (or maybe Poly's or NPC's vocab?)
|
||||
// ATTACK: Target finds a nearby non-Bloodsucker victim to attack.
|
||||
|
||||
/datum/action/bloodsucker/targeted/mesmerize
|
||||
name = "Mesmerize"
|
||||
desc = "Dominate the mind of a mortal who can see your eyes."
|
||||
button_icon_state = "power_mez"
|
||||
bloodcost = 30
|
||||
cooldown = 200
|
||||
target_range = 1
|
||||
power_activates_immediately = FALSE
|
||||
message_Trigger = "Whom will you subvert to your will?"
|
||||
must_be_capacitated = TRUE
|
||||
bloodsucker_can_buy = TRUE
|
||||
|
||||
/datum/action/bloodsucker/targeted/mesmerize/CheckCanUse(display_error)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
if (!owner.getorganslot(ORGAN_SLOT_EYES))
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>You have no eyes with which to mesmerize.</span>")
|
||||
return FALSE
|
||||
// Check: Eyes covered?
|
||||
var/mob/living/L = owner
|
||||
if (istype(L) && L.is_eyes_covered() || !isturf(owner.loc))
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>Your eyes are concealed from sight.</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/targeted/mesmerize/CheckValidTarget(atom/A)
|
||||
return iscarbon(A)
|
||||
|
||||
/datum/action/bloodsucker/targeted/mesmerize/CheckCanTarget(atom/A,display_error)
|
||||
// Check: Self
|
||||
if (A == owner)
|
||||
return FALSE
|
||||
var/mob/living/carbon/target = A // We already know it's carbon due to CheckValidTarget()
|
||||
// Bloodsucker
|
||||
if (target.mind && target.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER))
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>Bloodsuckers are immune to [src].</span>")
|
||||
return FALSE
|
||||
// Dead/Unconscious
|
||||
if (target.stat > CONSCIOUS)
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>Your victim is not [(target.stat == DEAD || HAS_TRAIT(target, TRAIT_FAKEDEATH))?"alive":"conscious"].</span>")
|
||||
return FALSE
|
||||
// Check: Target has eyes?
|
||||
if (!target.getorganslot(ORGAN_SLOT_EYES))
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>They have no eyes!</span>")
|
||||
return FALSE
|
||||
// Check: Target blind?
|
||||
if (target.eye_blind > 0)
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>Your victim's eyes are glazed over. They cannot perceive you.</span>")
|
||||
return FALSE
|
||||
// Check: Target See Me? (behind wall)
|
||||
if (!(owner in view(target_range, get_turf(target))))
|
||||
// Sub-Check: GET CLOSER
|
||||
//if (!(owner in range(target_range, get_turf(target)))
|
||||
// if (display_error)
|
||||
// to_chat(owner, "<span class='warning'>You're too far from your victim.</span>")
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>You're too far outside your victim's view.</span>")
|
||||
return FALSE
|
||||
// Check: Facing target?
|
||||
if (!is_A_facing_B(owner,target)) // in unsorted.dm
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>You must be facing your victim.</span>")
|
||||
return FALSE
|
||||
// Check: Target facing me?
|
||||
if (!target.resting && !is_A_facing_B(target,owner))
|
||||
if (display_error)
|
||||
to_chat(owner, "<span class='warning'>Your victim must be facing you to see into your eyes.</span>")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/targeted/mesmerize/FireTargetedPower(atom/A)
|
||||
// set waitfor = FALSE <---- DONT DO THIS!We WANT this power to hold up ClickWithPower(), so that we can unlock the power when it's done.
|
||||
var/mob/living/carbon/target = A
|
||||
var/mob/living/user = owner
|
||||
|
||||
if(istype(target))
|
||||
target.Stun(40) //Utterly useless without this, its okay since there are so many checks to go through
|
||||
target.apply_status_effect(STATUS_EFFECT_MESMERIZE, 45) //So you cant rotate with combat mode, plus fancy status alert
|
||||
|
||||
if(do_mob(user, target, 40, 0, TRUE, extra_checks = CALLBACK(src, .proc/ContinueActive, user, target)))
|
||||
PowerActivatedSuccessfully() // PAY COST! BEGIN COOLDOWN!
|
||||
var/power_time = 90 + level_current * 12
|
||||
target.apply_status_effect(STATUS_EFFECT_MESMERIZE, power_time + 80)
|
||||
to_chat(user, "<span class='notice'>[target] is fixed in place by your hypnotic gaze.</span>")
|
||||
target.Stun(power_time)
|
||||
target.next_move = world.time + power_time // <--- Use direct change instead. We want an unmodified delay to their next move // target.changeNext_move(power_time) // check click.dm
|
||||
target.notransform = TRUE // <--- Fuck it. We tried using next_move, but they could STILL resist. We're just doing a hard freeze.
|
||||
spawn(power_time)
|
||||
if(istype(target))
|
||||
target.notransform = FALSE
|
||||
// They Woke Up! (Notice if within view)
|
||||
if(istype(user) && target.stat == CONSCIOUS && (target in view(10, get_turf(user))) )
|
||||
to_chat(user, "<span class='warning'>[target] has snapped out of their trance.</span>")
|
||||
|
||||
|
||||
/datum/action/bloodsucker/targeted/mesmerize/ContinueActive(mob/living/user, mob/living/target)
|
||||
return ..() && CheckCanUse() && CheckCanTarget(target)
|
||||
@@ -0,0 +1,118 @@
|
||||
|
||||
|
||||
/datum/action/bloodsucker/targeted/trespass
|
||||
name = "Trespass"
|
||||
desc = "Become mist and advance two tiles in one direction, ignoring all obstacles except for walls. Useful for skipping past doors and barricades."
|
||||
button_icon_state = "power_tres"
|
||||
|
||||
bloodcost = 10
|
||||
cooldown = 60
|
||||
amToggle = FALSE
|
||||
//target_range = 2
|
||||
|
||||
bloodsucker_can_buy = TRUE
|
||||
must_be_capacitated = FALSE
|
||||
can_be_immobilized = TRUE
|
||||
|
||||
var/turf/target_turf // We need to decide where we're going based on where we clicked. It's not actually the tile we clicked.
|
||||
|
||||
/datum/action/bloodsucker/targeted/trespass/CheckCanUse(display_error)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
if(owner.notransform || !get_turf(owner))
|
||||
return FALSE
|
||||
|
||||
return TRUE
|
||||
|
||||
|
||||
/datum/action/bloodsucker/targeted/trespass/CheckValidTarget(atom/A)
|
||||
// Can't target my tile
|
||||
if (A == get_turf(owner) || get_turf(A) == get_turf(owner))
|
||||
return FALSE
|
||||
|
||||
return TRUE // All we care about is destination. Anything you click is fine.
|
||||
|
||||
|
||||
/datum/action/bloodsucker/targeted/trespass/CheckCanTarget(atom/A, display_error)
|
||||
// NOTE: Do NOT use ..()! We don't want to check distance or anything.
|
||||
|
||||
// Get clicked tile
|
||||
var/final_turf = isturf(A) ? A : get_turf(A)
|
||||
|
||||
// Are either tiles WALLS?
|
||||
var/turf/from_turf = get_turf(owner)
|
||||
var/this_dir // = get_dir(from_turf, target_turf)
|
||||
for (var/i=1 to 2)
|
||||
// Keep Prev Direction if we've reached final turf
|
||||
if (from_turf != final_turf)
|
||||
this_dir = get_dir(from_turf, final_turf) // Recalculate dir so we don't overshoot on a diagonal.
|
||||
from_turf = get_step(from_turf, this_dir)
|
||||
// ERROR! Wall!
|
||||
if (iswallturf(from_turf))
|
||||
if (display_error)
|
||||
var/wallwarning = (i == 1) ? "in the way" : "at your destination"
|
||||
to_chat(owner, "<span class='warning'>There is a solid wall [wallwarning].</span>")
|
||||
return FALSE
|
||||
// Done
|
||||
target_turf = from_turf
|
||||
|
||||
return TRUE
|
||||
|
||||
|
||||
/datum/action/bloodsucker/targeted/trespass/FireTargetedPower(atom/A)
|
||||
// set waitfor = FALSE <---- DONT DO THIS!We WANT this power to hold up ClickWithPower(), so that we can unlock the power when it's done.
|
||||
|
||||
// Find target turf, at or below Atom
|
||||
var/mob/living/carbon/user = owner
|
||||
var/turf/my_turf = get_turf(owner)
|
||||
|
||||
user.visible_message("<span class='warning'>[user]'s form dissipates into a cloud of mist!</span>", \
|
||||
"<span class='notice'>You disspiate into formless mist.</span>")
|
||||
|
||||
|
||||
// Effect Origin
|
||||
playsound(get_turf(owner), 'sound/magic/summon_karp.ogg', 60, 1)
|
||||
var/datum/effect_system/steam_spread/puff = new /datum/effect_system/steam_spread/()
|
||||
puff.effect_type = /obj/effect/particle_effect/smoke/vampsmoke
|
||||
puff.set_up(3, 0, my_turf)
|
||||
puff.start()
|
||||
|
||||
var/mist_delay = max(5, 20 - level_current * 2.5) // Level up and do this faster.
|
||||
|
||||
// Freeze Me
|
||||
user.next_move = world.time + mist_delay
|
||||
user.Stun(mist_delay, ignore_canstun = TRUE)
|
||||
user.notransform = TRUE
|
||||
user.density = 0
|
||||
var/invis_was = user.invisibility
|
||||
user.invisibility = INVISIBILITY_MAXIMUM
|
||||
|
||||
// LOSE CUFFS
|
||||
|
||||
// Wait...
|
||||
sleep(mist_delay / 2)
|
||||
|
||||
// Move & Freeze
|
||||
if (isturf(target_turf))
|
||||
do_teleport(owner, target_turf, no_effects=TRUE, channel = TELEPORT_CHANNEL_QUANTUM) // in teleport.dm?
|
||||
user.next_move = world.time + mist_delay / 2
|
||||
user.Stun(mist_delay / 2, ignore_canstun = TRUE)
|
||||
|
||||
// Wait...
|
||||
sleep(mist_delay / 2)
|
||||
|
||||
// Un-Hide & Freeze
|
||||
user.dir = get_dir(my_turf, target_turf)
|
||||
user.next_move = world.time + mist_delay / 2
|
||||
user.Stun(mist_delay / 2, ignore_canstun = TRUE)
|
||||
user.notransform = FALSE
|
||||
user.density = 1
|
||||
user.invisibility = invis_was
|
||||
|
||||
// Effect Destination
|
||||
playsound(get_turf(owner), 'sound/magic/summon_karp.ogg', 60, 1)
|
||||
puff = new /datum/effect_system/steam_spread/()
|
||||
puff.effect_type = /obj/effect/particle_effect/smoke/vampsmoke
|
||||
puff.set_up(3, 0, target_turf)
|
||||
puff.start()
|
||||
@@ -0,0 +1,163 @@
|
||||
|
||||
/datum/action/bloodsucker/veil
|
||||
name = "Veil of Many Faces"
|
||||
desc = "Disguise yourself in the illusion of another identity."
|
||||
button_icon_state = "power_veil"
|
||||
bloodcost = 15
|
||||
cooldown = 100
|
||||
amToggle = TRUE
|
||||
bloodsucker_can_buy = TRUE
|
||||
warn_constant_cost = TRUE
|
||||
|
||||
// Outfit Vars
|
||||
var/list/original_items = list()
|
||||
|
||||
// Identity Vars
|
||||
var/prev_gender
|
||||
var/prev_skin_tone
|
||||
var/prev_hair_style
|
||||
var/prev_facial_hair_style
|
||||
var/prev_hair_color
|
||||
var/prev_facial_hair_color
|
||||
var/prev_underwear
|
||||
var/prev_undie_color
|
||||
var/prev_undershirt
|
||||
var/prev_shirt_color
|
||||
var/prev_socks
|
||||
var/prev_socks_color
|
||||
var/prev_disfigured
|
||||
var/list/prev_features // For lizards and such
|
||||
|
||||
|
||||
/datum/action/bloodsucker/veil/CheckCanUse(display_error)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
|
||||
return TRUE
|
||||
|
||||
|
||||
/datum/action/bloodsucker/veil/ActivatePower()
|
||||
|
||||
cast_effect() // POOF
|
||||
|
||||
//if (blahblahblah)
|
||||
// Disguise_Outfit()
|
||||
|
||||
Disguise_FaceName()
|
||||
|
||||
|
||||
/datum/action/bloodsucker/veil/proc/Disguise_Outfit()
|
||||
|
||||
// Step One: Back up original items
|
||||
|
||||
|
||||
|
||||
|
||||
/datum/action/bloodsucker/veil/proc/Disguise_FaceName()
|
||||
|
||||
// Change Name/Voice
|
||||
var/mob/living/carbon/human/H = owner
|
||||
H.name_override = H.dna.species.random_name(H.gender)
|
||||
H.name = H.name_override
|
||||
H.SetSpecialVoice(H.name_override)
|
||||
to_chat(owner, "<span class='warning'>You mystify the air around your person. Your identity is now altered.</span>")
|
||||
|
||||
// Store Prev Appearance
|
||||
prev_gender = H.gender
|
||||
prev_skin_tone = H.skin_tone
|
||||
prev_hair_style = H.hair_style
|
||||
prev_facial_hair_style = H.facial_hair_style
|
||||
prev_hair_color = H.hair_color
|
||||
prev_facial_hair_color = H.facial_hair_color
|
||||
prev_underwear = H.underwear
|
||||
prev_undie_color = H.undie_color
|
||||
prev_undershirt = H.undershirt
|
||||
prev_shirt_color = H.shirt_color
|
||||
prev_socks = H.socks
|
||||
prev_socks_color = H.socks_color
|
||||
//prev_eye_color
|
||||
prev_disfigured = HAS_TRAIT(H, TRAIT_DISFIGURED) // I was disfigured! //prev_disabilities = H.disabilities
|
||||
prev_features = H.dna.features
|
||||
|
||||
// Change Appearance, not randomizing clothes colour, itll just be janky
|
||||
H.gender = pick(MALE, FEMALE)
|
||||
H.skin_tone = random_skin_tone()
|
||||
H.hair_style = random_hair_style(H.gender)
|
||||
H.facial_hair_style = pick(random_facial_hair_style(H.gender),"Shaved")
|
||||
H.hair_color = random_short_color()
|
||||
H.facial_hair_color = H.hair_color
|
||||
H.underwear = random_underwear(H.gender)
|
||||
H.undershirt = random_undershirt(H.gender)
|
||||
H.socks = random_socks(H.gender)
|
||||
//H.eye_color = random_eye_color()
|
||||
REMOVE_TRAIT(H, TRAIT_DISFIGURED, null) //
|
||||
H.dna.features = random_features()
|
||||
|
||||
// Apply Appearance
|
||||
H.update_body() // Outfit and underware, also body.
|
||||
//H.update_mutant_bodyparts() // Lizard tails etc
|
||||
H.update_hair()
|
||||
H.update_body_parts()
|
||||
|
||||
// Wait here til we deactivate power or go unconscious
|
||||
var/datum/antagonist/bloodsucker/bloodsuckerdatum = owner.mind.has_antag_datum(ANTAG_DATUM_BLOODSUCKER)
|
||||
while (ContinueActive(owner) && istype(bloodsuckerdatum))//active && owner && owner.stat == CONSCIOUS)
|
||||
bloodsuckerdatum.AddBloodVolume(-0.2)
|
||||
sleep(10)
|
||||
|
||||
// Wait for a moment if you fell unconscious...
|
||||
if (owner && owner.stat > CONSCIOUS)
|
||||
sleep(50)
|
||||
|
||||
|
||||
/datum/action/bloodsucker/veil/DeactivatePower(mob/living/user = owner, mob/living/target)
|
||||
..()
|
||||
if (ishuman(user))
|
||||
var/mob/living/carbon/human/H = user
|
||||
|
||||
// Revert Identity
|
||||
H.UnsetSpecialVoice()
|
||||
H.name_override = null
|
||||
H.name = H.real_name
|
||||
|
||||
// Revert Appearance
|
||||
H.gender = prev_gender
|
||||
H.skin_tone = prev_skin_tone
|
||||
H.hair_style = prev_hair_style
|
||||
H.facial_hair_style = prev_facial_hair_style
|
||||
H.hair_color = prev_hair_color
|
||||
H.facial_hair_color = prev_facial_hair_color
|
||||
H.underwear = prev_underwear
|
||||
H.undie_color = prev_undie_color
|
||||
H.undershirt = prev_undershirt
|
||||
H.shirt_color = prev_shirt_color
|
||||
H.socks = prev_socks
|
||||
H.socks_color = prev_socks_color
|
||||
|
||||
//H.disabilities = prev_disabilities // Restore HUSK, CLUMSY, etc.
|
||||
if (prev_disfigured)
|
||||
ADD_TRAIT(H, TRAIT_DISFIGURED, "husk") // NOTE: We are ASSUMING husk. // H.status_flags |= DISFIGURED // Restore "Unknown" disfigurement
|
||||
H.dna.features = prev_features
|
||||
// Apply Appearance
|
||||
H.update_body() // Outfit and underware, also body.
|
||||
H.update_hair()
|
||||
H.update_body_parts() // Body itself, maybe skin color?
|
||||
cast_effect() // POOF
|
||||
|
||||
// CAST EFFECT // // General effect (poof, splat, etc) when you cast. Doesn't happen automatically!
|
||||
/datum/action/bloodsucker/veil/proc/cast_effect()
|
||||
// Effect
|
||||
playsound(get_turf(owner), 'sound/magic/smoke.ogg', 20, 1)
|
||||
var/datum/effect_system/steam_spread/puff = new /datum/effect_system/steam_spread/()
|
||||
puff.effect_type = /obj/effect/particle_effect/smoke/vampsmoke
|
||||
puff.set_up(3, 0, get_turf(owner))
|
||||
puff.attach(owner) // OPTIONAL
|
||||
puff.start()
|
||||
owner.spin(8, 1) // Spin around like a loon.
|
||||
|
||||
/obj/effect/particle_effect/smoke/vampsmoke
|
||||
opaque = FALSE
|
||||
lifetime = 0
|
||||
/obj/effect/particle_effect/smoke/vampsmoke/fade_out(frames = 6)
|
||||
..(frames)
|
||||
@@ -0,0 +1,38 @@
|
||||
/datum/action/bloodsucker/vassal/recuperate
|
||||
name = "Sanguine Recuperation"
|
||||
desc = "Slowly heal brute damage while active. This process is exhausting, and requires some of your tainted blood."
|
||||
button_icon_state = "power_recup"
|
||||
amToggle = TRUE
|
||||
bloodcost = 5
|
||||
cooldown = 100
|
||||
|
||||
/datum/action/bloodsucker/vassal/recuperate/CheckCanUse(display_error)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
if (owner.stat >= DEAD)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/bloodsucker/vassal/recuperate/ActivatePower()
|
||||
to_chat(owner, "<span class='notice'>Your muscles clench and your skin crawls as your master's immortal blood knits your wounds and gives you stamina.</span>")
|
||||
var/mob/living/carbon/C = owner
|
||||
var/mob/living/carbon/human/H
|
||||
if(ishuman(owner))
|
||||
H = owner
|
||||
while(ContinueActive(owner))
|
||||
C.adjustBruteLoss(-1.5)
|
||||
C.adjustFireLoss(-0.5)
|
||||
C.adjustToxLoss(-2, forced = TRUE)
|
||||
C.blood_volume -= 0.2
|
||||
C.adjustStaminaLoss(-15)
|
||||
// Stop Bleeding
|
||||
if(istype(H) && H.bleed_rate > 0 && rand(20) == 0)
|
||||
H.bleed_rate --
|
||||
C.Jitter(5)
|
||||
sleep(10)
|
||||
// DONE!
|
||||
//DeactivatePower(owner)
|
||||
|
||||
/datum/action/bloodsucker/vassal/recuperate/ContinueActive(mob/living/user, mob/living/target)
|
||||
return ..() && user.stat <= DEAD && user.blood_volume > 500
|
||||
@@ -1,84 +1,84 @@
|
||||
/*
|
||||
* Don't use the apostrophe in name or desc. Causes script errors.
|
||||
* TODO: combine atleast some of the functionality with /proc_holder/spell
|
||||
*/
|
||||
|
||||
/obj/effect/proc_holder/changeling
|
||||
panel = "Changeling"
|
||||
name = "Prototype Sting"
|
||||
desc = "" // Fluff
|
||||
var/helptext = "" // Details
|
||||
var/chemical_cost = 0 // negative chemical cost is for passive abilities (chemical glands)
|
||||
var/dna_cost = -1 //cost of the sting in dna points. 0 = auto-purchase, -1 = cannot be purchased
|
||||
var/req_dna = 0 //amount of dna needed to use this ability. Changelings always have atleast 1
|
||||
var/req_human = 0 //if you need to be human to use this ability
|
||||
var/req_absorbs = 0 //similar to req_dna, but only gained from absorbing, not DNA sting
|
||||
var/req_stat = CONSCIOUS // CONSCIOUS, UNCONSCIOUS or DEAD
|
||||
var/always_keep = 0 // important for abilities like revive that screw you if you lose them.
|
||||
var/ignores_fakedeath = FALSE // usable with the FAKEDEATH flag
|
||||
var/loudness = 0 //Determines how much having this ability will affect changeling blood tests. At 4, the blood will react violently and turn to ash, creating a unique message in the process. At 10, the blood will explode when heated.
|
||||
|
||||
|
||||
/obj/effect/proc_holder/changeling/proc/on_purchase(mob/user, is_respec)
|
||||
action.Grant(user)
|
||||
if(!is_respec)
|
||||
SSblackbox.record_feedback("tally", "changeling_power_purchase", 1, name)
|
||||
|
||||
/obj/effect/proc_holder/changeling/proc/on_refund(mob/user)
|
||||
action.Remove(user)
|
||||
return
|
||||
|
||||
/obj/effect/proc_holder/changeling/Click()
|
||||
var/mob/user = usr
|
||||
if(!user || !user.mind || !user.mind.has_antag_datum(/datum/antagonist/changeling))
|
||||
return
|
||||
try_to_sting(user)
|
||||
|
||||
/obj/effect/proc_holder/changeling/proc/try_to_sting(mob/user, mob/target)
|
||||
if(!can_sting(user, target))
|
||||
return
|
||||
var/datum/antagonist/changeling/c = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
if(sting_action(user, target))
|
||||
SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("[name]"))
|
||||
sting_feedback(user, target)
|
||||
c.chem_charges -= chemical_cost
|
||||
|
||||
/obj/effect/proc_holder/changeling/proc/sting_action(mob/user, mob/target)
|
||||
return 0
|
||||
|
||||
/obj/effect/proc_holder/changeling/proc/sting_feedback(mob/user, mob/target)
|
||||
return 0
|
||||
|
||||
//Fairly important to remember to return 1 on success >.<
|
||||
/obj/effect/proc_holder/changeling/proc/can_sting(mob/living/user, mob/target)
|
||||
if(!ishuman(user) && !ismonkey(user)) //typecast everything from mob to carbon from this point onwards
|
||||
return 0
|
||||
if(req_human && !ishuman(user))
|
||||
to_chat(user, "<span class='warning'>We cannot do that in this form!</span>")
|
||||
return 0
|
||||
var/datum/antagonist/changeling/c = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
if(c.chem_charges < chemical_cost)
|
||||
to_chat(user, "<span class='warning'>We require at least [chemical_cost] unit\s of chemicals to do that!</span>")
|
||||
return 0
|
||||
if(c.absorbedcount < req_dna)
|
||||
to_chat(user, "<span class='warning'>We require at least [req_dna] sample\s of compatible DNA.</span>")
|
||||
return 0
|
||||
if(c.trueabsorbs < req_absorbs)
|
||||
to_chat(user, "<span class='warning'>We require at least [req_absorbs] sample\s of DNA gained through our Absorb ability.</span>")
|
||||
if(req_stat < user.stat)
|
||||
to_chat(user, "<span class='warning'>We are incapacitated.</span>")
|
||||
return 0
|
||||
if((HAS_TRAIT(user, TRAIT_DEATHCOMA)) && (!ignores_fakedeath))
|
||||
to_chat(user, "<span class='warning'>We are incapacitated.</span>")
|
||||
return 0
|
||||
return 1
|
||||
|
||||
//used in /mob/Stat()
|
||||
/obj/effect/proc_holder/changeling/proc/can_be_used_by(mob/user)
|
||||
if(QDELETED(user))
|
||||
return FALSE
|
||||
if(!ishuman(user) && !ismonkey(user))
|
||||
return FALSE
|
||||
if(req_human && !ishuman(user))
|
||||
return FALSE
|
||||
return TRUE
|
||||
/*
|
||||
* Don't use the apostrophe in name or desc. Causes script errors.
|
||||
* TODO: combine atleast some of the functionality with /proc_holder/spell
|
||||
*/
|
||||
|
||||
/obj/effect/proc_holder/changeling
|
||||
panel = "Changeling"
|
||||
name = "Prototype Sting"
|
||||
desc = "" // Fluff
|
||||
var/helptext = "" // Details
|
||||
var/chemical_cost = 0 // negative chemical cost is for passive abilities (chemical glands)
|
||||
var/dna_cost = -1 //cost of the sting in dna points. 0 = auto-purchase, -1 = cannot be purchased
|
||||
var/req_dna = 0 //amount of dna needed to use this ability. Changelings always have atleast 1
|
||||
var/req_human = 0 //if you need to be human to use this ability
|
||||
var/req_absorbs = 0 //similar to req_dna, but only gained from absorbing, not DNA sting
|
||||
var/req_stat = CONSCIOUS // CONSCIOUS, UNCONSCIOUS or DEAD
|
||||
var/always_keep = 0 // important for abilities like revive that screw you if you lose them.
|
||||
var/ignores_fakedeath = FALSE // usable with the FAKEDEATH flag
|
||||
var/loudness = 0 //Determines how much having this ability will affect changeling blood tests. At 4, the blood will react violently and turn to ash, creating a unique message in the process. At 10, the blood will explode when heated.
|
||||
|
||||
|
||||
/obj/effect/proc_holder/changeling/proc/on_purchase(mob/user, is_respec)
|
||||
action.Grant(user)
|
||||
if(!is_respec)
|
||||
SSblackbox.record_feedback("tally", "changeling_power_purchase", 1, name)
|
||||
|
||||
/obj/effect/proc_holder/changeling/proc/on_refund(mob/user)
|
||||
action.Remove(user)
|
||||
return
|
||||
|
||||
/obj/effect/proc_holder/changeling/Click()
|
||||
var/mob/user = usr
|
||||
if(!user || !user.mind || !user.mind.has_antag_datum(/datum/antagonist/changeling))
|
||||
return
|
||||
try_to_sting(user)
|
||||
|
||||
/obj/effect/proc_holder/changeling/proc/try_to_sting(mob/user, mob/target)
|
||||
if(!can_sting(user, target))
|
||||
return
|
||||
var/datum/antagonist/changeling/c = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
if(sting_action(user, target))
|
||||
SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("[name]"))
|
||||
sting_feedback(user, target)
|
||||
c.chem_charges -= chemical_cost
|
||||
|
||||
/obj/effect/proc_holder/changeling/proc/sting_action(mob/user, mob/target)
|
||||
return 0
|
||||
|
||||
/obj/effect/proc_holder/changeling/proc/sting_feedback(mob/user, mob/target)
|
||||
return 0
|
||||
|
||||
//Fairly important to remember to return 1 on success >.<
|
||||
/obj/effect/proc_holder/changeling/proc/can_sting(mob/living/user, mob/target)
|
||||
if(!ishuman(user) && !ismonkey(user)) //typecast everything from mob to carbon from this point onwards
|
||||
return 0
|
||||
if(req_human && !ishuman(user))
|
||||
to_chat(user, "<span class='warning'>We cannot do that in this form!</span>")
|
||||
return 0
|
||||
var/datum/antagonist/changeling/c = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
if(c.chem_charges < chemical_cost)
|
||||
to_chat(user, "<span class='warning'>We require at least [chemical_cost] unit\s of chemicals to do that!</span>")
|
||||
return 0
|
||||
if(c.absorbedcount < req_dna)
|
||||
to_chat(user, "<span class='warning'>We require at least [req_dna] sample\s of compatible DNA.</span>")
|
||||
return 0
|
||||
if(c.trueabsorbs < req_absorbs)
|
||||
to_chat(user, "<span class='warning'>We require at least [req_absorbs] sample\s of DNA gained through our Absorb ability.</span>")
|
||||
if(req_stat < user.stat)
|
||||
to_chat(user, "<span class='warning'>We are incapacitated.</span>")
|
||||
return 0
|
||||
if((HAS_TRAIT(user, TRAIT_DEATHCOMA)) && (!ignores_fakedeath))
|
||||
to_chat(user, "<span class='warning'>We are incapacitated.</span>")
|
||||
return 0
|
||||
return 1
|
||||
|
||||
//used in /mob/Stat()
|
||||
/obj/effect/proc_holder/changeling/proc/can_be_used_by(mob/user)
|
||||
if(QDELETED(user))
|
||||
return FALSE
|
||||
if(!ishuman(user) && !ismonkey(user))
|
||||
return FALSE
|
||||
if(req_human && !ishuman(user))
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
@@ -1,117 +1,117 @@
|
||||
/obj/effect/proc_holder/changeling/absorbDNA
|
||||
name = "Absorb DNA"
|
||||
desc = "Absorb the DNA of our victim."
|
||||
chemical_cost = 0
|
||||
dna_cost = 0
|
||||
req_human = 1
|
||||
action_icon = 'icons/mob/actions/actions_changeling.dmi'
|
||||
action_icon_state = "ling_absorb_dna"
|
||||
action_background_icon_state = "bg_ling"
|
||||
|
||||
/obj/effect/proc_holder/changeling/absorbDNA/can_sting(mob/living/carbon/user)
|
||||
if(!..())
|
||||
return
|
||||
|
||||
var/datum/antagonist/changeling/changeling = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
if(changeling.isabsorbing)
|
||||
to_chat(user, "<span class='warning'>We are already absorbing!</span>")
|
||||
return
|
||||
|
||||
if(!user.pulling || !iscarbon(user.pulling))
|
||||
to_chat(user, "<span class='warning'>We must be grabbing a creature to absorb them!</span>")
|
||||
return
|
||||
if(user.grab_state <= GRAB_NECK)
|
||||
to_chat(user, "<span class='warning'>We must have a tighter grip to absorb this creature!</span>")
|
||||
return
|
||||
|
||||
var/mob/living/carbon/target = user.pulling
|
||||
return changeling.can_absorb_dna(target)
|
||||
|
||||
|
||||
|
||||
/obj/effect/proc_holder/changeling/absorbDNA/sting_action(mob/user)
|
||||
var/datum/antagonist/changeling/changeling = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
var/mob/living/carbon/human/target = user.pulling
|
||||
changeling.isabsorbing = 1
|
||||
for(var/i in 1 to 3)
|
||||
switch(i)
|
||||
if(1)
|
||||
to_chat(user, "<span class='notice'>This creature is compatible. We must hold still...</span>")
|
||||
if(2)
|
||||
user.visible_message("<span class='warning'>[user] extends a proboscis!</span>", "<span class='notice'>We extend a proboscis.</span>")
|
||||
if(3)
|
||||
user.visible_message("<span class='danger'>[user] stabs [target] with the proboscis!</span>", "<span class='notice'>We stab [target] with the proboscis.</span>")
|
||||
to_chat(target, "<span class='userdanger'>You feel a sharp stabbing pain!</span>")
|
||||
target.take_overall_damage(40)
|
||||
|
||||
SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("Absorb DNA", "[i]"))
|
||||
if(!do_mob(user, target, 150))
|
||||
to_chat(user, "<span class='warning'>Our absorption of [target] has been interrupted!</span>")
|
||||
changeling.isabsorbing = 0
|
||||
return
|
||||
|
||||
SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("Absorb DNA", "4"))
|
||||
user.visible_message("<span class='danger'>[user] sucks the fluids from [target]!</span>", "<span class='notice'>We have absorbed [target].</span>")
|
||||
to_chat(target, "<span class='userdanger'>You are absorbed by the changeling!</span>")
|
||||
|
||||
if(!changeling.has_dna(target.dna))
|
||||
changeling.add_new_profile(target)
|
||||
changeling.trueabsorbs++
|
||||
|
||||
if(user.nutrition < NUTRITION_LEVEL_WELL_FED)
|
||||
user.nutrition = min((user.nutrition + target.nutrition), NUTRITION_LEVEL_WELL_FED)
|
||||
|
||||
if(target.mind)//if the victim has got a mind
|
||||
// Absorb a lizard, speak Draconic.
|
||||
user.copy_known_languages_from(target)
|
||||
|
||||
target.mind.show_memory(user, 0) //I can read your mind, kekeke. Output all their notes.
|
||||
|
||||
//Some of target's recent speech, so the changeling can attempt to imitate them better.
|
||||
//Recent as opposed to all because rounds tend to have a LOT of text.
|
||||
var/list/recent_speech = list()
|
||||
|
||||
var/list/say_log = target.logging[LOG_SAY]
|
||||
|
||||
if(LAZYLEN(say_log) > LING_ABSORB_RECENT_SPEECH)
|
||||
recent_speech = say_log.Copy(say_log.len-LING_ABSORB_RECENT_SPEECH+1,0) //0 so len-LING_ARS+1 to end of list
|
||||
else
|
||||
for(var/spoken_memory in say_log)
|
||||
if(recent_speech.len >= LING_ABSORB_RECENT_SPEECH)
|
||||
break
|
||||
recent_speech[spoken_memory] = say_log[spoken_memory]
|
||||
|
||||
if(recent_speech.len)
|
||||
changeling.antag_memory += "<B>Some of [target]'s speech patterns, we should study these to better impersonate [target.p_them()]!</B><br>"
|
||||
to_chat(user, "<span class='boldnotice'>Some of [target]'s speech patterns, we should study these to better impersonate [target.p_them()]!</span>")
|
||||
for(var/spoken_memory in recent_speech)
|
||||
changeling.antag_memory += "\"[recent_speech[spoken_memory]]\"<br>"
|
||||
to_chat(user, "<span class='notice'>\"[recent_speech[spoken_memory]]\"</span>")
|
||||
changeling.antag_memory += "<B>We have no more knowledge of [target]'s speech patterns.</B><br>"
|
||||
to_chat(user, "<span class='boldnotice'>We have no more knowledge of [target]'s speech patterns.</span>")
|
||||
|
||||
|
||||
var/datum/antagonist/changeling/target_ling = target.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
if(target_ling)//If the target was a changeling, suck out their extra juice and objective points!
|
||||
to_chat(user, "<span class='boldnotice'>[target] was one of us. We have absorbed their power.</span>")
|
||||
target_ling.remove_changeling_powers()
|
||||
changeling.geneticpoints += round(target_ling.geneticpoints/2)
|
||||
target_ling.geneticpoints = 0
|
||||
target_ling.canrespec = 0
|
||||
changeling.chem_storage += round(target_ling.chem_storage/2)
|
||||
changeling.chem_charges += min(target_ling.chem_charges, changeling.chem_storage)
|
||||
target_ling.chem_charges = 0
|
||||
target_ling.chem_storage = 0
|
||||
changeling.absorbedcount += (target_ling.absorbedcount)
|
||||
target_ling.stored_profiles.len = 1
|
||||
target_ling.absorbedcount = 0
|
||||
|
||||
|
||||
changeling.chem_charges=min(changeling.chem_charges+10, changeling.chem_storage)
|
||||
|
||||
changeling.isabsorbing = 0
|
||||
changeling.canrespec = 1
|
||||
|
||||
target.death(0)
|
||||
target.Drain()
|
||||
return TRUE
|
||||
/obj/effect/proc_holder/changeling/absorbDNA
|
||||
name = "Absorb DNA"
|
||||
desc = "Absorb the DNA of our victim."
|
||||
chemical_cost = 0
|
||||
dna_cost = 0
|
||||
req_human = 1
|
||||
action_icon = 'icons/mob/actions/actions_changeling.dmi'
|
||||
action_icon_state = "ling_absorb_dna"
|
||||
action_background_icon_state = "bg_ling"
|
||||
|
||||
/obj/effect/proc_holder/changeling/absorbDNA/can_sting(mob/living/carbon/user)
|
||||
if(!..())
|
||||
return
|
||||
|
||||
var/datum/antagonist/changeling/changeling = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
if(changeling.isabsorbing)
|
||||
to_chat(user, "<span class='warning'>We are already absorbing!</span>")
|
||||
return
|
||||
|
||||
if(!user.pulling || !iscarbon(user.pulling))
|
||||
to_chat(user, "<span class='warning'>We must be grabbing a creature to absorb them!</span>")
|
||||
return
|
||||
if(user.grab_state <= GRAB_NECK)
|
||||
to_chat(user, "<span class='warning'>We must have a tighter grip to absorb this creature!</span>")
|
||||
return
|
||||
|
||||
var/mob/living/carbon/target = user.pulling
|
||||
return changeling.can_absorb_dna(target)
|
||||
|
||||
|
||||
|
||||
/obj/effect/proc_holder/changeling/absorbDNA/sting_action(mob/user)
|
||||
var/datum/antagonist/changeling/changeling = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
var/mob/living/carbon/human/target = user.pulling
|
||||
changeling.isabsorbing = 1
|
||||
for(var/i in 1 to 3)
|
||||
switch(i)
|
||||
if(1)
|
||||
to_chat(user, "<span class='notice'>This creature is compatible. We must hold still...</span>")
|
||||
if(2)
|
||||
user.visible_message("<span class='warning'>[user] extends a proboscis!</span>", "<span class='notice'>We extend a proboscis.</span>")
|
||||
if(3)
|
||||
user.visible_message("<span class='danger'>[user] stabs [target] with the proboscis!</span>", "<span class='notice'>We stab [target] with the proboscis.</span>")
|
||||
to_chat(target, "<span class='userdanger'>You feel a sharp stabbing pain!</span>")
|
||||
target.take_overall_damage(40)
|
||||
|
||||
SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("Absorb DNA", "[i]"))
|
||||
if(!do_mob(user, target, 150))
|
||||
to_chat(user, "<span class='warning'>Our absorption of [target] has been interrupted!</span>")
|
||||
changeling.isabsorbing = 0
|
||||
return
|
||||
|
||||
SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("Absorb DNA", "4"))
|
||||
user.visible_message("<span class='danger'>[user] sucks the fluids from [target]!</span>", "<span class='notice'>We have absorbed [target].</span>")
|
||||
to_chat(target, "<span class='userdanger'>You are absorbed by the changeling!</span>")
|
||||
|
||||
if(!changeling.has_dna(target.dna))
|
||||
changeling.add_new_profile(target)
|
||||
changeling.trueabsorbs++
|
||||
|
||||
if(user.nutrition < NUTRITION_LEVEL_WELL_FED)
|
||||
user.nutrition = min((user.nutrition + target.nutrition), NUTRITION_LEVEL_WELL_FED)
|
||||
|
||||
if(target.mind)//if the victim has got a mind
|
||||
// Absorb a lizard, speak Draconic.
|
||||
user.copy_known_languages_from(target)
|
||||
|
||||
target.mind.show_memory(user, 0) //I can read your mind, kekeke. Output all their notes.
|
||||
|
||||
//Some of target's recent speech, so the changeling can attempt to imitate them better.
|
||||
//Recent as opposed to all because rounds tend to have a LOT of text.
|
||||
var/list/recent_speech = list()
|
||||
|
||||
var/list/say_log = target.logging[LOG_SAY]
|
||||
|
||||
if(LAZYLEN(say_log) > LING_ABSORB_RECENT_SPEECH)
|
||||
recent_speech = say_log.Copy(say_log.len-LING_ABSORB_RECENT_SPEECH+1,0) //0 so len-LING_ARS+1 to end of list
|
||||
else
|
||||
for(var/spoken_memory in say_log)
|
||||
if(recent_speech.len >= LING_ABSORB_RECENT_SPEECH)
|
||||
break
|
||||
recent_speech[spoken_memory] = say_log[spoken_memory]
|
||||
|
||||
if(recent_speech.len)
|
||||
changeling.antag_memory += "<B>Some of [target]'s speech patterns, we should study these to better impersonate [target.p_them()]!</B><br>"
|
||||
to_chat(user, "<span class='boldnotice'>Some of [target]'s speech patterns, we should study these to better impersonate [target.p_them()]!</span>")
|
||||
for(var/spoken_memory in recent_speech)
|
||||
changeling.antag_memory += "\"[recent_speech[spoken_memory]]\"<br>"
|
||||
to_chat(user, "<span class='notice'>\"[recent_speech[spoken_memory]]\"</span>")
|
||||
changeling.antag_memory += "<B>We have no more knowledge of [target]'s speech patterns.</B><br>"
|
||||
to_chat(user, "<span class='boldnotice'>We have no more knowledge of [target]'s speech patterns.</span>")
|
||||
|
||||
|
||||
var/datum/antagonist/changeling/target_ling = target.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
if(target_ling)//If the target was a changeling, suck out their extra juice and objective points!
|
||||
to_chat(user, "<span class='boldnotice'>[target] was one of us. We have absorbed their power.</span>")
|
||||
target_ling.remove_changeling_powers()
|
||||
changeling.geneticpoints += round(target_ling.geneticpoints/2)
|
||||
target_ling.geneticpoints = 0
|
||||
target_ling.canrespec = 0
|
||||
changeling.chem_storage += round(target_ling.chem_storage/2)
|
||||
changeling.chem_charges += min(target_ling.chem_charges, changeling.chem_storage)
|
||||
target_ling.chem_charges = 0
|
||||
target_ling.chem_storage = 0
|
||||
changeling.absorbedcount += (target_ling.absorbedcount)
|
||||
target_ling.stored_profiles.len = 1
|
||||
target_ling.absorbedcount = 0
|
||||
|
||||
|
||||
changeling.chem_charges=min(changeling.chem_charges+10, changeling.chem_storage)
|
||||
|
||||
changeling.isabsorbing = 0
|
||||
changeling.canrespec = 1
|
||||
|
||||
target.death(0)
|
||||
target.Drain()
|
||||
return TRUE
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
|
||||
//Recover from stuns.
|
||||
/obj/effect/proc_holder/changeling/adrenaline/sting_action(mob/living/user)
|
||||
user.do_adrenaline(0, FALSE, 70, 0, TRUE, list("epinephrine" = 3, "changelingmeth" = 10, "mannitol" = 10, "regen_jelly" = 10, "changelingadrenaline" = 5), "<span class='notice'>Energy rushes through us.</span>", 0, 0.75, 0)
|
||||
user.do_adrenaline(0, FALSE, 70, 0, TRUE, list(/datum/reagent/medicine/epinephrine = 3, /datum/reagent/drug/methamphetamine/changeling = 10, /datum/reagent/medicine/mannitol = 10, /datum/reagent/medicine/regen_jelly = 10, /datum/reagent/medicine/changelingadrenaline = 5), "<span class='notice'>Energy rushes through us.</span>", 0, 0.75, 0)
|
||||
return TRUE
|
||||
@@ -1,43 +1,43 @@
|
||||
/obj/effect/proc_holder/changeling/fakedeath
|
||||
name = "Reviving Stasis"
|
||||
desc = "We fall into a stasis, allowing us to regenerate and trick our enemies."
|
||||
chemical_cost = 15
|
||||
dna_cost = 0
|
||||
req_dna = 1
|
||||
req_stat = DEAD
|
||||
ignores_fakedeath = TRUE
|
||||
action_icon = 'icons/mob/actions/actions_changeling.dmi'
|
||||
action_icon_state = "ling_regenerative_stasis"
|
||||
action_background_icon_state = "bg_ling"
|
||||
|
||||
//Fake our own death and fully heal. You will appear to be dead but regenerate fully after a short delay.
|
||||
/obj/effect/proc_holder/changeling/fakedeath/sting_action(mob/living/user)
|
||||
to_chat(user, "<span class='notice'>We begin our stasis, preparing energy to arise once more.</span>")
|
||||
if(user.stat != DEAD)
|
||||
user.emote("deathgasp")
|
||||
user.tod = STATION_TIME_TIMESTAMP("hh:mm:ss")
|
||||
user.fakedeath("changeling") //play dead
|
||||
user.update_stat()
|
||||
user.update_canmove()
|
||||
|
||||
addtimer(CALLBACK(src, .proc/ready_to_regenerate, user), LING_FAKEDEATH_TIME, TIMER_UNIQUE)
|
||||
return TRUE
|
||||
|
||||
/obj/effect/proc_holder/changeling/fakedeath/proc/ready_to_regenerate(mob/user)
|
||||
if(user && user.mind)
|
||||
var/datum/antagonist/changeling/C = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
if(C && C.purchasedpowers)
|
||||
to_chat(user, "<span class='notice'>We are ready to revive.</span>")
|
||||
var/obj/effect/proc_holder/changeling/revive/RV = new /obj/effect/proc_holder/changeling/revive(null)
|
||||
C.purchasedpowers += RV
|
||||
RV.action.Grant(user)
|
||||
|
||||
/obj/effect/proc_holder/changeling/fakedeath/can_sting(mob/living/user)
|
||||
if(HAS_TRAIT_FROM(user, TRAIT_DEATHCOMA, "changeling"))
|
||||
to_chat(user, "<span class='warning'>We are already reviving.</span>")
|
||||
return
|
||||
if(!user.stat) //Confirmation for living changelings if they want to fake their death
|
||||
switch(alert("Are we sure we wish to fake our own death?",,"Yes", "No"))
|
||||
if("No")
|
||||
return
|
||||
return ..()
|
||||
/obj/effect/proc_holder/changeling/fakedeath
|
||||
name = "Reviving Stasis"
|
||||
desc = "We fall into a stasis, allowing us to regenerate and trick our enemies."
|
||||
chemical_cost = 15
|
||||
dna_cost = 0
|
||||
req_dna = 1
|
||||
req_stat = DEAD
|
||||
ignores_fakedeath = TRUE
|
||||
action_icon = 'icons/mob/actions/actions_changeling.dmi'
|
||||
action_icon_state = "ling_regenerative_stasis"
|
||||
action_background_icon_state = "bg_ling"
|
||||
|
||||
//Fake our own death and fully heal. You will appear to be dead but regenerate fully after a short delay.
|
||||
/obj/effect/proc_holder/changeling/fakedeath/sting_action(mob/living/user)
|
||||
to_chat(user, "<span class='notice'>We begin our stasis, preparing energy to arise once more.</span>")
|
||||
if(user.stat != DEAD)
|
||||
user.emote("deathgasp")
|
||||
user.tod = STATION_TIME_TIMESTAMP("hh:mm:ss")
|
||||
user.fakedeath("changeling") //play dead
|
||||
user.update_stat()
|
||||
user.update_canmove()
|
||||
|
||||
addtimer(CALLBACK(src, .proc/ready_to_regenerate, user), LING_FAKEDEATH_TIME, TIMER_UNIQUE)
|
||||
return TRUE
|
||||
|
||||
/obj/effect/proc_holder/changeling/fakedeath/proc/ready_to_regenerate(mob/user)
|
||||
if(user && user.mind)
|
||||
var/datum/antagonist/changeling/C = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
if(C && C.purchasedpowers)
|
||||
to_chat(user, "<span class='notice'>We are ready to revive.</span>")
|
||||
var/obj/effect/proc_holder/changeling/revive/RV = new /obj/effect/proc_holder/changeling/revive(null)
|
||||
C.purchasedpowers += RV
|
||||
RV.action.Grant(user)
|
||||
|
||||
/obj/effect/proc_holder/changeling/fakedeath/can_sting(mob/living/user)
|
||||
if(HAS_TRAIT_FROM(user, TRAIT_DEATHCOMA, "changeling"))
|
||||
to_chat(user, "<span class='warning'>We are already reviving.</span>")
|
||||
return
|
||||
if(!user.stat) //Confirmation for living changelings if they want to fake their death
|
||||
switch(alert("Are we sure we wish to fake our own death?",,"Yes", "No"))
|
||||
if("No")
|
||||
return
|
||||
return ..()
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
to_chat(H, "<span class='userdanger'>You are blinded by a shower of blood!</span>")
|
||||
H.Stun(20)
|
||||
H.blur_eyes(20)
|
||||
H.adjust_eye_damage(5)
|
||||
var/obj/item/organ/eyes/eyes = H.getorganslot(ORGAN_SLOT_EYES)
|
||||
eyes?.applyOrganDamage(5)
|
||||
H.confused += 3
|
||||
for(var/mob/living/silicon/S in range(2,user))
|
||||
to_chat(S, "<span class='userdanger'>Your sensors are disabled by a shower of blood!</span>")
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
target.mind.linglink = 1
|
||||
target.say("[MODE_TOKEN_CHANGELING] AAAAARRRRGGGGGHHHHH!!")
|
||||
to_chat(target, "<font color=#800040><span class='boldannounce'>You can now communicate in the changeling hivemind, say \"[MODE_TOKEN_CHANGELING] message\" to communicate!</span>")
|
||||
target.reagents.add_reagent("salbutamol", 40) // So they don't choke to death while you interrogate them
|
||||
target.reagents.add_reagent(/datum/reagent/medicine/salbutamol, 40) // So they don't choke to death while you interrogate them
|
||||
sleep(1800)
|
||||
SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("[name]", "[i]"))
|
||||
if(!do_mob(user, target, 20))
|
||||
|
||||
@@ -345,13 +345,13 @@
|
||||
/obj/item/projectile/tentacle/on_hit(atom/target, blocked = FALSE)
|
||||
var/mob/living/carbon/human/H = firer
|
||||
if(blocked >= 100)
|
||||
return 0
|
||||
return BULLET_ACT_BLOCK
|
||||
if(isitem(target))
|
||||
var/obj/item/I = target
|
||||
if(!I.anchored)
|
||||
to_chat(firer, "<span class='notice'>You pull [I] right into your grasp.</span>")
|
||||
H.put_in_hands(I) //Because throwing it is goofy as fuck and unreliable. If you land the tentacle despite the penalties to accuracy, you should have your reward.
|
||||
. = 1
|
||||
. = BULLET_ACT_HIT
|
||||
|
||||
else if(isliving(target))
|
||||
var/mob/living/L = target
|
||||
@@ -366,7 +366,7 @@
|
||||
if(INTENT_HELP)
|
||||
C.visible_message("<span class='danger'>[L] is pulled by [H]'s tentacle!</span>","<span class='userdanger'>A tentacle grabs you and pulls you towards [H]!</span>")
|
||||
C.throw_at(get_step_towards(H,C), 8, 2)
|
||||
return 1
|
||||
return BULLET_ACT_HIT
|
||||
|
||||
if(INTENT_DISARM)
|
||||
var/obj/item/I = C.get_active_held_item()
|
||||
@@ -374,27 +374,27 @@
|
||||
if(C.dropItemToGround(I))
|
||||
C.visible_message("<span class='danger'>[I] is yanked off [C]'s hand by [src]!</span>","<span class='userdanger'>A tentacle pulls [I] away from you!</span>")
|
||||
on_hit(I) //grab the item as if you had hit it directly with the tentacle
|
||||
return 1
|
||||
return BULLET_ACT_HIT
|
||||
else
|
||||
to_chat(firer, "<span class='danger'>You can't seem to pry [I] off [C]'s hands!</span>")
|
||||
return 0
|
||||
return BULLET_ACT_BLOCK
|
||||
else
|
||||
to_chat(firer, "<span class='danger'>[C] has nothing in hand to disarm!</span>")
|
||||
return 0
|
||||
return BULLET_ACT_HIT
|
||||
|
||||
if(INTENT_GRAB)
|
||||
C.visible_message("<span class='danger'>[L] is grabbed by [H]'s tentacle!</span>","<span class='userdanger'>A tentacle grabs you and pulls you towards [H]!</span>")
|
||||
C.throw_at(get_step_towards(H,C), 8, 2, H, TRUE, TRUE, callback=CALLBACK(src, .proc/tentacle_grab, H, C))
|
||||
return 1
|
||||
return BULLET_ACT_HIT
|
||||
|
||||
if(INTENT_HARM)
|
||||
C.visible_message("<span class='danger'>[L] is thrown towards [H] by a tentacle!</span>","<span class='userdanger'>A tentacle grabs you and throws you towards [H]!</span>")
|
||||
C.throw_at(get_step_towards(H,C), 8, 2, H, TRUE, TRUE, callback=CALLBACK(src, .proc/tentacle_stab, H, C))
|
||||
return 1
|
||||
return BULLET_ACT_HIT
|
||||
else
|
||||
L.visible_message("<span class='danger'>[L] is pulled by [H]'s tentacle!</span>","<span class='userdanger'>A tentacle grabs you and pulls you towards [H]!</span>")
|
||||
L.throw_at(get_step_towards(H,L), 8, 2)
|
||||
. = 1
|
||||
. = BULLET_ACT_HIT
|
||||
|
||||
/obj/item/projectile/tentacle/Destroy()
|
||||
qdel(chain)
|
||||
@@ -500,7 +500,7 @@
|
||||
/obj/item/clothing/suit/space/changeling/process()
|
||||
if(ishuman(loc))
|
||||
var/mob/living/carbon/human/H = loc
|
||||
H.reagents.add_reagent("salbutamol", REAGENTS_METABOLISM)
|
||||
H.reagents.add_reagent(/datum/reagent/medicine/salbutamol, REAGENTS_METABOLISM)
|
||||
|
||||
/obj/item/clothing/head/helmet/space/changeling
|
||||
name = "flesh mass"
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
C.vomit(0, toxic = TRUE)
|
||||
O.forceMove(get_turf(user))
|
||||
|
||||
user.reagents.add_reagent("mutadone", 10)
|
||||
user.reagents.add_reagent("pen_jelly", 20)
|
||||
user.reagents.add_reagent("antihol", 10)
|
||||
user.reagents.add_reagent("mannitol", 25)
|
||||
user.reagents.add_reagent(/datum/reagent/medicine/mutadone, 10)
|
||||
user.reagents.add_reagent(/datum/reagent/medicine/pen_acid/pen_jelly, 20)
|
||||
user.reagents.add_reagent(/datum/reagent/medicine/antihol, 10)
|
||||
user.reagents.add_reagent(/datum/reagent/medicine/mannitol, 25)
|
||||
|
||||
if(isliving(user))
|
||||
var/mob/living/L = user
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
/obj/effect/proc_holder/changeling/revive
|
||||
name = "Revive"
|
||||
desc = "We regenerate, healing all damage from our form."
|
||||
helptext = "Does not regrow lost organs or a missing head."
|
||||
req_stat = DEAD
|
||||
always_keep = TRUE
|
||||
ignores_fakedeath = TRUE
|
||||
action_icon = 'icons/mob/actions/actions_changeling.dmi'
|
||||
action_icon_state = "ling_revive"
|
||||
action_background_icon_state = "bg_ling"
|
||||
|
||||
//Revive from revival stasis
|
||||
/obj/effect/proc_holder/changeling/revive/sting_action(mob/living/carbon/user)
|
||||
user.cure_fakedeath("changeling")
|
||||
user.revive(full_heal = 1)
|
||||
var/list/missing = user.get_missing_limbs()
|
||||
missing -= BODY_ZONE_HEAD // headless changelings are funny
|
||||
if(missing.len)
|
||||
playsound(user, 'sound/magic/demon_consume.ogg', 50, 1)
|
||||
user.visible_message("<span class='warning'>[user]'s missing limbs \
|
||||
reform, making a loud, grotesque sound!</span>",
|
||||
"<span class='userdanger'>Your limbs regrow, making a \
|
||||
loud, crunchy sound and giving you great pain!</span>",
|
||||
"<span class='italics'>You hear organic matter ripping \
|
||||
and tearing!</span>")
|
||||
user.emote("scream")
|
||||
user.regenerate_limbs(0, list(BODY_ZONE_HEAD))
|
||||
user.regenerate_organs()
|
||||
to_chat(user, "<span class='notice'>We have revived ourselves.</span>")
|
||||
var/datum/antagonist/changeling/changeling = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
changeling.purchasedpowers -= src
|
||||
src.action.Remove(user)
|
||||
return TRUE
|
||||
|
||||
/obj/effect/proc_holder/changeling/revive/can_be_used_by(mob/living/user)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
|
||||
if(HAS_TRAIT(user, CHANGELING_DRAIN) || ((user.stat != DEAD) && !(HAS_TRAIT(user, TRAIT_DEATHCOMA))))
|
||||
var/datum/antagonist/changeling/changeling = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
changeling.purchasedpowers -= src
|
||||
return FALSE
|
||||
|
||||
/obj/effect/proc_holder/changeling/revive
|
||||
name = "Revive"
|
||||
desc = "We regenerate, healing all damage from our form."
|
||||
helptext = "Does not regrow lost organs or a missing head."
|
||||
req_stat = DEAD
|
||||
always_keep = TRUE
|
||||
ignores_fakedeath = TRUE
|
||||
action_icon = 'icons/mob/actions/actions_changeling.dmi'
|
||||
action_icon_state = "ling_revive"
|
||||
action_background_icon_state = "bg_ling"
|
||||
|
||||
//Revive from revival stasis
|
||||
/obj/effect/proc_holder/changeling/revive/sting_action(mob/living/carbon/user)
|
||||
user.cure_fakedeath("changeling")
|
||||
user.revive(full_heal = 1)
|
||||
var/list/missing = user.get_missing_limbs()
|
||||
missing -= BODY_ZONE_HEAD // headless changelings are funny
|
||||
if(missing.len)
|
||||
playsound(user, 'sound/magic/demon_consume.ogg', 50, 1)
|
||||
user.visible_message("<span class='warning'>[user]'s missing limbs \
|
||||
reform, making a loud, grotesque sound!</span>",
|
||||
"<span class='userdanger'>Your limbs regrow, making a \
|
||||
loud, crunchy sound and giving you great pain!</span>",
|
||||
"<span class='italics'>You hear organic matter ripping \
|
||||
and tearing!</span>")
|
||||
user.emote("scream")
|
||||
user.regenerate_limbs(0, list(BODY_ZONE_HEAD))
|
||||
user.regenerate_organs()
|
||||
to_chat(user, "<span class='notice'>We have revived ourselves.</span>")
|
||||
var/datum/antagonist/changeling/changeling = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
changeling.purchasedpowers -= src
|
||||
src.action.Remove(user)
|
||||
return TRUE
|
||||
|
||||
/obj/effect/proc_holder/changeling/revive/can_be_used_by(mob/living/user)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
|
||||
if(HAS_TRAIT(user, CHANGELING_DRAIN) || ((user.stat != DEAD) && !(HAS_TRAIT(user, TRAIT_DEATHCOMA))))
|
||||
var/datum/antagonist/changeling/changeling = user.mind.has_antag_datum(/datum/antagonist/changeling)
|
||||
changeling.purchasedpowers -= src
|
||||
return FALSE
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user