Merge branch 'master' into upstream-merge-29839
This commit is contained in:
@@ -261,6 +261,8 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list(
|
||||
verbs += GLOB.admin_verbs_poll
|
||||
if(rights & R_SOUNDS)
|
||||
verbs += GLOB.admin_verbs_sounds
|
||||
if(config.invoke_youtubedl)
|
||||
verbs += /client/proc/play_web_sound
|
||||
if(rights & R_SPAWN)
|
||||
verbs += GLOB.admin_verbs_spawn
|
||||
|
||||
@@ -283,6 +285,7 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list(
|
||||
/client/proc/stealth,
|
||||
GLOB.admin_verbs_poll,
|
||||
GLOB.admin_verbs_sounds,
|
||||
/client/proc/play_web_sound,
|
||||
GLOB.admin_verbs_spawn,
|
||||
/*Debug verbs added by "show debug verbs"*/
|
||||
/client/proc/Cell,
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
return 0
|
||||
|
||||
/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)")
|
||||
|
||||
@@ -36,7 +36,11 @@
|
||||
|
||||
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.add_details("admin_verb","Play Global Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
@@ -52,6 +56,62 @@
|
||||
playsound(get_turf(src.mob), S, 50, 0, 0)
|
||||
SSblackbox.add_details("admin_verb","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
|
||||
|
||||
if(!config.invoke_youtubedl)
|
||||
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/pitch
|
||||
if(length(web_sound_input))
|
||||
|
||||
web_sound_input = trim(web_sound_input)
|
||||
var/static/regex/html_protocol_regex = regex("https?://")
|
||||
if(findtext(web_sound_input, ":") && !findtext(web_sound_input, html_protocol_regex))
|
||||
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("[config.invoke_youtubedl] --format \"bestaudio\[ext=aac]/bestaudio\[ext=mp3]/bestaudio\[ext=m4a]\" --get-url \"[shell_scrubbed_input]\"")
|
||||
var/errorlevel = output[SHELLEO_ERRORLEVEL]
|
||||
var/stdout = output[SHELLEO_STDOUT]
|
||||
var/stderr = output[SHELLEO_STDERR]
|
||||
if(!errorlevel)
|
||||
var/static/regex/content_url_regex = regex("https?://\\S+")
|
||||
if(content_url_regex.Find(stdout))
|
||||
web_sound_url = content_url_regex.match
|
||||
|
||||
if(SSevents.holidays && SSevents.holidays[APRIL_FOOLS])
|
||||
pitch = pick(0.5, 0.7, 0.8, 0.85, 0.9, 0.95, 1.1, 1.2, 1.4, 1.6, 2.0, 2.5)
|
||||
to_chat(src, "You feel the Honkmother messing with your song...")
|
||||
|
||||
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 = " "
|
||||
|
||||
if(web_sound_url)
|
||||
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_url, pitch)
|
||||
|
||||
SSblackbox.add_details("admin_verb","Play Internet Sound")
|
||||
|
||||
/client/proc/set_round_end_sound(S as sound)
|
||||
set category = "Fun"
|
||||
set name = "Set Round End Sound"
|
||||
@@ -75,4 +135,7 @@
|
||||
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.sendMusic(" ")
|
||||
SSblackbox.add_details("admin_verb","Stop All Playing Sounds") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
@@ -222,6 +222,7 @@ Pipelines + Other Objects -> Pipe network
|
||||
build_network()
|
||||
|
||||
/obj/machinery/atmospherics/singularity_pull(S, current_size)
|
||||
..()
|
||||
if(current_size >= STAGE_FIVE)
|
||||
deconstruct(FALSE)
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
return 1
|
||||
|
||||
/obj/machinery/meter/singularity_pull(S, current_size)
|
||||
..()
|
||||
if(current_size >= STAGE_FIVE)
|
||||
new /obj/item/pipe_meter(loc)
|
||||
qdel(src)
|
||||
|
||||
@@ -144,6 +144,9 @@ TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, togglemidis)()
|
||||
else
|
||||
to_chat(usr, "You will no longer hear sounds uploaded by admins")
|
||||
usr.stop_sound_channel(CHANNEL_ADMIN)
|
||||
var/client/C = usr.client
|
||||
if(C && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
|
||||
C.chatOutput.sendMusic(" ")
|
||||
SSblackbox.add_details("preferences_verb","Toggle Hearing Midis|[usr.client.prefs.toggles & SOUND_MIDI]") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
/datum/verbs/menu/Settings/Sound/togglemidis/Get_checked(client/C)
|
||||
return C.prefs.toggles & SOUND_MIDI
|
||||
@@ -230,6 +233,9 @@ TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, toggleprayersounds)()
|
||||
set category = "Preferences"
|
||||
set desc = "Stop Current Sounds"
|
||||
SEND_SOUND(usr, sound(null))
|
||||
var/client/C = usr.client
|
||||
if(C && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded)
|
||||
C.chatOutput.sendMusic(" ")
|
||||
SSblackbox.add_details("preferences_verb","Stop Self Sounds") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
|
||||
|
||||
@@ -142,69 +142,55 @@
|
||||
user.set_machine(src)
|
||||
interact(user)
|
||||
|
||||
/*******************
|
||||
* SmartFridge Menu
|
||||
********************/
|
||||
|
||||
/obj/machinery/smartfridge/interact(mob/user)
|
||||
if(stat)
|
||||
return FALSE
|
||||
|
||||
var/dat = "<TT><b>Select an item:</b><br>"
|
||||
/obj/machinery/smartfridge/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state)
|
||||
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
|
||||
if(!ui)
|
||||
ui = new(user, src, ui_key, "smartvend", name, 440, 550, master_ui, state)
|
||||
ui.open()
|
||||
|
||||
if (contents.len == 0)
|
||||
dat += "<font color = 'red'>No product loaded!</font>"
|
||||
else
|
||||
var/listofitems = list()
|
||||
for (var/atom/movable/O in contents)
|
||||
if (listofitems[O.name])
|
||||
listofitems[O.name]++
|
||||
else
|
||||
listofitems[O.name] = 1
|
||||
sortList(listofitems)
|
||||
/obj/machinery/smartfridge/ui_data(mob/user)
|
||||
. = list()
|
||||
|
||||
for (var/O in listofitems)
|
||||
if(listofitems[O] <= 0)
|
||||
continue
|
||||
var/N = listofitems[O]
|
||||
var/itemName = url_encode(O)
|
||||
dat += "<FONT color = 'blue'><B>[capitalize(O)]</B>:"
|
||||
dat += " [N] </font>"
|
||||
dat += "<a href='byond://?src=\ref[src];vend=[itemName];amount=1'>Vend</A> "
|
||||
if(N > 5)
|
||||
dat += "(<a href='byond://?src=\ref[src];vend=[itemName];amount=5'>x5</A>)"
|
||||
if(N > 10)
|
||||
dat += "(<a href='byond://?src=\ref[src];vend=[itemName];amount=10'>x10</A>)"
|
||||
if(N > 25)
|
||||
dat += "(<a href='byond://?src=\ref[src];vend=[itemName];amount=25'>x25</A>)"
|
||||
if(N > 1)
|
||||
dat += "(<a href='?src=\ref[src];vend=[itemName];amount=[N]'>All</A>)"
|
||||
var/listofitems = list()
|
||||
for (var/I in src)
|
||||
var/atom/movable/O = I
|
||||
if (listofitems[O.name])
|
||||
listofitems[O.name]["amount"]++
|
||||
else
|
||||
listofitems[O.name] = list("name" = O.name, "type" = O.type, "amount" = 1)
|
||||
sortList(listofitems)
|
||||
|
||||
dat += "<br>"
|
||||
.["contents"] = listofitems
|
||||
.["name"] = name
|
||||
.["isdryer"] = FALSE
|
||||
|
||||
dat += "</TT>"
|
||||
user << browse("<HEAD><TITLE>[src] supplies</TITLE></HEAD><TT>[dat]</TT>", "window=smartfridge")
|
||||
onclose(user, "smartfridge")
|
||||
return dat
|
||||
|
||||
/obj/machinery/smartfridge/Topic(var/href, var/list/href_list)
|
||||
if(..())
|
||||
/obj/machinery/smartfridge/ui_act(action, params)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
usr.set_machine(src)
|
||||
switch(action)
|
||||
if("Release")
|
||||
var/desired = 0
|
||||
|
||||
var/N = href_list["vend"]
|
||||
var/amount = text2num(href_list["amount"])
|
||||
if (params["amount"])
|
||||
desired = text2num(params["amount"])
|
||||
else
|
||||
desired = input("How many items?", "How many items would you like to take out?", 1) as null|num
|
||||
|
||||
var/i = amount
|
||||
for(var/obj/O in contents)
|
||||
if(i <= 0)
|
||||
break
|
||||
if(O.name == N)
|
||||
O.loc = src.loc
|
||||
i--
|
||||
if(QDELETED(src) || QDELETED(usr) || !usr.Adjacent(src)) // Sanity checkin' in case stupid stuff happens while we wait for input()
|
||||
return FALSE
|
||||
|
||||
|
||||
updateUsrDialog()
|
||||
for(var/obj/item/O in src)
|
||||
if(desired <= 0)
|
||||
break
|
||||
if(O.name == params["name"])
|
||||
O.forceMove(drop_location())
|
||||
desired--
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
|
||||
// ----------------------------
|
||||
@@ -240,20 +226,35 @@
|
||||
/obj/machinery/smartfridge/drying_rack/default_deconstruction_crowbar(obj/item/crowbar/C, ignore_panel = 1)
|
||||
..()
|
||||
|
||||
/obj/machinery/smartfridge/drying_rack/interact(mob/user)
|
||||
var/dat = ..()
|
||||
if(dat)
|
||||
dat += "<br>"
|
||||
dat += "<a href='byond://?src=\ref[src];dry=1'>Toggle Drying</A> "
|
||||
user << browse("<HEAD><TITLE>[src] supplies</TITLE></HEAD><TT>[dat]</TT>", "window=smartfridge")
|
||||
onclose(user, "smartfridge")
|
||||
/obj/machinery/smartfridge/drying_rack/ui_data(mob/user)
|
||||
. = list()
|
||||
|
||||
/obj/machinery/smartfridge/drying_rack/Topic(href, list/href_list)
|
||||
..()
|
||||
if(href_list["dry"])
|
||||
toggle_drying(FALSE)
|
||||
updateUsrDialog()
|
||||
update_icon()
|
||||
var/listofitems = list()
|
||||
for (var/I in src)
|
||||
var/atom/movable/O = I
|
||||
|
||||
if (listofitems[O.name])
|
||||
listofitems[O.name]["amount"]++
|
||||
else
|
||||
listofitems[O.name] = list("name" = O.name, "type" = O.type, "amount" = 1)
|
||||
sortList(listofitems)
|
||||
|
||||
.["contents"] = listofitems
|
||||
.["name"] = name
|
||||
.["isdryer"] = TRUE
|
||||
.["verb"] = "Take"
|
||||
.["drying"] = drying
|
||||
|
||||
|
||||
/obj/machinery/smartfridge/drying_rack/ui_act(action, params)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
switch(action)
|
||||
if("Dry")
|
||||
toggle_drying(FALSE)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/obj/machinery/smartfridge/drying_rack/power_change()
|
||||
if(powered() && anchored)
|
||||
|
||||
@@ -13,6 +13,7 @@ GLOBAL_DATUM_INIT(iconCache, /savefile, new("data/iconCache.sav")) //Cache of ic
|
||||
var/cookieSent = FALSE // Has the client sent a cookie for analysis
|
||||
var/broken = FALSE
|
||||
var/list/connectionHistory //Contains the connection history passed from chat cookie
|
||||
var/adminMusicVolume = 100 //This is for the Play Global Sound verb
|
||||
|
||||
/datum/chatOutput/New(client/C)
|
||||
owner = C
|
||||
@@ -79,6 +80,9 @@ GLOBAL_DATUM_INIT(iconCache, /savefile, new("data/iconCache.sav")) //Cache of ic
|
||||
if("analyzeClientData")
|
||||
data = analyzeClientData(arglist(params))
|
||||
|
||||
if("setMusicVolume")
|
||||
data = setMusicVolume(arglist(params))
|
||||
|
||||
if(data)
|
||||
ehjax_send(data = data)
|
||||
|
||||
@@ -120,6 +124,16 @@ GLOBAL_DATUM_INIT(iconCache, /savefile, new("data/iconCache.sav")) //Cache of ic
|
||||
data = json_encode(data)
|
||||
C << output("[data]", "[window]:ehjaxCallback")
|
||||
|
||||
/datum/chatOutput/proc/sendMusic(music, pitch)
|
||||
var/list/music_data = list("adminMusic" = url_encode(url_encode(music)))
|
||||
if(pitch)
|
||||
music_data["musicRate"] = pitch
|
||||
ehjax_send(data = music_data)
|
||||
|
||||
/datum/chatOutput/proc/setMusicVolume(volume = "")
|
||||
if(volume)
|
||||
adminMusicVolume = Clamp(text2num(volume), 0, 100)
|
||||
|
||||
//Sends client connection details to the chat to handle and save
|
||||
/datum/chatOutput/proc/sendClientData()
|
||||
//Get dem deets
|
||||
|
||||
@@ -101,7 +101,7 @@ a.popt {text-decoration: none;}
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
#options a {
|
||||
#options .optionsCell {
|
||||
background: #ddd;
|
||||
height: 30px;
|
||||
padding: 5px 0;
|
||||
@@ -111,7 +111,7 @@ a.popt {text-decoration: none;}
|
||||
line-height: 28px;
|
||||
border-top: 1px solid #b4b4b4;
|
||||
}
|
||||
#options a:hover {background: #ccc;}
|
||||
#options .optionsCell:hover {background: #ccc;}
|
||||
#options .toggle {
|
||||
width: 40px;
|
||||
background: #ccc;
|
||||
@@ -121,7 +121,7 @@ a.popt {text-decoration: none;}
|
||||
}
|
||||
#options .sub {clear: both; display: none; width: 160px;}
|
||||
#options .sub.scroll {overflow-y: scroll;}
|
||||
#options .sub a {padding: 3px 0 3px 8px; line-height: 30px; font-size: 0.9em; clear: both;}
|
||||
#options .sub.optionsCell {padding: 3px 0 3px 8px; line-height: 30px; font-size: 0.9em; clear: both;}
|
||||
#options .sub span {
|
||||
display: block;
|
||||
line-height: 30px;
|
||||
@@ -136,6 +136,13 @@ a.popt {text-decoration: none;}
|
||||
line-height: 30px;
|
||||
float: right;
|
||||
}
|
||||
#options .sub input {
|
||||
position: absolute;
|
||||
padding: 7px 5px;
|
||||
width: 121px;
|
||||
line-height: 30px;
|
||||
float: left;
|
||||
}
|
||||
#options .decreaseFont {border-top: 0;}
|
||||
|
||||
/* POPUPS */
|
||||
|
||||
@@ -28,17 +28,19 @@
|
||||
<span class="ms" id="pingMs">--ms</span>
|
||||
</div>
|
||||
<div id="options">
|
||||
<a href="#" class="toggle" id="toggleOptions" title="Options"><i class="icon-cog"></i></a>
|
||||
<a href="#" class="optionsCell toggle" id="toggleOptions" title="Options"><i class="icon-cog"></i></a>
|
||||
<div class="sub" id="subOptions">
|
||||
<a href="#" class="decreaseFont" id="decreaseFont"><span>Decrease font size</span> <i class="icon-font">-</i></a>
|
||||
<a href="#" class="increaseFont" id="increaseFont"><span>Increase font size</span> <i class="icon-font">+</i></a>
|
||||
<a href="#" class="togglePing" id="togglePing"><span>Toggle ping display</span> <i class="icon-circle"></i></a>
|
||||
<a href="#" class="highlightTerm" id="highlightTerm"><span>Highlight string</span> <i class="icon-tag"></i></a>
|
||||
<a href="#" class="saveLog" id="saveLog"><span>Save chat log</span> <i class="icon-save"></i></a>
|
||||
<a href="#" class="clearMessages" id="clearMessages"><span>Clear all messages</span> <i class="icon-eraser"></i></a>
|
||||
<a href="#" class="optionsCell decreaseFont" id="decreaseFont"><span>Decrease font size</span> <i class="icon-font">-</i></a>
|
||||
<a href="#" class="optionsCell increaseFont" id="increaseFont"><span>Increase font size</span> <i class="icon-font">+</i></a>
|
||||
<a href="#" class="optionsCell togglePing" id="togglePing"><span>Toggle ping display</span> <i class="icon-circle"></i></a>
|
||||
<a href="#" class="optionsCell highlightTerm" id="highlightTerm"><span>Highlight string</span> <i class="icon-tag"></i></a>
|
||||
<a href="#" class="optionsCell saveLog" id="saveLog"><span>Save chat log</span> <i class="icon-save"></i></a>
|
||||
<a href="#" class="optionsCell clearMessages" id="clearMessages"><span>Clear all messages</span> <i class="icon-eraser"></i></a>
|
||||
<span class="optionsCell" id="musicVolumeSpan"><input type="range" class="hidden" id="musicVolume"><span id="musicVolumeText">Admin music volume</span><i class="icon-music"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<audio class="hidden" id="adminMusic" autoplay></audio>
|
||||
<script type="text/javascript" src="browserOutput.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -61,8 +61,17 @@ var opts = {
|
||||
'clientDataLimit': 5,
|
||||
'clientData': [],
|
||||
|
||||
//Admin music volume update
|
||||
'volumeUpdateDelay': 5000, //Time from when the volume updates to data being sent to the server
|
||||
'volumeUpdating': false, //True if volume update function set to fire
|
||||
'updatedVolume': 0, //The volume level that is sent to the server
|
||||
|
||||
};
|
||||
|
||||
function clamp(val, min, max) {
|
||||
return Math.max(min, Math.min(val, max))
|
||||
}
|
||||
|
||||
function outerHTML(el) {
|
||||
var wrap = document.createElement('div');
|
||||
wrap.appendChild(el.cloneNode(true));
|
||||
@@ -95,6 +104,15 @@ function linkify(text) {
|
||||
});
|
||||
}
|
||||
|
||||
function byondDecode(message) {
|
||||
// Basically we url_encode twice server side so we can manually read the encoded version and actually do UTF-8.
|
||||
// The replace for + is because FOR SOME REASON, BYOND replaces spaces with a + instead of %20, and a plus with %2b.
|
||||
// Marvelous.
|
||||
message = message.replace(/\+/g, "%20");
|
||||
message = decoder(message);
|
||||
return message;
|
||||
}
|
||||
|
||||
//Actually turns the highlight term match into appropriate html
|
||||
function addHighlightMarkup(match) {
|
||||
var extra = '';
|
||||
@@ -176,11 +194,7 @@ function output(message, flag) {
|
||||
if (flag !== 'internal')
|
||||
opts.lastPang = Date.now();
|
||||
|
||||
// Basically we url_encode twice server side so we can manually read the encoded version and actually do UTF-8.
|
||||
// The replace for + is because FOR SOME REASON, BYOND replaces spaces with a + instead of %20, and a plus with %2b.
|
||||
// Marvelous.
|
||||
message = message.replace(/\+/g, "%20")
|
||||
message = decoder(message)
|
||||
message = byondDecode(message)
|
||||
|
||||
//The behemoth of filter-code (for Admin message filters)
|
||||
//Note: This is proooobably hella inefficient
|
||||
@@ -423,7 +437,22 @@ function ehjaxCallback(data) {
|
||||
var firebugEl = document.createElement('script');
|
||||
firebugEl.src = 'https://getfirebug.com/firebug-lite-debug.js';
|
||||
document.body.appendChild(firebugEl);
|
||||
}
|
||||
} else if (data.adminMusic) {
|
||||
if (typeof data.adminMusic === 'string') {
|
||||
var adminMusic = byondDecode(data.adminMusic);
|
||||
adminMusic = adminMusic.match(/https?:\/\/\S+/) || '';
|
||||
if (data.musicRate) {
|
||||
var newRate = Number(data.musicRate);
|
||||
if(newRate) {
|
||||
$('#adminMusic').prop('defaultPlaybackRate', newRate);
|
||||
}
|
||||
} else {
|
||||
$('#adminMusic').prop('defaultPlaybackRate', 1.0);
|
||||
}
|
||||
$('#adminMusic').prop('src', adminMusic);
|
||||
$('#adminMusic').trigger("play");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +475,13 @@ function toggleWasd(state) {
|
||||
opts.wasd = (state == 'on' ? true : false);
|
||||
}
|
||||
|
||||
function sendVolumeUpdate() {
|
||||
opts.volumeUpdating = false;
|
||||
if(opts.updatedVolume) {
|
||||
runByond('?_src_=chat&proc=setMusicVolume¶m[volume]='+opts.updatedVolume);
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************
|
||||
*
|
||||
* DOM READY
|
||||
@@ -486,6 +522,7 @@ $(function() {
|
||||
'spingDisabled': getCookie('pingdisabled'),
|
||||
'shighlightTerms': getCookie('highlightterms'),
|
||||
'shighlightColor': getCookie('highlightcolor'),
|
||||
'smusicVolume': getCookie('musicVolume'),
|
||||
};
|
||||
|
||||
if (savedConfig.sfontSize) {
|
||||
@@ -517,6 +554,14 @@ $(function() {
|
||||
opts.highlightColor = savedConfig.shighlightColor;
|
||||
internalOutput('<span class="internal boldnshit">Loaded highlight color of: '+savedConfig.shighlightColor+'</span>', 'internal');
|
||||
}
|
||||
if (savedConfig.smusicVolume) {
|
||||
var newVolume = clamp(savedConfig.smusicVolume, 0, 100);
|
||||
$('#adminMusic').prop('volume', newVolume / 100);
|
||||
$('#musicVolume').val(newVolume);
|
||||
opts.updatedVolume = newVolume;
|
||||
sendVolumeUpdate();
|
||||
internalOutput('<span class="internal boldnshit">Loaded music volume of: '+savedConfig.smusicVolume+'</span>', 'internal');
|
||||
}
|
||||
|
||||
(function() {
|
||||
var dataCookie = getCookie('connData');
|
||||
@@ -835,6 +880,26 @@ $(function() {
|
||||
opts.messageCount = 0;
|
||||
});
|
||||
|
||||
$('#musicVolumeSpan').hover(function() {
|
||||
$('#musicVolumeText').addClass('hidden');
|
||||
$('#musicVolume').removeClass('hidden');
|
||||
}, function() {
|
||||
$('#musicVolume').addClass('hidden');
|
||||
$('#musicVolumeText').removeClass('hidden');
|
||||
});
|
||||
|
||||
$('#musicVolume').change(function() {
|
||||
var newVolume = $('#musicVolume').val();
|
||||
newVolume = clamp(newVolume, 0, 100);
|
||||
$('#adminMusic').prop('volume', newVolume / 100);
|
||||
setCookie('musicVolume', newVolume, 365);
|
||||
opts.updatedVolume = newVolume;
|
||||
if(!opts.volumeUpdating) {
|
||||
setTimeout(sendVolumeUpdate, opts.volumeUpdateDelay);
|
||||
opts.volumeUpdating = true;
|
||||
}
|
||||
});
|
||||
|
||||
$('img.icon').error(iconError);
|
||||
|
||||
|
||||
|
||||
@@ -120,17 +120,15 @@
|
||||
else
|
||||
..()
|
||||
|
||||
/obj/structure/holohoop/CanPass(atom/movable/mover, turf/target)
|
||||
if (isitem(mover) && mover.throwing)
|
||||
var/obj/item/I = mover
|
||||
if(istype(I, /obj/item/projectile))
|
||||
return
|
||||
/obj/structure/holohoop/hitby(atom/movable/AM)
|
||||
if (isitem(AM) && !istype(AM,/obj/item/projectile))
|
||||
if(prob(50))
|
||||
I.forceMove(get_turf(src))
|
||||
visible_message("<span class='warning'>Swish! [I] lands in [src].</span>")
|
||||
AM.forceMove(get_turf(src))
|
||||
visible_message("<span class='warning'>Swish! [AM] lands in [src].</span>")
|
||||
return
|
||||
else
|
||||
visible_message("<span class='danger'>[I] bounces off of [src]'s rim!</span>")
|
||||
return 0
|
||||
visible_message("<span class='danger'>[AM] bounces off of [src]'s rim!</span>")
|
||||
return ..()
|
||||
else
|
||||
return ..()
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
if(istype(src, /obj/machinery/hydroponics/soil))
|
||||
add_atom_colour(rgb(255, 175, 0), FIXED_COLOUR_PRIORITY)
|
||||
else
|
||||
overlays += mutable_appearance('icons/obj/hydroponics/equipment.dmi', "gaia_blessing")
|
||||
add_overlay(mutable_appearance('icons/obj/hydroponics/equipment.dmi', "gaia_blessing"))
|
||||
set_light(3)
|
||||
|
||||
update_icon_hoses()
|
||||
|
||||
@@ -643,6 +643,7 @@ INITIALIZE_IMMEDIATE(/mob/living/carbon/human/dummy)
|
||||
update_hair()
|
||||
|
||||
/mob/living/carbon/human/singularity_pull(S, current_size)
|
||||
..()
|
||||
if(current_size >= STAGE_THREE)
|
||||
for(var/obj/item/hand in held_items)
|
||||
if(prob(current_size * 5) && hand.w_class >= ((11-current_size)/2) && dropItemToGround(hand))
|
||||
@@ -651,7 +652,6 @@ INITIALIZE_IMMEDIATE(/mob/living/carbon/human/dummy)
|
||||
rad_act(current_size * 3)
|
||||
if(mob_negates_gravity())
|
||||
return
|
||||
..()
|
||||
|
||||
/mob/living/carbon/human/proc/do_cpr(mob/living/carbon/C)
|
||||
CHECK_DNA_AND_SPECIES(C)
|
||||
|
||||
@@ -690,6 +690,7 @@
|
||||
who.equip_to_slot(what, where, TRUE)
|
||||
|
||||
/mob/living/singularity_pull(S, current_size)
|
||||
..()
|
||||
if(current_size >= STAGE_SIX)
|
||||
throw_at(S,14,3, spin=1)
|
||||
else
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
/mob/living/simple_animal/drone/verb/toggle_light()
|
||||
set category = "Drone"
|
||||
set name = "Toggle drone light"
|
||||
|
||||
if(stat == DEAD)
|
||||
to_chat(src, "<span class='warning'>There's no light in your life... by that I mean you're dead.</span>")
|
||||
return
|
||||
if(light_on)
|
||||
set_light(0)
|
||||
else
|
||||
|
||||
@@ -24,11 +24,17 @@
|
||||
butcher_results = list()
|
||||
gold_core_spawnable = 2
|
||||
|
||||
/mob/living/simple_animal/pet/penguin/emperor/shamebrero
|
||||
name = "Shamebrero penguin."
|
||||
desc = "Shameful of all he surveys."
|
||||
icon_state = "penguin_shamebrero"
|
||||
icon_living = "penguin_shamebrero"
|
||||
|
||||
/mob/living/simple_animal/pet/penguin/baby
|
||||
speak = list("gah", "noot noot", "noot!", "noot", "squeee!", "noo!")
|
||||
name = "Penguin chick"
|
||||
real_name = "penguin"
|
||||
desc = "Can't fly and can barely waddles, but the prince of all chicks."
|
||||
desc = "Can't fly and barely waddles, yet the prince of all chicks."
|
||||
icon_state = "penguin_baby"
|
||||
icon_living = "penguin_baby"
|
||||
icon_dead = "penguin_baby_dead"
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
var/ranged_message = "fires" //Fluff text for ranged mobs
|
||||
var/ranged_cooldown = 0 //What the current cooldown on ranged attacks is, generally world.time + ranged_cooldown_time
|
||||
var/ranged_cooldown_time = 30 //How long, in deciseconds, the cooldown of ranged attacks is
|
||||
var/ranged_telegraph = "prepares to fire at *TARGET*!" //A message shown when the mob prepares to fire; use *TARGET* if you want to show the target's name
|
||||
var/ranged_telegraph_sound //A sound played when the mob prepares to fire
|
||||
var/ranged_telegraph_time = 0 //In deciseconds, how long between the telegraph and ranged shot
|
||||
var/ranged_ignores_vision = FALSE //if it'll fire ranged attacks even if it lacks vision on its target, only works with environment smash
|
||||
var/check_friendly_fire = 0 // Should the ranged mob check for friendlies when shooting
|
||||
var/retreat_distance = null //If our mob runs from players when they're too close, set in tile distance. By default, mobs do not retreat.
|
||||
@@ -232,7 +235,14 @@
|
||||
var/target_distance = get_dist(targets_from,target)
|
||||
if(ranged) //We ranged? Shoot at em
|
||||
if(!target.Adjacent(targets_from) && ranged_cooldown <= world.time) //But make sure they're not in range for a melee attack and our range attack is off cooldown
|
||||
OpenFire(target)
|
||||
if(!ranged_telegraph_time || client)
|
||||
OpenFire(target)
|
||||
else
|
||||
if(ranged_telegraph)
|
||||
visible_message("<span class='danger'>[src] [replacetext(ranged_telegraph, "*TARGET*", "[target]")]</span>")
|
||||
if(ranged_telegraph_sound)
|
||||
playsound(src, ranged_telegraph_sound, 75, FALSE)
|
||||
addtimer(CALLBACK(src, .proc/OpenFire, target), ranged_telegraph_time)
|
||||
if(!Process_Spacemove()) //Drifting
|
||||
walk(src,0)
|
||||
return 1
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
ranged = 1
|
||||
ranged_message = "stares"
|
||||
ranged_cooldown_time = 30
|
||||
ranged_telegraph = "gathers energy and stares at *TARGET*!"
|
||||
ranged_telegraph_sound = 'sound/magic/magic_missile.ogg'
|
||||
ranged_telegraph_time = 7
|
||||
throw_message = "does nothing against the hard shell of"
|
||||
vision_range = 2
|
||||
speed = 3
|
||||
@@ -70,9 +73,11 @@
|
||||
melee_damage_lower = 15
|
||||
melee_damage_upper = 15
|
||||
attacktext = "impales"
|
||||
ranged_telegraph = "fixates on *TARGET* as its eye shines blue!"
|
||||
ranged_telegraph_sound = 'sound/magic/tail_swing.ogg'
|
||||
ranged_telegraph_time = 5
|
||||
a_intent = INTENT_HARM
|
||||
speak_emote = list("telepathically cries")
|
||||
attack_sound = 'sound/weapons/bladeslice.ogg'
|
||||
stat_attack = UNCONSCIOUS
|
||||
movement_type = FLYING
|
||||
robust_searching = 1
|
||||
|
||||
@@ -587,7 +587,7 @@
|
||||
var/turf/T = get_turf(client.eye)
|
||||
stat("Location:", COORD(T))
|
||||
stat("CPU:", "[world.cpu]")
|
||||
stat("Instances:", "[world.contents.len]")
|
||||
stat("Instances:", "[num2text(world.contents.len, 10)]")
|
||||
GLOB.stat_entry()
|
||||
config.stat_entry()
|
||||
stat(null)
|
||||
|
||||
@@ -170,6 +170,7 @@ By design, d1 is the smallest direction and d2 is the highest
|
||||
return 0
|
||||
|
||||
/obj/structure/cable/singularity_pull(S, current_size)
|
||||
..()
|
||||
if(current_size >= STAGE_FIVE)
|
||||
deconstruct()
|
||||
|
||||
|
||||
@@ -1072,7 +1072,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
|
||||
glass_desc = "Aromatic beverage served piping hot. According to folk tales it can almost wake the dead."
|
||||
|
||||
/datum/reagent/consumable/ethanol/hearty_punch/on_mob_life(mob/living/M)
|
||||
if(M.stat == UNCONSCIOUS && M.health <= 0)
|
||||
if(M.health <= 0)
|
||||
M.adjustBruteLoss(-7, 0)
|
||||
M.adjustFireLoss(-7, 0)
|
||||
M.adjustToxLoss(-7, 0)
|
||||
|
||||
@@ -335,6 +335,7 @@
|
||||
|
||||
|
||||
/obj/structure/disposalpipe/singularity_pull(S, current_size)
|
||||
..()
|
||||
if(current_size >= STAGE_FIVE)
|
||||
deconstruct()
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
return ..()
|
||||
|
||||
/obj/machinery/disposal/singularity_pull(S, current_size)
|
||||
..()
|
||||
if(current_size >= STAGE_FIVE)
|
||||
deconstruct()
|
||||
|
||||
@@ -335,20 +336,18 @@
|
||||
eject()
|
||||
. = TRUE
|
||||
|
||||
/obj/machinery/disposal/bin/CanPass(atom/movable/mover, turf/target)
|
||||
if (isitem(mover) && mover.throwing)
|
||||
var/obj/item/I = mover
|
||||
if(istype(I, /obj/item/projectile))
|
||||
return
|
||||
|
||||
/obj/machinery/disposal/bin/hitby(atom/movable/AM)
|
||||
if(isitem(AM) && AM.CanEnterDisposals())
|
||||
if(prob(75))
|
||||
I.forceMove(src)
|
||||
visible_message("<span class='notice'>[I] lands in [src].</span>")
|
||||
AM.forceMove(src)
|
||||
visible_message("<span class='notice'>[AM] lands in [src].</span>")
|
||||
update_icon()
|
||||
else
|
||||
visible_message("<span class='notice'>[I] bounces off of [src]'s rim!</span>")
|
||||
return 0
|
||||
visible_message("<span class='notice'>[AM] bounces off of [src]'s rim!</span>")
|
||||
return ..()
|
||||
else
|
||||
return ..(mover, target)
|
||||
return ..()
|
||||
|
||||
/obj/machinery/disposal/bin/flush()
|
||||
..()
|
||||
@@ -457,12 +456,12 @@
|
||||
trunk.linked = src // link the pipe trunk to self
|
||||
|
||||
/obj/machinery/disposal/deliveryChute/place_item_in_disposal(obj/item/I, mob/user)
|
||||
if(I.disposalEnterTry())
|
||||
if(I.CanEnterDisposals())
|
||||
..()
|
||||
flush()
|
||||
|
||||
/obj/machinery/disposal/deliveryChute/CollidedWith(atom/movable/AM) //Go straight into the chute
|
||||
if(!AM.disposalEnterTry())
|
||||
if(!AM.CanEnterDisposals())
|
||||
return
|
||||
switch(dir)
|
||||
if(NORTH)
|
||||
@@ -485,16 +484,16 @@
|
||||
M.forceMove(src)
|
||||
flush()
|
||||
|
||||
/atom/movable/proc/disposalEnterTry()
|
||||
/atom/movable/proc/CanEnterDisposals()
|
||||
return 1
|
||||
|
||||
/obj/item/projectile/disposalEnterTry()
|
||||
/obj/item/projectile/CanEnterDisposals()
|
||||
return
|
||||
|
||||
/obj/effect/disposalEnterTry()
|
||||
/obj/effect/CanEnterDisposals()
|
||||
return
|
||||
|
||||
/obj/mecha/disposalEnterTry()
|
||||
/obj/mecha/CanEnterDisposals()
|
||||
return
|
||||
|
||||
/obj/machinery/disposal/deliveryChute/newHolderDestination(obj/structure/disposalholder/H)
|
||||
|
||||
@@ -430,3 +430,23 @@
|
||||
materials = list(MAT_METAL = 500, MAT_GLASS = 500)
|
||||
build_path = /obj/item/organ/liver/cybernetic/upgraded
|
||||
category = list("Medical Designs")
|
||||
|
||||
/datum/design/cybernetic_lungs
|
||||
name = "Cybernetic Lungs"
|
||||
desc = "A pair of cybernetic lungs."
|
||||
id = "cybernetic_lungs"
|
||||
req_tech = list("biotech" = 4, "materials" = 4)
|
||||
build_type = PROTOLATHE
|
||||
materials = list(MAT_METAL = 500, MAT_GLASS = 500)
|
||||
build_path = /obj/item/organ/lungs/cybernetic
|
||||
category = list("Medical Designs")
|
||||
|
||||
/datum/design/cybernetic_lungs_u
|
||||
name = "Upgraded Cybernetic Lungs"
|
||||
desc = "A pair of upgraded cybernetic lungs."
|
||||
id = "cybernetic_lungs_u"
|
||||
req_tech = list("biotech" = 5, "materials" = 5, "engineering" = 5)
|
||||
build_type = PROTOLATHE
|
||||
materials = list(MAT_METAL = 500, MAT_GLASS = 500, MAT_SILVER = 500)
|
||||
build_path = /obj/item/organ/lungs/cybernetic/upgraded
|
||||
category = list("Medical Designs")
|
||||
@@ -46,6 +46,7 @@
|
||||
desc = "The incomplete body of a golem. Add ten sheets of any mineral to finish."
|
||||
var/shell_type = /obj/effect/mob_spawn/human/golem
|
||||
var/has_owner = FALSE //if the resulting golem obeys someone
|
||||
w_class = WEIGHT_CLASS_BULKY
|
||||
|
||||
/obj/item/golem_shell/attackby(obj/item/I, mob/user, params)
|
||||
..()
|
||||
|
||||
@@ -4,6 +4,10 @@ GLOBAL_PROTECT(reboot_mode)
|
||||
/world/proc/RunningService()
|
||||
return params[SERVICE_WORLD_PARAM]
|
||||
|
||||
/proc/ServiceVersion()
|
||||
if(world.RunningService())
|
||||
return world.params[SERVICE_VERSION_PARAM]
|
||||
|
||||
/world/proc/ExportService(command)
|
||||
return RunningService() && shell("python code/modules/server_tools/nudge.py \"[command]\"") == 0
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ All ShuttleMove procs go here
|
||||
// Called from the new turf before anything has been moved
|
||||
// Only gets called if fromShuttleMove returns true first
|
||||
// returns the new move_mode (based on the old)
|
||||
/turf/proc/toShuttleMove(turf/oldT, shuttle_dir, move_mode)
|
||||
/turf/proc/toShuttleMove(turf/oldT, move_mode, obj/docking_port/mobile/shuttle)
|
||||
var/shuttle_dir = shuttle.dir
|
||||
for(var/i in contents)
|
||||
var/atom/movable/thing = i
|
||||
if(ismob(thing))
|
||||
@@ -383,4 +384,4 @@ All ShuttleMove procs go here
|
||||
|
||||
/obj/effect/abstract/proximity_checker/onShuttleMove(turf/newT, turf/oldT, rotation, list/movement_force, move_dir, old_dock)
|
||||
//timer so it only happens once
|
||||
addtimer(CALLBACK(monitor, /datum/proximity_monitor/proc/SetRange, monitor.current_range, TRUE), 0, TIMER_UNIQUE)
|
||||
addtimer(CALLBACK(monitor, /datum/proximity_monitor/proc/SetRange, monitor.current_range, TRUE), 0, TIMER_UNIQUE)
|
||||
|
||||
@@ -570,7 +570,7 @@
|
||||
move_mode = moving_atom.beforeShuttleMove(newT, rotation, move_mode) //atoms
|
||||
|
||||
move_mode = oldT.fromShuttleMove(newT, underlying_turf_type, baseturf_cache, move_mode) //turfs
|
||||
move_mode = newT.toShuttleMove(oldT, dir, move_mode) //turfs
|
||||
move_mode = newT.toShuttleMove(oldT, move_mode , src) //turfs
|
||||
|
||||
if(move_mode & MOVE_AREA)
|
||||
areas_to_move[old_area] = TRUE
|
||||
|
||||
@@ -215,23 +215,37 @@
|
||||
/obj/effect/forcefield/luxury_shuttle
|
||||
var/threshold = 500
|
||||
var/static/list/approved_passengers = list()
|
||||
var/static/list/check_times = list()
|
||||
|
||||
/obj/effect/forcefield/luxury_shuttle/CanPass(atom/movable/mover, turf/target)
|
||||
if(mover in approved_passengers)
|
||||
return 1
|
||||
return TRUE
|
||||
|
||||
if(!isliving(mover)) //No stowaways
|
||||
return 0
|
||||
return FALSE
|
||||
|
||||
return FALSE
|
||||
|
||||
|
||||
#define LUXURY_MESSAGE_COOLDOWN 100
|
||||
/obj/effect/forcefield/luxury_shuttle/CollidedWith(atom/movable/AM)
|
||||
if(!isliving(AM))
|
||||
return ..()
|
||||
|
||||
if(check_times[AM] && check_times[AM] > world.time) //Let's not spam the message
|
||||
return ..()
|
||||
|
||||
check_times[AM] = world.time + LUXURY_MESSAGE_COOLDOWN
|
||||
|
||||
var/total_cash = 0
|
||||
var/list/counted_money = list()
|
||||
|
||||
for(var/obj/item/coin/C in mover.GetAllContents())
|
||||
for(var/obj/item/coin/C in AM.GetAllContents())
|
||||
total_cash += C.value
|
||||
counted_money += C
|
||||
if(total_cash >= threshold)
|
||||
break
|
||||
for(var/obj/item/stack/spacecash/S in mover.GetAllContents())
|
||||
for(var/obj/item/stack/spacecash/S in AM.GetAllContents())
|
||||
total_cash += S.value * S.amount
|
||||
counted_money += S
|
||||
if(total_cash >= threshold)
|
||||
@@ -241,12 +255,13 @@
|
||||
for(var/obj/I in counted_money)
|
||||
qdel(I)
|
||||
|
||||
to_chat(mover, "Thank you for your payment! Please enjoy your flight.")
|
||||
approved_passengers += mover
|
||||
return 1
|
||||
to_chat(AM, "Thank you for your payment! Please enjoy your flight.")
|
||||
approved_passengers += AM
|
||||
check_times -= AM
|
||||
return
|
||||
else
|
||||
to_chat(mover, "You don't have enough money to enter the main shuttle. You'll have to fly coach.")
|
||||
return 0
|
||||
to_chat(AM, "<span class='warning'>You don't have enough money to enter the main shuttle. You'll have to fly coach.</span>")
|
||||
return ..()
|
||||
|
||||
/mob/living/simple_animal/hostile/bear/fightpit
|
||||
name = "fight pit bear"
|
||||
|
||||
@@ -316,6 +316,29 @@
|
||||
safe_toxins_min = 16 //We breath THIS!
|
||||
safe_toxins_max = 0
|
||||
|
||||
/obj/item/organ/lungs/cybernetic
|
||||
name = "cybernetic lungs"
|
||||
desc = "A cybernetic version of the lungs found in traditional humanoid entities. It functions the same as an organic lung and is merely meant as a replacement."
|
||||
icon_state = "lungs-c"
|
||||
origin_tech = "biotech=4"
|
||||
|
||||
/obj/item/organ/lungs/cybernetic/emp_act()
|
||||
owner.losebreath = 20
|
||||
|
||||
|
||||
/obj/item/organ/lungs/cybernetic/upgraded
|
||||
name = "upgraded cybernetic lungs"
|
||||
desc = "A more advanced version of the stock cybernetic lungs. They are capable of filtering out lower levels of toxins and carbon-dioxide."
|
||||
icon_state = "lungs-c-u"
|
||||
origin_tech = "biotech=5"
|
||||
|
||||
safe_toxins_max = 20
|
||||
safe_co2_max = 20
|
||||
|
||||
cold_level_1_threshold = 200
|
||||
cold_level_2_threshold = 140
|
||||
cold_level_3_threshold = 100
|
||||
|
||||
#undef HUMAN_MAX_OXYLOSS
|
||||
#undef HUMAN_CRIT_MAX_OXYLOSS
|
||||
#undef HEAT_GAS_DAMAGE_LEVEL_1
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
say_mod = "hisses"
|
||||
taste_sensitivity = 10 // combined nose + tongue, extra sensitive
|
||||
|
||||
/*
|
||||
/obj/item/organ/tongue/lizard/TongueSpeech(var/message)
|
||||
var/regex/lizard_hiss = new("s+", "g")
|
||||
var/regex/lizard_hiSS = new("S+", "g")
|
||||
@@ -54,6 +55,7 @@
|
||||
message = lizard_hiss.Replace(message, "sss")
|
||||
message = lizard_hiSS.Replace(message, "SSS")
|
||||
return message
|
||||
*/
|
||||
|
||||
/obj/item/organ/tongue/fly
|
||||
name = "proboscis"
|
||||
|
||||
Reference in New Issue
Block a user