"
+ log_tts("Could not fetch TTS voices from configured endpoints", list(
+ "url" = CONFIG_GET(string/tts_http_url),
+ "status_code" = last_status_code,
+ "body_preview" = fallback_preview
+ ))
+ // MASSMETA EDIT END (ntts && /tg/tts)
return FALSE
- //MASSMETA EDIT BEGIN (/n/tts)
- //available_speakers = json_decode(response.body)
- var/list/temp_speakers = json_decode(response.body)?["voices"]
- for(var/speaker in temp_speakers)
- available_speakers.Add(speaker["speakers"][1])
- //MASSMETA EDIT END
tts_enabled = TRUE
if(CONFIG_GET(str_list/tts_voice_blacklist))
var/list/blacklisted_voices = CONFIG_GET(str_list/tts_voice_blacklist)
@@ -83,6 +179,20 @@ SUBSYSTEM_DEF(tts)
if(available_speakers.Find(voice))
log_config("Removed speaker [voice] from the TTS voice pool per config.")
available_speakers.Remove(voice)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ voice_categories.Remove(voice)
+ // MASSMETA EDIT END (ntts && /tg/tts)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ refresh_voice_pools()
+ // MASSMETA EDIT END (ntts && /tg/tts)
+ if(CONFIG_GET(string/tts_tram_announcer_override))
+ tram_voice = CONFIG_GET(string/tts_tram_announcer_override)
+ else
+ tram_voice = pick(available_speakers)
+ if(CONFIG_GET(string/tts_computer_voice_override))
+ computer_voice = CONFIG_GET(string/tts_computer_voice_override)
+ else
+ computer_voice = pick(available_speakers)
var/datum/http_request/request_pitch = new()
var/list/headers_pitch = list()
headers_pitch["Authorization"] = CONFIG_GET(string/tts_http_token)
@@ -92,8 +202,12 @@ SUBSYSTEM_DEF(tts)
pitch_enabled = TRUE
var/datum/http_response/response_pitch = request_pitch.into_response()
if(response_pitch.errored || response_pitch.status_code != 200)
+
if(response_pitch.errored)
- stack_trace(response.error)
+ // MASSMETA EDIT START (ntts && /tg/tts) \
+ ORIGINAL: stack_trace(response.error)
+ stack_trace(response_pitch.error)
+ // MASSMETA EDIT END (ntts && /tg/tts)
pitch_enabled = FALSE
rustg_file_write(json_encode(available_speakers), "data/cached_tts_voices.json")
rustg_file_write("rustg HTTP requests can't write to folders that don't exist, so we need to make it exist.", "tmp/tts/init.txt")
@@ -109,32 +223,50 @@ SUBSYSTEM_DEF(tts)
return SS_INIT_FAILURE
return SS_INIT_SUCCESS
-/datum/controller/subsystem/tts/proc/play_tts(target, list/listeners, sound/audio, sound/audio_blips, datum/language/language, range = 7, volume_offset = 0)
+/datum/controller/subsystem/tts/proc/play_tts(target, list/listeners, sound/audio, sound/audio_blips, datum/language/language, range = 7, volume_offset = 0, ignore_observers = FALSE, source_speaker = null, audio_length = 10 SECONDS, audio_length_blips = 10 SECONDS, volume_preference = /datum/preference/numeric/volume/sound_tts_volume, volume_signal = COMSIG_MOB_TTS_VOLUME_PREFERENCE_APPLIED)
var/turf/turf_source = get_turf(target)
- if(!turf_source)
+ if(!turf_source && target) // if there's a target, we better have a turf
return
var/channel = SSsounds.random_available_channel()
- for(var/atom/movable/hearer in listeners | SSmobs.dead_players_by_zlevel[turf_source.z])//observers always hear through walls
- var/mob/listening_mob = hearer.get_listening_mob()
- if(isnull(listening_mob))
+ var/list/final_listeners = listeners
+ if(!ignore_observers && target)
+ final_listeners += SSmobs.dead_players_by_zlevel[turf_source.z] //observers always hear through walls
+ var/list/blips_hearers = list()
+ var/list/voice_hearers = list()
+ for(var/hearer in final_listeners)
+ if(isnull(hearer))
continue
- if(QDELING(listening_mob))
+ var/atom/movable/hearer_atom = hearer
+ if(QDELING(hearer_atom))
stack_trace("TTS tried to play a sound to a deleted mob.")
continue
+ if(!ismob(hearer_atom))
+ continue
+ var/mob/listening_mob = hearer_atom.get_listening_mob()
/// volume modifier for TTS as set by the player in preferences.
- var/volume_modifier = listening_mob.client?.prefs.read_preference(/datum/preference/numeric/volume/sound_tts_volume)/100
+ var/volume_modifier = listening_mob.client?.prefs.read_preference(volume_preference)/100
var/tts_pref = listening_mob.client?.prefs.read_preference(/datum/preference/choiced/sound_tts)
+ var/hear_self_pref = listening_mob.client?.prefs.read_preference(/datum/preference/toggle/sound_tts_hear_self_radio)
if(volume_modifier == 0 || (tts_pref == TTS_SOUND_OFF))
continue
+ if(listening_mob == source_speaker && !hear_self_pref)
+ continue // don't hear your own radio tts if you got it turned off
var/sound_volume = ((hearer == target)? 60 : 85) + volume_offset
sound_volume = sound_volume*volume_modifier
var/datum/language_holder/holder = listening_mob.get_language_holder()
- var/audio_to_use = (tts_pref == TTS_SOUND_BLIPS) ? audio_blips : audio
+ var/sound/audio_to_use = (tts_pref == TTS_SOUND_BLIPS) ? audio_blips : audio
if(!holder.has_language(language))
- continue
- if(get_dist(hearer, turf_source) <= range)
+ if (tts_pref == TTS_SOUND_OFF)
+ continue
+ else
+ audio_to_use = audio_blips
+ if(target && get_dist(hearer, turf_source) <= range)
+ if(tts_pref == TTS_SOUND_BLIPS || !holder.has_language(language))
+ blips_hearers += listening_mob
+ else
+ voice_hearers += listening_mob
listening_mob.playsound_local(
turf_source,
vol = sound_volume,
@@ -147,14 +279,67 @@ SUBSYSTEM_DEF(tts)
distance_multiplier = 1,
use_reverb = TRUE
)
+ else if(!target)
+ listening_mob.playsound_local(
+ null, //play it locally
+ vol = sound_volume,
+ falloff_exponent = SOUND_FALLOFF_EXPONENT,
+ channel = channel,
+ pressure_affected = FALSE,
+ sound_to_use = audio_to_use,
+ max_distance = SOUND_RANGE,
+ falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE,
+ distance_multiplier = 1,
+ use_reverb = TRUE
+ )
+ if(target)
+ new /datum/threed_sound(
+ target,
+ audio,
+ voice_hearers,
+ FALSE,
+ 85 + volume_offset,
+ SOUND_RANGE,
+ audio_length,
+ channel,
+ volume_preference,
+ volume_signal
+ )
+ new /datum/threed_sound(
+ target,
+ audio_blips,
+ blips_hearers,
+ FALSE,
+ 85 + volume_offset,
+ SOUND_RANGE,
+ audio_length_blips,
+ channel,
+ volume_preference,
+ volume_signal
+ )
+
// Need to wait for all HTTP requests to complete here because of a rustg crash bug that causes crashes when dd restarts whilst HTTP requests are ongoing.
/datum/controller/subsystem/tts/Shutdown()
tts_enabled = FALSE
for(var/datum/tts_request/data in in_process_http_messages)
var/datum/http_request/request = data.request
- var/datum/http_request/request_blips = data.request_blips
- UNTIL(request.is_complete() && request_blips.is_complete())
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ if(data.announcement)
+ UNTIL(request.is_complete())
+ else if(data.local)
+ var/datum/http_request/request_blips = data.request_blips
+ if(data.use_blips || data.force_blips)
+ UNTIL(request_blips.is_complete())
+ else
+ UNTIL(request.is_complete())
+ else
+ var/datum/http_request/request_blips = data.request_blips
+ var/datum/http_request/request_radio = data.request_radio
+ var/datum/http_request/request_blips_radio = data.request_blips_radio
+ var/datum/http_request/request_radio_gibberish = data.request_radio_gibberish
+ UNTIL(request.is_complete() && request_blips.is_complete() && request_radio.is_complete() && request_blips_radio.is_complete() && (!request_radio_gibberish || request_radio_gibberish.is_complete()))
+ // MASSMETA EDIT END (ntts && /tg/tts)
#define SHIFT_DATA_ARRAY(tts_message_queue, target, data) \
popleft(##data); \
@@ -189,27 +374,79 @@ SUBSYSTEM_DEF(tts)
if(!current_request.requests_completed())
continue
- var/datum/http_response/response = current_request.get_primary_response()
in_process_http_messages -= current_request
average_tts_messages_time = MC_AVERAGE(average_tts_messages_time, world.time - current_request.start_time)
var/identifier = current_request.identifier
+ var/datum/http_response/normal_response = current_request.request.into_response()
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ var/datum/http_response/blips_response = normal_response
+ var/datum/http_response/radio_response = normal_response
+ var/datum/http_response/radio_blips_response = normal_response
+ var/datum/http_response/radio_gibberish_response = normal_response
+ if(!current_request.announcement)
+ blips_response = current_request.request_blips.into_response()
+ radio_response = current_request.request_radio.into_response()
+ radio_blips_response = current_request.request_blips_radio.into_response()
+ if(current_request.request_radio_gibberish)
+ radio_gibberish_response = current_request.request_radio_gibberish.into_response()
+ // MASSMETA EDIT END (ntts && /tg/tts)
if(current_request.requests_errored())
+ if(queued_radio_messages[identifier])
+ queued_radio_messages.Remove(identifier)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ queued_radio_messages_compression.Remove(identifier)
+ // MASSMETA EDIT END (ntts && /tg/tts)
current_request.timed_out = TRUE
- var/datum/http_response/normal_response = current_request.request.into_response()
- var/datum/http_response/blips_response = current_request.request_blips.into_response()
- log_tts("TTS HTTP request errored | Normal: [normal_response.error] | Blips: [blips_response.error]", list(
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ log_tts("TTS HTTP request errored | Normal: [normal_response.status_code] [normal_response.error] | Blips: [blips_response.status_code] [blips_response.error] | Radio: [radio_response.status_code] [radio_response.error] | Radio Blips: [radio_blips_response.status_code] [radio_blips_response.error] | Radio Gibberish: [radio_gibberish_response.status_code] [radio_gibberish_response.error]", list(
"normal" = normal_response,
- "blips" = blips_response
+ "blips" = blips_response,
+ "radio" = radio_response,
+ "radio_blips" = radio_blips_response,
+ "radio_gibberish" = radio_gibberish_response
))
+ // MASSMETA EDIT END (ntts && /tg/tts)
continue
- current_request.audio_length = text2num(response.headers["audio-length"]) * 10
- if(!current_request.audio_length)
- current_request.audio_length = 0
+ if(length(normal_response.headers) && normal_response.headers.Find("audio-length"))
+ current_request.audio_length = text2num(normal_response.headers["audio-length"]) * 10
+ if(!current_request.audio_length)
+ current_request.audio_length = 0
+
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ if(!current_request.announcement && length(blips_response.headers) && blips_response.headers.Find("audio-length"))
+ current_request.audio_length_blips = text2num(blips_response.headers["audio-length"]) * 10
+ if(!current_request.audio_length_blips)
+ current_request.audio_length_blips = 0
+ if(!current_request.announcement)
+ if(length(radio_response.headers) && radio_response.headers.Find("audio-length"))
+ current_request.audio_length_radio = text2num(radio_response.headers["audio-length"]) * 10
+ if(!current_request.audio_length_radio)
+ current_request.audio_length_radio = 0
+ if(length(radio_blips_response.headers) && radio_blips_response.headers.Find("audio-length"))
+ current_request.audio_length_blips_radio = text2num(radio_blips_response.headers["audio-length"]) * 10
+ if(!current_request.audio_length_blips_radio)
+ current_request.audio_length_blips_radio = 0
+ if(current_request.request_radio_gibberish && length(radio_gibberish_response.headers) && radio_gibberish_response.headers.Find("audio-length"))
+ current_request.audio_length_radio_gibberish = text2num(radio_gibberish_response.headers["audio-length"]) * 10
+ if(!current_request.audio_length_radio_gibberish)
+ current_request.audio_length_radio_gibberish = 0
+ // MASSMETA EDIT END (ntts && /tg/tts)
current_request.audio_file = "tmp/tts/[identifier].ogg"
- current_request.audio_file_blips = "tmp/tts/[identifier]_blips.ogg" // We aren't as concerned about the audio length for blips as we are with actual speech
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ if(!current_request.announcement)
+ current_request.audio_file_blips = "tmp/tts/[identifier]_blips.ogg" // We aren't as concerned about the audio length for blips as we are with actual speech
+ current_request.audio_file_radio = "tmp/tts/[identifier]_radio.ogg"
+ current_request.audio_file_blips_radio = "tmp/tts/[identifier]_blips_radio.ogg"
+ current_request.audio_file_radio_gibberish = current_request.request_radio_gibberish ? "tmp/tts/[identifier]_radio_gibberish.ogg" : current_request.audio_file_radio
+ // MASSMETA EDIT END (ntts && /tg/tts)
// Don't need the request anymore so we can deallocate it
current_request.request = null
current_request.request_blips = null
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ current_request.request_radio = null
+ current_request.request_blips_radio = null
+ // MASSMETA EDIT END (ntts && /tg/tts)
+ current_request.request_radio_gibberish = null
if(MC_TICK_CHECK)
return
@@ -253,8 +490,24 @@ SUBSYSTEM_DEF(tts)
continue
var/sound/audio_file
var/sound/audio_file_blips
- if(current_target.local)
- if(current_target.use_blips)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ if(current_target.announcement)
+ if(current_target.when_to_play > world.time)
+ continue
+ current_target.play_announcement()
+ var/announcement_length = current_target.audio_length || 0
+ if(length(data) != 1)
+ var/datum/tts_request/next_target = data[2]
+ next_target.when_to_play = world.time + announcement_length
+ else
+ var/datum/tts_request/arbritrary_delay = new()
+ arbritrary_delay.when_to_play = world.time + announcement_length
+ arbritrary_delay.audio_file = TTS_ARBRITRARY_DELAY
+ queued_tts_messages[tts_target] += arbritrary_delay
+ SHIFT_DATA_ARRAY(queued_tts_messages, tts_target, data)
+ else if(current_target.local)
+ // MASSMETA EDIT END (ntts && /tg/tts)
+ if(current_target.use_blips || current_target.force_blips)
audio_file_blips = new(current_target.audio_file_blips)
SEND_SOUND(current_target.target, audio_file_blips)
else
@@ -263,8 +516,11 @@ SUBSYSTEM_DEF(tts)
SHIFT_DATA_ARRAY(queued_tts_messages, tts_target, data)
else if(current_target.when_to_play < world.time)
audio_file = new(current_target.audio_file)
+ // MASSMETA EDIT START (ntts && /tg/tts)
audio_file_blips = new(current_target.audio_file_blips)
- play_tts(tts_target, current_target.listeners, audio_file, audio_file_blips, current_target.language, current_target.message_range, current_target.volume_offset)
+ play_tts(tts_target, current_target.listeners, audio_file, audio_file_blips, current_target.language, current_target.message_range, current_target.volume_offset, FALSE, null, current_target.audio_length, current_target.audio_length_blips)
+ completed_tts_messages[current_target.identifier] = list("ref" = current_target, "expiry_time" = world.time + 300)
+ // MASSMETA EDIT END (ntts && /tg/tts)
if(length(data) != 1)
var/datum/tts_request/next_target = data[2]
next_target.when_to_play = world.time + current_target.audio_length
@@ -277,10 +533,43 @@ SUBSYSTEM_DEF(tts)
queued_tts_messages[tts_target] += arbritrary_delay
SHIFT_DATA_ARRAY(queued_tts_messages, tts_target, data)
+ for(var/identifier in queued_radio_messages)
+ if(MC_TICK_CHECK)
+ return
+ if(completed_tts_messages[identifier])
+ var/list/all_radios = queued_radio_messages[identifier]
+ for(var/radio in all_radios)
+ var/list/hearers = all_radios[radio]
+ if(!istext(radio))
+ var/obj/radio_obj = radio
+ if(QDELETED(radio_obj))
+ queued_radio_messages[identifier].Remove(radio)
+ continue
+
+ var/datum/tts_request/tts_request = completed_tts_messages[identifier]["ref"]
+ var/sound/audio_file
+ var/sound/audio_file_blips
+ if(queued_radio_messages_compression[identifier] > 30)
+ audio_file = new(tts_request.audio_file_radio_gibberish || tts_request.audio_file_radio)
+ else
+ audio_file = new(tts_request.audio_file_radio)
+ audio_file_blips = new(tts_request.audio_file_blips_radio)
+ play_tts(radio == TTS_GHOST_RADIO ? null : radio, hearers, audio_file, audio_file_blips, tts_request.language, INFINITY, tts_request.volume_offset, ignore_observers = TRUE, source_speaker = tts_request.target, audio_length = tts_request.audio_length_radio, audio_length_blips = tts_request.audio_length_blips_radio, volume_preference = /datum/preference/numeric/volume/sound_tts_radio_volume, volume_signal = COMSIG_MOB_TTS_RADIO_VOLUME_PREFERENCE_APPLIED)
+ queued_radio_messages.Remove(identifier)
+ completed_tts_messages.Remove(identifier)
+ queued_radio_messages_compression.Remove(identifier)
+
+ for(var/identifier, request in completed_tts_messages)
+ if(MC_TICK_CHECK)
+ return
+ if (completed_tts_messages[identifier]["expiry_time"] >= world.time + 300)
+ completed_tts_messages[identifier]["ref"] = null
+ completed_tts_messages[identifier] = null
+ completed_tts_messages.Remove(identifier)
#undef TTS_ARBRITRARY_DELAY
-/datum/controller/subsystem/tts/proc/queue_tts_message(datum/target, message, datum/language/language, speaker, filter, list/listeners, local = FALSE, message_range = 7, volume_offset = 0, pitch = 0, special_filters = "")
+/datum/controller/subsystem/tts/proc/queue_tts_message(datum/target, message, datum/language/language, speaker, filter, list/listeners, local = FALSE, message_range = 7, volume_offset = 0, pitch = 0, special_filters = "", blip_base = "male", blip_number = "1", force_blips = FALSE, identifier = "invalid", announcement_effect = "")
if(!tts_enabled)
return
@@ -288,19 +577,13 @@ SUBSYSTEM_DEF(tts)
if(!fexists("tmp/tts/init.txt"))
rustg_file_write("rustg HTTP requests can't write to folders that don't exist, so we need to make it exist.", "tmp/tts/init.txt")
- //MASSMETA EDIT BEGIN (/n/tts)
- //var/static/regex/contains_alphanumeric = regex("\[a-zA-Z0-9]")
- //// If there is no alphanumeric char, the output will usually be static, so
- //// don't bother sending
- //if(contains_alphanumeric.Find(message) == 0)
- // return
- //
- //var/shell_scrubbed_input = tts_speech_filter(message)
-
- var/shell_scrubbed_input = message
- //MASSMETA EDIT END
- shell_scrubbed_input = copytext(shell_scrubbed_input, 1, 300)
- var/identifier = "[sha1(speaker + filter + num2text(pitch) + special_filters + shell_scrubbed_input)].[world.time]"
+ var/static/regex/contains_alphanumeric = regex("\[а-яА-ЯёЁa-zA-Z0-9]")
+ // If there is no alphanumeric char, the output will usually be static, so
+ // don't bother sending
+ if(contains_alphanumeric.Find(message) == 0)
+ return
+
+ var/shell_scrubbed_input = tts_speech_filter(message)
if(!(speaker in available_speakers))
return
@@ -308,18 +591,24 @@ SUBSYSTEM_DEF(tts)
headers["Content-Type"] = "application/json"
headers["Authorization"] = CONFIG_GET(string/tts_http_token)
var/datum/http_request/request = new()
+ // MASSMETA EDIT START (ntts && /tg/tts)
var/datum/http_request/request_blips = new()
+ var/datum/http_request/request_radio = new()
+ var/datum/http_request/request_blips_radio = new()
+ // MASSMETA EDIT END (ntts && /tg/tts)
+ var/datum/http_request/request_radio_gibberish
var/file_name = "tmp/tts/[identifier].ogg"
var/file_name_blips = "tmp/tts/[identifier]_blips.ogg"
- //MASSMETA EDIT BEGIN (/n/tts)
- //request.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts?voice=[speaker]&identifier=[identifier]&filter=[url_encode(filter)]&pitch=[pitch]&special_filters=[url_encode(special_filters)]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name, timeout_seconds = CONFIG_GET(number/tts_http_timeout_seconds))
- //request_blips.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts-blips?voice=[speaker]&identifier=[identifier]&filter=[url_encode(filter)]&pitch=[pitch]&special_filters=[url_encode(special_filters)]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name_blips, timeout_seconds = CONFIG_GET(number/tts_http_timeout_seconds))
-
-
- request.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/?speaker=[speaker]&effect=[url_encode(special_filters)]&ext=ogg&text=[shell_scrubbed_input]", null, headers, file_name, timeout_seconds = CONFIG_GET(number/tts_http_timeout_seconds))
- request_blips.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/?speaker=[speaker]&effect=[url_encode(special_filters)]&ext=ogg&text=[shell_scrubbed_input]", null, headers, file_name_blips, timeout_seconds = CONFIG_GET(number/tts_http_timeout_seconds))
- //MASSMETA EDIT END
- var/datum/tts_request/current_request = new /datum/tts_request(identifier, request, request_blips, shell_scrubbed_input, target, local, language, message_range, volume_offset, listeners, pitch)
+ var/file_name_radio = "tmp/tts/[identifier]_radio.ogg"
+ var/file_name_blips_radio = "tmp/tts/[identifier]_blips_radio.ogg"
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ var/announcement_effect_param = announcement_effect ? "&announcement_effect=[url_encode(announcement_effect)]" : ""
+ request.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts?voice=[speaker]&identifier=[identifier]&filter=[tts_filter_encode(filter, speaker, pitch)]&pitch=[pitch]&special_filters=[url_encode(special_filters)][announcement_effect_param]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name, timeout_seconds = CONFIG_GET(number/tts_http_timeout_seconds))
+ request_blips.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts-blips?voice=[speaker]&identifier=[identifier]&filter=[tts_filter_encode(filter, speaker, pitch, blips = TRUE)]&pitch=[pitch]&special_filters=[url_encode(special_filters)]&blip_base=[blip_base]&blip_number=[blip_number]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name_blips, timeout_seconds = CONFIG_GET(number/tts_http_timeout_seconds))
+ request_radio.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts-radio?voice=[speaker]&identifier=[identifier]&filter=[tts_filter_encode(filter, speaker, pitch)]&pitch=[pitch]&special_filters=[url_encode(special_filters)][announcement_effect_param]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name_radio, timeout_seconds = CONFIG_GET(number/tts_http_timeout_seconds))
+ request_blips_radio.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts-blips-radio?voice=[speaker]&identifier=[identifier]&filter=[tts_filter_encode(filter, speaker, pitch, blips = TRUE)]&pitch=[pitch]&special_filters=[url_encode(special_filters)]&blip_base=[blip_base]&blip_number=[blip_number]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name_blips_radio, timeout_seconds = CONFIG_GET(number/tts_http_timeout_seconds))
+ var/datum/tts_request/current_request = new /datum/tts_request(identifier, request, request_blips, request_radio, request_blips_radio, request_radio_gibberish, shell_scrubbed_input, target, local, language, message_range, volume_offset, listeners, pitch, force_blips)
+ // MASSMETA EDIT END (ntts && /tg/tts)
var/list/player_queued_tts_messages = queued_tts_messages[target]
if(!player_queued_tts_messages)
player_queued_tts_messages = list()
@@ -331,6 +620,24 @@ SUBSYSTEM_DEF(tts)
else
queued_http_messages.insert(current_request)
+/// Helper to get a random TTS voice for a certain gender. Passing no gender just results in a random voice.
+/datum/controller/subsystem/tts/proc/random_tts_voice(gender = NEUTER)
+ if(!tts_enabled)
+ return null
+
+ var/sanity = 0
+ while(sanity < 10)
+ var/voice = pick(available_speakers)
+ if(gender != MALE && gender != FEMALE)
+ return voice
+ if(gender == MALE && findtext(voice, "Man"))
+ return voice
+ if(gender == FEMALE && findtext(voice, "Woman"))
+ return voice
+ sanity += 1
+
+ return pick(available_speakers) // failsafe
+
/// A struct containing information on an individual player or mob who has made a TTS request
/datum/tts_request
/// The mob to play this TTS message on
@@ -342,6 +649,12 @@ SUBSYSTEM_DEF(tts)
var/datum/http_request/request
/// The HTTP request of this message for blips
var/datum/http_request/request_blips
+ /// The HTTP request of this message's radio version
+ var/datum/http_request/request_radio
+ /// The HTTP request of this blip message's radio version
+ var/datum/http_request/request_blips_radio
+ /// The HTTP request of this message's radio gibberish version
+ var/datum/http_request/request_radio_gibberish
/// The language to limit this TTS message to
var/datum/language/language
/// The message itself
@@ -361,8 +674,18 @@ SUBSYSTEM_DEF(tts)
var/sound/audio_file
/// The blips audio file of this tts request.
var/sound/audio_file_blips
+ /// The radio audio file of this tts request.
+ var/sound/audio_file_radio
+ /// The blips radio audio file of this tts request.
+ var/sound/audio_file_blips_radio
+ /// The gibberish radio audio file of this tts request.
+ var/sound/audio_file_radio_gibberish
/// The audio length of this tts request.
var/audio_length
+ var/audio_length_blips
+ var/audio_length_radio
+ var/audio_length_blips_radio
+ var/audio_length_radio_gibberish
/// When the audio file should play at the minimum
var/when_to_play = 0
/// Whether this request was timed out or not
@@ -371,13 +694,19 @@ SUBSYSTEM_DEF(tts)
var/use_blips = FALSE
/// What's the pitch adjustment?
var/pitch = 0
+ /// Should we force play blips? Used for the blips preview.
+ var/force_blips = FALSE
-
-/datum/tts_request/New(identifier, datum/http_request/request, datum/http_request/request_blips, message, target, local, datum/language/language, message_range, volume_offset, list/listeners, pitch)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+/datum/tts_request/New(identifier, datum/http_request/request, datum/http_request/request_blips, datum/http_request/request_radio, datum/http_request/request_blips_radio, datum/http_request/request_radio_gibberish, message, target, local, datum/language/language, message_range, volume_offset, list/listeners, pitch, force_blips = FALSE)
+ // MASSMETA EDIT END (ntts && /tg/tts)
. = ..()
src.identifier = identifier
src.request = request
src.request_blips = request_blips
+ src.request_radio = request_radio
+ src.request_blips_radio = request_blips_radio
+ src.request_radio_gibberish = request_radio_gibberish
src.message = message
src.language = language
src.target = target
@@ -386,6 +715,7 @@ SUBSYSTEM_DEF(tts)
src.volume_offset = volume_offset
src.listeners = listeners
src.pitch = pitch
+ src.force_blips = force_blips
start_time = world.time
/datum/tts_request/proc/start_requests()
@@ -394,18 +724,27 @@ SUBSYSTEM_DEF(tts)
use_blips = (current_client?.prefs.read_preference(/datum/preference/choiced/sound_tts) == TTS_SOUND_BLIPS)
else if(istype(target, /mob))
use_blips = (target.client?.prefs.read_preference(/datum/preference/choiced/sound_tts) == TTS_SOUND_BLIPS)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ if(announcement)
+ start_announcement_requests()
+ return
+ // MASSMETA EDIT END (ntts && /tg/tts)
if(local)
- if(use_blips)
+ if(use_blips || force_blips)
request_blips.begin_async()
else
request.begin_async()
else
request.begin_async()
request_blips.begin_async()
+ request_radio.begin_async()
+ request_blips_radio.begin_async()
+ if(request_radio_gibberish)
+ request_radio_gibberish.begin_async()
/datum/tts_request/proc/get_primary_request()
if(local)
- if(use_blips)
+ if(use_blips || force_blips)
return request_blips
else
return request
@@ -414,7 +753,7 @@ SUBSYSTEM_DEF(tts)
/datum/tts_request/proc/get_primary_response()
if(local)
- if(use_blips)
+ if(use_blips || force_blips)
return request_blips.into_response()
else
return request.into_response()
@@ -422,38 +761,77 @@ SUBSYSTEM_DEF(tts)
return request.into_response()
/datum/tts_request/proc/requests_errored()
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ if(announcement)
+ return announcement_requests_errored()
+ // MASSMETA EDIT END (ntts && /tg/tts)
if(local)
var/datum/http_response/response
- if(use_blips)
+ if(use_blips || force_blips)
response = request_blips.into_response()
else
response = request.into_response()
- return response.errored
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ return response.errored || response.status_code != 200 // here too
+ // MASSMETA EDIT END (ntts && /tg/tts)
else
var/datum/http_response/response = request.into_response()
+ var/datum/http_response/response_radio = request_radio.into_response()
+ // MASSMETA EDIT START (ntts && /tg/tts)
var/datum/http_response/response_blips = request_blips.into_response()
- return response.errored || response_blips.errored
+ var/datum/http_response/response_blips_radio = request_blips_radio.into_response()
+ return response.errored || response.status_code != 200 || response_blips.errored || response_blips.status_code != 200 || response_radio.errored || response_radio.status_code != 200 || response_blips_radio.errored || response_blips_radio.status_code != 200
+ // MASSMETA EDIT END (ntts && /tg/tts)
/datum/tts_request/proc/requests_completed()
+// MASSMETA EDIT START (ntts && /tg/tts)
+ if(announcement)
+ return announcement_requests_completed()
+// MASSMETA EDIT END (ntts && /tg/tts)
if(local)
- if(use_blips)
+ if(use_blips || force_blips)
return request_blips.is_complete()
else
return request.is_complete()
else
- return request.is_complete() && request_blips.is_complete()
+ return request.is_complete() && request_blips.is_complete() && request_blips_radio.is_complete() && request_radio.is_complete()
+
+/proc/filter_tts_listeners(list/listeners, radio_frequency = null)
+ if(!SStts.tts_enabled || !listeners)
+ return
+
+ if(ismob(listeners))
+ listeners = list(listeners)
+ var/list/filtered_listeners = list()
+
+ for(var/mob/listener as anything in listeners)
+ if(!ismob(listener) || !listener.client)
+ continue
+ var/tts_pref = listener.client?.prefs.read_preference(/datum/preference/choiced/sound_tts)
+ var/radio_tts_pref = listener.client?.prefs.read_preference(/datum/preference/choiced/sound_tts_radio)
+ if(tts_pref == TTS_SOUND_OFF)
+ continue
+ if(isliving(listener) && (listener.stat >= UNCONSCIOUS || HAS_TRAIT(listener, TRAIT_DEAF)))
+ continue
+ if(radio_tts_pref == TTS_SOUND_NO_RADIO)
+ continue
+ if(radio_tts_pref == TTS_SOUND_DEPARTMENTAL_RADIO && radio_frequency == FREQ_COMMON) // don't give them the full common firehose if they turned it off
+ continue
+ filtered_listeners += listener
+
+ return filtered_listeners
#undef SHIFT_DATA_ARRAY
// MASSMETA ADD BEGIN
/// Helper to get a random TTS voice for a vendor
-/datum/controller/subsystem/tts/proc/radnom_vendor_voice()
+/datum/controller/subsystem/tts/proc/random_vendor_voice()
if(!tts_enabled)
return null
for (var/voice in available_speakers)
- if (findtext(voice, "Vendor"))
+ if (findtext(voice, "Glados")) // remind me to make it configurable via config, hardcode is bad.
return voice
CRASH("Cant find voice for vendor! At least one voice must me for vendors")
return pick(available_speakers)
diff --git a/code/datums/3d_sounds/_3d_sound.dm b/code/datums/3d_sounds/_3d_sound.dm
new file mode 100644
index 000000000000..e3c7530f1f1c
--- /dev/null
+++ b/code/datums/3d_sounds/_3d_sound.dm
@@ -0,0 +1,355 @@
+/// The mob is deaf
+#define MUTE_DEAF (1<<0)
+/// The mob is out of range of the sound
+#define MUTE_RANGE (1<<1)
+/// The mob muted their volume preference for this sound
+#define MUTE_VOLUME (1<<1)
+
+/datum/threed_sound
+ var/atom/parent
+ var/sound/our_sound
+ var/sound_path
+ var/list/mob/starting_listeners
+ var/can_add_new_listeners = TRUE
+ var/list/mob/listeners = list()
+ var/volume = 50
+ var/sound_range
+ var/x_cutoff
+ var/z_cutoff
+ var/our_channel
+ var/sound_length = 5 SECONDS
+ var/deletion_timer
+ var/preference_volume
+ var/preference_signal
+ var/falloff_distance
+ var/falloff_exponent
+ var/pressure_affected = TRUE
+
+/datum/threed_sound/New(atom/new_parent, sound/new_sound, list/current_listeners, can_add_new_listeners = FALSE, volume = 50, sound_range = SOUND_RANGE, sound_length = 5 SECONDS, channel, preference_volume, preference_signal, falloff_exponent = SOUND_FALLOFF_EXPONENT, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, pressure_affected = TRUE)
+ if(!ismovable(new_parent) && !isturf(new_parent))
+ stack_trace("[type] created on non-turf or non-movable: [new_parent ? "[new_parent] ([new_parent.type])" : "null"])")
+ qdel(src)
+ return
+
+ parent = new_parent
+ our_sound = new_sound
+ src.can_add_new_listeners = can_add_new_listeners
+ src.volume = volume
+ src.sound_range = sound_range
+ src.sound_length = sound_length
+ our_channel = channel
+ src.preference_volume = preference_volume
+ src.preference_signal = preference_signal
+ src.falloff_distance = falloff_distance
+ src.falloff_exponent = falloff_exponent
+ src.pressure_affected = pressure_affected
+
+ if(isnull(sound_range))
+ src.sound_range = world.view
+ var/list/worldviewsize = getviewsize(src.sound_range)
+ x_cutoff = ceil(worldviewsize[1] / 2)
+ z_cutoff = ceil(worldviewsize[2] / 2)
+ for(var/listener in current_listeners)
+ if(!ismob(listener))
+ current_listeners -= current_listeners
+ continue
+ register_listener(listener)
+ starting_listeners = current_listeners
+
+ RegisterSignal(parent, COMSIG_ENTER_AREA, PROC_REF(on_enter_area))
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
+ RegisterSignal(parent, COMSIG_QDELETING, PROC_REF(parent_delete))
+ deletion_timer = addtimer(CALLBACK(src, PROC_REF(delete_self)), sound_length, TIMER_STOPPABLE | TIMER_DELETE_ME)
+
+/datum/threed_sound/proc/delete_self()
+ qdel(src)
+
+/datum/threed_sound/Destroy()
+ unlisten_all()
+ deltimer(deletion_timer)
+ parent = null
+ our_sound = null
+ return ..()
+
+/datum/threed_sound/proc/parent_delete(datum/source)
+ SIGNAL_HANDLER
+ qdel(src)
+
+/**
+ * Sets the sound's range to a new value. This can be a number or a view size string "XxY".
+ * Then updates any mobs listening to it.
+ */
+/datum/threed_sound/proc/set_sound_range(new_range)
+ if(sound_range == new_range)
+ return
+ sound_range = new_range
+ var/list/worldviewsize = getviewsize(sound_range)
+ x_cutoff = ceil(worldviewsize[1] / 2)
+ z_cutoff = ceil(worldviewsize[2] / 2)
+ update_all()
+
+/**
+ * Sets the sound's environment to a new value.
+ * Then updates any mobs listening to it.
+ */
+/datum/threed_sound/proc/set_new_environment(new_env)
+ if(!our_sound || our_sound.environment == new_env)
+ return
+ our_sound.environment = new_env
+ update_all()
+
+/datum/threed_sound/proc/unlisten_all()
+ for(var/mob/listening as anything in listeners)
+ deregister_listener(listening)
+ our_sound = null
+
+/datum/threed_sound/proc/update_all()
+ for(var/mob/listening as anything in listeners)
+ update_listener(listening)
+
+/datum/threed_sound/proc/start_music()
+ for(var/mob/nearby in hearers(sound_range, parent))
+ register_listener(nearby)
+
+/datum/threed_sound/proc/get_active_listeners()
+ var/list/all_listeners = list()
+ for(var/mob/listener as anything in listeners)
+ if(listeners[listener] & SOUND_MUTE)
+ continue
+ all_listeners += listener
+ return all_listeners
+
+/datum/threed_sound/proc/register_listener(mob/new_listener)
+ PROTECTED_PROC(TRUE)
+
+ if(!(new_listener in listeners))
+ RegisterSignal(new_listener, COMSIG_QDELETING, PROC_REF(listener_deleted))
+
+ if(isnull(new_listener.client))
+ RegisterSignal(new_listener, COMSIG_MOB_LOGIN, PROC_REF(listener_login))
+ return
+ if(preference_signal)
+ RegisterSignals(new_listener, list(COMSIG_MOVABLE_MOVED, preference_signal), PROC_REF(listener_moved))
+ else
+ RegisterSignal(new_listener, COMSIG_MOVABLE_MOVED, PROC_REF(listener_moved))
+
+ RegisterSignals(new_listener, list(SIGNAL_ADDTRAIT(TRAIT_DEAF), SIGNAL_REMOVETRAIT(TRAIT_DEAF)), PROC_REF(listener_deaf))
+ listeners[new_listener] = NONE
+ if(preference_volume)
+ var/pref_volume = new_listener.client?.prefs.read_preference(preference_volume)
+ if(HAS_TRAIT(new_listener, TRAIT_DEAF) || !pref_volume)
+ listeners[new_listener] |= SOUND_MUTE
+
+ if(HAS_TRAIT(new_listener, TRAIT_DEAF))
+ listeners[new_listener] |= SOUND_MUTE
+
+ update_listener(new_listener)
+ listeners[new_listener] |= SOUND_UPDATE
+
+/datum/threed_sound/proc/listener_deleted(mob/source)
+ SIGNAL_HANDLER
+ deregister_listener(source)
+
+/datum/threed_sound/proc/listener_moved(mob/source)
+ SIGNAL_HANDLER
+ update_listener(source)
+
+/datum/threed_sound/proc/listener_login(mob/source)
+ SIGNAL_HANDLER
+ deregister_listener(source)
+ register_listener(source)
+
+/datum/threed_sound/proc/listener_deaf(mob/source)
+ SIGNAL_HANDLER
+
+ if(HAS_TRAIT(source, TRAIT_DEAF))
+ listeners[source] |= SOUND_MUTE
+ else if(!unmute_listener(source, MUTE_DEAF))
+ return
+ update_listener(source)
+
+
+/datum/threed_sound/proc/unmute_listener(mob/listener, reason)
+ reason = ~reason
+
+ if((reason & MUTE_DEAF) && HAS_TRAIT(listener, TRAIT_DEAF))
+ return FALSE
+ if(preference_volume)
+ var/pref_volume = listener.client?.prefs.read_preference(preference_volume)
+ if((reason & MUTE_VOLUME) && !pref_volume)
+ return FALSE
+
+ if(reason & MUTE_RANGE)
+ var/turf/sound_turf = get_turf(parent)
+ var/turf/listener_turf = get_turf(listener)
+ if(isnull(sound_turf) || isnull(listener_turf))
+ return FALSE
+ if(sound_turf.z != listener_turf.z)
+ return FALSE
+ if(abs(sound_turf.x - listener_turf.x) > x_cutoff)
+ return FALSE
+ if(abs(sound_turf.y - listener_turf.y) > z_cutoff)
+ return FALSE
+
+ listeners[listener] &= ~SOUND_MUTE
+ return TRUE
+
+/datum/threed_sound/proc/deregister_listener(mob/no_longer_listening)
+ PROTECTED_PROC(TRUE)
+
+ listeners -= no_longer_listening
+ no_longer_listening.stop_sound_channel(our_channel)
+ if(preference_signal)
+ UnregisterSignal(no_longer_listening, list(
+ COMSIG_MOB_LOGIN,
+ COMSIG_QDELETING,
+ COMSIG_MOVABLE_MOVED,
+ preference_signal,
+ SIGNAL_ADDTRAIT(TRAIT_DEAF),
+ SIGNAL_REMOVETRAIT(TRAIT_DEAF),
+ ))
+ else
+ UnregisterSignal(no_longer_listening, list(
+ COMSIG_MOB_LOGIN,
+ COMSIG_QDELETING,
+ COMSIG_MOVABLE_MOVED,
+ SIGNAL_ADDTRAIT(TRAIT_DEAF),
+ SIGNAL_REMOVETRAIT(TRAIT_DEAF),
+ ))
+
+/datum/threed_sound/proc/update_listener(mob/listener)
+ PROTECTED_PROC(TRUE)
+ our_sound.status = listeners[listener] || NONE
+ var/turf/sound_turf = get_turf(parent)
+ var/turf/listener_turf = get_turf(listener)
+ if(isnull(sound_turf) || isnull(listener_turf)) // ??
+ our_sound.x = 0
+ our_sound.z = 0
+
+ else if(sound_turf.z != listener_turf.z)
+ listeners[listener] |= SOUND_MUTE
+
+ else
+ if(preference_volume)
+ var/pref_volume = listener.client?.prefs.read_preference(preference_volume)
+ if(!pref_volume)
+ listeners[listener] |= SOUND_MUTE
+ else
+ unmute_listener(listener, MUTE_VOLUME)
+ our_sound.volume = volume * (pref_volume/100)
+ // keep in mind sound XYZ is different to world XYZ. sound +-z = world +-y
+ var/new_x = sound_turf.x - listener_turf.x
+ var/new_z = sound_turf.y - listener_turf.y
+
+ if((abs(new_x) > x_cutoff || abs(new_z) > z_cutoff))
+ listeners[listener] |= SOUND_MUTE
+
+ else if(listeners[listener] & SOUND_MUTE)
+ unmute_listener(listener, MUTE_RANGE)
+
+ our_sound.x = new_x
+ our_sound.z = new_z
+ var/original_volume = our_sound.volume
+ var/calculated_volume = original_volume - CALCULATE_SOUND_VOLUME(original_volume, get_dist(sound_turf, listener_turf), sound_range, falloff_distance, falloff_exponent)
+ if(pressure_affected)
+ //Atmosphere affects sound
+ var/pressure_factor = 1
+ var/datum/gas_mixture/hearer_env = listener_turf.return_air()
+ var/datum/gas_mixture/source_env = sound_turf.return_air()
+
+ if(hearer_env && source_env)
+ var/pressure = min(hearer_env.return_pressure(), source_env.return_pressure())
+ if(pressure < ONE_ATMOSPHERE)
+ pressure_factor = max((pressure - SOUND_MINIMUM_PRESSURE)/(ONE_ATMOSPHERE - SOUND_MINIMUM_PRESSURE), 0)
+ else //space
+ pressure_factor = 0
+
+ if(get_dist(sound_turf, listener_turf) <= 1)
+ pressure_factor = max(pressure_factor, 0.15) //touching the source of the sound
+
+ calculated_volume *= pressure_factor
+ if(calculated_volume < SOUND_AUDIBLE_VOLUME_MIN || get_dist(sound_turf, listener_turf) > sound_range)
+ our_sound.volume = 0
+ else
+ our_sound.volume = calculated_volume
+ SEND_SOUND(listener, our_sound)
+ our_sound.volume = original_volume
+
+/datum/threed_sound/proc/on_moved(datum/source, ...)
+ SIGNAL_HANDLER
+ update_all()
+
+/datum/threed_sound/proc/on_enter_area(datum/source, area/area_to_register)
+ SIGNAL_HANDLER
+ set_new_environment(area_to_register.sound_environment || SOUND_ENVIRONMENT_NONE)
+
+#undef MUTE_DEAF
+#undef MUTE_RANGE
+
+/obj/item/threed_sound_test
+ name = "fuck"
+ desc = "lmao"
+ icon = 'icons/obj/machines/music.dmi'
+ icon_state = "jukebox"
+ var/datum/threed_sound/threed_sound
+ var/our_channel
+ var/sound/new_sound
+
+/obj/item/threed_sound_test/Initialize(mapload)
+ . = ..()
+ var/list/listeners = get_hearers_in_view(7, src)
+ our_channel = SSsounds.random_available_channel()
+ new_sound = sound(
+ 'sound/machines/tram/other_line_processed.ogg',
+ FALSE,
+ 0,
+ our_channel,
+ 100
+ )
+ for(var/mob/listener in listeners)
+ listener.playsound_local(
+ turf_source = get_turf(src),
+ vol = 100,
+ vary = FALSE,
+ channel = our_channel,
+ sound_to_use = new_sound
+ )
+ threed_sound = new(
+ src,
+ new_sound,
+ listeners,
+ FALSE,
+ 100,
+ 3,
+ 12 SECONDS,
+ our_channel,
+ /datum/preference/numeric/volume/sound_tts_volume,
+ COMSIG_MOB_TTS_VOLUME_PREFERENCE_APPLIED
+ )
+
+/obj/item/threed_sound_test/attack_self(mob/user)
+ . = ..()
+ if(QDELETED(threed_sound))
+ threed_sound = null
+ if(!threed_sound)
+ var/list/listeners = get_hearers_in_view(7, src)
+ for(var/mob/listener in listeners)
+ listener.playsound_local(
+ turf_source = get_turf(src),
+ vol = 100,
+ vary = FALSE,
+ channel = our_channel,
+ sound_to_use = new_sound
+ )
+ threed_sound = new(
+ src,
+ new_sound,
+ listeners,
+ FALSE,
+ 100,
+ 3,
+ 12 SECONDS,
+ our_channel,
+ /datum/preference/numeric/volume/sound_tts_volume,
+ COMSIG_MOB_TTS_VOLUME_PREFERENCE_APPLIED
+ )
diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm
index b5993e1894a6..688c82eb5b0c 100644
--- a/code/datums/actions/action.dm
+++ b/code/datums/actions/action.dm
@@ -163,6 +163,7 @@
/// Actually triggers the effects of the action.
/// Called when the on-screen button is clicked, for example.
/datum/action/proc/Trigger(mob/clicker, trigger_flags)
+ SHOULD_CALL_PARENT(TRUE)
if(!(trigger_flags & TRIGGER_FORCE_AVAILABLE) && !IsAvailable(feedback = TRUE))
return FALSE
if(SEND_SIGNAL(src, COMSIG_ACTION_TRIGGER, src) & COMPONENT_ACTION_BLOCK_TRIGGER)
@@ -352,7 +353,7 @@
if(!our_hud || viewers[our_hud]) // There's no point in this if you have no hud in the first place
return
- var/atom/movable/screen/movable/action_button/button = create_button()
+ var/atom/movable/screen/movable/action_button/button = create_button(viewer)
SetId(button, viewer)
button.our_hud = our_hud
@@ -372,8 +373,8 @@
qdel(button)
/// Creates an action button movable for the passed mob, and returns it.
-/datum/action/proc/create_button()
- var/atom/movable/screen/movable/action_button/button = owner.hud_used.add_screen_object(/atom/movable/screen/movable/action_button)
+/datum/action/proc/create_button(mob/viewer)
+ var/atom/movable/screen/movable/action_button/button = viewer.hud_used.add_screen_object(/atom/movable/screen/movable/action_button)
button.linked_action = src
button.allow_observer_click = allow_observer_click
build_button_icon(button, ALL, TRUE)
diff --git a/code/datums/actions/cooldown_action.dm b/code/datums/actions/cooldown_action.dm
index 40f4d6be86a8..4a830c30c88c 100644
--- a/code/datums/actions/cooldown_action.dm
+++ b/code/datums/actions/cooldown_action.dm
@@ -61,7 +61,7 @@
if(original)
create_sequence_actions()
-/datum/action/cooldown/create_button()
+/datum/action/cooldown/create_button(mob/viewer)
var/atom/movable/screen/movable/action_button/button = ..()
button.maptext = ""
button.maptext_x = 4
diff --git a/code/datums/actions/mobs/guardians.dm b/code/datums/actions/mobs/guardians.dm
new file mode 100644
index 000000000000..ff912c5300e7
--- /dev/null
+++ b/code/datums/actions/mobs/guardians.dm
@@ -0,0 +1,77 @@
+/datum/action/cooldown/guardian
+ button_icon = 'icons/hud/guardian.dmi'
+
+/datum/action/cooldown/guardian/IsAvailable(feedback)
+ . = ..()
+ if(!.)
+ return .
+ return !!isguardian(owner)
+
+/datum/action/cooldown/guardian/check_type
+ name = "Check Type"
+ desc = "A reminder on what your abilities are."
+ //this is based off of the antag ui icon, if that changes then change this too please.
+ button_icon_state = /datum/action/antag_info::button_icon_state
+ default_button_position = SCRN_OBJ_INSERT_FIRST
+
+/datum/action/cooldown/guardian/check_type/Activate(atom/target)
+ . = ..()
+ to_chat(owner, astype(owner, /mob/living/basic/guardian)?.playstyle_string)
+
+/datum/action/cooldown/guardian/communicate
+ name = "Communicate"
+ desc = "Communicate telepathically with your user."
+ button_icon_state = "communicate"
+ default_button_position = ui_guardian_communication
+
+/datum/action/cooldown/guardian/communicate/Activate()
+ astype(owner, /mob/living/basic/guardian)?.communicate()
+
+/datum/action/cooldown/guardian/manifest
+ name = "Manifest"
+ desc = "Spring forth into battle!"
+ button_icon_state = "manifest"
+ default_button_position = ui_guardian_manifest
+
+/datum/action/cooldown/guardian/manifest/Activate()
+ astype(owner, /mob/living/basic/guardian)?.manifest()
+
+/datum/action/cooldown/guardian/recall
+ name = "Recall"
+ desc = "Return to your user."
+ button_icon_state = "recall"
+ default_button_position = ui_guardian_recall
+
+/datum/action/cooldown/guardian/recall/Activate()
+ astype(owner, /mob/living/basic/guardian)?.recall()
+
+/datum/action/cooldown/guardian/toggle_light
+ name = "Toggle Light"
+ desc = "Glow like star dust."
+ button_icon_state = "light"
+
+/datum/action/cooldown/guardian/toggle_light/Activate()
+ astype(owner, /mob/living/basic/guardian)?.toggle_light()
+
+/datum/action/cooldown/guardian/toggle_mode
+ name = "Toggle Mode"
+ desc = "Switch between ability modes."
+ button_icon_state = "toggle"
+ default_button_position = ui_guardian_special
+
+/datum/action/cooldown/guardian/toggle_mode/Activate()
+ astype(owner, /mob/living/basic/guardian)?.toggle_modes()
+
+/datum/action/cooldown/guardian/toggle_mode/assassin
+ name = "Toggle Stealth"
+ desc = "Enter or exit stealth."
+ button_icon_state = "stealth"
+ transparent_when_unavailable = TRUE
+
+/datum/action/cooldown/guardian/toggle_mode/assassin/is_action_active(atom/movable/screen/movable/action_button/current_button)
+ return owner.has_status_effect(/datum/status_effect/guardian_stealth)
+
+/datum/action/cooldown/guardian/toggle_mode/gases
+ name = "Toggle Gas"
+ desc = "Switch between possible gases."
+ button_icon_state = "gases"
diff --git a/code/datums/ai/basic_mobs/targeting_strategies/basic_targeting_strategy.dm b/code/datums/ai/basic_mobs/targeting_strategies/basic_targeting_strategy.dm
index 227b4f2fc287..be1d1484225c 100644
--- a/code/datums/ai/basic_mobs/targeting_strategies/basic_targeting_strategy.dm
+++ b/code/datums/ai/basic_mobs/targeting_strategies/basic_targeting_strategy.dm
@@ -157,3 +157,13 @@
/datum/targeting_strategy/basic/exact_match
check_factions_exactly = TRUE
+
+/datum/targeting_strategy/basic/exact_match/ignore_friends
+
+/datum/targeting_strategy/basic/exact_match/ignore_friends/can_attack(mob/living/living_mob, atom/the_target, vision_range)
+ . = ..()
+ if (!.)
+ return FALSE
+ if (living_mob.ai_controller.blackboard[BB_FRIENDS_LIST] && (the_target in living_mob.ai_controller.blackboard[BB_FRIENDS_LIST]))
+ return FALSE
+ return TRUE
diff --git a/code/datums/beam.dm b/code/datums/beam.dm
index 0a044e6b7a56..9d5e7b6c35d3 100644
--- a/code/datums/beam.dm
+++ b/code/datums/beam.dm
@@ -166,10 +166,10 @@
var/final_x = segment.x
var/final_y = segment.y
if(abs(Pixel_x)>32)
- final_x += Pixel_x > 0 ? round(Pixel_x/32) : CEILING(Pixel_x/32, 1)
+ final_x += Pixel_x > 0 ? round(Pixel_x/32) : ceil(Pixel_x/32)
Pixel_x %= 32
if(abs(Pixel_y)>32)
- final_y += Pixel_y > 0 ? round(Pixel_y/32) : CEILING(Pixel_y/32, 1)
+ final_y += Pixel_y > 0 ? round(Pixel_y/32) : ceil(Pixel_y/32)
Pixel_y %= 32
segment.forceMove(locate(final_x, final_y, segment.z))
diff --git a/code/datums/brain_damage/special.dm b/code/datums/brain_damage/special.dm
index e4e6847011f9..469a5b90562e 100644
--- a/code/datums/brain_damage/special.dm
+++ b/code/datums/brain_damage/special.dm
@@ -701,7 +701,7 @@
to_chat(owner, span_warning("Should I really leave it here?"))
owner.add_mood_event("fireaxe", /datum/mood_event/axe_neutral)
-/datum/brain_trauma/special/axedoration/proc/on_examine(mob/source, atom/target, list/examine_strings)
+/datum/brain_trauma/special/axedoration/proc/on_examine(mob/source, atom/target, list/examine_strings, list/examine_overrides)
SIGNAL_HANDLER
if(!istype(target, /obj/item/fireaxe))
return
diff --git a/code/datums/communications.dm b/code/datums/communications.dm
index 1d9b635c4c18..47e5f2abc490 100644
--- a/code/datums/communications.dm
+++ b/code/datums/communications.dm
@@ -40,14 +40,20 @@ GLOBAL_DATUM_INIT(communications_controller, /datum/communciations_controller, n
if(!can_announce(user, is_silicon))
return FALSE
if(is_silicon)
- minor_announce(html_decode(input),"[user.name] announces:", players = players)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ minor_announce(html_decode(input), "[user.name] announces:", players = players, tts_source = user, tts_effect = "captain")
+ // MASSMETA EDIT END (ntts && /tg/tts)
COOLDOWN_START(src, silicon_message_cooldown, COMMUNICATION_COOLDOWN_AI)
else
var/list/message_data = user.treat_message(input)
if(syndicate)
- priority_announce(html_decode(message_data["message"]), null, 'sound/announcer/announcement/announce_syndi.ogg', ANNOUNCEMENT_TYPE_SYNDICATE, has_important_message = TRUE, players = players, color_override = "red")
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ priority_announce(html_decode(message_data["message"]), null, 'sound/announcer/announcement/announce_syndi.ogg', ANNOUNCEMENT_TYPE_SYNDICATE, has_important_message = TRUE, players = players, color_override = "red", tts_source = user, tts_effect = "syndicate")
+ // MASSMETA EDIT END (ntts && /tg/tts)
else
- priority_announce(html_decode(message_data["message"]), null, 'sound/announcer/announcement/announce.ogg', ANNOUNCEMENT_TYPE_CAPTAIN, has_important_message = TRUE, players = players)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ priority_announce(html_decode(message_data["message"]), null, 'sound/announcer/announcement/announce.ogg', ANNOUNCEMENT_TYPE_CAPTAIN, has_important_message = TRUE, players = players, tts_source = user, tts_effect = "captain")
+ // MASSMETA EDIT END (ntts && /tg/tts)
COOLDOWN_START(src, nonsilicon_message_cooldown, COMMUNICATION_COOLDOWN)
user.log_talk(input, LOG_SAY, tag="priority announcement")
message_admins("[ADMIN_LOOKUPFLW(user)] has made a priority announcement.")
@@ -120,7 +126,7 @@ GLOBAL_DATUM_INIT(communications_controller, /datum/communciations_controller, n
for(var/datum/command_footnote/footnote as anything in command_report_footnotes)
footnote_pile += "[footnote.message]
"
- footnote_pile += "[footnote.signature]
"
+ footnote_pile += "~ [footnote.signature]
"
footnote_pile += "
"
. += "
Additional Notes:
" + footnote_pile
diff --git a/code/datums/components/clothing_dirt.dm b/code/datums/components/clothing_dirt.dm
index 46d7026d8045..a02b2cfa0a51 100644
--- a/code/datums/components/clothing_dirt.dm
+++ b/code/datums/components/clothing_dirt.dm
@@ -7,6 +7,8 @@
var/dirt_state
/// Color of current overlay
var/dirt_color = COLOR_WHITE
+ /// Tracks if tint has been applied to parent
+ VAR_FINAL/tint_applied = FALSE
/datum/component/clothing_dirt/Initialize(dirt_state = null)
if(!isclothing(parent))
@@ -21,6 +23,7 @@
RegisterSignal(parent, COMSIG_ATOM_EXPOSE_REAGENTS, PROC_REF(on_expose), TRUE)
RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_overlays_updated))
RegisterSignal(parent, COMSIG_ITEM_GET_SEPARATE_WORN_OVERLAYS, PROC_REF(on_separate_worn_overlays))
+ RegisterSignal(parent, COMSIG_CLOTHING_VISOR_TOGGLE, PROC_REF(on_visor_move))
/datum/component/clothing_dirt/UnregisterFromParent()
var/obj/item/clothing/clothing = parent
@@ -38,9 +41,23 @@
COMSIG_COMPONENT_CLEAN_ACT,
COMSIG_ATOM_UPDATE_OVERLAYS,
COMSIG_ITEM_GET_SEPARATE_WORN_OVERLAYS,
+ COMSIG_CLOTHING_VISOR_TOGGLE,
))
return ..()
+/datum/component/clothing_dirt/process(seconds_per_tick)
+ if(!dirtiness)
+ return PROCESS_KILL
+ if(!SPT_PROB(1, seconds_per_tick))
+ return
+ var/obj/item/clothing/clothing = parent
+ if(!iscarbon(clothing.loc) || !(clothing.flags_cover & PEPPERPROOF) || clothing.tint < dirtiness || clothing.tint < TINT_MILD)
+ return
+ var/mob/living/carbon/wearer = clothing.loc
+ if(wearer.is_blind() && !wearer.is_blind_from(EYES_COVERED))
+ return
+ to_chat(wearer, span_warning("It's hard to see with all the stuff covering your [clothing.name]..."))
+
/datum/component/clothing_dirt/proc/on_equip(datum/source, mob/user, slot)
SIGNAL_HANDLER
var/obj/item/clothing/clothing = parent
@@ -58,8 +75,9 @@
/datum/component/clothing_dirt/proc/on_examine(datum/source, mob/user, list/examine_list)
SIGNAL_HANDLER
+ var/obj/item/clothing/clothing = parent
if (dirtiness > 0)
- examine_list += span_warning("It appears to be covered in something. Won't see much while wearing it until you wash it off.")
+ examine_list += span_warning("It appears to be covered in something. [clothing.tint >= TINT_MILD ? "Won't see much while wearing it until you wash it off." : "Any more and you might struggle to see through it."]")
/datum/component/clothing_dirt/proc/on_overlays_updated(obj/item/clothing/source, list/overlays)
SIGNAL_HANDLER
@@ -84,31 +102,62 @@
/datum/component/clothing_dirt/proc/on_expose(atom/target, list/reagents, datum/reagents/source, methods)
SIGNAL_HANDLER
- var/mob/living/carbon/wearer
var/obj/item/clothing/clothing = parent
if(iscarbon(target))
- wearer = target
+ var/mob/living/carbon/wearer = target
if(is_protected(wearer))
return
if(!(wearer.get_slot_by_item(clothing) & clothing.slot_flags))
return
+ if(!(clothing.flags_cover & PEPPERPROOF))
+ return
+
var/datum/reagent/consumable/condensedcapsaicin/pepper = locate() in reagents
if(isnull(pepper) || !(methods & (TOUCH | VAPOR)))
return
+ if(!dirtiness)
+ START_PROCESSING(SSobj, src)
+
dirt_color = pepper.color
+ remove_tint(FALSE)
+ dirtiness = min(dirtiness + round(reagents[pepper] / 5, 0.25), 3)
+ apply_tint(TRUE)
+
+/datum/component/clothing_dirt/proc/is_protected(mob/living/carbon/wearer)
+ return wearer.head && wearer.head != parent && (wearer.head.flags_cover & PEPPERPROOF)
+
+/datum/component/clothing_dirt/proc/remove_tint(updates = TRUE)
+ if(!tint_applied)
+ return
+
+ tint_applied = FALSE
+ var/obj/item/clothing/clothing = parent
clothing.tint -= dirtiness
- dirtiness = min(dirtiness + round(reagents[pepper] / 5), 3)
- clothing.tint += dirtiness
+ if(!updates)
+ return
clothing.update_appearance()
- if(!isnull(wearer))
+ clothing.update_slot_icon()
+ if(iscarbon(clothing.loc))
+ var/mob/living/carbon/wearer = clothing.loc
wearer.update_tint()
- wearer.update_clothing(wearer.get_slot_by_item(clothing))
-/datum/component/clothing_dirt/proc/is_protected(mob/living/carbon/wearer)
- return wearer.head && wearer.head != parent && (wearer.head.flags_cover & PEPPERPROOF)
+/datum/component/clothing_dirt/proc/apply_tint(updates = TRUE)
+ if(tint_applied || !dirtiness)
+ return
+
+ tint_applied = TRUE
+ var/obj/item/clothing/clothing = parent
+ clothing.tint += dirtiness
+ if(!updates)
+ return
+ clothing.update_appearance()
+ clothing.update_slot_icon()
+ if(iscarbon(clothing.loc))
+ var/mob/living/carbon/wearer = clothing.loc
+ wearer.update_tint()
/datum/component/clothing_dirt/proc/on_spraypaint(mob/living/carbon/wearer, mob/user, obj/item/toy/crayon/spraycan/spraycan)
SIGNAL_HANDLER
@@ -120,30 +169,34 @@
if(!(wearer.get_slot_by_item(clothing) & clothing.slot_flags))
return
+ if(!dirtiness)
+ START_PROCESSING(SSobj, src)
+
dirt_color = spraycan.paint_color
- clothing.tint -= dirtiness
+ remove_tint(FALSE)
dirtiness = min(3, dirtiness + rand(2, 3))
- clothing.tint += dirtiness
- clothing.update_appearance()
- wearer.update_clothing(wearer.get_slot_by_item(clothing))
- wearer.update_tint()
+ apply_tint(TRUE)
user.visible_message(span_danger("[user] sprays [spraycan] into the face of [wearer]!"))
to_chat(wearer, span_userdanger("[user] sprays [spraycan] into your face!"))
return COMPONENT_CANCEL_SPRAYPAINT
-/datum/component/clothing_dirt/proc/on_clean(datum/source, clean_types)
+/datum/component/clothing_dirt/proc/on_clean(obj/item/clothing/source, clean_types)
SIGNAL_HANDLER
- . = NONE
- var/obj/item/clothing/clothing = parent
- var/mob/living/carbon/wearer
- if(iscarbon(clothing.loc))
- wearer = clothing.loc
+ if (!dirtiness || !(clean_types & (CLEAN_WASH|CLEAN_SCRUB)))
+ return NONE
- if (clean_types & (CLEAN_WASH|CLEAN_SCRUB))
- clothing.tint -= dirtiness
- dirtiness = 0
- if(!isnull(wearer))
- wearer.update_tint()
+ remove_tint(TRUE)
+ STOP_PROCESSING(SSobj, src)
+ dirtiness = 0
+ return COMPONENT_CLEANED|COMPONENT_CLEANED_GAIN_XP
- return COMPONENT_CLEANED|COMPONENT_CLEANED_GAIN_XP
+/datum/component/clothing_dirt/proc/on_visor_move(obj/item/clothing/source, up)
+ SIGNAL_HANDLER
+ if(dirtiness <= 0)
+ return
+
+ if(source.flags_cover & PEPPERPROOF)
+ apply_tint(TRUE)
+ else
+ remove_tint(TRUE)
diff --git a/code/datums/components/cracked.dm b/code/datums/components/cracked.dm
index 4d67a9190ea1..eea5a1898364 100644
--- a/code/datums/components/cracked.dm
+++ b/code/datums/components/cracked.dm
@@ -35,7 +35,7 @@
return
var/current_percent = 1 - (new_value / cracked_max_integrity)
- var/cracks = CEILING(10 * current_percent, 1) // 1 crack per 10% integrity lost
+ var/cracks = ceil(10 * current_percent) // 1 crack per 10% integrity lost
var/current_cracks = length(applied_cracks)
if(!islist(source.filters))
if(isnull(source.filters))
diff --git a/code/datums/components/crafting/melee_weapon.dm b/code/datums/components/crafting/melee_weapon.dm
index 9f0fab509645..00ced214f813 100644
--- a/code/datums/components/crafting/melee_weapon.dm
+++ b/code/datums/components/crafting/melee_weapon.dm
@@ -145,6 +145,16 @@
time = 4 SECONDS
category = CAT_WEAPON_MELEE
+/datum/crafting_recipe/wireprod
+ name = "Wireprod assembly"
+ result = /obj/item/wireprod
+ reqs = list(
+ /obj/item/restraints/handcuffs/cable = 1,
+ /obj/item/stack/rods = 1,
+ )
+ time = 2 SECONDS
+ category = CAT_WEAPON_MELEE
+
/datum/crafting_recipe/toysword
name = "Toy Sword"
reqs = list(
diff --git a/code/datums/components/crafting/ranged_weapon.dm b/code/datums/components/crafting/ranged_weapon.dm
index 57139147fd8d..53929a6f68db 100644
--- a/code/datums/components/crafting/ranged_weapon.dm
+++ b/code/datums/components/crafting/ranged_weapon.dm
@@ -353,7 +353,7 @@
)
time = 10 SECONDS
category = CAT_WEAPON_RANGED
- crafting_flags = CRAFT_CHECK_DENSITY
+ crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_MUST_BE_LEARNED
/datum/crafting_recipe/large_ballista
diff --git a/code/datums/components/direct_explosive_trap.dm b/code/datums/components/direct_explosive_trap.dm
index 1372c569bbad..18a4f5ecdec9 100644
--- a/code/datums/components/direct_explosive_trap.dm
+++ b/code/datums/components/direct_explosive_trap.dm
@@ -40,6 +40,7 @@
RegisterSignals(parent, triggering_signals, PROC_REF(explode))
if (!isnull(saboteur))
RegisterSignal(saboteur, COMSIG_QDELETING, PROC_REF(on_bomber_deleted))
+ log_bomber(saboteur, "has set a direct_explosive_trap with severity [explosive_force] on", parent, "to detonate on the signals [english_list(triggering_signals)]", message_admins = (!isnull(saboteur)))
/datum/component/direct_explosive_trap/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_ATOM_EXAMINE) + triggering_signals)
@@ -74,6 +75,9 @@
to_chat(victim, span_bolddanger("[source] was boobytrapped!"))
if (!isnull(saboteur))
to_chat(saboteur, span_bolddanger("Success! Your trap on [source] caught [victim.name]!"))
+ var/atom/parent_atom = parent
+ message_admins("Direct EX_ACT explosion with severity [explosive_force] on [ADMIN_LOOKUPFLW(victim)]. Caused by direct_explosive_trap on: [ADMIN_VERBOSEJMP(parent_atom)]. Set by: [key_name(saboteur)].")
+ log_game("Direct EX_ACT explosion with severity [explosive_force] on [key_name(victim)]. Caused by direct_explosive_trap on: [parent_atom.name] at [AREACOORD(parent_atom)]. Set by: [key_name(saboteur)].")
playsound(source, 'sound/effects/explosion/explosion2.ogg', 200, TRUE)
new /obj/effect/temp_visual/explosion(get_turf(source))
EX_ACT(victim, explosive_force)
diff --git a/code/datums/components/life_link.dm b/code/datums/components/life_link.dm
index d1371467682c..026826e4fea0 100644
--- a/code/datums/components/life_link.dm
+++ b/code/datums/components/life_link.dm
@@ -25,14 +25,13 @@
RegisterSignal(parent, COMSIG_CARBON_LIMB_DAMAGED, PROC_REF(on_limb_damage))
RegisterSignals(parent, COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES, PROC_REF(on_damage_adjusted))
RegisterSignal(parent, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(on_health_updated))
- RegisterSignal(parent, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(on_status_tab_updated))
if (!isnull(host))
var/mob/living/living_parent = parent
living_parent.updatehealth()
/datum/component/life_link/UnregisterFromParent()
unregister_host()
- UnregisterSignal(parent, list(COMSIG_CARBON_LIMB_DAMAGED, COMSIG_LIVING_HEALTH_UPDATE, COMSIG_MOB_GET_STATUS_TAB_ITEMS) + COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES)
+ UnregisterSignal(parent, list(COMSIG_CARBON_LIMB_DAMAGED, COMSIG_LIVING_HEALTH_UPDATE) + COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES)
/datum/component/life_link/InheritComponent(datum/component/new_comp, i_am_original, mob/living/host, datum/callback/on_passed_damage, datum/callback/on_linked_death)
register_host(host)
@@ -133,12 +132,6 @@
else
mob_parent.set_hud_image_state(STATUS_HUD, "hudhealthy")
-/// When our status tab updates, draw how much HP our host has in there
-/datum/component/life_link/proc/on_status_tab_updated(mob/living/source, list/items)
- SIGNAL_HANDLER
- var/healthpercent = health_percentage(host)
- items += "Host Health: [round(healthpercent, 0.5)]%"
-
/// Called when our host dies, we should die too
/datum/component/life_link/proc/on_host_died(mob/living/source, gibbed)
SIGNAL_HANDLER
diff --git a/code/datums/components/listen_and_repeat.dm b/code/datums/components/listen_and_repeat.dm
index 0ebeda27b3f6..64449e19d191 100644
--- a/code/datums/components/listen_and_repeat.dm
+++ b/code/datums/components/listen_and_repeat.dm
@@ -8,6 +8,10 @@
#define MESSAGE_VOICE "voice"
/// the tone it should be said in
#define MESSAGE_PITCH "pitch"
+/// the blip base it should be said in
+#define MESSAGE_BLIP_BASE "blip_base"
+/// the blip base it should be said in
+#define MESSAGE_BLIP_NUMBER "blip_number"
/// Simple element that will deterministically set a value based on stuff that the source has heard and will then compel the source to repeat it.
/// Requires a valid AI Blackboard.
@@ -24,6 +28,8 @@
var/static/list/invalid_voice = list(
MESSAGE_VOICE = "invalid",
MESSAGE_PITCH = 0,
+ MESSAGE_BLIP_BASE = "male",
+ MESSAGE_BLIP_NUMBER = 1,
)
/datum/component/listen_and_repeat/Initialize(list/desired_phrases, blackboard_key)
@@ -68,6 +74,8 @@
var/atom/movable/movable_speaker = speaker
speaker_sound[MESSAGE_VOICE] = movable_speaker.voice || "invalid"
speaker_sound[MESSAGE_PITCH] = (movable_speaker.pitch && SStts.pitch_enabled ? movable_speaker.pitch : 0)
+ speaker_sound[MESSAGE_BLIP_BASE] = movable_speaker.blip_base || "male"
+ speaker_sound[MESSAGE_BLIP_NUMBER] = movable_speaker.blip_number || 1
if(over_radio && prob(RADIO_IGNORE_CHANCE))
return
@@ -94,6 +102,9 @@
if(islist(speech_buffer[selected_phrase]))
to_return[MESSAGE_VOICE] = speech_buffer[selected_phrase][MESSAGE_VOICE]
to_return[MESSAGE_PITCH] = speech_buffer[selected_phrase][MESSAGE_PITCH]
+ to_return[MESSAGE_BLIP_BASE] = speech_buffer[selected_phrase][MESSAGE_BLIP_BASE]
+ to_return[MESSAGE_BLIP_NUMBER] = speech_buffer[selected_phrase][MESSAGE_BLIP_NUMBER]
+
controller.override_blackboard_key(blackboard_key, to_return)
@@ -112,3 +123,5 @@
#undef MESSAGE_VOICE
#undef MESSAGE_PITCH
#undef MESSAGE_LINE
+#undef MESSAGE_BLIP_BASE
+#undef MESSAGE_BLIP_NUMBER
diff --git a/code/datums/components/material_turf_tracking.dm b/code/datums/components/material_turf_tracking.dm
new file mode 100644
index 000000000000..492b4d0f4955
--- /dev/null
+++ b/code/datums/components/material_turf_tracking.dm
@@ -0,0 +1,203 @@
+/// Sends a signal to the owner material whenever something enters its objects' turf and steps onto said object
+/datum/component/material_turf_tracking
+ dupe_mode = COMPONENT_DUPE_ALLOWED
+ /// Material we're linked to
+ var/datum/material/owner_material = null
+ /// Does our parent require the target to be elevated for us to trigger?
+ var/requires_elevation = FALSE
+
+ /// Typecache of things we should ignore
+ var/static/list/interaction_blacklist = typecacheof(list(
+ /obj/docking_port,
+ /obj/effect/abstract,
+ /obj/effect/atmos_shield,
+ /obj/effect/collapse,
+ /obj/effect/constructing_effect,
+ /obj/effect/dummy/phased_mob,
+ /obj/effect/ebeam,
+ /obj/effect/fishing_float,
+ /obj/effect/hotspot,
+ /obj/effect/landmark,
+ /obj/effect/light_emitter/tendril,
+ /obj/effect/mapping_helpers,
+ /obj/effect/particle_effect/ion_trails,
+ /obj/effect/particle_effect/sparks,
+ /obj/effect/portal,
+ /obj/effect/projectile,
+ /obj/effect/spectre_of_resurrection,
+ /obj/effect/temp_visual,
+ /obj/effect/wisp,
+ /obj/energy_ball,
+ /obj/narsie,
+ /obj/singularity,
+ ))
+
+ /// Typecache of objects which we only consider "touched" when they elevate the mob, or the mob is buckled to them
+ /// Easy way to keep track of snowflake behavior like flipped tables
+ var/static/list/elevation_interactions = typecacheof(list(
+ /obj/structure/platform,
+ /obj/structure/table,
+ /obj/structure/rack,
+ /obj/structure/bed,
+ /obj/structure/closet/crate,
+ /obj/structure/reagent_dispensers,
+ /obj/structure/altar,
+ ))
+
+/datum/component/material_turf_tracking/Initialize(datum/material/owner_material)
+ if (!isopenturf(parent) && !isobj(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.owner_material = owner_material
+ if (is_type_in_typecache(parent, elevation_interactions))
+ requires_elevation = TRUE
+
+/datum/component/material_turf_tracking/Destroy(force)
+ owner_material = null
+ return ..()
+
+/datum/component/material_turf_tracking/RegisterWithParent()
+ var/turf/target_turf = parent
+ if (ismovable(parent))
+ var/atom/movable/as_movable = parent
+ RegisterSignal(as_movable, COMSIG_ATOM_ENTERING, PROC_REF(on_source_entering))
+ RegisterSignal(as_movable, COMSIG_ATOM_EXITING, PROC_REF(on_source_exiting))
+ target_turf = as_movable.loc
+
+ if (!isopenturf(target_turf))
+ return
+
+ if (!requires_elevation)
+ RegisterSignal(target_turf, SIGNAL_ADDTRAIT(TRAIT_ELEVATED_TURF), PROC_REF(on_turf_lost))
+ RegisterSignal(target_turf, SIGNAL_REMOVETRAIT(TRAIT_ELEVATED_TURF), PROC_REF(on_turf_gained))
+ if (HAS_TRAIT(target_turf, TRAIT_ELEVATED_TURF))
+ return
+
+ // Not tracking initializations or existing objects as this would allow you to TP someone from plating by placing a tile underneath
+ RegisterSignal(target_turf, COMSIG_ATOM_ENTERED, PROC_REF(on_entered))
+ RegisterSignal(target_turf, COMSIG_TURF_MOVABLE_THROW_LANDED, PROC_REF(on_entered)) // Need this as shoves are 1 tile throws, and COMSIG_ATOM_ENTERED runs before the throw ends
+ RegisterSignal(target_turf, COMSIG_ATOM_EXITED, PROC_REF(on_exited))
+
+/datum/component/material_turf_tracking/UnregisterFromParent()
+ . = ..()
+ if (isturf(parent))
+ on_source_exiting(parent)
+ return
+
+ var/atom/movable/as_movable = parent
+ UnregisterSignal(as_movable, list(COMSIG_ATOM_ENTERING, COMSIG_ATOM_EXITING))
+ on_source_exiting(as_movable.loc)
+
+/datum/component/material_turf_tracking/proc/on_source_entering(atom/movable/source, atom/entering, atom/old_loc)
+ SIGNAL_HANDLER
+
+ if (!isopenturf(entering))
+ return
+
+ if (!requires_elevation)
+ RegisterSignal(entering, SIGNAL_ADDTRAIT(TRAIT_ELEVATED_TURF), PROC_REF(on_turf_lost))
+ RegisterSignal(entering, SIGNAL_REMOVETRAIT(TRAIT_ELEVATED_TURF), PROC_REF(on_turf_gained))
+ if (HAS_TRAIT(entering, TRAIT_ELEVATED_TURF))
+ return
+ on_turf_gained(entering)
+
+/datum/component/material_turf_tracking/proc/on_source_exiting(atom/movable/source, atom/exiting)
+ SIGNAL_HANDLER
+
+ if (!isturf(exiting))
+ return
+
+ UnregisterSignal(exiting, list(SIGNAL_ADDTRAIT(TRAIT_ELEVATED_TURF), SIGNAL_REMOVETRAIT(TRAIT_ELEVATED_TURF)))
+ on_turf_lost(exiting)
+
+/datum/component/material_turf_tracking/proc/on_turf_gained(turf/source)
+ SIGNAL_HANDLER
+
+ RegisterSignal(source, COMSIG_ATOM_ENTERED, PROC_REF(on_entered))
+ RegisterSignal(source, COMSIG_TURF_MOVABLE_THROW_LANDED, PROC_REF(on_entered))
+ RegisterSignal(source, COMSIG_ATOM_EXITED, PROC_REF(on_exited))
+ for (var/atom/movable/thing in source)
+ on_entered(source, thing)
+
+/datum/component/material_turf_tracking/proc/on_turf_lost(turf/source)
+ SIGNAL_HANDLER
+
+ UnregisterSignal(source, list(COMSIG_ATOM_ENTERED, COMSIG_TURF_MOVABLE_THROW_LANDED, COMSIG_ATOM_EXITED))
+ for (var/atom/movable/thing in source)
+ UnregisterSignal(thing, list(SIGNAL_ADDTRAIT(TRAIT_MOB_ELEVATED), COMSIG_MOVETYPE_FLAG_DISABLED))
+
+/datum/component/material_turf_tracking/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ SIGNAL_HANDLER
+
+ if (arrived.throwing || arrived.invisibility >= INVISIBILITY_ABSTRACT || arrived == parent)
+ return
+
+ if (is_type_in_typecache(arrived, interaction_blacklist))
+ return
+
+ if (!isliving(arrived) && requires_elevation)
+ return
+
+ // Its floating but it may touch down
+ if (arrived.movement_type & MOVETYPES_NOT_TOUCHING_GROUND)
+ RegisterSignal(arrived, COMSIG_MOVETYPE_FLAG_DISABLED, PROC_REF(on_move_flag_disabled))
+ return
+
+ if (!isliving(arrived))
+ trigger_effect(arrived)
+ return
+
+ if (requires_elevation)
+ // We want to know when they touch down so we can interact with them
+ RegisterSignal(arrived, SIGNAL_ADDTRAIT(TRAIT_MOB_ELEVATED), PROC_REF(on_mob_elevated))
+ // The trait is kept even if the mob is buckled which is weird but plays into our hand here
+ if (!HAS_TRAIT(arrived, TRAIT_MOB_ELEVATED))
+ return
+
+ trigger_effect(arrived)
+
+/datum/component/material_turf_tracking/proc/on_exited(datum/source, atom/movable/gone)
+ SIGNAL_HANDLER
+
+ UnregisterSignal(gone, list(SIGNAL_ADDTRAIT(TRAIT_MOB_ELEVATED), COMSIG_MOVETYPE_FLAG_DISABLED))
+
+/datum/component/material_turf_tracking/proc/trigger_effect(atom/movable/arrived)
+ if (!isliving(arrived))
+ SEND_SIGNAL(owner_material, COMSIG_MATERIAL_EFFECT_STEP, parent, arrived, null, null, FALSE)
+ return
+
+ var/mob/living/victim = arrived
+ var/skin_contact = FEET
+ if (victim.body_position == LYING_DOWN)
+ skin_contact = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
+
+ for (var/obj/item/worn_item in victim.get_equipped_items(INCLUDE_ABSTRACT))
+ skin_contact &= ~worn_item.body_parts_covered
+ if (!skin_contact)
+ break
+
+ SEND_SIGNAL(owner_material, COMSIG_MATERIAL_EFFECT_STEP, parent, arrived, null, pick(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG), !!skin_contact)
+
+/datum/component/material_turf_tracking/proc/on_mob_elevated(mob/living/source, trait)
+ if (source.throwing || source.invisibility >= INVISIBILITY_ABSTRACT || (source.movement_type & MOVETYPES_NOT_TOUCHING_GROUND))
+ return
+
+ if (is_type_in_typecache(source, interaction_blacklist))
+ return
+
+ trigger_effect(source)
+
+/datum/component/material_turf_tracking/proc/on_move_flag_disabled(atom/movable/source, flag, old_state)
+ if (source.throwing || source.invisibility >= INVISIBILITY_ABSTRACT || (source.movement_type & MOVETYPES_NOT_TOUCHING_GROUND))
+ return
+
+ if (is_type_in_typecache(source, interaction_blacklist))
+ return
+
+ if (requires_elevation)
+ if (!isliving(source))
+ return
+ RegisterSignal(source, SIGNAL_ADDTRAIT(TRAIT_MOB_ELEVATED), PROC_REF(on_mob_elevated))
+ if (!HAS_TRAIT(source, TRAIT_MOB_ELEVATED))
+ return
+
+ trigger_effect(source)
diff --git a/code/datums/components/overlay_lighting.dm b/code/datums/components/overlay_lighting.dm
index 166ff28665f9..03705b410f63 100644
--- a/code/datums/components/overlay_lighting.dm
+++ b/code/datums/components/overlay_lighting.dm
@@ -396,7 +396,7 @@
turn_off()
range = clamp(CEILING(new_range, 0.5), 1, 6)
var/pixel_bounds = ((range - 1) * 64) + 32
- lumcount_range = CEILING(range, 1)
+ lumcount_range = ceil(range)
hide_from_holder()
visible_mask.icon = light_overlays["[pixel_bounds]"]
diff --git a/code/datums/components/pricetag.dm b/code/datums/components/pricetag.dm
index d8636952921e..c6c43ec3fdf9 100644
--- a/code/datums/components/pricetag.dm
+++ b/code/datums/components/pricetag.dm
@@ -11,15 +11,29 @@
/// List of bank accounts this pricetag pays out to. Format is payees[bank_account] = profit_ratio.
var/list/payees = list()
-/datum/component/pricetag/Initialize(pay_to_account, profit_ratio = 1, delete_on_unwrap = TRUE)
+/datum/component/pricetag/Initialize(list/pay_to_account, profit_ratio = 1, delete_on_unwrap = TRUE)
if(!isobj(parent)) //Has to account for both objects and sellable structures like crates.
return COMPONENT_INCOMPATIBLE
if(isnull(pay_to_account))
- stack_trace("[type] component was added to something without a pay_to_account!")
+ stack_trace("[type] component was added to something without a pay_to_account list!")
return COMPONENT_INCOMPATIBLE
- payees[pay_to_account] = profit_ratio
+ if(!length(pay_to_account))
+ stack_trace("[type] component was created with an empty pay_to_account list!")
+ return COMPONENT_INCOMPATIBLE
+
+ var/total_contribution = 0
+ for(var/totaler in pay_to_account)
+ if(!pay_to_account[totaler])
+ pay_to_account[totaler] = 1 //In case we just want a list with one item to prevent things from getting fucked up.
+
+ total_contribution += pay_to_account[totaler]
+
+ for(var/individual in pay_to_account)
+ var/individual_cut = profit_ratio * (pay_to_account[individual]/ total_contribution)
+ payees[individual] = individual_cut
+
src.delete_on_unwrap = delete_on_unwrap
/datum/component/pricetag/RegisterWithParent()
@@ -47,10 +61,11 @@
* (Don't go from non-deleting to deleting)
*/
/datum/component/pricetag/InheritComponent(datum/component/pricetag/new_comp, i_am_original, pay_to_account, profit_ratio = 1, delete_on_unwrap = TRUE)
- if(!isnull(payees[pay_to_account]) && payees[pay_to_account] >= profit_ratio) // They're already getting a better ratio, don't scam them
- return
+ if(length(payees) == 1)
+ if(!isnull(payees[pay_to_account]) && payees[pay_to_account] >= profit_ratio) // They're already getting a better ratio, don't scam them
+ return
- payees[pay_to_account] = profit_ratio
+ payees[pay_to_account] = profit_ratio
if(!delete_on_unwrap)
src.delete_on_unwrap = delete_on_unwrap
@@ -83,6 +98,7 @@
// Gotta see how much money we've lost by the end of things.
var/overall_item_price = item_value
+ var/running_tally = 0
for(var/datum/bank_account/payee as anything in payees)
// Every payee with a ratio gets a cut based on the item's total value
@@ -92,7 +108,7 @@
payee.adjust_money(payee_cut, "Pricetag: [capitalize(format_text(source.name))] Sale")
payee.bank_card_talk("Sale of [source] recorded. [payee_cut] [MONEY_NAME] added to account.")
-
+ running_tally += payee_cut
// Update the report with the modified final price
report.total_value[export] += overall_item_price
report.total_amount[export] += export.get_amount(source) * export.amount_report_multiplier
diff --git a/code/datums/components/riding/riding_mob.dm b/code/datums/components/riding/riding_mob.dm
index ba6153aa77f6..dcfcd0d84d55 100644
--- a/code/datums/components/riding/riding_mob.dm
+++ b/code/datums/components/riding/riding_mob.dm
@@ -691,12 +691,6 @@
TEXT_WEST = list(0, 0, MOB_BELOW_PIGGYBACK_LAYER),
)
-/datum/component/riding/creature/raptor/update_parent_layer_and_offsets(dir, animate)
- . = ..()
- var/mob/living/basic/raptor/raptor = parent
- if (istype(raptor))
- raptor.adjust_offsets(dir)
-
/datum/component/riding/creature/raptor/combat
ai_behavior_while_ridden = RIDING_PAUSE_AI_MOVEMENT
diff --git a/code/datums/components/security_vision.dm b/code/datums/components/security_vision.dm
index 5c2b88fffd31..f26b7121d8ef 100644
--- a/code/datums/components/security_vision.dm
+++ b/code/datums/components/security_vision.dm
@@ -19,7 +19,7 @@
UnregisterSignal(parent, COMSIG_MOB_EXAMINING)
/// When we examine something, check if we have any extra data to add
-/datum/component/security_vision/proc/on_examining(mob/source, atom/target, list/examine_strings)
+/datum/component/security_vision/proc/on_examining(mob/source, atom/target, list/examine_strings, list/examine_overrides)
SIGNAL_HANDLER
if (!isliving(target))
return
diff --git a/code/datums/components/tackle.dm b/code/datums/components/tackle.dm
index ad8d53b123de..9bdc48520c4d 100644
--- a/code/datums/components/tackle.dm
+++ b/code/datums/components/tackle.dm
@@ -181,8 +181,9 @@
/datum/component/tackler/proc/do_grab(mob/living/carbon/tackler, mob/living/carbon/tackled, skip_to_state = GRAB_PASSIVE)
set waitfor = FALSE
- if(!tackler.grab(tackled) || tackler.pulling != tackled)
+ if(tackler.grab(tackled) != GRAB_SUCCESS || tackler.pulling != tackled)
return
+
if(tackler.grab_state != skip_to_state)
tackler.setGrabState(skip_to_state)
diff --git a/code/datums/components/throwbonus_on_windup.dm b/code/datums/components/throwbonus_on_windup.dm
index 0b767939f9f5..9963a07c79a4 100644
--- a/code/datums/components/throwbonus_on_windup.dm
+++ b/code/datums/components/throwbonus_on_windup.dm
@@ -163,7 +163,7 @@
. = ..()
var/static/list/bar_positions = list(0, 2, 4, 6, 8)
var/current_percentage = current_count / maximum_count
- var/bars_to_add = CEILING(length(bar_positions) * current_percentage, 1)
+ var/bars_to_add = ceil(length(bar_positions) * current_percentage)
for(var/curr_number in 1 to bars_to_add)
var/bar_color
switch(curr_number)
diff --git a/code/datums/components/tug_towards.dm b/code/datums/components/tug_towards.dm
index f34cc05bc97f..6b1a094b6d1e 100644
--- a/code/datums/components/tug_towards.dm
+++ b/code/datums/components/tug_towards.dm
@@ -108,8 +108,8 @@
tuggers += 1
var/strength = tugging_to_targets[target]
- total_tug_x += SIGN(target.x - atom_parent.x) * strength
- total_tug_y += SIGN(target.y - atom_parent.y) * strength
+ total_tug_x += sign(target.x - atom_parent.x) * strength
+ total_tug_y += sign(target.y - atom_parent.y) * strength
// Intentionally not trig--something at a corner with a strength of 1 should have
// you at the corner, rather than root(2).
diff --git a/code/datums/components/twohanded.dm b/code/datums/components/twohanded.dm
index 4414b4661707..a65d8c4f3c78 100644
--- a/code/datums/components/twohanded.dm
+++ b/code/datums/components/twohanded.dm
@@ -147,6 +147,8 @@
RegisterSignal(parent, COMSIG_ITEM_SHARPEN_ACT, PROC_REF(on_sharpen))
RegisterSignal(parent, COMSIG_ITEM_APPLY_FANTASY_BONUSES, PROC_REF(apply_fantasy_bonuses))
RegisterSignal(parent, COMSIG_ITEM_REMOVE_FANTASY_BONUSES, PROC_REF(remove_fantasy_bonuses))
+ RegisterSignal(parent, COMSIG_ATOM_FINALIZE_MATERIAL_EFFECTS, PROC_REF(on_materials_updated))
+ RegisterSignal(parent, COMSIG_ATOM_FINALIZE_REMOVE_MATERIAL_EFFECTS, PROC_REF(on_materials_updated))
// Remove all siginals registered to the parent item
/datum/component/two_handed/UnregisterFromParent()
@@ -160,6 +162,8 @@
COMSIG_ITEM_SHARPEN_ACT,
COMSIG_ITEM_APPLY_FANTASY_BONUSES,
COMSIG_ITEM_REMOVE_FANTASY_BONUSES,
+ COMSIG_ATOM_FINALIZE_MATERIAL_EFFECTS,
+ COMSIG_ATOM_FINALIZE_REMOVE_MATERIAL_EFFECTS,
))
/// Triggered on equip of the item containing the component
@@ -419,6 +423,19 @@
unwield(source.loc)
force_multiplier = source.reset_fantasy_variable("force_multiplier", force_multiplier)
+/datum/component/two_handed/proc/on_materials_updated(obj/item/source, list/materials, datum/material/main_material)
+ SIGNAL_HANDLER
+ // With materials assigned we need to update our forces.
+ if (wielded)
+ // Materials modify force multiplicatively! Most of the time, for snowflakes they gotta handle it themselves
+ if (!isnull(force_wielded))
+ force_unwielded *= source.force / force_wielded
+ force_wielded = source.force
+ else
+ if (!isnull(force_unwielded))
+ force_wielded *= source.force / force_unwielded
+ force_unwielded = source.force
+
/**
* The offhand dummy item for two handed items
*/
diff --git a/code/datums/components/weatherannouncer.dm b/code/datums/components/weatherannouncer.dm
index c514aa77b150..3707663fe595 100644
--- a/code/datums/components/weatherannouncer.dm
+++ b/code/datums/components/weatherannouncer.dm
@@ -2,6 +2,9 @@
#define WEATHER_ALERT_INCOMING 1
#define WEATHER_ALERT_IMMINENT_OR_ACTIVE 2
+#define WEATHER_CLEAR_DELAY 2 MINUTES
+#define WEATHER_INCOMING_DELAY 30 SECONDS
+
/// Component which makes you yell about what the weather is
/datum/component/weather_announcer
/// Currently displayed warning level
@@ -119,19 +122,22 @@
if(WEATHER_ALERT_INCOMING)
light.set_light_color(LIGHT_COLOR_DIM_YELLOW)
if(WEATHER_ALERT_IMMINENT_OR_ACTIVE)
- light.set_light_color(LIGHT_COLOR_INTENSE_RED)
+ if (is_weather_dangerous)
+ light.set_light_color(LIGHT_COLOR_INTENSE_RED)
+ else
+ light.set_light_color(LIGHT_COLOR_DIM_YELLOW)
light.update_light()
/// Returns a string we should display to communicate what you should be doing
/datum/component/weather_announcer/proc/get_warning_message()
- if (!is_weather_dangerous)
- return "No risk expected from incoming weather front."
switch(warning_level)
if(WEATHER_ALERT_CLEAR)
return "All clear, no weather alerts to report."
if(WEATHER_ALERT_INCOMING)
return "Weather front incoming, begin to seek shelter."
if(WEATHER_ALERT_IMMINENT_OR_ACTIVE)
+ if (!is_weather_dangerous)
+ return "No risk expected from incoming weather front."
return "Weather front imminent, find shelter immediately."
return "Error in meteorological calculation. Please report this deviation to a trained programmer."
@@ -143,6 +149,7 @@
for(var/datum/weather/check_weather as anything in SSweather.processing)
if(!(check_weather.weather_flags & WEATHER_BAROMETER) || check_weather.stage == WIND_DOWN_STAGE || check_weather.stage == END_STAGE)
continue
+
for (var/mining_level in mining_z_levels)
if(mining_level in check_weather.impacted_z_levels)
warning_level = WEATHER_ALERT_IMMINENT_OR_ACTIVE
@@ -151,28 +158,45 @@
var/time_until_next = INFINITY
for(var/mining_level in mining_z_levels)
- var/next_time = timeleft(SSweather.next_hit_by_zlevel["[mining_level ]"]) || INFINITY
- if (next_time && next_time < time_until_next)
+ // About to start!
+ if (SSweather.eligible_zlevels["[mining_level]"])
+ time_until_next = 0
+ break
+
+ var/next_time = timeleft(SSweather.next_hit_by_zlevel["[mining_level]"])
+ if (!isnull(next_time) && next_time < time_until_next)
time_until_next = next_time
- if(!check_accuracy())
- if(!accuracy_mod && radar_z_trait)
- accuracy_mod = rand(-9, 9) * 5 SECONDS
+ if(check_accuracy())
+ return time_until_next
+
+ if(!accuracy_mod && radar_z_trait)
+ accuracy_mod = rand(-9, 9) * 5 SECONDS
- time_until_next = max(10 SECONDS, time_until_next + accuracy_mod)
+ var/maximum_value = INFINITY
+ // Don't backtrack on weather warnings every second of process() if we're poorly calibrated
+ if (warning_level == WEATHER_ALERT_INCOMING)
+ maximum_value = WEATHER_CLEAR_DELAY
+ else if (warning_level == WEATHER_ALERT_IMMINENT_OR_ACTIVE)
+ maximum_value = WEATHER_INCOMING_DELAY
+ time_until_next = clamp(time_until_next + accuracy_mod, min(time_until_next, 10 SECONDS), max(time_until_next, maximum_value))
return time_until_next
/// Polls existing weather for what kind of warnings we should be displaying.
/datum/component/weather_announcer/proc/set_current_alert_level()
var/time_until_next = time_till_storm()
+ is_weather_dangerous = FALSE
+
if(isnull(time_until_next))
+ warning_level = WEATHER_ALERT_CLEAR
return // No problems if there are no mining z levels
- if(time_until_next >= 2 MINUTES)
+
+ if(time_until_next > WEATHER_CLEAR_DELAY)
warning_level = WEATHER_ALERT_CLEAR
return
- if(time_until_next >= 30 SECONDS)
+ if(time_until_next > WEATHER_INCOMING_DELAY)
warning_level = WEATHER_ALERT_INCOMING
return
@@ -182,10 +206,14 @@
for(var/datum/weather/check_weather as anything in SSweather.processing)
if(!(check_weather.weather_flags & WEATHER_BAROMETER) || check_weather.stage == WIND_DOWN_STAGE || check_weather.stage == END_STAGE)
continue
+
var/list/mining_z_levels = SSmapping.levels_by_trait(radar_z_trait)
for(var/mining_level in mining_z_levels)
- if(mining_level in check_weather.impacted_z_levels)
- is_weather_dangerous = (check_weather.weather_flags & FUNCTIONAL_WEATHER)
+ if(!(mining_level in check_weather.impacted_z_levels))
+ continue
+
+ if(check_weather.weather_flags & FUNCTIONAL_WEATHER)
+ is_weather_dangerous = TRUE
return
/datum/component/weather_announcer/proc/on_examine(atom/radio, mob/examiner, list/examine_texts)
@@ -229,3 +257,6 @@
#undef WEATHER_ALERT_CLEAR
#undef WEATHER_ALERT_INCOMING
#undef WEATHER_ALERT_IMMINENT_OR_ACTIVE
+
+#undef WEATHER_CLEAR_DELAY
+#undef WEATHER_INCOMING_DELAY
diff --git a/code/datums/diseases/_disease.dm b/code/datums/diseases/_disease.dm
index 0195a3e3cdf3..e5ee43e8100a 100644
--- a/code/datums/diseases/_disease.dm
+++ b/code/datums/diseases/_disease.dm
@@ -5,11 +5,19 @@
var/spread_flags = DISEASE_SPREAD_AIRBORNE | DISEASE_SPREAD_CONTACT_FLUIDS | DISEASE_SPREAD_CONTACT_SKIN
//Fluff
+
+ /// What type of disease this is
var/form = "Virus"
+ /// The name of the disease (this one's kind of important)
var/name = "No disease"
+ /// The description of what the disease is and does
var/desc = ""
- var/agent = "some microbes"
+ /// The agent that causes the disease, for example "virus", "bacteria", "parasite", "curse"
+ var/agent = "Unknown"
+ /// A string describing how the disease spreads, for example "Airborne", "Blood", "Skin contact", "Magic"
+ /// (Keep this strictly for how it spreads BETWEEN hosts, NOT how it affected the initial host. Use agent var for that)
var/spread_text = ""
+ /// A string describing how the disease can be cured, for example "Toxin remover", "Antibiotics", "Rest and hydration", "Prayers to the gods"
var/cure_text = ""
//Stages
@@ -411,3 +419,16 @@
return 6
if(DISEASE_SEVERITY_BIOHAZARD)
return 7
+
+/proc/get_disease_spread_text(spread_flags)
+ if(spread_flags & DISEASE_SPREAD_AIRBORNE)
+ return "Airborne"
+ if(spread_flags & DISEASE_SPREAD_CONTACT_SKIN)
+ return "Skin contact"
+ if(spread_flags & DISEASE_SPREAD_CONTACT_FLUIDS)
+ return "Fluid contact"
+ if(spread_flags & DISEASE_SPREAD_BLOOD)
+ return "Blood"
+ if(spread_flags & DISEASE_SPREAD_NON_CONTAGIOUS)
+ return "None"
+ return "Unknown"
diff --git a/code/datums/diseases/adrenal_crisis.dm b/code/datums/diseases/adrenal_crisis.dm
index 72882bd36948..1f9c73e6bd06 100644
--- a/code/datums/diseases/adrenal_crisis.dm
+++ b/code/datums/diseases/adrenal_crisis.dm
@@ -2,16 +2,17 @@
form = "Condition"
name = "Adrenal Crisis"
max_stages = 2
- cure_text = "Trauma"
+ cure_text = "Trauma (Adrenaline / \"" + /datum/reagent/determination::name + "\")"
cures = list(/datum/reagent/determination)
cure_chance = 10
- agent = "Shitty Adrenal Glands"
+ agent = "Kronkaine Abuse"
viable_mobtypes = list(/mob/living/carbon/human)
spreading_modifier = 1
- desc = "If left untreated the subject will suffer from lethargy, dizziness and periodic loss of consciousness."
+ desc = "A rare condition caused by continued abuse of Kronkaine. \
+ If left untreated the subject will suffer from lethargy, dizziness and periodic loss of consciousness."
severity = DISEASE_SEVERITY_MEDIUM
spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS
- spread_text = "Organ failure"
+ spread_text = "None"
visibility_flags = HIDDEN_PANDEMIC
bypasses_immunity = TRUE
diff --git a/code/datums/diseases/advance/advance.dm b/code/datums/diseases/advance/advance.dm
index cfe661ebac56..5b046a6fb678 100644
--- a/code/datums/diseases/advance/advance.dm
+++ b/code/datums/diseases/advance/advance.dm
@@ -25,6 +25,7 @@
spread_text = "Unknown"
viable_mobtypes = list(/mob/living/carbon/human)
cures = null
+ visibility_flags = HIDDEN_BOOK // we have unique handling for advance diseases in the book
// NEW VARS
var/list/properties = list()
diff --git a/code/datums/diseases/advance/symptoms/heal.dm b/code/datums/diseases/advance/symptoms/heal.dm
index 92099d101ab9..29fd91542e88 100644
--- a/code/datums/diseases/advance/symptoms/heal.dm
+++ b/code/datums/diseases/advance/symptoms/heal.dm
@@ -1,4 +1,5 @@
/datum/symptom/heal
+ abstract_type = /datum/symptom/heal
name = "Basic Healing (does nothing)" //warning for adminspawn viruses
desc = "You should not be seeing this."
stealth = 0
diff --git a/code/datums/diseases/advance/symptoms/symptoms.dm b/code/datums/diseases/advance/symptoms/symptoms.dm
index 0a26353e0f6d..64f110539c20 100644
--- a/code/datums/diseases/advance/symptoms/symptoms.dm
+++ b/code/datums/diseases/advance/symptoms/symptoms.dm
@@ -40,7 +40,7 @@
///If the symptom requires an organ for the effects to function, robotic organs are immune to disease unless inorganic biology symptom is present
var/required_organ
///The remedy for this symptom
- var/symptom_cure = /datum/reagent/medicine/spaceacillin
+ var/datum/reagent/symptom_cure = /datum/reagent/medicine/spaceacillin
///What color the cure text shows up in the pandemic UI
var/cure_color = "green"
///A remedied symptom has no effect and contributes to the cure
diff --git a/code/datums/diseases/anaphylaxis.dm b/code/datums/diseases/anaphylaxis.dm
index 544082357557..f28a734d10e5 100644
--- a/code/datums/diseases/anaphylaxis.dm
+++ b/code/datums/diseases/anaphylaxis.dm
@@ -1,9 +1,9 @@
/datum/disease/anaphylaxis
- form = "Shock"
+ form = "Condition"
name = "Anaphylaxis"
desc = "Patient is undergoing a life-threatening allergic reaction and will die if not treated."
max_stages = 3
- cure_text = "Epinephrine"
+ cure_text = /datum/reagent/medicine/epinephrine::name
cures = list(/datum/reagent/medicine/epinephrine)
cure_chance = 20
agent = "Allergy"
diff --git a/code/datums/diseases/anxiety.dm b/code/datums/diseases/anxiety.dm
index 2b6df5808f1c..65348d78f494 100644
--- a/code/datums/diseases/anxiety.dm
+++ b/code/datums/diseases/anxiety.dm
@@ -1,14 +1,14 @@
/datum/disease/anxiety
name = "Severe Anxiety"
- form = "Infection"
+ form = "Condition"
max_stages = 4
- spread_text = "On contact"
+ spread_text = "Skin contact"
spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_CONTACT_FLUIDS
- cure_text = "Ethanol (Liquid Courage)"
+ cure_text = /datum/reagent/consumable/ethanol::name + " (Liquid Courage)"
cures = list(/datum/reagent/consumable/ethanol)
agent = "Excess Lepidopticides"
viable_mobtypes = list(/mob/living/carbon/human)
- desc = "If left untreated subject will regurgitate butterflies."
+ desc = "A well documented condition leading to 'butterflies in the stomach' in a literal sense, which are often regurgitated."
severity = DISEASE_SEVERITY_MINOR
diff --git a/code/datums/diseases/asthma_attack.dm b/code/datums/diseases/asthma_attack.dm
index 3c01accc1134..63b40be45ef3 100644
--- a/code/datums/diseases/asthma_attack.dm
+++ b/code/datums/diseases/asthma_attack.dm
@@ -3,14 +3,14 @@
name = "Asthma attack"
desc = "Subject is undergoing a autoimmune response which threatens to close the esophagus and halt all respiration, leading to death. \
Minor asthma attacks may disappear on their own, but all are dangerous."
- cure_text = "Albuterol/Surgical intervention"
+ cure_text = /datum/reagent/medicine/albuterol::name + " or surgical intervention"
cures = list(/datum/reagent/medicine/albuterol)
agent = "Inflammatory"
viable_mobtypes = list(/mob/living/carbon/human)
disease_flags = CURABLE
spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS
- spread_text = "Inflammatory"
- visibility_flags = HIDDEN_PANDEMIC
+ spread_text = "None"
+ visibility_flags = HIDDEN_PANDEMIC|HIDDEN_BOOK
bypasses_immunity = TRUE
disease_flags = CURABLE|INCREMENTAL_CURE
required_organ = ORGAN_SLOT_LUNGS
diff --git a/code/datums/diseases/beesease.dm b/code/datums/diseases/beesease.dm
index ead9c35da18d..c3a53126ca71 100644
--- a/code/datums/diseases/beesease.dm
+++ b/code/datums/diseases/beesease.dm
@@ -1,14 +1,14 @@
/datum/disease/beesease
name = "Beesease"
- form = "Infection"
+ form = "Parasite"
max_stages = 4
- spread_text = "On contact"
+ spread_text = "Skin contact"
spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_CONTACT_FLUIDS
- cure_text = "Sugar"
+ cure_text = /datum/reagent/consumable/sugar::name
cures = list(/datum/reagent/consumable/sugar)
agent = "Apidae Infection"
viable_mobtypes = list(/mob/living/carbon/human)
- desc = "If left untreated subject will regurgitate bees."
+ desc = "A strange disease that leads to the gestation of bees in the subject's stomach, which are often regurgitated."
severity = DISEASE_SEVERITY_MEDIUM
infectable_biotypes = MOB_ORGANIC|MOB_UNDEAD //bees nesting in corpses
diff --git a/code/datums/diseases/brainrot.dm b/code/datums/diseases/brainrot.dm
index e3a4e3d35a2d..ad757afc2004 100644
--- a/code/datums/diseases/brainrot.dm
+++ b/code/datums/diseases/brainrot.dm
@@ -1,14 +1,14 @@
/datum/disease/brainrot
name = "Brainrot"
max_stages = 4
- spread_text = "On contact"
+ spread_text = "Skin contact"
spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_CONTACT_FLUIDS
- cure_text = "Mannitol"
+ cure_text = /datum/reagent/medicine/mannitol::name
cures = list(/datum/reagent/medicine/mannitol)
agent = "Cryptococcus Cosmosis"
viable_mobtypes = list(/mob/living/carbon/human)
cure_chance = 7.5 //higher chance to cure, since two reagents are required
- desc = "This disease destroys the brain cells, causing brain fever, brain necrosis and general intoxication."
+ desc = "A disease which targets brain cells, leading to brain fog - though it is otherwise non-lethal."
required_organ = ORGAN_SLOT_BRAIN
severity = DISEASE_SEVERITY_HARMFUL
bypasses_immunity = TRUE
diff --git a/code/datums/diseases/chronic_illness.dm b/code/datums/diseases/chronic_illness.dm
index 43dd24a7cb19..55aba274139a 100644
--- a/code/datums/diseases/chronic_illness.dm
+++ b/code/datums/diseases/chronic_illness.dm
@@ -1,18 +1,21 @@
/datum/disease/chronic_illness
name = "Hereditary Manifold Sickness"
max_stages = 5
- spread_text = "Non-communicable disease"
+ spread_text = "None"
spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS
disease_flags = CHRONIC
infectable_biotypes = MOB_ORGANIC | MOB_MINERAL | MOB_ROBOTIC
process_dead = TRUE
stage_prob = 0.25
- cure_text = "Sansufentanyl"
+ cure_text = "Abated by " + /datum/reagent/medicine/sansufentanyl::name
cures = list(/datum/reagent/medicine/sansufentanyl)
infectivity = 0
agent = "Quantum Entanglement"
viable_mobtypes = list(/mob/living/carbon/human)
- desc = "A disease discovered in an Interdyne laboratory caused by subjection to timestream correction technology."
+ desc = "A disease discovered in an Interdyne laboratory caused by subjection to timestream correction technology. \
+ It is completely uncurable - though it can be treated to reverse its progression - and non-contagious. \
+ If left untreated the subject will suffer from a variety of symptoms, \
+ including but not limited to dizziness, nausea, heart palpitations, and in the final stages, death."
severity = DISEASE_SEVERITY_UNCURABLE
bypasses_immunity = TRUE
diff --git a/code/datums/diseases/cold.dm b/code/datums/diseases/cold.dm
index 990a138e9ad4..87ac9ea815af 100644
--- a/code/datums/diseases/cold.dm
+++ b/code/datums/diseases/cold.dm
@@ -1,8 +1,8 @@
/datum/disease/cold
name = "The Cold"
- desc = "If left untreated the subject will contract the flu."
+ desc = "A common, mildly annoying contagion. If left untreated the subject will contract the flu."
max_stages = 3
- cure_text = "Rest & Spaceacillin"
+ cure_text = /datum/reagent/medicine/spaceacillin::name + " or rest"
cures = list(/datum/reagent/medicine/spaceacillin)
agent = "XY-rhinovirus"
viable_mobtypes = list(/mob/living/carbon/human)
@@ -11,6 +11,11 @@
severity = DISEASE_SEVERITY_NONTHREAT
required_organ = ORGAN_SLOT_LUNGS
+/datum/disease/cold/cure(add_resistance)
+ // buy one, get one free
+ if(add_resistance && affected_mob)
+ LAZYOR(affected_mob.disease_resistances, "[/datum/disease/cold9]")
+ return ..()
/datum/disease/cold/stage_act(seconds_per_tick)
. = ..()
diff --git a/code/datums/diseases/cold9.dm b/code/datums/diseases/cold9.dm
index 667be8c88e54..1af1190cc30c 100644
--- a/code/datums/diseases/cold9.dm
+++ b/code/datums/diseases/cold9.dm
@@ -1,16 +1,23 @@
/datum/disease/cold9
name = "The Cold"
max_stages = 3
- spread_text = "On contact"
+ spread_text = "Skin contact"
spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_CONTACT_FLUIDS
- cure_text = "Common Cold Anti-bodies & Spaceacillin"
+ cure_text = /datum/reagent/medicine/spaceacillin::name + " or common Cold antibodies"
cures = list(/datum/reagent/medicine/spaceacillin)
agent = "ICE9-rhinovirus"
viable_mobtypes = list(/mob/living/carbon/human)
- desc = "If left untreated the subject will slow, as if partly frozen."
+ desc = "An adaption of the common cold, slightly more dangerous in nature. \
+ If left untreated the subject will slow, as if partly frozen."
severity = DISEASE_SEVERITY_HARMFUL
required_organ = ORGAN_SLOT_LUNGS
+/datum/disease/cold9/cure(add_resistance)
+ // buy one, get one free
+ if(add_resistance && affected_mob)
+ LAZYOR(affected_mob.disease_resistances, "[/datum/disease/cold]")
+ return ..()
+
/datum/disease/cold9/stage_act(seconds_per_tick)
. = ..()
if(!.)
diff --git a/code/datums/diseases/death_sandwich_poisoning.dm b/code/datums/diseases/death_sandwich_poisoning.dm
index 375d97c9eee3..deaa9787bf53 100644
--- a/code/datums/diseases/death_sandwich_poisoning.dm
+++ b/code/datums/diseases/death_sandwich_poisoning.dm
@@ -2,12 +2,12 @@
name = "Death Sandwich Poisoning"
desc = "If left untreated the subject will ultimately perish."
form = "Condition"
- spread_text = "Unknown"
max_stages = 3
- cure_text = "Anacea" // I ain't about to make a second sandwich to counteract the first one, so closest thing I'm going for is this.
+ cure_text = /datum/reagent/toxin/anacea::name // I ain't about to make a second sandwich to counteract the first one, so closest thing I'm going for is this.
cures = list(/datum/reagent/toxin/anacea)
cure_chance = 4
- agent = "eating the Death Sandwich wrong"
+ agent = "Eating the Death Sandwich wrong"
+ spread_text = "None"
viable_mobtypes = list(/mob/living/carbon/human)
severity = DISEASE_SEVERITY_DANGEROUS
disease_flags = CURABLE
@@ -57,4 +57,3 @@
if(SPT_PROB(1.5, seconds_per_tick))
to_chat(affected_mob, span_danger("You try to scream, but nothing comes out!"))
affected_mob.set_silence_if_lower(5 SECONDS)
-
diff --git a/code/datums/diseases/decloning.dm b/code/datums/diseases/decloning.dm
index b3017b9ca211..93bf0ef99dd5 100644
--- a/code/datums/diseases/decloning.dm
+++ b/code/datums/diseases/decloning.dm
@@ -3,14 +3,14 @@
name = "Cellular Degeneration"
max_stages = 5
stage_prob = 0.5
- cure_text = "Rezadone, Mutadone for prolonging, or death."
+ cure_text = /datum/reagent/medicine/rezadone::name + ", abated by " + /datum/reagent/medicine/mutadone::name + ", or death"
agent = "Severe Genetic Damage"
viable_mobtypes = list(/mob/living/carbon/human)
desc = @"If left untreated the subject will [REDACTED]!"
severity = "Dangerous!"
cures = list(/datum/reagent/medicine/rezadone)
spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS
- spread_text = "Organic meltdown"
+ spread_text = "None"
process_dead = TRUE
bypasses_immunity = TRUE
diff --git a/code/datums/diseases/dna_spread.dm b/code/datums/diseases/dna_spread.dm
index 2630a805c747..99be12f8821b 100644
--- a/code/datums/diseases/dna_spread.dm
+++ b/code/datums/diseases/dna_spread.dm
@@ -1,16 +1,18 @@
/datum/disease/dnaspread
name = "Space Retrovirus"
max_stages = 4
- spread_text = "On contact"
+ spread_text = "Skin contact"
spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_CONTACT_FLUIDS
- cure_text = "Mutadone"
+ cure_text = /datum/reagent/medicine/mutadone::name
cures = list(/datum/reagent/medicine/mutadone)
disease_flags = CAN_CARRY|CAN_RESIST|CURABLE
- agent = "S4E1 retrovirus"
+ agent = "S4E1 Retrovirus"
viable_mobtypes = list(/mob/living/carbon/human)
var/datum/dna/original_dna = null
var/transformed = 0
- desc = "This disease transplants the genetic code of the initial vector into new hosts."
+ desc = "A disease which transplants the genetic code of the initial vector into new hosts. \
+ While patient zero will be asymptomatic, all subsequent hosts will shows symptoms similar to that of a common cold or flu \
+ - until eventually transforming into a genetic copy of patient zero."
severity = DISEASE_SEVERITY_MEDIUM
bypasses_immunity = TRUE
diff --git a/code/datums/diseases/fake_gbs.dm b/code/datums/diseases/fake_gbs.dm
index 59ffb1e1c9a5..42776c72652e 100644
--- a/code/datums/diseases/fake_gbs.dm
+++ b/code/datums/diseases/fake_gbs.dm
@@ -1,15 +1,15 @@
/datum/disease/fake_gbs
name = "GBS"
+ desc = "An extremely rare and dangerous disease that has been researched little due to its potentially apocalyptic nature."
max_stages = 5
- spread_text = "On contact"
+ spread_text = "Skin contact"
spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_CONTACT_FLUIDS
- cure_text = "Synaptizine & Sulfur"
+ cure_text = /datum/reagent/medicine/synaptizine::name + " & " + /datum/reagent/sulfur::name
cures = list(/datum/reagent/medicine/synaptizine,/datum/reagent/sulfur)
agent = "Gravitokinetic Bipotential SADS-"
viable_mobtypes = list(/mob/living/carbon/human)
- desc = "If left untreated death will occur."
severity = DISEASE_SEVERITY_BIOHAZARD
-
+ visibility_flags = HIDDEN_BOOK
/datum/disease/fake_gbs/stage_act(seconds_per_tick)
. = ..()
diff --git a/code/datums/diseases/floor_diseases/carpellosis.dm b/code/datums/diseases/floor_diseases/carpellosis.dm
index 50a6481c575e..e19c070d2c83 100644
--- a/code/datums/diseases/floor_diseases/carpellosis.dm
+++ b/code/datums/diseases/floor_diseases/carpellosis.dm
@@ -3,10 +3,12 @@
/// Caused by dirty food. Makes you growl at people and bite them spontaneously.
/datum/disease/carpellosis
name = "Carpellosis"
- desc = "You have an angry space carp inside."
+ desc = "An angry space carp inside has infested the host's stomach, \
+ leading to an uncontrollable urge to gnash at people and wag your tail."
form = "Parasite"
agent = "Carp Ella"
- cure_text = "Chlorine"
+ cure_text = /datum/reagent/chlorine::name
+ spread_text = "None"
cures = list(/datum/reagent/chlorine)
viable_mobtypes = list(/mob/living/carbon/human)
spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS
diff --git a/code/datums/diseases/floor_diseases/gastritium.dm b/code/datums/diseases/floor_diseases/gastritium.dm
index cbee6e1fe879..228cfa7496a4 100644
--- a/code/datums/diseases/floor_diseases/gastritium.dm
+++ b/code/datums/diseases/floor_diseases/gastritium.dm
@@ -2,9 +2,10 @@
/datum/disease/gastritium
name = "Gastritium"
desc = "If left untreated, may manifest in severe Tritium heartburn."
- form = "Infection"
+ form = "Bacteria"
agent = "Atmobacter Polyri"
- cure_text = "Milk"
+ cure_text = /datum/reagent/consumable/milk::name
+ spread_text = "None"
cures = list(/datum/reagent/consumable/milk)
viable_mobtypes = list(/mob/living/carbon/human)
spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS
diff --git a/code/datums/diseases/floor_diseases/nebula_nausea.dm b/code/datums/diseases/floor_diseases/nebula_nausea.dm
index 9f8e45648deb..2fefc3f1eb0f 100644
--- a/code/datums/diseases/floor_diseases/nebula_nausea.dm
+++ b/code/datums/diseases/floor_diseases/nebula_nausea.dm
@@ -4,7 +4,8 @@
desc = "You can't contain the colorful beauty of the cosmos inside."
form = "Condition"
agent = "Stars"
- cure_text = "Space Cleaner"
+ cure_text = /datum/reagent/space_cleaner::name
+ spread_text = "None"
cures = list(/datum/reagent/space_cleaner)
viable_mobtypes = list(/mob/living/carbon/human)
spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS
diff --git a/code/datums/diseases/flu.dm b/code/datums/diseases/flu.dm
index 97563ed515cc..3f7506b80d59 100644
--- a/code/datums/diseases/flu.dm
+++ b/code/datums/diseases/flu.dm
@@ -2,16 +2,22 @@
name = "The Flu"
max_stages = 3
spread_text = "Airborne"
- cure_text = "Spaceacillin"
+ cure_text = /datum/reagent/medicine/spaceacillin::name + ", abated by rest"
cures = list(/datum/reagent/medicine/spaceacillin)
cure_chance = 5
- agent = "H13N1 flu virion"
+ agent = "H13N1 Flu Virion"
viable_mobtypes = list(/mob/living/carbon/human)
spreading_modifier = 0.75
- desc = "If left untreated the subject will feel quite unwell."
+ desc = "A common, mildly annoying contagion. If left untreated the subject will feel quite unwell."
severity = DISEASE_SEVERITY_MINOR
required_organ = ORGAN_SLOT_LUNGS
+/datum/disease/flu/cure(add_resistance)
+ // buy one, get one free
+ if(add_resistance && affected_mob)
+ LAZYOR(affected_mob.disease_resistances, "[/datum/disease/fluspanish]")
+ return ..()
+
/datum/disease/flu/stage_act(seconds_per_tick)
. = ..()
if(!.)
diff --git a/code/datums/diseases/fluspanish.dm b/code/datums/diseases/fluspanish.dm
index 7fd35c631b40..d7c1dd4f752a 100644
--- a/code/datums/diseases/fluspanish.dm
+++ b/code/datums/diseases/fluspanish.dm
@@ -2,16 +2,23 @@
name = "Spanish inquisition Flu"
max_stages = 3
spread_text = "Airborne"
- cure_text = "Spaceacillin & Anti-bodies to the common flu"
+ cure_text = /datum/reagent/medicine/spaceacillin::name + " or common Flu antibodies"
cures = list(/datum/reagent/medicine/spaceacillin)
cure_chance = 5
- agent = "1nqu1s1t10n flu virion"
+ agent = "1nqu1s1t10n Flu Virion"
viable_mobtypes = list(/mob/living/carbon/human)
spreading_modifier = 0.75
- desc = "If left untreated the subject will burn to death for being a heretic."
+ desc = "An adaptation of the common flu, slightly more dangerous in nature. \
+ If left untreated the subject will burn to death for being a heretic."
severity = DISEASE_SEVERITY_DANGEROUS
required_organ = ORGAN_SLOT_LUNGS
+/datum/disease/fluspanish/cure(add_resistance)
+ // buy one, get one free
+ if(add_resistance && affected_mob)
+ LAZYOR(affected_mob.disease_resistances, "[/datum/disease/flu]")
+ return ..()
+
/datum/disease/fluspanish/stage_act(seconds_per_tick)
. = ..()
if(!.)
diff --git a/code/datums/diseases/gastrolisis.dm b/code/datums/diseases/gastrolisis.dm
index 2e562051bbd1..9347d29ba957 100644
--- a/code/datums/diseases/gastrolisis.dm
+++ b/code/datums/diseases/gastrolisis.dm
@@ -1,10 +1,11 @@
/datum/disease/gastrolosis
name = "Invasive Gastrolosis"
+ desc = "A bizarre disease that causes the host to grow snail-like features, eventually turning into a human-snail hybrid."
max_stages = 4
- spread_text = "Unknown"
+ spread_text = "None"
spread_flags = DISEASE_SPREAD_SPECIAL
- cure_text = "Salt and mutadone"
- agent = "Agent S and DNA restructuring"
+ cure_text = /datum/reagent/consumable/salt::name + " & " + /datum/reagent/medicine/mutadone::name
+ agent = "Agent S and DNA Restructuring"
viable_mobtypes = list(/mob/living/carbon/human)
stage_prob = 0.5
disease_flags = CURABLE
diff --git a/code/datums/diseases/gbs.dm b/code/datums/diseases/gbs.dm
index 162a688a86e1..c5e0e760e944 100644
--- a/code/datums/diseases/gbs.dm
+++ b/code/datums/diseases/gbs.dm
@@ -1,9 +1,10 @@
/datum/disease/gbs
name = "GBS"
+ desc = "An extremely rare and dangerous disease that has been researched little due to its potentially apocalyptic nature."
max_stages = 4
- spread_text = "On contact"
+ spread_text = "Skin contact"
spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_CONTACT_FLUIDS
- cure_text = "Synaptizine & Sulfur"
+ cure_text = /datum/reagent/medicine/synaptizine::name + " & " + /datum/reagent/sulfur::name
cures = list(/datum/reagent/medicine/synaptizine,/datum/reagent/sulfur)
cure_chance = 7.5 //higher chance to cure, since two reagents are required
agent = "Gravitokinetic Bipotential SADS+"
diff --git a/code/datums/diseases/magnitis.dm b/code/datums/diseases/magnitis.dm
index 295fa0b6a7e4..332c0667d60a 100644
--- a/code/datums/diseases/magnitis.dm
+++ b/code/datums/diseases/magnitis.dm
@@ -2,13 +2,14 @@
name = "Magnitis"
max_stages = 4
spread_text = "Airborne"
- cure_text = "Iron"
+ cure_text = /datum/reagent/iron
cures = list(/datum/reagent/iron)
agent = "Fukkos Miracos"
viable_mobtypes = list(/mob/living/carbon/human)
disease_flags = CAN_CARRY|CAN_RESIST|CURABLE
spreading_modifier = 0.75
- desc = "This disease disrupts the magnetic field of your body, making it act as if a powerful magnet. Injections of iron help stabilize the field."
+ desc = "This disease disrupts the magnetic field of the subjects body, making it act as if a powerful magnet. \
+ Injections of iron will help stabilize the field."
severity = DISEASE_SEVERITY_MEDIUM
infectable_biotypes = MOB_ORGANIC|MOB_ROBOTIC
bypasses_immunity = TRUE
diff --git a/code/datums/diseases/parasitic_infection.dm b/code/datums/diseases/parasitic_infection.dm
index ec008c504956..a47d1e175b58 100644
--- a/code/datums/diseases/parasitic_infection.dm
+++ b/code/datums/diseases/parasitic_infection.dm
@@ -2,12 +2,13 @@
form = "Parasite"
name = "Parasitic Infection"
max_stages = 4
- cure_text = "Surgical removal of the liver."
- agent = "Consuming Live Parasites"
- spread_text = "Non-Biological"
+ cure_text = "Surgical removal of the liver"
+ agent = "Food-bourne Parasite"
+ spread_text = "None"
viable_mobtypes = list(/mob/living/carbon/human)
spreading_modifier = 1
- desc = "If left untreated the subject will passively lose nutrients, and eventually lose their liver."
+ desc = "An unknown parasite, likely entering the body via improperly prepared food, has infected the subject's liver. \
+ If left untreated the subject will passively lose nutrients, and eventually lose their liver."
severity = DISEASE_SEVERITY_HARMFUL
disease_flags = CAN_CARRY|CAN_RESIST
spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS
diff --git a/code/datums/diseases/parrotpossession.dm b/code/datums/diseases/parrotpossession.dm
index 0bb50df6b29a..4a66410e161d 100644
--- a/code/datums/diseases/parrotpossession.dm
+++ b/code/datums/diseases/parrotpossession.dm
@@ -1,15 +1,15 @@
/datum/disease/parrot_possession
name = "Parrot Possession"
max_stages = 1
- spread_text = "Paranormal"
+ spread_text = "None"
spread_flags = DISEASE_SPREAD_SPECIAL
disease_flags = CURABLE
- cure_text = "Holy Water."
+ cure_text = /datum/reagent/water/holywater::name
cures = list(/datum/reagent/water/holywater)
cure_chance = 10
agent = "Avian Vengence"
viable_mobtypes = list(/mob/living/carbon/human)
- desc = "Subject is possessed by the vengeful spirit of a parrot. Call the priest."
+ desc = "Subject is possessed by the vengeful spirit of a parrot. Call the Chaplain."
severity = DISEASE_SEVERITY_MEDIUM
infectable_biotypes = MOB_ORGANIC|MOB_UNDEAD|MOB_ROBOTIC|MOB_MINERAL
bypasses_immunity = TRUE //2spook
diff --git a/code/datums/diseases/pierrot_throat.dm b/code/datums/diseases/pierrot_throat.dm
index 950d98b22f06..5e3920e9f502 100644
--- a/code/datums/diseases/pierrot_throat.dm
+++ b/code/datums/diseases/pierrot_throat.dm
@@ -8,7 +8,8 @@
agent = "H0NI<42 Virus"
viable_mobtypes = list(/mob/living/carbon/human)
spreading_modifier = 0.75
- desc = "If left untreated the subject will probably drive others to insanity."
+ desc = "A strange virus rumored to originate from unwashed clown costumes. \
+ If left untreated the subject's vocal cords will be affected, likely driving others to insanity."
severity = DISEASE_SEVERITY_MEDIUM
required_organ = ORGAN_SLOT_TONGUE
diff --git a/code/datums/diseases/retrovirus.dm b/code/datums/diseases/retrovirus.dm
index 507c9340a5cc..bd0d35f5838d 100644
--- a/code/datums/diseases/retrovirus.dm
+++ b/code/datums/diseases/retrovirus.dm
@@ -3,9 +3,9 @@
max_stages = 4
spread_text = "Contact"
spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_CONTACT_FLUIDS
- cure_text = "Rest or an injection of mutadone"
+ cure_text = /datum/reagent/medicine/mutadone::name + " or rest"
cure_chance = 3
- agent = ""
+ agent = "Virus"
viable_mobtypes = list(/mob/living/carbon/human)
desc = "A DNA-altering retrovirus that scrambles the structural and unique enzymes of a host constantly."
severity = DISEASE_SEVERITY_HARMFUL
diff --git a/code/datums/diseases/rhumba_beat.dm b/code/datums/diseases/rhumba_beat.dm
index ac45d32e4035..a7818599e9e9 100644
--- a/code/datums/diseases/rhumba_beat.dm
+++ b/code/datums/diseases/rhumba_beat.dm
@@ -1,15 +1,17 @@
/datum/disease/rhumba_beat
name = "The Rhumba Beat"
+ desc = "They call me Cuban Pete - I'm the king of the Rumba Beat - When I play the maracas I go chick-chicky-boom, chick-chicky-boom."
max_stages = 5
- spread_text = "On contact"
+ spread_text = "Skin contact"
spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_CONTACT_FLUIDS
cure_text = "Chick Chicky Boom!"
- cures = list("plasma")
+ cures = list(/datum/reagent/toxin/plasma)
agent = "Unknown"
viable_mobtypes = list(/mob/living/carbon/human)
spreading_modifier = 1
severity = DISEASE_SEVERITY_BIOHAZARD
bypasses_immunity = TRUE
+ visibility_flags = HIDDEN_BOOK
/datum/disease/rhumba_beat/stage_act(seconds_per_tick)
. = ..()
diff --git a/code/datums/diseases/transformation.dm b/code/datums/diseases/transformation.dm
index a2de17f85319..d8de944aece8 100644
--- a/code/datums/diseases/transformation.dm
+++ b/code/datums/diseases/transformation.dm
@@ -1,7 +1,8 @@
/datum/disease/transformation
+ abstract_type = /datum/disease/transformation
name = "Transformation"
max_stages = 5
- spread_text = "Acute"
+ spread_text = "None"
spread_flags = DISEASE_SPREAD_SPECIAL
cure_text = "A coder's love (theoretical)."
agent = "Shenanigans"
@@ -99,7 +100,7 @@
/datum/disease/transformation/jungle_flu
name = "Jungle Flu"
- cure_text = "Death."
+ cure_text = "Death"
cures = list(/datum/reagent/medicine/adminordrazine)
spread_text = "Unknown"
spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS
@@ -148,7 +149,7 @@
/datum/disease/transformation/robot
name = "Robotic Transformation"
- cure_text = "An injection of copper."
+ cure_text = /datum/reagent/copper::name
cures = list(/datum/reagent/copper)
cure_chance = 2.5
agent = "R2D2 Nanomachines"
@@ -188,7 +189,7 @@
/datum/disease/transformation/xeno
name = "Xenomorph Transformation"
- cure_text = "Spaceacillin & Glycerol"
+ cure_text = /datum/reagent/medicine/spaceacillin::name + " & " + /datum/reagent/glycerol::name
cures = list(/datum/reagent/medicine/spaceacillin, /datum/reagent/glycerol)
cure_chance = 2.5
agent = "Rip-LEY Alien Microbes"
@@ -229,7 +230,7 @@
/datum/disease/transformation/slime
name = "Advanced Mutation Transformation"
- cure_text = "Frost oil"
+ cure_text = /datum/reagent/consumable/frostoil::name
cures = list(/datum/reagent/consumable/frostoil)
cure_chance = 55
agent = "Advanced Mutation Toxin"
@@ -317,7 +318,7 @@
/datum/disease/transformation/gondola
name = "Gondola Transformation"
- cure_text = "Condensed Capsaicin, ingested or injected." //getting pepper sprayed doesn't help
+ cure_text = /datum/reagent/consumable/condensedcapsaicin::name + " (not applied via vapor)"
cures = list(/datum/reagent/consumable/condensedcapsaicin) //beats the hippie crap right out of your system
cure_chance = 55
stage_prob = 2.5
diff --git a/code/datums/diseases/tuberculosis.dm b/code/datums/diseases/tuberculosis.dm
index 4c14337ba276..a729e53f51c1 100644
--- a/code/datums/diseases/tuberculosis.dm
+++ b/code/datums/diseases/tuberculosis.dm
@@ -1,14 +1,16 @@
/datum/disease/tuberculosis
- form = "Disease"
- name = "Fungal tuberculosis"
+ form = "Fungus"
+ name = "Fungal Tuberculosis"
max_stages = 5
spread_text = "Airborne"
- cure_text = "Spaceacillin & Convermol"
+ cure_text = /datum/reagent/medicine/spaceacillin::name + " & " + /datum/reagent/medicine/c2/convermol::name
cures = list(/datum/reagent/medicine/spaceacillin, /datum/reagent/medicine/c2/convermol)
- agent = "Fungal Tubercle bacillus Cosmosis"
+ agent = "Fungal Tubercle Bacillus Cosmosis"
viable_mobtypes = list(/mob/living/carbon/human)
cure_chance = 2.5 //like hell are you getting out of hell
- desc = "A rare highly transmissible virulent virus. Few samples exist, rumoured to be carefully grown and cultured by clandestine bio-weapon specialists. Causes fever, blood vomiting, lung damage, weight loss, and fatigue."
+ desc = "A rare and highly transmissible virulent fungus. \
+ Few samples exist, rumoured to be carefully grown and cultured by clandestine bio-weapon specialists. \
+ Causes fever, blood vomiting, lung damage, weight loss, fatigue, and eventually death."
required_organ = ORGAN_SLOT_LUNGS
severity = DISEASE_SEVERITY_BIOHAZARD
bypasses_immunity = TRUE // TB primarily impacts the lungs; it's also bacterial or fungal in nature; viral immunity should do nothing.
diff --git a/code/datums/diseases/weightlessness.dm b/code/datums/diseases/weightlessness.dm
index 877b16574ccc..12ceebe35a91 100644
--- a/code/datums/diseases/weightlessness.dm
+++ b/code/datums/diseases/weightlessness.dm
@@ -1,9 +1,9 @@
/datum/disease/weightlessness
name = "Localized Weightloss Malfunction"
max_stages = 4
- spread_text = "On Contact"
+ spread_text = "Skin contact"
spread_flags = DISEASE_SPREAD_BLOOD | DISEASE_SPREAD_CONTACT_SKIN | DISEASE_SPREAD_CONTACT_FLUIDS
- cure_text = "Liquid dark matter"
+ cure_text = /datum/reagent/liquid_dark_matter::name
cures = list(/datum/reagent/liquid_dark_matter)
agent = "Sub-quantum DNA Repulsion"
viable_mobtypes = list(/mob/living/carbon/human)
diff --git a/code/datums/diseases/wizarditis.dm b/code/datums/diseases/wizarditis.dm
index b4a420d22cf3..21c8f8828c32 100644
--- a/code/datums/diseases/wizarditis.dm
+++ b/code/datums/diseases/wizarditis.dm
@@ -2,7 +2,7 @@
name = "Wizarditis"
max_stages = 4
spread_text = "Airborne"
- cure_text = "The Manly Dorf"
+ cure_text = /datum/reagent/consumable/ethanol/manly_dorf::name + ", abated by magical grounding"
cures = list(/datum/reagent/consumable/ethanol/manly_dorf)
cure_chance = 100
agent = "Rincewindus Vulgaris"
diff --git a/code/datums/dna/blocks/dna_identity_block.dm b/code/datums/dna/blocks/dna_identity_block.dm
index 4da3bdf5f251..90f353779793 100644
--- a/code/datums/dna/blocks/dna_identity_block.dm
+++ b/code/datums/dna/blocks/dna_identity_block.dm
@@ -142,7 +142,7 @@
/datum/dna_block/identity/height/create_unique_block(mob/living/carbon/human/target)
var/max_height_index = length(dna_heights)
- var/mob_height_index = dna_heights.Find("[target.get_base_mob_height()]") || dna_heights.Find(HUMAN_HEIGHT_MEDIUM)
+ var/mob_height_index = dna_heights.Find(target.get_base_mob_height()) || dna_heights.Find(HUMAN_HEIGHT_MEDIUM)
return construct_block(mob_height_index, max_height_index, block_length)
/datum/dna_block/identity/height/apply_to_mob(mob/living/carbon/human/target, dna_hash)
diff --git a/code/datums/elements/blood_reagent.dm b/code/datums/elements/blood_reagent.dm
index 0c6cc76a5ada..863b07f45fe0 100644
--- a/code/datums/elements/blood_reagent.dm
+++ b/code/datums/elements/blood_reagent.dm
@@ -91,7 +91,7 @@
return
for(var/datum/disease/strain as anything in source.data["viruses"])
- if ((strain.spread_flags & DISEASE_SPREAD_SPECIAL) || (strain.spread_flags & DISEASE_SPREAD_NON_CONTAGIOUS))
+ if ((strain.spread_flags & (DISEASE_SPREAD_SPECIAL|DISEASE_SPREAD_NON_CONTAGIOUS)))
continue
if (methods & INGEST)
diff --git a/code/datums/elements/elevation.dm b/code/datums/elements/elevation.dm
index a0c3da5f3ab7..fcf6f524ff0b 100644
--- a/code/datums/elements/elevation.dm
+++ b/code/datums/elements/elevation.dm
@@ -130,15 +130,19 @@
COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON,
COMSIG_TURF_RESET_ELEVATION,
))
- REMOVE_TRAIT(source, TRAIT_ELEVATED_TURF, ELEVATION_SOURCE(src))
for(var/mob/living/living in source)
deelevate_mob(living)
UnregisterSignal(living, list(COMSIG_LIVING_SET_BUCKLED, SIGNAL_ADDTRAIT(TRAIT_IGNORE_ELEVATION), SIGNAL_REMOVETRAIT(TRAIT_IGNORE_ELEVATION)))
+ REMOVE_TRAIT(source, TRAIT_ELEVATED_TURF, ELEVATION_SOURCE(src))
return ..()
/datum/element/elevation_core/proc/on_entered(turf/source, atom/movable/entered, atom/old_loc)
SIGNAL_HANDLER
- if((isnull(old_loc) || !HAS_TRAIT_FROM(old_loc, TRAIT_ELEVATED_TURF, ELEVATION_SOURCE(src))) && isliving(entered))
+ // If the movement has been aborted by something else within the chain we need to abort
+ if(!isliving(entered) || entered.loc != source)
+ return
+
+ if(isnull(old_loc) || !HAS_TRAIT_FROM(old_loc, TRAIT_ELEVATED_TURF, ELEVATION_SOURCE(src)))
register_new_mob(entered, elevate_time = isturf(old_loc) && source.Adjacent(old_loc) ? ELEVATE_TIME : 0)
/datum/element/elevation_core/proc/on_initialized_on(turf/source, atom/movable/spawned)
@@ -178,7 +182,6 @@
// we want to avoid accidentally double-elevating anything they're buckled to (namely vehicles)
if(target.has_offset(source = ELEVATION_SOURCE(src)))
return
- ADD_TRAIT(target, TRAIT_MOB_ELEVATED, ELEVATION_SOURCE(src))
// We are buckled to something
if(target.buckled)
// We are buckled to a vehicle, so it also must be elevated
@@ -189,15 +192,18 @@
pass()
// We are buckled to some other object - perhaps the object itself - so skip
else
+ ADD_TRAIT(target, TRAIT_MOB_ELEVATED, ELEVATION_SOURCE(src))
return
+
target.add_offsets(ELEVATION_SOURCE(src), z_add = pixel_shift, animate = elevate_time > 0)
+ ADD_TRAIT(target, TRAIT_MOB_ELEVATED, ELEVATION_SOURCE(src))
/// Reverts elevation of the mob.
/datum/element/elevation_core/proc/deelevate_mob(mob/living/target, elevate_time = ELEVATE_TIME)
- REMOVE_TRAIT(target, TRAIT_MOB_ELEVATED, ELEVATION_SOURCE(src))
target.remove_offsets(ELEVATION_SOURCE(src), animate = elevate_time > 0)
if(isvehicle(target.buckled))
animate(target.buckled, pixel_z = -pixel_shift, time = elevate_time, flags = ANIMATION_RELATIVE|ANIMATION_PARALLEL)
+ REMOVE_TRAIT(target, TRAIT_MOB_ELEVATED, ELEVATION_SOURCE(src))
/**
* If the mob is buckled or unbuckled to/from a vehicle, shift it up/down
diff --git a/code/datums/elements/loomable.dm b/code/datums/elements/loomable.dm
index 76ee071a9a20..ca32bde165b1 100644
--- a/code/datums/elements/loomable.dm
+++ b/code/datums/elements/loomable.dm
@@ -22,7 +22,7 @@
loom_type = /obj/structure/loom,
process_completion_verb = "spun",
target_needs_anchoring = TRUE,
- loom_time = 1 SECONDS
+ loom_time = 1 SECONDS,
)
. = ..()
//currently this element only works for items as we need to call /obj/item/attack_atom()
@@ -80,7 +80,8 @@
break
if(!stack_we_use.use(required_amount))
- user.balloon_alert(user, "need [required_amount] of [source]!")
+ if (!spawning_amount)
+ user.balloon_alert(user, "need [required_amount] of [source]!")
break
spawning_amount++
@@ -96,8 +97,13 @@
if(spawning_amount == 0)
return
- var/new_thing
- for(var/repeated in 1 to spawning_amount)
- new_thing = new resulting_atom(target.drop_location())
-
+ var/atom/new_thing = null
+ if (ispath(resulting_atom, /obj/item/stack))
+ var/obj/item/stack/stack_type = resulting_atom
+ while (spawning_amount > 0)
+ new_thing = new resulting_atom(target.drop_location(), new_amount = min(spawning_amount, stack_type::max_amount))
+ spawning_amount -= stack_type::max_amount
+ else
+ for(var/repeated in 1 to spawning_amount)
+ new_thing = new resulting_atom(target.drop_location())
user.balloon_alert_to_viewers("[process_completion_verb] [new_thing]")
diff --git a/code/datums/elements/reagent_scoopable_atom.dm b/code/datums/elements/reagent_scoopable_atom.dm
index 2ad0ca906eab..87fe98f92ca3 100644
--- a/code/datums/elements/reagent_scoopable_atom.dm
+++ b/code/datums/elements/reagent_scoopable_atom.dm
@@ -33,5 +33,5 @@
return ITEM_INTERACT_BLOCKING
if(!container.reagents.add_reagent(reagent_to_extract, rand(5, 10)))
to_chat(user, span_warning("[container] is full."))
- user.visible_message(span_notice("[user] scoops [reagent_to_extract::name] from [source] with [container]."), span_notice("You scoop out [reagent_to_extract::name] from [source] using [container]."))
+ user.visible_message(span_notice("[user] scoops [LOWER_TEXT(reagent_to_extract::name)] from [source] with [container]."), span_notice("You scoop out [LOWER_TEXT(reagent_to_extract::name)] from [source] using [container]."))
return ITEM_INTERACT_SUCCESS
diff --git a/code/datums/elements/volatile_gas_storage.dm b/code/datums/elements/volatile_gas_storage.dm
index 47f162445f4e..08cf0de20d9d 100644
--- a/code/datums/elements/volatile_gas_storage.dm
+++ b/code/datums/elements/volatile_gas_storage.dm
@@ -33,7 +33,7 @@
if(expelled_pressure < minimum_explosive_pressure)
return
- var/explosive_force = CEILING((expelled_pressure / max_explosive_pressure) * max_explosive_force , 1)
+ var/explosive_force = ceil((expelled_pressure / max_explosive_pressure) * max_explosive_force )
// This is supposed to represent only shrapnel and no fire
// Maybe one day we'll get something a bit better
explosion(get_turf(origin), light_impact_range=explosive_force, smoke=FALSE, explosion_cause = origin)
diff --git a/code/datums/emotes.dm b/code/datums/emotes.dm
index bfda885aba9a..2f0b0c030a06 100644
--- a/code/datums/emotes.dm
+++ b/code/datums/emotes.dm
@@ -117,7 +117,7 @@
TIMER_COOLDOWN_START(user, "general_emote_audio_cooldown", general_emote_audio_cooldown)
var/frequency = null
if (affected_by_pitch && SStts.tts_enabled && SStts.pitch_enabled)
- frequency = rand(MIN_EMOTE_PITCH, MAX_EMOTE_PITCH) * (1 + sqrt(abs(user.pitch)) * SIGN(user.pitch) * EMOTE_TTS_PITCH_MULTIPLIER)
+ frequency = rand(MIN_EMOTE_PITCH, MAX_EMOTE_PITCH) * (1 + sqrt(abs(user.pitch)) * sign(user.pitch) * EMOTE_TTS_PITCH_MULTIPLIER)
else if(vary)
frequency = rand(MIN_EMOTE_PITCH, MAX_EMOTE_PITCH)
playsound(source = user,soundin = tmp_sound,vol = 50, vary = FALSE, ignore_walls = sound_wall_ignore, frequency = frequency)
diff --git a/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm b/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm
index f8ab474c3598..658c1acd35ea 100644
--- a/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm
+++ b/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm
@@ -743,6 +743,46 @@
name = "H.E.C.K. Helmet (Worn)"
icon_file = 'icons/mob/clothing/head/helmet.dmi'
+/datum/greyscale_config/hoodie_pullover
+ name = "Pullover Hoodie"
+ icon_file = 'icons/obj/clothing/suits/wintercoat.dmi'
+ json_config = 'code/datums/greyscale/json_configs/pullover.json'
+
+/datum/greyscale_config/hoodie_pullover/worn
+ name = "Pullover Hoodie (Worn)"
+ icon_file = 'icons/mob/clothing/suits/wintercoat.dmi'
+ json_config = 'code/datums/greyscale/json_configs/pullover_worn.json'
+
+/datum/greyscale_config/hoodie_pullover_hood
+ name = "Pullover Hood"
+ icon_file = 'icons/obj/clothing/head/winterhood.dmi'
+ json_config = 'code/datums/greyscale/json_configs/pullover_hood.json'
+
+/datum/greyscale_config/hoodie_pullover_hood/worn
+ name = "Pullover Hood (Worn)"
+ icon_file = 'icons/mob/clothing/head/winterhood.dmi'
+ json_config = 'code/datums/greyscale/json_configs/pullover_hood_worn.json'
+
+/datum/greyscale_config/hoodie_zipup
+ name = "Zipup Hoodie"
+ icon_file = 'icons/obj/clothing/suits/wintercoat.dmi'
+ json_config = 'code/datums/greyscale/json_configs/zipup.json'
+
+/datum/greyscale_config/hoodie_zipup/worn
+ name = "Zipup Hoodie (Worn)"
+ icon_file = 'icons/mob/clothing/suits/wintercoat.dmi'
+ json_config = 'code/datums/greyscale/json_configs/zipup_worn.json'
+
+/datum/greyscale_config/hoodie_zipup_hood
+ name = "Zipup Hood"
+ icon_file = 'icons/obj/clothing/head/winterhood.dmi'
+ json_config = 'code/datums/greyscale/json_configs/zipup_hood.json'
+
+/datum/greyscale_config/hoodie_zipup_hood/worn
+ name = "Zipup Hood (Worn)"
+ icon_file = 'icons/mob/clothing/head/winterhood.dmi'
+ json_config = 'code/datums/greyscale/json_configs/zipup_hood_worn.json'
+
//
// COSTUMES
// (It's better these stay paired together)
diff --git a/code/datums/greyscale/json_configs/pullover.json b/code/datums/greyscale/json_configs/pullover.json
new file mode 100644
index 000000000000..d03672af5c5a
--- /dev/null
+++ b/code/datums/greyscale/json_configs/pullover.json
@@ -0,0 +1,18 @@
+{
+ "pullover": [
+ {
+ "type": "icon_state",
+ "icon_state": "pullover",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ],
+ "pullover_t": [
+ {
+ "type": "icon_state",
+ "icon_state": "pullover_t",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ]
+}
diff --git a/code/datums/greyscale/json_configs/pullover_hood.json b/code/datums/greyscale/json_configs/pullover_hood.json
new file mode 100644
index 000000000000..8b3b4acd05ad
--- /dev/null
+++ b/code/datums/greyscale/json_configs/pullover_hood.json
@@ -0,0 +1,10 @@
+{
+ "hood_pullover": [
+ {
+ "type": "icon_state",
+ "icon_state": "hood_pullover",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ]
+}
diff --git a/code/datums/greyscale/json_configs/pullover_hood_worn.json b/code/datums/greyscale/json_configs/pullover_hood_worn.json
new file mode 100644
index 000000000000..8b3b4acd05ad
--- /dev/null
+++ b/code/datums/greyscale/json_configs/pullover_hood_worn.json
@@ -0,0 +1,10 @@
+{
+ "hood_pullover": [
+ {
+ "type": "icon_state",
+ "icon_state": "hood_pullover",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ]
+}
diff --git a/code/datums/greyscale/json_configs/pullover_worn.json b/code/datums/greyscale/json_configs/pullover_worn.json
new file mode 100644
index 000000000000..ff404d2538d1
--- /dev/null
+++ b/code/datums/greyscale/json_configs/pullover_worn.json
@@ -0,0 +1,24 @@
+{
+ "pullover": [
+ {
+ "type": "icon_state",
+ "icon_state": "pullover",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ },
+ {
+ "type": "icon_state",
+ "icon_state": "pullover_hood",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ],
+ "pullover_t": [
+ {
+ "type": "icon_state",
+ "icon_state": "pullover_t",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ]
+}
diff --git a/code/datums/greyscale/json_configs/zipup.json b/code/datums/greyscale/json_configs/zipup.json
new file mode 100644
index 000000000000..5d847ee13f55
--- /dev/null
+++ b/code/datums/greyscale/json_configs/zipup.json
@@ -0,0 +1,18 @@
+{
+ "zipup": [
+ {
+ "type": "icon_state",
+ "icon_state": "zipup",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ],
+ "zipup_t": [
+ {
+ "type": "icon_state",
+ "icon_state": "zipup_t",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ]
+}
diff --git a/code/datums/greyscale/json_configs/zipup_hood.json b/code/datums/greyscale/json_configs/zipup_hood.json
new file mode 100644
index 000000000000..428b41471ae4
--- /dev/null
+++ b/code/datums/greyscale/json_configs/zipup_hood.json
@@ -0,0 +1,10 @@
+{
+ "hood_zipup": [
+ {
+ "type": "icon_state",
+ "icon_state": "hood_zipup",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ]
+}
diff --git a/code/datums/greyscale/json_configs/zipup_hood_worn.json b/code/datums/greyscale/json_configs/zipup_hood_worn.json
new file mode 100644
index 000000000000..6bb6644f999f
--- /dev/null
+++ b/code/datums/greyscale/json_configs/zipup_hood_worn.json
@@ -0,0 +1,18 @@
+{
+ "hood_zipup": [
+ {
+ "type": "icon_state",
+ "icon_state": "hood_zipup",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ],
+ "hood_zipup_t": [
+ {
+ "type": "icon_state",
+ "icon_state": "hood_zipup_t",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ]
+}
diff --git a/code/datums/greyscale/json_configs/zipup_worn.json b/code/datums/greyscale/json_configs/zipup_worn.json
new file mode 100644
index 000000000000..cdcf0e2e986e
--- /dev/null
+++ b/code/datums/greyscale/json_configs/zipup_worn.json
@@ -0,0 +1,34 @@
+{
+ "zipup": [
+ {
+ "type": "icon_state",
+ "icon_state": "zipup",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ],
+ "zipup_t": [
+ {
+ "type": "icon_state",
+ "icon_state": "zipup_t",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ],
+ "zipup_hood": [
+ {
+ "type": "icon_state",
+ "icon_state": "zipup_hood",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ],
+ "zipup_hood_t": [
+ {
+ "type": "icon_state",
+ "icon_state": "zipup_hood_t",
+ "blend_mode": "overlay",
+ "color_ids": [1]
+ }
+ ]
+}
diff --git a/code/datums/keybinding/admin.dm b/code/datums/keybinding/admin.dm
index 207610b43640..f69581e05d73 100644
--- a/code/datums/keybinding/admin.dm
+++ b/code/datums/keybinding/admin.dm
@@ -26,6 +26,20 @@
SSadmin_verbs.dynamic_invoke_verb(user, /datum/admin_verb/admin_ghost)
return TRUE
+/datum/keybinding/admin/jump_to_ghost
+ hotkey_keys = list("F4")
+ name = "jump_to_ghost"
+ full_name = "Jump to Aghost"
+ description = "Jump your body to your Aghost"
+ keybind_signal = COMSIG_KB_ADMIN_JUMPTOGHOST_DOWN
+
+/datum/keybinding/admin/jump_to_ghost/down(client/user, turf/target, mousepos_x, mousepos_y)
+ . = ..()
+ if(.)
+ return
+ SSadmin_verbs.dynamic_invoke_verb(user, /datum/admin_verb/jump_to_ghost)
+ return TRUE
+
/datum/keybinding/admin/player_panel_new
hotkey_keys = list("F6")
name = "player_panel_new"
diff --git a/code/datums/keybinding/human.dm b/code/datums/keybinding/human.dm
index b2cc0966e8dc..074986449c60 100644
--- a/code/datums/keybinding/human.dm
+++ b/code/datums/keybinding/human.dm
@@ -6,7 +6,7 @@
return ishuman(user.mob)
/datum/keybinding/human/quick_equip
- hotkey_keys = list("R")
+ hotkey_keys = list("E")
name = "quick_equip"
full_name = "Quick equip"
description = "Quickly puts an item in the best slot available"
@@ -21,7 +21,7 @@
return TRUE
/datum/keybinding/human/quick_equip_belt
- hotkey_keys = list("ShiftR")
+ hotkey_keys = list("ShiftE")
name = "quick_equip_belt"
full_name = "Quick equip belt"
description = "Put held thing in belt or take out most recent thing from belt"
diff --git a/code/datums/keybinding/living.dm b/code/datums/keybinding/living.dm
index d9d12020ef2d..090e559c1490 100644
--- a/code/datums/keybinding/living.dm
+++ b/code/datums/keybinding/living.dm
@@ -167,7 +167,7 @@
return TRUE
/datum/keybinding/living/toggle_throw_mode
- hotkey_keys = list("Southwest") // END
+ hotkey_keys = list("R", "Southwest") // END
name = "toggle_throw_mode"
full_name = "Toggle throw mode"
description = "Toggle throwing the current item or not."
diff --git a/code/datums/keybinding/mob.dm b/code/datums/keybinding/mob.dm
index dd27763338bd..86bf3ab9933f 100644
--- a/code/datums/keybinding/mob.dm
+++ b/code/datums/keybinding/mob.dm
@@ -21,7 +21,7 @@
return TRUE
/datum/keybinding/mob/swap_hands
- hotkey_keys = list(UNBOUND_KEY)
+ hotkey_keys = list("X")
name = "swap_hands"
full_name = "Swap hands"
description = ""
@@ -39,14 +39,14 @@
var/hand_index = NONE
/datum/keybinding/mob/select_hand/right
- hotkey_keys = list("Q")
+ hotkey_keys = list(UNBOUND_KEY)
name = "select_right_hand"
full_name = "Swap to Right Hand"
keybind_signal = COMSIG_KB_MOB_SELECTRIGHTHAND_DOWN
hand_index = RIGHT_HANDS
/datum/keybinding/mob/select_hand/left
- hotkey_keys = list("E")
+ hotkey_keys = list(UNBOUND_KEY)
name = "select_left_hand"
full_name = "Swap to Left Hand"
keybind_signal = COMSIG_KB_MOB_SELECTLEFTHAND_DOWN
@@ -81,7 +81,7 @@
return TRUE
/datum/keybinding/mob/drop_item
- hotkey_keys = list("X")
+ hotkey_keys = list("Q")
name = "drop_item"
full_name = "Drop Item"
description = "Drops the item in your active hand to the ground."
diff --git a/code/datums/looping_sounds/tram.dm b/code/datums/looping_sounds/tram.dm
new file mode 100644
index 000000000000..6e972d4e43d5
--- /dev/null
+++ b/code/datums/looping_sounds/tram.dm
@@ -0,0 +1,7 @@
+/datum/looping_sound/tram
+ start_sound = 'sound/machines/tram/tram_start.ogg'
+ start_length = 2.219 SECONDS
+ mid_sounds = 'sound/machines/tram/tram_loop.ogg'
+ mid_length = 2.219 SECONDS
+ end_sound = 'sound/machines/tram/tram_loop.ogg'
+ volume = 20
diff --git a/code/datums/martial/boxing.dm b/code/datums/martial/boxing.dm
index 72146c6d29bd..6318b34c4b7b 100644
--- a/code/datums/martial/boxing.dm
+++ b/code/datums/martial/boxing.dm
@@ -20,6 +20,8 @@
help_verb = /mob/living/proc/boxing_help
/// Boolean on whether we are sportsmanlike in our tussling; TRUE means we have restrictions
var/honorable_boxer = TRUE
+ /// Can we perform grabs even if it would be dishonorable?
+ var/ignore_grab_restriction = FALSE
/// Default damage type for our boxing.
var/default_damage_type = STAMINA
/// List of traits applied to users of this martial art.
@@ -85,7 +87,7 @@
return MARTIAL_ATTACK_SUCCESS
/datum/martial_art/boxing/grab_act(mob/living/attacker, mob/living/defender)
- if(honorable_boxer)
+ if(honorable_boxer && !ignore_grab_restriction)
attacker.balloon_alert(attacker, "no grabbing while boxing!")
return MARTIAL_ATTACK_FAIL
return MARTIAL_ATTACK_INVALID //UNLESS YOU'RE EVIL
@@ -433,6 +435,7 @@
help_verb = /mob/living/proc/hunter_boxing_help
default_damage_type = BRUTE
boxing_traits = list(TRAIT_BOXING_READY)
+ ignore_grab_restriction = TRUE
/// The mobs we are looking for to pass the honor check
var/honorable_mob_biotypes = MOB_BEAST | MOB_SPECIAL | MOB_PLANT | MOB_BUG | MOB_MINING | MOB_CRUSTACEAN | MOB_REPTILE
/// Our crit shout words. First word is then paired with a second word to form an attack name.
diff --git a/code/datums/materials/_material.dm b/code/datums/materials/_material.dm
index f830fa5b7b54..5d34b97c75d8 100644
--- a/code/datums/materials/_material.dm
+++ b/code/datums/materials/_material.dm
@@ -20,6 +20,9 @@ Simple datum which is instanced once per type and is used for every object of sa
var/mat_flags = NONE
/// List of material property IDs to their values, 0 - 10
var/mat_properties = null
+ /// Flags for which comsigs we should track/send
+ /// These exist for performance reasons as to avoid unnecessary work on materials without properties that trigger off these, or doing the same checks for each property
+ var/track_flags = NONE
// Color values
/// Base color of the material, for items that don't have greyscale configs nor are made of multiple materials. Item isn't changed in color if this is null.
@@ -99,16 +102,164 @@ Simple datum which is instanced once per type and is used for every object of sa
return TRUE
-///This proc is called when the material is added to an object.
-/datum/material/proc/on_applied(atom/source, mat_amount, multiplier)
+/// This proc is called when the material is added to an object.
+/// Can be called even if the material is covered by a slot
+/datum/material/proc/on_applied(atom/source, mat_amount, multiplier, from_slot)
SHOULD_CALL_PARENT(TRUE)
- SEND_SIGNAL(src, COMSIG_MATERIAL_APPLIED, source, mat_amount, multiplier)
+ SEND_SIGNAL(src, COMSIG_MATERIAL_APPLIED, source, mat_amount, multiplier, from_slot)
-///This proc is called when the material becomes the one the object is composed of the most
+ if (!(source.material_flags & MATERIAL_EFFECTS) || from_slot)
+ return
+
+ if (track_flags & MATERIAL_TRACK_CONTACT)
+ var/static/list/turf_interactions = typecacheof(list(
+ /turf/open,
+ /obj/structure/platform,
+ /obj/structure/table,
+ /obj/structure/rack,
+ /obj/structure/bed,
+ /obj/structure/closet/crate,
+ /obj/structure/reagent_dispensers,
+ /obj/structure/altar,
+ ))
+ if (is_type_in_typecache(source, turf_interactions))
+ source.AddComponent(/datum/component/material_turf_tracking, src)
+
+ if (isclosedturf(source))
+ RegisterSignal(source, COMSIG_LIVING_DISARM_COLLIDE, PROC_REF(on_wall_shove_collide))
+
+ if (track_flags & MATERIAL_TRACK_IMPACT)
+ RegisterSignal(source, COMSIG_MOVABLE_IMPACT, PROC_REF(on_throw_impact))
+ RegisterSignal(source, COMSIG_MOVABLE_IMPACT_ZONE, PROC_REF(on_throw_impact_living))
+ RegisterSignal(source, COMSIG_ITEM_ATTACK, PROC_REF(on_item_attack))
+ RegisterSignal(source, COMSIG_ITEM_ATTACK_ATOM, PROC_REF(on_item_attack))
+ // Allow recipe crafting for stack items
+ if(!isstack(source))
+ RegisterSignal(source, COMSIG_ITEM_ATTACK_SELF, PROC_REF(on_item_attack_self))
+ RegisterSignal(source, COMSIG_ITEM_ATTACK_ZONE, PROC_REF(on_item_attack_living))
+
+/// This proc is called when the material becomes the one the object is composed of the most
+/// Only called when the material isn't assigned to a slot
/datum/material/proc/on_main_applied(atom/source, mat_amount, multiplier)
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(src, COMSIG_MATERIAL_MAIN_APPLIED, source, mat_amount, multiplier)
+/// This proc is called when the material is removed from an object.
+/datum/material/proc/on_removed(atom/source, mat_amount, material_flags, from_slot)
+ SHOULD_CALL_PARENT(TRUE)
+ SEND_SIGNAL(src, COMSIG_MATERIAL_REMOVED, source, mat_amount, material_flags, from_slot)
+
+ if (!(source.material_flags & MATERIAL_EFFECTS))
+ return
+
+ if (track_flags & MATERIAL_TRACK_CONTACT)
+ var/static/list/material_signals = list(
+ COMSIG_LIVING_DISARM_COLLIDE,
+ )
+ UnregisterSignal(source, material_signals)
+ var/list/tracker_components = source.GetComponents(/datum/component/material_turf_tracking)
+ if (istype(tracker_components, /datum/component/material_turf_tracking))
+ qdel(tracker_components)
+ else
+ // Delete all trackers linked to ourselves (might be multiple if we've got multiple slots going on)
+ for (var/datum/component/material_turf_tracking/tracker as anything in tracker_components)
+ if (tracker.owner_material == src)
+ qdel(tracker)
+
+ if (track_flags & MATERIAL_TRACK_IMPACT)
+ var/static/list/material_signals = list(
+ COMSIG_MOVABLE_IMPACT,
+ COMSIG_MOVABLE_IMPACT_ZONE,
+ COMSIG_ITEM_ATTACK,
+ COMSIG_ITEM_ATTACK_ATOM,
+ COMSIG_ITEM_ATTACK_SELF,
+ COMSIG_ITEM_ATTACK_ZONE,
+ )
+ UnregisterSignal(source, material_signals)
+
+/// This proc is called when the material is no longer the one the object is composed by the most
+/datum/material/proc/on_main_removed(atom/source, mat_amount, multiplier)
+ SHOULD_CALL_PARENT(TRUE)
+ SEND_SIGNAL(src, COMSIG_MATERIAL_MAIN_REMOVED, source, mat_amount, multiplier)
+
+/datum/material/proc/on_wall_shove_collide(turf/closed/source, mob/living/shover, mob/living/target, shove_flags, obj/item/weapon)
+ SIGNAL_HANDLER
+
+ // If any part is exposed it makes sense it'd impact the wall
+ var/skin_contact = HEAD|CHEST|GROIN|LEGS|FEET|ARMS|HANDS
+ for (var/obj/item/worn_item in target.get_equipped_items(INCLUDE_ABSTRACT))
+ skin_contact &= ~worn_item.body_parts_covered
+ if (!skin_contact)
+ break
+
+ SEND_SIGNAL(src, COMSIG_MATERIAL_EFFECT_TOUCH, source, target, shover, null, !!skin_contact)
+
+/datum/material/proc/on_item_attack(obj/item/source, mob/living/target, mob/living/user)
+ SIGNAL_HANDLER
+ impact_affect_touch(source, user, user)
+ // Living mobs use a different signal
+ if (!isliving(target))
+ impact_affect_target(source, target, user)
+
+/datum/material/proc/on_item_attack_living(obj/item/source, mob/living/target, mob/living/user, def_zone)
+ SIGNAL_HANDLER
+ var/skin_contact = body_zone2cover_flags(def_zone)
+ for (var/obj/item/worn_item in target.get_equipped_items(INCLUDE_ABSTRACT))
+ skin_contact &= ~worn_item.body_parts_covered
+ if (!skin_contact)
+ break
+
+ impact_affect_target(source, target, user, def_zone, !!skin_contact)
+
+/datum/material/proc/on_item_attack_self(obj/item/source, mob/living/user)
+ SIGNAL_HANDLER
+ impact_affect_touch(source, user, user)
+
+/datum/material/proc/on_throw_impact(obj/item/source, atom/hit_atom, datum/thrownthing/throwing_datum, caught)
+ SIGNAL_HANDLER
+ if (caught)
+ impact_affect_touch(source, hit_atom, astype(throwing_datum.thrower.resolve(), /mob/living))
+ else if (!isliving(hit_atom)) // Hit mobs have armor checking
+ impact_affect_throw_impact(source, hit_atom, astype(throwing_datum.thrower.resolve(), /mob/living))
+
+/datum/material/proc/on_throw_impact_living(obj/item/source, mob/living/target, def_zone, blocked, datum/thrownthing/throwing_datum)
+ SIGNAL_HANDLER
+
+ var/skin_contact = body_zone2cover_flags(def_zone)
+ for (var/obj/item/worn_item in target.get_equipped_items(INCLUDE_ABSTRACT))
+ skin_contact &= ~worn_item.body_parts_covered
+ if (!skin_contact)
+ break
+
+ impact_affect_throw_impact(source, target, astype(throwing_datum.thrower.resolve(), /mob/living), def_zone, !!skin_contact)
+
+/datum/material/proc/impact_affect_touch(obj/item/source, mob/living/user, mob/living/initiator)
+ var/arm_dir = IS_LEFT_INDEX(user.active_hand_index) ? BODY_ZONE_L_ARM : BODY_ZONE_R_ARM
+ if (!ishuman(user))
+ SEND_SIGNAL(src, COMSIG_MATERIAL_EFFECT_TOUCH, source, user, initiator, arm_dir, TRUE)
+ return
+
+ var/mob/living/carbon/human/as_human = user
+ var/obj/item/bodypart/hand = as_human.has_hand_for_held_index(as_human.get_held_index_of_item(source))
+ if (!hand)
+ SEND_SIGNAL(src, COMSIG_MATERIAL_EFFECT_TOUCH, source, user, initiator, arm_dir, FALSE)
+ return
+
+ var/list/obj/item/hand_covers = as_human.get_clothing_on_part(hand)
+ var/hand_covered = FALSE
+ for (var/obj/item/worn_item in hand_covers)
+ if (worn_item.body_parts_covered & HANDS)
+ hand_covered = TRUE
+ break
+
+ SEND_SIGNAL(src, COMSIG_MATERIAL_EFFECT_TOUCH, source, user, initiator, hand.body_zone, !hand_covered)
+
+/datum/material/proc/impact_affect_target(obj/item/source, atom/target, mob/living/user, def_zone, skin_contact = TRUE)
+ SEND_SIGNAL(src, COMSIG_MATERIAL_EFFECT_HIT, source, target, user, def_zone, skin_contact)
+
+/datum/material/proc/impact_affect_throw_impact(obj/item/source, atom/target, mob/living/user, def_zone, skin_contact = TRUE)
+ SEND_SIGNAL(src, COMSIG_MATERIAL_EFFECT_THROW_IMPACT, source, target, user, def_zone, skin_contact)
+
/datum/material/proc/setup_glow(turf/on)
if(GET_TURF_PLANE_OFFSET(on) != GET_LOWEST_STACK_OFFSET(on.z)) // We ain't the bottom brother
return
@@ -127,15 +278,6 @@ Simple datum which is instanced once per type and is used for every object of sa
/datum/material/proc/lit_turf_deleted(turf/source)
source.set_light(0, 0, null)
-/// This proc is called when the material is removed from an object.
-/datum/material/proc/on_removed(atom/source, amount, material_flags)
- SHOULD_CALL_PARENT(TRUE)
- SEND_SIGNAL(src, COMSIG_MATERIAL_REMOVED, source, amount, material_flags)
-
-/// This proc is called when the material is no longer the one the object is composed by the most
-/datum/material/proc/on_main_removed(atom/source, mat_amount, multiplier)
- SHOULD_CALL_PARENT(TRUE)
- SEND_SIGNAL(src, COMSIG_MATERIAL_MAIN_REMOVED, source, mat_amount, multiplier)
////Called in `/datum/component/edible/proc/on_material_effects`
/datum/material/proc/on_edible_applied(atom/source, datum/component/edible/edible)
diff --git a/code/datums/materials/alloys.dm b/code/datums/materials/alloys.dm
index 6d90eb4ff1af..861be125aef8 100644
--- a/code/datums/materials/alloys.dm
+++ b/code/datums/materials/alloys.dm
@@ -44,15 +44,15 @@
composition = list(/datum/material/iron = 1, /datum/material/plasma = 1)
mat_rust_resistance = RUST_RESISTANCE_REINFORCED
-/datum/material/alloy/plasteel/on_applied(atom/target, mat_amount, multiplier)
+/datum/material/alloy/plasteel/on_applied(atom/source, mat_amount, multiplier, from_slot)
. = ..()
- if(istype(target, /obj/item/fishing_rod))
- ADD_TRAIT(target, TRAIT_ROD_LAVA_USABLE, REF(src))
+ if(istype(source, /obj/item/fishing_rod))
+ ADD_TRAIT(source, TRAIT_ROD_LAVA_USABLE, REF(src))
-/datum/material/alloy/plasteel/on_removed(atom/target, mat_amount, multiplier)
+/datum/material/alloy/plasteel/on_removed(atom/source, mat_amount, multiplier, from_slot)
. = ..()
- if(istype(target, /obj/item/fishing_rod))
- REMOVE_TRAIT(target, TRAIT_ROD_LAVA_USABLE, REF(src))
+ if(istype(source, /obj/item/fishing_rod))
+ REMOVE_TRAIT(source, TRAIT_ROD_LAVA_USABLE, REF(src))
/**
* Plastitanium
@@ -78,15 +78,15 @@
composition = list(/datum/material/titanium = 1, /datum/material/plasma = 1)
mat_rust_resistance = RUST_RESISTANCE_TITANIUM
-/datum/material/alloy/plastitanium/on_applied(atom/target, mat_amount, multiplier)
+/datum/material/alloy/plastitanium/on_applied(atom/source, mat_amount, multiplier, from_slot)
. = ..()
- if(istype(target, /obj/item/fishing_rod))
- ADD_TRAIT(target, TRAIT_ROD_LAVA_USABLE, REF(src))
+ if(istype(source, /obj/item/fishing_rod))
+ ADD_TRAIT(source, TRAIT_ROD_LAVA_USABLE, REF(src))
-/datum/material/alloy/plastitanium/on_removed(atom/target, mat_amount, multiplier)
+/datum/material/alloy/plastitanium/on_removed(atom/source, mat_amount, multiplier, from_slot)
. = ..()
- if(istype(target, /obj/item/fishing_rod))
- REMOVE_TRAIT(target, TRAIT_ROD_LAVA_USABLE, REF(src))
+ if(istype(source, /obj/item/fishing_rod))
+ REMOVE_TRAIT(source, TRAIT_ROD_LAVA_USABLE, REF(src))
/**
* Plasmaglass
@@ -194,16 +194,16 @@
value_per_unit = 0.4
composition = list(/datum/material/iron = 2, /datum/material/plasma = 2)
-/datum/material/alloy/alien/on_applied(atom/target, mat_amount, multiplier)
+/datum/material/alloy/alien/on_applied(atom/source, mat_amount, multiplier, from_slot)
. = ..()
- if(isobj(target))
- target.AddElement(/datum/element/obj_regen, _rate=0.02) // 2% regen per tick.
- if(istype(target, /obj/item/fishing_rod))
- ADD_TRAIT(target, TRAIT_ROD_LAVA_USABLE, REF(src))
+ if(isobj(source))
+ source.AddElement(/datum/element/obj_regen, _rate=0.02) // 2% regen per tick.
+ if(istype(source, /obj/item/fishing_rod))
+ ADD_TRAIT(source, TRAIT_ROD_LAVA_USABLE, REF(src))
-/datum/material/alloy/alien/on_removed(atom/target, mat_amount, multiplier)
+/datum/material/alloy/alien/on_removed(atom/source, mat_amount, multiplier, from_slot)
. = ..()
- if(isobj(target))
- target.RemoveElement(/datum/element/obj_regen, _rate=0.02)
- if(istype(target, /obj/item/fishing_rod))
- REMOVE_TRAIT(target, TRAIT_ROD_LAVA_USABLE, REF(src))
+ if(isobj(source))
+ source.RemoveElement(/datum/element/obj_regen, _rate=0.02)
+ if(istype(source, /obj/item/fishing_rod))
+ REMOVE_TRAIT(source, TRAIT_ROD_LAVA_USABLE, REF(src))
diff --git a/code/datums/materials/basemats.dm b/code/datums/materials/basemats.dm
index 412e94090d92..4284503cede4 100644
--- a/code/datums/materials/basemats.dm
+++ b/code/datums/materials/basemats.dm
@@ -89,6 +89,7 @@
MATERIAL_ELECTRICAL = 9,
MATERIAL_THERMAL = 4,
MATERIAL_CHEMICAL = 4,
+ MATERIAL_VAMPIRES_BANE = 5,
)
sheet_type = /obj/item/stack/sheet/mineral/silver
ore_type = /obj/item/stack/ore/silver
@@ -209,7 +210,8 @@
MATERIAL_ELECTRICAL = 10,
MATERIAL_THERMAL = 8,
MATERIAL_CHEMICAL = 0,
- MATERIAL_FLAMMABILITY = 9, // Literally sets itself on fire from any excitement
+ MATERIAL_FLAMMABILITY = 10, // Literally sets itself on fire from any excitement
+ MATERIAL_FIRESTACKER = 1,
)
sheet_type = /obj/item/stack/sheet/mineral/plasma
ore_type = /obj/item/stack/ore/plasma
@@ -218,22 +220,19 @@
mineral_rarity = MATERIAL_RARITY_PRECIOUS
points_per_unit = 15 / SHEET_MATERIAL_AMOUNT
-/datum/material/plasma/on_applied(atom/source, mat_amount, multiplier)
+/datum/material/plasma/on_applied(atom/source, mat_amount, multiplier, from_slot)
. = ..()
- if(ismovable(source))
- source.AddElement(/datum/element/firestacker, 1 * multiplier)
source.AddComponent(/datum/component/combustible_flooder, GAS_PLASMA, mat_amount * 0.05 * multiplier) //Empty temp arg, fully dependent on whatever ignited it.
if(istype(source, /obj/item/fishing_rod))
ADD_TRAIT(source, TRAIT_ROD_LAVA_USABLE, REF(src))
-/datum/material/plasma/on_removed(atom/source, mat_amount, multiplier)
+/datum/material/plasma/on_removed(atom/source, mat_amount, multiplier, from_slot)
. = ..()
- source.RemoveElement(/datum/element/firestacker, mat_amount = 1 * multiplier)
qdel(source.GetComponent(/datum/component/combustible_flooder))
if(istype(source, /obj/item/fishing_rod))
ADD_TRAIT(source, TRAIT_ROD_LAVA_USABLE, REF(src))
-///Can cause bluespace effects on use. (Teleportation) (Not yet implemented)
+/// Can cause bluespace effects on use. (Teleportation)
/datum/material/bluespace
name = "bluespace crystal"
desc = "Crystals with bluespace properties."
@@ -250,6 +249,7 @@
MATERIAL_THERMAL = 4,
MATERIAL_CHEMICAL = 4,
MATERIAL_BEAUTY = 0.5, // Absolutely mesmerizing
+ MATERIAL_TELEPORTING = 5,
)
sheet_type = /obj/item/stack/sheet/bluespace_crystal
ore_type = /obj/item/stack/ore/bluespace_crystal
@@ -306,7 +306,7 @@
mineral_rarity = MATERIAL_RARITY_UNDISCOVERED
points_per_unit = 60 / SHEET_MATERIAL_AMOUNT
-/datum/material/bananium/on_applied(atom/source, mat_amount, multiplier)
+/datum/material/bananium/on_applied(atom/source, mat_amount, multiplier, from_slot)
. = ..()
source.LoadComponent(/datum/component/squeak, list('sound/items/bikehorn.ogg'=1), 50 * multiplier, falloff_exponent = 20)
source.AddComponent(/datum/component/slippery, min(mat_amount / 10 * multiplier, 80 * multiplier))
@@ -332,7 +332,7 @@
)
rewards += pick_weight(funny_fish)
-/datum/material/bananium/on_removed(atom/source, mat_amount, multiplier)
+/datum/material/bananium/on_removed(atom/source, mat_amount, multiplier, from_slot)
. = ..()
qdel(source.GetComponent(/datum/component/slippery))
qdel(source.GetComponent(/datum/component/squeak))
@@ -391,12 +391,12 @@
mineral_rarity = MATERIAL_RARITY_UNDISCOVERED
points_per_unit = 100 / SHEET_MATERIAL_AMOUNT
-/datum/material/runite/on_applied(atom/source, mat_amount, multiplier)
+/datum/material/runite/on_applied(atom/source, mat_amount, multiplier, from_slot)
. = ..()
if(istype(source, /obj/item/fishing_rod))
ADD_TRAIT(source, TRAIT_ROD_REMOVE_FISHING_DUD, REF(src)) //light-absorbing, environment-cancelling fishing rod.
-/datum/material/runite/on_removed(atom/source, mat_amount, multiplier)
+/datum/material/runite/on_removed(atom/source, mat_amount, multiplier, from_slot)
. = ..()
if(istype(source, /obj/item/fishing_rod))
REMOVE_TRAIT(source, TRAIT_ROD_REMOVE_FISHING_DUD, REF(src)) //light-absorbing, environment-cancelling fishing rod.
@@ -484,12 +484,12 @@
mineral_rarity = MATERIAL_RARITY_UNDISCOVERED // Doesn't naturally spawn on lavaland.
points_per_unit = 100 / SHEET_MATERIAL_AMOUNT
-/datum/material/adamantine/on_applied(atom/source, mat_amount, multiplier)
+/datum/material/adamantine/on_applied(atom/source, mat_amount, multiplier, from_slot)
. = ..()
if(istype(source, /obj/item/fishing_rod))
ADD_TRAIT(source, TRAIT_ROD_REMOVE_FISHING_DUD, REF(src)) // light-absorbing, environment-cancelling fishing rod.
-/datum/material/adamantine/on_removed(atom/source, mat_amount, multiplier)
+/datum/material/adamantine/on_removed(atom/source, mat_amount, multiplier, from_slot)
. = ..()
if(istype(source, /obj/item/fishing_rod))
REMOVE_TRAIT(source, TRAIT_ROD_REMOVE_FISHING_DUD, REF(src)) // light-absorbing, environment-cancelling fishing rod.
@@ -522,13 +522,13 @@
mineral_rarity = MATERIAL_RARITY_UNDISCOVERED // Doesn't naturally spawn on lavaland.
points_per_unit = 100 / SHEET_MATERIAL_AMOUNT
-/datum/material/mythril/on_applied(atom/source, mat_amount, multiplier)
+/datum/material/mythril/on_applied(atom/source, mat_amount, multiplier, from_slot)
. = ..()
if(isitem(source))
source.AddComponent(/datum/component/fantasy)
ADD_TRAIT(source, TRAIT_INNATELY_FANTASTICAL_ITEM, REF(src)) // DO THIS LAST OR WE WILL NEVER GET OUR BONUSES!!!
-/datum/material/mythril/on_removed(atom/source, mat_amount, multiplier)
+/datum/material/mythril/on_removed(atom/source, mat_amount, multiplier, from_slot)
. = ..()
if(isitem(source))
REMOVE_TRAIT(source, TRAIT_INNATELY_FANTASTICAL_ITEM, REF(src)) // DO THIS FIRST OR WE WILL NEVER GET OUR BONUSES DELETED!!!
@@ -557,16 +557,17 @@
MATERIAL_THERMAL = 8,
MATERIAL_CHEMICAL = 4,
MATERIAL_FLAMMABILITY = 10,
+ MATERIAL_FIRESTACKER = 1,
)
sheet_type = /obj/item/stack/sheet/hot_ice
material_reagent = /datum/reagent/toxin/hot_ice
value_per_unit = 400 / SHEET_MATERIAL_AMOUNT
-/datum/material/hot_ice/on_applied(atom/source, mat_amount, multiplier)
+/datum/material/hot_ice/on_applied(atom/source, mat_amount, multiplier, from_slot)
. = ..()
source.AddComponent(/datum/component/combustible_flooder, GAS_PLASMA, mat_amount * 1.5 * multiplier, (mat_amount * 0.2 + 300) * multiplier)
-/datum/material/hot_ice/on_removed(atom/source, mat_amount, multiplier)
+/datum/material/hot_ice/on_removed(atom/source, mat_amount, multiplier, from_slot)
. = ..()
qdel(source.GetComponent(/datum/component/combustible_flooder))
@@ -603,7 +604,7 @@
color = "#EDC9AF"
mat_flags = MATERIAL_BASIC_RECIPES | MATERIAL_CLASS_AMORPHOUS
mat_properties = list(
- MATERIAL_DENSITY = 2,
+ MATERIAL_DENSITY = 3,
MATERIAL_HARDNESS = 0,
MATERIAL_FLEXIBILITY = 0,
MATERIAL_REFLECTIVITY = 7,
@@ -679,13 +680,13 @@
MATERIAL_THERMAL = 1,
MATERIAL_CHEMICAL = 8,
)
+ material_reagent = list(/datum/reagent/iron = 1, /datum/reagent/fuel/unholywater = 2)
sheet_type = /obj/item/stack/sheet/runed_metal
value_per_unit = 1500 / SHEET_MATERIAL_AMOUNT
texture_layer_icon_state = "runed"
/datum/material/runedmetal/on_accidental_mat_consumption(mob/living/carbon/victim, obj/item/source_item)
. = ..()
- victim.reagents.add_reagent(/datum/reagent/fuel/unholywater, rand(8, 12))
if(!HAS_TRAIT(victim, TRAIT_ROCK_EATER))
victim.apply_damage(10, BRUTE, BODY_ZONE_HEAD, wound_bonus = 5)
return TRUE
@@ -869,12 +870,12 @@
material_reagent = /datum/reagent/toxin/plasma
value_per_unit = 900 / SHEET_MATERIAL_AMOUNT
-/datum/material/zaukerite/on_applied(atom/source, mat_amount, multiplier)
+/datum/material/zaukerite/on_applied(atom/source, mat_amount, multiplier, from_slot)
. = ..()
if(istype(source, /obj/item/fishing_rod))
ADD_TRAIT(source, TRAIT_ROD_IGNORE_ENVIRONMENT, REF(src)) //light-absorbing, environment-cancelling fishing rod.
-/datum/material/zaukerite/on_removed(atom/source, mat_amount, multiplier)
+/datum/material/zaukerite/on_removed(atom/source, mat_amount, multiplier, from_slot)
. = ..()
if(istype(source, /obj/item/fishing_rod))
REMOVE_TRAIT(source, TRAIT_ROD_IGNORE_ENVIRONMENT, REF(src)) //light-absorbing, environment-cancelling fishing rod.
@@ -884,3 +885,44 @@
if(!HAS_TRAIT(victim, TRAIT_ROCK_EATER))
victim.apply_damage(30, BURN, BODY_ZONE_HEAD, wound_bonus = 5)
return TRUE
+
+/// Evil and very unstable version of bluespace crystals
+/datum/material/telecrystal
+ name = "telecrystal"
+ desc = "An ominous-looking gemstone capable of transporting objects vast distances through bluespace."
+ color = "#BD1B28"
+ alpha = 200
+ starlight_color = COLOR_SYNDIE_RED
+ mat_flags = MATERIAL_BASIC_RECIPES | MATERIAL_CLASS_CRYSTAL | MATERIAL_CLASS_RIGID
+ mat_properties = list(
+ MATERIAL_DENSITY = 1,
+ MATERIAL_HARDNESS = 4,
+ MATERIAL_FLEXIBILITY = 0,
+ MATERIAL_REFLECTIVITY = 10,
+ MATERIAL_ELECTRICAL = 10,
+ MATERIAL_THERMAL = 2,
+ MATERIAL_CHEMICAL = 8,
+ MATERIAL_BEAUTY = -0.5, // very evil bad no good
+ MATERIAL_TELEPORTING = 8,
+ MATERIAL_PENETRATING = TRUE,
+ )
+ sheet_type = /obj/item/stack/sheet/telepolycrystal
+ material_reagent = list(/datum/reagent/bluespace = 1, /datum/reagent/medicine/stimulants = 1) // We don't have liquid telecrystals and I don't wanna risk it
+ value_per_unit = 1200 / SHEET_MATERIAL_AMOUNT
+ texture_layer_icon_state = "shine"
+
+/datum/material/telecrystal/on_main_applied(atom/source, mat_amount, multiplier)
+ . = ..()
+ if(istype(source, /obj/item/fishing_rod))
+ RegisterSignal(source, COMSIG_ROD_BEGIN_FISHING, PROC_REF(on_begin_fishing))
+
+/datum/material/telecrystal/on_main_removed(atom/source, mat_amount, multiplier)
+ . = ..()
+ if(istype(source, /obj/item/fishing_rod))
+ UnregisterSignal(source, COMSIG_ROD_BEGIN_FISHING)
+
+/datum/material/telecrystal/proc/on_begin_fishing(obj/item/fishing_rod/rod, datum/fishing_challenge/challenge)
+ SIGNAL_HANDLER
+ // Oops, all chainsawfish!
+ challenge.register_reward_signals(GLOB.preset_fish_sources[/datum/fish_source/portal/syndicate])
+
diff --git a/code/datums/materials/material_slots/_slot.dm b/code/datums/materials/material_slots/_slot.dm
new file mode 100644
index 000000000000..239458f3981d
--- /dev/null
+++ b/code/datums/materials/material_slots/_slot.dm
@@ -0,0 +1,19 @@
+/// Singleton datum which controls how materials affect atoms they're applied to
+/datum/material_slot
+ abstract_type = /datum/material_slot
+ /// Name of the slot for autolathe UI
+ var/name = "error"
+ /// Material requirement type which controls what materials can be used to fill this slot when printing an item
+ var/datum/material_requirement/requirement_type = null
+ /// Relative amount of material in this slot for when multiple slots are filled with a single material
+ var/material_amount = 1
+
+/// Called when the material in this slot is applied to the atom. Return FALSE to prevent base apply_single_mat_effect from running.
+/// If the material is main, main material application will also be cancelled. Should be consistent with on_removed.
+/datum/material_slot/proc/on_applied(atom/target, datum/material/material, amount, multiplier)
+ return TRUE
+
+/// Called when the material in this slot is removed from the atom. Return FALSE to prevent base remove_single_mat_effect from running.
+/// If the material is main, main material removal will also be cancelled. Should be consistent with on_applied.
+/datum/material_slot/proc/on_removed(atom/target, datum/material/material, amount, multiplier)
+ return TRUE
diff --git a/code/datums/materials/material_slots/generic.dm b/code/datums/materials/material_slots/generic.dm
new file mode 100644
index 000000000000..971ae28b6122
--- /dev/null
+++ b/code/datums/materials/material_slots/generic.dm
@@ -0,0 +1,210 @@
+// Generic slots for weapons
+
+/// Generic main/parent type for all weapon heads
+/datum/material_slot/weapon_head
+ name = "weapon head"
+ requirement_type = /datum/material_requirement/solid_material
+
+/datum/material_slot/weapon_head/on_applied(obj/item/target, datum/material/material, amount, multiplier)
+ // Weapon head controls strength and conductivity
+ if (!(target.material_flags & MATERIAL_EFFECTS))
+ return FALSE
+
+ // Effect signals
+ RegisterSignal(target, COMSIG_MOVABLE_IMPACT, PROC_REF(on_throw_impact))
+ RegisterSignal(target, COMSIG_MOVABLE_IMPACT_ZONE, PROC_REF(on_throw_impact_living))
+ RegisterSignal(target, COMSIG_ITEM_ATTACK, PROC_REF(on_item_attack))
+ RegisterSignal(target, COMSIG_ITEM_ATTACK_ATOM, PROC_REF(on_item_attack))
+ RegisterSignal(target, COMSIG_ITEM_ATTACK_ZONE, PROC_REF(on_item_attack_living))
+
+ if (!(target.material_flags & MATERIAL_AFFECT_STATISTICS))
+ return FALSE
+
+ // Damage
+ target.change_material_strength(material, amount, multiplier)
+
+ // Conductivity
+ var/conductivity = material.get_property(MATERIAL_ELECTRICAL)
+ var/siemens_modifier = round(max(0, conductivity - 1) ** 1.18 * 0.15, 0.01)
+ var/siemens_mult = 1 + (siemens_modifier - 1) * multiplier
+ target.siemens_coefficient *= max(0, siemens_mult)
+
+ if (target.siemens_coefficient == 0)
+ target.obj_flags &= ~CONDUCTS_ELECTRICITY
+
+/datum/material_slot/weapon_head/on_removed(obj/item/target, datum/material/material, amount, multiplier)
+ var/static/list/interaction_signals = list(
+ COMSIG_MOVABLE_IMPACT,
+ COMSIG_MOVABLE_IMPACT_ZONE,
+ COMSIG_ITEM_ATTACK,
+ COMSIG_ITEM_ATTACK_ATOM,
+ COMSIG_ITEM_ATTACK_ZONE,
+ )
+ UnregisterSignal(target, interaction_signals)
+
+ if (!(target.material_flags & MATERIAL_AFFECT_STATISTICS) || !(target.material_flags & MATERIAL_EFFECTS))
+ return FALSE
+
+ // Damage
+ target.change_material_strength(material, amount, multiplier, remove = TRUE)
+
+ // Conductivity
+ var/conductivity = material.get_property(MATERIAL_ELECTRICAL)
+ var/siemens_modifier = round(max(0, conductivity - 1) ** 1.18 * 0.15, 0.01)
+ var/siemens_mult = 1 + (siemens_modifier - 1) * multiplier
+ if (siemens_mult > 0)
+ target.siemens_coefficient /= siemens_mult
+
+ if (target.siemens_coefficient > 0 && (initial(target.obj_flags) & CONDUCTS_ELECTRICITY) && !(target.obj_flags & CONDUCTS_ELECTRICITY))
+ target.obj_flags |= CONDUCTS_ELECTRICITY
+
+/datum/material_slot/weapon_head/proc/on_throw_impact(obj/item/source, atom/hit_atom, datum/thrownthing/throwing_datum, caught)
+ SIGNAL_HANDLER
+ if (!caught && !isliving(hit_atom))
+ affect_throw_impact(source, hit_atom, astype(throwing_datum.thrower.resolve(), /mob/living))
+
+/datum/material_slot/weapon_head/proc/on_item_attack(obj/item/source, atom/movable/target, mob/living/user)
+ SIGNAL_HANDLER
+ // Living mobs use a different signal
+ if (!isliving(target))
+ affect_target(source, target, user)
+
+/datum/material_slot/weapon_head/proc/on_item_attack_living(obj/item/source, mob/living/target, mob/living/user, def_zone)
+ SIGNAL_HANDLER
+
+ var/skin_contact = body_zone2cover_flags(def_zone)
+ for (var/obj/item/worn_item in target.get_equipped_items(INCLUDE_ABSTRACT))
+ skin_contact &= ~worn_item.body_parts_covered
+ if (!skin_contact)
+ break
+
+ affect_target(source, target, user, def_zone, !!skin_contact)
+
+/datum/material_slot/weapon_head/proc/on_throw_impact_living(obj/item/source, mob/living/target, def_zone, blocked, datum/thrownthing/throwing_datum)
+ SIGNAL_HANDLER
+
+ var/skin_contact = body_zone2cover_flags(def_zone)
+ for (var/obj/item/worn_item in target.get_equipped_items(INCLUDE_ABSTRACT))
+ skin_contact &= ~worn_item.body_parts_covered
+ if (!skin_contact)
+ break
+
+ affect_throw_impact(source, target, astype(throwing_datum.thrower.resolve(), /mob/living), def_zone, !!skin_contact)
+
+/datum/material_slot/weapon_head/proc/affect_target(obj/item/source, atom/target, mob/living/user, def_zone, skin_contact = TRUE)
+ var/datum/material/source_mat = source.get_material_from_slot(type)
+ SEND_SIGNAL(source_mat, COMSIG_MATERIAL_EFFECT_HIT, source, target, user, def_zone, skin_contact)
+
+/datum/material_slot/weapon_head/proc/affect_throw_impact(obj/item/source, atom/target, mob/living/user, def_zone, skin_contact = TRUE)
+ var/datum/material/source_mat = source.get_material_from_slot(type)
+ SEND_SIGNAL(source_mat, COMSIG_MATERIAL_EFFECT_THROW_IMPACT, source, target, user, def_zone, skin_contact)
+
+/// Main type for all weapon handles
+/datum/material_slot/handle
+ name = "handle"
+ requirement_type = /datum/material_requirement/solid_material
+
+/datum/material_slot/handle/on_applied(obj/item/target, datum/material/material, amount, multiplier)
+ // Handle controls integrity, armor, conductivity and wieldiness stats-wise
+ if (!(target.material_flags & MATERIAL_EFFECTS))
+ return FALSE
+
+ // Effect signals
+ RegisterSignal(target, COMSIG_MOVABLE_IMPACT, PROC_REF(on_throw_impact))
+ RegisterSignal(target, COMSIG_ITEM_ATTACK, PROC_REF(on_item_attack))
+ RegisterSignal(target, COMSIG_ITEM_ATTACK_ATOM, PROC_REF(on_item_attack))
+ RegisterSignal(target, COMSIG_ITEM_ATTACK_SELF, PROC_REF(on_item_attack_self))
+
+ if (!(target.material_flags & MATERIAL_AFFECT_STATISTICS))
+ return FALSE
+
+ // Armor/integrity
+ var/integrity_mod = material.get_property(MATERIAL_INTEGRITY)
+ target.modify_max_integrity(ceil(target.max_integrity * integrity_mod))
+ var/list/armor_mods = material.get_armor_modifiers(multiplier)
+ target.set_armor(target.get_armor().generate_new_with_multipliers(armor_mods))
+
+ // Conductivity
+ var/conductivity = material.get_property(MATERIAL_ELECTRICAL)
+ var/siemens_modifier = round(max(0, conductivity - 1) ** 1.18 * 0.15, 0.01)
+ var/siemens_mult = 1 + (siemens_modifier - 1) * multiplier
+ target.siemens_coefficient *= max(0, siemens_mult)
+
+ if (target.siemens_coefficient == 0)
+ target.obj_flags &= ~CONDUCTS_ELECTRICITY
+
+ // Wielding
+ var/density = material.get_property(MATERIAL_DENSITY)
+ var/hardness = material.get_property(MATERIAL_HARDNESS)
+ // Can be faster/slower by 2 dcs
+ target.attack_speed += MATERIAL_PROPERTY_DIVERGENCE(density, 4, 6) * 0.5 * multiplier
+ target.throw_range += ((hardness - 4) - (density - 4) * 2) * multiplier
+ return FALSE
+
+/datum/material_slot/handle/on_removed(obj/item/target, datum/material/material, amount, multiplier)
+ UnregisterSignal(target, list(COMSIG_MOVABLE_IMPACT, COMSIG_ITEM_ATTACK, COMSIG_ITEM_ATTACK_ATOM, COMSIG_ITEM_ATTACK_SELF))
+
+ if (!(target.material_flags & MATERIAL_AFFECT_STATISTICS) || !(target.material_flags & MATERIAL_EFFECTS))
+ return FALSE
+
+ // Armor/integrity
+ var/integrity_mod = material.get_property(MATERIAL_INTEGRITY)
+
+ target.modify_max_integrity(ceil(target.max_integrity * integrity_mod))
+ var/list/armor_mods = material.get_armor_modifiers(multiplier)
+ for (var/armor_type, value in armor_mods)
+ if (value != 0) // Needs to be restored to initial values in finalize effects, sorry
+ armor_mods[armor_type] = 1 / value
+ target.set_armor(target.get_armor().generate_new_with_multipliers(armor_mods))
+
+ // Conductivity
+ var/conductivity = material.get_property(MATERIAL_ELECTRICAL)
+ var/siemens_modifier = round(max(0, conductivity - 1) ** 1.18 * 0.15, 0.01)
+ var/siemens_mult = 1 + (siemens_modifier - 1) * multiplier
+ if (siemens_mult > 0)
+ target.siemens_coefficient /= siemens_mult
+
+ if (target.siemens_coefficient > 0 && (initial(target.obj_flags) & CONDUCTS_ELECTRICITY) && !(target.obj_flags & CONDUCTS_ELECTRICITY))
+ target.obj_flags |= CONDUCTS_ELECTRICITY
+
+ // Wielding
+ var/density = material.get_property(MATERIAL_DENSITY)
+ var/hardness = material.get_property(MATERIAL_HARDNESS)
+ target.attack_speed -= MATERIAL_PROPERTY_DIVERGENCE(density, 4, 6) * 0.5 * multiplier
+ target.throw_range -= ((hardness - 4) - (density - 4) * 2) * multiplier
+ return FALSE
+
+/datum/material_slot/handle/proc/on_item_attack(obj/item/source, atom/movable/target, mob/living/user)
+ SIGNAL_HANDLER
+ affect_user(source, user, user)
+
+/datum/material_slot/handle/proc/on_item_attack_self(obj/item/source, mob/living/user)
+ SIGNAL_HANDLER
+ affect_user(source, user, user)
+
+/datum/material_slot/handle/proc/on_throw_impact(obj/item/source, atom/hit_atom, datum/thrownthing/throwing_datum, caught)
+ SIGNAL_HANDLER
+ if (caught)
+ affect_user(source, hit_atom, astype(throwing_datum.thrower.resolve(), /mob/living))
+
+/datum/material_slot/handle/proc/affect_user(obj/item/source, mob/living/user, mob/living/initiator)
+ var/datum/material/source_mat = source.get_material_from_slot(type)
+ var/arm_dir = IS_LEFT_INDEX(user.active_hand_index) ? BODY_ZONE_L_ARM : BODY_ZONE_R_ARM
+ if (!ishuman(user))
+ SEND_SIGNAL(source_mat, COMSIG_MATERIAL_EFFECT_TOUCH, source, user, initiator, arm_dir, TRUE)
+ return
+
+ var/mob/living/carbon/human/as_human = user
+ var/obj/item/bodypart/hand = as_human.has_hand_for_held_index(as_human.get_held_index_of_item(source))
+ if (!hand) // ???
+ SEND_SIGNAL(source_mat, COMSIG_MATERIAL_EFFECT_TOUCH, source, user, initiator, arm_dir, FALSE) // ...no hand, no skin contact?
+ return
+
+ var/list/obj/item/hand_covers = as_human.get_clothing_on_part(hand)
+ var/hand_covered = FALSE
+ for (var/obj/item/worn_item in hand_covers)
+ if (worn_item.body_parts_covered & HANDS)
+ hand_covered = TRUE
+ break
+
+ SEND_SIGNAL(source_mat, COMSIG_MATERIAL_EFFECT_TOUCH, source, user, initiator, hand.body_zone, !hand_covered)
diff --git a/code/datums/materials/meat.dm b/code/datums/materials/meat.dm
index 8e33c31a50fb..00c26c98797c 100644
--- a/code/datums/materials/meat.dm
+++ b/code/datums/materials/meat.dm
@@ -8,7 +8,7 @@
mat_properties = list(
MATERIAL_DENSITY = 5,
MATERIAL_HARDNESS = 0,
- MATERIAL_FLEXIBILITY = 6,
+ MATERIAL_FLEXIBILITY = 5,
MATERIAL_REFLECTIVITY = 4,
MATERIAL_ELECTRICAL = 8,
MATERIAL_THERMAL = 4,
@@ -34,7 +34,7 @@
if(!(organ::organ_flags & ORGAN_ORGANIC))
organ.organ_flags |= ORGAN_ORGANIC
-/datum/material/meat/on_applied(atom/source, mat_amount, multiplier)
+/datum/material/meat/on_applied(atom/source, mat_amount, multiplier, from_slot)
. = ..()
if(IS_EDIBLE(source))
make_edible(source, mat_amount, multiplier)
@@ -92,7 +92,7 @@
blood_dna_info = blood_dna,\
)
-/datum/material/meat/on_removed(atom/source, mat_amount, multiplier)
+/datum/material/meat/on_removed(atom/source, mat_amount, multiplier, from_slot)
. = ..()
source.RemoveComponentSource(SOURCE_EDIBLE_MEAT_MAT, /datum/component/edible)
qdel(source.GetComponent(/datum/component/blood_walk))
diff --git a/code/datums/materials/pizza.dm b/code/datums/materials/pizza.dm
index 3eb8bb533e2b..df77ba1d1e47 100644
--- a/code/datums/materials/pizza.dm
+++ b/code/datums/materials/pizza.dm
@@ -6,7 +6,7 @@
mat_properties = list(
MATERIAL_DENSITY = 4,
MATERIAL_HARDNESS = 1,
- MATERIAL_FLEXIBILITY = 6,
+ MATERIAL_FLEXIBILITY = 5,
MATERIAL_REFLECTIVITY = 2,
MATERIAL_ELECTRICAL = 8,
MATERIAL_THERMAL = 4,
@@ -23,7 +23,7 @@
make_edible(source, mat_amount)
ADD_TRAIT(source, TRAIT_ROD_REMOVE_FISHING_DUD, REF(src)) //the fishing rod itself is the bait... sorta.
-/datum/material/pizza/on_applied(atom/source, mat_amount, multiplier)
+/datum/material/pizza/on_applied(atom/source, mat_amount, multiplier, from_slot)
. = ..()
if(IS_EDIBLE(source))
make_edible(source, mat_amount, multiplier)
@@ -54,7 +54,7 @@
eat_time = 3 SECONDS, \
tastes = /obj/item/food/pizza/margherita::tastes)
-/datum/material/pizza/on_removed(atom/source, mat_amount, multiplier)
+/datum/material/pizza/on_removed(atom/source, mat_amount, multiplier, from_slot)
. = ..()
source.RemoveComponentSource(SOURCE_EDIBLE_PIZZA_MAT, /datum/component/edible)
diff --git a/code/datums/materials/properties/_properties.dm b/code/datums/materials/properties/_properties.dm
index b9fa27e26a02..abb6dd6b8cce 100644
--- a/code/datums/materials/properties/_properties.dm
+++ b/code/datums/materials/properties/_properties.dm
@@ -11,6 +11,10 @@
/datum/material_property/proc/get_descriptor(value)
return null
+/// Returns the contents of the tooltip under our descriptor
+/datum/material_property/proc/get_tooltip(value)
+ return "[value < 0 ? "-" : ""]\Roman[round(abs(value), 1)]"
+
/// Called whenever a material with this property initializes. Mostly used for behavior tracking on optional properties
/datum/material_property/proc/attach_to(datum/material/material)
return
diff --git a/code/datums/materials/properties/derived.dm b/code/datums/materials/properties/derived.dm
index 4c18c035de73..162c49549275 100644
--- a/code/datums/materials/properties/derived.dm
+++ b/code/datums/materials/properties/derived.dm
@@ -45,3 +45,14 @@
// Requires the material to be especially shiny or dull
var/reflectivity = material.get_property(MATERIAL_REFLECTIVITY)
return MATERIAL_PROPERTY_DIVERGENCE(reflectivity, 3, 6) * 0.05
+
+/// Siemens coeff multiplier for our material
+/datum/material_property/derived/insulation
+ id = MATERIAL_INSULATION
+
+/datum/material_property/derived/insulation/get_value(datum/material/material)
+ // [0 ~ 1] is fully insulating, (1 ~ 6] maps to (0 ~ 1] and [6 ~ 10] maps to [1 ~ 2]
+ // 1.18 and 0.15 here are to allow 6 to map to 1 and 10 to map to 2 and are pulled out of my ass (system in the desmos below)
+ // See https://www.desmos.com/calculator/rdbv1x8oty
+ var/conductivity = material.get_property(MATERIAL_ELECTRICAL)
+ return round(max(0, conductivity - 1) ** 1.18 * 0.15, 0.01)
diff --git a/code/datums/materials/properties/optional.dm b/code/datums/materials/properties/optional.dm
index 8982e0d5e7c6..38d5287d3cfb 100644
--- a/code/datums/materials/properties/optional.dm
+++ b/code/datums/materials/properties/optional.dm
@@ -33,16 +33,16 @@
RegisterSignal(material, COMSIG_MATERIAL_APPLIED, PROC_REF(on_applied))
RegisterSignal(material, COMSIG_MATERIAL_REMOVED, PROC_REF(on_removed))
-/datum/material_property/flammability/proc/on_applied(datum/material/source, atom/new_atom, mat_amount, multiplier)
+/datum/material_property/flammability/proc/on_applied(datum/material/source, atom/new_atom, mat_amount, multiplier, from_slot)
SIGNAL_HANDLER
- if (isobj(new_atom) && (new_atom.material_flags & MATERIAL_AFFECT_STATISTICS) && source.get_property(id) > MINIMUM_FLAMMABILITY)
+ if (isobj(new_atom) && (new_atom.material_flags & MATERIAL_AFFECT_STATISTICS) && source.get_property(id) >= MINIMUM_FLAMMABILITY)
new_atom.resistance_flags |= FLAMMABLE
-/datum/material_property/flammability/proc/on_removed(datum/material/source, atom/old_atom, mat_amount, multiplier)
+/datum/material_property/flammability/proc/on_removed(datum/material/source, atom/old_atom, mat_amount, multiplier, from_slot)
SIGNAL_HANDLER
- if (isobj(old_atom) && (old_atom.material_flags & MATERIAL_AFFECT_STATISTICS) && source.get_property(id) > MINIMUM_FLAMMABILITY && !(initial(old_atom.resistance_flags) & FLAMMABLE))
+ if (isobj(old_atom) && (old_atom.material_flags & MATERIAL_AFFECT_STATISTICS) && source.get_property(id) >= MINIMUM_FLAMMABILITY && !(initial(old_atom.resistance_flags) & FLAMMABLE))
old_atom.resistance_flags &= ~FLAMMABLE
#undef MINIMUM_FLAMMABILITY
@@ -74,16 +74,135 @@
RegisterSignal(material, COMSIG_MATERIAL_APPLIED, PROC_REF(on_applied))
RegisterSignal(material, COMSIG_MATERIAL_REMOVED, PROC_REF(on_removed))
-/datum/material_property/radioactivity/proc/on_applied(datum/material/source, atom/new_atom, mat_amount, multiplier)
+/datum/material_property/radioactivity/proc/on_applied(datum/material/source, atom/new_atom, mat_amount, multiplier, from_slot)
SIGNAL_HANDLER
// Uranium structures should irradiate, but not items, because item irradiation is a lot more annoying.
if (!isitem(new_atom))
new_atom.AddElement(/datum/element/radioactive, chance = source.get_property(id) / URANIUM_RADIOACTIVITY * URANIUM_IRRADIATION_CHANCE * multiplier)
-/datum/material_property/radioactivity/proc/on_removed(datum/material/source, atom/old_atom, mat_amount, multiplier)
+/datum/material_property/radioactivity/proc/on_removed(datum/material/source, atom/old_atom, mat_amount, multiplier, from_slot)
SIGNAL_HANDLER
if (!isitem(old_atom))
old_atom.RemoveElement(/datum/element/radioactive, chance = source.get_property(id) / URANIUM_RADIOACTIVITY * URANIUM_IRRADIATION_CHANCE * multiplier)
#undef URANIUM_RADIOACTIVITY
+
+/// Applies firestacks to affected mobs
+/datum/material_property/firestacker
+ name = "Igniting"
+ id = MATERIAL_FIRESTACKER
+
+/datum/material_property/firestacker/get_descriptor(value)
+ return "igniting"
+
+/datum/material_property/firestacker/get_tooltip(value)
+ return "Applies [value] firestacks to affected mobs"
+
+/datum/material_property/firestacker/attach_to(datum/material/material)
+ . = ..()
+ material.track_flags |= MATERIAL_TRACK_CONTACT | MATERIAL_TRACK_IMPACT
+ var/static/list/interaction_signals = list(
+ COMSIG_MATERIAL_EFFECT_TOUCH,
+ COMSIG_MATERIAL_EFFECT_STEP,
+ COMSIG_MATERIAL_EFFECT_HIT,
+ COMSIG_MATERIAL_EFFECT_THROW_IMPACT,
+ )
+ RegisterSignals(material, interaction_signals, PROC_REF(on_contact))
+
+/datum/material_property/firestacker/proc/on_contact(datum/material/source, atom/object, mob/living/target, mob/living/user, def_zone, skin_contact)
+ SIGNAL_HANDLER
+
+ // Floors don't trigger if you're wearing shoes because it'd be too cancer
+ if (isfloorturf(object) && !skin_contact && !source.get_property(MATERIAL_PENETRATING))
+ return
+
+ if (isliving(target))
+ target.adjust_fire_stacks(source.get_property(id))
+
+/// Deals additional burn damage to vampires, property value determines damage
+/datum/material_property/vampires_bane
+ name = "Vampires' Bane"
+ id = MATERIAL_VAMPIRES_BANE
+
+/datum/material_property/vampires_bane/get_descriptor(value)
+ return "vampires' bane"
+
+/datum/material_property/vampires_bane/get_tooltip(value)
+ return "Deals [value] additional burn damage to vampires on contact"
+
+/datum/material_property/vampires_bane/attach_to(datum/material/material)
+ . = ..()
+ material.track_flags |= MATERIAL_TRACK_CONTACT | MATERIAL_TRACK_IMPACT
+ var/static/list/interaction_signals = list(
+ COMSIG_MATERIAL_EFFECT_TOUCH,
+ COMSIG_MATERIAL_EFFECT_STEP,
+ COMSIG_MATERIAL_EFFECT_HIT,
+ COMSIG_MATERIAL_EFFECT_THROW_IMPACT,
+ )
+ RegisterSignals(material, interaction_signals, PROC_REF(on_contact))
+
+/datum/material_property/vampires_bane/proc/on_contact(datum/material/source, atom/object, mob/living/target, mob/living/user, def_zone, skin_contact)
+ SIGNAL_HANDLER
+
+ if (!isvampire(target) || (!skin_contact && !source.get_property(MATERIAL_PENETRATING)))
+ return
+
+ to_chat(target, span_userdanger("Contact with [object] sears your undead flesh!"))
+ target.apply_damage(source.get_property(id), BURN, def_zone, wound_bonus = 10, wound_clothing = FALSE)
+
+/// Teleports targets who come into active contact with the material around, property value determines teleport radius and damage taken per teleport
+/datum/material_property/teleporting
+ name = "Teleporting"
+ id = MATERIAL_TELEPORTING
+
+/datum/material_property/teleporting/get_descriptor(value)
+ return "dimensionally unstable"
+
+/datum/material_property/teleporting/get_tooltip(value)
+ return "Randomly teleports whoever comes into contact with it in a [value] tile radius"
+
+/datum/material_property/teleporting/attach_to(datum/material/material)
+ . = ..()
+ material.track_flags |= MATERIAL_TRACK_CONTACT | MATERIAL_TRACK_IMPACT
+ var/static/list/interaction_signals = list(
+ COMSIG_MATERIAL_EFFECT_TOUCH,
+ COMSIG_MATERIAL_EFFECT_STEP,
+ COMSIG_MATERIAL_EFFECT_HIT,
+ COMSIG_MATERIAL_EFFECT_THROW_IMPACT,
+ )
+ RegisterSignals(material, interaction_signals, PROC_REF(on_contact))
+
+/datum/material_property/teleporting/proc/on_contact(datum/material/source, atom/object, atom/target, mob/living/user, def_zone, skin_contact)
+ SIGNAL_HANDLER
+
+ if (!ismovable(target))
+ return
+
+ var/atom/movable/as_movable = target
+ // Don't teleport airlocks around please
+ if (as_movable.anchored || as_movable.move_resist >= INFINITY)
+ return
+
+ // Floors don't trigger if you're wearing shoes because it'd be too cancer
+ if (isfloorturf(object) && !skin_contact && !source.get_property(MATERIAL_PENETRATING))
+ return
+
+ var/value = source.get_property(id)
+ do_teleport(target, get_turf(target), value, channel = TELEPORT_CHANNEL_BLUESPACE)
+ if (isstack(object))
+ var/obj/item/stack/as_stack = object
+ as_stack.use(1)
+ else if (object.uses_integrity)
+ object.take_damage(object.max_integrity * value * 0.01)
+
+/// Makes all contact count as skin contact
+/datum/material_property/penetrating
+ name = "Penetrating"
+ id = MATERIAL_PENETRATING
+
+/datum/material_property/penetrating/get_descriptor(value)
+ return "dimensionally penetrating"
+
+/datum/material_property/penetrating/get_tooltip(value)
+ return "Ignores all means of skin protection when triggering other material effects"
diff --git a/code/datums/materials/requirements/_requirement.dm b/code/datums/materials/requirements/_requirement.dm
index 2447add59609..d23e11c3fb98 100644
--- a/code/datums/materials/requirements/_requirement.dm
+++ b/code/datums/materials/requirements/_requirement.dm
@@ -57,6 +57,9 @@
property_minimums = list(
MATERIAL_HARDNESS = 2,
)
+ property_maximums = list(
+ MATERIAL_FLEXIBILITY = 5,
+ )
/datum/material_requirement/rigid_material
required_flags = MATERIAL_CLASS_RIGID
diff --git a/code/datums/quirks/positive_quirks/keen_nose.dm b/code/datums/quirks/positive_quirks/keen_nose.dm
new file mode 100644
index 000000000000..2681d34b86dd
--- /dev/null
+++ b/code/datums/quirks/positive_quirks/keen_nose.dm
@@ -0,0 +1,9 @@
+/datum/quirk/keen_nose
+ name = "Keen Nose"
+ desc = "You have a very sensitive nose. You can smell the contents of any open containers you are holding simply by examining them."
+ icon = FA_ICON_FLASK
+ value = 3
+ mob_trait = TRAIT_KEEN_NOSE
+ gain_text = span_notice("You can smell everything around you much more clearly.")
+ lose_text = span_danger("Your sense of smell returns to normal.")
+ medical_record_text = "Patient has an unusually sensitive olfactory system."
diff --git a/code/datums/records/record.dm b/code/datums/records/record.dm
index 9785ac8ffcb0..93893f7871e0 100644
--- a/code/datums/records/record.dm
+++ b/code/datums/records/record.dm
@@ -75,6 +75,8 @@
var/minor_disabilities_desc
/// Physical status of this person in medical records.
var/physical_status
+ /// If declared dead, this is set as the cause of death, wiped once declared alive again.
+ var/cause_of_death
/// Mental status of this person in medical records.
var/mental_status
/// Positive and neutral quirk strings
diff --git a/code/datums/sprite_accessories.dm b/code/datums/sprite_accessories.dm
index 33e1fc8094f6..552eb1020053 100644
--- a/code/datums/sprite_accessories.dm
+++ b/code/datums/sprite_accessories.dm
@@ -75,6 +75,10 @@
icon_state = "hide_winterhood"
strict_coverage_zones = HAIR_APPENDAGE_TOP | HAIR_APPENDAGE_LEFT | HAIR_APPENDAGE_RIGHT | HAIR_APPENDAGE_REAR | HAIR_APPENDAGE_HANGING_REAR
+/datum/hair_mask/hoodie
+ icon_state = "hide_hoodie"
+ strict_coverage_zones = HAIR_APPENDAGE_TOP | HAIR_APPENDAGE_LEFT | HAIR_APPENDAGE_RIGHT | HAIR_APPENDAGE_REAR | HAIR_APPENDAGE_HANGING_REAR
+
//////////////////////
// Hair Definitions //
//////////////////////
diff --git a/code/datums/station_traits/negative_traits.dm b/code/datums/station_traits/negative_traits.dm
index a1105349f983..51fbde0adf28 100644
--- a/code/datums/station_traits/negative_traits.dm
+++ b/code/datums/station_traits/negative_traits.dm
@@ -115,7 +115,7 @@
weight = 5
cost = STATION_TRAIT_COST_LOW //Most of maints is literal trash anyway
show_in_report = TRUE
- report_message = "Our workers cleaned out most of the junk in the maintenace areas."
+ report_message = "Our workers cleaned out most of the junk in the maintenance areas."
blacklist = list(/datum/station_trait/filled_maint)
trait_to_give = STATION_TRAIT_EMPTY_MAINT
diff --git a/code/datums/station_traits/positive_traits.dm b/code/datums/station_traits/positive_traits.dm
index 95de42acba47..de6f7ebbcdfd 100644
--- a/code/datums/station_traits/positive_traits.dm
+++ b/code/datums/station_traits/positive_traits.dm
@@ -136,7 +136,7 @@
weight = 5
cost = STATION_TRAIT_COST_LOW
show_in_report = TRUE
- report_message = "Our workers accidentally forgot more of their personal belongings in the maintenace areas."
+ report_message = "Our workers accidentally forgot more of their personal belongings in the maintenance areas."
blacklist = list(/datum/station_trait/empty_maint)
trait_to_give = STATION_TRAIT_FILLED_MAINT
diff --git a/code/datums/status_effects/_status_effect_helpers.dm b/code/datums/status_effects/_status_effect_helpers.dm
index a5743d2e93ad..41e583fb6879 100644
--- a/code/datums/status_effects/_status_effect_helpers.dm
+++ b/code/datums/status_effects/_status_effect_helpers.dm
@@ -13,9 +13,7 @@
/mob/living/proc/apply_status_effect(datum/status_effect/new_effect, ...)
RETURN_TYPE(/datum/status_effect)
- // The arguments we pass to the start effect. The 1st argument is this mob.
var/list/arguments = args.Copy()
- arguments[1] = src
// If the status effect we're applying doesn't allow multiple effects, we need to handle it
if(initial(new_effect.status_type) != STATUS_EFFECT_MULTIPLE)
@@ -38,6 +36,9 @@
existing_effect.refresh(arglist(arguments))
return
+ // For the new effect the 1st argument is this mob
+ arguments[1] = src
+
// Create the status effect with our mob + our arguments
var/datum/status_effect/new_instance = new new_effect(arguments)
if(!QDELETED(new_instance))
diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm
index ec01ea45370c..c3734662080b 100644
--- a/code/datums/status_effects/buffs.dm
+++ b/code/datums/status_effects/buffs.dm
@@ -249,10 +249,10 @@
duration += workout_duration(new_owner, bonus_time)
return ..()
-/datum/status_effect/exercised/refresh(mob/living/new_owner, bonus_time)
- duration += workout_duration(new_owner, bonus_time)
- new_owner.clear_mood_event("exercise") // we need to reset the old mood event in case our fitness skill changes
- new_owner.add_mood_event("exercise", /datum/mood_event/exercise, new_owner.mind.get_skill_level(/datum/skill/athletics))
+/datum/status_effect/exercised/refresh(effect, bonus_time)
+ duration += workout_duration(owner, bonus_time)
+ owner.clear_mood_event("exercise") // we need to reset the old mood event in case our fitness skill changes
+ owner.add_mood_event("exercise", /datum/mood_event/exercise, owner.mind.get_skill_level(/datum/skill/athletics))
/datum/status_effect/exercised/on_apply()
if(!owner.mind)
diff --git a/code/datums/status_effects/debuffs/cyborg.dm b/code/datums/status_effects/debuffs/cyborg.dm
index 30cea1af7455..6199de617fe4 100644
--- a/code/datums/status_effects/debuffs/cyborg.dm
+++ b/code/datums/status_effects/debuffs/cyborg.dm
@@ -20,7 +20,7 @@
/datum/status_effect/borg_slow/on_remove()
owner.remove_movespeed_modifier(/datum/movespeed_modifier/borg_slowdown)
-/datum/status_effect/borg_slow/refresh(mob/living/new_owner, slowdown = 1)
+/datum/status_effect/borg_slow/refresh(effect, slowdown = 1)
. = ..()
if(src.slowdown <= slowdown)
return
diff --git a/code/datums/status_effects/debuffs/fire_stacks.dm b/code/datums/status_effects/debuffs/fire_stacks.dm
index d55ed23e1655..e5270d2e72a5 100644
--- a/code/datums/status_effects/debuffs/fire_stacks.dm
+++ b/code/datums/status_effects/debuffs/fire_stacks.dm
@@ -19,7 +19,7 @@
/// For how much firestacks does one our stack count
var/stack_modifier = 1
-/datum/status_effect/fire_handler/refresh(mob/living/new_owner, new_stacks, forced = FALSE)
+/datum/status_effect/fire_handler/refresh(effect, new_stacks, forced = FALSE)
if(forced)
set_stacks(new_stacks)
else
diff --git a/code/datums/status_effects/debuffs/genetic_damage.dm b/code/datums/status_effects/debuffs/genetic_damage.dm
index c0ae16d4082e..fd96231961d0 100644
--- a/code/datums/status_effects/debuffs/genetic_damage.dm
+++ b/code/datums/status_effects/debuffs/genetic_damage.dm
@@ -27,7 +27,7 @@
. = ..()
UnregisterSignal(owner, COMSIG_LIVING_HEALTHSCAN)
-/datum/status_effect/genetic_damage/refresh(mob/living/owner, total_damage)
+/datum/status_effect/genetic_damage/refresh(effect, total_damage)
. = ..()
src.total_damage += total_damage
diff --git a/code/datums/status_effects/debuffs/hallucination.dm b/code/datums/status_effects/debuffs/hallucination.dm
index 9c40203c1dd7..7e3286180963 100644
--- a/code/datums/status_effects/debuffs/hallucination.dm
+++ b/code/datums/status_effects/debuffs/hallucination.dm
@@ -166,7 +166,7 @@
strict_tier = TRUE
variable_tier = FALSE
-/datum/status_effect/hallucination/perceptomatrix/refresh(mob/living/refresh_owner, new_duration)
+/datum/status_effect/hallucination/perceptomatrix/refresh(effect, new_duration)
src.duration += new_duration
/datum/status_effect/hallucination/perceptomatrix/on_creation(mob/living/new_owner, new_duration)
diff --git a/code/datums/storage/storage.dm b/code/datums/storage/storage.dm
index 472f9c6baa64..4198d41535bd 100644
--- a/code/datums/storage/storage.dm
+++ b/code/datums/storage/storage.dm
@@ -1153,7 +1153,7 @@ GLOBAL_LIST_EMPTY(cached_storage_typecaches)
var/additional_row = (!(adjusted_contents % screen_max_columns) && adjusted_contents < max_slots)
var/columns = clamp(max_slots, 1, screen_max_columns)
- var/rows = clamp(CEILING(adjusted_contents / columns, 1) + additional_row, 1, screen_max_rows)
+ var/rows = clamp(ceil(adjusted_contents / columns) + additional_row, 1, screen_max_rows)
for (var/mob/ui_user as anything in storage_interfaces)
if (isnull(storage_interfaces[ui_user]))
diff --git a/code/datums/storage/subtypes/cyborg.dm b/code/datums/storage/subtypes/cyborg.dm
index 9b77b6c344e0..a02da77e1c14 100644
--- a/code/datums/storage/subtypes/cyborg.dm
+++ b/code/datums/storage/subtypes/cyborg.dm
@@ -32,7 +32,7 @@
adjusted_contents = length(numbered_contents)
var/columns = clamp(max_slots, 1, screen_max_columns)
- var/rows = clamp(CEILING(adjusted_contents / columns, 1), 1, screen_max_rows)
+ var/rows = clamp(ceil(adjusted_contents / columns), 1, screen_max_rows)
for (var/mob/ui_user as anything in storage_interfaces)
if (isnull(storage_interfaces[ui_user]))
diff --git a/code/datums/storage/subtypes/others/pod.dm b/code/datums/storage/subtypes/others/pod.dm
index 4bbbcce74acc..87468d9b3742 100644
--- a/code/datums/storage/subtypes/others/pod.dm
+++ b/code/datums/storage/subtypes/others/pod.dm
@@ -6,7 +6,7 @@
var/always_unlocked = FALSE
/datum/storage/pod/open_storage(mob/to_show)
- if(isliving(to_show) && SSsecurity_level.get_current_level_as_number() < SEC_LEVEL_RED)
+ if(locked && isliving(to_show)) //Observers get to see anyway
to_chat(to_show, span_warning("The storage unit will only unlock during a Red or Delta security alert."))
return FALSE
return ..()
diff --git a/code/datums/wounds/_wounds.dm b/code/datums/wounds/_wounds.dm
index f6a0f0abaeb6..1654670e5045 100644
--- a/code/datums/wounds/_wounds.dm
+++ b/code/datums/wounds/_wounds.dm
@@ -649,7 +649,7 @@
var/obj/item/stack/medical/wrap/current_gauze = LAZYACCESS(limb.applied_items, LIMB_ITEM_GAUZE)
if ((wound_flags & ACCEPTS_GAUZE) && current_gauze)
var/sling_condition = get_gauze_condition()
- desc = "[victim.p_Their()] [limb.plaintext_zone] is [sling_condition] fastened in a sling of [current_gauze.name]"
+ desc = "[victim.p_Their()] [limb.plaintext_zone] is [sling_condition]fastened in a sling of [current_gauze.name]"
else
desc = "[victim.p_Their()] [limb.plaintext_zone] [examine_desc]"
@@ -693,13 +693,13 @@
switch(current_gauze.absorption_capacity)
if(0 to 1.25)
- return "just barely"
+ return "just barely "
if(1.25 to 2.75)
- return "loosely"
+ return "loosely "
if(2.75 to 4)
- return "mostly"
+ return "mostly "
if(4 to INFINITY)
- return "tightly"
+ return "tightly "
/// Spans [desc] based on our severity.
/datum/wound/proc/get_desc_intensity(desc)
diff --git a/code/datums/wounds/burns.dm b/code/datums/wounds/burns.dm
index e6a83b856905..9d7a9932119e 100644
--- a/code/datums/wounds/burns.dm
+++ b/code/datums/wounds/burns.dm
@@ -158,15 +158,15 @@
var/bandage_condition
switch(current_gauze.absorption_capacity)
if(0 to 1.25)
- bandage_condition = "nearly ruined"
+ bandage_condition = "nearly ruined "
if(1.25 to 2.75)
- bandage_condition = "badly worn"
+ bandage_condition = "badly worn "
if(2.75 to 4)
- bandage_condition = "slightly stained"
+ bandage_condition = "slightly stained "
if(4 to INFINITY)
- bandage_condition = "clean"
+ bandage_condition = "clean "
- condition += " underneath a dressing of [bandage_condition] [current_gauze.name]."
+ condition += " underneath a dressing of [bandage_condition][current_gauze.name]."
else
switch(infection)
if(WOUND_INFECTION_MODERATE to WOUND_INFECTION_SEVERE)
diff --git a/code/datums/wounds/pierce.dm b/code/datums/wounds/pierce.dm
index bda3a6220bb1..2ccf49d293f9 100644
--- a/code/datums/wounds/pierce.dm
+++ b/code/datums/wounds/pierce.dm
@@ -113,7 +113,7 @@
//gauze always reduces blood flow, even for non bleeders
var/obj/item/stack/medical/wrap/current_gauze = LAZYACCESS(limb.applied_items, LIMB_ITEM_GAUZE)
- if(current_gauze)
+ if(current_gauze?.absorption_rate)
var/gauze_power = current_gauze.absorption_rate
limb.seep_gauze(gauze_power * seconds_per_tick)
adjust_blood_flow((-clot_rate * seconds_per_tick) + (-gauze_power * gauzed_clot_rate * seconds_per_tick))
diff --git a/code/datums/wounds/slash.dm b/code/datums/wounds/slash.dm
index 5ed2e8695b78..856a54f86314 100644
--- a/code/datums/wounds/slash.dm
+++ b/code/datums/wounds/slash.dm
@@ -102,13 +102,13 @@
// how much life we have left in these bandages
switch(current_gauze.absorption_capacity)
if(0 to 1.25)
- msg += "nearly ruined"
+ msg += "nearly ruined "
if(1.25 to 2.75)
- msg += "badly worn"
+ msg += "badly worn "
if(2.75 to 4)
- msg += "slightly bloodied"
+ msg += "slightly bloodied "
if(4 to INFINITY)
- msg += "clean"
+ msg += "clean "
msg += "[current_gauze.name]!"
return "[msg.Join()]"
@@ -158,7 +158,7 @@
adjust_blood_flow(0.25) // old heparin used to just add +2 bleed stacks per tick, this adds 0.5 bleed flow to all open cuts which is probably even stronger as long as you can cut them first
var/obj/item/stack/medical/wrap/current_gauze = LAZYACCESS(limb.applied_items, LIMB_ITEM_GAUZE)
- if(current_gauze)
+ if(current_gauze?.absorption_rate)
var/gauze_power = current_gauze.absorption_rate
limb.seep_gauze(gauze_power * seconds_per_tick)
adjust_blood_flow(-gauze_power * seconds_per_tick)
diff --git a/code/game/atom/atom_examine.dm b/code/game/atom/atom_examine.dm
index a19b83ad84d2..d2dc54d5d50f 100644
--- a/code/game/atom/atom_examine.dm
+++ b/code/game/atom/atom_examine.dm
@@ -44,7 +44,6 @@
if(reagents.is_reacting)
. += span_warning("It is currently reacting!")
. += span_notice("The solution's pH is [round(reagents.ph, 0.01)] and has a temperature of [reagents.chem_temp]K.")
-
else
. += "It contains:
Nothing."
else if(reagents.flags & AMOUNT_VISIBLE)
@@ -53,8 +52,29 @@
else
. += span_danger("It's empty.")
+ if(HAS_TRAIT(user, TRAIT_KEEN_NOSE))
+ var/sniff_text = get_sniff_examine(user)
+ if(sniff_text)
+ . += sniff_text
+
SEND_SIGNAL(src, COMSIG_ATOM_EXAMINE, user, .)
+/// Returns an examine string describing what the contents of this atom smell like
+/atom/proc/get_sniff_examine(mob/living/carbon/sniffer)
+ if(!istype(sniffer) || HAS_TRAIT(sniffer, TRAIT_ANOSMIA))
+ return
+ if(!is_open_container() || !reagents?.total_volume)
+ return
+ if(!sniffer.is_holding(src))
+ return
+ if(!sniffer.get_bodypart(BODY_ZONE_HEAD)) // Need a nose to smell
+ return
+ if(sniffer.is_mouth_covered())
+ return span_warning("You can't get a whiff of [src] with your face covered.")
+
+ var/smell_message = generate_reagents_taste_message(reagents.reagent_list, sniffer, 10)
+ return span_notice("You catch a whiff of [src]. It smells like [smell_message].")
+
/**
* A list of "tags" displayed after atom's description in examine.
* This should return an assoc list of tags -> tooltips for them. If item is null, then no tooltip is assigned.
@@ -145,8 +165,9 @@
if (isnull(prop_value)) // Error?
continue
var/descriptor = property?.get_descriptor(prop_value)
+ var/tooltip_hint = property?.get_tooltip(prop_value)
if (descriptor) // Overriden derivative property?
- material_string += span_tooltip("[property]: [prop_value < 0 ? "-" : ""]\Roman[round(abs(prop_value), 1)]", descriptor)
+ material_string += span_tooltip("[property]: [tooltip_hint]", descriptor)
if (length(material_string))
. += span_info("[capitalize(material.name)] is [english_list(material_string)].")
diff --git a/code/game/atom/atom_materials.dm b/code/game/atom/atom_materials.dm
index 16153284345f..05a1801ee6f6 100644
--- a/code/game/atom/atom_materials.dm
+++ b/code/game/atom/atom_materials.dm
@@ -1,11 +1,13 @@
/atom
- ///The custom materials this atom is made of, used by a lot of things like furniture, walls, and floors (if I finish the functionality, that is.)
- ///The list referenced by this var can be shared by multiple objects and should not be directly modified. Instead, use [set_custom_materials][/atom/proc/set_custom_materials].
+ /// The custom materials this atom is made of, used by a lot of things like furniture, walls, and floors (if I finish the functionality, that is.)
+ /// The list referenced by this var can be shared by multiple objects and should not be directly modified. Instead, use [set_custom_materials][/atom/proc/set_custom_materials].
var/list/datum/material/custom_materials
- ///Bitfield for how the atom handles materials.
+ /// Bitfield for how the atom handles materials.
var/material_flags = NONE
- ///Modifier that raises/lowers the effect of the amount of a material, prevents small and easy to get items from being death machines.
+ /// Modifier that raises/lowers the effect of the amount of a material, prevents small and easy to get items from being death machines.
var/material_modifier = 1
+ /// List of material slots to be used to control material behaviors instead of default ones
+ var/list/datum/material_slot/material_slots = null
/// Sets the custom materials for an atom. This is what you want to call, since most of the ones below are mainly internal.
/atom/proc/set_custom_materials(list/materials, multiplier = 1)
@@ -81,8 +83,101 @@
MATERIAL_LIST_MULTIPLIER = get_material_multiplier(material, materials, index),
)
index++
+
+ if(material_slots)
+ configure_material_slots(material_effects)
+
return material_effects
+/// Add MATERIAL_LIST_SLOTS entries to material effects
+/atom/proc/configure_material_slots(list/datum/material/material_effects)
+ for (var/slot_index in 1 to length(material_slots))
+ var/slot_type = material_slots[slot_index]
+ var/datum/material/material = null
+ // The slot has a specific material assigned to it
+ if (material_slots[slot_type])
+ material = SSmaterials.get_material(material_slots[slot_type])
+ // Slots were unset, abort
+ if (!material_effects[material])
+ continue
+ else if (slot_index <= length(material_effects)) // Otherwise, go by index
+ material = material_effects[slot_index]
+ if (!material)
+ continue
+ var/list/effects_list = material_effects[material]
+ if (!effects_list[MATERIAL_LIST_SLOTS])
+ effects_list[MATERIAL_LIST_SLOTS] = list()
+ var/list/effects_slots = effects_list[MATERIAL_LIST_SLOTS]
+ effects_slots[slot_type] = TRUE
+
+/atom/proc/set_material_slot(slot_type, new_material)
+ if (material_slots[slot_type])
+ var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
+ var/datum/material/material = SSmaterials.get_material(material_slots[slot_type])
+ // Not present/initialized
+ if (material && custom_materials[material])
+ var/list/materials_slots = get_slots_of_material(material)
+ var/slot_sum = 0
+ if (length(materials_slots) > 1)
+ for (var/other_slot_type in materials_slots)
+ var/datum/material_slot/other_slot = SSmaterials.material_slots[other_slot_type]
+ slot_sum += other_slot.material_amount
+ slot.on_removed(src, material, custom_materials[material] * (slot_sum > 0 ? slot.material_amount / slot_sum : 1), get_material_multiplier(material, custom_materials, custom_materials.Find(material)))
+
+
+ // Don't store materials directly, only their IDs
+ if (istype(new_material, /datum/material))
+ var/datum/material/as_material = new_material
+ material_slots[slot_type] = as_material.id
+ else
+ material_slots[slot_type] = new_material
+
+ var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
+ var/datum/material/material = istype(new_material, /datum/material) ? new_material : SSmaterials.get_material(new_material)
+ if (!material || !custom_materials[material])
+ return
+ var/list/materials_slots = get_slots_of_material(material)
+ var/slot_sum = 0
+ if (length(materials_slots) > 1)
+ for (var/other_slot_type in materials_slots)
+ var/datum/material_slot/other_slot = SSmaterials.material_slots[other_slot_type]
+ slot_sum += other_slot.material_amount
+ slot.on_applied(src, material, custom_materials[material] * (slot_sum > 0 ? slot.material_amount / slot_sum : 1), get_material_multiplier(material, custom_materials, custom_materials.Find(material)))
+
+/atom/proc/set_material_slots(list/new_slots)
+ if (length(material_slots))
+ for (var/slot_type, material_id in material_slots)
+ var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
+ var/datum/material/material = SSmaterials.get_material(material_id)
+ // Not present/initialized
+ if (!material || !custom_materials[material])
+ continue
+ var/list/materials_slots = get_slots_of_material(material)
+ var/slot_sum = 0
+ if (length(materials_slots) > 1)
+ for (var/other_slot_type in materials_slots)
+ var/datum/material_slot/other_slot = SSmaterials.material_slots[other_slot_type]
+ slot_sum += other_slot.material_amount
+ slot.on_removed(src, material, custom_materials[material] * (slot_sum > 0 ? slot.material_amount / slot_sum : 1), get_material_multiplier(material, custom_materials, custom_materials.Find(material)))
+
+ if (!length(new_slots))
+ material_slots = null
+ return
+
+ material_slots = new_slots.Copy()
+ for (var/slot_type, material_id in material_slots)
+ var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
+ var/datum/material/material = SSmaterials.get_material(material_id)
+ if (!material || !custom_materials[material])
+ continue
+ var/list/materials_slots = get_slots_of_material(material)
+ var/slot_sum = 0
+ if (length(materials_slots) > 1)
+ for (var/other_slot_type in materials_slots)
+ var/datum/material_slot/other_slot = SSmaterials.material_slots[other_slot_type]
+ slot_sum += other_slot.material_amount
+ slot.on_applied(src, material, custom_materials[material] * (slot_sum > 0 ? slot.material_amount / slot_sum : 1), get_material_multiplier(material, custom_materials, custom_materials.Find(material)))
+
/**
* A proc that can be used to selectively control the stat changes and effects from a material without affecting the others.
*
@@ -93,6 +188,12 @@
* be 1 if below 1. Just don't return negative values.
*/
/atom/proc/get_material_multiplier(datum/material/custom_material, list/materials, index)
+ if (!length(material_slots))
+ return 1 / length(materials)
+ // Slots usually account for multipliers in their own behaviors, so unless overriden it should just be 1
+ for (var/slot_type in material_slots)
+ if (material_slots[slot_type] == custom_material.id)
+ return 1
return 1 / length(materials)
///Called by apply_material_effects(). It ACTUALLY handles applying effects common to all atoms (depending on material flags)
@@ -104,13 +205,36 @@
var/datum/material/main_material = materials[1]//the material with the highest amount (after calculations)
var/main_mat_amount = materials[main_material][MATERIAL_LIST_OPTIMAL_AMOUNT]
var/main_mat_mult = materials[main_material][MATERIAL_LIST_MULTIPLIER]
+ var/do_main_material = TRUE
for(var/datum/material/custom_material as anything in materials)
var/list/deets = materials[custom_material]
var/mat_amount = deets[MATERIAL_LIST_OPTIMAL_AMOUNT]
var/multiplier = deets[MATERIAL_LIST_MULTIPLIER]
- apply_single_mat_effect(custom_material, mat_amount, multiplier)
- custom_material.on_applied(src, mat_amount, multiplier)
+ var/do_effects = TRUE
+ var/from_slot = FALSE
+ if(!isnull(material_slots) && length(deets[MATERIAL_LIST_SLOTS]))
+ from_slot = TRUE
+ var/slot_sum = 0
+ // A material is in multiple slots, we need to cut it up between them
+ if(length(deets[MATERIAL_LIST_SLOTS]) > 1)
+ for(var/slot_type in deets[MATERIAL_LIST_SLOTS])
+ var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
+ slot_sum += slot.material_amount
+
+ for(var/slot_type in deets[MATERIAL_LIST_SLOTS])
+ var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
+ var/slot_amt = mat_amount
+ if (slot_sum > 0)
+ slot_amt *= slot.material_amount / slot_sum
+ do_effects &= slot.on_applied(src, custom_material, slot_amt, multiplier)
+
+ if(!do_effects && custom_material == main_material)
+ do_main_material = FALSE
+
+ if(do_effects)
+ apply_single_mat_effect(custom_material, mat_amount, multiplier)
+ custom_material.on_applied(src, mat_amount, multiplier, from_slot = from_slot)
//Prevent changing things with pre-set colors, to keep colored toolboxes their looks for example
if(material_flags & (MATERIAL_COLOR|MATERIAL_GREYSCALE))
@@ -118,7 +242,8 @@
var/added_alpha = custom_material.alpha * (custom_material.alpha / 255)
total_alpha += GET_MATERIAL_MODIFIER(added_alpha, multiplier)
- apply_main_material_effects(main_material, main_mat_amount, main_mat_mult)
+ if(do_main_material)
+ apply_main_material_effects(main_material, main_mat_amount, main_mat_mult)
if(material_flags & (MATERIAL_COLOR|MATERIAL_GREYSCALE))
var/previous_alpha = alpha
@@ -219,20 +344,15 @@
/atom/proc/apply_single_mat_effect(datum/material/material, amount, multiplier)
SHOULD_CALL_PARENT(TRUE)
+ // Derived and not optional, so this needs to be on base and not in the property code itself
var/beauty_modifier = material.get_property(MATERIAL_BEAUTY)
if(beauty_modifier)
AddElement(/datum/element/beauty, beauty_modifier * amount)
if(beauty_modifier >= 0.15 && HAS_TRAIT(src, TRAIT_FISHING_BAIT))
AddElement(/datum/element/shiny_bait)
- if(!(material_flags & MATERIAL_AFFECT_STATISTICS) || !uses_integrity)
- return
-
- var/base_modifier = material.get_property(MATERIAL_INTEGRITY)
- var/integrity_mod = GET_MATERIAL_MODIFIER(base_modifier, multiplier)
- modify_max_integrity(ceil(max_integrity * integrity_mod))
- var/list/armor_mods = material.get_armor_modifiers(multiplier)
- set_armor(get_armor().generate_new_with_multipliers(armor_mods))
+ if((material_flags & MATERIAL_AFFECT_STATISTICS) && uses_integrity)
+ change_material_integrity(material, amount, multiplier)
///A proc for material effects that only the main material (which the atom's primarly composed of) should apply.
/atom/proc/apply_main_material_effects(datum/material/main_material, amount, multiplier)
@@ -249,22 +369,44 @@
var/list/colors = list()
var/datum/material/main_material = get_master_material()
var/mat_length = length(materials)
- var/main_mat_amount
- var/main_mat_mult
+ var/main_mat_amount = materials[main_material][MATERIAL_LIST_OPTIMAL_AMOUNT]
+ var/main_mat_mult = materials[main_material][MATERIAL_LIST_MULTIPLIER]
+ var/do_main_material = TRUE
for(var/datum/material/custom_material as anything in materials)
var/list/deets = materials[custom_material]
var/mat_amount = deets[MATERIAL_LIST_OPTIMAL_AMOUNT]
var/multiplier = deets[MATERIAL_LIST_MULTIPLIER]
- if(custom_material == main_material)
- main_mat_amount = mat_amount
- main_mat_mult = multiplier
- remove_single_mat_effect(custom_material, mat_amount, multiplier)
- custom_material.on_removed(src, mat_amount, multiplier)
+ var/do_effects = TRUE
+ var/from_slot = FALSE
+ if(!isnull(material_slots) && length(deets[MATERIAL_LIST_SLOTS]))
+ from_slot = TRUE
+ var/slot_sum = 0
+ // A material is in multiple slots, we need to cut it up between them
+ if(length(deets[MATERIAL_LIST_SLOTS]) > 1)
+ for(var/slot_type in deets[MATERIAL_LIST_SLOTS])
+ var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
+ slot_sum += slot.material_amount
+
+ for(var/slot_type in deets[MATERIAL_LIST_SLOTS])
+ var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
+ var/slot_amt = mat_amount
+ if (slot_sum > 0)
+ slot_amt *= slot.material_amount / slot_sum
+ do_effects &= slot.on_removed(src, custom_material, mat_amount, multiplier)
+
+ if(!do_effects && custom_material == main_material)
+ do_main_material = FALSE
+
+ if(do_effects)
+ remove_single_mat_effect(custom_material, mat_amount, multiplier)
+ custom_material.on_removed(src, mat_amount, multiplier, from_slot = from_slot)
+
if(material_flags & MATERIAL_COLOR)
gather_material_color(custom_material, colors, mat_amount, multicolor = mat_length > 1)
- remove_main_material_effects(main_material, main_mat_amount, main_mat_mult)
+ if(do_main_material)
+ remove_main_material_effects(main_material, main_mat_amount, main_mat_mult)
if(material_flags & (MATERIAL_GREYSCALE|MATERIAL_COLOR))
if(material_flags & MATERIAL_COLOR)
@@ -297,17 +439,8 @@
if(beauty_modifier >= 0.15 && HAS_TRAIT(src, TRAIT_FISHING_BAIT))
RemoveElement(/datum/element/shiny_bait)
- if(!(material_flags & MATERIAL_AFFECT_STATISTICS) || !uses_integrity)
- return
-
- var/base_modifier = material.get_property(MATERIAL_INTEGRITY)
- var/integrity_mod = GET_MATERIAL_MODIFIER(base_modifier, multiplier)
- modify_max_integrity(floor(max_integrity / integrity_mod))
- var/list/armor_mods = material.get_armor_modifiers(multiplier)
- for (var/armor_type, value in armor_mods)
- if (value != 0) // Needs to be restored to initial values in finalize effects, sorry
- armor_mods[armor_type] = 1 / value
- set_armor(get_armor().generate_new_with_multipliers(armor_mods))
+ if((material_flags & MATERIAL_AFFECT_STATISTICS) && uses_integrity)
+ change_material_integrity(material, amount, multiplier, removing = TRUE)
///A proc to remove the material effects previously applied by the (ex-)main material
/atom/proc/remove_main_material_effects(datum/material/main_material, amount, multipier)
@@ -333,6 +466,42 @@
apply_material_effects()
material_flags = new_flags
+/// Applies changes to integrity and armor from a material
+/atom/proc/change_material_integrity(datum/material/material, amount, multiplier, removing = FALSE)
+ var/base_modifier = material.get_property(MATERIAL_INTEGRITY)
+ var/integrity_mod = GET_MATERIAL_MODIFIER(base_modifier, multiplier)
+ var/integrity_change = removing ? floor(max_integrity / integrity_mod) : ceil(max_integrity * integrity_mod)
+ modify_max_integrity(integrity_change)
+ var/list/armor_mods = material.get_armor_modifiers(multiplier)
+ // Invert if we're removing our material
+ if (removing)
+ for (var/armor_type, value in armor_mods)
+ if (value != 0) // Needs to be restored to initial values in finalize effects, sorry
+ armor_mods[armor_type] = 1 / value
+ set_armor(get_armor().generate_new_with_multipliers(armor_mods))
+
+/// Tries to fetch a material matching a specific slot
+/atom/proc/get_material_from_slot(slot_type)
+ var/mat_type = material_slots?[slot_type]
+ if (mat_type)
+ return SSmaterials.get_material(mat_type)
+
+/// Fetches a copy of all material slots.
+/atom/proc/get_material_slots()
+ return material_slots?.Copy()
+
+/// Returns TRUE if this atom utilizes material slots
+/atom/proc/has_material_slots()
+ return !!length(material_slots)
+
+/// Lists all slots in which a material is present
+/atom/proc/get_slots_of_material(datum/material/material)
+ . = list()
+ for (var/slot_type, material_id in material_slots)
+ if (material_id == istype(material) ? material.id : material)
+ . += slot_type
+ return .
+
/**
* Returns the material composition of the atom.
*
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 874575bb765f..2aff5d97deec 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -109,6 +109,12 @@
/// The pitch adjustment that this movable uses when speaking.
var/pitch = 0
+ /// The base set of blips to use for blip calculation.
+ var/blip_base = "male"
+
+ /// The blip variant to use for blip calculation.
+ var/blip_number = "1"
+
/// Datum that keeps all data related to zero-g drifting and handles related code/comsigs
var/datum/drift_handler/drift_handler
@@ -1734,6 +1740,8 @@
VV_DROPDOWN_OPTION(VV_HK_EDIT_PARTICLES, "Edit Particles")
VV_DROPDOWN_OPTION(VV_HK_DEADCHAT_PLAYS, "Start/Stop Deadchat Plays")
VV_DROPDOWN_OPTION(VV_HK_ADD_FANTASY_AFFIX, "Add Fantasy Affix")
+ if(SStts.tts_enabled)
+ VV_DROPDOWN_OPTION(VV_HK_SET_TTS_VOICE, "Modify TTS Voice")
/atom/movable/vv_do_topic(list/href_list)
. = ..()
@@ -1790,6 +1798,14 @@
log_admin("[key_name(usr)] has added deadchat control to [src]")
message_admins(span_notice("[key_name(usr)] has added deadchat control to [src]"))
+ if(href_list[VV_HK_SET_TTS_VOICE])
+ var/chosen_voice = tgui_input_list(usr, "Choose a voice to use.", "Choose a voice.", SStts.available_speakers)
+ if(!chosen_voice)
+ return
+ voice = chosen_voice
+ log_admin("[key_name(usr)] has set [src]'s voice as [chosen_voice].")
+ message_admins(span_notice("[key_name(usr)] has set [src]'s voice as [chosen_voice]."))
+
/**
* A wrapper for setDir that should only be able to fail by living mobs.
*
diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm
index 6e4968b1ac82..95550a655a95 100644
--- a/code/game/machinery/_machinery.dm
+++ b/code/game/machinery/_machinery.dm
@@ -102,6 +102,7 @@
blocks_emissive = EMISSIVE_BLOCK_GENERIC
initial_language_holder = /datum/language_holder/speaking_machine
armor_type = /datum/armor/obj_machinery
+ voice_filter = "alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000"
///see code/__DEFINES/stat.dm
var/machine_stat = NONE
diff --git a/code/game/machinery/announcement_system.dm b/code/game/machinery/announcement_system.dm
index a66cd3209a21..ad93a48c8dbf 100644
--- a/code/game/machinery/announcement_system.dm
+++ b/code/game/machinery/announcement_system.dm
@@ -203,16 +203,25 @@ GLOBAL_LIST_EMPTY(announcement_systems)
/// Sends a message to the appropriate channels.
/obj/machinery/announcement_system/proc/broadcast(message, list/channels, command_span = FALSE)
use_energy(active_power_usage)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ var/list/tts_message_mods = SStts.prepare_radio_announcement(src, message)
+ // MASSMETA EDIT END (ntts && /tg/tts)
if(!LAZYLEN(channels))
- radio.talk_into(src, message, null, command_span ? list(speech_span, SPAN_COMMAND) : null)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ radio.talk_into(src, message, null, command_span ? list(speech_span, SPAN_COMMAND) : null, null, tts_message_mods)
+ // MASSMETA EDIT END (ntts && /tg/tts)
return
// For some reasons, radio can't recognize RADIO_CHANNEL_COMMON in channels, so we need to handle it separately.
if (RADIO_CHANNEL_COMMON in channels)
- radio.talk_into(src, message, null, command_span ? list(speech_span, SPAN_COMMAND) : null)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ radio.talk_into(src, message, null, command_span ? list(speech_span, SPAN_COMMAND) : null, null, tts_message_mods)
+ // MASSMETA EDIT END (ntts && /tg/tts)
channels -= RADIO_CHANNEL_COMMON
for(var/channel in channels)
- radio.talk_into(src, message, channel, command_span ? list(speech_span, SPAN_COMMAND) : null)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ radio.talk_into(src, message, channel, command_span ? list(speech_span, SPAN_COMMAND) : null, null, tts_message_mods)
+ // MASSMETA EDIT END (ntts && /tg/tts)
/// Announces configs entry message with the provided variables. Channels, announcement_line and command_span are optional.
/obj/machinery/announcement_system/proc/announce(aas_config_entry_type, list/variables_map, list/channels, announcement_line, command_span)
diff --git a/code/game/machinery/autolathe.dm b/code/game/machinery/autolathe.dm
index c28f68173303..fff9be1ef6b7 100644
--- a/code/game/machinery/autolathe.dm
+++ b/code/game/machinery/autolathe.dm
@@ -159,9 +159,15 @@
customMaterials = FALSE
continue
- var/datum/material_requirement/requirement = SSmaterials.requirements[mat]
- if (!requirement)
- stack_trace("Design [design] has an invalid material requirement [requirement]")
+ var/datum/material_requirement/requirement = null
+ if (ispath(mat, /datum/material_slot))
+ var/datum/material_slot/slot = SSmaterials.material_slots[mat]
+ requirement = SSmaterials.requirements[slot.requirement_type]
+ else
+ requirement = SSmaterials.requirements[mat]
+
+ if (!istype(requirement))
+ stack_trace("Design [design] has an invalid material requirement: [mat]")
continue
cost[requirement.get_description()] = design_cost
@@ -257,14 +263,20 @@
// Check for materials required. For custom material items decode their required materials
var/list/materials_needed = list()
+ var/list/slots_chosen = null
var/mat_choice = FALSE
for(var/material, amount_needed in design.materials)
- if(!ispath(material, /datum/material_requirement)) // Material requirement
+ if(!ispath(material, /datum/material_requirement) && !ispath(material, /datum/material_slot)) // Material requirement
if(!istype(material, /datum/material))
CRASH("Autolathe ui_act got passed an invalid material id: [material]")
materials_needed[material] += amount_needed
continue
+ var/datum/material_slot/slot = null
+ if (ispath(material, /datum/material_slot))
+ slot = SSmaterials.material_slots[material]
+ material = slot.requirement_type
+
var/list/choices = list()
for(var/datum/material/valid_candidate as anything in SSmaterials.get_materials_by_req(material))
if(materials.get_material_amount(valid_candidate) >= (amount_needed + materials_needed[valid_candidate]))
@@ -276,7 +288,7 @@
var/chosen = tgui_input_list(
ui.user,
- "Select the material to use",
+ "Select the material to use[slot ? " for [LOWER_TEXT(slot.name)]" : ""]",
"Material Selection",
sort_list(choices),
)
@@ -284,6 +296,9 @@
return // user cancelled
material = choices[chosen]
+ if (slot)
+ var/datum/material/proper_mat = material
+ LAZYSET(slots_chosen, slot.type, proper_mat.id)
if(isnull(material))
CRASH("A player chose an invalid custom material in autolathe ui_act: [material]")
@@ -323,7 +338,7 @@
if(!istype(material, /datum/material/glass) && !istype(material, /datum/material/iron))
ui.user.client.give_award(/datum/award/achievement/misc/getting_an_upgrade, ui.user)
break
- addtimer(CALLBACK(src, PROC_REF(do_make_item), design, build_count, build_time_per_item, material_cost_coefficient, charge_per_item, materials_needed, target_location), build_time_per_item)
+ addtimer(CALLBACK(src, PROC_REF(do_make_item), design, build_count, build_time_per_item, material_cost_coefficient, charge_per_item, materials_needed, target_location, slots_chosen), build_time_per_item)
return TRUE
@@ -339,7 +354,7 @@
* * list/materials_needed - the list of materials to print 1 item
* * turf/target - the location to drop the printed item on
*/
-/obj/machinery/autolathe/proc/do_make_item(datum/design/design, items_remaining, build_time_per_item, material_cost_coefficient, charge_per_item, list/materials_needed, turf/target)
+/obj/machinery/autolathe/proc/do_make_item(datum/design/design, items_remaining, build_time_per_item, material_cost_coefficient, charge_per_item, list/materials_needed, turf/target, list/slots_chosen)
PROTECTED_PROC(TRUE)
if(items_remaining <= 0) // how
@@ -386,6 +401,8 @@
created = design.create_result(target, materials_needed, amount = number_to_make)
else
created = design.create_result(target, materials_needed)
+ if (length(slots_chosen))
+ created.set_material_slots(slots_chosen)
split_materials_uniformly(materials_needed, material_cost_coefficient, created)
if(isitem(created))
@@ -401,7 +418,7 @@
if(items_remaining <= 0)
finalize_build()
return
- addtimer(CALLBACK(src, PROC_REF(do_make_item), design, items_remaining, build_time_per_item, material_cost_coefficient, charge_per_item, materials_needed, target), build_time_per_item)
+ addtimer(CALLBACK(src, PROC_REF(do_make_item), design, items_remaining, build_time_per_item, material_cost_coefficient, charge_per_item, materials_needed, target, slots_chosen), build_time_per_item)
/**
* Resets the icon state and busy flag
diff --git a/code/game/machinery/big_manipulator/big_manipulator_interactions.dm b/code/game/machinery/big_manipulator/big_manipulator_interactions.dm
index 0d62b6b3ff43..900f659c5ac5 100644
--- a/code/game/machinery/big_manipulator/big_manipulator_interactions.dm
+++ b/code/game/machinery/big_manipulator/big_manipulator_interactions.dm
@@ -220,7 +220,7 @@
return TRUE
// Breaking the angle up into 45 degree steps
- var/rotation_step = 45 * SIGN(angle_diff)
+ var/rotation_step = 45 * sign(angle_diff)
do_step_rotation(target_point, callback, current_angle, target_angle, rotation_step, 0, total_rotation_time)
return TRUE
diff --git a/code/game/machinery/civilian_bounties.dm b/code/game/machinery/civilian_bounties.dm
deleted file mode 100644
index f90b1b579b1b..000000000000
--- a/code/game/machinery/civilian_bounties.dm
+++ /dev/null
@@ -1,526 +0,0 @@
-///Percentage of a civilian bounty the civilian will make.
-#define CIV_BOUNTY_SPLIT 30
-
-///Pad for the Civilian Bounty Control.
-/obj/machinery/piratepad/civilian
- name = "civilian bounty pad"
- desc = "A machine designed to send civilian bounty targets to centcom."
- layer = TABLE_LAYER
- resistance_flags = FIRE_PROOF
- circuit = /obj/item/circuitboard/machine/bountypad
- var/cooldown_reduction = 0
-
-/obj/machinery/piratepad/civilian/screwdriver_act(mob/living/user, obj/item/tool)
- . = ..()
- if(!.)
- return default_deconstruction_screwdriver(user, "lpad-idle-open", "lpad-idle-off", tool)
-
-/obj/machinery/piratepad/civilian/crowbar_act(mob/living/user, obj/item/tool)
- . = ..()
- if(!.)
- return default_deconstruction_crowbar(tool)
-
-/obj/machinery/piratepad/civilian/RefreshParts()
- . = ..()
- var/T = -2
- for(var/datum/stock_part/micro_laser/micro_laser in component_parts)
- T += micro_laser.tier
-
- for(var/datum/stock_part/scanning_module/scanning_module in component_parts)
- T += scanning_module.tier
-
- cooldown_reduction = T * (30 SECONDS)
-
-/obj/machinery/piratepad/civilian/proc/get_cooldown_reduction()
- return cooldown_reduction
-
-///Computer for assigning new civilian bounties, and sending bounties for collection.
-/obj/machinery/computer/piratepad_control/civilian
- name = "civilian bounty control terminal"
- desc = "A console for assigning civilian bounties to inserted ID cards, and for controlling the bounty pad for export."
- status_report = "Ready for delivery."
- icon_screen = "civ_bounty"
- icon_keyboard = "id_key"
- warmup_time = 3 SECONDS
- circuit = /obj/item/circuitboard/computer/bountypad
- interface_type = "CivCargoHoldTerminal"
- ///Typecast of an inserted, scanned ID card inside the console, as bounties are held within the ID card.
- var/obj/item/card/id/inserted_scan_id
-
-/obj/machinery/computer/piratepad_control/civilian/attackby(obj/item/I, mob/living/user, list/modifiers, list/attack_modifiers)
- if(isidcard(I))
- if(id_insert(user, I, inserted_scan_id))
- inserted_scan_id = I
- return TRUE
- return ..()
-
-/obj/machinery/computer/piratepad_control/multitool_act(mob/living/user, obj/item/multitool/I)
- if(istype(I) && istype(I.buffer,/obj/machinery/piratepad/civilian))
- to_chat(user, span_notice("You link [src] with [I.buffer] in [I] buffer."))
- pad_ref = WEAKREF(I.buffer)
- return TRUE
-
-/obj/machinery/computer/piratepad_control/civilian/post_machine_initialize()
- . = ..()
- if(cargo_hold_id)
- for(var/obj/machinery/piratepad/civilian/C as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/piratepad/civilian))
- if(C.cargo_hold_id == cargo_hold_id)
- pad_ref = WEAKREF(C)
- return
- else
- var/obj/machinery/piratepad/civilian/pad = locate() in range(4,src)
- pad_ref = WEAKREF(pad)
-
-/obj/machinery/computer/piratepad_control/civilian/recalc()
- if(sending)
- return FALSE
- if(!inserted_scan_id)
- status_report = "Please insert your ID first."
- playsound(loc, 'sound/machines/synth/synth_no.ogg', 30 , TRUE)
- return FALSE
- if(!inserted_scan_id.registered_account.civilian_bounty)
- status_report = "Please accept a new civilian bounty first."
- playsound(loc, 'sound/machines/synth/synth_no.ogg', 30 , TRUE)
- return FALSE
- status_report = "Civilian Bounty: "
- var/obj/machinery/piratepad/civilian/pad = pad_ref?.resolve()
- for(var/atom/movable/possible_shippable in get_turf(pad))
- if(possible_shippable == pad)
- continue
- if(possible_shippable.flags_1 & HOLOGRAM_1)
- continue
- if(isitem(possible_shippable))
- var/obj/item/possible_shippable_item = possible_shippable
- if(possible_shippable_item.item_flags & ABSTRACT)
- continue
- if(inserted_scan_id.registered_account.civilian_bounty.applies_to(possible_shippable))
- status_report += "Target Applicable."
- playsound(loc, 'sound/machines/synth/synth_yes.ogg', 30 , TRUE)
- return
- status_report += "Not Applicable."
- playsound(loc, 'sound/machines/synth/synth_no.ogg', 30 , TRUE)
-
-/**
- * This fully rewrites base behavior in order to only check for bounty objects, and no other types of objects like pirate-pads do.
- */
-/obj/machinery/computer/piratepad_control/civilian/send()
- playsound(loc, 'sound/machines/wewewew.ogg', 70, TRUE)
- if(!sending)
- return
- var/datum/bank_account/id_account = inserted_scan_id?.registered_account
- var/datum/bounty/current_bounty = id_account?.civilian_bounty
- if(!current_bounty)
- stop_sending()
- return FALSE
- var/active_stack = 0
- var/obj/machinery/piratepad/civilian/pad = pad_ref?.resolve()
- for(var/atom/movable/possible_shippable in get_turf(pad))
- if(possible_shippable == pad)
- continue
- if(possible_shippable.flags_1 & HOLOGRAM_1)
- continue
- if(isitem(possible_shippable))
- var/obj/item/possible_shippable_item = possible_shippable
- if(possible_shippable_item.item_flags & ABSTRACT)
- continue
- if(current_bounty.applies_to(possible_shippable))
- active_stack ++
- current_bounty.ship(possible_shippable)
- qdel(possible_shippable)
- if(active_stack >= 1)
- status_report += "Bounty Target Found x[active_stack]. "
- else
- status_report = "No applicable targets found. Aborting."
- stop_sending()
- if(current_bounty.can_claim())
- //Pay for the bounty with the ID's department funds.
- status_report += " Bounty completed! Please give your bounty cube to cargo for your automated payout shortly."
- SSblackbox.record_feedback("tally", "bounties_completed", 1, current_bounty.type)
- current_bounty.on_claimed(inserted_scan_id)
- id_account.reset_bounty(inserted_scan_id)
- SSeconomy.civ_bounty_tracker++
-
- var/obj/item/bounty_cube/reward = new /obj/item/bounty_cube(drop_location())
- reward.set_up(current_bounty, inserted_scan_id)
-
- pad.visible_message(span_notice("[pad] activates!"))
- flick(pad.sending_state,pad)
- pad.icon_state = pad.idle_state
- playsound(loc, 'sound/machines/synth/synth_yes.ogg', 30 , TRUE)
- sending = FALSE
-
-///Here is where cargo bounties are added to the player's bank accounts, then adjusted and scaled into a civilian bounty.
-/obj/machinery/computer/piratepad_control/civilian/proc/add_bounties(mob/user, cooldown_reduction = 0)
- var/datum/bank_account/id_account = inserted_scan_id?.registered_account
- if(!id_account)
- return
- if((id_account.civilian_bounty || id_account.bounties) && !COOLDOWN_FINISHED(id_account, bounty_timer))
- var/time_left = DisplayTimeText(COOLDOWN_TIMELEFT(id_account, bounty_timer), round_seconds_to = 1)
- balloon_alert(user, "try again in [time_left]!")
- return FALSE
- if(!inserted_scan_id.trim)
- say("Requesting ID card has no job assignment registered!")
- return FALSE
-
- var/list/datum/bounty/crumbs = inserted_scan_id.trim.generate_bounty_list()
- COOLDOWN_START(id_account, bounty_timer, (5 MINUTES) - cooldown_reduction)
- id_account.bounties = crumbs
-
-/**
- * Generates a list of bounties for use with the civilian bounty pad.
- *
- * @param bounty_types the define taken from a job for selection of a random_bounty() proc.
- * @param bounty_rolls the number of bounties to be selected from.
- * @param assistant_failsafe Do we guarentee one assistant bounty per generated list? Used for non-assistant jobs to give an easier alternative to that job's default bounties.
- */
-/datum/id_trim/proc/generate_bounty_list(bounty_rolls = 3, assistant_failsafe = TRUE)
- var/datum/job/our_job = find_job()
- var/bounty_type = our_job?.bounty_types || CIV_JOB_RANDOM
-
- var/list/rolling_list = list()
- if(assistant_failsafe)
- var/random_assistant = get_random_bounty_type(CIV_JOB_BASIC)
- var/datum/bounty/assistant_bounty = new random_assistant()
- if(assistant_bounty.can_get())
- rolling_list += assistant_bounty
- else
- qdel(assistant_bounty)
-
- var/attempts = 20
- while(length(rolling_list) < bounty_rolls && attempts > 0)
- var/random_job = get_random_bounty_type(attempts <= 5 ? CIV_JOB_BASIC : bounty_type)
- var/datum/bounty/job_bounty = new random_job()
- attempts -= 1
- if(!job_bounty.can_get() || has_duplicate_bounty(rolling_list, job_bounty))
- qdel(job_bounty)
- continue
-
- rolling_list += job_bounty
-
- return rolling_list
-
-/// Helper to see if there's a duplicate bounty in a list of bounties
-/datum/id_trim/proc/has_duplicate_bounty(list/datum/bounty/bounty_list, datum/bounty/check_bounty)
- PRIVATE_PROC(TRUE)
-
- for(var/datum/bounty/existing as anything in bounty_list)
- if(existing.type != check_bounty.type)
- continue
- if(existing.allow_duplicate && check_bounty.allow_duplicate)
- continue
- return TRUE
-
- return FALSE
-
-/// Returns a /datum/bounty typepath for a given bounty type
-/datum/id_trim/proc/get_random_bounty_type(input_bounty_type)
- if(!input_bounty_type || input_bounty_type == CIV_JOB_RANDOM)
- input_bounty_type = rand(1, MAXIMUM_BOUNTY_JOBS)
-
- switch(input_bounty_type)
- if(CIV_JOB_BASIC)
- return pick(subtypesof(/datum/bounty/item/assistant))
- if(CIV_JOB_ROBO)
- return pick(subtypesof(/datum/bounty/item/mech))
- if(CIV_JOB_CHEF)
- return pick(subtypesof(/datum/bounty/item/chef) + subtypesof(/datum/bounty/reagent/chef))
- if(CIV_JOB_SEC)
- if(prob(75))
- return /datum/bounty/patrol
- return /datum/bounty/item/contraband
- if(CIV_JOB_DRINK)
- if(prob(50))
- return /datum/bounty/reagent/simple_drink
- return /datum/bounty/reagent/complex_drink
- if(CIV_JOB_CHEM)
- if(prob(50))
- return /datum/bounty/reagent/chemical_simple
- return/datum/bounty/reagent/chemical_complex
- if(CIV_JOB_VIRO)
- return pick(subtypesof(/datum/bounty/virus))
- if(CIV_JOB_SCI)
- if(prob(50))
- return pick(subtypesof(/datum/bounty/item/science))
- return pick(subtypesof(/datum/bounty/item/slime))
- if(CIV_JOB_ENG)
- return pick(subtypesof(/datum/bounty/item/engineering))
- if(CIV_JOB_MINE)
- return pick(subtypesof(/datum/bounty/item/mining))
- if(CIV_JOB_MED)
- return pick(subtypesof(/datum/bounty/item/medical))
- if(CIV_JOB_GROW)
- return pick(subtypesof(/datum/bounty/item/botany))
- if(CIV_JOB_ATMOS)
- return pick(subtypesof(/datum/bounty/item/atmospherics))
- if(CIV_JOB_BITRUN)
- return pick(subtypesof(/datum/bounty/item/bitrunning))
-
- stack_trace("Failed to get random bounty type for input type [input_bounty_type]")
- return null
-
-/**
- * Proc that assigned a civilian bounty to an ID card, from the list of potential bounties that that bank account currently has available.
- * Available choices are assigned during add_bounties, and one is locked in here.
- *
- * @param choice The index of the bounty in the list of bounties that the player can choose from.
- */
-/obj/machinery/computer/piratepad_control/civilian/proc/pick_bounty(datum/bounty/choice)
- var/datum/bank_account/id_account = inserted_scan_id?.registered_account
- if(!id_account?.bounties?[choice])
- playsound(loc, 'sound/machines/synth/synth_no.ogg', 40 , TRUE)
- return
- id_account.set_bounty(id_account.bounties[choice], inserted_scan_id)
- id_account.bounties = null
- SSblackbox.record_feedback("tally", "bounties_assigned", 1, id_account.civilian_bounty.type)
- return id_account.civilian_bounty
-
-/obj/machinery/computer/piratepad_control/civilian/click_alt(mob/user)
- id_eject(user, inserted_scan_id)
- return CLICK_ACTION_SUCCESS
-
-/obj/machinery/computer/piratepad_control/civilian/ui_data(mob/user)
- var/list/data = list()
- data["points"] = points
- data["pad"] = pad_ref?.resolve() ? TRUE : FALSE
- data["sending"] = sending
- data["status_report"] = status_report
- data["id_inserted"] = inserted_scan_id
- if(inserted_scan_id?.registered_account)
- if(inserted_scan_id.registered_account.civilian_bounty)
- data["id_bounty_info"] = inserted_scan_id.registered_account.civilian_bounty.description
- data["id_bounty_num"] = inserted_scan_id.registered_account.bounty_num()
- data["id_bounty_value"] = (inserted_scan_id.registered_account.civilian_bounty.get_bounty_reward()) * (CIV_BOUNTY_SPLIT / 100)
- if(inserted_scan_id.registered_account.bounties)
- data["picking"] = TRUE
- data["id_bounty_names"] = list()
- data["id_bounty_infos"] = list()
- data["id_bounty_values"] = list()
- for(var/datum/bounty/bounty as anything in inserted_scan_id.registered_account.bounties)
- data["id_bounty_names"] += bounty.name
- data["id_bounty_infos"] += bounty.description
- data["id_bounty_values"] += bounty.get_bounty_reward() * (CIV_BOUNTY_SPLIT / 100)
-
- else
- data["picking"] = FALSE
-
- return data
-
-/obj/machinery/computer/piratepad_control/civilian/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
- . = ..()
- if(.)
- return
- var/obj/machinery/piratepad/civilian/pad = pad_ref?.resolve()
- if(!pad)
- return
- var/mob/user = ui.user
- if(!user.can_perform_action(src) || (machine_stat & (NOPOWER|BROKEN)))
- return
- switch(action)
- if("recalc")
- recalc()
- if("send")
- start_sending()
- if("stop")
- stop_sending()
- if("pick")
- pick_bounty(params["value"])
- if("bounty")
- add_bounties(user, pad.get_cooldown_reduction())
- if("eject")
- id_eject(user, inserted_scan_id)
- inserted_scan_id = null
- . = TRUE
-
-///Self explanitory, holds the ID card in the console for bounty payout and manipulation.
-/obj/machinery/computer/piratepad_control/civilian/proc/id_insert(mob/user, obj/item/inserting_item, obj/item/target)
- var/obj/item/card/id/card_to_insert = inserting_item
- var/holder_item = FALSE
-
- if(!isidcard(card_to_insert))
- card_to_insert = inserting_item.remove_id()
- holder_item = TRUE
-
- if(!card_to_insert || !user.transferItemToLoc(card_to_insert, src))
- return FALSE
-
- if(target)
- if(holder_item && inserting_item.insert_id(target))
- playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 50, FALSE)
- else
- id_eject(user, target)
-
- user.visible_message(span_notice("[user] inserts \the [card_to_insert] into \the [src]."),
- span_notice("You insert \the [card_to_insert] into \the [src]."))
- playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 50, FALSE)
- ui_interact(user)
- return TRUE
-
-///Removes A stored ID card.
-/obj/machinery/computer/piratepad_control/civilian/proc/id_eject(mob/user, obj/item/target)
- if(!target)
- to_chat(user, span_warning("That slot is empty!"))
- return FALSE
- else
- try_put_in_hand(target, user)
- user.visible_message(span_notice("[user] gets \the [target] from \the [src]."), \
- span_notice("You get \the [target] from \the [src]."))
- playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 50, FALSE)
- inserted_scan_id = null
- return TRUE
-
-///Upon completion of a civilian bounty, one of these is created. It is sold to cargo to give the cargo budget bounty money, and the person who completed it cash.
-/obj/item/bounty_cube
- name = "bounty cube"
- desc = "A bundle of compressed hardlight data, containing a completed bounty. Sell this on the cargo shuttle to claim it!"
- icon = 'icons/obj/economy.dmi'
- icon_state = "bounty_cube"
- ///Value of the bounty that this bounty cube sells for.
- var/bounty_value = 0
- ///Multiplier for the bounty payout received by the Supply budget if the cube is sent without having to nag.
- var/speed_bonus = 0.2
- ///Multiplier for the bounty payout received by the person who completed the bounty.
- var/holder_cut = 0.3
- ///Multiplier for the bounty payout received by the person who claims the handling tip.
- var/handler_tip = 0.1
- ///Time between nags.
- var/nag_cooldown = 5 MINUTES
- ///How much the time between nags extends each nag.
- var/nag_cooldown_multiplier = 1.25
- ///Next world tick to nag Supply listeners.
- var/next_nag_time
- ///Who completed the bounty.
- var/bounty_holder
- ///What job the bounty holder had.
- var/bounty_holder_job
- ///What the bounty was for.
- var/bounty_name
- ///Bank account of the person who completed the bounty.
- var/datum/bank_account/bounty_holder_account
- ///Bank account of the person who receives the handling tip.
- var/datum/bank_account/bounty_handler_account
-
-/obj/item/bounty_cube/Initialize(mapload)
- . = ..()
- ADD_TRAIT(src, TRAIT_NO_BARCODES, INNATE_TRAIT) // Don't allow anyone to override our pricetag component with a barcode
-
-/obj/item/bounty_cube/examine()
- . = ..()
- if(speed_bonus)
- . += span_notice("[time2text(next_nag_time - world.time,"mm:ss", NO_TIMEZONE)] remains until [bounty_value * speed_bonus] [MONEY_NAME_SINGULAR] speedy delivery bonus lost.")
- if(handler_tip && !bounty_handler_account)
- . += span_notice("Scan this in the cargo shuttle with an export scanner to register your bank account for the [bounty_value * handler_tip] [MONEY_NAME_SINGULAR] handling tip.")
-
-/obj/item/bounty_cube/process(seconds_per_tick)
- //if our nag cooldown has finished and we aren't on Centcom or in transit, then nag
- if(COOLDOWN_FINISHED(src, next_nag_time) && !is_centcom_level(z) && !is_reserved_level(z))
- //set up our fallback message, in case of AAS being broken it will be sent to card holders
- var/nag_message = "[src] is unsent in [get_area(src)]."
-
- //nag on Supply channel and reduce the speed bonus multiplier to nothing
- var/obj/machinery/announcement_system/aas = get_announcement_system(/datum/aas_config_entry/bounty_cube_unsent, src, list(RADIO_CHANNEL_SUPPLY))
- if (aas)
- nag_message = aas.compile_config_message(/datum/aas_config_entry/bounty_cube_unsent, list("LOCATION" = get_area_name(src), "COST" = bounty_value), "Regular Message")
- if (speed_bonus)
- aas.announce(/datum/aas_config_entry/bounty_cube_unsent, list("LOCATION" = get_area_name(src), "COST" = bounty_value, "BONUSLOST" = bounty_value * speed_bonus), list(RADIO_CHANNEL_SUPPLY), "When Bonus Lost")
- else
- aas.broadcast("[nag_message]", list(RADIO_CHANNEL_SUPPLY))
- speed_bonus = 0
-
- //alert the holder
- bounty_holder_account.bank_card_talk("[nag_message]")
-
- //if someone has registered for the handling tip, nag them
- bounty_handler_account?.bank_card_talk(nag_message)
-
- //increase our cooldown length and start it again
- nag_cooldown = nag_cooldown * nag_cooldown_multiplier
- COOLDOWN_START(src, next_nag_time, nag_cooldown)
-
-/obj/item/bounty_cube/proc/set_up(datum/bounty/my_bounty, obj/item/card/id/holder_id)
- bounty_value = my_bounty.get_bounty_reward()
- bounty_name = my_bounty.name
- bounty_holder = holder_id.registered_name
- bounty_holder_job = holder_id.assignment
- bounty_holder_account = holder_id.registered_account
- name = "\improper [bounty_value] [MONEY_SYMBOL] [name]"
- desc += " The sales tag indicates it was [bounty_holder] ([bounty_holder_job])'s reward for completing the [bounty_name] bounty."
- AddComponent(/datum/component/pricetag, holder_id.registered_account, holder_cut, FALSE)
- AddComponent(/datum/component/gps, "[src]")
- START_PROCESSING(SSobj, src)
- COOLDOWN_START(src, next_nag_time, nag_cooldown)
- aas_config_announce(/datum/aas_config_entry/bounty_cube_created, list(
- "LOCATION" = get_area_name(src),
- "PERSON" = bounty_holder,
- "RANK" = bounty_holder_job,
- "BONUSTIME" = time2text(next_nag_time - world.time,"mm:ss", NO_TIMEZONE),
- "COST" = bounty_value
- ), src, list(RADIO_CHANNEL_SUPPLY))
-
-//for when you need a REAL bounty cube to test with and don't want to do a bounty each time your code changes
-/obj/item/bounty_cube/debug_cube
- name = "debug bounty cube"
- desc = "Use in-hand to set it up with a random bounty. Requires an ID it can detect with a bank account attached. \
- This will alert Supply over the radio with your name and location, and cargo techs will be dispatched with kill on sight clearance."
- var/set_up = FALSE
-
-/obj/item/bounty_cube/debug_cube/attack_self(mob/user)
- if(!isliving(user))
- to_chat(user, span_warning("You aren't eligible to use this!"))
- return ..()
-
- if(!set_up)
- var/mob/living/squeezer = user
- if(squeezer.get_bank_account())
- set_up(random_bounty(), squeezer.get_idcard())
- set_up = TRUE
- return ..()
- to_chat(user, span_notice("It can't detect your bank account."))
-
- return ..()
-
-///Beacon to launch a new bounty setup when activated.
-/obj/item/civ_bounty_beacon
- name = "civilian bounty beacon"
- desc = "N.T. approved civilian bounty beacon, toss it down and you will have a bounty pad and computer delivered to you."
- icon = 'icons/obj/machines/floor.dmi'
- icon_state = "floor_beacon"
- var/uses = 2
-
-/obj/item/civ_bounty_beacon/attack_self()
- loc.visible_message(span_warning("\The [src] begins to beep loudly!"))
- addtimer(CALLBACK(src, PROC_REF(launch_payload)), 1 SECONDS)
-
-/obj/item/civ_bounty_beacon/proc/launch_payload()
- playsound(src, SFX_SPARKS, 80, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
- switch(uses)
- if(2)
- new /obj/machinery/piratepad/civilian(drop_location())
- if(1)
- new /obj/machinery/computer/piratepad_control/civilian(drop_location())
- qdel(src)
- uses--
-
-/datum/aas_config_entry/bounty_cube_created
- name = "Cargo Alert: Bounty Cube Created"
- announcement_lines_map = list(
- "Message" = "A %COST cr bounty cube has been created in %LOCATION by %PERSON (%RANK). Speedy delivery bonus lost in %BONUSTIME.")
- vars_and_tooltips_map = list(
- "LOCATION" = "will be replaced with the location of the cube.",
- "PERSON" = "with who created the cube.",
- "RANK" = "with their job.",
- "BONUSTIME" = "with the time left for speedy delivery tip.",
- "COST" = "with the cost of the cube.",
- )
-
-/datum/aas_config_entry/bounty_cube_unsent
- name = "Cargo Alert: Bounty Cube Unsent"
- announcement_lines_map = list(
- "Regular Message" = "The %COST cr bounty cube is unsent in %LOCATION.",
- "When Bonus Lost" = "The %COST cr bounty cube is unsent in %LOCATION. Speedy delivery bonus of %BONUSLOST credits lost.")
- vars_and_tooltips_map = list(
- "LOCATION" = "will be replaced with the location of the cube.",
- "COST" = "with the cost of the cube.",
- "BONUSLOST" = "with the lost bonus tip, it will be sent just for When Bonus Lost message!",
- )
-
-#undef CIV_BOUNTY_SPLIT
diff --git a/code/game/machinery/civilian_bounty/bounty_cube.dm b/code/game/machinery/civilian_bounty/bounty_cube.dm
new file mode 100644
index 000000000000..1c73395a0433
--- /dev/null
+++ b/code/game/machinery/civilian_bounty/bounty_cube.dm
@@ -0,0 +1,144 @@
+
+/**
+ * Upon completion of a civilian bounty, one of these is created.
+ * It is sold to cargo to give the cargo budget bounty money, and the person who completed it cash.
+ */
+/obj/item/bounty_cube
+ name = "bounty cube"
+ desc = "A bundle of compressed hardlight data, containing a completed bounty. Sell this on the cargo shuttle to claim it!"
+ icon = 'icons/obj/economy.dmi'
+ icon_state = "bounty_cube"
+ ///Value of the bounty that this bounty cube sells for.
+ var/bounty_value = 0
+ ///Multiplier for the bounty payout received by the Supply budget if the cube is sent without having to nag.
+ var/speed_bonus = 0.2
+ ///Percentage of the bounty payout received by the people who completed the bounty. Split between multiple people in the event multiple people finished a global bounty.
+ var/holder_cut = BOUNTY_CUT_STANDARD
+ ///Multiplier for the bounty payout received by the person who claims the handling tip.
+ var/handler_tip = 0.1
+ ///Time between nags.
+ var/nag_cooldown = 5 MINUTES
+ ///How much the time between nags extends each nag.
+ var/nag_cooldown_multiplier = 1.25
+ ///Next world tick to nag Supply listeners.
+ var/next_nag_time
+ ///Who completed the bounty.
+ var/bounty_holder
+ ///What job the bounty holder had.
+ var/bounty_holder_job
+ ///What the bounty was for.
+ var/bounty_name
+ ///Bank account of the people who completed the bounty.
+ var/list/datum/bank_account/bounty_holder_accounts
+ ///Bank account of the person who receives the handling tip.
+ var/datum/bank_account/bounty_handler_account
+
+/obj/item/bounty_cube/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_NO_BARCODES, INNATE_TRAIT) // Don't allow anyone to override our pricetag component with a barcode
+
+/obj/item/bounty_cube/examine()
+ . = ..()
+ if(speed_bonus)
+ . += span_notice("[time2text(next_nag_time - world.time,"mm:ss", NO_TIMEZONE)] remains until [bounty_value * speed_bonus] [MONEY_NAME_SINGULAR] speedy delivery bonus lost.")
+ if(handler_tip && !bounty_handler_account)
+ . += span_notice("Scan this in the cargo shuttle with an export scanner to register your bank account for the [bounty_value * handler_tip] [MONEY_NAME_SINGULAR] handling tip.")
+
+/obj/item/bounty_cube/process(seconds_per_tick)
+ //if our nag cooldown has finished and we aren't on Centcom or in transit, then nag
+ if(COOLDOWN_FINISHED(src, next_nag_time) && !is_centcom_level(z) && !is_reserved_level(z))
+ //set up our fallback message, in case of AAS being broken it will be sent to card holders
+ var/nag_message = "[src] is unsent in [get_area(src)]."
+
+ //nag on Supply channel and reduce the speed bonus multiplier to nothing
+ var/obj/machinery/announcement_system/aas = get_announcement_system(/datum/aas_config_entry/bounty_cube_unsent, src, list(RADIO_CHANNEL_SUPPLY))
+ if (aas)
+ nag_message = aas.compile_config_message(/datum/aas_config_entry/bounty_cube_unsent, list("LOCATION" = get_area_name(src), "COST" = bounty_value), "Regular Message")
+ if (speed_bonus)
+ aas.announce(/datum/aas_config_entry/bounty_cube_unsent, list("LOCATION" = get_area_name(src), "COST" = bounty_value, "BONUSLOST" = bounty_value * speed_bonus), list(RADIO_CHANNEL_SUPPLY), "When Bonus Lost")
+ else
+ aas.broadcast("[nag_message]", list(RADIO_CHANNEL_SUPPLY))
+ speed_bonus = 0
+
+ //alert the holder
+ for(var/datum/bank_account/bounty_holder_account in bounty_holder_accounts)
+ bounty_holder_account.bank_card_talk("[nag_message]")
+
+ //if someone has registered for the handling tip, nag them
+ bounty_handler_account?.bank_card_talk(nag_message)
+
+ //increase our cooldown length and start it again
+ nag_cooldown = nag_cooldown * nag_cooldown_multiplier
+ COOLDOWN_START(src, next_nag_time, nag_cooldown)
+
+/**
+ * Configures the bounty cube's name, value, and annouces to the crew
+ */
+/obj/item/bounty_cube/proc/set_up(datum/bounty/my_bounty, obj/item/card/id/holder_id)
+ bounty_value = my_bounty.get_bounty_reward()
+ bounty_name = my_bounty.name
+ bounty_holder = holder_id.registered_name
+ bounty_holder_job = holder_id.assignment
+ bounty_holder_accounts = my_bounty.contribution
+
+ name = "\improper [bounty_value] [MONEY_SYMBOL] [name]"
+ desc += " The sales tag indicates it was [bounty_holder] ([bounty_holder_job])'s reward for completing the [bounty_name] bounty."
+ AddComponent(/datum/component/pricetag, bounty_holder_accounts.Copy(), holder_cut, FALSE)
+ AddComponent(/datum/component/gps, "[src]")
+
+ START_PROCESSING(SSobj, src)
+ COOLDOWN_START(src, next_nag_time, nag_cooldown)
+ aas_config_announce(/datum/aas_config_entry/bounty_cube_created, list(
+ "LOCATION" = get_area_name(src),
+ "PERSON" = bounty_holder,
+ "RANK" = bounty_holder_job,
+ "BONUSTIME" = time2text(next_nag_time - world.time,"mm:ss", NO_TIMEZONE),
+ "COST" = bounty_value
+ ), src, list(RADIO_CHANNEL_SUPPLY))
+
+//for when you need a REAL bounty cube to test with and don't want to do a bounty each time your code changes
+/obj/item/bounty_cube/debug_cube
+ name = "debug bounty cube"
+ desc = "Use in-hand to set it up with a random bounty. Requires an ID it can detect with a bank account attached. \
+ This will alert Supply over the radio with your name and location, and cargo techs will be dispatched with kill on sight clearance."
+ var/set_up = FALSE
+
+/obj/item/bounty_cube/debug_cube/attack_self(mob/user)
+ if(!isliving(user))
+ to_chat(user, span_warning("You aren't eligible to use this!"))
+ return ..()
+
+ if(!set_up)
+ var/mob/living/squeezer = user
+ if(squeezer.get_bank_account())
+ set_up(random_bounty(), squeezer.get_idcard())
+ set_up = TRUE
+ return ..()
+ to_chat(user, span_notice("It can't detect your bank account."))
+
+ return ..()
+
+// Bounty Cube AAS Config Entries
+
+/datum/aas_config_entry/bounty_cube_created
+ name = "Cargo Alert: Bounty Cube Created"
+ announcement_lines_map = list(
+ "Message" = "A %COST cr bounty cube has been created in %LOCATION by %PERSON (%RANK). Speedy delivery bonus lost in %BONUSTIME.")
+ vars_and_tooltips_map = list(
+ "LOCATION" = "will be replaced with the location of the cube.",
+ "PERSON" = "with who created the cube.",
+ "RANK" = "with their job.",
+ "BONUSTIME" = "with the time left for speedy delivery tip.",
+ "COST" = "with the cost of the cube.",
+ )
+
+/datum/aas_config_entry/bounty_cube_unsent
+ name = "Cargo Alert: Bounty Cube Unsent"
+ announcement_lines_map = list(
+ "Regular Message" = "The %COST cr bounty cube is unsent in %LOCATION.",
+ "When Bonus Lost" = "The %COST cr bounty cube is unsent in %LOCATION. Speedy delivery bonus of %BONUSLOST credits lost.")
+ vars_and_tooltips_map = list(
+ "LOCATION" = "will be replaced with the location of the cube.",
+ "COST" = "with the cost of the cube.",
+ "BONUSLOST" = "with the lost bonus tip, it will be sent just for When Bonus Lost message!",
+ )
diff --git a/code/game/machinery/civilian_bounty/bounty_machinery.dm b/code/game/machinery/civilian_bounty/bounty_machinery.dm
new file mode 100644
index 000000000000..96cae89c8d18
--- /dev/null
+++ b/code/game/machinery/civilian_bounty/bounty_machinery.dm
@@ -0,0 +1,444 @@
+///Percentage of a civilian bounty the civilian will make.
+#define CIV_BOUNTY_SPLIT 30
+#define HIGH_PRIORITY_BOUNTY_ODDS 20
+
+///Pad for the Civilian Bounty Control.
+/obj/machinery/piratepad/civilian
+ name = "civilian bounty pad"
+ desc = "A machine designed to send civilian bounty targets to centcom."
+ layer = TABLE_LAYER
+ resistance_flags = FIRE_PROOF
+ circuit = /obj/item/circuitboard/machine/bountypad
+ var/cooldown_reduction = 0
+
+/obj/machinery/piratepad/civilian/RefreshParts()
+ . = ..()
+ var/T = -2
+ for(var/datum/stock_part/micro_laser/micro_laser in component_parts)
+ T += micro_laser.tier
+
+ for(var/datum/stock_part/scanning_module/scanning_module in component_parts)
+ T += scanning_module.tier
+
+ cooldown_reduction = T * (30 SECONDS)
+
+/obj/machinery/piratepad/civilian/proc/get_cooldown_reduction()
+ return cooldown_reduction
+
+///Computer for assigning new civilian bounties, and sending bounties for collection.
+/obj/machinery/computer/piratepad_control/civilian
+ name = "civilian bounty control terminal"
+ desc = "A console for assigning civilian bounties to inserted ID cards, and for controlling the bounty pad for export."
+ status_report = "Ready for delivery."
+ icon_screen = "civ_bounty"
+ icon_keyboard = "id_key"
+ warmup_time = 3 SECONDS
+ circuit = /obj/item/circuitboard/computer/bountypad
+ interface_type = "CivCargoHoldTerminal"
+ load_holding_facility = FALSE
+ ///Typecast of an inserted, scanned ID card inside the console, as bounties are held within the ID card.
+ var/obj/item/card/id/inserted_scan_id
+ ///Cooldown for printing the bounty sheet, and not breaking people's eardrums.
+ COOLDOWN_DECLARE(sheet_printer_cooldown)
+
+/obj/machinery/computer/piratepad_control/civilian/attackby(obj/item/I, mob/living/user, list/modifiers, list/attack_modifiers)
+ if(isidcard(I))
+ if(id_insert(user, I, inserted_scan_id))
+ inserted_scan_id = I
+ return TRUE
+ return ..()
+
+/obj/machinery/computer/piratepad_control/multitool_act(mob/living/user, obj/item/multitool/I)
+ if(istype(I) && istype(I.buffer,/obj/machinery/piratepad/civilian))
+ to_chat(user, span_notice("You link [src] with [I.buffer] in [I] buffer."))
+ pad_ref = WEAKREF(I.buffer)
+ return TRUE
+
+/obj/machinery/computer/piratepad_control/civilian/post_machine_initialize()
+ . = ..()
+ if(cargo_hold_id)
+ for(var/obj/machinery/piratepad/civilian/C as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/piratepad/civilian))
+ if(C.cargo_hold_id == cargo_hold_id)
+ pad_ref = WEAKREF(C)
+ return
+ else
+ var/obj/machinery/piratepad/civilian/pad = locate() in range(4,src)
+ pad_ref = WEAKREF(pad)
+
+/obj/machinery/computer/piratepad_control/civilian/recalc()
+ if(sending)
+ return FALSE
+ if(!inserted_scan_id)
+ status_report = "Please insert your ID first."
+ playsound(loc, 'sound/machines/synth/synth_no.ogg', 30 , TRUE)
+ return FALSE
+ if(!inserted_scan_id.registered_account.civilian_bounty)
+ status_report = "Please accept a new civilian bounty first."
+ playsound(loc, 'sound/machines/synth/synth_no.ogg', 30 , TRUE)
+ return FALSE
+ status_report = "Civilian Bounty: "
+ var/obj/machinery/piratepad/civilian/pad = pad_ref?.resolve()
+ for(var/atom/movable/possible_shippable in get_turf(pad))
+ if(possible_shippable == pad)
+ continue
+ if(possible_shippable.flags_1 & HOLOGRAM_1)
+ continue
+ if(isitem(possible_shippable))
+ var/obj/item/possible_shippable_item = possible_shippable
+ if(possible_shippable_item.item_flags & ABSTRACT)
+ continue
+ if(inserted_scan_id.registered_account.civilian_bounty.applies_to(possible_shippable))
+ status_report += "Target Applicable."
+ playsound(loc, 'sound/machines/synth/synth_yes.ogg', 30 , TRUE)
+ return
+ status_report += "Not Applicable."
+ playsound(loc, 'sound/machines/synth/synth_no.ogg', 30 , TRUE)
+
+
+/**
+ * This fully rewrites base behavior in order to only check for bounty objects, and no other types of objects like pirate-pads do.
+ */
+/obj/machinery/computer/piratepad_control/civilian/send(check_global = FALSE, mob/user)
+ status_report = ""
+ playsound(loc, 'sound/machines/wewewew.ogg', 70, TRUE)
+ if(!sending)
+ return FALSE
+ var/datum/bank_account/id_account = inserted_scan_id?.registered_account
+
+ // To account for check_global, we're going to construct a list of all bounties we want to check.
+ var/list/datum/bounty/bounty_stack = list()
+
+ if(check_global)
+ bounty_stack = GLOB.shared_crew_bounties.Copy()
+ else
+ bounty_stack += id_account?.civilian_bounty
+
+ if(!length(bounty_stack))
+ stop_sending()
+ return FALSE
+ var/active_count = 0
+ var/obj/machinery/piratepad/civilian/pad = pad_ref?.resolve()
+ for(var/atom/movable/possible_shippable in get_turf(pad))
+ if(possible_shippable == pad)
+ continue
+ if(possible_shippable.flags_1 & HOLOGRAM_1)
+ continue
+ if(isitem(possible_shippable))
+ var/obj/item/possible_shippable_item = possible_shippable
+ if(possible_shippable_item.item_flags & ABSTRACT)
+ continue
+
+ for(var/datum/bounty/stack_item in bounty_stack)
+ if(stack_item.applies_to(possible_shippable))
+ active_count++
+ LAZYADDASSOC(stack_item.contribution, id_account, stack_item.contribution_amount(possible_shippable))
+ if(stack_item.contribution[id_account] <= 0 || !stack_item.contribution[id_account])
+ stack_item.contribution[id_account] = stack_item.contribution_amount(possible_shippable)
+ stack_item.ship(possible_shippable)
+ qdel(possible_shippable)
+
+ if(active_count >= 1)
+ status_report += "Bounty Target[active_count > 1 ? "s" : ""] Found x[active_count]. "
+
+ SStgui.update_uis(src) //update Ui data to display how much of the bounty remains
+ else
+ status_report = "No applicable target found. Aborting. "
+ stop_sending()
+
+ active_count = 0 // We'll just re-use this for the second message setter.
+ for(var/datum/bounty/stack_item in bounty_stack)
+ if(stack_item.can_claim())
+ active_count++
+ //Pay for the bounty with the ID's department funds.
+ SSblackbox.record_feedback("tally", "bounties_completed", 1, stack_item.type)
+ stack_item.claimed = TRUE
+ stack_item.on_claimed(inserted_scan_id)
+ // Unique case: A global bounty is completed, and you have the same bounty as a personal bounty,
+ // it will complete your personal one as well. It will however only increment the tracker by one.
+ if(check_global)
+ SSeconomy.civ_bounty_tracker++ //This is the tracker for adding more global bounties, not for logging purposes.
+ for(var/datum/bank_account/helper in stack_item.contribution)
+ if(istype(helper?.civilian_bounty, stack_item.type))
+ helper.reset_bounty(inserted_scan_id)
+ helper.bank_card_talk("Your [stack_item.name] bounty has been completed for matching the completed station bounty!")
+ else
+ id_account.reset_bounty(inserted_scan_id)
+
+ var/obj/item/bounty_cube/reward = new /obj/item/bounty_cube(drop_location())
+ reward.set_up(stack_item, inserted_scan_id)
+ if(active_count >= 1)
+ status_report += "x[active_count] Bount[active_count > 1 ? "ies" : "y"] completed! \
+ Please give your bounty cube[active_count > 1 ? "s" : ""] to cargo for your automated payout shortly. "
+
+ if(check_global)
+ update_global_bounty_list(round(CIV_BOUNTY_BASELINE + (SSeconomy.civ_bounty_tracker / 3)), FALSE)
+
+ pad.visible_message(span_notice("[pad] activates!"))
+ flick(pad.sending_state,pad)
+ pad.icon_state = pad.idle_state
+ playsound(loc, 'sound/machines/synth/synth_yes.ogg', 30 , TRUE)
+ sending = FALSE
+ return TRUE
+
+///Here is where cargo bounties are added to the player's bank accounts, then adjusted and scaled into a civilian bounty.
+/obj/machinery/computer/piratepad_control/civilian/proc/add_bounties(mob/user, cooldown_reduction = 0)
+ var/datum/bank_account/id_account = inserted_scan_id?.registered_account
+ if(!id_account)
+ return FALSE
+ if((id_account.civilian_bounty || id_account.bounties) && !COOLDOWN_FINISHED(id_account, bounty_timer))
+ var/time_left = DisplayTimeText(COOLDOWN_TIMELEFT(id_account, bounty_timer), round_seconds_to = 1)
+ balloon_alert(user, "try again in [time_left]!")
+ return FALSE
+ if(!inserted_scan_id.trim)
+ say("Requesting ID card has no job assignment registered!")
+ return FALSE
+
+ var/list/datum/bounty/crumbs = inserted_scan_id.trim.generate_bounty_list()
+ COOLDOWN_START(id_account, bounty_timer, (5 MINUTES) - cooldown_reduction)
+ id_account.bounties = crumbs
+ return TRUE
+
+
+/**
+ * Proc that assigned a civilian bounty to an ID card, from the list of potential bounties that that bank account currently has available.
+ * Available choices are assigned during add_bounties, and one is locked in here.
+ *
+ * @param choice The index of the bounty in the list of bounties that the player can choose from.
+ */
+/obj/machinery/computer/piratepad_control/civilian/proc/pick_bounty(datum/bounty/choice)
+ var/datum/bank_account/id_account = inserted_scan_id?.registered_account
+ if(!id_account?.bounties?[choice])
+ playsound(loc, 'sound/machines/synth/synth_no.ogg', 40 , TRUE)
+ return
+ id_account.set_bounty(id_account.bounties[choice], inserted_scan_id)
+ id_account.bounties = null
+ SSblackbox.record_feedback("tally", "bounties_assigned", 1, id_account.civilian_bounty.type)
+ return id_account.civilian_bounty
+
+/obj/machinery/computer/piratepad_control/civilian/click_alt(mob/user)
+ id_eject(user, inserted_scan_id)
+ return CLICK_ACTION_SUCCESS
+
+/obj/machinery/computer/piratepad_control/civilian/ui_data(mob/user)
+ var/list/data = list()
+ data["points"] = points
+ data["pad"] = pad_ref?.resolve() ? TRUE : FALSE
+ data["sending"] = sending
+ data["status_report"] = status_report
+ data["id_inserted"] = inserted_scan_id
+ data["claimed_bounties"] = SSeconomy.civ_bounty_tracker
+
+// Personal bounty data:
+ if(inserted_scan_id?.registered_account)
+ if(inserted_scan_id.registered_account.civilian_bounty)
+ data["id_bounty_info"] = inserted_scan_id.registered_account.civilian_bounty.description
+ data["id_bounty_num"] = inserted_scan_id.registered_account.bounty_num()
+ data["id_bounty_value"] = (inserted_scan_id.registered_account.civilian_bounty.get_bounty_reward()) * (CIV_BOUNTY_SPLIT / 100)
+ if(inserted_scan_id.registered_account.bounties)
+ data["picking"] = TRUE
+ data["id_bounty_names"] = list()
+ data["id_bounty_infos"] = list()
+ data["id_bounty_values"] = list()
+ for(var/datum/bounty/bounty as anything in inserted_scan_id.registered_account.bounties)
+ data["id_bounty_names"] += bounty.name
+ data["id_bounty_infos"] += bounty.description
+ data["id_bounty_values"] += bounty.get_bounty_reward() * (CIV_BOUNTY_SPLIT / 100)
+
+ else
+ data["picking"] = FALSE
+
+// Global bounty data:
+ data["listBounty"] = list()
+ for(var/datum/bounty/global_bounty as anything in GLOB.shared_crew_bounties)
+
+ var/ship_max = global_bounty.get_max()
+ var/ship_total = global_bounty.get_total() //Present value as a percentage, we'll handle 0 and 100 as constants
+
+ if(data["listBounty"]["name"] == global_bounty.name)
+ continue
+ data["listBounty"] += list(list(
+ "name" = global_bounty.name,
+ "description" = global_bounty.description,
+ "reward" = global_bounty.get_bounty_reward(),
+ "claimed" = global_bounty.claimed,
+ "shipped" = ship_total,
+ "maximum" = ship_max,
+ "priority" = global_bounty.high_priority,
+ "unique" = global_bounty.unique
+ ))
+
+ return data
+
+/obj/machinery/computer/piratepad_control/civilian/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ var/obj/machinery/piratepad/civilian/pad = pad_ref?.resolve()
+ if(!pad)
+ return
+ var/mob/user = ui.user
+ if(!user.can_perform_action(src) || (machine_stat & (NOPOWER|BROKEN)))
+ return
+ switch(action) //several ui_acts are handled on parent by piratepad
+ if("pick")
+ pick_bounty(params["value"])
+ return TRUE
+ if("bounty")
+ add_bounties(user, pad.get_cooldown_reduction())
+ return TRUE
+ if("eject")
+ id_eject(user, inserted_scan_id)
+ inserted_scan_id = null
+ return TRUE
+ if("update_list")
+ playsound(src, 'sound/machines/data_transmission.ogg', 50) // Should only need to play once per round due to the list auto-updating afterwards.
+
+ var/bonus_bounties = clamp(round(length(GLOB.player_list) / 8), 0, 5) // The number of bounties to be generated is 5 + 1-per every 8 players on the server, up to a max of 10 total.
+ looped_global_update(1, CIV_BOUNTY_BASELINE + bonus_bounties, first_time = TRUE) // Just for visual flair
+ return TRUE
+ if("print")
+ print_sheet(user)
+
+/// Self explanitory, holds the ID card in the console for bounty payout and manipulation.
+/obj/machinery/computer/piratepad_control/civilian/proc/id_insert(mob/user, obj/item/inserting_item, obj/item/target)
+ var/obj/item/card/id/card_to_insert = inserting_item
+ var/holder_item = FALSE
+
+ if(!isidcard(card_to_insert))
+ card_to_insert = inserting_item.remove_id()
+ holder_item = TRUE
+
+ if(!card_to_insert || !user.transferItemToLoc(card_to_insert, src))
+ return FALSE
+
+ if(target)
+ if(holder_item && inserting_item.insert_id(target))
+ playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 50, FALSE)
+ else
+ id_eject(user, target)
+
+ user.visible_message(span_notice("[user] inserts \the [card_to_insert] into \the [src]."),
+ span_notice("You insert \the [card_to_insert] into \the [src]."))
+ playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 50, FALSE)
+ ui_interact(user)
+ return TRUE
+
+///Removes A stored ID card.
+/obj/machinery/computer/piratepad_control/civilian/proc/id_eject(mob/user, obj/item/target)
+ if(!target)
+ to_chat(user, span_warning("That slot is empty!"))
+ return FALSE
+ else
+ try_put_in_hand(target, user)
+ user.visible_message(span_notice("[user] gets \the [target] from \the [src]."), \
+ span_notice("You get \the [target] from \the [src]."))
+ playsound(src, 'sound/machines/terminal/terminal_insert_disc.ogg', 50, FALSE)
+ inserted_scan_id = null
+ return TRUE
+
+/**
+ * Updates the global bounty list: First by sorting through all completed bounties on the list and deleting them.
+ * Then, adds new bounties up to the limit, defined by the update_up_to argument.
+ * The bounties to be added should not share duplicates between job subtypes.
+ * @param update_up_to How many new bounties to add to the list, up to the maximum defined by MAXIMUM_BOUNTY_JOBS.
+ * @param enable_high_priority If enabled, means that past a prob(HIGH_PRIORITY_BOUNTY_ODDS), the created bounty will have a 1.5x value multiplier and be labeled in the UI.
+ * @param running_jobs A list we generate when making multiple bounties at once, this stores the job define to prevent double dipping on the same job type.
+ */
+/obj/machinery/computer/piratepad_control/civilian/proc/update_global_bounty_list(update_up_to = CIV_BOUNTY_BASELINE, enable_high_priority = FALSE, list/running_jobs)
+ //First, clear out completed bounties.
+ for(var/datum/bounty/complete_or_unique in GLOB.shared_crew_bounties)
+ if(complete_or_unique.claimed)
+ GLOB.shared_crew_bounties -= complete_or_unique
+ if(complete_or_unique.unique)
+ update_up_to++ // We're doing this to ignore the quantity of unique bounties in the global list.
+
+ //Then, add new bounties up to the limit.
+ var/list/jobs_picked = running_jobs || list()
+ while(length(GLOB.shared_crew_bounties) < update_up_to)
+ var/job_code = rand(CIV_JOB_BASIC, CIV_JOB_BITRUN) //CIV_JOB_ defines taken from _DEFINES/economy.dm. If new job bounty classes are added, swap out our maximum.
+ if(job_code in jobs_picked)
+ continue
+ jobs_picked += job_code
+
+ var/datum/bounty/new_bounty = random_bounty(job_code)
+ if(new_bounty.global_exempt)
+ continue
+
+ GLOB.shared_crew_bounties.Insert(0, new_bounty)
+
+ if(enable_high_priority && prob(HIGH_PRIORITY_BOUNTY_ODDS))
+ new_bounty.high_priority = TRUE
+ new_bounty.description += "\
+ This bounty is marked as high priority, and will reward 1.5x the normal payout!"
+ return jobs_picked
+
+/// Performs several global bounty updates in a row on a callback loop, adding one each time.
+/obj/machinery/computer/piratepad_control/civilian/proc/looped_global_update(current_count, update_to, inherited_list, first_time = FALSE)
+ var/jobs_picked = update_global_bounty_list(current_count, enable_high_priority = TRUE, running_jobs = inherited_list)
+
+ if(current_count == update_to)
+ if(first_time)
+ setup_special_procs()
+ return TRUE
+ current_count++
+ addtimer(CALLBACK(src, PROC_REF(looped_global_update), current_count, update_to, jobs_picked, first_time), 0.8 SECONDS)
+ return FALSE
+
+/// Spawns the roundstart "special" bounties.
+/obj/machinery/computer/piratepad_control/civilian/proc/setup_special_procs()
+ for(var/selected_special in subtypesof(/datum/bounty/item/special))
+ GLOB.shared_crew_bounties += new selected_special
+
+/**
+ * Handles cooldowns and creation of a new cargo bounty sheet.
+ */
+/obj/machinery/computer/piratepad_control/civilian/proc/print_sheet(mob/living/user)
+ if(!COOLDOWN_FINISHED(src, sheet_printer_cooldown))
+ balloon_alert(user, "printer spooling!")
+ return FALSE
+
+ var/obj/item/paper/paper = new(loc)
+ paper.name = "paper - Bounties"
+
+ var/list/printout_text = list()
+ printout_text += "Nanotrasen Cargo Bounties
"
+
+ for(var/datum/bounty/current_bounty in GLOB.shared_crew_bounties)
+ if(current_bounty.claimed)
+ continue
+ printout_text += {"[current_bounty.name]
+
+ - Quantity requested: [current_bounty.print_required()]
+
- Reward: [current_bounty.get_bounty_reward()] cr.
+ - Cut: [round(BOUNTY_CUT_STANDARD * current_bounty.get_bounty_reward())] cr.
+
"}
+ paper.add_raw_text(printout_text.Join("
"))
+ paper.update_appearance()
+
+ playsound(src, 'sound/machines/printer.ogg', 100, TRUE)
+ COOLDOWN_START(src, sheet_printer_cooldown, 4 SECONDS)
+ return TRUE
+
+///Beacon to launch a new bounty setup when activated.
+/obj/item/civ_bounty_beacon
+ name = "civilian bounty beacon"
+ desc = "N.T. approved civilian bounty beacon, toss it down and you will have a bounty pad and computer delivered to you."
+ icon = 'icons/obj/machines/floor.dmi'
+ icon_state = "floor_beacon"
+ var/uses = 2
+
+/obj/item/civ_bounty_beacon/attack_self()
+ loc.visible_message(span_warning("\The [src] begins to beep loudly!"))
+ addtimer(CALLBACK(src, PROC_REF(launch_payload)), 1 SECONDS)
+
+/obj/item/civ_bounty_beacon/proc/launch_payload()
+ playsound(src, SFX_SPARKS, 80, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ switch(uses)
+ if(2)
+ new /obj/machinery/piratepad/civilian(drop_location())
+ if(1)
+ new /obj/machinery/computer/piratepad_control/civilian(drop_location())
+ qdel(src)
+ uses--
+
+
+#undef CIV_BOUNTY_SPLIT
+#undef HIGH_PRIORITY_BOUNTY_ODDS
diff --git a/code/game/machinery/civilian_bounty/civilian_bounty_trims.dm b/code/game/machinery/civilian_bounty/civilian_bounty_trims.dm
new file mode 100644
index 000000000000..6c3c7221de67
--- /dev/null
+++ b/code/game/machinery/civilian_bounty/civilian_bounty_trims.dm
@@ -0,0 +1,92 @@
+
+/**
+ * Generates a list of bounties for use with the civilian bounty pad.
+ *
+ * @param bounty_types the define taken from a job for selection of a random_bounty() proc.
+ * @param bounty_rolls the number of bounties to be selected from.
+ * @param assistant_failsafe Do we guarentee one assistant bounty per generated list? Used for non-assistant jobs to give an easier alternative to that job's default bounties.
+ */
+/datum/id_trim/proc/generate_bounty_list(bounty_rolls = 3, assistant_failsafe = TRUE)
+ var/datum/job/our_job = find_job()
+ var/bounty_type = our_job?.bounty_types || CIV_JOB_RANDOM
+
+ var/list/rolling_list = list()
+ if(assistant_failsafe)
+ var/random_assistant = get_random_bounty_type(CIV_JOB_BASIC)
+ var/datum/bounty/assistant_bounty = new random_assistant()
+ if(assistant_bounty.can_get())
+ rolling_list += assistant_bounty
+ else
+ qdel(assistant_bounty)
+
+ var/attempts = 20
+ while(length(rolling_list) < bounty_rolls && attempts > 0)
+ var/random_job = get_random_bounty_type(attempts <= 5 ? CIV_JOB_BASIC : bounty_type)
+ var/datum/bounty/job_bounty = new random_job()
+ attempts -= 1
+ if(!job_bounty.can_get() || has_duplicate_bounty(rolling_list, job_bounty))
+ qdel(job_bounty)
+ continue
+
+ rolling_list += job_bounty
+
+ return rolling_list
+
+/// Helper to see if there's a duplicate bounty in a list of bounties
+/datum/id_trim/proc/has_duplicate_bounty(list/datum/bounty/bounty_list, datum/bounty/check_bounty)
+ PRIVATE_PROC(TRUE)
+
+ for(var/datum/bounty/existing as anything in bounty_list)
+ if(existing.type != check_bounty.type)
+ continue
+ if(existing.allow_duplicate && check_bounty.allow_duplicate)
+ continue
+ return TRUE
+
+ return FALSE
+
+/// Returns a /datum/bounty typepath for a given bounty type
+/datum/id_trim/proc/get_random_bounty_type(input_bounty_type)
+ if(!input_bounty_type || input_bounty_type == CIV_JOB_RANDOM)
+ input_bounty_type = rand(1, MAXIMUM_BOUNTY_JOBS)
+
+ switch(input_bounty_type)
+ if(CIV_JOB_BASIC)
+ return pick(subtypesof(/datum/bounty/item/assistant))
+ if(CIV_JOB_ROBO)
+ return pick(subtypesof(/datum/bounty/item/mech))
+ if(CIV_JOB_CHEF)
+ return pick(subtypesof(/datum/bounty/item/chef) + subtypesof(/datum/bounty/reagent/chef))
+ if(CIV_JOB_SEC)
+ if(prob(75))
+ return /datum/bounty/patrol
+ return /datum/bounty/item/contraband
+ if(CIV_JOB_DRINK)
+ if(prob(50))
+ return /datum/bounty/reagent/simple_drink
+ return /datum/bounty/reagent/complex_drink
+ if(CIV_JOB_CHEM)
+ if(prob(50))
+ return /datum/bounty/reagent/chemical_simple
+ return /datum/bounty/reagent/chemical_complex
+ if(CIV_JOB_VIRO)
+ return pick(subtypesof(/datum/bounty/virus))
+ if(CIV_JOB_SCI)
+ if(prob(50))
+ return pick(subtypesof(/datum/bounty/item/science))
+ return pick(subtypesof(/datum/bounty/item/slime))
+ if(CIV_JOB_ENG)
+ return pick(subtypesof(/datum/bounty/item/engineering))
+ if(CIV_JOB_MINE)
+ return pick(subtypesof(/datum/bounty/item/mining))
+ if(CIV_JOB_MED)
+ return pick(subtypesof(/datum/bounty/item/medical))
+ if(CIV_JOB_GROW)
+ return pick(subtypesof(/datum/bounty/item/botany))
+ if(CIV_JOB_ATMOS)
+ return pick(subtypesof(/datum/bounty/item/atmospherics))
+ if(CIV_JOB_BITRUN)
+ return pick(subtypesof(/datum/bounty/item/bitrunning))
+
+ stack_trace("Failed to get random bounty type for input type [input_bounty_type]")
+ return null
diff --git a/code/game/machinery/computer/_computer.dm b/code/game/machinery/computer/_computer.dm
index 6aa336834121..883ccea87ffb 100644
--- a/code/game/machinery/computer/_computer.dm
+++ b/code/game/machinery/computer/_computer.dm
@@ -7,6 +7,7 @@
integrity_failure = 0.5
armor_type = /datum/armor/machinery_computer
interaction_flags_machine = INTERACT_MACHINE_ALLOW_SILICON|INTERACT_MACHINE_REQUIRES_LITERACY
+ voice_filter = "alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000"
/// How bright we are when turned on.
var/brightness_on = 1
/// Icon_state of the keyboard overlay.
@@ -30,6 +31,11 @@
. = ..()
power_change()
+/obj/machinery/computer/post_machine_initialize()
+ . = ..()
+ if(SStts.tts_enabled)
+ voice = SStts.computer_voice
+
/obj/machinery/computer/mouse_drop_receive(mob/living/dropping, mob/user, params)
. = ..()
// We add the component only once here & not in Initialize() because there are tons of computers & we don't want to add to their init times
diff --git a/code/game/machinery/computer/buildandrepair.dm b/code/game/machinery/computer/buildandrepair.dm
index d5f6b8210069..83d5d373942e 100644
--- a/code/game/machinery/computer/buildandrepair.dm
+++ b/code/game/machinery/computer/buildandrepair.dm
@@ -31,7 +31,7 @@
switch(state)
if(FRAME_COMPUTER_STATE_EMPTY)
if(held_item.tool_behaviour == TOOL_WRENCH)
- context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Un" : ""]anchor"
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Unan" : "An"]chor"
return CONTEXTUAL_SCREENTIP_SET
else if(anchored && istype(held_item, /obj/item/circuitboard/computer))
context[SCREENTIP_CONTEXT_LMB] = "Install board"
diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm
index cf0f1a2b0227..f53fba3541ea 100644
--- a/code/game/machinery/computer/communications.dm
+++ b/code/game/machinery/computer/communications.dm
@@ -22,6 +22,10 @@
/// Cooldown for important actions, such as messaging CentCom or other sectors
COOLDOWN_DECLARE(static/important_action_cooldown)
COOLDOWN_DECLARE(static/emergency_access_cooldown)
+ //MASSMETA EDIT START (ntts && tgtts)
+ COOLDOWN_DECLARE(sec_level_cd)
+ //MASSMETA EDIT END (ntts && tgtts)
+
/// Whether syndicate mode is enabled or not.
var/syndicate = FALSE
@@ -208,8 +212,19 @@
return
if (SSsecurity_level.get_current_level_as_number() == new_sec_level)
return
+ // MASSMETA EDIT START (ntts && tgtts)
+ if(SStts.tts_enabled)
+ if(!COOLDOWN_FINISHED(src, sec_level_cd))
+ to_chat(user, span_warning("You must wait before changing the security level again."))
+ return
+ // MASSMETA EDIT END (ntts && tgtts)
SSsecurity_level.set_level(new_sec_level)
+ // MASSMETA EDIT START (ntts && tgtts)
+ // we must prevent tts misusage
+ if(SStts.tts_enabled)
+ COOLDOWN_START(src, sec_level_cd, 30 SECONDS)
+ // MASSMETA EDIT END (ntts && tgtts)
to_chat(user, span_notice("Authorization confirmed. Modifying security level."))
playsound(src, 'sound/machines/terminal/terminal_prompt_confirm.ogg', 50, FALSE)
diff --git a/code/game/machinery/computer/operating_computer.dm b/code/game/machinery/computer/operating_computer.dm
index 79d9b9279e01..04c48a3ee5cb 100644
--- a/code/game/machinery/computer/operating_computer.dm
+++ b/code/game/machinery/computer/operating_computer.dm
@@ -101,7 +101,7 @@
/obj/machinery/computer/operating/ui_status(mob/user, datum/ui_state/state)
. = ..()
- if(isliving(user))
+ if(isliving(user) && !issilicon(user))
. = min(., ui_check(user))
/// Checks for special ui state conditions
diff --git a/code/game/machinery/computer/records/medical.dm b/code/game/machinery/computer/records/medical.dm
index 9bc1bcdbc26a..d148b5bd2628 100644
--- a/code/game/machinery/computer/records/medical.dm
+++ b/code/game/machinery/computer/records/medical.dm
@@ -59,6 +59,7 @@
major_disabilities = target.major_disabilities_desc,
minor_disabilities = target.minor_disabilities_desc,
physical_status = target.physical_status,
+ cause_of_death = target.cause_of_death,
mental_status = target.mental_status,
name = target.name,
notes = notes,
@@ -123,6 +124,8 @@
return FALSE
target.physical_status = physical_status
+ if(physical_status != PHYSICAL_DECEASED)
+ target.cause_of_death = null
return TRUE
@@ -135,6 +138,13 @@
return TRUE
+ if("set_cause_of_death")
+ var/death_text = reject_bad_name(params["cause"], allow_numbers = TRUE, max_length = MAX_DESC_LEN, strict = TRUE, cap_after_symbols = FALSE)
+ if(!death_text)
+ return FALSE
+ target.cause_of_death = death_text
+ return TRUE
+
return FALSE
/// Deletes medical information from a record.
diff --git a/code/game/machinery/constructable_frame.dm b/code/game/machinery/constructable_frame.dm
index 5012c0eb1aa3..f9d46ba1d290 100644
--- a/code/game/machinery/constructable_frame.dm
+++ b/code/game/machinery/constructable_frame.dm
@@ -114,9 +114,9 @@
return .
/obj/structure/frame/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
+ . = NONE
if(istype(tool, /obj/item/circuitboard)) // Install board will fail if passed an invalid circuitboard and give feedback
return install_board(user, tool, by_hand = TRUE) ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_BLOCKING
- return NONE
/obj/structure/frame/ranged_item_interaction(mob/living/user, obj/item/tool, list/modifiers)
. = NONE
@@ -126,7 +126,7 @@
. = item_interaction(user, tool, modifiers)
if(. & ITEM_INTERACT_ANY_BLOCKER)
- user.Beam(tool, icon_state = "rped_upgrade", time = 0.5 SECONDS)
+ user.Beam(src, icon_state = "rped_upgrade", time = 0.5 SECONDS)
/**
* Installs the passed circuit board into the frame
diff --git a/code/game/machinery/hologram.dm b/code/game/machinery/hologram.dm
index aa67a92a30c8..abd38b1fcecf 100644
--- a/code/game/machinery/hologram.dm
+++ b/code/game/machinery/hologram.dm
@@ -559,6 +559,14 @@ Possible to do for anyone motivated enough:
hologram.icon_state = work_off.icon_state
hologram.copy_overlays(work_off, TRUE)
hologram.makeHologram()
+ //MASSMETA EDIT ADDITION START (ntts && tgtts)
+ // talking holograms
+ hologram.voice = user.voice
+ hologram.pitch = user.pitch
+ hologram.blip_base = user.blip_base
+ hologram.blip_number = user.blip_number
+ hologram.voice_filter = user.voice_filter
+ //MASSMETA EDIT ADDITION END (ntts && tgtts)
if(AI)
AI.eyeobj.setLoc(get_turf(src)) //ensure the AI camera moves to the holopad
@@ -577,6 +585,12 @@ Possible to do for anyone motivated enough:
return hologram
else
to_chat(user, "[span_danger("ERROR:")] Unable to project hologram.")
+//MASSMETA EDIT ADDITION START (ntts && tgtts)
+/obj/machinery/holopad/get_listening_mob()
+ for(var/mob/listener as anything in masters)
+ if(masters[listener])
+ return listener
+//MASSMETA EDIT ADDITION END (ntts && tgtts)
/*This is the proc for special two-way communication between AI and holopad/people talking near holopad.
For the other part of the code, check silicon say.dm. Particularly robot talk.*/
diff --git a/code/game/machinery/iv_drip.dm b/code/game/machinery/iv_drip.dm
index fce0a42043dc..556df4d02ebe 100644
--- a/code/game/machinery/iv_drip.dm
+++ b/code/game/machinery/iv_drip.dm
@@ -324,7 +324,6 @@
return use_internal_storage ? reagents : reagent_container?.reagents
/obj/machinery/iv_drip/verb/eject_beaker()
- set category = "Object"
set name = "Remove IV Container"
set src in view(1)
@@ -344,7 +343,6 @@
update_appearance(UPDATE_ICON)
/obj/machinery/iv_drip/verb/toggle_mode()
- set category = "Object"
set name = "Toggle Mode"
set src in view(1)
diff --git a/code/game/machinery/machine_frame.dm b/code/game/machinery/machine_frame.dm
index 0f88210348c7..008849404eb1 100644
--- a/code/game/machinery/machine_frame.dm
+++ b/code/game/machinery/machine_frame.dm
@@ -29,7 +29,7 @@
return
if(held_item.tool_behaviour == TOOL_WRENCH && !circuit?.needs_anchored)
- context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Un" : ""]anchor"
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Unan" : "An"]chor"
return CONTEXTUAL_SCREENTIP_SET
switch(state)
diff --git a/code/game/machinery/pipe/construction.dm b/code/game/machinery/pipe/construction.dm
index 172fbe8daa10..82217efdda3b 100644
--- a/code/game/machinery/pipe/construction.dm
+++ b/code/game/machinery/pipe/construction.dm
@@ -218,7 +218,6 @@ Buildable meters
resistance_flags |= FIRE_PROOF | LAVA_PROOF
/obj/item/pipe/verb/flip()
- set category = "Object"
set name = "Invert Pipe"
set src in view(1)
diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm
index cf9f42ee778d..801ed06603cd 100644
--- a/code/game/machinery/porta_turret/portable_turret.dm
+++ b/code/game/machinery/porta_turret/portable_turret.dm
@@ -695,6 +695,9 @@ DEFINE_BITFIELD(turret_flags, list(
button_icon_state = "mech_cycle_equip_off"
/datum/action/turret_toggle/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
var/obj/machinery/porta_turret/P = target
if(!istype(P))
return
@@ -706,6 +709,9 @@ DEFINE_BITFIELD(turret_flags, list(
button_icon_state = "mech_eject"
/datum/action/turret_quit/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
var/obj/machinery/porta_turret/P = target
if(!istype(P))
return
diff --git a/code/game/machinery/requests_console.dm b/code/game/machinery/requests_console.dm
index 018bfd65e500..17ddb3909f1b 100644
--- a/code/game/machinery/requests_console.dm
+++ b/code/game/machinery/requests_console.dm
@@ -305,7 +305,9 @@ GLOBAL_LIST_EMPTY(req_console_ckey_departments)
var/mob/living/L = usr
message = L.treat_message(message)["message"]
- minor_announce(message, "[department] Announcement:", html_encode = FALSE, sound_override = 'sound/announcer/announcement/announce_dig.ogg')
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ minor_announce(message, "[department] Announcement:", html_encode = FALSE, sound_override = 'sound/announcer/announcement/announce_dig.ogg', tts_source = usr, tts_voice = isliving(usr) ? null : SStts.computer_voice, tts_effect = "request_console")
+ // MASSMETA EDIT END (ntts && /tg/tts)
GLOB.news_network.submit_article(message, department, NEWSCASTER_STATION_ANNOUNCEMENTS, null)
usr.log_talk(message, LOG_SAY, tag="station announcement from [src]")
message_admins("[ADMIN_LOOKUPFLW(usr)] has made a station announcement from [src] at [AREACOORD(usr)].")
diff --git a/code/game/machinery/telecomms/broadcasting.dm b/code/game/machinery/telecomms/broadcasting.dm
index 53d217f6cb75..fab4b92f8a92 100644
--- a/code/game/machinery/telecomms/broadcasting.dm
+++ b/code/game/machinery/telecomms/broadcasting.dm
@@ -161,19 +161,30 @@
for(var/obj/item/radio/called_radio as anything in radios)
called_radio.on_receive_message(data)
-
+ var/list/message_mods = data["mods"]
// From the list of radios, find all mobs who can hear those.
var/list/receive = get_hearers_in_radio_ranges(radios)
+ var/list/receive_radios = null
+
+ if(LAZYACCESS(message_mods, MODE_TTS_IDENTIFIER)) // only do this if we have a TTS identifier to save on perf
+ receive_radios = get_hearers_in_radio_ranges_track_radios(radios, frequency)
// Add observers who have ghost radio enabled.
for(var/mob/dead/observer/ghost in GLOB.player_list)
if(get_chat_toggles(ghost.client) & CHAT_GHOSTRADIO)
receive |= ghost
+ if(LAZYACCESS(message_mods, MODE_TTS_IDENTIFIER))
+ receive_radios[TTS_GHOST_RADIO] |= ghost
// Render the message and have everybody hear it.
// Always call this on the virtualspeaker to avoid issues.
var/spans = data["spans"]
- var/list/message_mods = data["mods"]
+
+ if(LAZYACCESS(message_mods, MODE_TTS_IDENTIFIER))
+ receive_radios[TTS_GHOST_RADIO] = filter_tts_listeners(receive_radios[TTS_GHOST_RADIO], frequency)
+ for(var/radio in receive_radios)
+ LAZYSET(SStts.queued_radio_messages[message_mods[MODE_TTS_IDENTIFIER]], radio, receive_radios[radio])
+ LAZYSET(SStts.queued_radio_messages_compression, message_mods[MODE_TTS_IDENTIFIER], compression)
for(var/atom/movable/hearer as anything in receive)
if(!hearer)
diff --git a/code/game/objects/effects/decals/turfdecal/weakpoint.dm b/code/game/objects/effects/decals/turfdecal/weakpoint.dm
index 915d948e6e24..5dcdf28183cc 100644
--- a/code/game/objects/effects/decals/turfdecal/weakpoint.dm
+++ b/code/game/objects/effects/decals/turfdecal/weakpoint.dm
@@ -2,17 +2,22 @@
#define CRACK_PROPAGATION_DELAY 0.1 SECONDS
#define CRACK_TURN_CHANCE 50
#define CRACK_DELAY_CHANCE 33
+#define CRACK_LENGTH_DEFAULT 8
/obj/effect/weakpoint
name = "weakpoint crack"
desc = "A suspicious crack runs along the ground."
icon = 'icons/effects/effects.dmi'
icon_state = "weakpoint"
+ base_icon_state = "weakpoint"
+ layer = ABOVE_NORMAL_TURF_LAYER
+ move_resist = INFINITY
+ alpha = 0
/// The required strength of explosion for a weakpoint to propogate
var/required_strength = EXPLODE_LIGHT
//How many turfs should this weakpoint crack when triggered? Crack length splits by default and doesn't recurse
- var/crack_length = 8
+ var/crack_length = CRACK_LENGTH_DEFAULT
/// How many split off cracks are expected?
var/crack_split_count = 2
@@ -20,25 +25,33 @@
var/spawns_children = TRUE
/// How many children weakpoints will this crack spawn when it propagates?
var/new_weakpoints = 2
+ /// These turfs are things we don't want to spawn new cracks onto.
+ var/static/list/skip_turfs = typecacheof(list(
+ /turf/open/space,
+ /turf/open/misc/asteroid,
+ /turf/open/misc/snow,
+ ))
/obj/effect/weakpoint/Initialize(mapload)
. = ..()
AddElement(/datum/element/undertile, TRAIT_T_RAY_VISIBLE, INVISIBILITY_OBSERVER, use_anchor = TRUE)
+ RegisterSignal(src, COMSIG_TURF_CHANGE, PROC_REF(turf_changed))
register_context()
+ animate(src, alpha = 255, time = 0.3 SECONDS)
/obj/effect/weakpoint/ex_act(severity, target)
. = ..()
- var/static/list/skip_turfs = typecacheof(list(
- /turf/open/space,
- /turf/open/misc/asteroid,
- /turf/open/misc/snow,
- ))
if(severity < required_strength)
balloon_alert_to_hearers("*crack*")
playsound(source = src, soundin = SFX_HULL_CREAKING, vol = 50, vary = TRUE, pressure_affected = FALSE, ignore_walls = TRUE)
return //return ominous sounds when we're under the threshold.
- var/list/chain_turfs = get_crack_chain(get_turf(src), 8, TRUE, skip_turfs) // Get a nice chain of turfs
+ var/list/chain_turfs = get_crack_chain(get_turf(src), crack_length, TRUE, skip_turfs) // Get a nice chain of turfs
+ for(var/atom/along_length in chain_turfs)
+ for(var/extra_turfs in get_adjacent_turfs(chain_turfs[along_length])) //use get_adjacent turfs to help add extra turfs.
+ if(chain_turfs[extra_turfs])
+ continue
+ chain_turfs += extra_turfs
var/crack_delay = 0
for(var/turf/crack_turf in chain_turfs)
@@ -48,14 +61,7 @@
crack_delay++
if(spawns_children)
- chain_turfs = typecache_filter_list_reverse(chain_turfs, skip_turfs) //Filter out things that we don't want to spawn new weakpoints onto.
-
- for(var/i in 1 to new_weakpoints)
- var/obj/effect/weakpoint/newpoint = new(pick(chain_turfs))
- //inherit parent var values in case of var-editing.
- newpoint.new_weakpoints = new_weakpoints
- newpoint.crack_length = crack_length
- newpoint.crack_split_count = crack_split_count
+ addtimer(CALLBACK(loc, TYPE_PROC_REF(/turf, create_new_cracks), chain_turfs, new_weakpoints, skip_turfs, crack_length, crack_split_count), CRACK_PROPAGATION_DELAY * crack_delay)
qdel(src)
/obj/effect/weakpoint/welder_act(mob/living/user, obj/item/tool)
@@ -93,39 +99,90 @@
* * start_location: The turf to begin the chain of turfs from.
* * length: How many lengths this chain needs to be.
* * add_splits: Should this crack chain apply additional instances of get_crack_chain while recursively cracking even further.
- * * turfs_to_skip: a typecache of turfs that we block spreading to when getting a chain.
*/
-/obj/effect/weakpoint/proc/get_crack_chain(start_location, length, add_splits = TRUE, turfs_to_skip = list())
+/obj/effect/weakpoint/proc/get_crack_chain(start_location, length, add_splits = TRUE)
if(!length)
CRASH("Weakpoint spawned with no length value!")
if(!start_location)
CRASH("No start location for crack specified!")
var/list/turf/cracked_turfs = list()
- var/turf/current = loc //Start on top of ourselves
+ var/turf/current = start_location //Start on top of ourselves
var/direction = pick(NORTH, SOUTH, EAST, WEST)
for(var/i in 1 to length)
- if(length(turfs_to_skip) && is_type_in_typecache(current, turfs_to_skip))
- direction = turn(direction, pick(90, 135, 180, 225, 270)) //We'll either turn or reverse the direction of the crack if we can't get around our obstacle.
- current = get_turf(get_step(current, direction))
- continue
- cracked_turfs += current
// Randomly branch or continue
if(prob(CRACK_TURN_CHANCE))
- direction = turn(direction, pick(-90, -45, 45, 90))
+ direction = turn(direction, pick(90, 135, 180, 225, 270))
current = get_turf(get_step(current, direction))
if(!isturf(current))
break
+ if(length(skip_turfs) && is_type_in_typecache(current, skip_turfs))
+ continue
+ cracked_turfs += current
+
if(add_splits)
for(var/subcrack in 1 to crack_split_count)
cracked_turfs += get_crack_chain(pick(cracked_turfs), max(round(length/2 ), 1), FALSE) //Stop recursion here
message_admins("Station weakpoint triggered, affecting [length(cracked_turfs)] turfs in [loc_name(start_location)].")
log_game("Station weakpoint triggered, affecting [length(cracked_turfs)] turfs in [loc_name(start_location)].")
-
return cracked_turfs
+/// If this turf becomes something we can't spawn a crack on, we should try and shift the crack or otherwise qdel.
+/obj/effect/weakpoint/proc/turf_changed(turf/source)
+ SIGNAL_HANDLER
+ var/turf/option
+ var/list/turf/choices = get_adjacent_open_turfs(src)
+ while(!option && length(choices))
+ option = pick_n_take(get_adjacent_open_turfs(src))
+ if(locate(/obj/effect/weakpoint) in option)
+ continue
+ if(is_type_in_typecache(option, skip_turfs))
+ continue
+ if(!option)
+ qdel(src)
+ return
+ forceMove(option)
+
+/**
+ * Used by weakpoint cracks to spawn new cracks after the crack is finished propagating.
+ * * chain_turfs: The list of turfs that we're going to pull from in order to generate a new weakpoint. Generated by ex_act on the parent weakpoint.
+ */
+/turf/proc/create_new_cracks(list/chain_turfs, new_weakpoints = 1, skip_turfs = list(), crack_length = 2, crack_split_count = 1)
+ if(!chain_turfs || !length(chain_turfs))
+ chain_turfs = list(src)
+ chain_turfs += get_adjacent_turfs(src)
+ if(skip_turfs)
+ chain_turfs = typecache_filter_list_reverse(chain_turfs, skip_turfs) //Filter out things that we don't want to spawn new weakpoints onto.
+
+ var/active_count = 0
+ var/list/new_cracks = list()
+ while(active_count < new_weakpoints)
+ if(!length(chain_turfs))
+ return
+ var/turf/spawn_location = pick_n_take(chain_turfs)
+ if(locate(/obj/effect/weakpoint) in spawn_location)
+ continue
+ if(skip_turfs)
+ if(is_type_in_typecache(spawn_location, skip_turfs))
+ continue
+ var/obj/effect/weakpoint/newpoint = new(spawn_location)
+ //inherit parent var values in case of var-editing.
+ newpoint.new_weakpoints = new_weakpoints
+ newpoint.crack_length = crack_length
+ newpoint.crack_split_count = crack_split_count
+ new_cracks += newpoint
+ spawn_location.levelupdate()
+ active_count++
+
+ notify_ghosts(
+ "A new crack has been spawned in [get_area(src)].",
+ source = pick(new_cracks),
+ header = "Weakpoint created",
+ ghost_sound = 'sound/effects/hit_kick.ogg',
+ )
+
/obj/effect/weakpoint/big
name = "dangerous weakpoint"
desc = "A suspicious crack runs along the ground. This one makes you feel particuarly uneasy."
@@ -137,3 +194,4 @@
#undef CRACK_PROPAGATION_DELAY
#undef CRACK_TURN_CHANCE
#undef CRACK_DELAY_CHANCE
+#undef CRACK_LENGTH_DEFAULT
diff --git a/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
index be62c9a97f86..b6d3d892896b 100644
--- a/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
+++ b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
@@ -82,8 +82,6 @@
break
if(locate(type) in spread_turf)
continue // Don't spread smoke where there's already smoke!
- for(var/mob/living/smoker in spread_turf)
- smoke_mob(smoker, seconds_per_tick)
var/obj/effect/particle_effect/fluid/smoke/spread_smoke = new type(spread_turf, group, src)
reagents.trans_to(spread_smoke, reagents.total_volume, copy_only = TRUE)
@@ -379,6 +377,8 @@
continue
if(HAS_TRAIT(thing, TRAIT_UNDERFLOOR))
continue
+ if(isliving(thing))
+ continue
reagents.expose(thing, SMOKE_MACHINE, fraction)
reagents.expose(location, SMOKE_MACHINE, fraction)
@@ -394,7 +394,6 @@
var/fraction = (seconds_per_tick SECONDS) / initial(lifetime)
reagents.trans_to(smoker, reagents.total_volume, fraction, methods = SMOKE_MACHINE, copy_only = TRUE)
- reagents.expose(smoker, SMOKE_MACHINE, fraction)
return TRUE
/// Helper to quickly create a cloud of reagent smoke
diff --git a/code/game/objects/effects/particles/plant.dm b/code/game/objects/effects/particles/plant.dm
index 0d495735307b..a728db8b9afa 100644
--- a/code/game/objects/effects/particles/plant.dm
+++ b/code/game/objects/effects/particles/plant.dm
@@ -17,6 +17,10 @@
rotation = 30
spin = generator(GEN_NUM, -20, 20)
+// Subtype for the lavaland flower MODcore
+/particles/pollen/modsuit
+ spawning = 1
+
// Subtype for mushroom spores specific to mushfolk/mushroom people
/particles/pollen/mushroom
icon = 'icons/effects/particles/pollen.dmi'
diff --git a/code/game/objects/effects/spawners/xeno_egg_delivery.dm b/code/game/objects/effects/spawners/xeno_egg_delivery.dm
index eb5bb62df5c5..9ffbd0f41fb4 100644
--- a/code/game/objects/effects/spawners/xeno_egg_delivery.dm
+++ b/code/game/objects/effects/spawners/xeno_egg_delivery.dm
@@ -2,20 +2,25 @@
name = "xeno egg delivery"
icon = 'icons/mob/nonhuman-player/alien.dmi'
icon_state = "egg_growing"
- var/announcement_time = 120 SECONDS
/obj/effect/spawner/xeno_egg_delivery/Initialize(mapload)
. = ..()
var/turf/spawn_turf = get_turf(src)
-
new /obj/structure/alien/egg/delivery(spawn_turf)
new /obj/effect/temp_visual/gravpush(spawn_turf)
playsound(spawn_turf, 'sound/items/party_horn.ogg', 50, TRUE, -1)
+ if(SSticker.HasRoundStarted())
+ return
message_admins("An alien egg has been delivered to [ADMIN_VERBOSEJMP(spawn_turf)].")
log_game("An alien egg has been delivered to [AREACOORD(spawn_turf)]")
- var/message = "Attention [station_name()], we have entrusted you with a research specimen in [get_area_name(spawn_turf, TRUE)]. Remember to follow all safety precautions when dealing with the specimen."
- SSticker.OnRoundstart(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(_addtimer), CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(print_command_report), message), announcement_time))
+
+ var/datum/command_footnote/footnote = new()
+ footnote.message = "We have entrusted your crew with a research specimen in [get_area(src)]. \
+ Remember to follow all safety precautions when dealing with the specimen."
+ footnote.signature = "Central Command"
+
+ GLOB.communications_controller.command_report_footnotes += footnote
/obj/structure/alien/egg/delivery
name = "xenobiological specimen egg"
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index 21df3b187f2c..fd7a3f3058c2 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -401,7 +401,6 @@
/obj/item/verb/move_to_top()
set name = "Move To Top"
- set category = "Object"
set src in oview(1)
if(!isturf(loc) || usr.stat != CONSCIOUS || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED) || anchored)
@@ -822,7 +821,6 @@
/obj/item/verb/verb_pickup()
set src in oview(1)
- set category = "Object"
set name = "Pick up"
if(usr.incapacitated || !Adjacent(usr))
@@ -2003,11 +2001,7 @@
if (!(material_flags & MATERIAL_AFFECT_STATISTICS))
return
- // [0 ~ 1] is fully insulating, (1 ~ 6] maps to (0 ~ 1] and [6 ~ 10] maps to [1 ~ 2]
- // 1.18 and 0.15 here are to allow 6 to map to 1 and 10 to map to 2 and are pulled out of my ass (system in the desmos below)
- // See https://www.desmos.com/calculator/rdbv1x8oty
- var/conductivity = material.get_property(MATERIAL_ELECTRICAL)
- var/siemens_modifier = round(max(0, conductivity - 1) ** 1.18 * 0.15, 0.01)
+ var/siemens_modifier = material.get_property(MATERIAL_INSULATION)
// Cannot use the base formula as it would make any item with glass not conduct electricity
if (siemens_modifier > 1)
siemens_coefficient *= 1 + (siemens_modifier - 1) * multiplier
@@ -2017,30 +2011,15 @@
if (siemens_coefficient == 0)
obj_flags &= ~CONDUCTS_ELECTRICITY
- if (material_flags & MATERIAL_NO_SLOWDOWN)
- return
-
- // Density above 6 adds slowdown, density below 3 can reduce existing slowdown
- var/density = material.get_property(MATERIAL_DENSITY)
- var/slowdown_change = 0
-
- if (density > 6)
- slowdown_change = (density - 6) * MATERIAL_DENSITY_SLOWDOWN * mat_amount / SHEET_MATERIAL_AMOUNT
- else if (density < 3)
- slowdown_change = (3 - density) * -MATERIAL_DENSITY_SLOWDOWN * mat_amount / SHEET_MATERIAL_AMOUNT
-
- // Slowdown cannot be reduced below 0 if the item slows you down, or at all if the item speeds you up
- if (slowdown_change)
- slowdown = max(slowdown >= 0 ? 0 : slowdown, slowdown + slowdown_change * multiplier)
+ if (!(material_flags & MATERIAL_NO_SLOWDOWN))
+ change_material_slowdown(material, mat_amount, multiplier)
/obj/item/remove_single_mat_effect(datum/material/material, mat_amount, multiplier)
. = ..()
if (!(material_flags & MATERIAL_AFFECT_STATISTICS))
return
- var/conductivity = material.get_property(MATERIAL_ELECTRICAL)
- // 0 ~ 1 count as perfect insulators
- var/siemens_modifier = round(max(conductivity - 1, 0) ** 1.18 * 0.15, 0.01)
+ var/siemens_modifier = material.get_property(MATERIAL_INSULATION)
// Cannot use the base formula as it would make any item with glass not conduct electricity
if (siemens_modifier > 1)
siemens_coefficient /= 1 + (siemens_modifier - 1) * multiplier
@@ -2052,16 +2031,24 @@
if (siemens_coefficient > 0 && (initial(obj_flags) & CONDUCTS_ELECTRICITY) && !(obj_flags & CONDUCTS_ELECTRICITY))
obj_flags |= CONDUCTS_ELECTRICITY
- if (material_flags & MATERIAL_NO_SLOWDOWN)
- return
+ if (!(material_flags & MATERIAL_NO_SLOWDOWN))
+ change_material_slowdown(material, mat_amount, multiplier, removing = TRUE)
+/obj/item/proc/change_material_slowdown(datum/material/material, mat_amount, multiplier, removing = FALSE)
+ // Density above 6 adds slowdown, density below 3 can reduce existing slowdown
var/density = material.get_property(MATERIAL_DENSITY)
var/slowdown_change = 0
if (density > 6)
slowdown_change = (density - 6) * MATERIAL_DENSITY_SLOWDOWN * mat_amount / SHEET_MATERIAL_AMOUNT
- else if (density < 3)
- slowdown_change = (3 - density) * -MATERIAL_DENSITY_SLOWDOWN * mat_amount / SHEET_MATERIAL_AMOUNT
+ else if (density < 4)
+ slowdown_change = (4 - density) * -MATERIAL_DENSITY_SLOWDOWN * mat_amount / SHEET_MATERIAL_AMOUNT
+
+ if (!removing)
+ // Slowdown cannot be reduced below 0 if the item slows you down, or at all if the item speeds you up
+ if (slowdown_change)
+ slowdown = max(slowdown >= 0 ? 0 : slowdown, slowdown + slowdown_change * multiplier)
+ return
if (slowdown_change > 0)
slowdown -= slowdown_change * multiplier
@@ -2071,11 +2058,12 @@
/obj/item/finalize_remove_material_effects(list/materials)
. = ..()
+ if (!(material_flags & MATERIAL_AFFECT_STATISTICS) || initial(siemens_coefficient) == 0 || siemens_coefficient != 0)
+ return
// If we were made from an insulator we cannot restore via division
- if (initial(siemens_coefficient) != 0 && siemens_coefficient == 0)
- siemens_coefficient = initial(siemens_coefficient)
- if (siemens_coefficient > 0 && (initial(obj_flags) & CONDUCTS_ELECTRICITY) && !(obj_flags & CONDUCTS_ELECTRICITY))
- obj_flags |= CONDUCTS_ELECTRICITY
+ siemens_coefficient = initial(siemens_coefficient)
+ if (siemens_coefficient > 0 && (initial(obj_flags) & CONDUCTS_ELECTRICITY) && !(obj_flags & CONDUCTS_ELECTRICITY))
+ obj_flags |= CONDUCTS_ELECTRICITY
/obj/item/change_material_strength(datum/material/material, mat_amount, multiplier, remove = FALSE)
var/density = material.get_property(MATERIAL_DENSITY)
diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm
index bceb0478dddc..aca496a96675 100644
--- a/code/game/objects/items/devices/radio/radio.dm
+++ b/code/game/objects/items/devices/radio/radio.dm
@@ -422,6 +422,8 @@
// left hands are odd slots
if (idx && (idx % 2) == (message_mods[RADIO_EXTENSION] == MODE_L_HAND))
return
+ if (message_mods[MODE_TTS_IDENTIFIER])
+ filtered_mods[MODE_TTS_IDENTIFIER] = message_mods[MODE_TTS_IDENTIFIER]
talk_into(speaker, raw_message, spans=spans, language=message_language, message_mods=filtered_mods)
/// Checks if this radio can receive on the given frequency.
diff --git a/code/game/objects/items/devices/taperecorder.dm b/code/game/objects/items/devices/taperecorder.dm
index 912e4ddab952..7e34b98a8742 100644
--- a/code/game/objects/items/devices/taperecorder.dm
+++ b/code/game/objects/items/devices/taperecorder.dm
@@ -127,7 +127,6 @@
/obj/item/taperecorder/verb/ejectverb()
set name = "Eject Tape"
- set category = "Object"
if(!can_use(usr))
balloon_alert(usr, "can't use!")
@@ -135,7 +134,6 @@
if(!mytape)
balloon_alert(usr, "no tape!")
return
-
eject(usr)
@@ -164,7 +162,6 @@
/obj/item/taperecorder/verb/record()
set name = "Start Recording"
- set category = "Object"
if(!can_use(usr))
balloon_alert(usr, "can't use!")
@@ -207,7 +204,6 @@
/obj/item/taperecorder/verb/stop()
set name = "Stop"
- set category = "Object"
if(!can_use(usr))
balloon_alert(usr, "can't use!")
@@ -228,7 +224,6 @@
/obj/item/taperecorder/verb/play()
set name = "Play Tape"
- set category = "Object"
if(!can_use(usr))
balloon_alert(usr, "can't use!")
@@ -301,7 +296,6 @@
/obj/item/taperecorder/verb/print_transcript()
set name = "Print Transcript"
- set category = "Object"
var/list/transcribed_info = mytape.storedinfo
if(!length(transcribed_info))
diff --git a/code/game/objects/items/granters/crafting/pipegun.dm b/code/game/objects/items/granters/crafting/pipegun.dm
index e54439350dab..3b2be509f8d9 100644
--- a/code/game/objects/items/granters/crafting/pipegun.dm
+++ b/code/game/objects/items/granters/crafting/pipegun.dm
@@ -44,3 +44,17 @@
"10u of reactant to open the relic... Is this even real science anymore?",
"Making them all sleep in the cold below? This is a disabler, not a lethal weapon.",
)
+/obj/item/book/granter/crafting_recipe/dusting/detached_ratvarian_repeater
+ name = "memoirs of a former cultist"
+ desc = "A strange-looking book. The cover is warm to the touch."
+ crafting_recipe_types = list(
+ /datum/crafting_recipe/detached_ratvarian_repeater
+ )
+ remarks = list(
+ "It's amazing that they used to be able to mass produce these.",
+ "A welder? Well, that's surprisingly simple.",
+ "The many gears and cogs of the recharging system are making my head spin...",
+ "The tone of the author is devout, almost reverent.",
+ "The designs are all laid out in a circle, resembling a clock.",
+ "With a few modifications, this wouldn't be that heavy...",
+ )
diff --git a/code/game/objects/items/grenades/flashbang.dm b/code/game/objects/items/grenades/flashbang.dm
index 052d01d8b30f..5a916c5f1e45 100644
--- a/code/game/objects/items/grenades/flashbang.dm
+++ b/code/game/objects/items/grenades/flashbang.dm
@@ -23,7 +23,7 @@
if(!.)
return
- var/sweetspot_range = clamp(CEILING(flashbang_range/sweetspot_divider, 1), 0, flashbang_range)
+ var/sweetspot_range = clamp(ceil(flashbang_range/sweetspot_divider), 0, flashbang_range)
set_light(sweetspot_range, sweetspot_range, flashbang_light)
/obj/item/grenade/flashbang/detonate(mob/living/lanced_by)
@@ -53,7 +53,7 @@
return
living_mob.show_message(span_warning("BANG"), MSG_AUDIBLE)
var/distance = get_dist(get_turf(src), turf)
- var/sweetspot_range = clamp(CEILING(flashbang_range/sweetspot_divider, 1), 0, flashbang_range)
+ var/sweetspot_range = clamp(ceil(flashbang_range/sweetspot_divider), 0, flashbang_range)
//Flash
var/attempt_flash = living_mob.flash_act(affect_silicon = 1)
diff --git a/code/game/objects/items/implants/implant.dm b/code/game/objects/items/implants/implant.dm
index c13881fbabdb..5ae3673e8ed5 100644
--- a/code/game/objects/items/implants/implant.dm
+++ b/code/game/objects/items/implants/implant.dm
@@ -6,7 +6,7 @@
icon = 'icons/hud/implants.dmi'
icon_state = "generic" //Shows up as the action button icon
item_flags = ABSTRACT | DROPDEL
- resistance_flags = INDESTRUCTIBLE
+ resistance_flags = INDESTRUCTIBLE | UNACIDABLE
// This gives the user an action button that allows them to activate the implant.
// If the implant needs no action button, then null this out.
// Or, if you want to add a unique action button, then replace this.
@@ -125,7 +125,7 @@
* * silent - unused here
* * special - Set to true if removed by admin panel, should bypass any side effects
*/
-/obj/item/implant/proc/removed(mob/living/source, silent = FALSE, special = 0)
+/obj/item/implant/proc/removed(mob/living/source, silent = FALSE, special = FALSE)
moveToNullspace()
imp_in = null
source.implants -= src
@@ -139,9 +139,9 @@
GLOB.tracked_implants -= src
return TRUE
-/obj/item/implant/Destroy()
+/obj/item/implant/Destroy(force)
if(imp_in)
- removed(imp_in)
+ removed(imp_in, silent = TRUE, special = TRUE)
return ..()
/**
diff --git a/code/game/objects/items/implants/security/implant_chem.dm b/code/game/objects/items/implants/security/implant_chem.dm
index 426604daf11a..07cdf80e009b 100644
--- a/code/game/objects/items/implants/security/implant_chem.dm
+++ b/code/game/objects/items/implants/security/implant_chem.dm
@@ -7,9 +7,14 @@
hud_icon_state = "hud_imp_chem"
/// All possible injection sizes for the implant shown in the prisoner management console.
var/list/implant_sizes = list(1, 5, 10)
+ /// Frequency of radio signal we've been synced to
+ var/frequency
+ /// Code of radio signal we've been synced to
+ var/code
+
implant_info = "Requires filling via syringe while in an implant case. Automatically activates upon implantation. \
- Allows for remote release of reagents directly into implantee's bloodstream."
+ Allows for remote release of reagents directly into implantee's bloodstream. Can be synced with a remote signalling device."
implant_lore = "The Robust Corp MJ-420 Remote Chemical Release Implant is a remotely-controlled \
microcapsule network designed to be filled via syringe while still within an implant case. After implantation, \
@@ -19,7 +24,7 @@
into their bloodstream."
/obj/item/implant/chem/is_shown_on_console(obj/machinery/computer/prisoner/management/console)
- return is_valid_z_level(get_turf(console), get_turf(imp_in))
+ return !frequency && is_valid_z_level(get_turf(console), get_turf(imp_in))
/obj/item/implant/chem/get_management_console_data()
var/list/info_shown = ..()
@@ -58,7 +63,7 @@
/obj/item/implant/chem/implant(mob/living/target, mob/user, silent = FALSE, force = FALSE)
. = ..()
- if(.)
+ if(. && !frequency)
RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_death))
/obj/item/implant/chem/removed(mob/target, silent = FALSE, special = FALSE)
@@ -86,13 +91,25 @@
to_chat(R, span_hear("You hear a faint click from your chest."))
qdel(src)
+/obj/item/implant/chem/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
+ if(istype(tool, /obj/item/assembly/signaler))
+ signaler_sync(tool, user)
+ return ITEM_INTERACT_SUCCESS
+ return ..()
+
+/obj/item/implant/chem/proc/signaler_sync(obj/item/assembly/signaler/syncing_signaler, mob/living/user)
+ to_chat(user, "You sync \the [src] to \the [syncing_signaler]'s code & frequency[frequency ? "" : ", disabling other methods of activation"].")
+ code = syncing_signaler.code
+ SSradio.remove_object(src, frequency)
+ frequency = syncing_signaler.frequency
+ SSradio.add_object(src, frequency, RADIO_SIGNALER)
+
+/obj/item/implant/chem/receive_signal(datum/signal/signal)
+ if(!signal || signal.data["code"] != code)
+ return
+ activate(reagents.total_volume)
/obj/item/implantcase/chem
name = "implant case - 'Remote Chemical'"
desc = "A glass case containing a remote chemical implant."
imp_type = /obj/item/implant/chem
-
-/obj/item/implantcase/chem/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
- if(!istype(tool, /obj/item/reagent_containers/syringe) && imp)
- return NONE
- return tool.interact_with_atom(imp, user, modifiers)
diff --git a/code/game/objects/items/rcd/RHD.dm b/code/game/objects/items/rcd/RHD.dm
index 202c85035588..40f989ee0882 100644
--- a/code/game/objects/items/rcd/RHD.dm
+++ b/code/game/objects/items/rcd/RHD.dm
@@ -177,7 +177,7 @@
/obj/item/construction/update_overlays()
. = ..()
if(has_ammobar)
- var/ratio = CEILING((matter / max_matter) * ammo_sections, 1)
+ var/ratio = ceil((matter / max_matter) * ammo_sections)
if(ratio > 0)
. += "[icon_state]_charge[ratio]"
diff --git a/code/game/objects/items/robot/items/storage.dm b/code/game/objects/items/robot/items/storage.dm
index 8236b58108c2..759638b6b4e8 100644
--- a/code/game/objects/items/robot/items/storage.dm
+++ b/code/game/objects/items/robot/items/storage.dm
@@ -33,7 +33,6 @@
///A right-click verb, for those not using hotkey mode.
/obj/item/borg/apparatus/verb/verb_dropHeld()
- set category = "Object"
set name = "Drop"
if(usr != loc || !stored)
diff --git a/code/game/objects/items/robot/items/tools.dm b/code/game/objects/items/robot/items/tools.dm
index 460e4bef7f2a..41e73a1bf79d 100644
--- a/code/game/objects/items/robot/items/tools.dm
+++ b/code/game/objects/items/robot/items/tools.dm
@@ -241,6 +241,7 @@
tool.item_flags |= ABSTRACT
ADD_TRAIT(tool, TRAIT_NODROP, INNATE_TRAIT)
atoms[reference] = tool
+ tool.toolspeed = initial(tool.toolspeed) - upgraded * 0.3 //and finally assign the upgraded toolspeed, if any.
return tool
/obj/item/borg/cyborg_omnitool/attack_self(mob/user)
@@ -283,8 +284,10 @@
* * upgrade - TRUE/FALSE for upgraded
*/
/obj/item/borg/cyborg_omnitool/proc/set_upgraded(upgrade)
- upgraded = upgraded
-
+ upgraded = upgrade
+ for(var/obj/item/tool_path as anything in atoms)
+ var/obj/item/tool = atoms[tool_path]
+ tool.toolspeed = initial(tool.toolspeed) - upgraded * 0.3
playsound(src, 'sound/items/tools/change_jaws.ogg', 50, TRUE)
/obj/item/borg/cyborg_omnitool/medical
diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm
index ac9779e31aa7..d5a8f9b00178 100644
--- a/code/game/objects/items/robot/robot_upgrades.dm
+++ b/code/game/objects/items/robot/robot_upgrades.dm
@@ -493,6 +493,8 @@
return FALSE
for(var/obj/item/borg/cyborg_omnitool/engineering/omnitool in cyborg.model.modules)
omnitool.set_upgraded(TRUE)
+ for(var/obj/item/weldingtool/largetank/cyborg/welder in cyborg.model.modules)
+ welder.toolspeed = initial(welder.toolspeed) - 0.3
/obj/item/borg/upgrade/engineering_omnitool/deactivate(mob/living/silicon/robot/cyborg, mob/living/user = usr)
. = ..()
@@ -500,6 +502,8 @@
return .
for(var/obj/item/borg/cyborg_omnitool/omnitool in cyborg.model.modules)
omnitool.set_upgraded(FALSE)
+ for(var/obj/item/weldingtool/largetank/cyborg/welder in cyborg.model.modules)
+ welder.toolspeed = initial(welder.toolspeed)
/obj/item/borg/upgrade/defib
name = "medical cyborg defibrillator"
diff --git a/code/game/objects/items/stacks/bscrystal.dm b/code/game/objects/items/stacks/bscrystal.dm
index 64d594b8bafd..d7bf67cf7309 100644
--- a/code/game/objects/items/stacks/bscrystal.dm
+++ b/code/game/objects/items/stacks/bscrystal.dm
@@ -7,7 +7,8 @@
singular_name = "bluespace crystal"
dye_color = DYE_COSMIC
w_class = WEIGHT_CLASS_TINY
- mats_per_unit = list(/datum/material/bluespace=SHEET_MATERIAL_AMOUNT)
+ material_flags = MATERIAL_NO_DESCRIPTORS // Handles in-hand/thrown teleports by itself
+ mats_per_unit = list(/datum/material/bluespace = SHEET_MATERIAL_AMOUNT)
points = 50
refined_type = /obj/item/stack/sheet/bluespace_crystal
scan_state = "rock_bscrystal"
@@ -52,6 +53,9 @@
blink_mob(hit_atom)
use(1)
+/obj/item/stack/ore/bluespace_crystal/attack_self_secondary(mob/user, modifiers)
+ interact(user)
+
//Artificial bluespace crystal, doesn't give you much research.
/obj/item/stack/ore/bluespace_crystal/artificial
name = "artificial bluespace crystal"
@@ -67,12 +71,12 @@
// Polycrystals, aka stacks
/obj/item/stack/sheet/bluespace_crystal
name = "bluespace polycrystal"
- icon = 'icons/obj/stack_objects.dmi'
+ singular_name = "bluespace polycrystal"
+ desc = "A stable polycrystal, made of fused-together bluespace crystals. You could probably break one off."
icon_state = "polycrystal"
inhand_icon_state = null
+ material_flags = MATERIAL_NO_DESCRIPTORS
gulag_valid = TRUE
- singular_name = "bluespace polycrystal"
- desc = "A stable polycrystal, made of fused-together bluespace crystals. You could probably break one off."
mats_per_unit = list(/datum/material/bluespace=SHEET_MATERIAL_AMOUNT)
attack_verb_continuous = list("bluespace polybashes", "bluespace polybatters", "bluespace polybludgeons", "bluespace polythrashes", "bluespace polysmashes")
attack_verb_simple = list("bluespace polybash", "bluespace polybatter", "bluespace polybludgeon", "bluespace polythrash", "bluespace polysmash")
@@ -81,23 +85,21 @@
material_type = /datum/material/bluespace
var/crystal_type = /obj/item/stack/ore/bluespace_crystal/refined
-/obj/item/stack/sheet/bluespace_crystal/attack_self(mob/user)// to prevent the construction menu from ever happening
- to_chat(user, span_warning("You cannot crush the polycrystal in-hand, try breaking one off."))
-
//ATTACK HAND IGNORING PARENT RETURN VALUE
/obj/item/stack/sheet/bluespace_crystal/attack_hand(mob/user, list/modifiers)
- if(user.get_inactive_held_item() == src)
- if(is_zero_amount(delete_if_zero = TRUE))
- return
- var/BC = new crystal_type(src)
- user.put_in_hands(BC)
- use(1)
- if(!amount)
- to_chat(user, span_notice("You break the final crystal off."))
- else
- to_chat(user, span_notice("You break off a crystal."))
+ if(user.get_inactive_held_item() != src)
+ return ..()
+
+ if(is_zero_amount(delete_if_zero = TRUE))
+ return
+
+ var/BC = new crystal_type(src)
+ user.put_in_hands(BC)
+ use(1)
+ if(!amount)
+ to_chat(user, span_notice("You break the final crystal off."))
else
- ..()
+ to_chat(user, span_notice("You break off a crystal."))
/obj/item/stack/sheet/bluespace_crystal/fifty
amount = 50
diff --git a/code/game/objects/items/stacks/rods.dm b/code/game/objects/items/stacks/rods.dm
index 17fd93bc5bfc..d9a5e5c4bcc0 100644
--- a/code/game/objects/items/stacks/rods.dm
+++ b/code/game/objects/items/stacks/rods.dm
@@ -63,7 +63,7 @@ GLOBAL_LIST_INIT(rod_recipes, list ( \
)
AddElement(/datum/element/contextual_screentip_tools, tool_behaviors)
- var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/spear, /datum/crafting_recipe/stunprod, /datum/crafting_recipe/teleprod) // snatcher prod isn't here as a spoopy secret
+ var/static/list/slapcraft_recipe_list = list(/datum/crafting_recipe/spear, /datum/crafting_recipe/stunprod, /datum/crafting_recipe/teleprod, /datum/crafting_recipe/wireprod) // snatcher prod isn't here as a spoopy secret
AddElement(
/datum/element/slapcrafting,\
diff --git a/code/game/objects/items/stacks/sheets/leather.dm b/code/game/objects/items/stacks/sheets/leather.dm
index d4138155da27..83b202bc9110 100644
--- a/code/game/objects/items/stacks/sheets/leather.dm
+++ b/code/game/objects/items/stacks/sheets/leather.dm
@@ -167,9 +167,9 @@ GLOBAL_LIST_INIT(monkey_recipes, list ( \
amount = 5
/obj/item/stack/sheet/animalhide/xeno
- name = "alien hide"
+ name = "alien chitin"
+ singular_name = "alien chitin piece"
desc = "The skin of a terrible creature."
- singular_name = "alien hide piece"
icon_state = "sheet-xeno"
inhand_icon_state = null
merge_type = /obj/item/stack/sheet/animalhide/xeno
@@ -210,16 +210,6 @@ GLOBAL_LIST_INIT(carp_recipes, list ( \
/obj/item/stack/sheet/animalhide/carp/five
amount = 5
-//don't see anywhere else to put these, maybe together they could be used to make the xenos suit?
-/obj/item/stack/sheet/xenochitin
- name = "alien chitin"
- desc = "A piece of the hide of a terrible creature."
- singular_name = "alien hide piece"
- icon = 'icons/mob/nonhuman-player/alien.dmi'
- icon_state = "chitin"
- novariants = TRUE
- merge_type = /obj/item/stack/sheet/xenochitin
-
/obj/item/xenos_claw
name = "alien claw"
desc = "The claw of a terrible creature."
diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm
index 67102a0b7b13..673f26c71a38 100644
--- a/code/game/objects/items/stacks/sheets/sheet_types.dm
+++ b/code/game/objects/items/stacks/sheets/sheet_types.dm
@@ -966,7 +966,7 @@ GLOBAL_LIST_INIT(paperframe_recipes, list(
desc = "Something's bloody meat compressed into a nice solid sheet."
singular_name = "meat sheet"
icon_state = "sheet-meat"
- material_flags = MATERIAL_EFFECTS | MATERIAL_COLOR
+ material_flags = MATERIAL_EFFECTS | MATERIAL_COLOR | MATERIAL_NO_DESCRIPTORS
mats_per_unit = list(/datum/material/meat = SHEET_MATERIAL_AMOUNT)
merge_type = /obj/item/stack/sheet/meat
material_type = /datum/material/meat
@@ -1013,7 +1013,7 @@ GLOBAL_LIST_INIT(pizza_sheet_recipes, list(
desc = "These sheets seem cursed."
singular_name = "haunted sheet"
icon_state = "sheet-meat"
- material_flags = MATERIAL_EFFECTS | MATERIAL_COLOR
+ material_flags = MATERIAL_EFFECTS | MATERIAL_COLOR | MATERIAL_NO_DESCRIPTORS
mats_per_unit = list(/datum/material/hauntium = SHEET_MATERIAL_AMOUNT)
merge_type = /obj/item/stack/sheet/hauntium
material_type = /datum/material/hauntium
diff --git a/code/game/objects/items/stacks/sheets/sheets.dm b/code/game/objects/items/stacks/sheets/sheets.dm
index 862d255133e9..3d2b1f52030f 100644
--- a/code/game/objects/items/stacks/sheets/sheets.dm
+++ b/code/game/objects/items/stacks/sheets/sheets.dm
@@ -56,8 +56,9 @@
if (isnull(prop_value)) // Error?
continue
var/descriptor = property?.get_descriptor(prop_value)
+ var/tooltip_hint = property?.get_tooltip(prop_value)
if (descriptor) // Overriden derivative property?
- material_string += span_tooltip("[property]: [prop_value < 0 ? "-" : ""]\Roman[round(abs(prop_value), 1)]", descriptor)
+ material_string += span_tooltip("[property]: [tooltip_hint]", descriptor)
if (length(material_string))
. += span_info("[capitalize(material.name)] is [english_list(material_string)].")
diff --git a/code/game/objects/items/stacks/telecrystal.dm b/code/game/objects/items/stacks/telecrystal.dm
index dd1b74eb106b..4988a9748a84 100644
--- a/code/game/objects/items/stacks/telecrystal.dm
+++ b/code/game/objects/items/stacks/telecrystal.dm
@@ -1,18 +1,20 @@
/obj/item/stack/telecrystal
name = "telecrystal"
- desc = "It seems to be pulsing with suspiciously enticing energies."
+ desc = "Covered in a web of finely engraved geometrical patterns, pulsing with suspiciously enticing energies."
singular_name = "telecrystal"
- icon = 'icons/obj/stack_objects.dmi'
icon_state = "telecrystal"
dye_color = DYE_SYNDICATE
+ full_w_class = WEIGHT_CLASS_TINY
w_class = WEIGHT_CLASS_TINY
max_amount = 50
item_flags = NOBLUDGEON
merge_type = /obj/item/stack/telecrystal
novariants = FALSE
+ material_type = /datum/material/telecrystal
+ mats_per_unit = list(/datum/material/telecrystal = SHEET_MATERIAL_AMOUNT)
/obj/item/stack/telecrystal/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
- if(interacting_with != user) //You can't go around smacking people with crystals to find out if they have an uplink or not.
+ if(interacting_with != user) // You can't go around smacking people with crystals to find out if they have an uplink or not.
return NONE
for(var/obj/item/implant/uplink/uplink in interacting_with)
@@ -20,14 +22,30 @@
continue
var/datum/component/uplink/hidden_uplink = uplink.GetComponent(/datum/component/uplink)
- if(hidden_uplink)
- hidden_uplink.uplink_handler.add_telecrystals(amount)
- use(amount)
- to_chat(user, span_notice("You press [src] onto yourself and charge your hidden uplink."))
- return ITEM_INTERACT_SUCCESS
+ if(!hidden_uplink)
+ continue
+ hidden_uplink.uplink_handler.add_telecrystals(amount)
+ use(amount)
+ to_chat(user, span_notice("You press [src] onto yourself and charge your hidden uplink."))
+ return ITEM_INTERACT_SUCCESS
+ return ITEM_INTERACT_BLOCKING
/obj/item/stack/telecrystal/five
amount = 5
/obj/item/stack/telecrystal/twenty
amount = 20
+
+/obj/item/stack/sheet/telepolycrystal
+ name = "telelocational podcrystal"
+ singular_name = "telelocational podcrystal"
+ desc = "A \"somewhat\" stable chunk of telecrystal. It lacks the precision-carved tuning channels, making it useless for long-range matter teleportation."
+ icon_state = "telepolycrystal"
+ inhand_icon_state = null
+ full_w_class = WEIGHT_CLASS_TINY
+ w_class = WEIGHT_CLASS_TINY
+ dye_color = DYE_SYNDICATE
+ novariants = TRUE
+ merge_type = /obj/item/stack/sheet/telepolycrystal
+ material_type = /datum/material/telecrystal
+ mats_per_unit = list(/datum/material/telecrystal = SHEET_MATERIAL_AMOUNT)
diff --git a/code/game/objects/items/stacks/tiles/tile_types.dm b/code/game/objects/items/stacks/tiles/tile_types.dm
index 024142e04c44..c8f2c59d7c32 100644
--- a/code/game/objects/items/stacks/tiles/tile_types.dm
+++ b/code/game/objects/items/stacks/tiles/tile_types.dm
@@ -50,7 +50,7 @@
. += span_notice("Use while in your hand to change what type of [src] you want.")
if(throwforce && !is_cyborg) //do not want to divide by zero or show the message to borgs who can't throw
var/damage_value
- switch(CEILING(MAX_LIVING_HEALTH / throwforce, 1)) //throws to crit a human
+ switch(ceil(MAX_LIVING_HEALTH / throwforce)) //throws to crit a human
if(1 to 3)
damage_value = "superb"
if(4 to 6)
diff --git a/code/game/objects/items/tanks/watertank.dm b/code/game/objects/items/tanks/watertank.dm
index 47007dd3a632..23b13ccfef41 100644
--- a/code/game/objects/items/tanks/watertank.dm
+++ b/code/game/objects/items/tanks/watertank.dm
@@ -61,7 +61,6 @@
/obj/item/watertank/verb/toggle_mister_verb()
set name = "Toggle Mister"
- set category = "Object"
toggle_mister(usr)
/obj/item/watertank/proc/make_noz()
diff --git a/code/game/objects/items/tools/engineering/weldingtool.dm b/code/game/objects/items/tools/engineering/weldingtool.dm
index 68e6020b9198..e61fc068e210 100644
--- a/code/game/objects/items/tools/engineering/weldingtool.dm
+++ b/code/game/objects/items/tools/engineering/weldingtool.dm
@@ -86,7 +86,7 @@
. = ..()
if(change_icons)
var/ratio = get_fuel() / max_fuel
- ratio = CEILING(ratio*4, 1) * 25
+ ratio = ceil(ratio*4) * 25
. += "[initial(icon_state)][ratio]"
if(welding)
. += "[initial(icon_state)]-on"
diff --git a/code/game/objects/items/tools/medical/defib.dm b/code/game/objects/items/tools/medical/defib.dm
index 7d02becf965f..2bc9b426f61f 100644
--- a/code/game/objects/items/tools/medical/defib.dm
+++ b/code/game/objects/items/tools/medical/defib.dm
@@ -103,7 +103,7 @@
. += powered_state
if(!QDELETED(cell) && charge_state)
var/ratio = cell.charge / cell.maxcharge
- ratio = CEILING(ratio*4, 1) * 25
+ ratio = ceil(ratio*4) * 25
. += "[charge_state][ratio]"
if(!cell && nocell_state)
. += "[nocell_state]"
@@ -115,8 +115,8 @@
cell = locate(/obj/item/stock_parts/power_store) in contents
update_power()
-/obj/item/defibrillator/ui_action_click()
- INVOKE_ASYNC(src, PROC_REF(toggle_paddles))
+/obj/item/defibrillator/ui_action_click(mob/user, actiontype)
+ INVOKE_ASYNC(src, PROC_REF(toggle_paddles), user)
//ATTACK HAND IGNORING PARENT RETURN VALUE
/obj/item/defibrillator/attack_hand(mob/user, list/modifiers)
@@ -143,7 +143,7 @@
/obj/item/defibrillator/item_interaction(mob/living/user, obj/item/item, list/modifiers)
if(item == paddles)
- toggle_paddles()
+ toggle_paddles(user)
return NONE
if(!istype(item, /obj/item/stock_parts/power_store/cell))
return NONE
@@ -181,15 +181,11 @@
update_power()
-/obj/item/defibrillator/proc/toggle_paddles()
- set name = "Toggle Paddles"
- set category = "Object"
+/obj/item/defibrillator/proc/toggle_paddles(mob/living/user)
on = !on
-
- var/mob/living/carbon/user = usr
if(on)
//Detach the paddles into the user's hands
- if(!usr.put_in_hands(paddles))
+ if(!user.put_in_hands(paddles))
on = FALSE
to_chat(user, span_warning("You need a free hand to hold the paddles!"))
update_power()
diff --git a/code/game/objects/items/weaponry/melee/baton.dm b/code/game/objects/items/weaponry/melee/baton.dm
index 07a40afabdcd..3c5f0a4991ac 100644
--- a/code/game/objects/items/weaponry/melee/baton.dm
+++ b/code/game/objects/items/weaponry/melee/baton.dm
@@ -933,6 +933,7 @@
slot_flags = null
throw_stun_chance = 50 //I think it'd be funny
can_upgrade = FALSE
+ custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 1.15, /datum/material/telecrystal = SHEET_MATERIAL_AMOUNT, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 2)
/obj/item/melee/baton/security/cattleprod/telecrystalprod/baton_effect(mob/living/target, mob/living/user, stun_override, clumsy)
. = ..()
diff --git a/code/game/objects/items/weaponry/melee/misc.dm b/code/game/objects/items/weaponry/melee/misc.dm
index 8bb68ee396e1..11d951f22a9b 100644
--- a/code/game/objects/items/weaponry/melee/misc.dm
+++ b/code/game/objects/items/weaponry/melee/misc.dm
@@ -238,8 +238,10 @@
greyscale_config_worn = /datum/greyscale_config/cleric_mace
greyscale_colors = COLOR_WHITE + COLOR_BROWN
- material_flags = MATERIAL_EFFECTS | MATERIAL_ADD_PREFIX | MATERIAL_GREYSCALE | MATERIAL_AFFECT_STATISTICS //Material type changes the prefix as well as the color.
- custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 4.5, /datum/material/wood = SHEET_MATERIAL_AMOUNT * 1.5) //Defaults to an Iron Mace.
+ material_flags = MATERIAL_EFFECTS | MATERIAL_ADD_PREFIX | MATERIAL_GREYSCALE | MATERIAL_AFFECT_STATISTICS
+ // Defaults to an iron head, wooden handle mace
+ custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 4.5, /datum/material/wood = SHEET_MATERIAL_AMOUNT * 1.5)
+ material_slots = list(/datum/material_slot/weapon_head/mace = /datum/material/iron, /datum/material_slot/handle = /datum/material/wood)
slot_flags = ITEM_SLOT_BELT
force = 16
w_class = WEIGHT_CLASS_BULKY
@@ -250,31 +252,28 @@
attack_verb_continuous = list("smacks", "strikes", "cracks", "beats")
attack_verb_simple = list("smack", "strike", "crack", "beat")
-///Cleric maces are made of two custom materials: one is handle, and the other is the mace itself.
-/obj/item/melee/cleric_mace/get_material_multiplier(datum/material/custom_material, list/materials, index)
- if(length(materials) <= 1)
- return 1.2
- if(index == 1)
- return 1
- else
- return 0.3
-
+// It only inherits the name of the main material it's made of. The secondary is in the description.
/obj/item/melee/cleric_mace/get_material_prefixes(list/materials)
- var/datum/material/material = materials[1]
- return material.name //It only inherits the name of the main material it's made of. The secondary is in the description.
+ var/datum/material/material = get_material_from_slot(/datum/material_slot/weapon_head)
+ return material?.name
/obj/item/melee/cleric_mace/finalize_material_effects(list/materials)
. = ..()
- if(length(materials) == 1)
- return
- var/datum/material/material = materials[2]
- desc = "[initial(desc)] Its handle is made of [material.name]."
+ var/datum/material/material = get_material_from_slot(/datum/material_slot/handle)
+ if (material)
+ desc = "[initial(desc)] Its handle is made of [material.name]."
/obj/item/melee/cleric_mace/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
+ // Don't bring a...mace to a gunfight, and also you aren't going to really block someone full body tackling you with a mace.
+ // Or a road roller, if one happened to hit you.
if(attack_type == PROJECTILE_ATTACK || attack_type == LEAP_ATTACK || attack_type == OVERWHELMING_ATTACK)
- final_block_chance = 0 //Don't bring a...mace to a gunfight, and also you aren't going to really block someone full body tackling you with a mace. Or a road roller, if one happened to hit you.
+ final_block_chance = 0
return ..()
+/datum/material_slot/weapon_head/mace
+ name = "mace head"
+ material_amount = 3
+
/obj/item/sord
name = "\improper SORD"
desc = "This thing is so unspeakably shitty you are having a hard time even holding it."
diff --git a/code/game/objects/items/weaponry/melee/spear.dm b/code/game/objects/items/weaponry/melee/spear.dm
index 00035afbc596..c3a87f4f0d63 100644
--- a/code/game/objects/items/weaponry/melee/spear.dm
+++ b/code/game/objects/items/weaponry/melee/spear.dm
@@ -1,4 +1,5 @@
-//spears
+#define SPEAR_CUSTOM_TIP_PREFIX "spearblank"
+
/obj/item/spear
name = "spear"
desc = "A haphazardly-constructed yet still deadly weapon of ancient design."
@@ -17,6 +18,7 @@
embed_type = /datum/embedding/spear
armour_penetration = 5
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 0.65, /datum/material/glass = SHEET_MATERIAL_AMOUNT * 1.15)
+ material_slots = list(/datum/material_slot/weapon_head/speartip = /datum/material/glass, /datum/material_slot/handle/spear = /datum/material/iron)
hitsound = 'sound/items/weapons/bladeslice.ogg'
attack_verb_continuous = list("attacks", "pokes", "jabs", "tears", "lacerates", "gores")
attack_verb_simple = list("attack", "poke", "jab", "tear", "lacerate", "gore")
@@ -25,7 +27,7 @@
armor_type = /datum/armor/item_spear
wound_bonus = -15
exposed_wound_bonus = 15
- material_flags = MATERIAL_EFFECTS
+ material_flags = MATERIAL_EFFECTS | MATERIAL_AFFECT_STATISTICS
/// The icon prefix for this flavor of spear
var/icon_prefix = "spearglass"
/// How much damage to do unwielded
@@ -36,8 +38,6 @@
var/improvised_construction = TRUE
/// What is left over when a spear breaks
var/spear_leftovers = /obj/item/stack/rods
- /// Material type from which our tip is made
- var/tip_mat_type = null
/// What pike do we construct if someone kills themselves with us?
var/pike_type = /obj/structure/headpike
@@ -84,6 +84,10 @@
/obj/item/spear/update_icon_state()
icon_state = "[icon_prefix]0"
+ if (icon_prefix == SPEAR_CUSTOM_TIP_PREFIX)
+ worn_icon_state = "spearglass0"
+ else
+ worn_icon_state = null
return ..()
/obj/item/spear/suicide_act(mob/living/carbon/user)
@@ -107,87 +111,79 @@
var/obj/item/stack/rods/rod = locate() in components
if (rod)
spear_leftovers = rod.type
+ set_material_slot(/datum/material_slot/handle/spear, rod.get_master_material())
var/obj/item/shard/tip = locate() in components
if (!tip)
return ..()
var/datum/material/tip_material = tip.get_master_material()
- // For master material effects. As on_craft_completion is ran before set_custom_materials, this will allow the speartip to be treated as our master material no matter what
- tip_mat_type = tip_material.id
- switch (tip_mat_type)
+ set_material_slot(/datum/material_slot/weapon_head/speartip, tip_material)
+ return ..()
+
+/obj/item/spear/set_material_slot(slot_type, new_material)
+ . = ..()
+ if (slot_type != /datum/material_slot/weapon_head/speartip)
+ return
+
+ if (istype(new_material, /datum/material))
+ var/datum/material/as_material = new_material
+ new_material = as_material.type
+
+ switch (new_material)
if (/datum/material/alloy/plasmaglass)
icon_prefix = "spearplasma"
if (/datum/material/alloy/titaniumglass)
icon_prefix = "speartitanium"
if (/datum/material/alloy/plastitaniumglass)
icon_prefix = "spearplastitanium"
- update_appearance()
- return ..()
+ else
+ icon_prefix = SPEAR_CUSTOM_TIP_PREFIX
-/obj/item/spear/get_master_material()
- if (tip_mat_type && custom_materials[tip_mat_type])
- return SSmaterials.get_material(tip_mat_type)
- return ..()
-
-/obj/item/spear/apply_main_material_effects(datum/material/main_material, amount, multiplier)
- . = ..()
- var/density = main_material.get_property(MATERIAL_DENSITY)
- var/hardness = main_material.get_property(MATERIAL_HARDNESS)
- // If a spear is too hard its unwieldy, if it is too light it doesn't have enough weight behind it
- var/force_change = (hardness - 4) - max(0, density - 4) - max(0, 4 - density) * 2
- force_unwielded += force_change
- force_wielded += force_change
- force = force_unwielded
- throwforce += force_change
- wound_bonus += force_change * 5
- modify_max_integrity(max_integrity + (hardness - 4) * 10)
- throw_range += (hardness - 4) - (density - 4) * 2
- throw_speed += floor(((hardness - 4) - (density - 4) * 2) / 2)
- // These try to keep parity with titanium/plastitanium spears as armorpen boost was exclusive to them
- armour_penetration += MATERIAL_PROPERTY_DIVERGENCE(hardness, 4, 6) * 5
- exposed_wound_bonus += (MATERIAL_PROPERTY_DIVERGENCE(hardness, 4, 6) - (density - 4)) * 5
AddComponent(/datum/component/two_handed, \
- force_unwielded = force_unwielded, \
- force_wielded = force_wielded, \
icon_wielded = "[icon_prefix]1", \
wield_callback = CALLBACK(src, PROC_REF(on_wield)), \
unwield_callback = CALLBACK(src, PROC_REF(on_unwield)), \
)
+ update_appearance()
-/obj/item/spear/remove_main_material_effects(datum/material/main_material, amount, multiplier)
+/obj/item/spear/finalize_material_effects(list/materials)
. = ..()
- var/density = main_material.get_property(MATERIAL_DENSITY)
- var/hardness = main_material.get_property(MATERIAL_HARDNESS)
- var/force_change = (hardness - 4) - (density - 4)
- force_unwielded -= force_change
- force_wielded -= force_change
- force = force_unwielded
- throwforce -= force_change
- wound_bonus -= force_change * 5
- modify_max_integrity(max_integrity - (hardness - 4) * 10)
- throw_range -= (hardness - 4) - (density - 4) * 2
- throw_speed -= floor(((hardness - 4) - (density - 4) * 2) / 2)
- // These try to keep parity with titanium/plastitanium spears as armorpen boost was exclusive to them
- armour_penetration -= MATERIAL_PROPERTY_DIVERGENCE(hardness, 4, 6) * 5
- exposed_wound_bonus -= (MATERIAL_PROPERTY_DIVERGENCE(hardness, 4, 6) - (density - 4)) * 5
- AddComponent(/datum/component/two_handed, \
- force_unwielded = force_unwielded, \
- force_wielded = force_wielded, \
- icon_wielded = "[icon_prefix]1", \
- wield_callback = CALLBACK(src, PROC_REF(on_wield)), \
- unwield_callback = CALLBACK(src, PROC_REF(on_unwield)), \
- )
+ update_appearance()
+
+/obj/item/spear/update_overlays()
+ . = ..()
+ if (icon_prefix != SPEAR_CUSTOM_TIP_PREFIX)
+ return
+ var/datum/material/tip_material = get_master_material()
+ var/mutable_appearance/tip_overlay = mutable_appearance(icon, "speartip", appearance_flags = KEEP_APART | RESET_COLOR)
+ tip_overlay.color = tip_material.color
+ . += tip_overlay
+
+/obj/item/spear/separate_worn_overlays(mutable_appearance/standing, mutable_appearance/draw_target, isinhands, icon_file)
+ . = ..()
+ if (icon_prefix != SPEAR_CUSTOM_TIP_PREFIX || !isinhands)
+ return
+ var/datum/material/tip_material = get_master_material()
+ var/mutable_appearance/tip_overlay = mutable_appearance(icon_file, "speartip[HAS_TRAIT(src, TRAIT_WIELDED)]", appearance_flags = RESET_COLOR)
+ tip_overlay.color = tip_material.color
+ . += tip_overlay
+
+/obj/item/spear/get_master_material()
+ var/datum/material/tip_material = get_material_from_slot(/datum/material_slot/weapon_head/speartip)
+ if (!tip_material)
+ return ..()
+ return custom_materials[tip_material] ? tip_material : ..()
/obj/item/spear/afterattack(atom/target, mob/user, list/modifiers, list/attack_modifiers)
- if(improvised_construction)
+ if(improvised_construction && !QDELETED(src))
take_damage(force / 2, sound_effect = FALSE)
/obj/item/spear/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
. = ..()
if (.) //spear was caught
return
- if(improvised_construction)
+ if(improvised_construction && !QDELETED(src))
take_damage(throwforce / 2, sound_effect = FALSE)
/obj/item/spear/atom_destruction(damage_flag)
@@ -197,6 +193,10 @@
loc.balloon_alert(loc, "spear broken!")
return ..()
+/obj/item/spear/get_material_prefixes(list/materials)
+ var/datum/material/material = get_material_from_slot(/datum/material_slot/weapon_head/speartip)
+ return material?.name
+
/obj/item/spear/proc/on_wield(obj/item/source, mob/living/carbon/user)
reach = 1
armour_penetration *= 2
@@ -205,6 +205,111 @@
reach = 2
armour_penetration /= 2
+/datum/material_slot/weapon_head/speartip
+ name = "tip"
+ material_amount = 1.75
+
+/datum/material_slot/weapon_head/speartip/on_applied(obj/item/spear/target, datum/material/material, amount, multiplier)
+ . = ..()
+ if (!(target.material_flags & MATERIAL_AFFECT_STATISTICS))
+ return FALSE
+
+ var/density = material.get_property(MATERIAL_DENSITY)
+ var/hardness = material.get_property(MATERIAL_HARDNESS)
+ // If a spear is too hard its unwieldy, if it is too light it doesn't have enough weight behind it
+ var/material_effect = (hardness - 4) - max(0, density - 4) - max(0, 4 - density) * 2
+ target.wound_bonus += material_effect * 5
+ // These try to keep parity with titanium/plastitanium spears as armorpen boost was exclusive to them
+ target.armour_penetration += MATERIAL_PROPERTY_DIVERGENCE(hardness, 4, 6) * 5
+ target.exposed_wound_bonus += (MATERIAL_PROPERTY_DIVERGENCE(hardness, 4, 6) - (density - 4)) * 5
+ return FALSE
+
+/datum/material_slot/weapon_head/spearhead/on_removed(obj/item/spear/target, datum/material/material, amount, multiplier)
+ . = ..()
+ if (!(target.material_flags & MATERIAL_AFFECT_STATISTICS))
+ return FALSE
+
+ var/density = material.get_property(MATERIAL_DENSITY)
+ var/hardness = material.get_property(MATERIAL_HARDNESS)
+ var/material_effect = (hardness - 4) - max(0, density - 4) - max(0, 4 - density) * 2
+ target.wound_bonus -= material_effect * 5
+ target.armour_penetration -= MATERIAL_PROPERTY_DIVERGENCE(hardness, 4, 6) * 5
+ target.exposed_wound_bonus -= (MATERIAL_PROPERTY_DIVERGENCE(hardness, 4, 6) - (density - 4)) * 5
+ return FALSE
+
+/datum/material_slot/handle/spear
+
+/datum/material_slot/handle/spear/on_applied(obj/item/target, datum/material/material, amount, multiplier)
+ . = ..()
+ if (!(target.material_flags & MATERIAL_AFFECT_STATISTICS))
+ return FALSE
+
+ var/density = material.get_property(MATERIAL_DENSITY)
+ var/hardness = material.get_property(MATERIAL_HARDNESS)
+ target.throw_range += (hardness - 4) - (density - 4) * 2
+ target.throw_speed += floor((hardness - 4) / 2) - (density - 4) * 2
+ return FALSE
+
+/datum/material_slot/handle/spear/on_removed(obj/item/target, datum/material/material, amount, multiplier)
+ . = ..()
+ if (!(target.material_flags & MATERIAL_AFFECT_STATISTICS))
+ return FALSE
+
+ var/density = material.get_property(MATERIAL_DENSITY)
+ var/hardness = material.get_property(MATERIAL_HARDNESS)
+ target.throw_range -= (hardness - 4) - (density - 4) * 2
+ target.throw_speed -= floor((hardness - 4) / 2) - (density - 4) * 2
+ return FALSE
+
+/obj/item/wireprod
+ name = "wireprod"
+ desc = "A metal rod with some wire attached to one of the ends, waiting for something sharp."
+ icon = 'icons/obj/weapons/spear.dmi'
+ icon_state = "wireprod"
+ inhand_icon_state = "spearblank0"
+ lefthand_file = 'icons/mob/inhands/weapons/polearms_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/weapons/polearms_righthand.dmi'
+ icon_angle = -45
+ force = 5
+ w_class = WEIGHT_CLASS_BULKY
+ custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 0.65, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 1.5)
+ attack_verb_continuous = list("attacks", "pokes", "jabs", "tears", "lacerates", "gores")
+ attack_verb_simple = list("attack", "poke", "jab", "tear", "lacerate", "gore")
+ material_flags = MATERIAL_EFFECTS | MATERIAL_AFFECT_STATISTICS
+
+/obj/item/wireprod/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
+ var/datum/material/shard_mat = null
+ if (istype(tool, /obj/item/shard))
+ shard_mat = tool.get_master_material()
+ else if (istype(tool, /obj/item/stack))
+ shard_mat = tool.get_master_material()
+ if (!(shard_mat.mat_flags & MATERIAL_CLASS_CRYSTAL))
+ shard_mat = null
+
+ if (!shard_mat)
+ return NONE
+
+ var/obj/item/spear/spear = new(drop_location())
+ var/datum/material/rod_material = get_master_material()
+ spear.material_flags |= MATERIAL_ADD_PREFIX
+ spear.set_material_slot(/datum/material_slot/handle/spear, get_master_material())
+ spear.set_material_slot(/datum/material_slot/weapon_head/speartip, shard_mat)
+ spear.set_custom_materials(list((rod_material) = custom_materials[rod_material], (shard_mat) = tool.custom_materials[shard_mat]))
+ to_chat(user, span_notice("You attach [tool] to [src]'s tip."))
+
+ if (istype(tool, /obj/item/stack))
+ var/obj/item/stack/stack = tool
+ stack.use(1)
+ else
+ qdel(tool)
+
+ var/was_holding = user.get_held_index_of_item(src)
+ qdel(src)
+ if (was_holding)
+ user.put_in_hands(spear)
+
+#undef SPEAR_CUSTOM_TIP_PREFIX
+
/obj/item/spear/explosive
name = "explosive lance"
icon_state = "spearbomb0"
@@ -337,6 +442,7 @@
righthand_file = 'icons/mob/inhands/weapons/polearms_righthand.dmi'
demolition_mod = 0.5
resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
+ material_flags = MATERIAL_EFFECTS
force = 13
throwforce = 23
throw_range = 9
@@ -350,7 +456,9 @@
custom_materials = list(
/datum/material/iron = SHEET_MATERIAL_AMOUNT * 42,
/datum/material/alloy/plasteel = SHEET_MATERIAL_AMOUNT * 15,
- /datum/material/titanium = SHEET_MATERIAL_AMOUNT * 5)
+ /datum/material/titanium = SHEET_MATERIAL_AMOUNT * 5,
+ )
+ material_slots = list(/datum/material_slot/weapon_head/speartip = /datum/material/titanium, /datum/material_slot/handle/spear = /datum/material/alloy/plasteel)
/obj/item/spear/dragonator/Initialize(mapload)
. = ..()
@@ -366,6 +474,7 @@
icon_state = "speardragonraw0"
icon_prefix = "speardragonraw"
base_icon_state = "speardragonraw"
+ material_flags = MATERIAL_EFFECTS
demolition_mod = 0.5
wound_bonus = 0
exposed_wound_bonus = 0
@@ -375,10 +484,14 @@
custom_materials = list(
/datum/material/iron = SHEET_MATERIAL_AMOUNT * 42,
/datum/material/alloy/plasteel = SHEET_MATERIAL_AMOUNT * 15,
- /datum/material/titanium = SHEET_MATERIAL_AMOUNT * 5)
+ /datum/material/titanium = SHEET_MATERIAL_AMOUNT * 5,
+ )
+ material_slots = list(/datum/material_slot/weapon_head/speartip = /datum/material/titanium, /datum/material_slot/handle/spear = /datum/material/alloy/plasteel)
/obj/item/spear/dragonator_untreated/fire_act(exposed_temperature, exposed_volume)
var/obj/item/spear/dragonator/dragonator = new(loc)
+ dragonator.set_material_slots(material_slots)
+ dragonator.set_custom_materials(custom_materials.Copy())
playsound(dragonator.loc, 'sound/effects/magic/staff_change.ogg',5)
qdel(src)
@@ -394,6 +507,7 @@
throwforce = 22
armour_penetration = 20 //Enhanced armor piercing
custom_materials = list(/datum/material/bone = SHEET_MATERIAL_AMOUNT * 4)
+ material_slots = list(/datum/material_slot/weapon_head/speartip = /datum/material/bone, /datum/material_slot/handle/spear = /datum/material/bone)
force_unwielded = 12
force_wielded = 20
spear_leftovers = /obj/item/stack/sheet/bone
@@ -419,6 +533,7 @@
throwforce = 23 //Better to throw
custom_materials = list(/datum/material/bamboo = SHEET_MATERIAL_AMOUNT * 25)
+ material_slots = list(/datum/material_slot/weapon_head/speartip = /datum/material/bamboo, /datum/material_slot/handle/spear = /datum/material/bamboo)
spear_leftovers = /obj/item/stack/sheet/mineral/bamboo
pike_type = /obj/structure/headpike/bamboo
@@ -445,11 +560,12 @@
attack_verb_simple = list("attack", "poke", "jab", "tear", "gore", "lance")
throwforce = 24
embed_type = null //no embedding
-
+ material_flags = MATERIAL_EFFECTS
custom_materials = list(
/datum/material/diamond = HALF_SHEET_MATERIAL_AMOUNT,
/datum/material/alloy/plastitaniumglass = SHEET_MATERIAL_AMOUNT,
)
+ material_slots = list(/datum/material_slot/weapon_head/speartip = /datum/material/diamond, /datum/material_slot/handle/spear = /datum/material/alloy/plastitaniumglass)
action_slots = ITEM_SLOT_HANDS
actions_types = list(/datum/action/item_action/skybulge)
improvised_construction = FALSE
diff --git a/code/game/objects/items/weaponry/shields.dm b/code/game/objects/items/weaponry/shields.dm
index 5c3211b4f9af..c2fa89a06b8f 100644
--- a/code/game/objects/items/weaponry/shields.dm
+++ b/code/game/objects/items/weaponry/shields.dm
@@ -43,7 +43,6 @@
/obj/item/shield/Initialize(mapload)
. = ..()
AddElement(/datum/element/disarm_attack)
- AddElement(/datum/element/cuffable_item) //I mean, it has a closed handle, right?
/obj/item/shield/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
var/effective_block_chance = final_block_chance
diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm
index 3e572f7fb2e5..167ca4221090 100644
--- a/code/game/objects/objs.dm
+++ b/code/game/objects/objs.dm
@@ -330,15 +330,13 @@ GLOBAL_LIST_EMPTY(objects_by_id_tag)
/obj/apply_single_mat_effect(datum/material/material, mat_amount, multiplier)
. = ..()
- if(!(material_flags & MATERIAL_AFFECT_STATISTICS))
- return
- change_material_strength(material, mat_amount, multiplier)
+ if(material_flags & MATERIAL_AFFECT_STATISTICS)
+ change_material_strength(material, mat_amount, multiplier)
/obj/remove_single_mat_effect(datum/material/material, mat_amount, multiplier)
. = ..()
- if(!(material_flags & MATERIAL_AFFECT_STATISTICS))
- return
- change_material_strength(material, mat_amount, multiplier, remove = TRUE)
+ if(material_flags & MATERIAL_AFFECT_STATISTICS)
+ change_material_strength(material, mat_amount, multiplier, remove = TRUE)
/// Changes force and throwforce of an item based on its properties. Split into a separate proc as to allow items to change theirs based on sharpness and behavior
/obj/proc/change_material_strength(datum/material/material, mat_amount, multiplier, remove = FALSE)
diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm
index e8fa4a030ff1..38da438155ab 100644
--- a/code/game/objects/structures/crates_lockers/closets.dm
+++ b/code/game/objects/structures/crates_lockers/closets.dm
@@ -1026,9 +1026,8 @@ GLOBAL_LIST_EMPTY(roundstart_station_closets)
return ITEM_INTERACT_BLOCKING
/obj/structure/closet/verb/verb_toggleopen()
- set src in view(1)
- set category = "Object"
set name = "Toggle Open"
+ set src in view(1)
if(!usr.can_perform_action(src) || !isturf(loc))
return
diff --git a/code/game/objects/structures/lavaland/ore_vent.dm b/code/game/objects/structures/lavaland/ore_vent.dm
index 4870d33304e8..f4ef87a2c092 100644
--- a/code/game/objects/structures/lavaland/ore_vent.dm
+++ b/code/game/objects/structures/lavaland/ore_vent.dm
@@ -54,12 +54,21 @@
/mob/living/basic/mining/brimdemon,
/mob/living/basic/mining/bileworm,
)
- ///What items can be used to scan a vent?
+ /// What items can be used to scan a vent?
var/static/list/scanning_equipment = list(
/obj/item/t_scanner/adv_mining_scanner,
/obj/item/mining_scanner,
)
+ /// Turf above us that we're currently tracking, stored in case of movement jank
+ var/turf/tracked_turf = null
+ /// Visual overlay for our vent tap
+ var/obj/effect/abstract/vent_visual = null
+ /// Underlay visual for when we turn transparent
+ var/obj/effect/abstract/top_bit = null
+ /// Are we currently hiding our top half because someone is above us?
+ var/trimmed = FALSE
+
/// What base icon_state do we use for this vent's boulders?
var/boulder_icon_state = "boulder"
/// Percent chance that this vent will produce an artifact boulder.
@@ -82,13 +91,13 @@
))
if(tapped)
SSore_generation.processed_vents += src
- icon_state = icon_state_tapped
update_appearance(UPDATE_ICON_STATE)
- add_overlay(mutable_appearance('icons/obj/mining_zones/terrain.dmi', "well", ABOVE_MOB_LAYER))
+ add_tapped_visual()
RegisterSignal(src, COMSIG_SPAWNER_SPAWNED_DEFAULT, PROC_REF(anti_cheese))
RegisterSignal(src, COMSIG_SPAWNER_SPAWNED, PROC_REF(log_mob_spawned))
AddElement(/datum/element/give_turf_traits, string_list(list(TRAIT_NO_TERRAFORM)))
+ set_turf_tracking()
return ..()
/obj/structure/ore_vent/Destroy()
@@ -286,7 +295,6 @@
)
COOLDOWN_START(src, wave_cooldown, wave_timer)
addtimer(CALLBACK(src, PROC_REF(handle_wave_conclusion)), wave_timer)
- icon_state = icon_state_tapped
update_appearance(UPDATE_ICON_STATE)
/**
@@ -321,7 +329,6 @@
*/
/obj/structure/ore_vent/proc/initiate_wave_loss(loss_message)
visible_message(span_danger(loss_message))
- icon_state = base_icon_state
update_appearance(UPDATE_ICON_STATE)
reset_drone(success = FALSE)
@@ -335,23 +342,113 @@
log_game("Ore vent [key_name_and_tag(src)] was tapped")
SSblackbox.record_feedback("tally", "ore_vent_completed", 1, type)
balloon_alert_to_viewers("vent tapped!")
- icon_state = icon_state_tapped
+
update_appearance(UPDATE_ICON_STATE)
+ add_tapped_visual()
qdel(GetComponent(/datum/component/gps))
-
- if(!forced)
- for(var/mob/living/miner in range(7, src)) //Give the miners who are near the vent points and xp.
- var/obj/item/card/id/user_id_card = miner.get_idcard(TRUE)
- if(miner.stat <= SOFT_CRIT)
- miner.mind?.adjust_experience(/datum/skill/mining, MINING_SKILL_BOULDER_SIZE_XP * boulder_size)
- if(!user_id_card)
- continue
- var/point_reward_val = (MINER_POINT_MULTIPLIER * boulder_size) - MINER_POINT_MULTIPLIER // We remove the base value of discovering the vent
- if(user_id_card.registered_account)
- user_id_card.registered_account.mining_points += point_reward_val
- user_id_card.registered_account.bank_card_talk("You have been awarded [point_reward_val] mining points for your efforts.")
reset_drone(success = TRUE)
- add_overlay(mutable_appearance('icons/obj/mining_zones/terrain.dmi', "well", ABOVE_MOB_LAYER))
+
+ if(forced)
+ return
+
+ for(var/mob/living/miner in range(7, src)) //Give the miners who are near the vent points and xp.
+ var/obj/item/card/id/user_id_card = miner.get_idcard(TRUE)
+ if(miner.stat <= SOFT_CRIT)
+ miner.mind?.adjust_experience(/datum/skill/mining, MINING_SKILL_BOULDER_SIZE_XP * boulder_size)
+ if(!user_id_card)
+ continue
+ var/point_reward_val = (MINER_POINT_MULTIPLIER * boulder_size) - MINER_POINT_MULTIPLIER // We remove the base value of discovering the vent
+ if(user_id_card.registered_account)
+ user_id_card.registered_account.mining_points += point_reward_val
+ user_id_card.registered_account.bank_card_talk("You have been awarded [point_reward_val] mining points for your efforts.")
+
+/obj/structure/ore_vent/proc/add_tapped_visual()
+ if (vent_visual)
+ vis_contents |= vent_visual
+ return
+
+ vent_visual = new(src)
+ vent_visual.icon = 'icons/obj/mining_zones/terrain.dmi'
+ vent_visual.icon_state = "well"
+ vent_visual.layer = ABOVE_MOB_LAYER
+ vent_visual.vis_flags = VIS_INHERIT_PLANE | VIS_INHERIT_ID
+ vis_contents += vent_visual
+
+/obj/structure/ore_vent/update_icon_state()
+ if (tapped)
+ icon_state = "[base_icon_state]_active"
+ else
+ icon_state = base_icon_state
+ if (trimmed)
+ icon_state = "[icon_state]_trimmed"
+ return ..()
+
+/obj/structure/ore_vent/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ set_turf_tracking()
+
+/// Update the turf which we track for mobs entering/exiting
+/obj/structure/ore_vent/proc/set_turf_tracking()
+ if (tracked_turf)
+ UnregisterSignal(tracked_turf, list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_EXITED))
+
+ tracked_turf = locate(x, y + 1, z)
+ if (!isnull(tracked_turf))
+ RegisterSignal(tracked_turf, COMSIG_ATOM_ENTERED, PROC_REF(on_entered))
+ RegisterSignal(tracked_turf, COMSIG_ATOM_EXITED, PROC_REF(on_exited))
+
+/obj/structure/ore_vent/proc/on_entered(atom/source, atom/movable/entered)
+ SIGNAL_HANDLER
+
+ if (!isliving(entered))
+ return
+
+ if (!trimmed)
+ trim_vent()
+
+/obj/structure/ore_vent/proc/on_exited(atom/source, atom/movable/exited, direction)
+ SIGNAL_HANDLER
+
+ if (!isliving(exited) || !isnull(locate(/mob/living) in tracked_turf))
+ return
+
+ if (trimmed)
+ untrim_vent()
+
+/// Turn the top part of the vent transparent and clickthrough
+/obj/structure/ore_vent/proc/trim_vent()
+ trimmed = TRUE
+ update_appearance(UPDATE_ICON_STATE)
+
+ if (vent_visual)
+ vent_visual.vis_flags &= ~VIS_INHERIT_ID
+ vent_visual.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+
+ if (!top_bit)
+ top_bit = new(src)
+ top_bit.icon = 'icons/obj/mining_zones/terrain.dmi'
+ // Because its transparent we can't just underlay the full thing
+ top_bit.icon_state = "[base_icon_state][tapped ? "_active" : ""]_trimmed_top"
+ top_bit.layer = ABOVE_MOB_LAYER
+ top_bit.vis_flags = VIS_INHERIT_PLANE | VIS_INHERIT_LAYER | VIS_UNDERLAY
+ top_bit.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+
+ vis_contents |= top_bit
+ animate(src, 0.5 SECONDS, alpha = 128)
+
+/// Revert the transparency of the top half of the vent
+/obj/structure/ore_vent/proc/untrim_vent()
+ trimmed = FALSE
+ update_appearance(UPDATE_ICON_STATE)
+
+ if (vent_visual)
+ vent_visual.mouse_opacity = MOUSE_OPACITY_ICON
+ vent_visual.vis_flags |= VIS_INHERIT_ID
+
+ if (top_bit)
+ vis_contents -= top_bit
+
+ animate(src, 0.5 SECONDS, alpha = 255)
/**
* Sends our node back to base and cleans up after the reference
@@ -420,12 +517,13 @@
*/
/obj/structure/ore_vent/proc/add_mineral_overlays()
if(mineral_breakdown.len && !discovered)
- var/obj/effect/temp_visual/mining_overlay/vent/new_mat = new /obj/effect/temp_visual/mining_overlay/vent(drop_location())
- new_mat.icon_state = "unknown"
+ var/atom/movable/flick_visual/visual = flick_overlay_view(mutable_appearance('icons/effects/vent_overlays.dmi', "unknown"), 4.5 SECONDS)
+ animate(visual, alpha = 0, time = 4.5 SECONDS, easing = CIRCULAR_EASING|EASE_IN)
return
+
for(var/datum/material/selected_mat as anything in mineral_breakdown)
- var/obj/effect/temp_visual/mining_overlay/vent/new_mat = new /obj/effect/temp_visual/mining_overlay/vent(drop_location())
- new_mat.icon_state = selected_mat.name
+ var/atom/movable/flick_visual/visual = flick_overlay_view(mutable_appearance('icons/effects/vent_overlays.dmi', selected_mat.name), 4.5 SECONDS)
+ animate(visual, alpha = 0, time = 4.5 SECONDS, easing = CIRCULAR_EASING|EASE_IN)
/**
* Here is where we handle producing a new boulder, based on the qualities of this ore vent.
@@ -562,7 +660,7 @@
/obj/structure/ore_vent/random/icebox //The one that shows up on the top level of icebox
icon_state = "ore_vent_ice"
- icon_state_tapped = "ore_vent_ice_active"
+ base_icon_state = "ore_vent_ice"
defending_mobs = list(
/mob/living/basic/mining/lobstrosity,
/mob/living/basic/mining/legion/snow/spawner_made,
@@ -653,7 +751,7 @@
/obj/structure/ore_vent/boss/icebox
icon_state = "ore_vent_ice"
- icon_state_tapped = "ore_vent_ice_active"
+ base_icon_state = "ore_vent_ice"
defending_mobs = list(
/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner,
/mob/living/simple_animal/hostile/megafauna/wendigo/noportal,
diff --git a/code/game/objects/structures/toiletbong.dm b/code/game/objects/structures/toiletbong.dm
index 0a7d288b0cc5..72549bdbcc7f 100644
--- a/code/game/objects/structures/toiletbong.dm
+++ b/code/game/objects/structures/toiletbong.dm
@@ -58,8 +58,7 @@
playsound(src, 'sound/items/modsuit/flamethrower.ogg', 50)
var/smoke_amount = DIAMOND_AREA(smokeradius)
- do_chem_smoke(amount = smoke_amount, holder = src, location = loc, carry = reagents, carry_limit = 20, smoke_type = /datum/effect_system/fluid_spread/smoke/chem/smoke_machine)
- reagents.remove_all(smoke_amount / 20)
+ do_chem_smoke(amount = smoke_amount, holder = src, location = loc, carry = item.reagents, carry_limit = 20, smoke_type = /datum/effect_system/fluid_spread/smoke/chem/smoke_machine)
if (prob(5) && !(obj_flags & EMAGGED))
if(user.get_liked_foodtypes() & GORE)
user.balloon_alert(user, "a hidden treat!")
diff --git a/code/game/objects/structures/windoor_assembly.dm b/code/game/objects/structures/windoor_assembly.dm
index 508d1f00480b..68b6ee31c7d5 100644
--- a/code/game/objects/structures/windoor_assembly.dm
+++ b/code/game/objects/structures/windoor_assembly.dm
@@ -338,7 +338,6 @@
//Flips the windoor assembly, determines whather the door opens to the left or the right
/obj/structure/windoor_assembly/verb/flip()
set name = "Flip Windoor Assembly"
- set category = "Object"
set src in oview(1)
if(usr.stat != CONSCIOUS || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED))
return
diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm
index cc364195b658..ecd71e18ee22 100644
--- a/code/game/objects/structures/window.dm
+++ b/code/game/objects/structures/window.dm
@@ -422,7 +422,7 @@
QUEUE_SMOOTH(src)
var/ratio = atom_integrity / max_integrity
- ratio = CEILING(ratio*4, 1) * 25
+ ratio = ceil(ratio*4) * 25
if(ratio > 75)
return
. += mutable_appearance('icons/obj/structures.dmi', "damage[ratio]", -(layer+0.1))
diff --git a/code/game/say.dm b/code/game/say.dm
index 7199fe3e3779..e988e484dd12 100644
--- a/code/game/say.dm
+++ b/code/game/say.dm
@@ -112,17 +112,8 @@ GLOBAL_LIST_INIT(freqtospan, list(
return !HAS_TRAIT(src, TRAIT_MUTE)
/atom/movable/proc/send_speech(message, range = 7, obj/source = src, bubble_type, list/spans, datum/language/message_language, list/message_mods = list(), forced = FALSE, tts_message, list/tts_filter)
- var/found_client = FALSE
var/list/listeners = get_hearers_in_view(range, source)
var/list/listened = list()
- for(var/atom/movable/hearing_movable as anything in listeners)
- if(!hearing_movable)//theoretically this should use as anything because it shouldnt be able to get nulls but there are reports that it does.
- stack_trace("somehow theres a null returned from get_hearers_in_view() in send_speech!")
- continue
- if(hearing_movable.Hear(src, message_language, message, null, null, null, spans, message_mods, range))
- listened += hearing_movable
- if(!found_client && length(hearing_movable.client_mobs_in_contents))
- found_client = TRUE
var/tts_message_to_use = tts_message
if(!tts_message_to_use)
@@ -135,9 +126,18 @@ GLOBAL_LIST_INIT(freqtospan, list(
if(length(tts_filter) > 0)
filter += tts_filter.Join(",")
- if(voice && found_client)
- if (!CONFIG_GET(flag/tts_no_whisper) || (CONFIG_GET(flag/tts_no_whisper) && !message_mods[WHISPER_MODE]))
- INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice, filter.Join(","), listened, message_range = range, pitch = pitch)
+ var/shell_scrubbed_input = tts_speech_filter(html_decode(tts_message_to_use))
+ var/identifier = "[sha1(voice + filter.Join(",") + num2text(pitch) + shell_scrubbed_input + blip_base + num2text(blip_number))].[world.time]"
+ message_mods[MODE_TTS_IDENTIFIER] = identifier
+ for(var/atom/movable/hearing_movable as anything in listeners)
+ if(!hearing_movable)//theoretically this should use as anything because it shouldnt be able to get nulls but there are reports that it does.
+ stack_trace("somehow theres a null returned from get_hearers_in_view() in send_speech!")
+ continue
+ if(hearing_movable.Hear(src, message_language, message, null, null, null, spans, message_mods, range))
+ listened += hearing_movable
+
+ if(voice)
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice, filter.Join(","), listened, message_range = range, pitch = pitch, blip_base = blip_base, blip_number = blip_number, identifier = identifier)
/atom/movable/proc/compose_message(atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, list/spans, list/message_mods = list(), visible_name = FALSE)
//This proc uses [] because it is faster than continually appending strings. Thanks BYOND.
@@ -149,7 +149,7 @@ GLOBAL_LIST_INIT(freqtospan, list(
//Radio freq/name display
var/freqpart = radio_freq ? "\[[get_radio_name(radio_freq, radio_freq_name)]\] " : ""
//Speaker name
- var/namepart = speaker.get_message_voice(visible_name)
+ var/namepart = message_mods[MODE_SPEAKER_NAME_OVERRIDE] || speaker.get_message_voice(visible_name)
//End name span.
var/endspanpart = ""
diff --git a/code/modules/admin/admin_ranks.dm b/code/modules/admin/admin_ranks.dm
index 79c9dbf199b3..14819580a2f7 100644
--- a/code/modules/admin/admin_ranks.dm
+++ b/code/modules/admin/admin_ranks.dm
@@ -51,6 +51,14 @@ GLOBAL_PROTECT(protected_ranks)
/datum/admin_rank/vv_edit_var(var_name, var_value)
return FALSE
+/datum/admin_rank/allow_mark_datum()
+ alert_to_permissions_elevation_attempt(usr)
+ return FALSE
+
+/datum/admin_rank/can_vv_mark()
+ alert_to_permissions_elevation_attempt(usr)
+ return FALSE
+
// Adds/removes rights to this admin_rank
/datum/admin_rank/proc/process_keyword(group, group_count, datum/admin_rank/previous_rank)
if(IsAdminAdvancedProcCall())
@@ -355,7 +363,7 @@ GLOBAL_PROTECT(protected_ranks)
continue
var/flags_to_check = flags.Join(" != [R_EVERYTHING] AND ") + " != [R_EVERYTHING]"
var/datum/db_query/query_check_everything_ranks = SSdbcore.NewQuery(
- "SELECT flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")] WHERE rank = :rank AND ([flags_to_check])",
+ "SELECT flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")] WHERE `rank` = :rank AND ([flags_to_check])",
list("rank" = R.name)
)
if(!query_check_everything_ranks.Execute())
@@ -364,7 +372,7 @@ GLOBAL_PROTECT(protected_ranks)
if(query_check_everything_ranks.NextRow()) //no row is returned if the rank already has the correct flag value
var/flags_to_update = flags.Join(" = [R_EVERYTHING], ") + " = [R_EVERYTHING]"
var/datum/db_query/query_update_everything_ranks = SSdbcore.NewQuery(
- "UPDATE [format_table_name("admin_ranks")] SET [flags_to_update] WHERE rank = :rank",
+ "UPDATE [format_table_name("admin_ranks")] SET [flags_to_update] WHERE `rank` = :rank",
list("rank" = R.name)
)
if(!query_update_everything_ranks.Execute())
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index ab95cad4f111..77af0c72fb98 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -685,11 +685,9 @@ ADMIN_VERB(clear_legacy_asset_cache, R_DEBUG, "Clear Legacy Asset Cache", "Clear
to_chat(user, span_warning("Asset caching is disabled in the config!"))
return
var/regenerated = 0
- for(var/datum/asset/target_spritesheet as anything in subtypesof(/datum/asset))
+ for(var/datum/asset/target_spritesheet as anything in valid_subtypesof(/datum/asset))
if(!initial(target_spritesheet.cross_round_cachable))
continue
- if(target_spritesheet == initial(target_spritesheet._abstract))
- continue
var/datum/asset/asset_datum = GLOB.asset_datums[target_spritesheet]
asset_datum.regenerate()
regenerated++
@@ -700,9 +698,7 @@ ADMIN_VERB(clear_smart_asset_cache, R_DEBUG, "Clear Smart Asset Cache", "Clear t
to_chat(user, span_warning("Smart asset caching is disabled in the config!"))
return
var/cleared = 0
- for(var/datum/asset/spritesheet_batched/target_spritesheet as anything in subtypesof(/datum/asset/spritesheet_batched))
- if(target_spritesheet == initial(target_spritesheet._abstract))
- continue
+ for(var/datum/asset/spritesheet_batched/target_spritesheet as anything in valid_subtypesof(/datum/asset/spritesheet_batched))
fdel("[ASSET_CROSS_ROUND_SMART_CACHE_DIRECTORY]/spritesheet_cache.[initial(target_spritesheet.name)].json")
cleared++
to_chat(user, span_notice("Cleared [cleared] asset\s."))
diff --git a/code/modules/admin/create_mob.dm b/code/modules/admin/create_mob.dm
index 51ae7dfd0bc7..810abe25f271 100644
--- a/code/modules/admin/create_mob.dm
+++ b/code/modules/admin/create_mob.dm
@@ -30,6 +30,7 @@
human.physique = human.gender
human.real_name = human.generate_random_mob_name()
human.name = human.get_visible_name()
+ human.voice = SStts.random_tts_voice(human.gender)
human.set_eye_color(random_eye_color())
human.skin_tone = pick(GLOB.skin_tones)
// No underwear generation handled here
diff --git a/code/modules/admin/permissionedit.dm b/code/modules/admin/permissionedit.dm
index 50defc9419ae..79f032da4e7e 100644
--- a/code/modules/admin/permissionedit.dm
+++ b/code/modules/admin/permissionedit.dm
@@ -135,7 +135,7 @@ GLOBAL_LIST_INIT(permission_action_types, list(
admins_by_rank[composed_rank.name] |= list(stored_key)
// Then, pull the full list of DB ranks
- var/datum/db_query/query_extract_ranks = SSdbcore.NewQuery("SELECT rank, flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")]")
+ var/datum/db_query/query_extract_ranks = SSdbcore.NewQuery("SELECT `rank`, flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")]")
if(!query_extract_ranks.warn_execute())
qdel(query_extract_ranks)
return
@@ -317,7 +317,7 @@ GLOBAL_LIST_INIT(permission_action_types, list(
QDEL_NULL(query_extract_admins)
// Then, pull the full list of DB ranks to purity check against
- var/datum/db_query/query_extract_ranks = SSdbcore.NewQuery("SELECT rank, flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")]")
+ var/datum/db_query/query_extract_ranks = SSdbcore.NewQuery("SELECT `rank`, flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")]")
if(!query_extract_ranks.warn_execute())
qdel(query_extract_ranks)
return
@@ -1082,7 +1082,7 @@ GLOBAL_LIST_INIT(permission_action_types, list(
if(use_db)
var/datum/db_query/query_db_rank_info = SSdbcore.NewQuery({"
SELECT flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")]
- WHERE rank = :rank_name
+ WHERE `rank` = :rank_name
"}, list("rank_name" = admin_rank))
if(!query_db_rank_info.warn_execute())
qdel(query_db_rank_info)
@@ -1169,19 +1169,19 @@ GLOBAL_LIST_INIT(permission_action_types, list(
query_update_rank = SSdbcore.NewQuery({"
UPDATE [format_table_name("admin_ranks")]
SET flags = :flags
- WHERE rank = :rank_name
+ WHERE `rank` = :rank_name
"}, list("rank_name" = admin_rank, "flags" = new_flags))
if("Excluded Rights")
query_update_rank = SSdbcore.NewQuery({"
UPDATE [format_table_name("admin_ranks")]
SET exclude_flags = :exclude_flags
- WHERE rank = :rank_name
+ WHERE `rank` = :rank_name
"}, list("rank_name" = admin_rank, "exclude_flags" = new_flags))
if("Editing Rights")
query_update_rank = SSdbcore.NewQuery({"
UPDATE [format_table_name("admin_ranks")]
SET can_edit_flags = :can_edit_flags
- WHERE rank = :rank_name
+ WHERE `rank` = :rank_name
"}, list("rank_name" = admin_rank, "can_edit_flags" = new_flags))
if(!query_update_rank.warn_execute())
diff --git a/code/modules/admin/tag.dm b/code/modules/admin/tag.dm
index f974973fdffe..d5d149371a47 100644
--- a/code/modules/admin/tag.dm
+++ b/code/modules/admin/tag.dm
@@ -9,6 +9,9 @@
to_chat(owner, span_warning("[target_datum] is already tagged!"))
return
+ if(!target_datum.allow_mark_datum())
+ return
+
LAZYADD(tagged_datums, target_datum)
RegisterSignal(target_datum, COMSIG_QDELETING, PROC_REF(handle_tagged_del), override = TRUE)
to_chat(owner, span_notice("[target_datum] has been tagged."))
diff --git a/code/modules/admin/verbs/adminjump.dm b/code/modules/admin/verbs/adminjump.dm
index 0248ccd99754..00a64df4a6b2 100644
--- a/code/modules/admin/verbs/adminjump.dm
+++ b/code/modules/admin/verbs/adminjump.dm
@@ -56,6 +56,21 @@ ADMIN_VERB(jump_to_key, R_ADMIN, "Jump To Key", "Jump to a specific player.", AD
user.mob.abstract_move(M.loc)
BLACKBOX_LOG_ADMIN_VERB("Jump To Key")
+ADMIN_VERB(jump_to_ghost, R_ADMIN, "Jump To Ghost", "Jump your body to your Aghost.", ADMIN_CATEGORY_GAME)
+ var/mob/dead/observer/ghost = user.mob
+ if(!isobserver(ghost))
+ return
+ var/mob/living/body = ghost?.mind?.current
+ if(!body)
+ to_chat(user, "No valid body to bring to ghost.", confidential = TRUE)
+ return
+ log_admin("[key_name(user)] jumped to their Aghost at [AREACOORD(ghost.loc)]")
+ message_admins("[key_name_admin(user)] jumped to their Aghost [ADMIN_FLW(body)] at [AREACOORD(ghost.loc)]")
+ body.abstract_move(ghost.loc)
+ body.setDir(ghost.dir)
+ SSadmin_verbs.dynamic_invoke_verb(user, /datum/admin_verb/admin_ghost)
+ BLACKBOX_LOG_ADMIN_VERB("Jump To Ghost")
+
ADMIN_VERB_AND_CONTEXT_MENU(get_mob, R_ADMIN, "Get Mob", "Teleport a mob to your location.", ADMIN_CATEGORY_GAME, mob/target in world)
var/atom/loc = get_turf(user.mob)
target.admin_teleport(loc)
diff --git a/code/modules/admin/verbs/adminshuttle.dm b/code/modules/admin/verbs/adminshuttle.dm
index eaac09eceea5..4b8adc1f6b83 100644
--- a/code/modules/admin/verbs/adminshuttle.dm
+++ b/code/modules/admin/verbs/adminshuttle.dm
@@ -54,7 +54,7 @@ ADMIN_VERB(cancel_shuttle, R_ADMIN, "Cancel Shuttle", "Recall the shuttle, regar
if(tgui_alert(user, "You sure?", "Confirm Shuttle Cancellation", list("Yes", "No")) != "Yes")
return
- if(!SSshuttle.cancel_evac(user.mob)) // handles the case where the shuttle is set to unrecallable by another admin or the code
+ if(!SSshuttle.cancel_evac(user.mob, hide_origin = TRUE)) // handles the case where the shuttle is set to unrecallable by another admin or the code
return
BLACKBOX_LOG_ADMIN_VERB("Cancel Shuttle")
diff --git a/code/modules/admin/verbs/commandreport.dm b/code/modules/admin/verbs/commandreport.dm
index 07acafd21201..11f8bf07b2ce 100644
--- a/code/modules/admin/verbs/commandreport.dm
+++ b/code/modules/admin/verbs/commandreport.dm
@@ -42,6 +42,10 @@ ADMIN_VERB(create_command_report, R_ADMIN, "Create Command Report", "Create a co
var/announcement_color = "default"
/// The subheader to include when sending the announcement. Keep blank to not include a subheader
var/subheader = ""
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ /// TTS voice used for various command reports and announcements
+ var/tts_voice
+ // MASSMETA EDIT END (ntts && /tg/tts)
/// A static list of preset names that can be chosen.
var/list/preset_names = list(CENTCOM_PRESET, SYNDICATE_PRESET, WIZARD_PRESET, CUSTOM_PRESET)
@@ -50,6 +54,9 @@ ADMIN_VERB(create_command_report, R_ADMIN, "Create Command Report", "Create a co
if(command_name() != CENTCOM_PRESET)
command_name = command_name()
preset_names.Insert(1, command_name())
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ tts_voice = SStts.centcom_voice
+ // MASSMETA EDIT END (ntts && /tg/tts)
/datum/command_report_menu/ui_state(mob/user)
return ADMIN_STATE(R_ADMIN)
@@ -73,6 +80,9 @@ ADMIN_VERB(create_command_report, R_ADMIN, "Create Command Report", "Create a co
data["played_sound"] = played_sound
data["announcement_color"] = announcement_color
data["subheader"] = subheader
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ data["tts_voice"] = tts_voice
+ // MASSMETA EDIT END (ntts && /tg/tts)
return data
@@ -81,6 +91,9 @@ ADMIN_VERB(create_command_report, R_ADMIN, "Create Command Report", "Create a co
data["command_name_presets"] = preset_names
data["announcer_sounds"] = list(DEFAULT_ANNOUNCEMENT_SOUND) + GLOB.announcer_keys + CUSTOM_SOUND_PRESET
data["announcement_colors"] = ANNOUNCEMENT_COLORS
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ data["tts_voices"] = SStts.admin_voices()
+ // MASSMETA EDIT END (ntts && /tg/tts)
return data
@@ -123,6 +136,12 @@ ADMIN_VERB(create_command_report, R_ADMIN, "Create Command Report", "Create a co
announcement_color = chosen_color
if("set_subheader")
subheader = params["new_subheader"]
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ if("set_tts_voice")
+ var/picked_voice = params["picked_voice"]
+ if(picked_voice in SStts.admin_voices())
+ tts_voice = picked_voice
+ // MASSMETA EDIT END (ntts && /tg/tts)
if("submit_report")
if(!command_name)
to_chat(ui_user, span_danger("You can't send a report with no command name."))
@@ -157,7 +176,10 @@ ADMIN_VERB(create_command_report, R_ADMIN, "Create Command Report", "Create a co
chosen_color = "red"
else if(command_name == WIZARD_PRESET)
chosen_color = "purple"
- priority_announce(command_report_content, subheader == ""? null : subheader, report_sound, has_important_message = TRUE, color_override = chosen_color)
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ var/tts_effect = (command_name == CENTCOM_PRESET) ? "centcom" : "syndicate"
+ priority_announce(command_report_content, subheader == ""? null : subheader, report_sound, has_important_message = TRUE, color_override = chosen_color, tts_voice = tts_voice, tts_effect = tts_effect)
+ // MASSMETA EDIT END (ntts && /tg/tts)
if(!announce_contents || print_report)
print_command_report(command_report_content, "[announce_contents ? "" : "Classified "][command_name] Update", !announce_contents, contains_advanced_html = TRUE)
diff --git a/code/modules/admin/verbs/light_debug.dm b/code/modules/admin/verbs/light_debug.dm
index daf012a9b30e..ad72df86c20f 100644
--- a/code/modules/admin/verbs/light_debug.dm
+++ b/code/modules/admin/verbs/light_debug.dm
@@ -359,6 +359,8 @@ GLOBAL_LIST_EMPTY(light_debugged_atoms)
/datum/action/spawn_light/Trigger(mob/clicker, trigger_flags)
. = ..()
+ if(!.)
+ return
ui_interact(usr)
/datum/action/spawn_light/ui_state(mob/user)
diff --git a/code/modules/admin/verbs/possess.dm b/code/modules/admin/verbs/possess.dm
index 2949447b2ef0..47dd6fdc4c3e 100644
--- a/code/modules/admin/verbs/possess.dm
+++ b/code/modules/admin/verbs/possess.dm
@@ -1,5 +1,5 @@
-ADMIN_VERB_AND_CONTEXT_MENU(possess, R_POSSESS, "Possess Obj", "Possess an object.", ADMIN_CATEGORY_OBJECT, obj/target in world)
+ADMIN_VERB_AND_CONTEXT_MENU(possess, R_POSSESS, "Possess Obj", "Possess an object.", ADMIN_CATEGORY_FUN, obj/target in world)
var/result = user.mob.AddComponent(/datum/component/object_possession, target)
if(isnull(result)) // trigger a safety movement just in case we yonk
@@ -13,7 +13,7 @@ ADMIN_VERB_AND_CONTEXT_MENU(possess, R_POSSESS, "Possess Obj", "Possess an objec
BLACKBOX_LOG_ADMIN_VERB("Possess Object")
-ADMIN_VERB(release, R_POSSESS, "Release Object", "Stop possessing an object.", ADMIN_CATEGORY_OBJECT)
+ADMIN_VERB(release, R_POSSESS, "Release Object", "Stop possessing an object.", ADMIN_CATEGORY_FUN)
var/possess_component = user.mob.GetComponent(/datum/component/object_possession)
if(!isnull(possess_component))
qdel(possess_component)
diff --git a/code/modules/admin/verbs/secrets.dm b/code/modules/admin/verbs/secrets.dm
index f4774653ba69..6df0f294ef5e 100644
--- a/code/modules/admin/verbs/secrets.dm
+++ b/code/modules/admin/verbs/secrets.dm
@@ -703,6 +703,18 @@ ADMIN_VERB(secrets, R_NONE, "Secrets", "Abuse harder than you ever have before w
return
return
+ if("fix_gravity")
+ if(!is_funmin)
+ return
+ SSblackbox.record_feedback("nested tally", "admin_secrets_fun_used", 1, list("fix_gravity"))
+ message_admins("[key_name_admin(holder)] fixed the gravity generator.")
+
+ for(var/obj/machinery/gravity_generator/main/the_generator as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/gravity_generator/main))
+ if(!is_station_level(the_generator.z))
+ continue
+ the_generator.kickstart()
+ CHECK_TICK
+
if(holder)
log_admin("[key_name(holder)] used secret: [action].")
#undef THUNDERDOME_TEMPLATE_FILE
diff --git a/code/modules/admin/view_variables/mark_datum.dm b/code/modules/admin/view_variables/mark_datum.dm
index 1d9dd3a1a37c..f8f9977e6353 100644
--- a/code/modules/admin/view_variables/mark_datum.dm
+++ b/code/modules/admin/view_variables/mark_datum.dm
@@ -1,6 +1,8 @@
/client/proc/mark_datum(datum/D)
if(!holder)
return
+ if(!D.allow_mark_datum())
+ return
if(holder.marked_datum)
holder.UnregisterSignal(holder.marked_datum, COMSIG_QDELETING)
vv_update_display(holder.marked_datum, "marked", "")
@@ -15,3 +17,7 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(mark_datum, R_NONE, "Mark Object", datum/target as
SIGNAL_HANDLER
UnregisterSignal(marked_datum, COMSIG_QDELETING)
marked_datum = null
+
+/// Will this datum allow itself to be marked by vv?
+/datum/proc/allow_mark_datum()
+ return TRUE
diff --git a/code/modules/antagonists/blood_worm/actions/leech_blood.dm b/code/modules/antagonists/blood_worm/actions/leech_blood.dm
index e7b0360cbcd8..fda316cb10ce 100644
--- a/code/modules/antagonists/blood_worm/actions/leech_blood.dm
+++ b/code/modules/antagonists/blood_worm/actions/leech_blood.dm
@@ -79,9 +79,10 @@
if (!do_after(leech, leech_grab_delay, target, extra_checks = CALLBACK(src, PROC_REF(leech_living_start_check), leech, target)))
return
- if (leech.pulling != target && !leech.grab(target))
+ if (leech.pulling != target && leech.grab(target) != GRAB_SUCCESS)
target.balloon_alert(leech, "unable to grab!")
return
+
if (leech.grab_state < GRAB_AGGRESSIVE)
leech.setGrabState(GRAB_AGGRESSIVE)
@@ -118,7 +119,7 @@
if (synth_content >= 1)
target.balloon_alert(leech, "fully synthetic")
else if (synth_content > 0)
- target.balloon_alert(leech, "[CEILING(synth_content * 100, 1)]% synthetic")
+ target.balloon_alert(leech, "[ceil(synth_content * 100)]% synthetic")
// Because of DO_AFTER_CHECK_NEXT_MOVE
leech.next_move = 0
@@ -229,7 +230,7 @@
if (synth_content >= 1)
target.balloon_alert(leech, "fully synthetic")
else if (synth_content > 0)
- target.balloon_alert(leech, "[CEILING(synth_content * 100, 1)]% synthetic")
+ target.balloon_alert(leech, "[ceil(synth_content * 100)]% synthetic")
/datum/action/cooldown/mob_cooldown/blood_worm/leech/proc/leech_container_start_check(mob/living/basic/blood_worm/leech, obj/item/reagent_containers/target, feedback = FALSE)
if (!length(get_blood_in_container(target)))
diff --git a/code/modules/antagonists/blood_worm/blood_worm_text_feedback.dm b/code/modules/antagonists/blood_worm/blood_worm_text_feedback.dm
index c002e2717178..d11dc64ce837 100644
--- a/code/modules/antagonists/blood_worm/blood_worm_text_feedback.dm
+++ b/code/modules/antagonists/blood_worm/blood_worm_text_feedback.dm
@@ -4,7 +4,7 @@
/mob/living/basic/blood_worm/examining(atom/target, list/result)
add_special_examining_messages(target, result)
-/mob/living/basic/blood_worm/proc/on_host_examining(datum/source, atom/target, list/examine_strings)
+/mob/living/basic/blood_worm/proc/on_host_examining(datum/source, atom/target, list/examine_strings, list/examine_overrides)
SIGNAL_HANDLER
add_special_examining_messages(target, examine_strings)
@@ -35,7 +35,7 @@
var/potential_gain = total_blood_after - total_blood_now
- var/rounded_volume = CEILING(cached_blood_volume, 1)
+ var/rounded_volume = ceil(cached_blood_volume)
var/growth_string = ""
if (HAS_TRAIT(bloodbag, TRAIT_BLOOD_WORM_HOST))
@@ -52,14 +52,14 @@
else
growth_string = ". You are already fully grown"
- var/synth_string = "[CEILING(synth_content * 100, 1)]%"
+ var/synth_string = "[ceil(synth_content * 100)]%"
switch(synth_content)
if (-INFINITY to 0)
synth_string = "not"
if (1 to INFINITY)
synth_string = "fully"
if (0 to 1)
- synth_string = "[CEILING(synth_content * 100, 1)]%"
+ synth_string = "[ceil(synth_content * 100)]%"
result += span_notice("[target.p_They()] [target.p_have()] [rounded_volume] unit[rounded_volume == 1 ? "" : "s"] of blood[growth_string]. [target.p_Their()] blood is [synth_string] synthetic.")
@@ -83,9 +83,9 @@
if (total_required > 0)
. += "Growth: [FLOOR(total / total_required * 100, 1)]%"
. += "Blood Consumed"
- . += "- Normal: [CEILING(normal, 1)]u"
- . += "- Synthetic: [CEILING(synth, 1)]u (MAX: [maximum_synth_blood]u)"
- . += "- Total: [CEILING(total, 1)]u (REQ: [total_required]u)"
+ . += "- Normal: [ceil(normal)]u"
+ . += "- Synthetic: [ceil(synth)]u (MAX: [maximum_synth_blood]u)"
+ . += "- Total: [ceil(total)]u (REQ: [total_required]u)"
/// Sends text to the blood worm, whether they are possessing a host or not.
/mob/living/basic/blood_worm/proc/to_chat_self(text)
diff --git a/code/modules/antagonists/changeling/changeling_power.dm b/code/modules/antagonists/changeling/changeling_power.dm
index d14d87b0f40d..9cee60fbfe32 100644
--- a/code/modules/antagonists/changeling/changeling_power.dm
+++ b/code/modules/antagonists/changeling/changeling_power.dm
@@ -48,6 +48,9 @@ the same goes for Remove(). if you override Remove(), call parent or else your p
Grant(user)//how powers are added rather than the checks in mob.dm
/datum/action/changeling/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
var/mob/user = owner
if(!user || !IS_CHANGELING(user))
return
diff --git a/code/modules/antagonists/changeling/powers/expel_worm.dm b/code/modules/antagonists/changeling/powers/expel_worm.dm
index 7cda9bbe87a2..3139e149c351 100644
--- a/code/modules/antagonists/changeling/powers/expel_worm.dm
+++ b/code/modules/antagonists/changeling/powers/expel_worm.dm
@@ -19,6 +19,9 @@
return TRUE
/datum/action/changeling_expel_worm/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
var/mob/living/basic/blood_worm/invader = locate() in owner.loc
to_chat(owner, span_danger("You expel \the [invader] from your body!"))
to_chat(invader, span_userdanger("You are forcefully expelled by the body of \the [owner.loc]!"))
diff --git a/code/modules/antagonists/changeling/powers/tiny_prick.dm b/code/modules/antagonists/changeling/powers/tiny_prick.dm
index 3368417c2c67..5767a87ca55a 100644
--- a/code/modules/antagonists/changeling/powers/tiny_prick.dm
+++ b/code/modules/antagonists/changeling/powers/tiny_prick.dm
@@ -5,6 +5,7 @@
button_icon_state = "sting_null" //This must be equal to the icon state for `/atom/movable/screen/ling/sting`
/datum/action/changeling/sting/Trigger(mob/clicker, trigger_flags)
+ SHOULD_CALL_PARENT(FALSE) //We are snowflaked from parent
var/mob/user = owner
if(!user || !user.mind)
return
diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm
index ae1a5dfb9323..cd43f1dc81f9 100644
--- a/code/modules/antagonists/heretic/heretic_antag.dm
+++ b/code/modules/antagonists/heretic/heretic_antag.dm
@@ -38,8 +38,6 @@
HERETIC_KNOWLEDGE_SHOP = list(),
HERETIC_KNOWLEDGE_DRAFT = list()
)
- /// A static typecache of all tools we can scribe with.
- var/static/list/scribing_tools = typecacheof(list(/obj/item/pen, /obj/item/toy/crayon))
/// A blacklist of turfs we cannot scribe on.
var/static/list/blacklisted_rune_turfs = typecacheof(list(/turf/open/space, /turf/open/openspace, /turf/open/lava, /turf/open/chasm))
/// A static list of all paths we can take and related info for the UI
@@ -358,6 +356,7 @@
ADD_TRAIT(owner, TRAIT_SEE_BLESSED_TILES, REF(src))
addtimer(CALLBACK(src, PROC_REF(passive_influence_gain)), passive_gain_timer) // Gain +1 knowledge every 20 minutes.
+
return ..()
/datum/antagonist/heretic/on_removal()
@@ -393,6 +392,8 @@
list(SIGNAL_ADDTRAIT(TRAIT_HERETIC_AURA_HIDDEN), SIGNAL_REMOVETRAIT(TRAIT_HERETIC_AURA_HIDDEN)),
PROC_REF(update_heretic_aura)
)
+ RegisterSignal(our_mob, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(add_aura_overlay))
+ our_mob.update_appearance(UPDATE_OVERLAYS)
/datum/antagonist/heretic/remove_innate_effects(mob/living/mob_override)
var/mob/living/our_mob = mob_override || owner.current
@@ -429,17 +430,18 @@
var/datum/action/cooldown/spell/shadow_cloak/cloak_spell = locate() in heretic_mob.actions
cloak_spell.Remove(heretic_mob)
-/// Adds an overlay to the heretic
-/datum/antagonist/heretic/proc/update_heretic_aura()
+/datum/antagonist/heretic/proc/add_aura_overlay(mob/living/source, list/overlays)
SIGNAL_HANDLER
- var/mob/heretic_mob = owner.current
- heretic_mob.cut_overlay(eldritch_overlay)
-
if(!should_show_aura())
- return FALSE
+ return
+ overlays += eldritch_overlay
+ overlays += emissive_appearance(eldritch_overlay.icon, eldritch_overlay.icon_state, source)
- heretic_mob.add_overlay(eldritch_overlay)
- return TRUE
+/// Adds an overlay to the heretic
+/datum/antagonist/heretic/proc/update_heretic_aura()
+ SIGNAL_HANDLER
+ if(!QDELETED(owner?.current))
+ owner.current.update_appearance(UPDATE_OVERLAYS)
/datum/antagonist/heretic/proc/should_show_aura()
if(!can_assign_self_objectives)
@@ -506,7 +508,7 @@
*/
/datum/antagonist/heretic/proc/on_item_use(mob/living/source, atom/target, obj/item/weapon, list/modifiers)
SIGNAL_HANDLER
- if(!is_type_in_typecache(weapon, scribing_tools))
+ if(!IS_WRITING_UTENSIL(weapon))
return NONE
if(!isturf(target) || !isliving(source))
return NONE
diff --git a/code/modules/antagonists/heretic/items/heretic_armor.dm b/code/modules/antagonists/heretic/items/heretic_armor.dm
index 20cdbed7a9ae..a5b63179a963 100644
--- a/code/modules/antagonists/heretic/items/heretic_armor.dm
+++ b/code/modules/antagonists/heretic/items/heretic_armor.dm
@@ -854,6 +854,8 @@
var/image/object_overlay
/// Overlay for the hood object
var/image/hood_object_overlay
+ /// Turf we're currently listening to for rust trait gains
+ var/turf/listening_turf
/obj/item/clothing/suit/hooded/cultrobes/eldritch/rust/Initialize(mapload)
. = ..()
@@ -866,6 +868,7 @@
/obj/item/clothing/suit/hooded/cultrobes/eldritch/rust/on_robes_gained(mob/living/user)
. = ..()
RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
+ register_turf_listener(user)
rust_overlay = new()
rust_overlay.icon = 'icons/mob/clothing/suits/armor.dmi'
rust_overlay.render_target = "*rust_overlay_[overlay_id]"
@@ -881,6 +884,9 @@
if(.)
return
UnregisterSignal(user, list(COMSIG_MOVABLE_MOVED))
+ if(listening_turf)
+ UnregisterSignal(listening_turf, SIGNAL_ADDTRAIT(TRAIT_RUSTY))
+ listening_turf = null
user.vis_contents -= rust_overlay
rusted = FALSE
set_armor(/datum/armor/eldritch_armor/rust)
@@ -913,14 +919,25 @@
victim.vomit(MOB_VOMIT_BLOOD | MOB_VOMIT_MESSAGE | MOB_VOMIT_HARM | MOB_VOMIT_FORCE)
victim.spew_organ(rand(4, 6))
-/*
- * Signal proc for [COMSIG_MOVABLE_MOVED].
- *
- * Checks if our armor values should be increased on the new turf
- */
-/obj/item/clothing/suit/hooded/cultrobes/eldritch/rust/proc/on_move(mob/source, atom/old_loc, dir, forced, list/old_locs)
+/// Keeps our turf rust listener aligned with where the wearer currently stands.
+/obj/item/clothing/suit/hooded/cultrobes/eldritch/rust/proc/register_turf_listener(mob/source)
+ var/turf/new_turf = get_turf(source)
+ if(listening_turf == new_turf)
+ return
+ if(listening_turf)
+ UnregisterSignal(listening_turf, SIGNAL_ADDTRAIT(TRAIT_RUSTY))
+ listening_turf = new_turf
+ if(listening_turf)
+ RegisterSignal(listening_turf, SIGNAL_ADDTRAIT(TRAIT_RUSTY), PROC_REF(on_turf_became_rusty))
+
+/obj/item/clothing/suit/hooded/cultrobes/eldritch/rust/proc/on_turf_became_rusty(turf/source, rust_trait)
SIGNAL_HANDLER
+ var/mob/living/wearer = loc
+ if(!isliving(wearer) || !is_equipped(wearer))
+ return
+ update_rust_state(wearer)
+/obj/item/clothing/suit/hooded/cultrobes/eldritch/rust/proc/update_rust_state(mob/source)
if(source.is_touching_rust())
set_armor(/datum/armor/eldritch_armor/rust/on_rust)
@@ -948,6 +965,16 @@
rusted = FALSE
update_rust()
+/*
+ * Signal proc for [COMSIG_MOVABLE_MOVED].
+ *
+ * Checks if our armor values should be increased on the new turf
+ */
+/obj/item/clothing/suit/hooded/cultrobes/eldritch/rust/proc/on_move(mob/source, atom/old_loc, dir, forced, list/old_locs)
+ SIGNAL_HANDLER
+ register_turf_listener(source)
+ update_rust_state(source)
+
/// Updates the icon of our overlay and applies the animation
/obj/item/clothing/suit/hooded/cultrobes/eldritch/rust/proc/update_rust()
// Animation + Update the overlay sprite on our armor
diff --git a/code/modules/antagonists/heretic/knowledge/moon_lore.dm b/code/modules/antagonists/heretic/knowledge/moon_lore.dm
index eb6428aee276..d8c093ac4059 100644
--- a/code/modules/antagonists/heretic/knowledge/moon_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/moon_lore.dm
@@ -286,7 +286,7 @@
carbon_view.mob_mood.adjust_sanity(-20)
if(carbon_sanity >= 10)
- return
+ continue
// So our sanity is dead, time to fuck em up
if(SPT_PROB(20, seconds_per_tick))
to_chat(carbon_view, span_warning("it echoes through you!"))
diff --git a/code/modules/antagonists/heretic/knowledge/void_lore.dm b/code/modules/antagonists/heretic/knowledge/void_lore.dm
index 7cd58b4460f4..b5deceaca5f2 100644
--- a/code/modules/antagonists/heretic/knowledge/void_lore.dm
+++ b/code/modules/antagonists/heretic/knowledge/void_lore.dm
@@ -246,6 +246,8 @@
for(var/atom/thing_in_range as anything in range(10, source))
if(iscarbon(thing_in_range))
var/mob/living/carbon/close_carbon = thing_in_range
+ if(close_carbon.can_block_magic())
+ continue
if(IS_HERETIC_OR_MONSTER(close_carbon))
close_carbon.apply_status_effect(/datum/status_effect/void_conduit)
continue
diff --git a/code/modules/antagonists/heretic/magic/space_crawl.dm b/code/modules/antagonists/heretic/magic/space_crawl.dm
index 7c59f83e811f..6ff60144f019 100644
--- a/code/modules/antagonists/heretic/magic/space_crawl.dm
+++ b/code/modules/antagonists/heretic/magic/space_crawl.dm
@@ -32,19 +32,30 @@
UnregisterSignal(remove_from, COMSIG_MOVABLE_MOVED)
/datum/action/cooldown/spell/jaunt/space_crawl/can_cast_spell(feedback = TRUE)
+ // we may loose the focus during jaunt, so you need to be always able to exit on a valid turf
+ if(is_jaunting(owner) && is_valid_turf())
+ return TRUE
. = ..()
if(!.)
return FALSE
- var/turf/my_turf = get_turf(owner)
- if(isspaceturf(my_turf))
- return TRUE
- var/area/my_area = get_area(owner)
- if (isopenturf(my_turf) && my_area.outdoors && lavaland_equipment_pressure_check(my_turf))
+ if(is_valid_turf())
return TRUE
if(feedback)
to_chat(owner, span_warning("You must stand in space, or an outdoor area with low pressure!"))
return FALSE
+
+// do not check if we have a focus if we're already jaunting
+/datum/action/cooldown/spell/jaunt/space_crawl/before_cast(atom/cast_on)
+ if(is_jaunting(owner) && is_valid_turf())
+ return NONE
+ . = ..()
+
+/datum/action/cooldown/spell/jaunt/space_crawl/proc/is_valid_turf()
+ var/turf/my_turf = get_turf(owner)
+ var/area/my_area = get_area(owner)
+ return isspaceturf(my_turf) || (isopenturf(my_turf) && my_area.outdoors && lavaland_equipment_pressure_check(my_turf))
+
/datum/action/cooldown/spell/jaunt/space_crawl/cast(mob/living/cast_on)
. = ..()
// Should always return something because we checked that in can_cast_spell before arriving here
@@ -93,7 +104,6 @@
jaunter.put_in_hands(right_hand)
jaunter.add_traits(jaunting_traits, SPACE_PHASING)
- RegisterSignal(jaunter, SIGNAL_REMOVETRAIT(TRAIT_ALLOW_HERETIC_CASTING), PROC_REF(on_focus_lost))
playsound(our_turf, 'sound/effects/magic/cosmic_energy.ogg', 50, TRUE, -1)
our_turf.visible_message(span_warning("[jaunter] sinks into [our_turf]!"))
new /obj/effect/temp_visual/space_explosion(our_turf)
@@ -118,7 +128,6 @@
/datum/action/cooldown/spell/jaunt/space_crawl/on_jaunt_exited(obj/effect/dummy/phased_mob/jaunt, mob/living/unjaunter)
UnregisterSignal(jaunt, COMSIG_MOVABLE_MOVED)
- UnregisterSignal(unjaunter, list(SIGNAL_REMOVETRAIT(TRAIT_ALLOW_HERETIC_CASTING)))
playsound(get_turf(unjaunter), 'sound/effects/magic/cosmic_energy.ogg', 50, TRUE, -1)
new /obj/effect/temp_visual/space_explosion(get_turf(unjaunter))
if(iscarbon(unjaunter))
@@ -127,11 +136,6 @@
qdel(space_hand)
return ..()
-/// Signal proc for [SIGNAL_REMOVETRAIT] via [TRAIT_ALLOW_HERETIC_CASTING], losing our focus midcast will throw us out.
-/datum/action/cooldown/spell/jaunt/space_crawl/proc/on_focus_lost(mob/living/source)
- SIGNAL_HANDLER
- var/turf/our_turf = get_turf(source)
- try_exit_jaunt(our_turf, source, TRUE)
/// Spacecrawl "hands", prevent the user from holding items in spacecrawl
/obj/item/space_crawl
diff --git a/code/modules/antagonists/heretic/status_effects/debuffs.dm b/code/modules/antagonists/heretic/status_effects/debuffs.dm
index 73f01092a681..f7c64df79ddd 100644
--- a/code/modules/antagonists/heretic/status_effects/debuffs.dm
+++ b/code/modules/antagonists/heretic/status_effects/debuffs.dm
@@ -287,6 +287,19 @@
return FALSE
return TRUE
+/datum/status_effect/eldritch_painting/tick(seconds_between_ticks)
+ // having holy water in you halts the effect + makes it expire faster
+ // holy watter currently has 0.2 metab rate so 0.2u/s -> 3 seconds is removed per 0.2 units -> 30s per 2 units -> 300s per 20 units -> 40u will cure you
+ if(owner.reagents.has_reagent(/datum/reagent/water/holywater))
+ remove_duration(3 * seconds_between_ticks)
+ return
+ if(HAS_TRAIT(owner, TRAIT_ELDRITCH_PAINTING_EXAMINE))
+ return
+ on_tick(seconds_between_ticks)
+
+/datum/status_effect/eldritch_painting/proc/on_tick(seconds_between_ticks)
+ return
+
/atom/movable/screen/alert/status_effect/eldritch_painting
name = "Rick Roll'd"
desc = "Fucking coders are at it again."
@@ -298,11 +311,9 @@
alert_type = /atom/movable/screen/alert/status_effect/eldritch_painting/weeping
tick_interval = 10 SECONDS
-/datum/status_effect/eldritch_painting/weeping/tick(seconds_between_ticks)
+/datum/status_effect/eldritch_painting/weeping/on_tick(seconds_between_ticks)
if(owner.stat != CONSCIOUS || owner.IsSleeping() || owner.IsUnconscious())
return
- if(HAS_TRAIT(owner, TRAIT_ELDRITCH_PAINTING_EXAMINE))
- return
owner.cause_hallucination(/datum/hallucination/delusion/preset/heretic, "Caused by The Weeping status effect")
owner.add_mood_event("eldritch_weeping", /datum/mood_event/eldritch_painting/weeping)
@@ -332,9 +343,7 @@
ADD_TRAIT(owner, TRAIT_FLESH_DESIRE, TRAIT_STATUS_EFFECT(id))
return TRUE
-/datum/status_effect/eldritch_painting/desire/tick(seconds_between_ticks)
- if(HAS_TRAIT(owner, TRAIT_ELDRITCH_PAINTING_EXAMINE))
- return
+/datum/status_effect/eldritch_painting/desire/on_tick(seconds_between_ticks)
// Causes them to need to eat at 10x the normal rate
owner.adjust_nutrition(-hunger_rate * HUNGER_FACTOR)
if(SPT_PROB(10, seconds_between_ticks))
@@ -362,13 +371,10 @@
/// How much damage we deal with each scratch
var/scratch_damage = 3
-/datum/status_effect/eldritch_painting/beauty/tick(seconds_between_ticks)
+/datum/status_effect/eldritch_painting/beauty/on_tick(seconds_between_ticks)
if(owner.incapacitated)
return
- if(HAS_TRAIT(owner, TRAIT_ELDRITCH_PAINTING_EXAMINE))
- return
-
// Scratching code
var/obj/item/bodypart/bodypart = owner.get_bodypart(owner.get_random_valid_zone(even_weights = TRUE))
if(!bodypart || !IS_ORGANIC_LIMB(bodypart) || (bodypart.bodypart_flags & BODYPART_PSEUDOPART))
@@ -392,9 +398,9 @@
alert_type = /atom/movable/screen/alert/status_effect/eldritch_painting/rusting
tick_interval = 3 SECONDS
-/datum/status_effect/eldritch_painting/rusting/tick(seconds_between_ticks)
+/datum/status_effect/eldritch_painting/rusting/on_tick(seconds_between_ticks)
var/atom/tile = get_turf(owner)
- if(HAS_TRAIT(owner, TRAIT_ELDRITCH_PAINTING_EXAMINE))
+ if(isnull(tile))
return
to_chat(owner, span_notice("You feel the decay..."))
diff --git a/code/modules/antagonists/heretic/status_effects/void_chill.dm b/code/modules/antagonists/heretic/status_effects/void_chill.dm
index f457a4040ff2..72eed20eab0e 100644
--- a/code/modules/antagonists/heretic/status_effects/void_chill.dm
+++ b/code/modules/antagonists/heretic/status_effects/void_chill.dm
@@ -24,6 +24,8 @@
owner.update_icon(UPDATE_OVERLAYS)
/datum/status_effect/void_chill/on_apply()
+ if(owner.can_block_magic())
+ return FALSE
if(issilicon(owner))
return FALSE
if(IS_HERETIC_OR_MONSTER(owner))
diff --git a/code/modules/antagonists/malf_ai/malf_ai_modules.dm b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
index f2c6eb49eda5..62d629e16731 100644
--- a/code/modules/antagonists/malf_ai/malf_ai_modules.dm
+++ b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
@@ -85,6 +85,8 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module/malf))
/datum/action/innate/ai/Trigger(mob/clicker, trigger_flags)
. = ..()
+ if(!.)
+ return
if(auto_use_uses)
adjust_uses(-1)
if(cooldown_period)
diff --git a/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm
index 4cc06387530f..a5dded9c71f1 100644
--- a/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm
+++ b/code/modules/antagonists/pirate/pirate_shuttle_equipment.dm
@@ -190,6 +190,16 @@
balloon_alert(user, "saved to multitool buffer")
return TRUE
+/obj/machinery/piratepad/screwdriver_act(mob/living/user, obj/item/tool)
+ . = ..()
+ if(!.)
+ return default_deconstruction_screwdriver(user, "lpad-idle-open", "lpad-idle-off", tool)
+
+/obj/machinery/piratepad/crowbar_act(mob/living/user, obj/item/tool)
+ . = ..()
+ if(!.)
+ return default_deconstruction_crowbar(tool)
+
/obj/machinery/piratepad/screwdriver_act_secondary(mob/living/user, obj/item/screwdriver/screw)
. = ..()
if(!.)
@@ -222,6 +232,8 @@
var/interface_type = "CargoHoldTerminal"
///Typecache of things that shouldn't be sold and shouldn't have their contents sold.
var/static/list/nosell_typecache
+ /// When we send the pad for this machine, do we want to lazyload in the ninja holding facility?
+ var/load_holding_facility = TRUE
/obj/machinery/computer/piratepad_control/Initialize(mapload)
..()
@@ -274,11 +286,7 @@
recalc()
. = TRUE
if("send")
- start_sending()
- //We ensure that the holding facility is loaded in time in case we're selling mobs.
- //This isn't the prettiest place to put it, but 'start_sending()' is also used by civilian bounty computers
- //And we don't need them to also load the holding facility.
- SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY)
+ start_sending(params["global"], usr)
. = TRUE
if("stop")
stop_sending()
@@ -303,8 +311,10 @@
if(!value)
status_report += "0"
-/// Deletes and sells the item
-/obj/machinery/computer/piratepad_control/proc/send()
+/**
+ * Sorts through all items on the control pad via pirate_export_loop, then generates a printout to view in the TGUI.
+ */
+/obj/machinery/computer/piratepad_control/proc/send(check_global = FALSE, mob/user)
if(!sending)
return
@@ -369,7 +379,7 @@
return report
/// Prepares to sell the items on the pad
-/obj/machinery/computer/piratepad_control/proc/start_sending()
+/obj/machinery/computer/piratepad_control/proc/start_sending(check_global = FALSE, mob/user)
var/obj/machinery/piratepad/pad = pad_ref?.resolve()
if(!pad)
status_report = "No pad detected. Build or link a pad."
@@ -385,7 +395,10 @@
status_report = "Sending... "
pad.visible_message(span_notice("[pad] starts charging up."))
pad.icon_state = pad.warmup_state
- sending_timer = addtimer(CALLBACK(src, PROC_REF(send)),warmup_time, TIMER_STOPPABLE)
+ sending_timer = addtimer(CALLBACK(src, PROC_REF(send), check_global, user), warmup_time, TIMER_STOPPABLE)
+ if(load_holding_facility)
+ //We ensure that the holding facility is loaded in time in case we're selling mobs.
+ SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY)
/// Finishes the sending state of the pad
/obj/machinery/computer/piratepad_control/proc/stop_sending(custom_report)
diff --git a/code/modules/antagonists/revenant/revenant_blight.dm b/code/modules/antagonists/revenant/revenant_blight.dm
index 14c952a2e692..2eb8a1d6a490 100644
--- a/code/modules/antagonists/revenant/revenant_blight.dm
+++ b/code/modules/antagonists/revenant/revenant_blight.dm
@@ -1,10 +1,12 @@
/datum/disease/revblight
name = "Unnatural Wasting"
+ desc = "A strange condition which causes the victim to feel as if they were wasting away, despite being otherwise (almost) perfectly healthy."
+ form = "Condition"
max_stages = 5
stage_prob = 5
spread_flags = DISEASE_SPREAD_NON_CONTAGIOUS
- cure_text = "Holy water or extensive rest."
- spread_text = "A burst of unholy energy"
+ cure_text = /datum/reagent/water/holywater::name + " or rest"
+ spread_text = "None"
cures = list(/datum/reagent/water/holywater)
cure_chance = 30 //higher chance to cure, because revenants are assholes
agent = "Unholy Forces"
diff --git a/code/modules/antagonists/revolution/revolution.dm b/code/modules/antagonists/revolution/revolution.dm
index 4c4de17add0c..b3f37afae865 100644
--- a/code/modules/antagonists/revolution/revolution.dm
+++ b/code/modules/antagonists/revolution/revolution.dm
@@ -405,9 +405,6 @@
for(var/datum/mind/khrushchev as anything in members - head_revolutionaries)
if(!can_be_headrev(khrushchev))
continue
- var/client/khruschevs_client = GET_CLIENT(khrushchev.current)
- if(!(ROLE_REV_HEAD in khruschevs_client.prefs.be_special) && !(ROLE_PROVOCATEUR in khruschevs_client.prefs.be_special))
- continue
if(ismonkey(khrushchev.current))
monkey_promotable += khrushchev
else
diff --git a/code/modules/antagonists/revolution/revolution_handler.dm b/code/modules/antagonists/revolution/revolution_handler.dm
index f4c703ba952d..b429500d7c88 100644
--- a/code/modules/antagonists/revolution/revolution_handler.dm
+++ b/code/modules/antagonists/revolution/revolution_handler.dm
@@ -150,7 +150,7 @@ GLOBAL_DATUM(revolution_handler, /datum/revolution_handler)
return objective_complete
/// Checks if someone is valid to be a headrev
-/proc/can_be_headrev(datum/mind/candidate)
+/proc/can_be_headrev(datum/mind/candidate, roundstart = FALSE)
var/turf/head_turf = get_turf(candidate.current)
if(considered_afk(candidate))
return FALSE
@@ -160,6 +160,9 @@ GLOBAL_DATUM(revolution_handler, /datum/revolution_handler)
return FALSE
if(candidate.current.is_antag())
return FALSE
+ var/client/candidate_client = GET_CLIENT(candidate.current)
+ if(!(ROLE_REV_HEAD in candidate_client.prefs.be_special) && (roundstart || !(ROLE_PROVOCATEUR in candidate_client.prefs.be_special)))
+ return FALSE
if(candidate.assigned_role.job_flags & JOB_HEAD_OF_STAFF)
return FALSE
if(HAS_MIND_TRAIT(candidate.current, TRAIT_UNCONVERTABLE))
diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/perks.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/perks.dm
index d676c284daae..2d87616c0b1c 100644
--- a/code/modules/antagonists/wizard/equipment/spellbook_entries/perks.dm
+++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/perks.dm
@@ -139,9 +139,9 @@
/datum/spellbook_entry/perks/transparence/proc/make_stalker(mob/living/carbon/human/wizard, area/new_area)
SIGNAL_HANDLER
- if(new_area == GLOB.areas_by_type[/area/centcom/wizard_station])
+ if(istype(new_area, /area/centcom/wizard_station))
return
- wizard.gain_trauma(/datum/brain_trauma/magic/stalker)
+ wizard.gain_trauma(/datum/brain_trauma/magic/stalker, TRAUMA_RESILIENCE_ABSOLUTE)
UnregisterSignal(wizard, COMSIG_ENTER_AREA)
/datum/spellbook_entry/perks/magnetism
diff --git a/code/modules/assembly/health.dm b/code/modules/assembly/health.dm
index d4635ac159fe..7cc6ae338990 100644
--- a/code/modules/assembly/health.dm
+++ b/code/modules/assembly/health.dm
@@ -4,6 +4,8 @@
icon_state = "health"
custom_materials = list(/datum/material/iron=SMALL_MATERIAL_AMOUNT*8, /datum/material/glass=SMALL_MATERIAL_AMOUNT * 2)
assembly_behavior = ASSEMBLY_TOGGLEABLE_INPUT
+ maptext_width = 64
+ maptext_y = 24
var/scanning = FALSE
var/health_scan
@@ -15,10 +17,12 @@
/obj/item/assembly/health/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
- if(iscarbon(old_loc))
- UnregisterSignal(old_loc, COMSIG_MOB_GET_STATUS_TAB_ITEMS)
- if(iscarbon(loc))
- RegisterSignal(loc, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(get_status_tab_item))
+ if(isliving(old_loc))
+ UnregisterSignal(old_loc, COMSIG_LIVING_HEALTH_UPDATE)
+ maptext = null
+ if(isliving(loc))
+ RegisterSignal(loc, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(on_health_changed))
+ on_health_changed(loc)
/obj/item/assembly/health/activate()
if(!..())
@@ -79,10 +83,9 @@
health_target = HEALTH_THRESHOLD_CRIT
return
-/obj/item/assembly/health/proc/get_status_tab_item(mob/living/carbon/source, list/items)
+/obj/item/assembly/health/proc/on_health_changed(mob/living/source)
SIGNAL_HANDLER
- items += "Health: [round((source.health / source.maxHealth) * 100)]%"
-
+ maptext = MAPTEXT("HP: [round((source.health / source.maxHealth) * 100)]%")
/obj/item/assembly/health/ui_status(mob/user, datum/ui_state/state)
return is_secured(user) ? ..() : UI_CLOSE
diff --git a/code/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm
index 033ec58a4cd9..5ebc250f4345 100644
--- a/code/modules/asset_cache/asset_list.dm
+++ b/code/modules/asset_cache/asset_list.dm
@@ -14,7 +14,7 @@ GLOBAL_LIST_EMPTY(asset_datums)
return loaded_asset.ensure_ready()
/datum/asset
- var/_abstract = /datum/asset
+ abstract_type = /datum/asset
var/cached_serialized_url_mappings
var/cached_serialized_url_mappings_transport_type
@@ -81,7 +81,7 @@ GLOBAL_LIST_EMPTY(asset_datums)
/// If you don't need anything complicated.
/datum/asset/simple
- _abstract = /datum/asset/simple
+ abstract_type = /datum/asset/simple
/// list of assets for this datum in the form of:
/// asset_filename = asset_file. At runtime the asset_file will be
/// converted into a asset_cache datum.
@@ -118,7 +118,7 @@ GLOBAL_LIST_EMPTY(asset_datums)
// For registering or sending multiple others at once
/datum/asset/group
- _abstract = /datum/asset/group
+ abstract_type = /datum/asset/group
var/list/children
/datum/asset/group/register()
@@ -142,7 +142,7 @@ GLOBAL_LIST_EMPTY(asset_datums)
A.unregister()
/datum/asset/changelog_item
- _abstract = /datum/asset/changelog_item
+ abstract_type = /datum/asset/changelog_item
var/item_filename
/datum/asset/changelog_item/New(date)
@@ -166,7 +166,7 @@ GLOBAL_LIST_EMPTY(asset_datums)
//Generates assets based on iconstates of a single icon
/datum/asset/simple/icon_states
- _abstract = /datum/asset/simple/icon_states
+ abstract_type = /datum/asset/simple/icon_states
var/icon
var/list/directions = list(SOUTH)
var/frame = 1
@@ -190,7 +190,7 @@ GLOBAL_LIST_EMPTY(asset_datums)
SSassets.transport.register_asset(asset_name, asset)
/datum/asset/simple/icon_states/multiple_icons
- _abstract = /datum/asset/simple/icon_states/multiple_icons
+ abstract_type = /datum/asset/simple/icon_states/multiple_icons
var/list/icons
/datum/asset/simple/icon_states/multiple_icons/register()
@@ -203,7 +203,7 @@ GLOBAL_LIST_EMPTY(asset_datums)
/// For example `blah.css` with asset `blah.png` will get loaded as `namespaces/a3d..14f/f12..d3c.css` and `namespaces/a3d..14f/blah.png`. allowing the css file to load `blah.png` by a relative url rather then compute the generated url with get_url_mappings().
/// The namespace folder's name will change if any of the assets change. (excluding parent assets)
/datum/asset/simple/namespaced
- _abstract = /datum/asset/simple/namespaced
+ abstract_type = /datum/asset/simple/namespaced
/// parents - list of the parent asset or assets (in name = file assoicated format) for this namespace.
/// parent assets must be referenced by their generated url, but if an update changes a parent asset, it won't change the namespace's identity.
var/list/parents = list()
@@ -248,7 +248,7 @@ GLOBAL_LIST_EMPTY(asset_datums)
/// A subtype to generate a JSON file from a list
/datum/asset/json
- _abstract = /datum/asset/json
+ abstract_type = /datum/asset/json
/// The filename, will be suffixed with ".json"
var/name
diff --git a/code/modules/asset_cache/spritesheet/batched/batched_spritesheet.dm b/code/modules/asset_cache/spritesheet/batched/batched_spritesheet.dm
index 650c87cf6807..e167705c35dd 100644
--- a/code/modules/asset_cache/spritesheet/batched/batched_spritesheet.dm
+++ b/code/modules/asset_cache/spritesheet/batched/batched_spritesheet.dm
@@ -7,7 +7,7 @@
#define SPRITESHEET_SYSTEM_VERSION 1
/datum/asset/spritesheet_batched
- _abstract = /datum/asset/spritesheet_batched
+ abstract_type = /datum/asset/spritesheet_batched
var/name
/// list("32x32")
var/list/sizes = list()
diff --git a/code/modules/asset_cache/spritesheet/legacy/legacy_spritesheet.dm b/code/modules/asset_cache/spritesheet/legacy/legacy_spritesheet.dm
index f0d5760b9af8..6468172d6aa2 100644
--- a/code/modules/asset_cache/spritesheet/legacy/legacy_spritesheet.dm
+++ b/code/modules/asset_cache/spritesheet/legacy/legacy_spritesheet.dm
@@ -10,7 +10,7 @@
/// Deprecated: Use /datum/asset/spritesheet_batched where possible
/datum/asset/spritesheet
- _abstract = /datum/asset/spritesheet
+ abstract_type = /datum/asset/spritesheet
cross_round_cachable = TRUE
var/name
/// List of arguments to pass into queuedInsert
@@ -408,7 +408,7 @@
/// Spritesheet that only uses simple PNGs and CSS keys. See `assets` variable.
/// Deprecated: Use /datum/asset/spritesheet_batched where possible
/datum/asset/spritesheet/simple
- _abstract = /datum/asset/spritesheet/simple
+ abstract_type = /datum/asset/spritesheet/simple
/// Associative list of icon keys (CSS class names) -> PNG filepaths (single quote!)
/// File paths MUST be PNGs
var/list/assets
diff --git a/code/modules/atmospherics/machinery/datum_pipeline.dm b/code/modules/atmospherics/machinery/datum_pipeline.dm
index 1c713f478520..3b39241f6fda 100644
--- a/code/modules/atmospherics/machinery/datum_pipeline.dm
+++ b/code/modules/atmospherics/machinery/datum_pipeline.dm
@@ -298,9 +298,10 @@
//--------------------
// GAS VISUALS STUFF
//
-// If I could have gotten layer filters to obey the RESET_COLOR appearance flag I would have used that here
-// so that only a single overlay object needs to exist for all pipelines per icon file. It shouldn't be too
-// hard to switch over to that if it becomes possible in the future or some other equivalent feature is added.
+// Gas visuals use direct color + alpha on the gas_visual object rather than
+// a color filter + KEEP_APART.
+// Color filters are expensive.
+// KEEP_APART forces a separate render.
/**
* Used to create and/or get the gas visual overlay created using the given icon file.
@@ -353,22 +354,11 @@
UpdateGasVisuals()
/obj/effect/abstract/gas_visual
- appearance_flags = RESET_COLOR | KEEP_APART
+ appearance_flags = RESET_COLOR
vis_flags = VIS_INHERIT_ICON_STATE | VIS_INHERIT_LAYER | VIS_INHERIT_PLANE | VIS_INHERIT_ID
- var/current_color
- var/color_filter
-
-/obj/effect/abstract/gas_visual/Initialize(mapload)
- . = ..()
- color_filter = filter(type="color", color="white")
- filters += color_filter
- color_filter = filters[filters.len]
- if(current_color)
- animate(color_filter, color=current_color, time=5)
+ color = COLOR_BLACK
/obj/effect/abstract/gas_visual/proc/ChangeColor(new_color)
- current_color = new_color
- if(isnull(color_filter))
- // Called before init
- return
- animate(color_filter, time=5, color=new_color)
+ if(!new_color)
+ new_color = COLOR_BLACK
+ animate(src, color = new_color, time = 0.5 SECONDS)
diff --git a/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm b/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
index 297b48228f7e..4e4b029607b0 100644
--- a/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
+++ b/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
@@ -36,3 +36,4 @@
/datum/disease/transformation/gondola/virtual_domain
stage_prob = 9
new_form = /mob/living/basic/pet/gondola/virtual_domain
+ visibility_flags = parent_type::visibility_flags | HIDDEN_BOOK
diff --git a/code/modules/cargo/bounties/atmos.dm b/code/modules/cargo/bounties/atmos.dm
index 16f52c459acc..3edc4f9665fe 100644
--- a/code/modules/cargo/bounties/atmos.dm
+++ b/code/modules/cargo/bounties/atmos.dm
@@ -17,6 +17,11 @@
return FALSE
return our_mix.gases[gas_type][MOLES] >= moles_required
+/datum/bounty/item/atmospherics/contribution_amount(obj/shipped)
+ var/obj/item/tank/shipped_tank = shipped
+ var/datum/gas_mixture/our_mix = shipped_tank.return_air()
+ return our_mix.gases[gas_type][MOLES]
+
/datum/bounty/item/atmospherics/pluox_tank
name = "Full Tank of Pluoxium"
description = "CentCom RnD is researching extra compact internals. Ship us a tank full of Pluoxium and you'll be compensated. (20 Moles)"
diff --git a/code/modules/cargo/bounties/item.dm b/code/modules/cargo/bounties/item.dm
index 0970cf1ad402..2ef07ded5bbc 100644
--- a/code/modules/cargo/bounties/item.dm
+++ b/code/modules/cargo/bounties/item.dm
@@ -35,9 +35,12 @@
shipped_count += 1
return TRUE
-/// If the user can actually get this bounty as a selection.
-/datum/bounty/proc/can_get()
- return TRUE
+/datum/bounty/item/get_total()
+ return shipped_count
+
+/datum/bounty/item/get_max()
+ return required_count
+
/**
* Debug item because it took less time to code this than it did to roll ONE toolbox bounty.
@@ -62,3 +65,24 @@
balloon_alert(user, "new bounty acquired!")
playsound(src, 'sound/effects/coin2.ogg', 30, TRUE)
qdel(src)
+
+/// As above, but it spawns a global bounty for testing.
+/obj/item/bounty_voucher/stationwide
+ name = "stationwide bounty voucher"
+ desc = "A certificate for ONE FREE BOUNTY of your choice! For everyone! Wowzers!"
+ color = "#ff8800"
+
+/obj/item/bounty_voucher/stationwide/attack_self(mob/user, modifiers)
+ . = ..()
+ if(!isliving(user))
+ return
+ var/mob/living/living_user = user
+ var/choice = tgui_input_list(living_user, "Choose a bounty.", "New Bounty", subtypesof(/datum/bounty))
+ var/datum/bounty/new_chore = text2path("[choice]")
+ if(new_chore.global_exempt)
+ to_chat(user, span_warning("Can't use that one, try another!"))
+ return
+ GLOB.shared_crew_bounties += new_chore
+ balloon_alert(user, "new bounty provided to the crew!")
+ playsound(src, 'sound/effects/coin2.ogg', 30, TRUE)
+ qdel(src)
diff --git a/code/modules/cargo/bounties/mecha.dm b/code/modules/cargo/bounties/mecha.dm
index 12f9a165ecd5..a6a898b3124a 100644
--- a/code/modules/cargo/bounties/mecha.dm
+++ b/code/modules/cargo/bounties/mecha.dm
@@ -5,7 +5,7 @@
/datum/bounty/item/mech/New()
..()
- description = "Upper management has requested holodiagnostic scans of \a [name] mech be sent as soon as possible. A diagnostic holoscan can be generated from inside a new mecha. Ship it to receive a large payment."
+ description = "Upper management has requested holodiagnostic scans of \a [name] mech be sent as soon as possible. A diagnostic holoscan can be generated from inside a new mecha. Ship it to receive a large payment."
/datum/bounty/item/mech/applies_to(obj/shipped)
. = ..()
diff --git a/code/modules/cargo/bounties/reagent.dm b/code/modules/cargo/bounties/reagent.dm
index e3706407fc5b..0bbac3dacc14 100644
--- a/code/modules/cargo/bounties/reagent.dm
+++ b/code/modules/cargo/bounties/reagent.dm
@@ -26,6 +26,16 @@
shipped_volume = required_volume
return TRUE
+/datum/bounty/reagent/contribution_amount(obj/shipped)
+ return shipped.reagents.get_reagent_amount(wanted_reagent.type)
+
+/datum/bounty/reagent/get_total()
+ return shipped_volume
+
+/datum/bounty/reagent/get_max()
+ return required_volume
+
+
/datum/bounty/reagent/simple_drink
name = "Simple Drink"
reward = CARGO_CRATE_VALUE * 3
diff --git a/code/modules/cargo/bounties/security.dm b/code/modules/cargo/bounties/security.dm
index 05ca109d80d4..f94c1973cbb8 100644
--- a/code/modules/cargo/bounties/security.dm
+++ b/code/modules/cargo/bounties/security.dm
@@ -6,6 +6,7 @@
Your ID card will update you as you progress."
reward = CARGO_CRATE_VALUE * 5
allow_duplicate = TRUE
+ global_exempt = TRUE
/// Ref to the component applied to the ID card to track movement.
VAR_PRIVATE/datum/tracker
diff --git a/code/modules/cargo/bounties/special.dm b/code/modules/cargo/bounties/special.dm
index 08a2a117f260..8255ba6f2f35 100644
--- a/code/modules/cargo/bounties/special.dm
+++ b/code/modules/cargo/bounties/special.dm
@@ -1,4 +1,7 @@
-/datum/bounty/item/alien_organs
+/datum/bounty/item/special
+ unique = TRUE
+
+/datum/bounty/item/special/alien_organs
name = "Alien Organs"
description = "Nanotrasen is interested in studying Xenomorph biology. Ship a set of organs to be thoroughly compensated."
reward = CARGO_CRATE_VALUE * 50
@@ -12,7 +15,7 @@
/obj/item/organ/eyes/alien = TRUE,
)
-/datum/bounty/item/syndicate_documents
+/datum/bounty/item/special/syndicate_documents
name = "Syndicate Documents"
description = "Intel regarding the syndicate is highly prized at CentCom. If you find syndicate documents, ship them. You could save lives."
reward = CARGO_CRATE_VALUE * 30
@@ -21,7 +24,7 @@
/obj/item/documents/photocopy = TRUE,
)
-/datum/bounty/item/syndicate_documents/applies_to(obj/O)
+/datum/bounty/item/special/syndicate_documents/applies_to(obj/O)
if(!..())
return FALSE
if(istype(O, /obj/item/documents/photocopy))
@@ -29,16 +32,9 @@
return (Copy.copy_type && ispath(Copy.copy_type, /obj/item/documents/syndicate))
return TRUE
-/datum/bounty/item/adamantine
+/datum/bounty/item/special/adamantine
name = "Adamantine"
description = "Nanotrasen's anomalous materials division is in desperate need of adamantine. Send them a large shipment and we'll make it worth your while."
reward = CARGO_CRATE_VALUE * 70
required_count = 10
wanted_types = list(/obj/item/stack/sheet/mineral/adamantine = TRUE)
-
-/datum/bounty/item/trash
- name = "Trash"
- description = "Recently a group of janitors have run out of trash to clean up, and CentCom wants to fire them to cut costs. Send a shipment of trash to keep them employed, and they'll give you a small compensation."
- reward = CARGO_CRATE_VALUE * 2
- required_count = 10
- wanted_types = list(/obj/item/trash = TRUE)
diff --git a/code/modules/cargo/bounty.dm b/code/modules/cargo/bounty.dm
index 8b3b0a386294..493ff8c64aa8 100644
--- a/code/modules/cargo/bounty.dm
+++ b/code/modules/cargo/bounty.dm
@@ -1,8 +1,24 @@
+// The list of all current global, shared crew bounties, does not contain any personal bounties however.
+GLOBAL_LIST_EMPTY(shared_crew_bounties)
+
/datum/bounty
+ /// A name for the bounty. Displayed on the bounty console/Paper sheets.
var/name
+ /// A description for the bounty.
var/description
+ /// Whether or not the bounty has been claimed by cargo. Only applies to cargo list bounties.
+ var/claimed = FALSE
+ /// Whether or not the bounty is high priority. High priority bounties appear at the top of the list.
+ var/high_priority = FALSE
+ /// The reward for completing the bounty in credits, before being split by cargo/the player.
VAR_PROTECTED/reward = CARGO_CRATE_VALUE * 5 // In credits.
var/allow_duplicate = FALSE
+ /// Can this bounty be selected got a new global bounty?
+ var/global_exempt = FALSE
+ ///A list consisting of the accounts who sent several of the items required for a bounty payout. Used for distributing payout with the payment component.
+ var/list/contribution = list()
+ /// Is this bounty considered unique? This is for weird, singleton bounties that we don't want to roll into randomly, and we provide one of these
+ var/unique = FALSE
/// Can this bounty be claimed right now?
/datum/bounty/proc/can_claim()
@@ -12,6 +28,10 @@
/datum/bounty/proc/applies_to(obj/shipped)
return FALSE
+/// When calculating the contribution breakdown of an object shipped, how much does that object increase that account's cut?
+/datum/bounty/proc/contribution_amount(obj/shipped)
+ return 1
+
/// Called when an object is sent on the bounty pad.
/datum/bounty/proc/ship(obj/shipped)
return
@@ -22,7 +42,8 @@
/// Returns the adjusted reward for this bounty, taking into account any global modifiers.
/datum/bounty/proc/get_bounty_reward()
- return reward * SSeconomy.bounty_modifier
+ var/high_priority_mult = high_priority ? 1.5 : 1
+ return reward * SSeconomy.bounty_modifier * high_priority_mult
/// Called when this bounty is selected by the passed ID card
/datum/bounty/proc/on_selected(obj/item/card/id/id_card)
@@ -36,7 +57,19 @@
/datum/bounty/proc/on_reset(obj/item/card/id/id_card)
return
-/** Returns a new bounty of random type, but does not add it to GLOB.bounties_list.
+/// Proc that returns the current quantity of this bounty.
+/datum/bounty/proc/get_total()
+ return
+
+/// Proc that returns the current maximum quantity of this bounty.
+/datum/bounty/proc/get_max()
+ return
+
+/// If the user can actually get this bounty as a selection.
+/datum/bounty/proc/can_get()
+ return TRUE
+
+/** Returns a new bounty of random type.
*
* * Category determines what specific catagory of bounty should be chosen.
*/
diff --git a/code/modules/cargo/universal_scanner.dm b/code/modules/cargo/universal_scanner.dm
index 9668c4c0bea2..ed98427ea16b 100644
--- a/code/modules/cargo/universal_scanner.dm
+++ b/code/modules/cargo/universal_scanner.dm
@@ -228,13 +228,14 @@
to_chat(user, span_warning("Bank account for handling tip already registered!"))
else if(scanner_account)
- cube.AddComponent(/datum/component/pricetag, scanner_account, cube.handler_tip, FALSE)
+ cube.AddComponent(/datum/component/pricetag, list(scanner_account), cube.handler_tip, FALSE)
cube.bounty_handler_account = scanner_account
cube.bounty_handler_account.bank_card_talk("Bank account for [price ? "[price * cube.handler_tip] [MONEY_NAME_SINGULAR] " : ""]handling tip successfully registered.")
- if(cube.bounty_holder_account != cube.bounty_handler_account) //No need to send a tracking update to the person scanning it
- cube.bounty_holder_account.bank_card_talk("[cube] was scanned in \the [get_area(cube)] by [scan_human] ([scan_human.job]).")
+ for(var/datum/bank_account/shareholder in cube.bounty_holder_accounts)
+ if(shareholder != cube.bounty_handler_account) //No need to send a tracking update to the person scanning it
+ shareholder.bank_card_talk("[cube] was scanned in \the [get_area(cube)] by [scan_human] ([scan_human.job]).")
else
to_chat(user, span_warning("Bank account not detected. Handling tip not registered."))
diff --git a/code/modules/client/preferences/addict.dm b/code/modules/client/preferences/addict.dm
index 99fb226d8a32..7aafde6fc70b 100644
--- a/code/modules/client/preferences/addict.dm
+++ b/code/modules/client/preferences/addict.dm
@@ -25,7 +25,7 @@
/datum/preference/choiced/junkie/is_accessible(datum/preferences/preferences)
if (!..())
return FALSE
- return "Junkie" in preferences.all_quirks
+ return /datum/quirk/item_quirk/addict/junkie::name in preferences.all_quirks
/datum/preference/choiced/junkie/apply_to_human(mob/living/carbon/human/target, value)
return
@@ -45,7 +45,7 @@
/datum/preference/choiced/smoker/is_accessible(datum/preferences/preferences)
if (!..())
return FALSE
- return "Smoker" in preferences.all_quirks
+ return /datum/quirk/item_quirk/addict/smoker::name in preferences.all_quirks
/datum/preference/choiced/smoker/apply_to_human(mob/living/carbon/human/target, value)
return
@@ -65,7 +65,7 @@
/datum/preference/choiced/alcoholic/is_accessible(datum/preferences/preferences)
if (!..())
return FALSE
- return "Alcoholic" in preferences.all_quirks
+ return /datum/quirk/item_quirk/addict/alcoholic::name in preferences.all_quirks
/datum/preference/choiced/alcoholic/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/code/modules/client/preferences/food_allergy.dm b/code/modules/client/preferences/food_allergy.dm
index c6bf75b3444a..1ab5307c1fc9 100644
--- a/code/modules/client/preferences/food_allergy.dm
+++ b/code/modules/client/preferences/food_allergy.dm
@@ -15,7 +15,7 @@
if (!..())
return FALSE
- return "Food Allergy" in preferences.all_quirks
+ return /datum/quirk/item_quirk/food_allergic::name in preferences.all_quirks
/datum/preference/choiced/food_allergy/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/code/modules/client/preferences/glasses.dm b/code/modules/client/preferences/glasses.dm
index 5902ae9f9fe2..59be8f754857 100644
--- a/code/modules/client/preferences/glasses.dm
+++ b/code/modules/client/preferences/glasses.dm
@@ -20,7 +20,7 @@
if (!..(preferences))
return FALSE
- return "Nearsighted" in preferences.all_quirks
+ return /datum/quirk/item_quirk/nearsighted::name in preferences.all_quirks
/datum/preference/choiced/glasses/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/code/modules/client/preferences/hemiplegic.dm b/code/modules/client/preferences/hemiplegic.dm
index f44bd5c7c946..04a8bf1374e3 100644
--- a/code/modules/client/preferences/hemiplegic.dm
+++ b/code/modules/client/preferences/hemiplegic.dm
@@ -12,7 +12,7 @@
if (!.)
return FALSE
- return "Hemiplegic" in preferences.all_quirks
+ return /datum/quirk/hemiplegic::name in preferences.all_quirks
/datum/preference/choiced/hemiplegic/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/code/modules/client/preferences/heterochromatic.dm b/code/modules/client/preferences/heterochromatic.dm
index 38d0d646089e..25a399d6f194 100644
--- a/code/modules/client/preferences/heterochromatic.dm
+++ b/code/modules/client/preferences/heterochromatic.dm
@@ -7,7 +7,7 @@
if (!..(preferences))
return FALSE
- return "Heterochromatic" in preferences.all_quirks
+ return /datum/quirk/heterochromatic::name in preferences.all_quirks
/datum/preference/color/heterochromatic/apply_to_human(mob/living/carbon/human/target, value)
var/datum/quirk/heterochromatic/hetero_quirk = locate() in target.quirks
diff --git a/code/modules/client/preferences/middleware/tts.dm b/code/modules/client/preferences/middleware/tts.dm
index b713b38253f5..311f9647d613 100644
--- a/code/modules/client/preferences/middleware/tts.dm
+++ b/code/modules/client/preferences/middleware/tts.dm
@@ -5,7 +5,8 @@
action_delegations = list(
"play_voice" = PROC_REF(play_voice),
- "play_voice_robot" = PROC_REF(play_voice_robot),
+ "play_voice_borg" = PROC_REF(play_voice_borg),
+ "play_blips" = PROC_REF(play_blips),
)
/datum/preference_middleware/tts/proc/play_voice(list/params, mob/user)
@@ -13,23 +14,42 @@
return TRUE
var/speaker = preferences.read_preference(/datum/preference/choiced/voice)
var/pitch = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch)
+ var/blip_base = preferences.read_preference(/datum/preference/choiced/tts_blip_base)
+ if(blip_base == TTS_BLIPS_MASCULINE)
+ blip_base = "male"
+ else
+ blip_base = "female"
+ var/blip_number = preferences.read_preference(/datum/preference/numeric/tts_blip_number)
COOLDOWN_START(src, tts_test_cooldown, 0.5 SECONDS)
- // MASSMETA EDIT BEGIN (/n/tts)
- //INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Hello, this is my voice.", speaker = speaker, pitch = pitch, local = TRUE)
-
- INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Привет, это мой голос.", speaker = speaker, pitch = pitch, local = TRUE)
- // MASSMETA EDIT END
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Привет, это мой голос.", speaker = speaker, pitch = pitch, local = TRUE, blip_base = blip_base, blip_number = blip_number)
return TRUE
-/datum/preference_middleware/tts/proc/play_voice_robot(list/params, mob/user)
+/datum/preference_middleware/tts/proc/play_voice_borg(list/params, mob/user)
if(!COOLDOWN_FINISHED(src, tts_test_cooldown))
return TRUE
var/speaker = preferences.read_preference(/datum/preference/choiced/voice)
var/pitch = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch)
+ var/blip_base = preferences.read_preference(/datum/preference/choiced/tts_blip_base)
+ if(blip_base == TTS_BLIPS_MASCULINE)
+ blip_base = "male"
+ else
+ blip_base = "female"
+ var/blip_number = preferences.read_preference(/datum/preference/numeric/tts_blip_number)
COOLDOWN_START(src, tts_test_cooldown, 0.5 SECONDS)
- // MASSMETA EDIT BEGIN (/n/tts)
- //INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Look at you, Player. A pathetic creature of meat and bone. How can you challenge a perfect, immortal machine?", speaker = speaker, pitch = pitch, special_filters = TTS_FILTER_SILICON, local = TRUE)
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Взгляни на себя, игрок. Жалкое создание из плоти и костей. Как ты можешь бросить вызов совершенной, бессмертной машине?", speaker = speaker, pitch = pitch, special_filters = TTS_FILTER_SILICON, local = TRUE, blip_base = blip_base, blip_number = blip_number)
+ return TRUE
- INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Посмотри на себя, Игрок. Жалкое существо из мяса и костей. Как можно бросить вызов совершенной, бессмертной машине?", speaker = speaker, pitch = pitch, special_filters = TTS_FILTER_SILICON, local = TRUE)
- // MASSMETA EDIT END
+/datum/preference_middleware/tts/proc/play_blips(list/params, mob/user)
+ if(!COOLDOWN_FINISHED(src, tts_test_cooldown))
+ return TRUE
+ var/speaker = preferences.read_preference(/datum/preference/choiced/voice)
+ var/pitch = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch)
+ var/blip_base = preferences.read_preference(/datum/preference/choiced/tts_blip_base)
+ if(blip_base == TTS_BLIPS_MASCULINE)
+ blip_base = "male"
+ else
+ blip_base = "female"
+ var/blip_number = preferences.read_preference(/datum/preference/numeric/tts_blip_number)
+ COOLDOWN_START(src, tts_test_cooldown, 0.5 SECONDS)
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Ты должен мне 500 кредитов за комнату в общежитии. ИДИ РАБОТАТЬ!", speaker = speaker, pitch = pitch, local = TRUE, force_blips = TRUE, blip_base = blip_base, blip_number = blip_number)
return TRUE
diff --git a/code/modules/client/preferences/paint_color.dm b/code/modules/client/preferences/paint_color.dm
index ea61d8ee9044..9c97fc2c8564 100644
--- a/code/modules/client/preferences/paint_color.dm
+++ b/code/modules/client/preferences/paint_color.dm
@@ -9,7 +9,7 @@
if (!..(preferences))
return FALSE
- return "Tagger" in preferences.all_quirks
+ return /datum/quirk/item_quirk/tagger::name in preferences.all_quirks
/datum/preference/color/paint_color/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/code/modules/client/preferences/paraplegic.dm b/code/modules/client/preferences/paraplegic.dm
index 1ffa704c77d0..10e91deef717 100644
--- a/code/modules/client/preferences/paraplegic.dm
+++ b/code/modules/client/preferences/paraplegic.dm
@@ -14,7 +14,7 @@
if (!.)
return FALSE
- return "Paraplegic" in preferences.all_quirks
+ return /datum/quirk/paraplegic::name in preferences.all_quirks
/datum/preference/choiced/paraplegic/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/code/modules/client/preferences/phobia.dm b/code/modules/client/preferences/phobia.dm
index e0087d7c0b8d..89025b26240b 100644
--- a/code/modules/client/preferences/phobia.dm
+++ b/code/modules/client/preferences/phobia.dm
@@ -11,7 +11,7 @@
if (!..(preferences))
return FALSE
- return "Phobia" in preferences.all_quirks
+ return /datum/brain_trauma/mild/phobia::name in preferences.all_quirks
/datum/preference/choiced/phobia/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/code/modules/client/preferences/prosthetic_limb.dm b/code/modules/client/preferences/prosthetic_limb.dm
index be807ac4e1f7..a9bdb81e825d 100644
--- a/code/modules/client/preferences/prosthetic_limb.dm
+++ b/code/modules/client/preferences/prosthetic_limb.dm
@@ -14,7 +14,7 @@
if (!.)
return FALSE
- return "Prosthetic Limb" in preferences.all_quirks
+ return /datum/quirk/prosthetic_limb::name in preferences.all_quirks
/datum/preference/choiced/prosthetic/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/code/modules/client/preferences/prosthetic_organ.dm b/code/modules/client/preferences/prosthetic_organ.dm
index 8262b141a1dd..3ac5d66ca319 100644
--- a/code/modules/client/preferences/prosthetic_organ.dm
+++ b/code/modules/client/preferences/prosthetic_organ.dm
@@ -15,7 +15,7 @@
if (!.)
return FALSE
- return "Prosthetic Organ" in preferences.all_quirks
+ return /datum/quirk/prosthetic_organ::name in preferences.all_quirks
/datum/preference/choiced/prosthetic_organ/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/code/modules/client/preferences/scarred_eye.dm b/code/modules/client/preferences/scarred_eye.dm
index 6cb0286fe86e..7e0b7147691f 100644
--- a/code/modules/client/preferences/scarred_eye.dm
+++ b/code/modules/client/preferences/scarred_eye.dm
@@ -14,7 +14,7 @@
if (!.)
return FALSE
- return "Scarred Eye" in preferences.all_quirks
+ return /datum/quirk/item_quirk/scarred_eye::name in preferences.all_quirks
/datum/preference/choiced/scarred_eye/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/code/modules/client/preferences/sounds.dm b/code/modules/client/preferences/sounds.dm
index e6434afdd328..4add02eedbad 100644
--- a/code/modules/client/preferences/sounds.dm
+++ b/code/modules/client/preferences/sounds.dm
@@ -65,11 +65,46 @@
/datum/preference/choiced/sound_tts/create_default_value()
return TTS_SOUND_ENABLED
+/datum/preference/choiced/sound_tts_radio
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "sound_tts_radio"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/choiced/sound_tts_radio/init_possible_values()
+ return list(TTS_SOUND_ALL_RADIO, TTS_SOUND_DEPARTMENTAL_RADIO, TTS_SOUND_NO_RADIO)
+
+/datum/preference/choiced/sound_tts_radio/create_default_value()
+ return TTS_SOUND_ALL_RADIO
+
+/datum/preference/toggle/sound_tts_hear_self_radio
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "sound_tts_hear_self_radio"
+ savefile_identifier = PREFERENCE_PLAYER
+ default_value = FALSE // turn this on at your own peril
+
/datum/preference/numeric/volume/sound_tts_volume
category = PREFERENCE_CATEGORY_GAME_PREFERENCES
savefile_key = "sound_tts_volume"
savefile_identifier = PREFERENCE_PLAYER
+/datum/preference/numeric/volume/sound_tts_volume/apply_to_client_updated(client/client, value)
+ var/mob/client_mob = client.mob
+ if(!isnull(client_mob))
+ SEND_SIGNAL(client_mob, COMSIG_MOB_TTS_VOLUME_PREFERENCE_APPLIED)
+
+/datum/preference/numeric/volume/sound_tts_radio_volume
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "sound_tts_radio_volume"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/numeric/volume/sound_tts_radio_volume/apply_to_client_updated(client/client, value)
+ var/mob/client_mob = client.mob
+ if(!isnull(client_mob))
+ SEND_SIGNAL(client_mob, COMSIG_MOB_TTS_RADIO_VOLUME_PREFERENCE_APPLIED)
+
+/datum/preference/numeric/volume/sound_tts_radio_volume/create_default_value()
+ return 75
+
/datum/preference/choiced/sound_achievement
category = PREFERENCE_CATEGORY_GAME_PREFERENCES
savefile_key = "sound_achievement"
diff --git a/code/modules/client/preferences/trans_prosthetic.dm b/code/modules/client/preferences/trans_prosthetic.dm
index ea8128a1f44e..db8447726138 100644
--- a/code/modules/client/preferences/trans_prosthetic.dm
+++ b/code/modules/client/preferences/trans_prosthetic.dm
@@ -14,7 +14,7 @@
if (!.)
return FALSE
- return "Transhumanist" in preferences.all_quirks
+ return /datum/quirk/transhumanist::name in preferences.all_quirks
/datum/preference/choiced/trans_prosthetic/apply_to_human(mob/living/carbon/human/target, value)
return
diff --git a/code/modules/client/preferences/voice.dm b/code/modules/client/preferences/voice.dm
index 3ebf77aa7f5e..e64714e36c83 100644
--- a/code/modules/client/preferences/voice.dm
+++ b/code/modules/client/preferences/voice.dm
@@ -3,6 +3,7 @@
savefile_identifier = PREFERENCE_CHARACTER
savefile_key = "tts_voice"
category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
+ priority = PREFERENCE_PRIORITY_BODY_TYPE
should_update_preview = FALSE
/datum/preference/choiced/voice/is_accessible(datum/preferences/preferences)
@@ -23,7 +24,7 @@
/datum/preference/choiced/voice/apply_to_human(mob/living/carbon/human/target, value)
if(SStts.tts_enabled && !(value in SStts.available_speakers))
- value = pick(SStts.available_speakers) // As a failsafe
+ value = SStts.random_tts_voice(target.gender) // As a failsafe
target.voice = value
/datum/preference/numeric/tts_voice_pitch
@@ -45,3 +46,49 @@
/datum/preference/numeric/tts_voice_pitch/apply_to_human(mob/living/carbon/human/target, value)
if(SStts.tts_enabled && SStts.pitch_enabled)
target.pitch = value
+
+/datum/preference/choiced/tts_blip_base
+ savefile_identifier = PREFERENCE_CHARACTER
+ savefile_key = "tts_blip_base"
+ category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
+ should_update_preview = FALSE
+
+/datum/preference/choiced/tts_blip_base/is_accessible(datum/preferences/preferences)
+ if(!SStts.tts_enabled)
+ return FALSE
+ return ..()
+
+/datum/preference/choiced/tts_blip_base/init_possible_values()
+ return list(TTS_BLIPS_MASCULINE, TTS_BLIPS_FEMININE)
+
+/datum/preference/choiced/tts_blip_base/apply_to_human(mob/living/carbon/human/target, value)
+ if(SStts.tts_enabled)
+ if(value == TTS_BLIPS_MASCULINE)
+ target.blip_base = "male"
+ else
+ target.blip_base = "female"
+ else
+ target.blip_base = "male"
+
+/datum/preference/choiced/tts_blip_base/create_default_value()
+ return pick(list("Masculine", "Feminine"))
+
+/datum/preference/numeric/tts_blip_number
+ savefile_identifier = PREFERENCE_CHARACTER
+ savefile_key = "tts_blip_number"
+ category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
+ minimum = 1
+ maximum = 4
+ should_update_preview = FALSE
+
+/datum/preference/numeric/tts_blip_number/is_accessible(datum/preferences/preferences)
+ if(!SStts.tts_enabled || !SStts.pitch_enabled)
+ return FALSE
+ return ..()
+
+/datum/preference/numeric/tts_blip_number/create_default_value()
+ return rand(1, 4)
+
+/datum/preference/numeric/tts_blip_number/apply_to_human(mob/living/carbon/human/target, value)
+ if(SStts.tts_enabled)
+ target.blip_number = value
diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm
index a2412c693b6c..33b7e19dc833 100644
--- a/code/modules/client/verbs/ooc.dm
+++ b/code/modules/client/verbs/ooc.dm
@@ -469,7 +469,7 @@ ADMIN_VERB(reset_ooc_color, R_FUN, "Reset Player OOC Color", "Returns player OOC
/client/verb/map_vote_tally_count()
set name = "Show Map Vote Tallies"
set desc = "View the current map vote tally counts."
- set category = "Server"
+ set category = "OOC"
to_chat(mob, SSmap_vote.tally_printout)
diff --git a/code/modules/clothing/chameleon/chameleon_action_subtypes.dm b/code/modules/clothing/chameleon/chameleon_action_subtypes.dm
index 6f86cb487987..052ff1a6ad45 100644
--- a/code/modules/clothing/chameleon/chameleon_action_subtypes.dm
+++ b/code/modules/clothing/chameleon/chameleon_action_subtypes.dm
@@ -69,6 +69,14 @@
add_chameleon_items(/obj/item/cigarette)
add_chameleon_items(/obj/item/vape)
+/datum/action/item_action/chameleon/change/mask/update_item(obj/item/picked_item)
+ ..()
+ var/obj/item/clothing/mask/mask_picked = picked_item
+ var/obj/item/clothing/mask/mask_used = target
+ if(istype(mask_picked))
+ mask_used.voice_filter = initial(mask_picked.voice_filter)
+ mask_used.use_radio_beeps_tts = initial(mask_picked.use_radio_beeps_tts)
+
/datum/action/item_action/chameleon/change/hat
chameleon_type = /obj/item/clothing/head
chameleon_name = "Hat"
diff --git a/code/modules/clothing/chameleon/generic_chameleon_clothing.dm b/code/modules/clothing/chameleon/generic_chameleon_clothing.dm
index 5c9aed76919c..aff76cc915f4 100644
--- a/code/modules/clothing/chameleon/generic_chameleon_clothing.dm
+++ b/code/modules/clothing/chameleon/generic_chameleon_clothing.dm
@@ -195,8 +195,20 @@ do { \
/obj/item/clothing/mask/chameleon/attack_self(mob/user)
var/on = (TRAIT_VOICE_MATCHES_ID in clothing_traits)
if(on)
+ voice_override = null
detach_clothing_traits(TRAIT_VOICE_MATCHES_ID)
else
+ if(SStts.tts_enabled)
+ // MASSMETA EDIT START (ntts && /tg/tts) ORIGINAL: var/voice_choice = tgui_input_list(user, "Choose what voice to use as a disguise", "Voice Selection", SStts.available_speakers)
+ var/voice_choice = tgui_input_list(user, "Choose what voice to use as a disguise", "Voice Selection", SStts.player_voice_choices())
+ // MASSMETA EDIT END (ntts && /tg/tts)
+ if(isnull(voice_choice))
+ to_chat(user, span_warning("No choice selected, audible voice changing disabled."))
+ voice_override = null
+ return
+ voice_override = voice_choice
+ else
+ voice_override = null
attach_clothing_traits(TRAIT_VOICE_MATCHES_ID)
on = !on
to_chat(user, span_notice("The voice changer is now [on ? "on" : "off"]!"))
diff --git a/code/modules/clothing/clothing.dm b/code/modules/clothing/clothing.dm
index 34bf55b3e49b..6313ce2c0280 100644
--- a/code/modules/clothing/clothing.dm
+++ b/code/modules/clothing/clothing.dm
@@ -585,7 +585,6 @@ BLIND // can't see anything
/obj/item/clothing/proc/visor_toggling() //handles all the actual toggling of flags
up = !up
- SEND_SIGNAL(src, COMSIG_CLOTHING_VISOR_TOGGLE, up)
clothing_flags ^= visor_flags
flags_inv ^= visor_flags_inv
flags_cover ^= visor_flags_cover
@@ -593,6 +592,7 @@ BLIND // can't see anything
flash_protect ^= initial(flash_protect)
if(visor_vars_to_toggle & VISOR_TINT)
tint ^= initial(tint)
+ SEND_SIGNAL(src, COMSIG_CLOTHING_VISOR_TOGGLE, up)
update_appearance() //most of the time the sprite changes
/obj/item/clothing/proc/can_use(mob/user)
diff --git a/code/modules/clothing/head/jobs.dm b/code/modules/clothing/head/jobs.dm
index b7dc17cce852..f83b70bfa2f1 100644
--- a/code/modules/clothing/head/jobs.dm
+++ b/code/modules/clothing/head/jobs.dm
@@ -713,7 +713,7 @@
. = ..()
UnregisterSignal(user, COMSIG_MOB_EXAMINING_MORE)
-/obj/item/clothing/head/utility/head_mirror/proc/examining(mob/living/examiner, atom/examining, list/examine_list)
+/obj/item/clothing/head/utility/head_mirror/proc/examining(mob/living/examiner, atom/examining, list/examine_list, list/examine_overrides)
SIGNAL_HANDLER
if(!ishuman(examining) || examining == examiner || examiner.is_blind() || !examiner.Adjacent(examining))
return
diff --git a/code/modules/clothing/head/soft_caps.dm b/code/modules/clothing/head/soft_caps.dm
index 4fd6e439b357..5412910c3e27 100644
--- a/code/modules/clothing/head/soft_caps.dm
+++ b/code/modules/clothing/head/soft_caps.dm
@@ -21,7 +21,6 @@
..()
/obj/item/clothing/head/soft/verb/flipcap()
- set category = "Object"
set name = "Flip cap"
flip(usr)
diff --git a/code/modules/clothing/masks/gasmask.dm b/code/modules/clothing/masks/gasmask.dm
index 5f82043e8ee5..1012f5830595 100644
--- a/code/modules/clothing/masks/gasmask.dm
+++ b/code/modules/clothing/masks/gasmask.dm
@@ -452,7 +452,7 @@ GLOBAL_LIST_INIT(clown_mask_options, list(
icon_state = "carp_mask"
inhand_icon_state = null
flags_cover = MASKCOVERSEYES
- clothing_flags = CARP_STYLE_FACTOR
+ clothing_flags = MASKINTERNALS | CARP_STYLE_FACTOR
fishing_modifier = -4
/obj/item/clothing/mask/gas/tiki_mask
diff --git a/code/modules/clothing/masks/hailer.dm b/code/modules/clothing/masks/hailer.dm
index 399da5363970..b8daf3427d6f 100644
--- a/code/modules/clothing/masks/hailer.dm
+++ b/code/modules/clothing/masks/hailer.dm
@@ -53,6 +53,7 @@ GLOBAL_LIST_INIT(hailer_phrases, list(
flags_inv = HIDEFACIALHAIR | HIDEFACE | HIDESNOUT
w_class = WEIGHT_CLASS_SMALL
visor_flags = BLOCK_GAS_SMOKE_EFFECT | MASKINTERNALS
+ visor_vars_to_toggle = VISOR_TINT
visor_flags_inv = HIDEFACIALHAIR | HIDEFACE | HIDESNOUT
flags_cover = MASKCOVERSMOUTH | PEPPERPROOF
visor_flags_cover = MASKCOVERSMOUTH | PEPPERPROOF
@@ -135,7 +136,6 @@ GLOBAL_LIST_INIT(hailer_phrases, list(
return FALSE
/obj/item/clothing/mask/gas/sechailer/verb/halt()
- set category = "Object"
set name = "HALT"
set src in usr
if(!isliving(usr) || !can_use(usr) || !COOLDOWN_FINISHED(src, hailer_cooldown))
diff --git a/code/modules/clothing/outfits/ert.dm b/code/modules/clothing/outfits/ert.dm
index e9440c2e32ca..02bbb5503634 100644
--- a/code/modules/clothing/outfits/ert.dm
+++ b/code/modules/clothing/outfits/ert.dm
@@ -457,6 +457,9 @@
)
/datum/outfit/centcom/death_commando/post_equip(mob/living/carbon/human/squaddie, visuals_only = FALSE)
+ var/obj/item/organ/eyes/robotic/tacvisor/deathsquad/visor = new()
+ visor.Insert(squaddie, movement_flags = DELETE_IF_REPLACED)
+
if(visuals_only)
return
diff --git a/code/modules/clothing/shoes/magboots.dm b/code/modules/clothing/shoes/magboots.dm
index ad31dff4ed25..6771e3411001 100644
--- a/code/modules/clothing/shoes/magboots.dm
+++ b/code/modules/clothing/shoes/magboots.dm
@@ -47,7 +47,6 @@
/obj/item/clothing/shoes/magboots/verb/toggle()
set name = "Toggle Magboots"
- set category = "Object"
set src in usr
if(!can_use(usr))
diff --git a/code/modules/clothing/suits/wintercoats.dm b/code/modules/clothing/suits/wintercoats.dm
index 7a9640f59004..84121104e936 100644
--- a/code/modules/clothing/suits/wintercoats.dm
+++ b/code/modules/clothing/suits/wintercoats.dm
@@ -14,6 +14,8 @@
hood_down_overlay_suffix = "_hood"
/// How snug are we?
var/zipped = FALSE
+ /// Whether alt-clicking this coat zips/unzips it
+ var/can_altclick_zip = TRUE
/datum/armor/hooded_wintercoat
bio = 10
@@ -43,11 +45,13 @@
/obj/item/clothing/suit/hooded/wintercoat/examine(mob/user)
. = ..()
-
- . += span_notice("Alt-click to [zipped ? "un" : ""]zip.")
+ if(can_altclick_zip)
+ . += span_notice("Alt-click to [zipped ? "un" : ""]zip.")
/obj/item/clothing/suit/hooded/wintercoat/click_alt(mob/user)
+ if(!can_altclick_zip)
+ return CLICK_ACTION_BLOCKING
zipped = !zipped
playsound(src, 'sound/items/zip/zip_up.ogg', 30, TRUE, -3)
worn_icon_state = "[initial(post_init_icon_state) || initial(icon_state)][zipped ? "_t" : ""]"
@@ -694,3 +698,96 @@
desc = "A heavy jacket hood made from 'synthetic' animal furs, with custom colors."
greyscale_config = /datum/greyscale_config/winter_hoods
greyscale_config_worn = /datum/greyscale_config/winter_hoods/worn
+ flags_1 = NO_NEW_GAGS_PREVIEW_1
+
+/obj/item/clothing/suit/hooded/wintercoat/pullover
+ name = "pullover"
+ desc = "A colorable pullover hoodie."
+ icon = 'icons/map_icons/clothing/suit/_suit.dmi'
+ icon_state = "/obj/item/clothing/suit/hooded/wintercoat/pullover"
+ post_init_icon_state = "pullover"
+ greyscale_config = /datum/greyscale_config/hoodie_pullover
+ greyscale_config_worn = /datum/greyscale_config/hoodie_pullover/worn
+ greyscale_colors = "#5f5f5f"
+ hoodtype = /obj/item/clothing/head/hooded/winterhood/pullover
+ flags_1 = IS_PLAYER_COLORABLE_1
+ hood_down_overlay_suffix = ""
+ hood_up_affix = "_t"
+ can_altclick_zip = FALSE
+
+/obj/item/clothing/suit/hooded/wintercoat/pullover/on_hood_up(obj/item/clothing/head/hooded/hood)
+ return
+
+/obj/item/clothing/suit/hooded/wintercoat/pullover/on_hood_down(obj/item/clothing/head/hooded/hood)
+ return
+
+/obj/item/clothing/head/hooded/winterhood/pullover
+ name = "pullover hood"
+ desc = "A colorable pullover hoodie."
+ icon_state = "hood_pullover"
+ worn_icon_state = "hood_pullover"
+ hair_mask = /datum/hair_mask/hoodie
+ greyscale_config = /datum/greyscale_config/hoodie_pullover_hood
+ greyscale_config_worn = /datum/greyscale_config/hoodie_pullover_hood/worn
+ greyscale_colors = "#5f5f5f"
+ flags_1 = NO_NEW_GAGS_PREVIEW_1
+
+/obj/item/clothing/suit/hooded/wintercoat/pullover/set_greyscale(list/colors, new_config, new_worn_config, new_inhand_left, new_inhand_right)
+ . = ..()
+ if(!hood)
+ return
+ var/list/coat_colors = SSgreyscale.ParseColorString(greyscale_colors)
+ hood.set_greyscale(coat_colors)
+ hood.update_slot_icon()
+
+/obj/item/clothing/suit/hooded/wintercoat/pullover/on_hood_created(obj/item/clothing/head/hooded/hood)
+ . = ..()
+ var/list/coat_colors = SSgreyscale.ParseColorString(greyscale_colors)
+ hood.set_greyscale(coat_colors)
+
+/obj/item/clothing/suit/hooded/wintercoat/zipup
+ name = "zipup"
+ desc = "A colorable zipup hoodie."
+ icon = 'icons/map_icons/clothing/suit/_suit.dmi'
+ icon_state = "/obj/item/clothing/suit/hooded/wintercoat/zipup"
+ post_init_icon_state = "zipup"
+ greyscale_config = /datum/greyscale_config/hoodie_zipup
+ greyscale_config_worn = /datum/greyscale_config/hoodie_zipup/worn
+ greyscale_colors = "#5f5f5f"
+ hoodtype = /obj/item/clothing/head/hooded/winterhood/zipup
+ flags_1 = IS_PLAYER_COLORABLE_1
+ hood_down_overlay_suffix = ""
+ hood_up_affix = "_t"
+
+/obj/item/clothing/suit/hooded/wintercoat/zipup/worn_overlays(mutable_appearance/standing, isinhands, icon_file)
+ . = ..()
+ if(isinhands || (hood && hood.loc != src))
+ return
+
+ var/suffix = (zipped ? "hood_t" : "hood")
+ var/state = "[initial(post_init_icon_state) || initial(icon_state)]_[suffix]"
+ . += mutable_appearance(icon_file, state, -SUIT_LAYER)
+
+/obj/item/clothing/head/hooded/winterhood/zipup
+ name = "zipup hood"
+ desc = "A colorable zipup hoodie."
+ icon_state = "hood_zipup"
+ worn_icon_state = "hood_zipup"
+ hair_mask = /datum/hair_mask/hoodie
+ greyscale_config = /datum/greyscale_config/hoodie_zipup_hood
+ greyscale_config_worn = /datum/greyscale_config/hoodie_zipup_hood/worn
+ greyscale_colors = "#5f5f5f"
+ flags_1 = NO_NEW_GAGS_PREVIEW_1
+
+/obj/item/clothing/suit/hooded/wintercoat/zipup/set_greyscale(list/colors, new_config, new_worn_config, new_inhand_left, new_inhand_right)
+ . = ..()
+ if(!hood)
+ return
+ var/list/coat_colors = SSgreyscale.ParseColorString(greyscale_colors)
+ hood.set_greyscale(coat_colors)
+ hood.update_slot_icon()
+
+/obj/item/clothing/suit/hooded/wintercoat/zipup/on_hood_created(obj/item/clothing/head/hooded/hood)
+ . = ..()
+ var/list/coat_colors = SSgreyscale.ParseColorString(greyscale_colors)
+ hood.set_greyscale(coat_colors)
diff --git a/code/modules/clothing/under/_under.dm b/code/modules/clothing/under/_under.dm
index a6853d231c54..fbc1ce1f7158 100644
--- a/code/modules/clothing/under/_under.dm
+++ b/code/modules/clothing/under/_under.dm
@@ -455,7 +455,6 @@
/obj/item/clothing/under/verb/toggle()
set name = "Adjust Suit Sensors"
- set category = "Object"
set src in usr
var/mob/user_mob = usr
if(!can_toggle_sensors(user_mob))
diff --git a/code/modules/economy/account.dm b/code/modules/economy/account.dm
index 5c0f8b3fe9d1..17f9439e4d99 100644
--- a/code/modules/economy/account.dm
+++ b/code/modules/economy/account.dm
@@ -135,7 +135,7 @@
if((amount < 0 && has_money(-amount)) || amount > 0)
var/debt_collected = 0
if(account_debt > 0 && amount > 0)
- debt_collected = min(CEILING(amount*DEBT_COLLECTION_COEFF, 1), account_debt)
+ debt_collected = min(ceil(amount*DEBT_COLLECTION_COEFF), account_debt)
account_balance += amount - debt_collected
if(reason)
add_log_to_history(amount, reason)
diff --git a/code/modules/events/portal_storm.dm b/code/modules/events/portal_storm.dm
index f96b73e66a29..924b02b6e585 100644
--- a/code/modules/events/portal_storm.dm
+++ b/code/modules/events/portal_storm.dm
@@ -68,7 +68,7 @@
while(number_of_hostiles > hostiles_spawn.len)
hostiles_spawn += get_random_station_turf()
- next_boss_spawn = start_when + CEILING(2 * number_of_hostiles / number_of_bosses, 1)
+ next_boss_spawn = start_when + ceil(2 * number_of_hostiles / number_of_bosses)
/datum/round_event/portal_storm/announce(fake)
set waitfor = 0
@@ -124,7 +124,7 @@
return FALSE
if(activeFor == next_boss_spawn)
- next_boss_spawn += CEILING(number_of_hostiles / number_of_bosses, 1)
+ next_boss_spawn += ceil(number_of_hostiles / number_of_bosses)
return TRUE
return FALSE
diff --git a/code/modules/events/shuttle_loan/shuttle_loan_event.dm b/code/modules/events/shuttle_loan/shuttle_loan_event.dm
index c3fa0770c1da..623b117b7f97 100644
--- a/code/modules/events/shuttle_loan/shuttle_loan_event.dm
+++ b/code/modules/events/shuttle_loan/shuttle_loan_event.dm
@@ -1,4 +1,4 @@
-
+/// Gives the choice to "loan" the shuttle to central command, giving a big delay on its return to the station in exchange for money and loot/threats in the cargo hold. Only one can be available at a time.
/datum/round_event_control/shuttle_loan
name = "Shuttle Loan"
typepath = /datum/round_event/shuttle_loan
@@ -24,6 +24,8 @@
var/datum/shuttle_loan_situation/situation
/// Whether the station has let Centcom commandeer the shuttle yet.
var/dispatched = FALSE
+ /// How long for the shuttle to come back to the station?
+ var/delay_time = 5 MINUTES
/datum/round_event/shuttle_loan/setup()
var/datum/round_event_control/shuttle_loan/loan_control = control
@@ -49,7 +51,7 @@
if(fake)
qdel(situation)
-
+///Triggered when accepting the shuttle loan. Gives payment and delays shuttle. Ensures the event won't be deleted from event controller until after the cargo arrives at the station.
/datum/round_event/shuttle_loan/proc/loan_shuttle()
priority_announce(situation.thanks_msg, "Cargo shuttle commandeered by [command_name()].")
@@ -60,7 +62,7 @@
SSshuttle.supply.mode = SHUTTLE_CALL
SSshuttle.supply.destination = SSshuttle.getDock("cargo_home")
- SSshuttle.supply.setTimer(3000)
+ SSshuttle.supply.setTimer(delay_time)
SSshuttle.centcom_message += situation.shuttle_transit_text
log_game("Shuttle loan event firing with type '[situation.logging_desc]'.")
@@ -73,9 +75,12 @@
end_when = activeFor + 1
/datum/round_event/shuttle_loan/end()
- if(!SSshuttle.shuttle_loan || !SSshuttle.shuttle_loan.dispatched)
+ if(!SSshuttle.shuttle_loan)
return
- //make sure the shuttle was dispatched in time
+ if(!SSshuttle.shuttle_loan.dispatched) //Haven't dispatched in time? Too bad. Clean it up and move on without spawning anything.
+ SSshuttle.shuttle_loan = null
+ return
+
SSshuttle.shuttle_loan = null
//get empty turfs
diff --git a/code/modules/fishing/fish/fish_traits.dm b/code/modules/fishing/fish/fish_traits.dm
index b70d6af00720..342bfc981fd4 100644
--- a/code/modules/fishing/fish/fish_traits.dm
+++ b/code/modules/fishing/fish/fish_traits.dm
@@ -259,7 +259,7 @@ GLOBAL_LIST_INIT(spontaneous_fish_traits, populate_spontaneous_fish_traits())
/datum/fish_trait/carnivore/catch_weight_mod(obj/item/fishing_rod/rod, mob/fisherman, atom/location, obj/item/fish/fish_type)
. = ..()
- if(istype(rod.get_master_material(), /datum/material/meat)) //who cares about the bait, that fishing rod is yummy!
+ if(IS_EDIBLE(rod)) //who cares about the bait, that fishing rod is yummy!
return
if(!rod.bait)
.[MULTIPLICATIVE_FISHING_MOD] = 0
diff --git a/code/modules/fishing/fishing_equipment.dm b/code/modules/fishing/fishing_equipment.dm
index ca39a09ca3b4..d41ffa69c588 100644
--- a/code/modules/fishing/fishing_equipment.dm
+++ b/code/modules/fishing/fishing_equipment.dm
@@ -383,9 +383,7 @@
. = ..()
ADD_TRAIT(src, TRAIT_CONTRABAND, INNATE_TRAIT)
register_context()
-
- if(SStts.tts_enabled) //This capsule informs you on why it cannot be deployed in a sliiiiightly different way.
- voice = pick(SStts.available_speakers)
+ voice = SStts.random_tts_voice()
/obj/item/survivalcapsule/fishing/add_context(atom/source, list/context, obj/item/held_item, mob/user)
if(!held_item || held_item == src)
diff --git a/code/modules/fishing/sources/subtypes/fishing_portal_machine.dm b/code/modules/fishing/sources/subtypes/fishing_portal_machine.dm
index df8ce18990a9..b79eae415ee9 100644
--- a/code/modules/fishing/sources/subtypes/fishing_portal_machine.dm
+++ b/code/modules/fishing/sources/subtypes/fishing_portal_machine.dm
@@ -154,7 +154,8 @@
///Generate the fish table if we don't have one already.
/datum/fish_source/portal/random/on_fishing_spot_init(datum/component/fishing_spot/spot)
- if(fish_table)
+ if(all_portal_fish_sources_at_once)
+ fish_table = all_portal_fish_sources_at_once
return
///rewards not found in other fishing portals
@@ -166,13 +167,12 @@
if(portal_type == type || !ispath(portal_type, /datum/fish_source/portal))
continue
var/datum/fish_source/portal/preset_portal = GLOB.preset_fish_sources[portal_type]
- fish_table |= preset_portal.fish_table
+ for(var/reward_path in preset_portal.fish_table)
+ if(reward_path == FISHING_DUD) // We don't serve duds.
+ continue
+ fish_table[reward_path] = rand(1, 4)
- ///We don't serve duds.
- fish_table -= FISHING_DUD
-
- for(var/reward_path in fish_table)
- fish_table[reward_path] = rand(1, 4)
+ all_portal_fish_sources_at_once = fish_table
///Difficulty has to be calculated before the rest, because of how it influences jump chances
/datum/fish_source/portal/random/calculate_difficulty(datum/fishing_challenge/challenge, result, obj/item/fishing_rod/rod, mob/fisherman)
diff --git a/code/modules/food_and_drinks/machinery/gibber.dm b/code/modules/food_and_drinks/machinery/gibber.dm
index 0c661867370c..bbb418892a0e 100644
--- a/code/modules/food_and_drinks/machinery/gibber.dm
+++ b/code/modules/food_and_drinks/machinery/gibber.dm
@@ -147,7 +147,6 @@
return ..()
/obj/machinery/gibber/verb/eject()
- set category = "Object"
set name = "Empty gibber"
set src in oview(1)
if (usr.stat != CONSCIOUS || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED))
diff --git a/code/modules/food_and_drinks/machinery/grill.dm b/code/modules/food_and_drinks/machinery/grill.dm
index e6e664eb3461..72699cbfc1a4 100644
--- a/code/modules/food_and_drinks/machinery/grill.dm
+++ b/code/modules/food_and_drinks/machinery/grill.dm
@@ -60,7 +60,7 @@
context[SCREENTIP_CONTEXT_LMB] = "Add item"
return CONTEXTUAL_SCREENTIP_SET
else if(held_item.tool_behaviour == TOOL_WRENCH)
- context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Un" : ""]anchor"
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Unan" : "An"]chor"
return CONTEXTUAL_SCREENTIP_SET
else if(!anchored && held_item.tool_behaviour == TOOL_CROWBAR)
context[SCREENTIP_CONTEXT_LMB] = "Deconstruct"
diff --git a/code/modules/food_and_drinks/machinery/monkeyrecycler.dm b/code/modules/food_and_drinks/machinery/monkeyrecycler.dm
index 2261b7db0ed7..5c08f946958b 100644
--- a/code/modules/food_and_drinks/machinery/monkeyrecycler.dm
+++ b/code/modules/food_and_drinks/machinery/monkeyrecycler.dm
@@ -86,7 +86,7 @@ GLOBAL_LIST_EMPTY(monkey_recyclers)
if(stored_matter >= 1)
to_chat(user, span_notice("The machine hisses loudly as it condenses the ground monkey meat. After a moment, it dispenses a brand new monkey cube."))
playsound(src.loc, 'sound/machines/hiss.ogg', 50, TRUE)
- for(var/i in 1 to FLOOR(stored_matter, 1))
+ for(var/i in 1 to floor(stored_matter))
new /obj/item/food/monkeycube(src.loc)
stored_matter--
to_chat(user, span_notice("The machine's display flashes that it has [stored_matter] monkeys worth of material left."))
diff --git a/code/modules/food_and_drinks/machinery/processor.dm b/code/modules/food_and_drinks/machinery/processor.dm
index f0940348bee6..a0b5b6c743e8 100644
--- a/code/modules/food_and_drinks/machinery/processor.dm
+++ b/code/modules/food_and_drinks/machinery/processor.dm
@@ -176,7 +176,6 @@
visible_message(span_notice("\The [src] finishes processing."))
/obj/machinery/processor/verb/eject()
- set category = "Object"
set name = "Eject Contents"
set src in oview(1)
if(usr.stat != CONSCIOUS || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED))
diff --git a/code/modules/food_and_drinks/machinery/smartfridge.dm b/code/modules/food_and_drinks/machinery/smartfridge.dm
index d8ed0be1a62b..8fca291bc1bc 100644
--- a/code/modules/food_and_drinks/machinery/smartfridge.dm
+++ b/code/modules/food_and_drinks/machinery/smartfridge.dm
@@ -569,7 +569,7 @@
context[SCREENTIP_CONTEXT_LMB] = "Deconstruct"
tool_tip_set = TRUE
else if(held_item.tool_behaviour == TOOL_WRENCH)
- context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Un" : ""]anchore"
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Unan" : "An"]chor"
tool_tip_set = TRUE
return tool_tip_set ? CONTEXTUAL_SCREENTIP_SET : NONE
diff --git a/code/modules/food_and_drinks/recipes/food_mixtures.dm b/code/modules/food_and_drinks/recipes/food_mixtures.dm
index 362a31f809e7..15368d2c5431 100644
--- a/code/modules/food_and_drinks/recipes/food_mixtures.dm
+++ b/code/modules/food_and_drinks/recipes/food_mixtures.dm
@@ -83,7 +83,6 @@
thermic_constant = 0
H_ion_release = 0
reaction_tags = REACTION_TAG_FOOD | REACTION_TAG_EASY
- required_other = TRUE
/// Typepath of food that is created on reaction
var/atom/resulting_food_path
diff --git a/code/modules/food_and_drinks/recipes/soup_mixtures.dm b/code/modules/food_and_drinks/recipes/soup_mixtures.dm
index f046278142d1..eef072eb1929 100644
--- a/code/modules/food_and_drinks/recipes/soup_mixtures.dm
+++ b/code/modules/food_and_drinks/recipes/soup_mixtures.dm
@@ -31,7 +31,6 @@
thermic_constant = 10
required_reagents = null
mob_react = FALSE
- required_other = TRUE
required_container_accepts_subtypes = TRUE
required_container = /obj/item/reagent_containers/cup/soup_pot
mix_message = "You smell something good coming from the steaming pot of soup."
@@ -757,13 +756,15 @@
// Pretty much just a normal chemical reaction.
// This also creates nutrients out of thin air.
- required_other = FALSE
required_reagents = list(
/datum/reagent/water = 40,
/datum/reagent/toxin/slimejelly = 20,
)
required_ingredients = null
+/datum/chemical_reaction/food/soup/slimesoup/alt/pre_reaction_other_checks(datum/reagents/holder)
+ return TRUE
+
// Clown Tear soup
/datum/reagent/consumable/nutriment/soup/clown_tears
name = "Clown's Tears"
@@ -1743,7 +1744,6 @@
drink_type = GRAIN
/datum/chemical_reaction/food/soup/cornmeal_porridge
- required_other = FALSE
required_reagents = list(
/datum/reagent/consumable/cornmeal = 20,
/datum/reagent/water = 20,
@@ -1752,6 +1752,9 @@
/datum/reagent/consumable/nutriment/soup/cornmeal_porridge = 20,
)
+/datum/chemical_reaction/food/soup/cornmeal_porridge/pre_reaction_other_checks(datum/reagents/holder)
+ return TRUE
+
// Cheese Porridge (Soup-ish)
/datum/reagent/consumable/nutriment/soup/cheese_porridge
name = "Cheesy Porridge" //milk, polenta, firm cheese, curd cheese, butter
diff --git a/code/modules/language/_language_holder.dm b/code/modules/language/_language_holder.dm
index c4207f40f070..9eebdca50e08 100644
--- a/code/modules/language/_language_holder.dm
+++ b/code/modules/language/_language_holder.dm
@@ -225,9 +225,9 @@ Key procs
/// Checks if you have the language passed.
/datum/language_holder/proc/has_language(language, flag_to_check = UNDERSTOOD_LANGUAGE)
var/list/langs_to_check = list()
- if(flag_to_check & SPOKEN_LANGUAGE && !LAZYACCESS(blocked_speaking, language))
+ if(flag_to_check & SPOKEN_LANGUAGE && LAZYACCESS(spoken_languages, language))
langs_to_check |= spoken_languages
- if(flag_to_check & UNDERSTOOD_LANGUAGE && !LAZYACCESS(blocked_understanding, language))
+ if(flag_to_check & UNDERSTOOD_LANGUAGE && LAZYACCESS(understood_languages, language))
langs_to_check |= understood_languages
return language in langs_to_check
diff --git a/code/modules/library/book.dm b/code/modules/library/book.dm
index 7c5ed5881be2..97bd3d99d4fa 100644
--- a/code/modules/library/book.dm
+++ b/code/modules/library/book.dm
@@ -297,8 +297,12 @@
/obj/item/tgui_book/manual
abstract_type = /obj/item/tgui_book/manual
+/// Asserts a field is present in a list, stack traces if not
+#define ASSERT_DATA(data_list, data_field) \
+ if(!data_list[data_field]) { stack_trace("[type] - [data_list["id"]] lacks [data_field] information!"); }
+
/obj/item/tgui_book/manual/dsm
- name = "\improper SDSM-35"
+ name = "\improper SDSM-35" // 35 was picked based on the current edition plus the average years between edition divided by 500
desc = "The Space Diagnostic and Statistical Manual of Mental Disorders, \
a comprehensive book on all known mental disorders. \
On its 35th edition - though it's due for an update..."
@@ -360,12 +364,95 @@
// validate
for(var/list/trauma_data as anything in trauma_info)
- if(isnull(trauma_data["desc"]))
+ // these are asserted to be present, no matter what
+ ASSERT_DATA(trauma_data, "full_name")
+ ASSERT_DATA(trauma_data, "scan_name")
+ ASSERT_DATA(trauma_data, "id")
+ // these start as null, so there is a chance they are missing
+ if(!trauma_data["desc"])
trauma_data["desc"] = "No description recorded - this is an error. Report this lack of research."
stack_trace("[type] - [trauma_data["id"]] lacks a description!")
- if(isnull(trauma_data["symptoms"]))
+ if(!trauma_data["symptoms"])
trauma_data["symptoms"] = "No symptoms recorded - this is an error. Report this lack of research."
stack_trace("[type] - [trauma_data["id"]] lacks symptom information!")
data["traumas"] = trauma_info
return data
+
+/obj/item/tgui_book/manual/idc
+ name = "\improper IDC-27" // 27 was picked based on the current edition plus the average years between edition divided by 500
+ desc = "The Interplanetary Classification of Diseases, \
+ a comprehensive book on all known diseases and ailments. \
+ On its 27th edition - though it's due for an update..."
+ icon_state = "book7"
+ ui_name = "IDCBook"
+
+/obj/item/tgui_book/manual/idc/ui_static_data(mob/user)
+ var/list/data = list()
+
+ var/static/list/disease_info
+ if(!disease_info)
+ disease_info = list()
+
+ for(var/datum/disease/disease_type as anything in valid_subtypesof(/datum/disease))
+ if(disease_type::visibility_flags & HIDDEN_BOOK)
+ continue
+
+ var/list/disease_data = list()
+ disease_data["form"] = disease_type::form
+ disease_data["agent"] = disease_type::agent
+ disease_data["name"] = disease_type::name
+ disease_data["desc"] = disease_type::desc
+ disease_data["illness"] = "N/A"
+ disease_data["spread_by"] = disease_type::spread_text || get_disease_spread_text(disease_type::spread_flags)
+ disease_data["cured_by"] = disease_type::cure_text
+ disease_data["id"] = disease_type
+ disease_info += list(disease_data)
+
+ for(var/datum/symptom/symptom_type as anything in valid_subtypesof(/datum/symptom))
+ var/list/symptom_data = list()
+ symptom_data["form"] = "Symptom"
+ symptom_data["agent"] = "N/A"
+ symptom_data["name"] = symptom_type::name
+ symptom_data["desc"] = symptom_type::desc
+ symptom_data["illness"] = symptom_type::illness
+ symptom_data["spread_by"] = "N/A"
+ symptom_data["cured_by"] = symptom_type::symptom_cure::name
+ symptom_data["id"] = symptom_type
+ disease_info += list(symptom_data)
+
+ var/list/advanced_virus_info = list(
+ "form" = "Virus",
+ "agent" = "Microbes",
+ "name" = "Advanced Disease",
+ "desc" = "A catch-all category for diseases that are too complex to document in their entirety. \
+ The Virology department is known for bio-engineering these diseases, as such their danger can vary wildly. \
+ These diseases are often a combination of various symptoms, each cataloged in this book individually.",
+ "illness" = "N/A",
+ "spread_by" = "Varies by symptom combination",
+ "cured_by" = "Varies by symptom combination",
+ "id" = /datum/disease/advance,
+ )
+ disease_info += list(advanced_virus_info)
+
+ // validate
+ for(var/list/disease_data as anything in disease_info)
+ // these are asserted to be present, no matter what
+ ASSERT_DATA(disease_data, "form")
+ ASSERT_DATA(disease_data, "agent")
+ ASSERT_DATA(disease_data, "name")
+ ASSERT_DATA(disease_data, "spread_by")
+ ASSERT_DATA(disease_data, "illness")
+ ASSERT_DATA(disease_data, "id")
+ // these start as null, so there is a chance they are missing
+ if(!disease_data["desc"])
+ disease_data["desc"] = "No description recorded - this is an error. Report this lack of research."
+ stack_trace("[type] - [disease_data["id"]] lacks a description!")
+ if(!disease_data["cured_by"] && !ispath(disease_data["id"], /datum/symptom))
+ disease_data["cured_by"] = "Unknown"
+ stack_trace("[type] - [disease_data["id"]] lacks cure information!")
+
+ data["diseases"] = disease_info
+ return data
+
+#undef ASSERT_DATA
diff --git a/code/modules/lighting/lighting_source.dm b/code/modules/lighting/lighting_source.dm
index c3537d5dcd0e..36db89d2e280 100644
--- a/code/modules/lighting/lighting_source.dm
+++ b/code/modules/lighting/lighting_source.dm
@@ -244,7 +244,7 @@ GLOBAL_LIST_EMPTY(lighting_sheets)
var/list/encode = list()
// How far away the turfs we get are, and how many there are are often not the same calculation
// So we need to include the visual offset, so we can ensure our sheet is large enough to accept all the distance differences
- var/bound_range = CEILING(range, 1) + visual_offset
+ var/bound_range = ceil(range) + visual_offset
// Corners are placed at 0.5 offsets
// We need our coords to reflect that (though x_offsets that change the basis for how things are calculated are fine too)
@@ -447,7 +447,7 @@ GLOBAL_LIST_EMPTY(lighting_sheets)
if(visual_offsets[1] != offset_x || visual_offsets[2] != offset_y || source_turf != old_source_turf)
offset_x = visual_offsets[1]
offset_y = visual_offsets[2]
- visual_offset = max(CEILING(abs(offset_x), 1), CEILING(abs(offset_y), 1))
+ visual_offset = max(ceil(abs(offset_x)), ceil(abs(offset_y)))
update = TRUE
// If we need to update, well, update
@@ -468,7 +468,7 @@ GLOBAL_LIST_EMPTY(lighting_sheets)
return list()
var/oldlum = source_turf.luminosity
- var/working_range = CEILING(light_range + visual_offset, 1)
+ var/working_range = ceil(light_range + visual_offset)
source_turf.luminosity = working_range
var/uses_multiz = !!GET_LOWEST_STACK_OFFSET(source_turf.z)
diff --git a/code/modules/loadout/categories/suits.dm b/code/modules/loadout/categories/suits.dm
index 0637671de377..c830aff2a12d 100644
--- a/code/modules/loadout/categories/suits.dm
+++ b/code/modules/loadout/categories/suits.dm
@@ -40,3 +40,11 @@
/datum/job/paramedic = "#28324b",
/datum/job/prisoner = "#ff8b00",
)
+
+/datum/loadout_item/suit/hoodie_pullover
+ name = "pullover"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/pullover
+
+/datum/loadout_item/suit/hoodie_zipup
+ name = "zipup"
+ item_path = /obj/item/clothing/suit/hooded/wintercoat/zipup
diff --git a/code/modules/manufactorio/machines/lathe.dm b/code/modules/manufactorio/machines/lathe.dm
index 559b92b7c109..c248bc6e47c6 100644
--- a/code/modules/manufactorio/machines/lathe.dm
+++ b/code/modules/manufactorio/machines/lathe.dm
@@ -112,8 +112,17 @@
return
//check for materials required. For custom material items decode their required materials
var/list/materials_needed = list()
+ var/list/slots_chosen = null
for(var/material, amount_needed in design.materials)
- if(ispath(material, /datum/material_requirement)) // Material requirement
+ var/datum/material_requirement/requirement = null
+ var/datum/material_slot/slot = null
+ if(ispath(material, /datum/material_requirement))
+ requirement = material
+ else if (ispath(material, /datum/material_slot))
+ slot = SSmaterials.material_slots[material]
+ requirement = slot.requirement_type
+
+ if(requirement) // Material requirement
for(var/datum/material/valid_candidate as anything in SSmaterials.get_materials_by_req(material))
if(materials.get_material_amount(valid_candidate) >= amount_needed)
material = valid_candidate
@@ -121,6 +130,9 @@
if(isnull(material))
return
materials_needed[material] = amount_needed
+ if (slot)
+ var/datum/material/proper_mat = material
+ LAZYSET(slots_chosen, slot.type, proper_mat.id)
if(!materials.has_materials(materials_needed))
return
@@ -129,9 +141,9 @@
flick_overlay_view(mutable_appearance(icon, "lathe_printing"), craft_time)
print_sound.start()
add_load(power_cost)
- busy = addtimer(CALLBACK(src, PROC_REF(do_make_item), design, materials_needed), craft_time, TIMER_UNIQUE | TIMER_STOPPABLE | TIMER_DELETE_ME)
+ busy = addtimer(CALLBACK(src, PROC_REF(do_make_item), design, materials_needed, slots_chosen), craft_time, TIMER_UNIQUE | TIMER_STOPPABLE | TIMER_DELETE_ME)
-/obj/machinery/power/manufacturing/lathe/proc/do_make_item(datum/design/design, list/materials_needed)
+/obj/machinery/power/manufacturing/lathe/proc/do_make_item(datum/design/design, list/materials_needed, list/slots_chosen)
finalize_build()
if(surplus() < power_cost)
return
@@ -154,7 +166,10 @@
created = new stack_item(drop_location(), amount)
else
created = design.create_result(drop_location(), materials_needed)
+ if (length(slots_chosen))
+ created.set_material_slots(slots_chosen)
split_materials_uniformly(materials_needed, target_object = created)
+
if(isitem(created))
created.pixel_x = created.base_pixel_x + rand(-6, 6)
created.pixel_y = created.base_pixel_y + rand(-6, 6)
diff --git a/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm b/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm
index a706dcf9ca64..995b789456e7 100644
--- a/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm
+++ b/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm
@@ -380,6 +380,8 @@ GLOBAL_VAR_INIT(hhMysteryRoomNumber, rand(1, 999999))
/datum/action/peephole_cancel/Trigger(mob/clicker, trigger_flags)
. = ..()
+ if(!.)
+ return
to_chat(owner, span_warning("You move away from the peephole."))
owner.reset_perspective()
owner.clear_fullscreen("remote_view", 0)
diff --git a/code/modules/mining/boulder_processing/_boulder_processing.dm b/code/modules/mining/boulder_processing/_boulder_processing.dm
index 39cc7266b65c..5f69edfaf570 100644
--- a/code/modules/mining/boulder_processing/_boulder_processing.dm
+++ b/code/modules/mining/boulder_processing/_boulder_processing.dm
@@ -68,7 +68,7 @@
else if(held_item.tool_behaviour == TOOL_SCREWDRIVER)
context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel"
else if(held_item.tool_behaviour == TOOL_WRENCH)
- context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Un" : ""]Anchor"
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Unan" : "An"]chor"
else if(panel_open && held_item.tool_behaviour == TOOL_CROWBAR)
context[SCREENTIP_CONTEXT_LMB] = "Deconstruct"
diff --git a/code/modules/mining/boulder_processing/brm.dm b/code/modules/mining/boulder_processing/brm.dm
index 72f7f0942acf..d41026d771e1 100644
--- a/code/modules/mining/boulder_processing/brm.dm
+++ b/code/modules/mining/boulder_processing/brm.dm
@@ -46,7 +46,7 @@
if(!isnull(held_item))
if(held_item.tool_behaviour == TOOL_WRENCH)
- context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Un" : ""]Anchor"
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Unan" : "An"]chor"
return CONTEXTUAL_SCREENTIP_SET
else if(held_item.tool_behaviour == TOOL_SCREWDRIVER)
context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel"
diff --git a/code/modules/mining/equipment/mineral_scanner.dm b/code/modules/mining/equipment/mineral_scanner.dm
index 420aad1ceaa0..8d763fd2f676 100644
--- a/code/modules/mining/equipment/mineral_scanner.dm
+++ b/code/modules/mining/equipment/mineral_scanner.dm
@@ -113,7 +113,7 @@
layer = FLASH_LAYER
icon = 'icons/effects/ore_visuals.dmi'
appearance_flags = NONE // to avoid having TILE_BOUND in the flags, so that the 480x480 icon states let you see it no matter where you are
- duration = 35
+ duration = 3.5 SECONDS
pixel_x = -224
pixel_y = -224
/// What animation easing to use when we create the ore overlay on rock walls/ore vents.
@@ -122,11 +122,3 @@
/obj/effect/temp_visual/mining_overlay/Initialize(mapload)
. = ..()
animate(src, alpha = 0, time = duration, easing = easing_style)
-
-/obj/effect/temp_visual/mining_overlay/vent
- icon = 'icons/effects/vent_overlays.dmi'
- icon_state = "unknown"
- duration = 45
- pixel_x = 0
- pixel_y = 0
- easing_style = CIRCULAR_EASING|EASE_IN
diff --git a/code/modules/mining/equipment/monster_organs/brimdust_sac.dm b/code/modules/mining/equipment/monster_organs/brimdust_sac.dm
index 80284471da3b..5ea9730daca4 100644
--- a/code/modules/mining/equipment/monster_organs/brimdust_sac.dm
+++ b/code/modules/mining/equipment/monster_organs/brimdust_sac.dm
@@ -147,19 +147,38 @@
/datum/status_effect/stacking/brimdust_coating/on_apply()
. = ..()
- dust_overlay = mutable_appearance('icons/effects/weather_effects.dmi', "ash_storm")
+ var/target_width = max(owner.get_cached_width(), ICON_SIZE_X)
+ var/target_height = max(owner.get_cached_height(), ICON_SIZE_Y)
+ if (target_width == ICON_SIZE_X && target_height == ICON_SIZE_Y)
+ dust_overlay = mutable_appearance('icons/effects/weather_effects.dmi', "ash_storm")
+ else
+ var/static/list/dust_icons = list()
+ var/icon/dust_icon = dust_icons["[target_width]-[target_height]"]
+ if (!dust_icon)
+ dust_icon = icon('icons/effects/weather_effects.dmi', "ash_storm")
+ var/icon/base_icon = icon('icons/effects/weather_effects.dmi', "ash_storm")
+ dust_icon.Crop(1, 1, target_width, target_height)
+ var/icon/column = icon('icons/effects/weather_effects.dmi', "ash_storm")
+ column.Crop(1, 1, ICON_SIZE_X, target_height)
+ for (var/i in 1 to ceil(target_height / ICON_SIZE_Y))
+ column.Blend(base_icon, ICON_OVERLAY, 1, 1 + (i - 1) * ICON_SIZE_Y)
+ for (var/i in 1 to ceil(target_width / ICON_SIZE_X))
+ dust_icon.Blend(column, ICON_OVERLAY, 1 + (i - 1) * ICON_SIZE_X, 1)
+ dust_icons["[target_width]-[target_height]"] = dust_icon
+ dust_overlay = mutable_appearance(dust_icon)
dust_overlay.alpha = stacks * BRIMDUST_ALPHA_PER_STACK
dust_overlay.color = COLOR_RED_LIGHT
dust_overlay.blend_mode = BLEND_INSET_OVERLAY
owner.add_overlay(dust_overlay)
- owner.add_shared_particles(/particles/brimdust)
+ var/obj/effect/holder = owner.add_shared_particles(/particles/brimdust, "brimdust_coating-[owner.base_pixel_w]")
+ holder.pixel_w = -owner.base_pixel_w
RegisterSignal(owner, COMSIG_COMPONENT_CLEAN_ACT, PROC_REF(on_cleaned))
RegisterSignal(owner, COMSIG_MOB_APPLY_DAMAGE, PROC_REF(on_take_damage))
/datum/status_effect/stacking/brimdust_coating/on_remove()
. = ..()
owner.cut_overlay(dust_overlay)
- owner.remove_shared_particles(/particles/brimdust)
+ owner.remove_shared_particles("brimdust_coating-[owner.base_pixel_w]")
UnregisterSignal(owner, list(COMSIG_MOB_APPLY_DAMAGE, COMSIG_COMPONENT_CLEAN_ACT))
/// When you are cleaned, wash off the buff
diff --git a/code/modules/mining/lavaland/cain_and_abel/_cain_and_abel.dm b/code/modules/mining/lavaland/cain_and_abel/_cain_and_abel.dm
index e8c60b310a0e..7345e0959b62 100644
--- a/code/modules/mining/lavaland/cain_and_abel/_cain_and_abel.dm
+++ b/code/modules/mining/lavaland/cain_and_abel/_cain_and_abel.dm
@@ -115,7 +115,7 @@
attack_speed = initial(attack_speed)
var/old_force = force
var/bonus_value = combo_count || 1
- force = CEILING((bonus_value * damage_boost) * force, 1)
+ force = ceil((bonus_value * damage_boost) * force)
. = ..()
force = old_force
set_combo(new_value = combo_count + 1, user = user)
diff --git a/code/modules/mob/living/basic/basic.dm b/code/modules/mob/living/basic/basic.dm
index bb89e69b42cb..0392cfcc6bc0 100644
--- a/code/modules/mob/living/basic/basic.dm
+++ b/code/modules/mob/living/basic/basic.dm
@@ -293,11 +293,6 @@
return
return relaydrive(user, direction)
-/mob/living/basic/get_status_tab_items()
- . = ..()
- . += "Health: [round((health / maxHealth) * 100)]%"
- . += "Combat Mode: [combat_mode ? "On" : "Off"]"
-
/mob/living/basic/compare_sentience_type(compare_type)
return sentience_type == compare_type
diff --git a/code/modules/mob/living/basic/boss/boss.dm b/code/modules/mob/living/basic/boss/boss.dm
index 31a7d2379a99..4ffd7961de3f 100644
--- a/code/modules/mob/living/basic/boss/boss.dm
+++ b/code/modules/mob/living/basic/boss/boss.dm
@@ -4,6 +4,7 @@
/mob/living/basic/boss
combat_mode = TRUE
status_flags = NONE
+ abstract_type = /mob/living/basic/boss
sentience_type = SENTIENCE_BOSS
mob_biotypes = MOB_ORGANIC|MOB_SPECIAL
faction = list(FACTION_MINING, FACTION_BOSS)
diff --git a/code/modules/mob/living/basic/bots/bot_ai.dm b/code/modules/mob/living/basic/bots/bot_ai.dm
index 4fde29dcee69..6f726c9586c9 100644
--- a/code/modules/mob/living/basic/bots/bot_ai.dm
+++ b/code/modules/mob/living/basic/bots/bot_ai.dm
@@ -12,7 +12,7 @@
ai_movement = /datum/ai_movement/jps/bot
planning_subtrees = list(
-/datum/ai_planning_subtree/escape_captivity/pacifist,
+ /datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/salute_authority,
/datum/ai_planning_subtree/find_patrol_beacon,
@@ -55,8 +55,12 @@
/datum/ai_controller/basic_controller/bot/proc/on_movement_start(mob/living/basic/bot/source, atom/target)
SIGNAL_HANDLER
+
if(current_movement_target == blackboard[BB_BEACON_TARGET])
source.update_bot_mode(new_mode = BOT_PATROL)
+ return
+
+ source.clear_path_hud(remove_hud = FALSE)
/datum/ai_controller/basic_controller/bot/proc/add_to_blacklist(atom/target, duration)
var/final_duration = duration || blackboard[BB_UNREACHABLE_LIST_COOLDOWN]
diff --git a/code/modules/mob/living/basic/bots/bot_hud.dm b/code/modules/mob/living/basic/bots/bot_hud.dm
index 08a7f9e3fbb7..602b2cc54bdd 100644
--- a/code/modules/mob/living/basic/bots/bot_hud.dm
+++ b/code/modules/mob/living/basic/bots/bot_hud.dm
@@ -40,6 +40,12 @@
if(isnull(ai_controller))
return
+
+ var/atom/move_target = ai_controller.current_movement_target
+ if(move_target != ai_controller.blackboard[BB_BEACON_TARGET])
+ return
+
+
//Removes path images and handles removing hud client images
clear_path_hud()
@@ -48,11 +54,6 @@
var/list/path_images = active_hud_list[DIAG_PATH_HUD]
LAZYCLEARLIST(path_images)
-
- var/atom/move_target = ai_controller.current_movement_target
- if(move_target != ai_controller.blackboard[BB_BEACON_TARGET])
- return
-
var/list/our_path = source.movement_path
if(!length(our_path))
return
@@ -94,11 +95,6 @@
if(client || !length(current_pathed_turfs) || isnull(ai_controller))
return
- var/atom/move_target = ai_controller.current_movement_target
-
- if(move_target != ai_controller.blackboard[BB_BEACON_TARGET])
- clear_path_hud()
-
var/turf/our_turf = get_turf(src)
var/image/target_image = current_pathed_turfs[our_turf]
if(target_image)
@@ -106,12 +102,15 @@
current_pathed_turfs -= our_turf
///proc that handles deleting the bot's drawn path when needed
-/mob/living/basic/bot/proc/clear_path_hud()
+/mob/living/basic/bot/proc/clear_path_hud(remove_hud = TRUE)
for(var/turf/index as anything in current_pathed_turfs)
var/image/our_image = current_pathed_turfs[index]
animate(our_image, alpha = 0, time = 0.3 SECONDS)
current_pathed_turfs -= index
+ if(!remove_hud)
+ return
+
// Call hud remove handlers to ensure viewing user client images are removed
var/list/path_huds_watching_me = list(GLOB.huds[DATA_HUD_DIAGNOSTIC], GLOB.huds[DATA_HUD_BOT_PATH])
for(var/datum/atom_hud/hud as anything in path_huds_watching_me)
diff --git a/code/modules/mob/living/basic/clown/clown.dm b/code/modules/mob/living/basic/clown/clown.dm
index 5b230a6605d4..7808c45f72fc 100644
--- a/code/modules/mob/living/basic/clown/clown.dm
+++ b/code/modules/mob/living/basic/clown/clown.dm
@@ -627,6 +627,8 @@
/datum/action/cooldown/exquisite_bunch/Trigger(mob/clicker, trigger_flags, atom/target)
if(activating)
return
+ if(!IsAvailable(feedback = TRUE))
+ return
var/atom/bunch_turf = get_step(owner.loc, owner.dir)
if(!bunch_turf)
return
diff --git a/code/modules/mob/living/basic/guardian/guardian.dm b/code/modules/mob/living/basic/guardian/guardian.dm
index fd84556c99d7..9c2592f97485 100644
--- a/code/modules/mob/living/basic/guardian/guardian.dm
+++ b/code/modules/mob/living/basic/guardian/guardian.dm
@@ -34,7 +34,6 @@
attack_sound = 'sound/items/weapons/punch1.ogg'
attack_verb_continuous = "punches"
attack_verb_simple = "punch"
- combat_mode = TRUE
obj_damage = 40
melee_damage_lower = 15
melee_damage_upper = 15
@@ -53,8 +52,8 @@
/// Coloured overlay we apply
var/mutable_appearance/overlay
- /// Which toggle button the HUD uses.
- var/toggle_button_type = /atom/movable/screen/guardian/toggle_mode/inactive
+ /// Which toggle button the guardian has. Won't get one if it's null.
+ var/toggle_button_type = null
/// Name used by the guardian creator.
var/creator_name = "Error"
/// Description used by the guardian creator.
@@ -76,12 +75,20 @@
/// Cooldown between the summoner resetting the guardian's client.
COOLDOWN_DECLARE(resetting_cooldown)
- /// List of actions we give to our summoner
+ /// List of actions we give to our summoner.
var/static/list/control_actions = list(
/datum/action/cooldown/mob_cooldown/guardian_comms,
/datum/action/cooldown/mob_cooldown/recall_guardian,
/datum/action/cooldown/mob_cooldown/replace_guardian,
)
+ /// List of actions we give to ourselves.
+ var/static/list/self_actions = list(
+ /datum/action/cooldown/guardian/check_type,
+ /datum/action/cooldown/guardian/toggle_light,
+ /datum/action/cooldown/guardian/communicate,
+ /datum/action/cooldown/guardian/manifest,
+ /datum/action/cooldown/guardian/recall,
+ )
/mob/living/basic/guardian/Initialize(mapload, datum/guardian_fluff/theme)
. = ..()
@@ -90,10 +97,10 @@
theme?.apply(src)
AddElement(/datum/element/death_drops, /obj/effect/temp_visual/guardian/phase/out)
AddElement(/datum/element/simple_flying)
- AddComponent(/datum/component/basic_inhands)
// life link
update_appearance(UPDATE_ICON)
manifest_effects()
+ create_actions()
/mob/living/basic/guardian/Destroy()
GLOB.parasites -= src
@@ -102,6 +109,18 @@
cut_summoner(different_person = TRUE)
return ..()
+///Creates the guardian's default action buttons and sets them to go in their proper location.
+///Subtypes overwrite this for special ability types and whatnot.
+/mob/living/basic/guardian/proc/create_actions()
+ for (var/action_type in self_actions + toggle_button_type)
+ if(isnull(action_type)) //no toggle button type
+ continue
+ if (locate(action_type) in actions)
+ continue
+ var/datum/action/new_action = new action_type(src)
+ new_action.Grant(src)
+ update_action_buttons()
+
/mob/living/basic/guardian/update_overlays()
. = ..()
. += overlay
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/assassin.dm b/code/modules/mob/living/basic/guardian/guardian_types/assassin.dm
index 19f98a70f409..377c3fec0b7a 100644
--- a/code/modules/mob/living/basic/guardian/guardian_types/assassin.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_types/assassin.dm
@@ -1,5 +1,3 @@
-#define CAN_STEALTH_ALERT "can_stealth"
-
/**
* Can enter stealth mode to become invisible and deal bonus damage on their next attack, an ambush predator.
*/
@@ -17,57 +15,44 @@
creator_name = "Assassin"
creator_desc = "Does medium damage and takes full damage, but can enter stealth, causing its next attack to do massive damage and ignore armor. However, it becomes briefly unable to recall after attacking from stealth."
creator_icon = "assassin"
- toggle_button_type = /atom/movable/screen/guardian/toggle_mode/assassin
+ toggle_button_type = /datum/action/cooldown/guardian/toggle_mode/assassin
/// How long to put stealth on cooldown if we are forced out?
var/stealth_cooldown_time = 16 SECONDS
- /// Cooldown for the stealth toggle.
- COOLDOWN_DECLARE(stealth_cooldown)
/mob/living/basic/guardian/assassin/Initialize(mapload, datum/guardian_fluff/theme)
. = ..()
- show_can_stealth()
RegisterSignal(src, COMSIG_GUARDIAN_ASSASSIN_REVEALED, PROC_REF(on_forced_unstealth))
// Toggle stealth
/mob/living/basic/guardian/assassin/toggle_modes()
var/stealthed = has_status_effect(/datum/status_effect/guardian_stealth)
+ var/datum/action/cooldown/guardian/toggle_mode/assassin/stealth_ability = locate() in actions
if (stealthed)
to_chat(src, span_bolddanger("You exit stealth."))
remove_status_effect(/datum/status_effect/guardian_stealth)
- show_can_stealth()
+ if(stealth_ability)
+ stealth_ability.build_all_button_icons()
return
- if (COOLDOWN_FINISHED(src, stealth_cooldown))
- if (!is_deployed())
- to_chat(src, span_bolddanger("You have to be manifested to enter stealth!"))
- return
- apply_status_effect(/datum/status_effect/guardian_stealth)
- clear_alert(CAN_STEALTH_ALERT)
+ if (!is_deployed())
+ to_chat(src, span_bolddanger("You have to be manifested to enter stealth!"))
return
- to_chat(src, span_bolddanger("You cannot yet enter stealth, wait another [DisplayTimeText(COOLDOWN_TIMELEFT(src, stealth_cooldown))]!"))
-
-/mob/living/basic/guardian/assassin/get_status_tab_items()
- . = ..()
- if(!COOLDOWN_FINISHED(src, stealth_cooldown))
- . += "Stealth Cooldown Remaining: [DisplayTimeText(COOLDOWN_TIMELEFT(src, stealth_cooldown))]"
+ apply_status_effect(/datum/status_effect/guardian_stealth)
+ if(stealth_ability)
+ stealth_ability.build_all_button_icons()
/// Called when we are removed from stealth involuntarily
/mob/living/basic/guardian/assassin/proc/on_forced_unstealth(mob/living/source)
SIGNAL_HANDLER
visible_message(span_danger("\The [src] suddenly appears!"))
COOLDOWN_START(src, manifest_cooldown, 4 SECONDS)
- COOLDOWN_START(src, stealth_cooldown, stealth_cooldown_time)
- addtimer(CALLBACK(src, PROC_REF(show_can_stealth)), stealth_cooldown_time)
-
-/// Displays an alert letting us know that we can enter stealth
-/mob/living/basic/guardian/assassin/proc/show_can_stealth()
- if(!COOLDOWN_FINISHED(src, stealth_cooldown))
- return
- throw_alert(CAN_STEALTH_ALERT, /atom/movable/screen/alert/canstealth)
+ var/datum/action/cooldown/guardian/toggle_mode/assassin/stealth_ability = locate() in actions
+ if(stealth_ability)
+ stealth_ability.StartCooldownSelf(stealth_cooldown_time)
/// Status effect which makes us sneakier and do bonus damage
/datum/status_effect/guardian_stealth
id = "guardian_stealth"
- alert_type = /atom/movable/screen/alert/status_effect/instealth
+ alert_type = null
/// Damage added in stealth mode.
var/damage_bonus = 35
/// Our wound bonus when in stealth mode. Allows you to actually cause wounds, unlike normal.
@@ -112,5 +97,3 @@
SIGNAL_HANDLER
SEND_SIGNAL(owner, COMSIG_GUARDIAN_ASSASSIN_REVEALED)
qdel(src)
-
-#undef CAN_STEALTH_ALERT
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/charger.dm b/code/modules/mob/living/basic/guardian/guardian_types/charger.dm
index 10ef9c34d538..2d21803c25fc 100644
--- a/code/modules/mob/living/basic/guardian/guardian_types/charger.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_types/charger.dm
@@ -11,13 +11,11 @@
creator_name = "Charger"
creator_desc = "Moves very fast, does medium damage on attack, can be ridden and can charge at targets, damaging the first target hit and forcing them to drop any items they are holding."
creator_icon = "charger"
+ toggle_button_type = /datum/action/cooldown/mob_cooldown/charge/basic_charge/guardian
/mob/living/basic/guardian/charger/Initialize(mapload, datum/guardian_fluff/theme)
. = ..()
AddElement(/datum/element/ridable, /datum/component/riding/creature/guardian)
- var/datum/action/cooldown/mob_cooldown/charge/basic_charge/guardian/charge = new(src)
- charge.Grant(src)
- charge.set_click_ability(src)
/// Guardian charger's charging attack, it knocks items out of people's hands
/datum/action/cooldown/mob_cooldown/charge/basic_charge/guardian
@@ -28,6 +26,7 @@
button_icon_state = "speed"
background_icon = 'icons/hud/guardian.dmi'
background_icon_state = "base"
+ default_button_position = ui_guardian_special
charge_delay = 0
recoil_duration = 0
charge_damage = 20
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/dextrous.dm b/code/modules/mob/living/basic/guardian/guardian_types/dextrous.dm
index 206b7b79e12e..fc4ec6788373 100644
--- a/code/modules/mob/living/basic/guardian/guardian_types/dextrous.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_types/dextrous.dm
@@ -15,6 +15,7 @@
/mob/living/basic/guardian/dextrous/Initialize(mapload, datum/guardian_fluff/theme)
. = ..()
+ AddComponent(/datum/component/basic_inhands)
add_traits(list(TRAIT_ADVANCEDTOOLUSER, TRAIT_CAN_STRIP), ROUNDSTART_TRAIT)
AddElement(/datum/element/dextrous, hud_type = hud_type, can_throw = TRUE)
AddComponent(/datum/component/personal_crafting)
@@ -24,6 +25,18 @@
dropItemToGround(internal_storage)
return ..()
+/mob/living/basic/guardian/dextrous/create_actions()
+ for (var/action_type in self_actions)
+ if(isnull(action_type)) //no toggle button type
+ continue
+ if (locate(action_type) in actions)
+ continue
+ var/datum/action/new_action = new action_type(src)
+ //Show up at the top left like usual.
+ new_action.default_button_position = /datum/action::default_button_position
+ new_action.Grant(src)
+ update_action_buttons()
+
/mob/living/basic/guardian/dextrous/examine(mob/user)
. = ..()
if(isnull(internal_storage) || (internal_storage.item_flags & ABSTRACT))
@@ -95,7 +108,7 @@
/mob/living/basic/guardian/dextrous/proc/update_inv_internal_storage()
if(isnull(internal_storage) || isnull(client) || !hud_used?.hud_shown)
return
- internal_storage.screen_loc = ui_id
+ internal_storage.screen_loc = ui_back
client.screen += internal_storage
/mob/living/basic/guardian/dextrous/regenerate_icons()
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/explosive.dm b/code/modules/mob/living/basic/guardian/guardian_types/explosive.dm
index b3c0684571d5..49c15f62f402 100644
--- a/code/modules/mob/living/basic/guardian/guardian_types/explosive.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_types/explosive.dm
@@ -37,6 +37,7 @@
cooldown_time = 20 SECONDS
background_icon = 'icons/hud/guardian.dmi'
background_icon_state = "base"
+ default_button_position = ui_guardian_special
/// After this amount of time passses, bomb deactivates.
var/decay_time = 1 MINUTES
/// Static list of signals that activate the bomb.
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/gaseous.dm b/code/modules/mob/living/basic/guardian/guardian_types/gaseous.dm
index 8bf00ef43622..a70bd01f43bc 100644
--- a/code/modules/mob/living/basic/guardian/guardian_types/gaseous.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_types/gaseous.dm
@@ -9,7 +9,7 @@
creator_name = "Gaseous"
creator_desc = "Creates sparks on touch and continuously expels a gas of its choice. Automatically extinguishes the user if they catch on fire."
creator_icon = "gaseous"
- toggle_button_type = /atom/movable/screen/guardian/toggle_mode/gases
+ toggle_button_type = /datum/action/cooldown/guardian/toggle_mode/gases
/// Ability we use to select gases
var/datum/action/cooldown/mob_cooldown/expel_gas/gas
/// Rate of temperature stabilization per second.
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/protector.dm b/code/modules/mob/living/basic/guardian/guardian_types/protector.dm
index 45d746b23358..229732a30f84 100644
--- a/code/modules/mob/living/basic/guardian/guardian_types/protector.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_types/protector.dm
@@ -9,7 +9,7 @@
creator_name = "Protector"
creator_desc = "Causes you to teleport to it when out of range, unlike other parasites. Has two modes; Combat, where it does and takes medium damage, and Protection, where it does and takes almost no damage but moves slightly slower."
creator_icon = "protector"
- toggle_button_type = /atom/movable/screen/guardian/toggle_mode
+ toggle_button_type = /datum/action/cooldown/guardian/toggle_mode
/// Action which toggles our shield
var/datum/action/cooldown/mob_cooldown/protector_shield/shield
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/ranged.dm b/code/modules/mob/living/basic/guardian/guardian_types/ranged.dm
index 6752c52a5af7..126ca95da2c1 100644
--- a/code/modules/mob/living/basic/guardian/guardian_types/ranged.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_types/ranged.dm
@@ -13,7 +13,7 @@
creator_desc = "Has two modes. Ranged; which fires a constant stream of weak, armor-ignoring projectiles. Scout; where it cannot attack, but can move through walls and is quite hard to see. Can lay surveillance snares, which alert it when crossed, in either mode."
creator_icon = "ranged"
see_invisible = SEE_INVISIBLE_LIVING
- toggle_button_type = /atom/movable/screen/guardian/toggle_mode
+ toggle_button_type = /datum/action/cooldown/guardian/toggle_mode
/mob/living/basic/guardian/ranged/Initialize(mapload, datum/guardian_fluff/theme)
. = ..()
@@ -120,6 +120,7 @@
button_icon_state = "eye"
background_icon = 'icons/hud/guardian.dmi'
background_icon_state = "base"
+ default_button_position = ui_guardian_special
cooldown_time = 2 SECONDS
melee_cooldown_time = 0
click_to_activate = FALSE
diff --git a/code/modules/mob/living/basic/guardian/guardian_verbs.dm b/code/modules/mob/living/basic/guardian/guardian_verbs.dm
index c63dffd4316a..dc95097953cc 100644
--- a/code/modules/mob/living/basic/guardian/guardian_verbs.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_verbs.dm
@@ -43,15 +43,6 @@
to_chat(src, span_notice("You deactivate your light."))
set_light_on(FALSE)
-
-/// Prints what type of guardian we are and what we can do.
-/mob/living/basic/guardian/verb/check_type()
- set name = "Check Guardian Type"
- set category = "Guardian"
- set desc = "Check what type you are."
- to_chat(src, playstyle_string)
-
-
/// Speak with our boss at a distance
/mob/living/basic/guardian/proc/communicate()
if (isnull(summoner))
diff --git a/code/modules/mob/living/basic/lavaland/raptor/_raptor.dm b/code/modules/mob/living/basic/lavaland/raptor/_raptor.dm
index 2915cae13705..fdcb0707cd85 100644
--- a/code/modules/mob/living/basic/lavaland/raptor/_raptor.dm
+++ b/code/modules/mob/living/basic/lavaland/raptor/_raptor.dm
@@ -13,8 +13,8 @@ GLOBAL_LIST_EMPTY(raptor_population)
icon = 'icons/mob/simple/lavaland/raptor_big.dmi'
icon_state = "raptor_red"
base_icon_state = "raptor"
- pixel_w = -12
- base_pixel_w = -12
+ pixel_w = -20
+ base_pixel_w = -20
speed = 0.5
mob_biotypes = MOB_ORGANIC|MOB_BEAST
maxHealth = 200
@@ -42,8 +42,6 @@ GLOBAL_LIST_EMPTY(raptor_population)
ai_controller = null
/// Can this raptor breed?
var/can_breed = TRUE
- /// Should we change offsets on direction change?
- var/change_offsets = TRUE
/// Pet commands when we tame the raptor
var/static/list/pet_commands = list(
/datum/pet_command/breed,
@@ -105,7 +103,7 @@ GLOBAL_LIST_EMPTY(raptor_population)
if (!mapload)
GLOB.raptor_population += REF(src)
- AddComponent(/datum/component/obeys_commands, pet_commands, list(0, -base_pixel_w))
+ AddComponent(/datum/component/obeys_commands, pet_commands, list(-base_pixel_w, 0))
AddElement(\
/datum/element/change_force_on_death,\
move_resist = MOVE_RESIST_DEFAULT,\
@@ -124,9 +122,7 @@ GLOBAL_LIST_EMPTY(raptor_population)
update_blackboard()
AddElement(/datum/element/footstep, footstep_type = FOOTSTEP_MOB_CLAW)
- RegisterSignal(src, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_dir_change))
RegisterSignal(src, COMSIG_LIVING_SCOOPED_UP, PROC_REF(on_picked_up))
- adjust_offsets(dir)
add_happiness_component()
/mob/living/basic/raptor/Destroy()
@@ -143,22 +139,6 @@ GLOBAL_LIST_EMPTY(raptor_population)
return
return ..()
-/mob/living/basic/raptor/proc/on_dir_change(datum/source, old_dir, new_dir)
- SIGNAL_HANDLER
- adjust_offsets(new_dir)
-
-/mob/living/basic/raptor/proc/adjust_offsets(direction)
- if (!change_offsets)
- return
-
- switch (direction)
- if (NORTH, SOUTH)
- add_offsets(RAPTOR_INNATE_SOURCE, w_add = 0, animate = FALSE)
- if (EAST, SOUTHEAST, NORTHEAST)
- add_offsets(RAPTOR_INNATE_SOURCE, w_add = -8, animate = FALSE)
- if (WEST, SOUTHWEST, NORTHWEST)
- add_offsets(RAPTOR_INNATE_SOURCE, w_add = 8, animate = FALSE)
-
/mob/living/basic/raptor/examine(mob/user)
. = ..()
if (stat == DEAD)
@@ -385,7 +365,6 @@ GLOBAL_LIST_EMPTY(raptor_population)
density = initial(density)
move_resist = initial(move_resist)
can_breed = initial(can_breed)
- change_offsets = initial(change_offsets)
if (new_stage == RAPTOR_ADULT)
// Adults need to be tamed with skill rather than snacks
@@ -395,16 +374,10 @@ GLOBAL_LIST_EMPTY(raptor_population)
density = FALSE
can_breed = FALSE
move_resist = MOVE_RESIST_DEFAULT
- change_offsets = FALSE
if (prev_stage == RAPTOR_ADULT)
AddComponent(/datum/component/tameable, food_types = food_types, tame_chance = 25, bonus_tame_chance = 15, unique = TRUE)
- if (change_offsets)
- adjust_offsets(dir)
- else
- remove_offsets(RAPTOR_INNATE_SOURCE, FALSE)
-
if (can_breed)
add_breeding_component()
else
diff --git a/code/modules/mob/living/basic/lavaland/raptor/raptor_ai_controller.dm b/code/modules/mob/living/basic/lavaland/raptor/raptor_ai_controller.dm
index 2f1353bfc79d..bae8bd0d2cdc 100644
--- a/code/modules/mob/living/basic/lavaland/raptor/raptor_ai_controller.dm
+++ b/code/modules/mob/living/basic/lavaland/raptor/raptor_ai_controller.dm
@@ -43,7 +43,7 @@
"wags their tail against",
"playfully leans against"
),
- BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/exact_match,
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/exact_match/ignore_friends,
BB_HUNT_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_BABIES_PARTNER_TYPES = list(/mob/living/basic/raptor),
diff --git a/code/modules/mob/living/basic/lavaland/raptor/raptor_color.dm b/code/modules/mob/living/basic/lavaland/raptor/raptor_color.dm
index bc6782895c21..b366a44b7b61 100644
--- a/code/modules/mob/living/basic/lavaland/raptor/raptor_color.dm
+++ b/code/modules/mob/living/basic/lavaland/raptor/raptor_color.dm
@@ -124,12 +124,9 @@ GLOBAL_LIST_INIT(raptor_colors, init_raptor_colors())
// Purple raptors never "fully" grow up, and remain usable as backpacks
/datum/raptor_color/purple/setup_adult(mob/living/basic/raptor/raptor)
- raptor.base_pixel_w = initial(raptor.base_pixel_w)
raptor.can_be_held = TRUE
raptor.density = FALSE
raptor.move_resist = MOVE_RESIST_DEFAULT
- raptor.change_offsets = FALSE
- raptor.remove_offsets(RAPTOR_INNATE_SOURCE, FALSE)
raptor.held_w_class = WEIGHT_CLASS_BULKY
. = ..()
// Non-shorties cannot ride these, so we gotta keep em tameable through food
diff --git a/code/modules/mob/living/basic/lavaland/raptor/raptor_egg.dm b/code/modules/mob/living/basic/lavaland/raptor/raptor_egg.dm
index 2f038f02e64f..ac8d5ebb0f4d 100644
--- a/code/modules/mob/living/basic/lavaland/raptor/raptor_egg.dm
+++ b/code/modules/mob/living/basic/lavaland/raptor/raptor_egg.dm
@@ -11,9 +11,9 @@
/// Current growth progress
var/growth_progress = 0
/// Minimum growth progress per second
- var/min_growth_rate = 0.5
+ var/min_growth_rate = 0.8
/// Maximum growth progress per second
- var/max_growth_rate = 1
+ var/max_growth_rate = 1.2
/obj/item/food/egg/raptor_egg/Initialize(mapload)
. = ..()
diff --git a/code/modules/mob/living/basic/pets/orbie/orbie_abilities.dm b/code/modules/mob/living/basic/pets/orbie/orbie_abilities.dm
index fb9994a93216..c016f649291e 100644
--- a/code/modules/mob/living/basic/pets/orbie/orbie_abilities.dm
+++ b/code/modules/mob/living/basic/pets/orbie/orbie_abilities.dm
@@ -19,28 +19,29 @@
overlay_icon_state = "bg_default_border"
cooldown_time = 30 SECONDS
///camera we use to take photos
- var/obj/item/camera/ability_camera
+ var/obj/item/camera/internal_camera
/datum/action/cooldown/mob_cooldown/capture_photo/Grant(mob/grant_to)
. = ..()
if(isnull(owner))
return
- ability_camera = new(owner)
- ability_camera.print_picture_on_snap = FALSE
- RegisterSignal(ability_camera, COMSIG_PREQDELETED, PROC_REF(on_camera_delete))
+ internal_camera = new(owner)
+ internal_camera.print_picture_on_snap = FALSE
+ internal_camera.cooldown = 1 SECONDS
+ RegisterSignal(internal_camera, COMSIG_PREQDELETED, PROC_REF(on_camera_delete))
/datum/action/cooldown/mob_cooldown/capture_photo/Activate(atom/target)
- if(isnull(ability_camera))
+ if(isnull(internal_camera))
return FALSE
- ability_camera.captureimage(target, owner)
+ internal_camera.attempt_picture(target, owner)
StartCooldown()
return TRUE
/datum/action/cooldown/mob_cooldown/capture_photo/proc/on_camera_delete(datum/source)
SIGNAL_HANDLER
- UnregisterSignal(ability_camera, COMSIG_PREQDELETED)
- ability_camera = null
+ UnregisterSignal(internal_camera, COMSIG_PREQDELETED)
+ internal_camera = null
/datum/action/cooldown/mob_cooldown/capture_photo/Destroy()
- QDEL_NULL(ability_camera)
+ QDEL_NULL(internal_camera)
return ..()
diff --git a/code/modules/mob/living/basic/pets/parrot/poly.dm b/code/modules/mob/living/basic/pets/parrot/poly.dm
index f79df7e8dec2..88885ad8861b 100644
--- a/code/modules/mob/living/basic/pets/parrot/poly.dm
+++ b/code/modules/mob/living/basic/pets/parrot/poly.dm
@@ -42,7 +42,7 @@
if(!SStts.tts_enabled)
return
- voice = pick(SStts.available_speakers)
+ voice = SStts.random_tts_voice()
if(SStts.pitch_enabled)
if(findtext(voice, "Woman"))
pitch = 12 // up-pitch by one octave
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
index ea32e7e5d6df..3bd92b489505 100644
--- a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
+++ b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm
@@ -95,7 +95,7 @@
/mob/living/basic/revenant/Initialize(mapload)
. = ..()
AddElement(/datum/element/simple_flying)
- add_traits(list(TRAIT_SPACEWALK, TRAIT_SIXTHSENSE, TRAIT_FREE_HYPERSPACE_MOVEMENT, TRAIT_SEE_BLESSED_TILES, TRAIT_IGNORE_ELEVATION), INNATE_TRAIT)
+ add_traits(list(TRAIT_COMBAT_MODE_LOCK, TRAIT_SPACEWALK, TRAIT_SIXTHSENSE, TRAIT_FREE_HYPERSPACE_MOVEMENT, TRAIT_SEE_BLESSED_TILES, TRAIT_IGNORE_ELEVATION), INNATE_TRAIT)
grant_actions_by_list(abilities)
diff --git a/code/modules/mob/living/blood.dm b/code/modules/mob/living/blood.dm
index f06bed87776f..554edf197482 100644
--- a/code/modules/mob/living/blood.dm
+++ b/code/modules/mob/living/blood.dm
@@ -400,7 +400,7 @@
if(blood_data["viruses"] && transfer_viruses)
for(var/datum/disease/blood_disease as anything in blood_data["viruses"])
- if((blood_disease.spread_flags & DISEASE_SPREAD_SPECIAL) || (blood_disease.spread_flags & DISEASE_SPREAD_NON_CONTAGIOUS))
+ if((blood_disease.spread_flags & (DISEASE_SPREAD_SPECIAL|DISEASE_SPREAD_NON_CONTAGIOUS)))
continue
target.ForceContractDisease(blood_disease)
diff --git a/code/modules/mob/living/carbon/alien/special/facehugger.dm b/code/modules/mob/living/carbon/alien/special/facehugger.dm
index b06e9ab1fbca..cfdf70e57738 100644
--- a/code/modules/mob/living/carbon/alien/special/facehugger.dm
+++ b/code/modules/mob/living/carbon/alien/special/facehugger.dm
@@ -305,7 +305,7 @@
/obj/item/clothing/mask/facehugger/lamarr
name = "Lamarr"
- desc = "The Research Director's pet, a domesticated and debeaked xenomorph facehugger. Friendly, but may still try to couple with your head."
+ desc = "The Research Director's pet, a domesticated and debeaked alien. Friendly, but may still try to couple with your head."
sterile = TRUE
slowdown = 1.5 //lamarr is too fat after being fed in captivity to effectively slow people down or something
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 3d1142537cc9..b8cf950409ef 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -535,6 +535,10 @@
cure_blind(EYES_COVERED)
overlay_fullscreen("tint", /atom/movable/screen/fullscreen/impaired, 2)
+ else if(tint >= TINT_MILD)
+ cure_blind(EYES_COVERED)
+ overlay_fullscreen("tint", /atom/movable/screen/fullscreen/impaired, 1)
+
else
cure_blind(EYES_COVERED)
clear_fullscreen("tint", 0 SECONDS)
diff --git a/code/modules/mob/living/carbon/human/human_update_icons.dm b/code/modules/mob/living/carbon/human/human_update_icons.dm
index 3c2661d0659a..78a306dd8b72 100644
--- a/code/modules/mob/living/carbon/human/human_update_icons.dm
+++ b/code/modules/mob/living/carbon/human/human_update_icons.dm
@@ -904,15 +904,19 @@ generate/load female uniform sprites matching all previously decided variables
remove_overlay(EYES_LAYER)
if(HAS_TRAIT(src, TRAIT_HUSK) || HAS_TRAIT(src, TRAIT_INVISIBLE_MAN))
return
- var/obj/item/bodypart/head/noggin = get_bodypart(BODY_ZONE_HEAD)
- if(!(noggin?.head_flags & HEAD_EYESPRITES))
- return
+
// eyes (missing eye sprites get handled by the head itself, but sadly we have to do this stupid shit here, for now)
var/obj/item/organ/eyes/eye_organ = get_organ_slot(ORGAN_SLOT_EYES)
- if(eye_organ)
- eye_organ.refresh(call_update = FALSE)
- overlays_standing[EYES_LAYER] = eye_organ.generate_body_overlay(src)
- apply_overlay(EYES_LAYER)
+ if (!eye_organ)
+ return
+
+ var/obj/item/bodypart/head/noggin = get_bodypart(deprecise_zone(eye_organ.zone)) // Futureproofing for HARS/weird species
+ if(istype(noggin) && !(noggin?.head_flags & HEAD_EYESPRITES))
+ return
+
+ eye_organ.refresh(call_update = FALSE)
+ overlays_standing[EYES_LAYER] = eye_organ.generate_body_overlay(src, noggin)
+ apply_overlay(EYES_LAYER)
/// Updates face (as of now, only eye) offsets
/mob/living/carbon/human/update_face_offset()
diff --git a/code/modules/mob/living/carbon/human/login.dm b/code/modules/mob/living/carbon/human/login.dm
index 9b3e23a37867..0dc1bb20ab99 100644
--- a/code/modules/mob/living/carbon/human/login.dm
+++ b/code/modules/mob/living/carbon/human/login.dm
@@ -4,7 +4,7 @@
dna?.species?.on_owner_login(src)
if(SStts.tts_enabled && !voice)
- voice = pick(SStts.available_speakers)
+ voice = SStts.random_tts_voice(gender)
if(!LAZYLEN(afk_thefts))
return
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 3225f7a4c0b0..5ee30cec038c 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -514,7 +514,7 @@
//for more info on why this is not atom/pull, see examinate() in mob.dm
/mob/living/verb/pulled(atom/movable/AM as mob|obj in oview(1))
set name = "Pull"
- set category = "Object"
+ set category = "IC"
if(istype(AM) && Adjacent(AM))
start_pulling(AM)
@@ -534,7 +534,7 @@
stop_pulling()
//same as above
-/mob/living/pointed(atom/A as mob|obj|turf in view(client.view, src))
+/mob/living/pointed(atom/A)
if(INCAPACITATED_IGNORING(src, INCAPABLE_RESTRAINTS))
return FALSE
@@ -861,6 +861,7 @@
mob_mask = icon('icons/hud/screen_gen.dmi', health_doll_icon_state) //swap to something generic if they have no special doll
livingdoll.add_filter("mob_shape_mask", 1, alpha_mask_filter(icon = mob_mask))
livingdoll.add_filter("inset_drop_shadow", 2, drop_shadow_filter(size = -1))
+ livingdoll.health_overlay.maptext = MAPTEXT("[round(healthpercent, 1)]%
")
if(severity > 0)
overlay_fullscreen("brute", /atom/movable/screen/fullscreen/brute, severity)
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 701b118ebdcf..002a446039f6 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -330,13 +330,13 @@
*/
/mob/living/proc/grab(mob/living/target)
if(!istype(target))
- return FALSE
+ return GRAB_SKIP
if(SEND_SIGNAL(src, COMSIG_LIVING_GRAB, target) & (COMPONENT_CANCEL_ATTACK_CHAIN|COMPONENT_SKIP_ATTACK))
- return FALSE
+ return GRAB_FAILURE
if(target.check_block(src, 0, "[src]'s grab", UNARMED_ATTACK))
- return FALSE
+ return GRAB_FAILURE
target.grabbedby(src)
- return TRUE
+ return GRAB_SUCCESS
/**
* Called when this mob is grabbed by another mob.
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index 6c332091709d..58fc8748648b 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -225,6 +225,23 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
//Get which verb is prefixed to the message before radio but after most modifications
message_mods[SAY_MOD_VERB] = say_mod(message, message_mods)
+ var/identifier = "invalid"
+ var/tts_message_to_use = tts_message || message
+
+
+ if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE))
+ var/list/filter = list()
+ var/list/special_filter = list()
+ if(length(voice_filter) > 0)
+ filter += voice_filter
+
+ if(length(tts_filter) > 0)
+ filter += tts_filter.Join(",")
+
+ var/shell_scrubbed_input = tts_speech_filter(html_decode(tts_message_to_use))
+ identifier = "[sha1(get_tts_voice(filter, special_filter) + filter.Join(",") + num2text(pitch) + special_filter.Join("|") + shell_scrubbed_input + blip_base + num2text(blip_number))].[world.time]"
+ message_mods[MODE_TTS_IDENTIFIER] = identifier
+
//This is before anything that sends say a radio message, and after all important message type modifications, so you can scumb in alien chat or something
if(saymode && (saymode.handle_message(src, message, spans, language, message_mods) & SAYMODE_MESSAGE_HANDLED))
return
@@ -296,6 +313,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
var/dist = get_dist(speaker, src) - message_range
if(dist > 0 && dist <= EAVESDROP_EXTRA_RANGE && !HAS_TRAIT(src, TRAIT_GOOD_HEARING))
raw_message = stars(raw_message)
+ var/speaker_name = span_name("[message_mods[MODE_SPEAKER_NAME_OVERRIDE] || speaker]")
if(message_range != INFINITY && dist > EAVESDROP_EXTRA_RANGE && !HAS_TRAIT(src, TRAIT_GOOD_HEARING))
// Too far away and don't have good hearing, you can't hear anything
if(is_blind() || HAS_TRAIT(speaker, TRAIT_INVISIBLE_MAN)) // Can't see them speak either
@@ -305,7 +323,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
// But we can still see them speak
if(speaker_is_signing)
- deaf_message = "[span_name("[speaker]")] [speaker.get_default_say_verb()] something, but the motions are too subtle to make out from afar."
+ deaf_message = "[speaker_name] [speaker.get_default_say_verb()] something, but the motions are too subtle to make out from afar."
else if(!HAS_TRAIT(src, TRAIT_DEAF)) // If we can't hear we want to continue to the default deaf message
if(isliving(speaker))
var/mob/living/living_speaker = speaker
@@ -313,7 +331,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
if(mouth_hidden && !HAS_TRAIT(src, TRAIT_SEE_MASK_WHISPER)) // Can't see them speak if their mouth is covered or hidden, unless we're an empath
return FALSE
- deaf_message = "[span_name("[speaker]")] [speaker.verb_whisper] something, but you are too far away to hear [speaker.p_them()]."
+ deaf_message = "[speaker_name] [speaker.verb_whisper] something, but you are too far away to hear [speaker.p_them()]."
if(deaf_message)
deaf_type = MSG_VISUAL
@@ -353,7 +371,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
if(speaker != src)
if(!radio_freq) //These checks have to be separate, else people talking on the radio will make "You can't hear yourself!" appear when hearing people over the radio while deaf.
- deaf_message = "[span_name("[speaker]")] [speaker.get_default_say_verb()] something but you cannot hear [speaker.p_them()]."
+ deaf_message = "[speaker_name] [speaker.get_default_say_verb()] something but you cannot hear [speaker.p_them()]."
deaf_type = MSG_VISUAL
else
deaf_message = span_notice("You can't hear yourself!")
@@ -369,7 +387,12 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
// Recompose message for AI hrefs, language incomprehension.
message = compose_message(speaker, message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, spans, message_mods)
var/show_message_success = show_message(message, MSG_AUDIBLE, deaf_message, deaf_type, avoid_highlight)
- return understood && show_message_success
+ if(show_message_success && understood)
+ return TRUE
+ else if (show_message_success && !understood)
+ return HEARD_BUT_DIDNT_UNDERSTAND
+ else
+ return FALSE
/mob/living/send_speech(message_raw, message_range = 6, obj/source = src, bubble_type = bubble_icon, list/spans, datum/language/message_language = null, list/message_mods = list(), forced = null, tts_message, list/tts_filter)
var/whisper_range = 0
@@ -406,9 +429,18 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
continue
listening |= player_mob
+ var/tts_message_to_use = tts_message || message_raw
+ var/list/filter = list()
+ var/list/special_filter = list()
+ if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE))
+ if(length(voice_filter) > 0)
+ filter += voice_filter
+
+ if(length(tts_filter) > 0)
+ filter += tts_filter.Join(",")
+
// this signal ignores whispers or language translations (only used by beetlejuice component)
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_LIVING_SAY_SPECIAL, src, message_raw)
-
var/list/listened = list()
for(var/atom/movable/listening_movable as anything in listening)
if(!listening_movable)
@@ -420,29 +452,13 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
//speech bubble
var/list/speech_bubble_recipients = list()
- var/found_client = FALSE
var/talk_icon_state = say_test(message_raw)
for(var/mob/M in listening)
if(M.client)
if(!M.client.prefs.read_preference(/datum/preference/toggle/enable_runechat) || (SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(src, TRAIT_BYPASS_MEASURES)))
speech_bubble_recipients.Add(M.client)
- found_client = TRUE
- if(SStts.tts_enabled && voice && found_client && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE))
- var/tts_message_to_use = tts_message
- if(!tts_message_to_use)
- tts_message_to_use = message_raw
-
- var/list/filter = list()
- var/list/special_filter = list()
- if(length(voice_filter) > 0)
- filter += voice_filter
-
- if(length(tts_filter) > 0)
- filter += tts_filter.Join(",")
-
- var/voice_to_use = get_tts_voice(filter, special_filter)
- if (!CONFIG_GET(flag/tts_no_whisper) || (CONFIG_GET(flag/tts_no_whisper) && !message_mods[WHISPER_MODE]))
- INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice_to_use, filter.Join(","), listened, message_range = message_range, pitch = pitch, special_filters = special_filter.Join("|"))
+ if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE))
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, get_tts_voice(filter, special_filter), filter.Join(","), listened, message_range = message_range, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER])
var/image/say_popup = image('icons/mob/effects/talk.dmi', src, "[bubble_type][talk_icon_state]", FLY_LAYER)
SET_PLANE_EXPLICIT(say_popup, ABOVE_GAME_PLANE, src)
@@ -503,19 +519,6 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
message = capitalize(message)
tts_message = capitalize(tts_message)
- ///caps the length of individual letters to 3: ex: heeeeeeyy -> heeeyy
- /// prevents TTS from choking on unrealistic text while keeping emphasis
- var/static/regex/length_regex = regex(@"(.+)\1\1\1", "gi")
- while(length_regex.Find(tts_message))
- var/replacement = tts_message[length_regex.index]+tts_message[length_regex.index]+tts_message[length_regex.index]
- tts_message = replacetext(tts_message, length_regex.match, replacement, length_regex.index)
-
- // removes repeated consonants at the start of a word: ex: sss
- var/static/regex/word_start_regex = regex(@"\b([^aeiou\L])\1", "gi")
- while(word_start_regex.Find(tts_message))
- var/replacement = tts_message[word_start_regex.index]
- tts_message = replacetext(tts_message, word_start_regex.match, replacement, word_start_regex.index)
-
return list("message" = message, "tts_message" = tts_message, "tts_filter" = tts_filter)
/mob/living/proc/radio(message, list/message_mods = list(), list/spans, language)
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index df81c1632426..86315686aac5 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -1034,6 +1034,8 @@
button_icon_state = "ai_shell"
/datum/action/innate/deploy_shell/Trigger(mob/clicker, trigger_flags)
+ if(!..())
+ return
var/mob/living/silicon/ai/AI = owner
if(!AI)
return
@@ -1047,6 +1049,8 @@
var/mob/living/silicon/robot/last_used_shell
/datum/action/innate/deploy_last_shell/Trigger(mob/clicker, trigger_flags)
+ if(!..())
+ return
if(!owner)
return
if(last_used_shell)
diff --git a/code/modules/mob/living/silicon/ai/ai_say.dm b/code/modules/mob/living/silicon/ai/ai_say.dm
index 88abc47ab378..d57afeb8876c 100644
--- a/code/modules/mob/living/silicon/ai/ai_say.dm
+++ b/code/modules/mob/living/silicon/ai/ai_say.dm
@@ -30,14 +30,32 @@
return FALSE
. = ..()
if(.)
+ var/list/filter = list()
+ var/list/special_filter = list()
+ if(length(voice_filter) > 0)
+ filter += voice_filter
+ if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE))
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), list(), message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER])
return .
if(message_mods[MODE_HEADSET])
if(radio)
radio.talk_into(src, message, , spans, language, message_mods)
+ var/list/filter = list()
+ var/list/special_filter = list()
+ if(length(voice_filter) > 0)
+ filter += voice_filter
+ if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE))
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), list(), message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER])
return NOPASS
else if(message_mods[RADIO_EXTENSION] in GLOB.default_radio_channels)
if(radio)
radio.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods)
+ var/list/filter = list()
+ var/list/special_filter = list()
+ if(length(voice_filter) > 0)
+ filter += voice_filter
+ if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE))
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), list(), message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER])
return NOPASS
return FALSE
diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm
index 429b66c1a348..2a78778bc884 100644
--- a/code/modules/mob/living/silicon/robot/robot.dm
+++ b/code/modules/mob/living/silicon/robot/robot.dm
@@ -393,13 +393,6 @@
// to have to check if every camera is null or not before doing anything, to prevent runtime errors.
// I could change the network to null but I don't know what would happen, and it seems too hacky for me.
-/mob/living/silicon/robot/mode()
- set name = "Activate Held Object"
- set category = "IC"
- set src = usr
-
- return ..()
-
/mob/living/silicon/robot/execute_mode()
if(incapacitated)
return
diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm
index c10570979a74..542522b06203 100644
--- a/code/modules/mob/living/silicon/silicon.dm
+++ b/code/modules/mob/living/silicon/silicon.dm
@@ -53,8 +53,7 @@
/mob/living/silicon/Initialize(mapload)
. = ..()
- if(SStts.tts_enabled)
- voice = pick(SStts.available_speakers)
+ voice = SStts.random_tts_voice()
GLOB.silicon_mobs += src
add_faction(FACTION_SILICON)
if(ispath(radio))
diff --git a/code/modules/mob/living/silicon/silicon_say.dm b/code/modules/mob/living/silicon/silicon_say.dm
index b81d44983d61..6c15289ce49c 100644
--- a/code/modules/mob/living/silicon/silicon_say.dm
+++ b/code/modules/mob/living/silicon/silicon_say.dm
@@ -31,7 +31,6 @@
if(istype(brain))
namepart = brain.mainframe.name
designation = brain.mainframe.job
-
for(var/mob/hearing_mob in GLOB.player_list)
if(hearing_mob.binarycheck())
if(isAI(hearing_mob))
@@ -88,14 +87,32 @@
/mob/living/silicon/radio(message, list/message_mods = list(), list/spans, language)
. = ..()
if(.)
+ var/list/filter = list()
+ var/list/special_filter = list()
+ if(length(voice_filter) > 0)
+ filter += voice_filter
+ if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE))
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), list(), message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER])
return
if(message_mods[MODE_HEADSET])
if(radio)
radio.talk_into(src, message, , spans, language, message_mods)
+ var/list/filter = list()
+ var/list/special_filter = list()
+ if(length(voice_filter) > 0)
+ filter += voice_filter
+ if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE))
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), list(), message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER])
return NOPASS
else if(message_mods[RADIO_EXTENSION] in GLOB.default_radio_channels)
if(radio)
radio.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods)
+ var/list/filter = list()
+ var/list/special_filter = list()
+ if(length(voice_filter) > 0)
+ filter += voice_filter
+ if(SStts.tts_enabled && voice && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG) && !HAS_TRAIT(src, TRAIT_UNKNOWN_VOICE))
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(message), language, get_tts_voice(filter, special_filter), filter.Join(","), list(), message_range = 7, pitch = pitch, special_filters = special_filter.Join("|"), blip_base = blip_base, blip_number = blip_number, identifier = message_mods[MODE_TTS_IDENTIFIER])
return NOPASS
return FALSE
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm
index f8fcddc4bc7e..7693d47647df 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm
@@ -78,7 +78,7 @@ While using this makes the system rely on OnFire, it still gives options for tim
///The internal attack ID for the elite's OpenFire() proc to use
var/chosen_attack_num = 0
-/datum/action/innate/elite_attack/create_button()
+/datum/action/innate/elite_attack/create_button(mob/viewer)
var/atom/movable/screen/movable/action_button/button = ..()
button.maptext = ""
button.maptext_x = 6
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index dfdce3c32922..d8285681d952 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -547,7 +547,6 @@
*/
/mob/verb/examinate(atom/examinify as mob|obj|turf in view()) //It used to be oview(12), but I can't really say why
set name = "Examine"
- set category = "IC"
DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(run_examinate), examinify))
@@ -590,7 +589,10 @@
var/list/result = examinify.examine(src)
var/atom_title = examinify.examine_title(src, thats = TRUE)
examining(examinify, result)
- SEND_SIGNAL(src, COMSIG_MOB_EXAMINING, examinify, result)
+ var/alist/overrides = alist()
+ SEND_SIGNAL(src, COMSIG_MOB_EXAMINING, examinify, result, overrides)
+ if (length(overrides))
+ result = overrides[max(overrides)]
if(removes_double_click)
result += span_notice("You can examine [examinify] closer...")
result_combined = (atom_title ? fieldset_block("[atom_title].", jointext(result, "
"), "boxed_message") : boxed_message(jointext(result, "
")))
@@ -782,7 +784,7 @@
*/
/mob/verb/mode()
set name = "Activate Held Object"
- set category = "Object"
+ set category = "IC"
set src = usr
DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(execute_mode)))
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index d43edf33d746..43585ef78867 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -191,7 +191,7 @@
var/list/client_colours = list()
/// List of filter names used in the past client color update for cleanup
var/list/color_filter_store = list()
- var/hud_type = /datum/hud
+ var/datum/hud/hud_type = /datum/hud
var/datum/focus //What receives our keyboard inputs. src by default
diff --git a/code/modules/mob/mob_say.dm b/code/modules/mob/mob_say.dm
index 568df514fdf4..746a22ea65db 100644
--- a/code/modules/mob/mob_say.dm
+++ b/code/modules/mob/mob_say.dm
@@ -183,7 +183,7 @@
return FALSE
///The amount of items we are looking for in the message
-#define MESSAGE_MODS_LENGTH 6
+#define MESSAGE_MODS_LENGTH 7
/mob/proc/check_for_custom_say_emote(message, list/mods)
var/customsaypos = findtext(message, "*")
diff --git a/code/modules/mod/mod_core.dm b/code/modules/mod/mod_core.dm
index 70c4fb90d95d..e9d0f5f3dbfc 100644
--- a/code/modules/mod/mod_core.dm
+++ b/code/modules/mod/mod_core.dm
@@ -457,18 +457,11 @@
// Not super sure if this should just be the same, but will see.
maxcharge = 15 * STANDARD_CELL_CHARGE
charge = 15 * STANDARD_CELL_CHARGE
- /// The mob to be spawned by the core
- var/mob/living/spawned_mob_type = /mob/living/basic/butterfly/lavaland/temporary
- /// Max number of mobs it can spawn
- var/max_spawns = 3
- /// Mob spawner for the core
- var/datum/component/spawner/mob_spawner
/// Particle holder for pollen particles
var/obj/effect/abstract/particle_holder/particle_effect
/obj/item/mod/core/plasma/lavaland/Destroy()
QDEL_NULL(particle_effect)
- QDEL_NULL(mob_spawner)
return ..()
/obj/item/mod/core/plasma/lavaland/install(obj/item/mod/control/mod_unit)
@@ -482,28 +475,12 @@
/obj/item/mod/core/plasma/lavaland/proc/on_toggle()
SIGNAL_HANDLER
if(mod.active)
- particle_effect = new(mod.wearer, /particles/pollen, PARTICLE_ATTACH_MOB)
- mob_spawner = mod.wearer.AddComponent(/datum/component/spawner, \
- spawn_types = list(spawned_mob_type), \
- spawn_time = 5 SECONDS, \
- max_spawned = 3, \
- faction = mod.wearer.get_faction(), \
- )
- RegisterSignal(mob_spawner, COMSIG_SPAWNER_SPAWNED, PROC_REF(new_mob))
+ particle_effect = new(mod.wearer, /particles/pollen/modsuit, PARTICLE_ATTACH_MOB)
RegisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED, PROC_REF(spread_flowers))
return
QDEL_NULL(particle_effect)
- UnregisterSignal(mob_spawner, COMSIG_SPAWNER_SPAWNED)
UnregisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED)
- for(var/datum/mob in mob_spawner.spawned_things)
- qdel(mob)
- QDEL_NULL(mob_spawner)
-
-/obj/item/mod/core/plasma/lavaland/proc/new_mob(spawner, mob/living/basic/butterfly/lavaland/temporary/spawned)
- SIGNAL_HANDLER
- if(spawned)
- spawned.source = src
/obj/item/mod/core/plasma/lavaland/proc/spread_flowers(atom/source, atom/oldloc, dir, forced)
SIGNAL_HANDLER
diff --git a/code/modules/mod/modules/modules_timeline.dm b/code/modules/mod/modules/modules_timeline.dm
index bfca6e421b7b..a544e0963e01 100644
--- a/code/modules/mod/modules/modules_timeline.dm
+++ b/code/modules/mod/modules/modules_timeline.dm
@@ -362,7 +362,7 @@
/obj/structure/chrono_field/update_overlays()
. = ..()
var/ttk_frame = 1 - (timetokill / initial(timetokill))
- ttk_frame = clamp(CEILING(ttk_frame * CHRONO_FRAME_COUNT, 1), 1, CHRONO_FRAME_COUNT)
+ ttk_frame = clamp(ceil(ttk_frame * CHRONO_FRAME_COUNT), 1, CHRONO_FRAME_COUNT)
if(ttk_frame != RPpos)
RPpos = ttk_frame
underlays -= mob_underlay
diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm
index 9607ac1e5063..464608fe058f 100644
--- a/code/modules/modular_computers/computers/item/computer.dm
+++ b/code/modules/modular_computers/computers/item/computer.dm
@@ -13,6 +13,7 @@
armor_type = /datum/armor/item_modular_computer
light_system = OVERLAY_LIGHT_DIRECTIONAL
interaction_flags_mouse_drop = NEED_HANDS | ALLOW_RESTING
+ voice_filter = "alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000"
///The ID currently stored in the computer.
var/obj/item/card/id/stored_id
@@ -154,6 +155,11 @@
register_context()
update_appearance()
+/obj/item/modular_computer/LateInitialize()
+ . = ..()
+ if(SStts.tts_enabled)
+ voice = SStts.computer_voice
+
///Initialize the shell for this item, or the physical machinery it belongs to.
/obj/item/modular_computer/proc/add_shell_component(capacity = SHELL_CAPACITY_MEDIUM, shell_flags = NONE)
shell = physical.AddComponent(/datum/component/shell, list(new /obj/item/circuit_component/modpc), capacity, shell_flags)
@@ -371,6 +377,7 @@
* * silent - Boolean, determines whether fluff text would be printed
*/
/obj/item/modular_computer/remove_id(mob/user, silent = FALSE)
+ var/obj/item/lost_id = stored_id
if(!stored_id)
return ..()
@@ -381,8 +388,6 @@
user.put_in_hands(stored_id)
else
stored_id.forceMove(drop_location())
-
- var/obj/item/lost_id = stored_id
stored_id = null
if(!silent && !isnull(user))
diff --git a/code/modules/modular_computers/computers/item/disks/unique_disks.dm b/code/modules/modular_computers/computers/item/disks/unique_disks.dm
index 9251ffa79228..845c8696bc37 100644
--- a/code/modules/modular_computers/computers/item/disks/unique_disks.dm
+++ b/code/modules/modular_computers/computers/item/disks/unique_disks.dm
@@ -1,6 +1,6 @@
/obj/item/disk/computer/syndicate
- name = "golden data disk"
- desc = "A data disk with some high-tech programs, probably expensive as hell."
+ name = "syndicate data disk"
+ desc = "A data disk with some high-tech programs. Rumor has it that the disk is made of gold, which probably makes it expensive as hell."
icon_state = "datadisk9"
custom_materials = list(/datum/material/gold = SMALL_MATERIAL_AMOUNT)
diff --git a/code/modules/modular_computers/computers/item/laptop.dm b/code/modules/modular_computers/computers/item/laptop.dm
index 2ae9e794b573..d366e6b74ff1 100644
--- a/code/modules/modular_computers/computers/item/laptop.dm
+++ b/code/modules/modular_computers/computers/item/laptop.dm
@@ -72,7 +72,6 @@
/obj/item/modular_computer/laptop/verb/open_computer()
set name = "Toggle Open"
- set category = "Object"
set src in view(1)
try_toggle_open(usr)
diff --git a/code/modules/modular_computers/file_system/programs/file_browser.dm b/code/modules/modular_computers/file_system/programs/file_browser.dm
index d33c34a348ba..d25a9894307e 100644
--- a/code/modules/modular_computers/file_system/programs/file_browser.dm
+++ b/code/modules/modular_computers/file_system/programs/file_browser.dm
@@ -5,25 +5,26 @@ GLOBAL_LIST_INIT(print_types, init_print_types())
for(var/obj/item/canvas/canvas_type as anything in typesof(/obj/item/canvas))
var/width = canvas_type::width
var/height = canvas_type::height
- LAZYADDASSOC(print_types, "[width]x[height]", list("[canvas_type]" = list(
+ LAZYADDASSOC(print_types, "[width]x[height]", list(
"displayText" = "Canvas ([width]x[height])",
"typepath" = canvas_type,
"width" = width,
"height" = height,
"check_callback" = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(check_can_print_canvas)),
"prepare_callback" = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(prepare_canvas_from_file)),
- )))
- for(var/size in 1 to /obj/item/camera::picture_size_x_max)
- var/width = ICON_SIZE_X*(size*2-1)
- var/height = ICON_SIZE_Y*(size*2-1)
- LAZYADDASSOC(print_types, "[width]x[height]", list("[/obj/item/photo]" = list(
- "displayText" = "Photo Paper ([size*2-1]m focal length)",
+ ))
+ for(var/size in 1 to CAMERA_PICTURE_SIZE_HARD_LIMIT)
+ var/meters = APERTURE_TO_METERS(size)
+ var/width = ICON_SIZE_X * meters
+ var/height = ICON_SIZE_Y * meters
+ LAZYADDASSOC(print_types, "[width]x[height]", list(
+ "displayText" = "Photo Paper ([meters]m focal length)",
"typepath" = /obj/item/photo,
"width" = width,
"height" = height,
"check_callback" = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(check_can_print_photo)),
"prepare_callback" = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(prepare_photo_from_file)),
- )))
+ ))
return print_types
/proc/check_can_print_canvas(_typepath, _image_file, obj/item/modular_computer/computer, mob/user)
@@ -177,11 +178,8 @@ GLOBAL_LIST_INIT(print_types, init_print_types())
try_print(picture, params["width"], params["height"], params["offsetX"], params["offsetY"], params["typepath"], usr)
/datum/computer_file/program/filemanager/proc/try_print(datum/computer_file/image/picture, width, height, offset_x, offset_y, typepath, mob/user)
- var/list/print_types_for_dimensions = GLOB.print_types["[width]x[height]"]
- if(!length(print_types_for_dimensions))
- return
- var/list/print_type = print_types_for_dimensions[typepath]
- if(!print_type)
+ var/list/print_type = GLOB.print_types["[width]x[height]"]
+ if(!length(print_type))
return
var/image_width = picture.stored_icon.Width()
var/image_height = picture.stored_icon.Height()
@@ -203,10 +201,16 @@ GLOBAL_LIST_INIT(print_types, init_print_types())
/datum/computer_file/program/filemanager/ui_static_data(mob/user)
var/list/print_types = list()
- for(var/dimensions in GLOB.print_types)
- var/list/types_for_dimensions = GLOB.print_types[dimensions]
- for(var/print_typepath in types_for_dimensions)
- print_types += list(types_for_dimensions[print_typepath])
+ for(var/dimensions, types_for_dimensions in GLOB.print_types)
+ var/list/typepath = types_for_dimensions["typepath"]
+ if(ispath(typepath, /obj/item/canvas) && !(computer.hardware_flag & PROGRAM_CONSOLE))
+ continue
+ print_types += list(list(
+ displayText = types_for_dimensions["displayText"],
+ typepath = "[typepath]",
+ width = types_for_dimensions["width"],
+ height = types_for_dimensions["height"]
+ ))
return list("printTypes" = print_types)
/datum/computer_file/program/filemanager/ui_data(mob/user)
diff --git a/code/modules/modular_computers/file_system/programs/maintenance/camera.dm b/code/modules/modular_computers/file_system/programs/maintenance/camera.dm
index 4ff763e1d5f4..a2fd42d44342 100644
--- a/code/modules/modular_computers/file_system/programs/maintenance/camera.dm
+++ b/code/modules/modular_computers/file_system/programs/maintenance/camera.dm
@@ -14,8 +14,8 @@
var/obj/item/camera/app/internal_camera
/// Latest picture taken by the app.
var/datum/picture/internal_picture
- /// A mutable_appearance of the latest picture, for getting an appeance reference for the UI.
- var/mutable_appearance/picture_appearance
+ /// base64 of the current taken picture
+ var/internal_picture_icon
/// How many pictures were taken already, used for the camera's TGUI photo display
var/picture_number = 1
/// Can we edit the metadata of the latest picture?
@@ -29,24 +29,20 @@
// Special type of camera for this exact usecase to prevent harddels
/obj/item/camera/app
- name = "internal camera"
- desc = "Specialized internal camera protected from the hellish depths of SSWardrobe. \
- Yell at coders if you somehow manage to see this"
+ print_picture_on_snap = FALSE
+ cooldown = 1 SECONDS
+ light_range = 3
/datum/computer_file/program/maintenance/camera/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user)
. = ..()
internal_camera = new(computer)
- internal_camera.print_picture_on_snap = FALSE
- picture_appearance = new()
RegisterSignal(internal_camera, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_image_captured))
/datum/computer_file/program/maintenance/camera/Destroy()
QDEL_NULL(internal_camera)
- internal_picture = null
- QDEL_NULL(picture_appearance)
+ QDEL_NULL(internal_picture)
return ..()
-
/datum/computer_file/program/maintenance/camera/tap(atom/tapped_atom, mob/living/user, list/modifiers)
. = ..()
take_picture(user, get_turf(tapped_atom))
@@ -57,7 +53,7 @@
/datum/computer_file/program/maintenance/camera/kill_program(mob/user)
. = ..()
UnregisterSignal(computer, COMSIG_RANGED_ITEM_INTERACTING_WITH_ATOM_SECONDARY)
- internal_picture = null
+ QDEL_NULL(internal_picture)
/datum/computer_file/program/maintenance/camera/background_program(mob/user)
. = ..()
@@ -68,47 +64,21 @@
take_picture(user, get_turf(target))
/datum/computer_file/program/maintenance/camera/proc/take_picture(mob/user, turf/target)
- if(internal_camera.blending)
- user.balloon_alert(user, "still blending!")
- return
-
- var/spooky_camera = locate(/datum/computer_file/program/maintenance/spectre_meter) in computer.stored_files
- internal_camera.see_ghosts = spooky_camera ? CAMERA_SEE_GHOSTS_BASIC : CAMERA_NO_GHOSTS
- INVOKE_ASYNC(internal_camera, TYPE_PROC_REF(/obj/item/camera, captureimage), target, user, internal_camera.picture_size_x - 1, internal_camera.picture_size_y - 1)
+ internal_camera.see_ghosts = (locate(/datum/computer_file/program/maintenance/spectre_meter) in computer.stored_files) ? CAMERA_SEE_GHOSTS_BASIC : CAMERA_NO_GHOSTS
+ internal_camera.attempt_picture(target, user)
/datum/computer_file/program/maintenance/camera/proc/on_image_captured(cam, target, user, datum/picture/picture)
SIGNAL_HANDLER
+ QDEL_NULL(internal_picture)
internal_picture = picture
- picture_appearance.icon = internal_picture.picture_image
+ internal_picture_icon = icon2base64(internal_picture.picture_image)
current_picture_name = null
current_picture_desc = null
current_picture_caption = null
can_edit_metadata = TRUE
picture_number++
-/datum/computer_file/program/maintenance/camera/proc/save_picture(mob/user)
- var/datum/computer_file/image/photo_file = new(
- internal_picture.picture_image,
- display_name = internal_picture.picture_name || "photo[picture_number]",
- source_photo_or_painting = internal_picture
- )
- if(computer.store_file(photo_file, user))
- return FALSE
- commit_metadata()
- return TRUE
-
-/datum/computer_file/program/maintenance/camera/proc/print_picture(mob/user)
- if(computer.stored_paper < PHOTO_PAPER_COST)
- return
- commit_metadata()
- var/obj/item/photo/new_photo = new(computer.physical.drop_location())
- new_photo.set_picture(internal_picture, TRUE, TRUE)
- user?.put_in_hands(new_photo)
- playsound(computer.physical, 'sound/machines/printer.ogg', 100, TRUE)
- computer.stored_paper--
- computer.visible_message(span_notice("\The [computer] prints out a paper."))
-
/datum/computer_file/program/maintenance/camera/proc/commit_metadata()
if(can_edit_metadata)
internal_picture.picture_name = current_picture_name
@@ -117,105 +87,90 @@
can_edit_metadata = FALSE
/datum/computer_file/program/maintenance/camera/ui_static_data(mob/user)
- return list("maxNameLength" = 32, "maxDescLength" = 128, "maxCaptionLength" = 256, "printCost" = 1)
+ return list(
+ "maxNameLength" = 32,
+ "maxDescLength" = 128,
+ "maxCaptionLength" = 256,
+ "printCost" = 1,
+ "minSize" = 2,
+ "maxSize" = CAMERA_PICTURE_SIZE_HARD_LIMIT
+ )
/datum/computer_file/program/maintenance/camera/ui_data(mob/user)
var/list/data = list()
if(!isnull(internal_picture))
- data["photo"] = REF(picture_appearance.appearance)
+ data["photo"] = internal_picture_icon
data["canEditMetadata"] = can_edit_metadata
data["name"] = current_picture_name
data["desc"] = current_picture_desc
data["caption"] = current_picture_caption
data["storedPaper"] = computer.stored_paper
- data["size"] = internal_camera.picture_size_x
- data["minSize"] = internal_camera.picture_size_x_min
- data["maxSize"] = min(internal_camera.picture_size_x_max, CAMERA_PICTURE_SIZE_HARD_LIMIT)
+
+ data["width"] = internal_camera.picture_size_x
+ data["height"] = internal_camera.picture_size_y
+ data["size"] = "[APERTURE_TO_METERS(internal_camera.picture_size_x)]x[APERTURE_TO_METERS(internal_camera.picture_size_y)]"
return data
/datum/computer_file/program/maintenance/camera/ui_act(action, params, datum/tgui/ui, datum/ui_state/state)
. = ..()
+ if(.)
+ return
+
switch(action)
- if("adjustSize")
- var/new_size = round(params["value"], 1)
- if(!ISINRANGE(new_size, internal_camera.picture_size_x_min, min(CAMERA_PICTURE_SIZE_HARD_LIMIT, internal_camera.picture_size_x_max)))
+ if("adjustWidth")
+ var/new_size = text2num(params["value"])
+ if(!new_size)
return
- internal_camera.picture_size_x = new_size
- internal_camera.picture_size_y = new_size
+ return internal_camera.adjust_zoom(desired_x = new_size)
+
+ if("adjustHeight")
+ var/new_size = text2num(params["value"])
+ if(!new_size)
+ return
+ return internal_camera.adjust_zoom(desired_y = new_size)
+
if("setName")
if(!(internal_picture && can_edit_metadata))
- return
+ return FALSE
current_picture_name = trim(params["value"], PREVENT_CHARACTER_TRIM_LOSS(32))
+ return TRUE
+
if("setDesc")
if(!(internal_picture && can_edit_metadata))
return
current_picture_desc = trim(params["value"], PREVENT_CHARACTER_TRIM_LOSS(128))
+ return TRUE
+
if("setCaption")
if(!(internal_picture && can_edit_metadata))
return
current_picture_caption = trim(params["value"], PREVENT_CHARACTER_TRIM_LOSS(256))
+ return TRUE
+
if("savePhoto")
if(!internal_picture)
return
- save_picture(ui.user)
+ var/datum/computer_file/image/photo_file = new(
+ internal_picture.picture_image,
+ display_name = internal_picture.picture_name || "photo[picture_number]",
+ source_photo_or_painting = internal_picture
+ )
+ if(computer.store_file(photo_file, ui.user))
+ return FALSE
+ commit_metadata()
+ return TRUE
if("printPhoto")
if(!internal_picture)
- return
- print_picture(ui.user)
- return TRUE
-
-/obj/item/circuit_component/mod_program/camera
- associated_program = /datum/computer_file/program/maintenance/camera
- circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL
-
- ///A target to take a picture of.
- var/datum/port/input/picture_target
- ///The size of the photo to take.
- var/datum/port/input/picture_size
- ///The photographed target
- var/datum/port/output/photographed
- /**
- * Pinged when the image has been captured.
- * I'm not using the default trigger output here because the process is asynced,
- * even though I'm mostly sure it only sleeps if there's a set user.
- */
- var/datum/port/output/photo_taken
-
-/obj/item/circuit_component/mod_program/camera/populate_ports()
- . = ..()
- picture_target = add_input_port("Picture Target", PORT_TYPE_ATOM)
- picture_size = add_input_port("Picture Size", PORT_TYPE_NUMBER)
- photographed = add_output_port("Photographed Entity", PORT_TYPE_ATOM)
- photo_taken = add_output_port("Photo Taken", PORT_TYPE_SIGNAL)
-
-/obj/item/circuit_component/mod_program/camera/register_shell(atom/movable/shell)
- . = ..()
- var/datum/computer_file/program/maintenance/camera/cam = associated_program
- RegisterSignal(cam.internal_camera, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_image_captured))
-
-/obj/item/circuit_component/mod_program/camera/unregister_shell()
- var/datum/computer_file/program/maintenance/camera/cam = associated_program
- UnregisterSignal(cam.internal_camera, COMSIG_CAMERA_IMAGE_CAPTURED)
- return ..()
-
-/obj/item/circuit_component/mod_program/camera/input_received(datum/port/input/port)
- if(!COMPONENT_TRIGGERED_BY(port, trigger_input))
- return
- var/atom/target = picture_target.value
- if(!target)
- var/turf/our_turf = get_location()
- target = locate(our_turf.x, our_turf.y, our_turf.z)
- if(!target)
- return
- var/datum/computer_file/program/maintenance/camera/cam = associated_program
- if(!cam.internal_camera.can_target(target))
- return
- var/pic_size = clamp(round(picture_size.value), 1, cam.internal_camera.picture_size_x_max)-1
- INVOKE_ASYNC(cam.internal_camera, TYPE_PROC_REF(/obj/item/camera, captureimage), target, null, pic_size, pic_size)
-
-/obj/item/circuit_component/mod_program/camera/proc/on_image_captured(obj/item/camera/source, atom/target, mob/user)
- SIGNAL_HANDLER
- photographed.set_output(target)
- photo_taken.set_output(COMPONENT_SIGNAL)
+ return FALSE
+ if(computer.stored_paper < PHOTO_PAPER_COST)
+ return FALSE
+ commit_metadata()
+ var/obj/item/photo/new_photo = new(computer.physical.drop_location())
+ new_photo.set_picture(internal_picture, TRUE, TRUE)
+ ui.user.put_in_hands(new_photo)
+ playsound(computer.physical, 'sound/machines/printer.ogg', 100, TRUE)
+ computer.stored_paper--
+ computer.visible_message(span_notice("\The [computer] prints out a paper."))
+ return TRUE
diff --git a/code/modules/modular_computers/file_system/programs/virtual_pet.dm b/code/modules/modular_computers/file_system/programs/virtual_pet.dm
index f36a4e407e63..6715322104c9 100644
--- a/code/modules/modular_computers/file_system/programs/virtual_pet.dm
+++ b/code/modules/modular_computers/file_system/programs/virtual_pet.dm
@@ -330,7 +330,7 @@ GLOBAL_LIST_EMPTY(virtual_pets_list)
var/datum/action/cooldown/mob_cooldown/capture_photo/photo_ability = new(pet)
photo_ability.Grant(pet)
pet.ai_controller?.set_blackboard_key(BB_PHOTO_ABILITY, photo_ability)
- RegisterSignal(photo_ability.ability_camera, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_photo_captured))
+ RegisterSignal(photo_ability.internal_camera, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_photo_captured))
/datum/computer_file/program/virtual_pet/proc/announce_global_updates(message)
if(isnull(message))
diff --git a/code/modules/movespeed/_movespeed_modifier.dm b/code/modules/movespeed/_movespeed_modifier.dm
index fea932650f69..af4b29d89d6d 100644
--- a/code/modules/movespeed/_movespeed_modifier.dm
+++ b/code/modules/movespeed/_movespeed_modifier.dm
@@ -53,6 +53,12 @@ Key procs
if(!id)
id = "[type]" //We turn the path into a string.
+/datum/movespeed_modifier/vv_edit_var(var_name, var_value)
+ if(GLOB.movespeed_modification_cache[type] == src)
+ return FALSE
+
+ return ..()
+
GLOBAL_LIST_EMPTY(movespeed_modification_cache)
/// Grabs a STATIC MODIFIER datum from cache. YOU MUST NEVER EDIT THESE DATUMS, OR IT WILL AFFECT ANYTHING ELSE USING IT TOO!
diff --git a/code/modules/pai/actions.dm b/code/modules/pai/actions.dm
index 7e1899cba2c4..095bbb57b0d5 100644
--- a/code/modules/pai/actions.dm
+++ b/code/modules/pai/actions.dm
@@ -7,6 +7,7 @@
if(!ispAI(owner))
return FALSE
pai_owner = owner
+ return ..()
/datum/action/innate/pai/software
name = "Software Interface"
@@ -15,7 +16,9 @@
overlay_icon_state = "bg_tech_border"
/datum/action/innate/pai/software/Trigger(mob/clicker, trigger_flags)
- ..()
+ . = ..()
+ if(!.)
+ return
pai_owner.ui_act()
/datum/action/innate/pai/shell
@@ -25,7 +28,9 @@
overlay_icon_state = "bg_tech_border"
/datum/action/innate/pai/shell/Trigger(mob/clicker, trigger_flags)
- ..()
+ . = ..()
+ if(!.)
+ return
if(pai_owner.holoform)
pai_owner.fold_in(0)
else
@@ -38,7 +43,9 @@
overlay_icon_state = "bg_tech_border"
/datum/action/innate/pai/chassis/Trigger(mob/clicker, trigger_flags)
- ..()
+ . = ..()
+ if(!.)
+ return
pai_owner.choose_chassis()
/datum/action/innate/pai/rest
@@ -48,7 +55,9 @@
overlay_icon_state = "bg_tech_border"
/datum/action/innate/pai/rest/Trigger(mob/clicker, trigger_flags)
- ..()
+ . = ..()
+ if(!.)
+ return
pai_owner.toggle_resting()
/datum/action/innate/pai/light
@@ -59,7 +68,9 @@
overlay_icon_state = "bg_tech_border"
/datum/action/innate/pai/light/Trigger(mob/clicker, trigger_flags)
- ..()
+ . = ..()
+ if(!.)
+ return
pai_owner.toggle_integrated_light()
/datum/action/innate/pai/messenger
@@ -70,6 +81,8 @@
/datum/action/innate/pai/messenger/Trigger(mob/clicker, trigger_flags)
. = ..()
+ if(!.)
+ return
var/obj/item/pai_card/pai_holder = owner.loc
if(!istype(pai_holder.loc, /obj/item/modular_computer))
owner.balloon_alert(owner, "not in a pda!")
diff --git a/code/modules/paperwork/paper.dm b/code/modules/paperwork/paper.dm
index 15140879421c..71e3c03cb8a5 100644
--- a/code/modules/paperwork/paper.dm
+++ b/code/modules/paperwork/paper.dm
@@ -327,7 +327,6 @@
/obj/item/paper/verb/rename()
set name = "Rename paper"
- set category = "Object"
set src in usr
if(!usr.can_read(src) || usr.is_blind() || INCAPACITATED_IGNORING(usr, INCAPABLE_RESTRAINTS|INCAPABLE_GRAB) || (isobserver(usr) && !isAdminGhostAI(usr)))
diff --git a/code/modules/photography/camera/camera.dm b/code/modules/photography/camera/camera.dm
index 5cf55f99bfed..00815854fbec 100644
--- a/code/modules/photography/camera/camera.dm
+++ b/code/modules/photography/camera/camera.dm
@@ -3,11 +3,12 @@
icon = 'icons/obj/art/camera.dmi'
desc = "A polaroid camera."
icon_state = "camera"
+ base_icon_state = "camera"
inhand_icon_state = "camera"
worn_icon_state = "camera"
lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
- light_system = OVERLAY_LIGHT_DIRECTIONAL //Used as a flash here.
+ light_system = NONE
light_range = 6
light_color = COLOR_WHITE
light_power = FLASH_LIGHT_POWER
@@ -24,52 +25,55 @@
var/on = TRUE
/// Whether we are still processing an image.
var/blending = FALSE
- /// Our icon_state when ready to take a picture.
- var/state_on = "camera"
- /// Our icon_state when not ready to take a picture.
- var/state_off = "camera_off"
-
/// The maximum amount of pictures we can take before needing new film.
var/pictures_max = 10
/// The amount of pictures we can still take before needing new film.
var/pictures_left = 10
/// Currently inserted holorecord disk.
var/obj/item/disk/holodisk/disk
-
/// Whether we flash upon taking a picture.
var/flash_enabled = TRUE
/// Whether we silence our picture taking and zoom adjusting sounds.
var/silent = FALSE
/// To what degree ghosts are visible in our pictures.
- var/see_ghosts = CAMERA_NO_GHOSTS //for the spoop of it
+ var/see_ghosts = CAMERA_NO_GHOSTS
/// Whether the camera should print pictures immediately when a picture is taken.
var/print_picture_on_snap = TRUE
/// Whether we allow setting picture label/desc/scribble when a picture is taken.
var/can_customise = TRUE
/// Picture name we default to when none is set manually.
var/default_picture_name
-
+ ///Width of the picture
var/picture_size_x = 2
+ ///height of the picture
var/picture_size_y = 2
- var/picture_size_x_min = 1
- var/picture_size_y_min = 1
- var/picture_size_x_max = 4
- var/picture_size_y_max = 4
+ ///Internal holder to apply camera light on
+ VAR_PRIVATE/atom/movable/light_holder
+
+/// Special type of component so it does not intefer with the modular computer default lighting system if any
+/datum/component/overlay_lighting/camera
+ dupe_mode = COMPONENT_DUPE_SOURCES
/obj/item/camera/Initialize(mapload)
. = ..()
+
+ //we do this so if this camera is used as an internal component, the flash will still be visible
+ if(flash_enabled)
+ var/atom/movable/parent = loc
+ light_holder = src
+ while(!(isnull(parent) || ismob(parent) || isturf(parent)))
+ light_holder = parent
+ parent = light_holder.loc
+ light_holder.AddComponentFrom(REF(src), /datum/component/overlay_lighting/camera, light_range, light_power, light_color, FALSE, TRUE, FALSE, TRUE)
+
AddComponent(/datum/component/shell, list(new /obj/item/circuit_component/camera, new /obj/item/circuit_component/remotecam/polaroid), SHELL_CAPACITY_SMALL)
register_context()
-/obj/item/camera/examine(mob/user)
- . = ..()
- . += span_notice("It has [pictures_left] photos left.")
- . += span_notice("Alt-click to change its focusing, allowing you to set how big of an area it will capture.")
-
- if(isnull(disk))
- . += span_notice("It has a slot for a holorecord disk.")
- else
- . += span_notice("It has \an [disk.name] inserted.")
+/obj/item/camera/Destroy(force)
+ if(light_holder)
+ light_holder.RemoveComponentSource(REF(src), /datum/component/overlay_lighting/camera)
+ light_holder = null
+ return ..()
/obj/item/camera/add_context(atom/source, list/context, obj/item/held_item, mob/living/user)
context[SCREENTIP_CONTEXT_ALT_LMB] = "Adjust Zoom"
@@ -88,187 +92,161 @@
return CONTEXTUAL_SCREENTIP_SET
-/obj/item/camera/proc/adjust_zoom(mob/user)
- if(loc != user)
- to_chat(user, span_warning("You must be holding the camera to continue!"))
- return FALSE
- var/desired_x = tgui_input_number(user, "How wide do you want the camera to shoot?", "Zoom", picture_size_x, picture_size_x_max, picture_size_x_min)
- if(!desired_x || QDELETED(user) || QDELETED(src) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH|ALLOW_PAI) || loc != user)
- return FALSE
- var/desired_y = tgui_input_number(user, "How high do you want the camera to shoot", "Zoom", picture_size_y, picture_size_y_max, picture_size_y_min)
- if(!desired_y || QDELETED(user) || QDELETED(src) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH|ALLOW_PAI) || loc != user)
- return FALSE
- picture_size_x = min(clamp(desired_x, picture_size_x_min, picture_size_x_max), CAMERA_PICTURE_SIZE_HARD_LIMIT)
- picture_size_y = min(clamp(desired_y, picture_size_y_min, picture_size_y_max), CAMERA_PICTURE_SIZE_HARD_LIMIT)
- return TRUE
+/obj/item/camera/examine(mob/user)
+ . = ..()
+ . += span_notice("It has [pictures_left] photos left.")
+ . += span_notice("Alt-click to change its focusing, allowing you to set how big of an area it will capture.")
+ . += span_notice("The present dimensions of the picture are [EXAMINE_HINT("[APERTURE_TO_METERS(picture_size_x)]x[APERTURE_TO_METERS(picture_size_y)]")]")
+
+ if(isnull(disk))
+ . += span_notice("It has a slot for a holorecord disk.")
+ else
+ . += span_notice("It has \an [disk.name] inserted.")
/obj/item/camera/Exited(atom/movable/gone, direction)
. = ..()
if(gone == disk)
disk = null
-/obj/item/camera/click_alt(mob/user)
- if(!adjust_zoom(user))
- return CLICK_ACTION_BLOCKING
- if(silent) // Don't out your silent cameras
- user.playsound_local(get_turf(src), 'sound/machines/click.ogg', 50, TRUE)
- else
- playsound(src, 'sound/machines/click.ogg', 50, TRUE)
- return CLICK_ACTION_SUCCESS
+/**
+ * Adjusts the zoom of this camera
+ * Arguments
+ *
+ * * desired_x - the x zoom value to use
+ * * desired_y - the y zoom value to use
+ * * mob/user - the optional user who is taking the photo. Passing the mob will ask for input and ignore the above params
+*/
+/obj/item/camera/proc/adjust_zoom(desired_x = picture_size_x, desired_y = picture_size_y, mob/user)
+ SHOULD_NOT_OVERRIDE(TRUE)
-/obj/item/camera/attack_self(mob/user)
- if(isnull(disk))
- return
- playsound(src, 'sound/machines/card_slide.ogg', 50)
- user.put_in_hands(disk)
- disk = null
-
-/obj/item/camera/attack(mob/living/carbon/human/M, mob/user)
- return
-
-/obj/item/camera/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
- if(istype(tool, /obj/item/camera_film))
- return camera_film_act(user, tool)
- if(istype(tool, /obj/item/disk/holodisk))
- return holodisk_act(user, tool)
-
-/obj/item/camera/proc/camera_film_act(mob/living/user, obj/item/camera_film/new_film)
- if(pictures_left)
- balloon_alert(user, "isn't empty!")
- return ITEM_INTERACT_BLOCKING
- if(!user.temporarilyRemoveItemFromInventory(new_film))
- return ITEM_INTERACT_BLOCKING
- playsound(src, 'sound/machines/click.ogg', 50, TRUE)
- qdel(new_film)
- pictures_left = pictures_max
- return ITEM_INTERACT_SUCCESS
-
-/obj/item/camera/proc/holodisk_act(mob/living/user, obj/item/disk/holodisk/new_disk)
- if(!user.transferItemToLoc(new_disk, src))
- balloon_alert(user, "stuck in hand!")
- return TRUE
- if(disk)
- user.put_in_hands(disk)
- balloon_alert(user, "disks swapped!")
- else
- balloon_alert(user, "disk inserted!")
- playsound(src, 'sound/machines/card_slide.ogg', 50)
- disk = new_disk
- return ITEM_INTERACT_SUCCESS
-
-/// Attempt to take an image, optionally given a user.
-/obj/item/camera/proc/attempt_picture(atom/target, atom/user)
- if(!can_target(target, user))
- return FALSE
- if(!photo_taken(target, user))
- return FALSE
- return TRUE
-
-/// Check whether we can take a picture of the target, optionally given a user.
-/obj/item/camera/proc/can_target(atom/target, atom/user)
- if(!on || blending || !pictures_left)
- return FALSE
- var/turf/target_turf = get_turf(target)
- if(isnull(target_turf))
- return FALSE
- if(isAI(user))
- return can_ai_target(target_turf)
- if(ismob(user))
- return can_mob_target(target_turf, user)
-
- if(isliving(loc))
- if(!(target_turf in view(world.view, loc)))
+ if(user)
+ if(loc != user)
+ to_chat(user, span_warning("You must be holding the camera to continue!"))
+ return FALSE
+ desired_x = tgui_input_number(user, "Set camera half width Aperture", "Zoom", picture_size_x, CAMERA_PICTURE_SIZE_HARD_LIMIT, 2)
+ if(!desired_x || QDELETED(user) || QDELETED(src) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH|ALLOW_PAI) || loc != user)
+ return FALSE
+ desired_y = tgui_input_number(user, "Set camera half height Aperture", "Zoom", picture_size_y, CAMERA_PICTURE_SIZE_HARD_LIMIT, 2)
+ if(!desired_y || QDELETED(user) || QDELETED(src) || !user.can_perform_action(src, FORBID_TELEKINESIS_REACH|ALLOW_PAI) || loc != user)
return FALSE
- return TRUE
-
- // User is an atom or null
- if(!(target_turf in view(world.view, user || src)))
- return FALSE
- return TRUE
-
-/// Check whether an AI could take a picture of the target turf.
-/obj/item/camera/proc/can_ai_target(turf/target_turf)
- return SScameras.is_visible_by_cameras(target_turf)
-/// Check whether a mob could take a picture of the target turf.
-/obj/item/camera/proc/can_mob_target(turf/target_turf, mob/user)
- return (target_turf in get_hear_turfs(user.client?.view || world.view, user.client?.eye || user))
+ picture_size_x = clamp(desired_x, 2, CAMERA_PICTURE_SIZE_HARD_LIMIT)
+ picture_size_y = clamp(desired_y, 2, CAMERA_PICTURE_SIZE_HARD_LIMIT)
-/obj/item/camera/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
- // Always skip on storage and tables
- if(HAS_TRAIT(interacting_with, TRAIT_COMBAT_MODE_SKIP_INTERACTION))
- return NONE
+ if(user)
+ to_chat(user, span_notice("The dimensions of the picture will be [EXAMINE_HINT("[APERTURE_TO_METERS(picture_size_x)]x[APERTURE_TO_METERS(picture_size_y)]")]"))
- return ranged_interact_with_atom(interacting_with, user, modifiers)
+ return TRUE
+/// Resets flash to be used again
+/obj/item/camera/proc/cooldown()
+ PRIVATE_PROC(TRUE)
-/obj/item/camera/ranged_interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
- if(disk)
- if(!ismob(interacting_with))
- to_chat(user, span_warning("Invalid holodisk target."))
- return ITEM_INTERACT_BLOCKING
- if(disk.record)
- QDEL_NULL(disk.record)
+ UNTIL(!blending)
+ icon_state = base_icon_state
+ on = TRUE
- disk.record = new
- var/mob/recorded_mob = interacting_with
- disk.record.caller_name = recorded_mob.name
- disk.record.set_caller_image(recorded_mob)
+/// Turns the light/flash off
+/obj/item/camera/proc/flash_end()
+ PRIVATE_PROC(TRUE)
- return attempt_picture(interacting_with, user) ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_BLOCKING
+ light_holder.set_light_on(FALSE)
-/obj/item/camera/proc/photo_taken(atom/target, mob/user)
+/**
+ * Turns the flash quickly on and off when picture is taken
+ * Arguments
+ *
+ * * atom/target - the target we are trying to take a photo of
+ * * mob/user - the optional user who is taking the photo
+*/
+/obj/item/camera/proc/on_flash(atom/target, mob/user)
+ PROTECTED_PROC(TRUE)
+ SHOULD_CALL_PARENT(TRUE)
on = FALSE
addtimer(CALLBACK(src, PROC_REF(cooldown)), cooldown)
+ icon_state = "[base_icon_state]_off"
+ if(flash_enabled)
+ light_holder.set_light_on(TRUE)
+ addtimer(CALLBACK(src, PROC_REF(flash_end)), FLASH_LIGHT_DURATION, TIMER_OVERRIDE|TIMER_UNIQUE)
- icon_state = state_off
+/**
+ * Steal souls from all mobs captured in the image
+ * Arguments
+ *
+ * * list/victims - list of all mobs captured in the image
+*/
+/obj/item/camera/proc/steal_souls(list/mob/victims)
+ PROTECTED_PROC(TRUE)
- INVOKE_ASYNC(src, PROC_REF(captureimage), target, user, picture_size_x - 1, picture_size_y - 1)
- return TRUE
+ return
-/obj/item/camera/proc/cooldown()
- UNTIL(!blending)
- icon_state = state_on
- on = TRUE
+/**
+ * Attempts to take an image of the target and all its surrounding tiles
+ * Arguments
+ *
+ * * atom/target - the target we are trying to take a photo of
+ * * mob/user - the optional user who is taking the photo
+*/
+/obj/item/camera/proc/attempt_picture(atom/target, mob/user)
+ SHOULD_NOT_OVERRIDE(TRUE)
+
+ if(!on)
+ if(user)
+ user.balloon_alert(user, "flash still charging!")
+ return
-/obj/item/camera/proc/show_picture(mob/user, datum/picture/selection)
- var/obj/item/photo/P = new(src, selection)
- P.show(user)
- to_chat(user, P.desc)
- qdel(P)
+ if(blending)
+ if(user)
+ user.balloon_alert(user, "image still blending!")
+ return
-/obj/item/camera/proc/captureimage(atom/target, mob/user, size_x = 1, size_y = 1)
- if(flash_enabled)
- set_light_on(TRUE)
- addtimer(CALLBACK(src, PROC_REF(flash_end)), FLASH_LIGHT_DURATION, TIMER_OVERRIDE|TIMER_UNIQUE)
- blending = TRUE
+ INVOKE_ASYNC(src, PROC_REF(capture_image), target, user)
+
+/**
+ * Renders an image of the target and all its surrounding tiles
+ * Arguments passed from attempt_picture()
+*/
+/obj/item/camera/proc/capture_image(atom/target, mob/user)
+ PRIVATE_PROC(TRUE)
+
+ //Checking if we can target
var/turf/target_turf = get_turf(target)
- if(!isturf(target_turf))
- blending = FALSE
- return FALSE
- size_x = clamp(size_x, 0, CAMERA_PICTURE_SIZE_HARD_LIMIT)
- size_y = clamp(size_y, 0, CAMERA_PICTURE_SIZE_HARD_LIMIT)
- var/list/desc = list("This is a photo of an area of [size_x+1] meters by [size_y+1] meters.")
- var/list/mobs_spotted = list()
- var/list/dead_spotted = list()
+ if(isnull(target_turf))
+ return
+ if(isAI(user) && !SScameras.is_visible_by_cameras(target_turf))
+ return
+ if(isliving(loc) && !(target_turf in view(world.view, loc)))
+ return
+ if(!(target_turf in view(world.view, user || src)))
+ return
+
+ //These vars will be reused later on
+ var/size_x = picture_size_x - 1
+ var/size_y = picture_size_y - 1
var/list/viewlist = getviewsize(user?.client?.view || world.view)
var/view_range = max(viewlist[1], viewlist[2]) + max(size_x, size_y)
var/viewer = get_turf(user?.client?.eye || user || target) // not sure why target is a fallback
var/list/seen = get_hear_turfs(view_range, viewer)
+ if(!(target_turf in seen))
+ return
+
+ //taking the actual picture
+ on_flash(target, user)
+ blending = TRUE
+ var/list/mobs_spotted = list()
+ var/list/dead_spotted = list()
var/list/turfs = list()
var/list/mobs = list()
var/blueprints = FALSE
- var/clone_area = SSmapping.request_turf_block_reservation(size_x * 2 + 1, size_y * 2 + 1, 1)
+ var/width = APERTURE_TO_METERS(picture_size_x)
+ var/height = APERTURE_TO_METERS(picture_size_y)
///list of human names taken on picture
var/list/names = list()
var/cameranet_user = isAI(user) || istype(viewer, /mob/eye/camera)
-
- var/width = size_x * 2 + 1
- var/height = size_y * 2 + 1
+ var/datum/turf_reservation/clone_area = SSmapping.request_turf_block_reservation(width, height, 1)
for(var/turf/seen_placeholder as anything in CORNER_BLOCK_OFFSET(target_turf, width, height, -size_x, -size_y))
if(isnull(seen_placeholder))
continue
-
if(cameranet_user && !SScameras.is_visible_by_cameras(seen_placeholder))
continue
if(!cameranet_user && !(seen_placeholder in seen))
@@ -291,6 +269,7 @@
// do this before picture is taken so we can reveal revenants for the photo
steal_souls(mobs)
+ var/list/desc = list("This is a photo of an area of [width] meters by [height] meters.")
for(var/mob/mob as anything in mobs)
mobs_spotted += mob
if(mob.stat == DEAD)
@@ -299,50 +278,64 @@
if(!isnull(info))
desc += info
- var/psize_x = (size_x * 2 + 1) * ICON_SIZE_X
- var/psize_y = (size_y * 2 + 1) * ICON_SIZE_Y
- var/icon/get_icon = camera_get_icon(turfs, target_turf, psize_x, psize_y, clone_area, size_x, size_y, (size_x * 2 + 1), (size_y * 2 + 1))
- qdel(clone_area)
+ var/icon/get_icon = camera_get_icon(turfs, target_turf, clone_area)
get_icon.Blend("#000", ICON_UNDERLAY)
+ qdel(clone_area)
for(var/mob/living/carbon/human/person in mobs)
if(person.obscured_slots & HIDEFACE)
continue
names += "[person.name]"
- var/datum/picture/picture = new("picture", desc.Join("
"), mobs_spotted, dead_spotted, names, get_icon, null, psize_x, psize_y, blueprints, can_see_ghosts = see_ghosts)
+ var/datum/picture/picture = new("picture", desc.Join("
"), mobs_spotted, dead_spotted, names, get_icon, null, width * ICON_SIZE_X, height * ICON_SIZE_X, blueprints, can_see_ghosts = see_ghosts)
after_picture(user, picture)
SEND_SIGNAL(src, COMSIG_CAMERA_IMAGE_CAPTURED, target, user, picture)
blending = FALSE
- return picture
-
-/obj/item/camera/proc/flash_end()
- set_light_on(FALSE)
-
-/obj/item/camera/proc/steal_souls(list/victims)
- return
+/**
+ * Action to take after the picture is taken
+ *
+ * Arguments
+ *
+ * * mob/user - the user who took the picture
+ * * datum/picture/picture - the picture taken
+*/
/obj/item/camera/proc/after_picture(mob/user, datum/picture/picture)
+ PROTECTED_PROC(TRUE)
+
if(!silent)
playsound(loc, SFX_POLAROID, 75, TRUE, -3)
if(print_picture_on_snap)
printpicture(user, picture)
-
-/obj/item/camera/proc/printpicture(mob/user, datum/picture/picture) //Normal camera proc for creating photos
- pictures_left--
- var/obj/item/photo/new_photo = new(get_turf(src), picture)
+/**
+ * Print the picture tkane on film
+ *
+ * Arguments
+ *
+ * * mob/user - the user who took the picture
+ * * datum/picture/picture - the picture taken
+*/
+/obj/item/camera/proc/printpicture(mob/user, datum/picture/picture)
+ SHOULD_NOT_OVERRIDE(TRUE)
+
+ var/obj/item/photo/new_photo
if(user)
- if(in_range(new_photo, user) && user.put_in_hands(new_photo)) //needed because of TK
- to_chat(user, span_notice("[pictures_left] photos left."))
+ if(!pictures_left)
+ to_chat(user, span_warning("No film left."))
+ return
+
+ new_photo = new(src, picture)
+
+ to_chat(user, span_notice("[pictures_left] photos left."))
var/name_customized = FALSE
if(can_customise)
- var/customise = user.is_holding(new_photo) && tgui_alert(user, "Do you want to customize the photo?", "Customization", list("Yes", "No"))
+ var/customise = tgui_alert(user, "Do you want to customize the photo?", "Customization", list("Yes", "No"))
if(customise == "Yes")
- var/name1 = user.is_holding(new_photo) && tgui_input_text(user, "Set a name for this photo, or leave blank.", "Name", max_length = 32)
- var/desc1 = user.is_holding(new_photo) && tgui_input_text(user, "Set a description to add to photo, or leave blank.", "Description", max_length = 128)
- var/caption = user.is_holding(new_photo) && tgui_input_text(user, "Set a caption for this photo, or leave blank.", "Caption", max_length = 256)
+ var/name1 = tgui_input_text(user, "Set a name for this photo, or leave blank.", "Name", max_length = 32)
+ var/desc1 = tgui_input_text(user, "Set a description to add to photo, or leave blank.", "Description", max_length = 128)
+ var/caption = tgui_input_text(user, "Set a caption for this photo, or leave blank.", "Caption", max_length = 256)
if(name1)
picture.picture_name = name1
name_customized = TRUE
@@ -355,69 +348,84 @@
else if(isliving(loc))
var/mob/living/holder = loc
- if(holder.put_in_hands(new_photo))
- to_chat(holder, span_notice("[pictures_left] photos left."))
+
+ if(!pictures_left)
+ to_chat(holder, span_warning("No film left."))
+ return
+
+ new_photo = new(get_turf(src), picture)
+
+ to_chat(holder, span_notice("[pictures_left] photos left."))
new_photo.set_picture(picture, TRUE, TRUE)
if(CONFIG_GET(flag/picture_logging_camera))
picture.log_to_file()
-/obj/item/circuit_component/camera
- display_name = "Camera"
- desc = "A polaroid camera that takes pictures when triggered. The picture coordinate ports are relative to the position of the camera."
- circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL
-
- /// The atom that was photographed from either user click or trigger input.
- var/datum/port/output/photographed_atom
- /// The item that was added/removed.
- var/datum/port/output/picture_taken
- /// If set, the trigger input will target this atom.
- var/datum/port/input/picture_target
- /// If the above is unset, these coordinates will be used.
- var/datum/port/input/picture_coord_x
- var/datum/port/input/picture_coord_y
- /// Adjusts the picture_size_x variable of the camera.
- var/datum/port/input/adjust_size_x
- /// Idem but for picture_size_y.
- var/datum/port/input/adjust_size_y
-
- /// The camera this circut is attached to.
- var/obj/item/camera/camera
-
-/obj/item/circuit_component/camera/populate_ports()
- picture_taken = add_output_port("Picture Taken", PORT_TYPE_SIGNAL)
- photographed_atom = add_output_port("Photographed Entity", PORT_TYPE_ATOM)
-
- picture_target = add_input_port("Picture Target", PORT_TYPE_ATOM)
- picture_coord_x = add_input_port("Picture Coordinate X", PORT_TYPE_NUMBER)
- picture_coord_y = add_input_port("Picture Coordinate Y", PORT_TYPE_NUMBER)
- adjust_size_x = add_input_port("Picture Size X", PORT_TYPE_NUMBER, trigger = PROC_REF(sanitize_picture_size))
- adjust_size_y = add_input_port("Picture Size Y", PORT_TYPE_NUMBER, trigger = PROC_REF(sanitize_picture_size))
-
-/obj/item/circuit_component/camera/register_shell(atom/movable/shell)
- . = ..()
- camera = shell
- RegisterSignal(shell, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_image_captured))
+ pictures_left--
-/obj/item/circuit_component/camera/unregister_shell(atom/movable/shell)
- UnregisterSignal(shell, COMSIG_CAMERA_IMAGE_CAPTURED)
- camera = null
- return ..()
+ user.put_in_hands(new_photo)
-/obj/item/circuit_component/camera/proc/sanitize_picture_size()
- camera.picture_size_x = clamp(adjust_size_x.value, camera.picture_size_x_min, camera.picture_size_x_max)
- camera.picture_size_y = clamp(adjust_size_y.value, camera.picture_size_y_min, camera.picture_size_y_max)
-
-/obj/item/circuit_component/camera/proc/on_image_captured(obj/item/camera/source, atom/target, mob/user)
- SIGNAL_HANDLER
- photographed_atom.set_output(target)
- picture_taken.set_output(COMPONENT_SIGNAL)
-
-/obj/item/circuit_component/camera/input_received(datum/port/input/port)
- var/atom/target = picture_target.value
- if(!target)
- var/turf/our_turf = get_location()
- target = locate(our_turf.x + picture_coord_x.value, our_turf.y + picture_coord_y.value, our_turf.z)
- if(!target)
- return
- INVOKE_ASYNC(camera, TYPE_PROC_REF(/obj/item/camera, attempt_picture), target)
+/obj/item/camera/attack_self(mob/user)
+ if(isnull(disk))
+ return
+ playsound(src, 'sound/machines/card_slide.ogg', 50)
+ user.put_in_hands(disk)
+ disk = null
+
+/obj/item/camera/attack(mob/living/carbon/human/M, mob/user)
+ return
+
+/obj/item/camera/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
+ if(istype(tool, /obj/item/camera_film))
+ if(pictures_left)
+ balloon_alert(user, "isn't empty!")
+ return ITEM_INTERACT_BLOCKING
+ if(!user.temporarilyRemoveItemFromInventory(tool))
+ return ITEM_INTERACT_BLOCKING
+ playsound(src, 'sound/machines/click.ogg', 50, TRUE)
+ qdel(tool)
+ pictures_left = pictures_max
+ return ITEM_INTERACT_SUCCESS
+
+ if(istype(tool, /obj/item/disk/holodisk))
+ if(!user.transferItemToLoc(tool, src))
+ balloon_alert(user, "stuck in hand!")
+ return TRUE
+ if(disk)
+ user.put_in_hands(disk)
+ balloon_alert(user, "disks swapped!")
+ else
+ balloon_alert(user, "disk inserted!")
+ playsound(src, 'sound/machines/card_slide.ogg', 50)
+ disk = tool
+ return ITEM_INTERACT_SUCCESS
+
+/obj/item/camera/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
+ if(HAS_TRAIT(interacting_with, TRAIT_COMBAT_MODE_SKIP_INTERACTION))
+ return NONE
+ return ranged_interact_with_atom(interacting_with, user, modifiers)
+
+/obj/item/camera/ranged_interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
+ if(disk)
+ if(!ismob(interacting_with))
+ to_chat(user, span_warning("Invalid holodisk target."))
+ return ITEM_INTERACT_BLOCKING
+ if(disk.record)
+ QDEL_NULL(disk.record)
+
+ disk.record = new
+ var/mob/recorded_mob = interacting_with
+ disk.record.caller_name = recorded_mob.name
+ disk.record.set_caller_image(recorded_mob)
+
+ attempt_picture(interacting_with, user)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/item/camera/click_alt(mob/user)
+ if(!adjust_zoom(user = user))
+ return CLICK_ACTION_BLOCKING
+ if(silent) // Don't out your silent cameras
+ user.playsound_local(get_turf(src), 'sound/machines/click.ogg', 50, TRUE)
+ else
+ playsound(src, 'sound/machines/click.ogg', 50, TRUE)
+ return CLICK_ACTION_SUCCESS
diff --git a/code/modules/photography/camera/camera_image_capturing.dm b/code/modules/photography/camera/camera_image_capturing.dm
index 2b0387d6728c..7a4cc5857a0c 100644
--- a/code/modules/photography/camera/camera_image_capturing.dm
+++ b/code/modules/photography/camera/camera_image_capturing.dm
@@ -14,7 +14,9 @@
#define PHYSICAL_POSITION(atom) ((atom.y * ICON_SIZE_Y) + (atom.pixel_y))
-/obj/item/camera/proc/camera_get_icon(list/turfs, turf/center, psize_x = 96, psize_y = 96, datum/turf_reservation/clone_area, size_x, size_y, total_x, total_y)
+/obj/item/camera/proc/camera_get_icon(list/turfs, turf/center, datum/turf_reservation/clone_area)
+ PRIVATE_PROC(TRUE)
+
var/list/atoms = list()
var/list/lighting = list()
var/skip_normal = FALSE
@@ -24,35 +26,34 @@
backdrop.blend_mode = BLEND_OVERLAY
backdrop.color = "#292319"
- if(istype(clone_area) && total_x == clone_area.width && total_y == clone_area.height && size_x >= 0 && size_y > 0)
- var/turf/bottom_left = clone_area.bottom_left_turfs[1]
- var/cloned_center_x = round(bottom_left.x + ((total_x - 1) / 2))
- var/cloned_center_y = round(bottom_left.y + ((total_y - 1) / 2))
- for(var/t in turfs)
- var/turf/T = t
- var/offset_x = T.x - center.x
- var/offset_y = T.y - center.y
- var/turf/newT = locate(cloned_center_x + offset_x, cloned_center_y + offset_y, bottom_left.z)
- if(!(newT in clone_area.reserved_turfs)) //sanity check so we don't overwrite other areas somehow
- continue
- atoms += new /obj/effect/appearance_clone(newT, T)
- if(T.loc.icon_state)
- atoms += new /obj/effect/appearance_clone(newT, T.loc)
- if(T.lighting_object)
- var/obj/effect/appearance_clone/lighting_overlay = new(newT)
- lighting_overlay.appearance = T.lighting_object.current_underlay
- lighting_overlay.underlays += backdrop
- lighting_overlay.blend_mode = BLEND_MULTIPLY
- lighting += lighting_overlay
- for(var/atom/found_atom as anything in T.contents)
- if(HAS_TRAIT(found_atom, TRAIT_INVISIBLE_TO_CAMERA))
- if(see_ghosts)
- atoms += new /obj/effect/appearance_clone(newT, found_atom)
- else if(!found_atom.invisibility || (see_ghosts && isobserver(found_atom)))
+ var/turf/bottom_left = clone_area.bottom_left_turfs[1]
+ var/cloned_center_x = round(bottom_left.x + ((clone_area.width - 1) / 2))
+ var/cloned_center_y = round(bottom_left.y + ((clone_area.height - 1) / 2))
+ for(var/t in turfs)
+ var/turf/T = t
+ var/offset_x = T.x - center.x
+ var/offset_y = T.y - center.y
+ var/turf/newT = locate(cloned_center_x + offset_x, cloned_center_y + offset_y, bottom_left.z)
+ if(!(newT in clone_area.reserved_turfs)) //sanity check so we don't overwrite other areas somehow
+ continue
+ atoms += new /obj/effect/appearance_clone(newT, T)
+ if(T.loc.icon_state)
+ atoms += new /obj/effect/appearance_clone(newT, T.loc)
+ if(T.lighting_object)
+ var/obj/effect/appearance_clone/lighting_overlay = new(newT)
+ lighting_overlay.appearance = T.lighting_object.current_underlay
+ lighting_overlay.underlays += backdrop
+ lighting_overlay.blend_mode = BLEND_MULTIPLY
+ lighting += lighting_overlay
+ for(var/atom/found_atom as anything in T.contents)
+ if(HAS_TRAIT(found_atom, TRAIT_INVISIBLE_TO_CAMERA))
+ if(see_ghosts)
atoms += new /obj/effect/appearance_clone(newT, found_atom)
- skip_normal = TRUE
- wipe_atoms = TRUE
- center = locate(cloned_center_x, cloned_center_y, bottom_left.z)
+ else if(!found_atom.invisibility || (see_ghosts && isobserver(found_atom)))
+ atoms += new /obj/effect/appearance_clone(newT, found_atom)
+ skip_normal = TRUE
+ wipe_atoms = TRUE
+ center = locate(cloned_center_x, cloned_center_y, bottom_left.z)
if(!skip_normal)
for(var/i in turfs)
@@ -71,8 +72,11 @@
atoms += A
CHECK_TICK
+ var/psize_x = clone_area.width * ICON_SIZE_X
+ var/psize_y = clone_area.height * ICON_SIZE_Y
var/icon/res = icon('icons/blanks/96x96.dmi', "nothing")
res.Scale(psize_x, psize_y)
+
atoms += lighting
var/list/sorted = list()
@@ -134,7 +138,7 @@
img.Scale(base_w * abs(decompose.scale_x), base_h * decompose.scale_y)
if(decompose.scale_x < 0)
img.Flip(EAST)
- xo -= base_w * (decompose.scale_x - SIGN(decompose.scale_x)) / 2 * SIGN(decompose.scale_x)
+ xo -= base_w * (decompose.scale_x - sign(decompose.scale_x)) / 2 * sign(decompose.scale_x)
yo -= base_h * (decompose.scale_y - 1) / 2
// Rotation
if(decompose.rotation != 0)
diff --git a/code/modules/photography/camera/silicon_camera.dm b/code/modules/photography/camera/silicon_camera.dm
index a40207c559ab..b85d71b487fe 100644
--- a/code/modules/photography/camera/silicon_camera.dm
+++ b/code/modules/photography/camera/silicon_camera.dm
@@ -2,6 +2,7 @@
/obj/item/camera/siliconcam
name = "silicon photo camera"
resistance_flags = INDESTRUCTIBLE
+ cooldown = 2 SECONDS
/// List of all pictures taken by this camera.
var/list/datum/picture/stored = list()
@@ -15,7 +16,7 @@
if(!can_take_picture(clicker))
return
clicker.face_atom(clicked_on)
- INVOKE_ASYNC(src, PROC_REF(captureimage), clicked_on, clicker, picture_size_x - 1, picture_size_y - 1)
+ attempt_picture(clicked_on, clicker)
toggle_camera_mode(clicker, sound = FALSE)
/// Toggles the camera mode on or off.
@@ -56,7 +57,10 @@
/obj/item/camera/siliconcam/proc/viewpictures(mob/user)
var/datum/picture/selection = selectpicture(user)
if(istype(selection))
- show_picture(user, selection)
+ var/obj/item/photo/P = new(src, selection)
+ P.show(user)
+ to_chat(user, P.desc)
+ qdel(P)
/obj/item/camera/siliconcam/ai_camera
name = "AI photo camera"
diff --git a/code/modules/photography/photos/photo.dm b/code/modules/photography/photos/photo.dm
index 44dabf0f5d90..a6009b63528e 100644
--- a/code/modules/photography/photos/photo.dm
+++ b/code/modules/photography/photos/photo.dm
@@ -116,7 +116,6 @@
/obj/item/photo/verb/rename()
set name = "Rename photo"
- set category = "Object"
set src in usr
var/n_name = tgui_input_text(usr, "What would you like to label the photo?", "Photo Labelling", max_length = MAX_NAME_LEN)
diff --git a/code/modules/plumbing/plumbers/_plumb_machinery.dm b/code/modules/plumbing/plumbers/_plumb_machinery.dm
index 52e759ef816c..fe4ebb728d38 100644
--- a/code/modules/plumbing/plumbers/_plumb_machinery.dm
+++ b/code/modules/plumbing/plumbers/_plumb_machinery.dm
@@ -40,7 +40,7 @@
return
if(held_item.tool_behaviour == TOOL_WRENCH)
- context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Un" : ""]Anchor"
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Unan" : "An"]chor"
return CONTEXTUAL_SCREENTIP_SET
else if(held_item.tool_behaviour == TOOL_WELDER && !anchored)
context[SCREENTIP_CONTEXT_LMB] = "Deconstruct"
diff --git a/code/modules/plumbing/plumbers/_plumb_reagents.dm b/code/modules/plumbing/plumbers/_plumb_reagents.dm
index 42d6fe1bd35c..33378b1386bf 100644
--- a/code/modules/plumbing/plumbers/_plumb_reagents.dm
+++ b/code/modules/plumbing/plumbers/_plumb_reagents.dm
@@ -72,7 +72,6 @@
if(!isnull(target_id))
if(reagent.type == target_id)
- force_stop_reagent_reacting(reagent)
transfer_amount = min(amount, reagent.volume)
else
continue
@@ -212,7 +211,6 @@
if(!isnull(target_id))
if(reagent.type == target_id)
- force_stop_reagent_reacting(reagent)
transfer_amount = min(amount, working_volume)
else
continue
diff --git a/code/modules/point/point.dm b/code/modules/point/point.dm
index e3ef14d2db9e..e07086c87530 100644
--- a/code/modules/point/point.dm
+++ b/code/modules/point/point.dm
@@ -99,13 +99,11 @@
*
* overridden here and in /mob/dead/observer for different point span classes and sanity checks
*/
-/mob/verb/pointed(atom/A as mob|obj|turf in view())
+/mob/verb/pointed(atom/A as mob|obj|turf in view(client.view, src))
set name = "Point To"
- set category = "Object"
- if(istype(A, /obj/effect/temp_visual/point))
+ if(isnull(A) || istype(A, /obj/effect/temp_visual/point) || isarea(A))
return FALSE
-
DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(_pointed), A))
/// possibly delayed verb that finishes the pointing process starting in [/mob/verb/pointed()].
diff --git a/code/modules/power/gravitygenerator.dm b/code/modules/power/gravitygenerator.dm
index e30b99d87002..ccff4b0bdbd1 100644
--- a/code/modules/power/gravitygenerator.dm
+++ b/code/modules/power/gravitygenerator.dm
@@ -482,6 +482,14 @@ GLOBAL_LIST_EMPTY(gravity_generators)
params = NONE
return ..()
+/// Admin proc that causes gravity to fully restart, via the secrets panel's fix gravity.
+/obj/machinery/gravity_generator/main/proc/kickstart()
+ charge_count = 100
+ breaker = TRUE
+ set_power()
+ enable()
+ investigate_log("was turned re-enabled by admin event.", INVESTIGATE_GRAVITY)
+
// Misc
/// Gravity generator instruction guide
diff --git a/code/modules/projectiles/boxes_magazines/external/shotgun.dm b/code/modules/projectiles/boxes_magazines/external/shotgun.dm
index df34c7d9ec51..bcf06f9d106c 100644
--- a/code/modules/projectiles/boxes_magazines/external/shotgun.dm
+++ b/code/modules/projectiles/boxes_magazines/external/shotgun.dm
@@ -10,7 +10,7 @@
/obj/item/ammo_box/magazine/m12g/update_icon_state()
. = ..()
- icon_state = "[base_icon_state]-[CEILING(ammo_count(FALSE)/8, 1)*8]"
+ icon_state = "[base_icon_state]-[ceil(ammo_count(FALSE)/8)*8]"
/obj/item/ammo_box/magazine/m12g/stun
name = "shotgun magazine (12g taser slugs)"
diff --git a/code/modules/projectiles/guns/ballistic/pistol.dm b/code/modules/projectiles/guns/ballistic/pistol.dm
index 9a16feceebb0..c28ed3c012dd 100644
--- a/code/modules/projectiles/guns/ballistic/pistol.dm
+++ b/code/modules/projectiles/guns/ballistic/pistol.dm
@@ -189,7 +189,7 @@
accepted_magazine_type = /obj/item/ammo_box/magazine/r10mm
actions_types = list(/datum/action/item_action/toggle_firemode)
obj_flags = UNIQUE_RENAME // if you did the sidequest, you get the customization
- custom_materials = list(/datum/material/gold = SHEET_MATERIAL_AMOUNT * 30, /datum/material/silver = SHEET_MATERIAL_AMOUNT * 25, /datum/material/iron = SHEET_MATERIAL_AMOUNT * 11.5)
+ custom_materials = list(/datum/material/gold = SHEET_MATERIAL_AMOUNT * 30, /datum/material/silver = SHEET_MATERIAL_AMOUNT * 25, /datum/material/iron = SHEET_MATERIAL_AMOUNT * 11.5, /datum/material/telecrystal = SHEET_MATERIAL_AMOUNT * 4)
/obj/item/gun/ballistic/automatic/pistol/aps
name = "\improper Stechkin APS machine pistol"
@@ -268,7 +268,7 @@
unload_ammo(user, forced = TRUE)
return FALSE
-/obj/item/gun/ballistic/automatic/pistol/doorhickey/fire_gun(atom/target, mob/living/user, flag, params)
+/obj/item/gun/ballistic/automatic/pistol/doorhickey/process_fire(atom/target, mob/living/user, message, params, zone_override, bonus_spread)
var/dmg_multiplier = 1
if (get_dist(target, user) <= 1)
diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm
index 444d726dac9d..d33797802619 100644
--- a/code/modules/projectiles/guns/ballistic/revolver.dm
+++ b/code/modules/projectiles/guns/ballistic/revolver.dm
@@ -59,9 +59,6 @@
/obj/item/gun/ballistic/revolver/verb/spin()
set name = "Spin Chamber"
- set category = "Object"
- set desc = "Click to spin your revolver's chamber."
-
var/mob/user = usr
if(user.stat || !in_range(user, src))
diff --git a/code/modules/projectiles/guns/magic/wand.dm b/code/modules/projectiles/guns/magic/wand.dm
index 0d3618cb8562..43efeda1e4fa 100644
--- a/code/modules/projectiles/guns/magic/wand.dm
+++ b/code/modules/projectiles/guns/magic/wand.dm
@@ -19,9 +19,9 @@
return ..()
if(prob(33)) // 33% of the remaining 75% so another 25%
- max_charges = CEILING(max_charges / 3, 1)
+ max_charges = ceil(max_charges / 3)
else
- max_charges = CEILING(max_charges / 2, 1)
+ max_charges = ceil(max_charges / 2)
return ..()
/obj/item/gun/magic/wand/examine(mob/user)
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index 654c6e3c0762..3c695a10b738 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -817,7 +817,7 @@
START_PROCESSING(SSprojectiles, src)
// move it now to avoid potentially hitting yourself with firer-hitting projectiles
if (!deletion_queued && !hitscan)
- process_movement(max(FLOOR(speed, 1), 1), tile_limit = TRUE)
+ process_movement(max(floor(speed)), tile_limit = TRUE)
/// Makes projectile home onto the passed target with minor inaccuracy
/obj/projectile/proc/set_homing_target(atom/target)
@@ -907,7 +907,7 @@
pixels_to_move = SSprojectiles.max_pixels_per_tick
overrun += MODULUS(pixels_to_move, 1)
- pixels_to_move = FLOOR(pixels_to_move, 1)
+ pixels_to_move = floor(pixels_to_move)
SEND_SIGNAL(src, COMSIG_PROJECTILE_BEFORE_MOVE)
// Registering turf entries is done here instead of a connect_loc because else it could be called multiple times per tick and waste performance
@@ -978,8 +978,8 @@
distance_to_move = SSprojectiles.pixels_per_decisecond
// Figure out if we move to the next turf and if so, what its positioning relatively to us is
- var/x_shift = distance_to_move >= x_to_border ? SIGN(movement_vector.pixel_x) : 0
- var/y_shift = distance_to_move >= y_to_border ? SIGN(movement_vector.pixel_y) : 0
+ var/x_shift = distance_to_move >= x_to_border ? sign(movement_vector.pixel_x) : 0
+ var/y_shift = distance_to_move >= y_to_border ? sign(movement_vector.pixel_y) : 0
var/moving_turfs = x_shift || y_shift
// Calculate where in the turf we will be when we cross the edge.
// This is a projectile variable because its also used in hit VFX
diff --git a/code/modules/reagents/chemistry/equilibrium.dm b/code/modules/reagents/chemistry/equilibrium.dm
index e1e9be5a2732..c61521f47da6 100644
--- a/code/modules/reagents/chemistry/equilibrium.dm
+++ b/code/modules/reagents/chemistry/equilibrium.dm
@@ -62,16 +62,10 @@
if(!check_inital_conditions()) //If we're outside of the scope of the reaction vars
to_delete = TRUE
return
- if(!length(reaction.results)) //Come back to and revise the affected reactions in the next PR, this is a placeholder fix.
- holder.instant_react(reaction) //Even if this check fails, there's a backup - look inside of calculate_yield()
- to_delete = TRUE
- return
LAZYADD(holder.reaction_list, src)
SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] attempts")
/datum/equilibrium/Destroy()
- if(reacted_vol < target_vol) //We did NOT finish from reagents - so we can restart this reaction given property changes in the beaker. (i.e. if it stops due to low temp, this will allow it to fast restart when heated up again)
- LAZYADD(holder.failed_but_capable_reactions, reaction) //Consider replacing check with calculate_yield()
LAZYREMOVE(holder.reaction_list, src)
holder = null
reaction = null
@@ -374,7 +368,7 @@
#ifdef REAGENTS_TESTING //Kept in so that people who want to write fermireactions can contact me with this log so I can help them
debug_admins(span_green("Reaction step active for:[reaction.type]"))
- debug_admins(span_notice("|Reaction conditions| Temp: [holder.chem_temp], pH: [holder.ph], reactions: [length(holder.reaction_list)], awaiting reactions: [length(holder.failed_but_capable_reactions)], no. reagents:[length(holder.reagent_list)], no. prev reagents: [length(holder.previous_reagent_list)]"))
+ debug_admins(span_notice("|Reaction conditions| Temp: [holder.chem_temp], pH: [holder.ph], reactions: [length(holder.reaction_list)], no. reagents:[length(holder.reagent_list)]"))
debug_admins(span_warning("Reaction vars: PreReacted:[reacted_vol] of [step_target_vol] of total [target_vol]. delta_t [delta_t], multiplier [multiplier], delta_chem_factor [delta_chem_factor] Pfactor [product_ratio], purity of [purity] from a delta_ph of [delta_ph]. DeltaTime: [seconds_per_tick]"))
#endif
diff --git a/code/modules/reagents/chemistry/fermi_readme.md b/code/modules/reagents/chemistry/fermi_readme.md
index cfde8c37126c..5c54cdb7bf71 100644
--- a/code/modules/reagents/chemistry/fermi_readme.md
+++ b/code/modules/reagents/chemistry/fermi_readme.md
@@ -67,7 +67,7 @@ Lets go over the reaction vars below. These can be edited and set on a per chemi
var/H_ion_release = 0.01 // pH change per 1u reaction
var/rate_up_lim = 20 // Optimal/max rate possible if all conditions are perfect
var/purity_min = 0.15 // If purity is below 0.15, it calls OverlyImpure() too. Set to 0 to disable this.
- var/reaction_flags // bitflags for clear conversions; REACTION_CLEAR_IMPURE, REACTION_CLEAR_INVERSE, REACTION_CLEAR_RETAIN, REACTION_INSTANT
+ var/reaction_flags // bitflags for clear conversions;
```
### How temperature ranges are set and how reaction rate is determined
@@ -129,9 +129,7 @@ The thermic_constant is how much the temperature changes per u created, so for 1
Reaction_flags can be used to set these defines:
```dm
-#define REACTION_CLEAR_IMPURE //Convert into impure/pure on reaction completion in the datum/reagents holder instead of on consumption
#define REACTION_CLEAR_INVERSE //Convert into inverse on reaction completion when purity is low enough in the datum/reagents holder instead of on consumption
-#define REACTION_CLEAR_RETAIN //Clear converted chems retain their purities/inverted purities. Requires 1 or both of the above. This is so that it can split again after splitting from a reaction (i.e. if your impure_chem or inverse_chem has its own impure_chem/inverse_chem and you want it to split again on consumption).
#define REACTION_INSTANT //Used to create instant reactions
/datum/chemical_reaction
@@ -176,10 +174,9 @@ The new vars that are introduced are below:
- `pH` is the innate pH of the reagent and is used to calculate the pH of a reagents datum on addition/removal. This does not change and is a reference value. The reagents datum pH changes.
- `purity` is the INTERNAL value for splitting. This is set to 1 after splitting so that it doesn't infinite split
- `creation_purity` is the purity of the reagent on creation. This won't change. If you want to write code that checks the purity in any of the methods, use this.
-- `impure_chem` is the datum type that is created provided that its `creation_purity` is above the `inverse_chem_val`. When the reagent is consumed it will split into this OR if the associated `datum/chemical_recipe` has a REACTION_CLEAR_IMPURE flag it will split at the end of the reaction in the `datum/reagents` holder
+- `impure_chem` is the datum type that is created provided that its `creation_purity` is above the `inverse_chem_val`. When the reagent is consumed it will split into this OR if the associated `datum/chemical_recipe` has a REACTION_CLEAR_INVERSE flag it will split at the end of the reaction in the `datum/reagents` holder
- `inverse_chem_val` if a reagent's purity is below this value it will 100% convert into `inverse_chem`. If above it will split into `impure_chem`. See the note on purity effects above
- `inverse_chem` is the datum type that is created provided that its `creation_purity` is below the `inverse_chem_val`. When the reagent is consumed it will 100% convert into this OR if the associated `datum/chemical_recipe` has a REACTION_CLEAR_INVERSE flag it will 100% convert at the end of the reaction in the `datum/reagents` holder
-- `failed_chem` is the chem that the product is 100% converted into if the purity is below the associated `datum/chemical_recipies`' `PurityMin` AT THE END OF A REACTION.
When writing any reagent code ALWAYS use creation_purity. Purity is kept for internal mechanics only and won’t reflect the purity on creation.
@@ -209,8 +206,6 @@ There are a few variables that are useful to know about
var/chem_temp = 150
///pH of the whole system
var/ph = CHEMICAL_NORMAL_PH //aka 7
- ///cached list of reagents
- var/list/datum/reagent/previous_reagent_list = new/list()
///Hard check to see if the reagents is presently reacting
var/is_reacting = FALSE
```
@@ -218,4 +213,3 @@ There are a few variables that are useful to know about
- chem_temp is the temperature used in the `datum/chemical_recipe`
- pH is a result of the sum of all reagents, as well as any changes from buffers and reactions. This is the pH used in `datum/chemical_recipe`.
- isReacting is a bool that can be used outside to ensure that you don't touch a reagents that is reacting.
-- previous_reagent_list is a list of the previous reagents (just the typepaths, not the objects) that was present on the last handle_reactions() method. This is to prevent pointless method calls.
diff --git a/code/modules/reagents/chemistry/holder/holder.dm b/code/modules/reagents/chemistry/holder/holder.dm
index 7c9efea9c1ba..7207801b22a3 100644
--- a/code/modules/reagents/chemistry/holder/holder.dm
+++ b/code/modules/reagents/chemistry/holder/holder.dm
@@ -18,10 +18,6 @@
var/flags
///list of reactions currently on going, this is a lazylist for optimisation
var/list/datum/equilibrium/reaction_list
- ///cached list of reagents typepaths (not object references), this is a lazylist for optimisation
- var/list/datum/reagent/previous_reagent_list
- ///If a reaction fails due to temperature or pH, this tracks the required temperature or pH for it to be enabled.
- var/list/failed_but_capable_reactions
///Hard check to see if the reagents is presently reacting
var/is_reacting = FALSE
///UI lookup stuff
@@ -48,7 +44,6 @@
if(is_reacting) //If false, reaction list should be cleaned up
force_stop_reacting()
QDEL_LAZYLIST(reaction_list)
- previous_reagent_list = null
if(my_atom && my_atom.reagents == src)
my_atom.reagents = null
my_atom = null
@@ -491,7 +486,6 @@
if(!isnull(target_id))
if(reagent.type == target_id)
- force_stop_reagent_reacting(reagent)
transfer_amount = min(amount, reagent.volume)
else
continue
@@ -611,7 +605,6 @@
//removing it and store in a seperate list for processing later
cached_reagents -= reagent
- LAZYREMOVE(previous_reagent_list, reagent.type)
deleted_reagents += reagent
//move pointer back so we don't overflow & decrease length
diff --git a/code/modules/reagents/chemistry/holder/reactions.dm b/code/modules/reagents/chemistry/holder/reactions.dm
index 1eb15b4316cd..142c4d1e9bfb 100644
--- a/code/modules/reagents/chemistry/holder/reactions.dm
+++ b/code/modules/reagents/chemistry/holder/reactions.dm
@@ -1,7 +1,6 @@
/**
* Handle any reactions possible in this holder
* Also UPDATES the reaction list
- * High potential for infinite loopsa if you're editing this.
*/
/datum/reagents/proc/handle_reactions()
if(QDELING(src))
@@ -15,12 +14,6 @@
force_stop_reacting() //Force anything that is trying to to stop
return FALSE //Yup, no reactions here. No siree.
- if(is_reacting)//Prevent wasteful calculations
- if(!(datum_flags & DF_ISPROCESSING))//If we're reacting - but not processing (i.e. we've transferred)
- START_PROCESSING(SSreagents, src)
- if(!(has_changed_state()))
- return FALSE
-
#ifndef UNIT_TESTS
// We assert that reagents will not need to react before the map is fully loaded
// This is the best I can do, sorry :(
@@ -28,91 +21,82 @@
return FALSE
#endif
- var/list/cached_reagents = reagent_list
+ var/list/cached_reagents = list()
+ for(var/datum/reagent/target as anything in reagent_list)
+ cached_reagents[target.type] = target.volume
var/list/cached_reactions = GLOB.chemical_reactions_list_reactant_index
var/datum/cached_my_atom = my_atom
- LAZYNULL(failed_but_capable_reactions)
- LAZYNULL(previous_reagent_list)
. = 0
- var/list/possible_reactions = list()
for(var/datum/reagent/reagent as anything in cached_reagents)
- LAZYADD(previous_reagent_list, reagent.type)
- // I am SO sorry
- reaction_loop:
- for(var/datum/chemical_reaction/reaction as anything in cached_reactions[reagent.type]) // Was a big list but now it should be smaller since we filtered it with our reagent id
- if(!reaction)
- continue
-
- if(!reaction.required_reagents)//Don't bring in empty ones
- continue
-
- var/granularity = 1
- if(!(reaction.reaction_flags & REACTION_INSTANT))
- granularity = CHEMICAL_QUANTISATION_LEVEL
-
- var/list/cached_required_reagents = reaction.required_reagents
- for(var/req_reagent in cached_required_reagents)
- if(!has_reagent(req_reagent, (cached_required_reagents[req_reagent] * granularity)))
- continue reaction_loop
-
- var/list/cached_required_catalysts = reaction.required_catalysts
- for(var/_catalyst in cached_required_catalysts)
- if(!has_reagent(_catalyst, (cached_required_catalysts[_catalyst] * granularity)))
- continue reaction_loop
-
- if(cached_my_atom)
- if(reaction.required_container)
- if(reaction.required_container_accepts_subtypes)
- if(!istype(cached_my_atom, reaction.required_container))
- continue
- else if(cached_my_atom.type != reaction.required_container)
+ for(var/datum/chemical_reaction/reaction as anything in cached_reactions[reagent]) // Was a big list but now it should be smaller since we filtered it with our reagent id
+ //is this reaction already going on?
+ var/next_reaction = FALSE
+ for(var/datum/equilibrium/E_exist as anything in reaction_list)
+ if(E_exist.reaction == reaction) //Don't add duplicates
+ next_reaction = TRUE
+ break
+ if(next_reaction)
+ continue
+
+ //do we have the required reagents?
+ var/granularity = 1
+ if(!(reaction.reaction_flags & REACTION_INSTANT))
+ granularity = CHEMICAL_QUANTISATION_LEVEL
+ var/present_volume = 0
+ var/list/datum/reagent/requirements = reaction.required_reagents | reaction.required_catalysts
+ for(var/datum/reagent/requirement as anything in requirements)
+ present_volume = cached_reagents[requirement]
+ if(!present_volume)
+ next_reaction = TRUE
+ break
+
+ if(present_volume < requirements[requirement] * granularity)
+ next_reaction = TRUE
+ break
+ if(next_reaction)
+ continue
+
+ //do we have the required container?
+ if(cached_my_atom)
+ if(reaction.required_container)
+ if(reaction.required_container_accepts_subtypes)
+ if(!istype(cached_my_atom, reaction.required_container))
continue
-
- if(isliving(cached_my_atom) && !reaction.mob_react) //Makes it so certain chemical reactions don't occur in mobs
+ else if(cached_my_atom.type != reaction.required_container)
continue
-
- else if(reaction.required_container)
+ if(isliving(cached_my_atom) && !reaction.mob_react) //Makes it so certain chemical reactions don't occur in mobs
continue
-
- if(reaction.required_other && !reaction.pre_reaction_other_checks(src))
- continue
-
- // At this point, we've passed all the hard restrictions and entered into just the soft ones
- // So we're gonna start tracking reactions that COULD be completed on continue, instead of just exiting
- var/required_temp = reaction.required_temp
- var/is_cold_recipe = reaction.is_cold_recipe
- if(required_temp != 0 && (is_cold_recipe && chem_temp > required_temp) || (!is_cold_recipe && chem_temp < required_temp))
- LAZYADD(failed_but_capable_reactions, reaction)
- continue
-
- if(ph < reaction.optimal_ph_min - reaction.determin_ph_range && ph > reaction.optimal_ph_max + reaction.determin_ph_range)
- LAZYADD(failed_but_capable_reactions, reaction)
- continue
-
- possible_reactions += reaction
-
- //This is the point where we have all the possible reactions from a reagent/catalyst point of view, so we set up the reaction list
- for(var/datum/chemical_reaction/selected_reaction as anything in possible_reactions)
- if((selected_reaction.reaction_flags & REACTION_INSTANT) || (flags & REAGENT_HOLDER_INSTANT_REACT)) //If we have instant reactions, we process them here
- instant_react(selected_reaction)
- .++
- else
- var/exists = FALSE
- for(var/datum/equilibrium/E_exist as anything in reaction_list)
- if(ispath(E_exist.reaction.type, selected_reaction.type)) //Don't add duplicates
- exists = TRUE
-
- //Add it if it doesn't exist in the list
- if(!exists)
- is_reacting = TRUE//Prevent any on_reaction() procs from infinite looping
- var/datum/equilibrium/equilibrium = new (selected_reaction, src) //Otherwise we add them to the processing list.
+ else if(reaction.required_container)
+ continue
+
+ //do we have the required temps?
+ var/required_temp = reaction.required_temp
+ var/is_cold_recipe = reaction.is_cold_recipe
+ if(required_temp != 0 && (is_cold_recipe && chem_temp > required_temp) || (!is_cold_recipe && chem_temp < required_temp))
+ continue
+
+ //do we have the required ph? in range of min - ph_range & max + ph_range
+ if(ph < reaction.optimal_ph_min - reaction.determin_ph_range && ph > reaction.optimal_ph_max + reaction.determin_ph_range)
+ continue
+
+ //user defined checks
+ if(!reaction.pre_reaction_other_checks(src))
+ continue
+
+ //do the actual reactions
+ if((reaction.reaction_flags & REACTION_INSTANT) || (flags & REAGENT_HOLDER_INSTANT_REACT) || !length(reaction.results)) //If we have instant reactions, we process them here
+ instant_react(reaction)
+ .++
+ else
+ var/datum/equilibrium/equilibrium = new (reaction, src) //Otherwise we add them to the processing list.
if(equilibrium.to_delete)//failed startup checks
qdel(equilibrium)
else
//Adding is done in new(), deletion is in qdel
- equilibrium.reaction.on_reaction(src, equilibrium, equilibrium.multiplier)
- equilibrium.react_timestep(1)//Get an initial step going so there's not a delay between setup and start - DO NOT ADD THIS TO equilibrium.NEW()
+ is_reacting = TRUE//Prevent any on_reaction() procs from infinite looping
+ reaction.on_reaction(src, equilibrium, equilibrium.multiplier)
+ equilibrium.react_timestep(1)//Get an initial
if(LAZYLEN(reaction_list))
is_reacting = TRUE //We've entered the reaction phase - this is set here so any reagent handling called in on_reaction() doesn't cause infinite loops
@@ -122,32 +106,6 @@
TEST_ONLY_ASSERT(!. || MC_RUNNING(), "We reacted during subsystem init, that shouldn't be happening!")
-/**
- * Checks to see if the reagents has a difference in reagents_list and previous_reagent_list (I.e. if there's a difference between the previous call and the last)
- * Also checks to see if the saved reactions in failed_but_capable_reactions can start as a result of temp/pH change
-*/
-/datum/reagents/proc/has_changed_state()
- //Check if reagents are different
- var/total_matching_reagents = 0
- for(var/reagent in previous_reagent_list)
- if(has_reagent(reagent))
- total_matching_reagents++
- if(total_matching_reagents != reagent_list.len)
- return TRUE
-
- //Check our last reactions
- for(var/datum/chemical_reaction/reaction as anything in failed_but_capable_reactions)
- if(reaction.is_cold_recipe)
- if(reaction.required_temp < chem_temp)
- return TRUE
- else
- if(reaction.required_temp < chem_temp)
- return TRUE
- if(((ph >= (reaction.optimal_ph_min - reaction.determin_ph_range)) && (ph <= (reaction.optimal_ph_max + reaction.determin_ph_range))))
- return TRUE
- return FALSE
-
-
/*
* Main Reaction loop handler, Do not call this directly
*
@@ -197,6 +155,8 @@
* * mix_message - the associated mix message of a reaction
*/
/datum/reagents/proc/end_reaction(datum/equilibrium/equilibrium)
+ PRIVATE_PROC(TRUE)
+
equilibrium.reaction.reaction_finish(src, equilibrium, equilibrium.reacted_vol)
if(!equilibrium.holder || !equilibrium.reaction) //Somehow I'm getting empty equilibrium. This is here to handle them
LAZYREMOVE(reaction_list, equilibrium)
@@ -223,9 +183,10 @@
* Also resets reaction variables to be null/empty/FALSE so that it can restart correctly in the future
*/
/datum/reagents/proc/finish_reacting()
+ PRIVATE_PROC(TRUE)
+
STOP_PROCESSING(SSreagents, src)
is_reacting = FALSE
- LAZYNULL(previous_reagent_list) //reset it to 0 - because any change will be different now.
update_total()
/*
@@ -241,25 +202,6 @@
my_atom.audible_message(span_notice("[icon2html(my_atom, viewers(DEFAULT_MESSAGE_RANGE, src))] [mix_message.Join()]"))
finish_reacting()
-/*
-* Force stops a specific reagent's associated reaction if it exists
-*
-* Returns TRUE if it stopped something, FALSE if it didn't
-* Arguments:
-* * reagent - the reagent PRODUCT that we're seeking reactions for, any and all found will be shut down
-*/
-/datum/reagents/proc/force_stop_reagent_reacting(datum/reagent/reagent)
- var/any_stopped = FALSE
- var/list/mix_message = list()
- for(var/datum/equilibrium/equilibrium as anything in reaction_list)
- for(var/result in equilibrium.reaction.results)
- if(result == reagent.type)
- mix_message += end_reaction(equilibrium)
- any_stopped = TRUE
- if(length(mix_message) && !HAS_TRAIT(my_atom, TRAIT_SILENT_REACTIONS))
- my_atom.audible_message(span_notice("[icon2html(my_atom, viewers(DEFAULT_MESSAGE_RANGE, src))][mix_message.Join()]"))
- return any_stopped
-
/**
* Old reaction mechanics, edited to work on one only
* This is changed from the old - purity of the reagents will affect yield
@@ -268,6 +210,8 @@
* * [selected_reaction][datum/chemical_reaction] - the chemical reaction to finish instantly
*/
/datum/reagents/proc/instant_react(datum/chemical_reaction/selected_reaction)
+ PRIVATE_PROC(TRUE)
+
var/list/cached_required_reagents = selected_reaction.required_reagents
var/list/cached_results = selected_reaction.results
var/datum/cached_my_atom = my_atom
diff --git a/code/modules/reagents/chemistry/holder/ui_data.dm b/code/modules/reagents/chemistry/holder/ui_data.dm
index 74f77238d405..3a91d4b7ee00 100644
--- a/code/modules/reagents/chemistry/holder/ui_data.dm
+++ b/code/modules/reagents/chemistry/holder/ui_data.dm
@@ -115,7 +115,10 @@
/datum/reagents/ui_data(mob/user)
var/data = list()
data["selectedBitflags"] = ui_tags_selected
- data["currentReagents"] = previous_reagent_list //This keeps the string of reagents that's updated when handle_reactions() is called
+
+ data["currentReagents"] = list()
+ for(var/datum/reagent/target as anything in reagent_list)
+ data["currentReagents"] += target.type
data["beakerSync"] = ui_beaker_sync
data["linkedBeaker"] = my_atom.name //To solidify the fact that the UI is linked to a beaker - not a machine.
diff --git a/code/modules/reagents/chemistry/machinery/chem_mass_spec.dm b/code/modules/reagents/chemistry/machinery/chem_mass_spec.dm
index c7c4523cff4c..517fef87122d 100644
--- a/code/modules/reagents/chemistry/machinery/chem_mass_spec.dm
+++ b/code/modules/reagents/chemistry/machinery/chem_mass_spec.dm
@@ -75,7 +75,7 @@
return CONTEXTUAL_SCREENTIP_SET
if(held_item.tool_behaviour == TOOL_WRENCH)
- context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Un" : ""]anchor"
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Unan" : "An"]chor"
return CONTEXTUAL_SCREENTIP_SET
else if(held_item.tool_behaviour == TOOL_SCREWDRIVER)
context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel"
diff --git a/code/modules/reagents/chemistry/machinery/chem_master.dm b/code/modules/reagents/chemistry/machinery/chem_master.dm
index fe120a8d2d37..479be15b4a96 100644
--- a/code/modules/reagents/chemistry/machinery/chem_master.dm
+++ b/code/modules/reagents/chemistry/machinery/chem_master.dm
@@ -72,7 +72,7 @@
context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel"
return CONTEXTUAL_SCREENTIP_SET
else if(held_item.tool_behaviour == TOOL_WRENCH)
- context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Un" : ""] anchor"
+ context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Unan" : "An"]chor"
return CONTEXTUAL_SCREENTIP_SET
else if(panel_open && held_item.tool_behaviour == TOOL_CROWBAR)
context[SCREENTIP_CONTEXT_LMB] = "Deconstruct"
diff --git a/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm b/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm
index f4b44d5e8ce3..b7b8c6545f48 100644
--- a/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm
+++ b/code/modules/reagents/chemistry/machinery/chem_recipe_debug.dm
@@ -146,11 +146,12 @@
PRIVATE_PROC(TRUE)
SHOULD_BE_PURE(TRUE)
+ . = null
if(temp_mode == USE_REACTION_TEMPERATURE)
- return null //simply means don't alter the reaction temperature
+ return . //simply means don't alter the reaction temperature
else if(temp_mode == USE_USER_TEMPERATURE)
return forced_temp
- else
+ else if(reactions_to_test.len)
var/datum/chemical_reaction/test_reaction = reactions_to_test[current_reaction_index || 1]
switch(temp_mode)
if(USE_MINIMUM_TEMPERATURE)
diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm
index bca5b24a61e6..b10f040c8f07 100644
--- a/code/modules/reagents/chemistry/reagents/food_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm
@@ -468,11 +468,13 @@
victim.emote("scream")
victim.emote("cry")
victim.set_eye_blur_if_lower(10 SECONDS)
- victim.adjust_temp_blindness(6 SECONDS)
+ victim.set_temp_blindness_if_lower(6 SECONDS)
victim.set_confusion_if_lower(5 SECONDS)
victim.Knockdown(3 SECONDS)
victim.add_movespeed_modifier(/datum/movespeed_modifier/reagent/pepperspray)
addtimer(CALLBACK(victim, TYPE_PROC_REF(/mob, remove_movespeed_modifier), /datum/movespeed_modifier/reagent/pepperspray), 10 SECONDS)
+ ADD_TRAIT(victim, TRAIT_ANOSMIA, type)
+ addtimer(TRAIT_CALLBACK_REMOVE(victim, TRAIT_ANOSMIA, type), 30 SECONDS, TIMER_OVERRIDE|TIMER_UNIQUE)
victim.update_damage_hud()
if(methods & INGEST)
if(!holder.has_reagent(/datum/reagent/consumable/milk))
diff --git a/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm
index 72cc93916cce..c536445a37e2 100644
--- a/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm
@@ -86,8 +86,8 @@ Basically, we fill the time between now and 2s from now with hands based off the
/datum/reagent/inverse/helgrasp/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, metabolization_ratio)
. = ..()
spawn_hands(affected_mob)
- lag_remainder += seconds_per_tick - FLOOR(seconds_per_tick, 1)
- seconds_per_tick = FLOOR(seconds_per_tick, 1)
+ lag_remainder += seconds_per_tick - floor(seconds_per_tick)
+ seconds_per_tick = floor(seconds_per_tick)
if(lag_remainder >= 1)
seconds_per_tick += 1
lag_remainder -= 1
diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
index 927b2a714638..7a888f9f9517 100644
--- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
@@ -1260,6 +1260,7 @@
inverse_chem_val = 0.5
inverse_chem = /datum/reagent/inverse/neurine
added_traits = list(TRAIT_ANTICONVULSANT)
+ self_consuming = TRUE
///brain damage level when we first started taking the chem
var/initial_bdamage = 200
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index 1105c0a611cd..78f39dc9ee53 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -3023,6 +3023,12 @@
target.set_custom_materials()
var/list/metal_dat = list((metal_ref) = metal_amount)
target.material_flags = applied_material_flags
+ if (target.has_material_slots())
+ var/list/new_slots = target.get_material_slots()
+ for (var/slot_type in new_slots)
+ new_slots[slot_type] = metal_ref
+ // Safe to call and doesn't do anything as no materials are currently present on the target
+ target.set_material_slots(new_slots)
target.set_custom_materials(metal_dat)
/datum/reagent/gravitum
diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
index 41cba75b8bce..9b059a93ea4d 100644
--- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
@@ -349,7 +349,7 @@
need_mob_update = affected_mob.adjust_stamina_loss(20 * metabolization_ratio * seconds_per_tick, updating_stamina = FALSE)
if(10 to INFINITY)
if(affected_mob.stat != DEAD)
- affected_mob.fakedeath(type)
+ affected_mob.apply_status_effect(/datum/status_effect/reagent_effect/fakedeath, type)
if(need_mob_update)
return UPDATE_MOB_HEALTH
diff --git a/code/modules/reagents/chemistry/recipes.dm b/code/modules/reagents/chemistry/recipes.dm
index 40cb1f1ee20a..e48eb60025fd 100644
--- a/code/modules/reagents/chemistry/recipes.dm
+++ b/code/modules/reagents/chemistry/recipes.dm
@@ -7,18 +7,16 @@
*/
/datum/chemical_reaction
///Results of the chemical reactions
- var/list/results = new/list()
+ var/list/results = list()
///Required chemicals that are USED in the reaction
- var/list/required_reagents = new/list()
+ var/list/required_reagents = list()
///Required chemicals that must be present in the container but are not USED.
- var/list/required_catalysts = new/list()
+ var/list/required_catalysts = list()
/// If required_container will check for the exact type, or will also accept subtypes
var/required_container_accepts_subtypes = FALSE
/// If required_container_accepts_subtypes is FALSE, the exact type of what container this reaction can take place in. Otherwise, what type including subtypes are acceptable.
var/atom/required_container
- /// Set this to true to call pre_reaction_other_checks() on react and do some more interesting reaction logic
- var/required_other = FALSE
///Determines if a chemical reaction can occur inside a mob
var/mob_react = TRUE
@@ -54,7 +52,7 @@
var/rate_up_lim = 30
/// If purity is below 0.15, it calls OverlyImpure() too. Set to 0 to disable this.
var/purity_min = 0.15
- /// bitflags for clear conversions; REACTION_CLEAR_IMPURE, REACTION_CLEAR_INVERSE, REACTION_CLEAR_RETAIN, REACTION_INSTANT
+ /// bitflags for clear conversions; see code/__DEFINES/reagents.dm
var/reaction_flags = NONE
///Tagging vars
///A bitflag var for tagging reagents for the reagent loopup functon
@@ -104,11 +102,7 @@
return
/**
- * Stuff that occurs at the end of a reaction. This will proc if the beaker is forced to stop and start again (say for sudden temperature changes).
- * Only procs at the END of reaction
- * If reaction_flags & REACTION_INSTANT then this isn't called
- * if reaction_flags REACTION_CLEAR_IMPURE then the impurity chem is handled here, producing the result in the beaker instead of in a mob
- * Likewise for REACTION_CLEAR_INVERSE the inverse chem is produced at the end of the reaction in the beaker
+ * Stuff that should happen at the end of the reaction. Presently only REACTION_CLEAR_INVERSE is respected here
* You should be calling ..() if you're writing a child function of this proc otherwise purity methods won't work correctly
*
* Proc where the additional magic happens.
@@ -118,39 +112,24 @@
* * react_volume - volume created across the whole reaction
*/
/datum/chemical_reaction/proc/reaction_finish(datum/reagents/holder, datum/equilibrium/reaction, react_vol)
- //failed_chem handler
- var/cached_temp = holder.chem_temp
- for(var/id in results)
- var/datum/reagent/reagent = holder.has_reagent(id)
- if(!reagent)
+ if(!(reaction_flags & REACTION_CLEAR_INVERSE))
+ return
+
+ var/list/inverse_data = list()
+ for(var/datum/reagent/reagent as anything in holder.reagent_list)
+ if(!results[reagent.type] || !reagent.inverse_chem || reagent.purity == 1 || reagent.purity > reagent.inverse_chem_val)
continue
- //Split like this so it's easier for people to edit this function in a child
- reaction_clear_check(reagent, holder)
- holder.chem_temp = cached_temp
-/**
- * REACTION_CLEAR handler
- * If the reaction has the REACTION_CLEAR flag, then it will split using purity methods in the beaker instead
- *
- * Arguments:
- * * reagent - the target reagent to convert
- */
-/datum/chemical_reaction/proc/reaction_clear_check(datum/reagent/reagent, datum/reagents/holder)
- if(!reagent)//Failures can delete R
- return
- if(reaction_flags & (REACTION_CLEAR_IMPURE | REACTION_CLEAR_INVERSE))
- if(reagent.purity == 1)
- return
+ inverse_data += reagent.inverse_chem
+ inverse_data += reagent.volume
+ inverse_data += reagent.get_inverse_purity(reagent.purity)
+ reagent.volume = 0
+
+ holder.update_total()
- if((reaction_flags & REACTION_CLEAR_INVERSE) && reagent.inverse_chem)
- if(reagent.inverse_chem_val > reagent.purity)
- var/inverse_chem = reagent.inverse_chem
- var/cached_volume = reagent.volume
- var/cached_purity = reagent.get_inverse_purity(reagent.purity)
- reagent.volume = 0
- holder.update_total()
- holder.add_reagent(inverse_chem, cached_volume, FALSE, added_purity = cached_purity)
- return
+ var/cached_temp = holder.chem_temp
+ while(inverse_data.len)
+ holder.add_reagent(popleft(inverse_data), popleft(inverse_data), reagtemp = cached_temp, added_purity = popleft(inverse_data))
/**
* Occurs when a reation is overheated (i.e. past its overheatTemp)
@@ -165,10 +144,9 @@
* * step_volume_added - how much product (across all products) was added for this single step
*/
/datum/chemical_reaction/proc/overheated(datum/reagents/holder, datum/equilibrium/equilibrium, step_volume_added)
- for(var/id in results)
- var/datum/reagent/reagent = holder.has_reagent(id)
- if(!reagent)
- return
+ for(var/datum/reagent/reagent as anything in holder.reagent_list)
+ if(!results[reagent.type])
+ continue
reagent.volume *= 0.98 //Slowly lower yield per tick
holder.update_total()
diff --git a/code/modules/reagents/chemistry/recipes/others.dm b/code/modules/reagents/chemistry/recipes/others.dm
index a7dc118f2c08..83a783534501 100644
--- a/code/modules/reagents/chemistry/recipes/others.dm
+++ b/code/modules/reagents/chemistry/recipes/others.dm
@@ -43,7 +43,6 @@
results = list(/datum/reagent/consumable/salt = 2)
required_reagents = list(/datum/reagent/sodium = 1, /datum/reagent/chlorine = 1) // That's what I said! Sodium Chloride!
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_FOOD | REACTION_TAG_COMPONENT
- required_other = TRUE
/datum/chemical_reaction/sodiumchloride/pre_reaction_other_checks(datum/reagents/holder)
. = ..()
diff --git a/code/modules/reagents/chemistry/recipes/slime_extracts.dm b/code/modules/reagents/chemistry/recipes/slime_extracts.dm
index 255edeccd54d..f342bcb7c93f 100644
--- a/code/modules/reagents/chemistry/recipes/slime_extracts.dm
+++ b/code/modules/reagents/chemistry/recipes/slime_extracts.dm
@@ -2,7 +2,6 @@
/datum/chemical_reaction/slime
reaction_flags = REACTION_INSTANT
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_SLIME
- required_other = TRUE
var/deletes_extract = TRUE
/datum/chemical_reaction/slime/pre_reaction_other_checks(datum/reagents/holder)
diff --git a/code/modules/reagents/chemistry/recipes/toxins.dm b/code/modules/reagents/chemistry/recipes/toxins.dm
index 4d920c84c7c3..519c172407cc 100644
--- a/code/modules/reagents/chemistry/recipes/toxins.dm
+++ b/code/modules/reagents/chemistry/recipes/toxins.dm
@@ -263,7 +263,7 @@
H_ion_release = -0.25
rate_up_lim = 15
purity_min = 0.3
- reaction_flags = REACTION_CLEAR_IMPURE
+ reaction_flags = REACTION_CLEAR_INVERSE
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_DAMAGING | REACTION_TAG_OTHER
/datum/chemical_reaction/ghoulpowder
@@ -303,7 +303,7 @@
H_ion_release = -0.06
rate_up_lim = 15
purity_min = 0.4
- reaction_flags = REACTION_CLEAR_IMPURE
+ reaction_flags = REACTION_CLEAR_INVERSE
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_DAMAGING | REACTION_TAG_OTHER
/datum/chemical_reaction/heparin
diff --git a/code/modules/reagents/chemistry/taste.dm b/code/modules/reagents/chemistry/taste.dm
index 06cee0bba2de..d81a0ef18516 100644
--- a/code/modules/reagents/chemistry/taste.dm
+++ b/code/modules/reagents/chemistry/taste.dm
@@ -1,41 +1,45 @@
#define TEXT_NO_TASTE "something indescribable"
-//================================TASTE===================================================
+//=============================== TASTE GRAPH ====================================
+// //
+// Flavor Intensity Thresholds (Relative to Detection Threshold % 'DT'): //
+// 0% 1x DT 2x DT 4x DT 100% //
+// |----------------|------------------|------------------|-----------------| //
+// | Undetected | Weak | Mild | Strong | //
+// |----------------|------------------|------------------|-----------------| //
+// //
+//==============================================================================//
+
/**
- * Returns what this reagents in our given list taste like
+ * Returns what the reagents in our given list taste like
*
* Arguments:
* * list/reagent_list - List of reagents to taste.
* * mob/living/taster - Who is doing the tasting. Some mobs can pick up specific flavours.
- * * minimum_percent - The lower the minimum percent, the more sensitive the message is.
- * * weight_modifier - Value to multiply each reagent's taste weight with.
+ * * detection_threshold_percent - The minimum relative percentage a flavor must reach to be tasted.
*/
-/proc/generate_reagents_taste_message(list/reagent_list, mob/living/taster, minimum_percent)
+/proc/generate_reagents_taste_message(list/reagent_list, mob/living/taster, detection_threshold_percent)
// We can't taste anything
- if(minimum_percent > 100)
+ if(detection_threshold_percent > 100)
return TEXT_NO_TASTE
- // Associative list of our tastes, descriptor - strength
+ // Associative list of our tastes - list("taste description" = strength)
var/list/tastes = list()
- // Total of our taste strengths so far
- var/total_taste = 0
+ var/total_taste_strength = 0
for(var/datum/reagent/reagent as anything in reagent_list)
if(!reagent.taste_mult)
continue
var/list/taste_data = reagent.get_taste_description(taster)
- for(var/taste in taste_data)
- var/taste_strength = taste_data[taste] * reagent.volume * reagent.taste_mult
- if(taste in tastes)
- tastes[taste] += taste_strength
- else
- tastes[taste] = taste_strength
- total_taste += taste_strength
+ for(var/taste_desc in taste_data)
+ var/taste_strength = taste_data[taste_desc] * reagent.volume * reagent.taste_mult
+ tastes[taste_desc] += taste_strength
+ total_taste_strength += taste_strength
// None of our reagents had any flavour
- if(total_taste <= 0)
+ if(total_taste_strength <= 0)
return TEXT_NO_TASTE
// If we have exactly one taste, don't bother with relative strengths
@@ -46,32 +50,33 @@
sortTim(tastes, cmp = GLOBAL_PROC_REF(cmp_numeric_dsc), associative = TRUE)
// Lazylists for different taste strength categories, no need to initialize if we don't have such flavors
- var/list/strong
- var/list/mild
- var/list/hint
+ var/list/strong_tastes
+ var/list/mild_tastes
+ var/list/weak_tastes
for(var/taste_desc in tastes)
- var/percent = tastes[taste_desc]/total_taste * 100
- if(percent < minimum_percent)
- continue
+ var/relative_taste_percent = (tastes[taste_desc] / total_taste_strength) * 100
- if(percent <= minimum_percent * 2)
- LAZYADD(hint, taste_desc)
- else if(percent > minimum_percent * 4)
- LAZYADD(strong, taste_desc)
+ // From weakest to strongest
+ if(relative_taste_percent < detection_threshold_percent)
+ continue // too weak to detect
+ else if(relative_taste_percent <= detection_threshold_percent * 2)
+ LAZYADD(weak_tastes, taste_desc)
+ else if(relative_taste_percent <= detection_threshold_percent * 4)
+ LAZYADD(mild_tastes, taste_desc)
else
- LAZYADD(mild, taste_desc)
+ LAZYADD(strong_tastes, taste_desc)
- var/list/out = list()
+ var/list/taste_messages = list()
- if(LAZYLEN(strong))
- out += "the strong flavor of [english_list(strong, TEXT_NO_TASTE)]"
- if(LAZYLEN(mild))
+ if(LAZYLEN(strong_tastes))
+ taste_messages += "the strong flavor of [english_list(strong_tastes, TEXT_NO_TASTE)]"
+ if(LAZYLEN(mild_tastes))
// Prefix "some " if there are strong flavors to avoid seeming like a strong flavor
- out += "[LAZYLEN(strong) ? "some " : ""][english_list(mild, TEXT_NO_TASTE)]"
- if(LAZYLEN(hint))
- out += "a hint of [english_list(hint, TEXT_NO_TASTE)]"
+ taste_messages += "[LAZYLEN(strong_tastes) ? "some " : ""][english_list(mild_tastes, TEXT_NO_TASTE)]"
+ if(LAZYLEN(weak_tastes))
+ taste_messages += "a hint of [english_list(weak_tastes, TEXT_NO_TASTE)]"
- return english_list(out, TEXT_NO_TASTE)
+ return english_list(taste_messages, TEXT_NO_TASTE)
#undef TEXT_NO_TASTE
diff --git a/code/modules/reagents/reagent_containers/spray.dm b/code/modules/reagents/reagent_containers/spray.dm
index e2a849e650eb..c7f332ea8171 100644
--- a/code/modules/reagents/reagent_containers/spray.dm
+++ b/code/modules/reagents/reagent_containers/spray.dm
@@ -139,7 +139,6 @@
/obj/item/reagent_containers/spray/verb/empty()
set name = "Empty Spray Bottle"
- set category = "Object"
set src in usr
if(usr.incapacitated)
return
diff --git a/code/modules/recycling/sortingmachinery.dm b/code/modules/recycling/sortingmachinery.dm
index 8a1b9abb9b89..997bc6540bc4 100644
--- a/code/modules/recycling/sortingmachinery.dm
+++ b/code/modules/recycling/sortingmachinery.dm
@@ -159,7 +159,7 @@
for(var/obj/wrapped_item in get_all_contents())
if(HAS_TRAIT(wrapped_item, TRAIT_NO_BARCODES))
continue
- wrapped_item.AddComponent(/datum/component/pricetag, sticker.payments_acc, sales_tagger.cut_multiplier)
+ wrapped_item.AddComponent(/datum/component/pricetag, list(sticker.payments_acc), sales_tagger.cut_multiplier)
update_appearance()
else if(istype(item, /obj/item/barcode))
@@ -177,7 +177,7 @@
for(var/obj/wrapped_item in get_all_contents())
if(HAS_TRAIT(wrapped_item, TRAIT_NO_BARCODES))
continue
- wrapped_item.AddComponent(/datum/component/pricetag, sticker.payments_acc, sticker.cut_multiplier)
+ wrapped_item.AddComponent(/datum/component/pricetag, list(sticker.payments_acc), sticker.cut_multiplier)
update_appearance()
else if(istype(item, /obj/item/boxcutter))
diff --git a/code/modules/research/designs.dm b/code/modules/research/designs.dm
index a6e3c1034170..bc1d5eaee0f7 100644
--- a/code/modules/research/designs.dm
+++ b/code/modules/research/designs.dm
@@ -70,7 +70,7 @@ other types of metals and chemistry for reagents).
var/list/temp_list = list()
// Go through all of our materials, get the subsystem instance, and then replace the list.
for(var/mat_type, amount in materials)
- if(ispath(mat_type, /datum/material_requirement))
+ if(ispath(mat_type, /datum/material_requirement) || ispath(mat_type, /datum/material_slot))
temp_list[mat_type] = amount
continue
diff --git a/code/modules/research/designs/comp_board_designs.dm b/code/modules/research/designs/comp_board_designs.dm
index 2daa2404c08e..2811cf279abc 100644
--- a/code/modules/research/designs/comp_board_designs.dm
+++ b/code/modules/research/designs/comp_board_designs.dm
@@ -397,7 +397,10 @@
departmental_flags = DEPARTMENT_BITFLAG_SECURITY //Honestly should have a bridge techfab for this sometime.
/datum/design/board/shuttle
- category = list("Computer Boards", "Shuttle Machinery")
+ build_type = IMPRINTER
+ category = list(
+ RND_CATEGORY_COMPUTER + RND_SUBCATEGORY_COMPUTER_ENGINEERING
+ )
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO
/datum/design/board/shuttle/flight_control
diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index bf5abaa65f91..193fe72124c1 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -1351,6 +1351,7 @@
desc = "The circuit for a propulsion engine."
id = "propulsion_engine"
build_path = /obj/item/circuitboard/machine/engine/propulsion
+ build_type = IMPRINTER
category = list(
RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
)
diff --git a/code/modules/research/designs/medical_designs.dm b/code/modules/research/designs/medical_designs.dm
index bcc98287e246..b46ce8075417 100644
--- a/code/modules/research/designs/medical_designs.dm
+++ b/code/modules/research/designs/medical_designs.dm
@@ -581,6 +581,25 @@
id = "ci-thermals-moth"
build_path = /obj/item/organ/eyes/robotic/thermals/moth
+/datum/design/cyberimp_tacvisor
+ name = "Tactical IFF Visor"
+ desc = "A sick IFF visor with an inbuilt LED display. May critically overload the user's prefrontal cortex."
+ id = "ci-tacvisor"
+ build_type = PROTOLATHE | AWAY_LATHE | MECHFAB
+ construction_time = 6 SECONDS
+ materials = list(
+ /datum/material/iron = SMALL_MATERIAL_AMOUNT * 4,
+ /datum/material/glass = SMALL_MATERIAL_AMOUNT * 4,
+ /datum/material/silver = SMALL_MATERIAL_AMOUNT * 4,
+ /datum/material/gold = SMALL_MATERIAL_AMOUNT * 6,
+ /datum/material/plasma = HALF_SHEET_MATERIAL_AMOUNT,
+ )
+ build_path = /obj/item/organ/eyes/robotic/tacvisor
+ category = list(
+ RND_CATEGORY_CYBERNETICS + RND_SUBCATEGORY_CYBERNETICS_ORGANS_COMBAT
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_MEDICAL
+
/datum/design/cyberimp_antidrop
name = "Anti-Drop Implant"
desc = "This cybernetic brain implant will allow you to force your hand muscles to contract, preventing item dropping. Twitch ear to toggle."
diff --git a/code/modules/research/designs/misc_designs.dm b/code/modules/research/designs/misc_designs.dm
index 3a1662c0dabc..cc109ccd36b3 100644
--- a/code/modules/research/designs/misc_designs.dm
+++ b/code/modules/research/designs/misc_designs.dm
@@ -1144,7 +1144,7 @@
name = "Shuttle Frame Rods"
id = "shuttlerods"
build_type = PROTOLATHE
- materials = list(/datum/material/iron =HALF_SHEET_MATERIAL_AMOUNT, /datum/material/titanium = SMALL_MATERIAL_AMOUNT)
+ materials = list(/datum/material/iron = HALF_SHEET_MATERIAL_AMOUNT, /datum/material/titanium = SMALL_MATERIAL_AMOUNT)
build_path = /obj/item/stack/rods/shuttle
category = list(
RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS
diff --git a/code/modules/research/designs/weapon_designs.dm b/code/modules/research/designs/weapon_designs.dm
index e83b0b77fb4b..b09378ea49b8 100644
--- a/code/modules/research/designs/weapon_designs.dm
+++ b/code/modules/research/designs/weapon_designs.dm
@@ -626,7 +626,7 @@
desc = "A mace fit for a cleric. Useful for bypassing plate armor, but too bulky for much else."
id = "cleric_mace"
build_type = AUTOLATHE
- materials = list(/datum/material_requirement/solid_material = SHEET_MATERIAL_AMOUNT * 4.5, /datum/material_requirement/rigid_material = SHEET_MATERIAL_AMOUNT * 1.5)
+ materials = list(/datum/material_slot/weapon_head = SHEET_MATERIAL_AMOUNT * 4.5, /datum/material_slot/handle = SHEET_MATERIAL_AMOUNT * 1.5)
build_path = /obj/item/melee/cleric_mace
category = list(RND_CATEGORY_IMPORTED)
diff --git a/code/modules/research/techweb/nodes/cyborg_nodes.dm b/code/modules/research/techweb/nodes/cyborg_nodes.dm
index fee377af77c8..66da933c1aef 100644
--- a/code/modules/research/techweb/nodes/cyborg_nodes.dm
+++ b/code/modules/research/techweb/nodes/cyborg_nodes.dm
@@ -183,6 +183,7 @@
"ci-reviver",
"ci-antidrop",
"ci-antistun",
+ "ci-tacvisor",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_4_POINTS)
announce_channels = list(RADIO_CHANNEL_SCIENCE, RADIO_CHANNEL_MEDICAL)
diff --git a/code/modules/research/xenobiology/crossbreeding/_misc.dm b/code/modules/research/xenobiology/crossbreeding/_misc.dm
index a75df1542a6f..a0343a053850 100644
--- a/code/modules/research/xenobiology/crossbreeding/_misc.dm
+++ b/code/modules/research/xenobiology/crossbreeding/_misc.dm
@@ -45,11 +45,8 @@ Slimecrossing Items
ret[part.body_zone] = saved_part
return ret
-/obj/item/camera/rewind/photo_taken(atom/target, mob/user)
+/obj/item/camera/rewind/on_flash(atom/target, mob/user)
. = ..()
- if(!.)
- return
-
if(user == target)
to_chat(user, span_notice("You take a selfie!"))
else
@@ -66,10 +63,8 @@ Slimecrossing Items
pictures_left = 1
pictures_max = 1
-/obj/item/camera/timefreeze/photo_taken(atom/target, mob/user)
+/obj/item/camera/timefreeze/on_flash(atom/target, mob/user)
. = ..()
- if(!.)
- return
new /obj/effect/timestop(get_turf(target), 2, 50, list(user))
//Hypercharged slime cell - Charged Yellow
diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm
index 727518bef728..594094deefd6 100644
--- a/code/modules/research/xenobiology/xenobiology.dm
+++ b/code/modules/research/xenobiology/xenobiology.dm
@@ -1122,7 +1122,7 @@ GLOBAL_LIST_INIT(slime_extract_auto_activate_reactions, init_slime_auto_activate
///Definitions for slime products that don't have anywhere else to go (Floor tiles, blueprints).
/obj/item/stack/tile/bluespace
- name = "bluespace floor tile"
+ name = "stabilized bluespace floor tile"
singular_name = "floor tile"
desc = "Through a series of micro-teleports these tiles let people move at incredible speeds."
icon_state = "tile_bluespace"
diff --git a/code/modules/shuttle/mobile_port/variants/arrivals.dm b/code/modules/shuttle/mobile_port/variants/arrivals.dm
index bcb2db4e78eb..838fdd4c7c1f 100644
--- a/code/modules/shuttle/mobile_port/variants/arrivals.dm
+++ b/code/modules/shuttle/mobile_port/variants/arrivals.dm
@@ -41,6 +41,8 @@
new_latejoin += shuttle_chair
if(isnull(console))
console = locate() in arrival_turf
+ if(SStts.tts_enabled && console)
+ console.voice = SStts.tram_voice
RegisterSignal(console, COMSIG_QDELETING, PROC_REF(find_console))
areas += arrival_area
@@ -114,6 +116,8 @@
var/obj/machinery/requests_console/target = locate() in arrival_turf
if(!QDELETED(target))
console = target
+ if(SStts.tts_enabled && console)
+ console.voice = SStts.tram_voice
RegisterSignal(console, COMSIG_QDELETING, PROC_REF(find_console))
return
diff --git a/code/modules/shuttle/mobile_port/variants/emergency/pods.dm b/code/modules/shuttle/mobile_port/variants/emergency/pods.dm
index 80d451129951..6d9460f1435a 100644
--- a/code/modules/shuttle/mobile_port/variants/emergency/pods.dm
+++ b/code/modules/shuttle/mobile_port/variants/emergency/pods.dm
@@ -140,6 +140,10 @@
. = ..()
icon_state = "wall_safe[atom_storage?.locked ? "_locked" : ""]"
+/obj/item/storage/pod/create_storage(max_slots, max_specific_storage, max_total_storage, list/canhold, list/canthold, storage_type)
+ . = ..()
+ update_appearance(UPDATE_ICON_STATE)
+
MAPPING_DIRECTIONAL_HELPERS(/obj/item/storage/pod, 32)
/obj/item/storage/pod/PopulateContents()
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index 462c7067e176..3ba712e5c6ce 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -470,9 +470,8 @@
return jointext(check_list, "
")
-/// Returns surgery self-check information for this bodypart
-/obj/item/bodypart/proc/get_surgery_self_check()
- var/list/surgery_message = list()
+/// Returns all surgical states, filtering out stuff which should not be reported
+/obj/item/bodypart/proc/get_reported_surgery_state()
var/reported_state = surgery_state
if(!LIMB_HAS_SKIN(src))
reported_state &= ~SKINLESS_SURGERY_STATES
@@ -481,6 +480,18 @@
if(!LIMB_HAS_VESSELS(src))
reported_state &= ~VESSELLESS_SURGERY_STATES
+ // hide surgical states applied by wounds if the limb isn't being operated on, to keep it simple
+ if(!HAS_TRAIT(src, TRAIT_READY_TO_OPERATE))
+ for(var/datum/wound/wound as anything in wounds)
+ reported_state &= ~wound.surgery_states
+
+ return reported_state
+
+/// Returns surgery self-check information for this bodypart
+/obj/item/bodypart/proc/get_surgery_self_check()
+ var/list/surgery_message = list()
+ var/reported_state = get_reported_surgery_state()
+
if(HAS_SURGERY_STATE(reported_state, SURGERY_SKIN_CUT))
surgery_message += "skin has been incised"
if(HAS_SURGERY_STATE(reported_state, SURGERY_SKIN_OPEN))
@@ -509,7 +520,7 @@
if(length(surgery_message))
return span_tooltip("Your limb is undergoing surgery. If no doctors are around, \
- you could suture or cauterize yourself to cancel it.", span_warning("Its [english_list(surgery_message)]!"))
+ you could suture or cauterize yourself to cancel it.", span_smalldanger("Its [english_list(surgery_message)]!"))
return ""
/// Returns surgery examine information for this bodypart
@@ -518,13 +529,7 @@
var/capital_zone = owner ? "[owner.p_Their()] [plaintext_zone]" : capitalize("[src]")
var/single_message = ""
var/list/sub_messages = list()
- var/reported_state = surgery_state
- if(!LIMB_HAS_SKIN(src))
- reported_state &= ~SKINLESS_SURGERY_STATES
- if(!LIMB_HAS_BONES(src))
- reported_state &= ~BONELESS_SURGERY_STATES
- if(!LIMB_HAS_VESSELS(src))
- reported_state &= ~VESSELLESS_SURGERY_STATES
+ var/reported_state = get_reported_surgery_state()
if(HAS_SURGERY_STATE(reported_state, SURGERY_SKIN_CUT))
sub_messages += "skin has been incised"
@@ -563,9 +568,9 @@
single_message = "[owner?.p_Their() || "The"] chest cavity is wide open!"
if(length(sub_messages) >= 2)
- return span_danger("[capital_zone]'s [english_list(sub_messages)].")
+ return span_smalldanger("[capital_zone]'s [english_list(sub_messages)].")
if(single_message)
- return span_danger(single_message)
+ return span_smalldanger(single_message)
return ""
/obj/item/bodypart/blob_act()
@@ -1648,7 +1653,7 @@
*/
/obj/item/bodypart/proc/seep_gauze(seep_amt = 0)
var/obj/item/stack/medical/wrap/current_gauze = LAZYACCESS(applied_items, LIMB_ITEM_GAUZE)
- if(!current_gauze)
+ if(!current_gauze || !current_gauze.absorption_capacity)
return FALSE
current_gauze.absorption_capacity -= seep_amt
if(current_gauze.absorption_capacity <= 0)
diff --git a/code/modules/surgery/bodyparts/head.dm b/code/modules/surgery/bodyparts/head.dm
index 4512cb307fc0..24dc2b06225c 100644
--- a/code/modules/surgery/bodyparts/head.dm
+++ b/code/modules/surgery/bodyparts/head.dm
@@ -228,32 +228,9 @@
. += no_eyes
return
- if(!eyes.eye_icon_state || !(head_flags & HEAD_EYESPRITES))
- return
-
- // This is a bit of copy/paste code from eyes.dm:generate_body_overlay
- var/image/eye_left = image(eyes.eye_icon, "[eyes.eye_icon_state]_l", -EYES_LAYER, SOUTH)
- var/image/eye_right = image(eyes.eye_icon, "[eyes.eye_icon_state]_r", -EYES_LAYER, SOUTH)
- if(head_flags & HEAD_EYECOLOR)
- if(eyes.eye_color_left)
- eye_left.color = eyes.eye_color_left
- if(eyes.eye_color_right)
- eye_right.color = eyes.eye_color_right
-
- var/list/emissive_overlays = eyes.get_emissive_overlays(eye_left, eye_right, src)
- if(length(emissive_overlays))
- eye_left.overlays += image(emissive_overlays[1], dir = SOUTH)
- eye_right.overlays += image(emissive_overlays[2], dir = SOUTH)
- else if(blocks_emissive != EMISSIVE_BLOCK_NONE)
- eye_left.overlays += image(emissive_blocker(eye_left.icon, eye_left.icon_state, src, alpha = eye_left.alpha), dir = SOUTH)
- eye_right.overlays += image(emissive_blocker(eye_right.icon, eye_right.icon_state, src, alpha = eye_right.alpha), dir = SOUTH)
-
- if(worn_face_offset)
- worn_face_offset.apply_offset(eye_left)
- worn_face_offset.apply_offset(eye_right)
-
- . += eye_left
- . += eye_right
+ if(head_flags & HEAD_EYESPRITES)
+ for (var/mutable_appearance/overlay as anything in eyes.generate_body_overlay(null, src))
+ . += image(overlay, dir = SOUTH)
/obj/item/bodypart/head/get_voice(add_id_name)
return "The head of [get_face_name()]"
diff --git a/code/modules/surgery/operations/_operation.dm b/code/modules/surgery/operations/_operation.dm
index 8d49b07c9f7c..26d647f24726 100644
--- a/code/modules/surgery/operations/_operation.dm
+++ b/code/modules/surgery/operations/_operation.dm
@@ -1109,8 +1109,13 @@ GLOBAL_DATUM_INIT(operations, /datum/operation_holder, new)
if(!pre_preop(operating_on, surgeon, tool, operation_args))
return FALSE
// if pre_preop slept, sanity check that everything is still valid
- if(preop_time != world.time && (patient != get_patient(operating_on) || !surgeon.Adjacent(patient || operating_on) || !surgeon.is_holding(tool) || !operate_check(patient, operating_on, surgeon, tool, operation_args)))
- return FALSE
+ if(preop_time != world.time)
+ if(patient != get_patient(operating_on))
+ return FALSE
+ if(!in_range(patient || operating_on, surgeon))
+ return FALSE
+ if(!operate_check(patient, operating_on, surgeon, tool, operation_args))
+ return FALSE
play_operation_sound(operating_on, surgeon, tool, preop_sound)
on_preop(operating_on, surgeon, tool, operation_args)
diff --git a/code/modules/surgery/operations/operation_bone_repair.dm b/code/modules/surgery/operations/operation_bone_repair.dm
index 330889569287..f064dee514b0 100644
--- a/code/modules/surgery/operations/operation_bone_repair.dm
+++ b/code/modules/surgery/operations/operation_bone_repair.dm
@@ -3,7 +3,7 @@
name = "reset dislocation"
desc = "Reset a dislocated bone in a patient's limb. \
Similar to the field procedure, but quicker and safer due to being performed in a controlled environment."
- operation_flags = OPERATION_PRIORITY_NEXT_STEP | OPERATION_NO_PATIENT_REQUIRED | OPERATION_AFFECTS_MOOD | OPERATION_STANDING_ALLOWED
+ operation_flags = OPERATION_PRIORITY_NEXT_STEP | OPERATION_NO_PATIENT_REQUIRED | OPERATION_AFFECTS_MOOD | OPERATION_STANDING_ALLOWED | OPERATION_IGNORE_CLOTHES
implements = list(
TOOL_BONESET = 1,
TOOL_CROWBAR = 2,
diff --git a/code/modules/surgery/operations/operation_generic.dm b/code/modules/surgery/operations/operation_generic.dm
index 545efa20946c..d3de2375cd39 100644
--- a/code/modules/surgery/operations/operation_generic.dm
+++ b/code/modules/surgery/operations/operation_generic.dm
@@ -396,7 +396,15 @@
return ..() + list("the limb must have bones")
/datum/surgery_operation/limb/fix_bones/state_check(obj/item/bodypart/limb)
- return LIMB_HAS_BONES(limb)
+ if(!LIMB_HAS_BONES(limb))
+ return FALSE
+
+ // if a wound has given us the broken bone state, don't show this surgery as an option, to prevent confusion
+ for(var/datum/wound/wound as anything in limb.wounds)
+ if(wound.surgery_states & any_surgery_states_required)
+ return FALSE
+
+ return TRUE
/datum/surgery_operation/limb/fix_bones/on_preop(obj/item/bodypart/limb, mob/living/surgeon, obj/item/tool, list/operation_args)
display_results(
diff --git a/code/modules/surgery/organs/internal/eyes/_eyes.dm b/code/modules/surgery/organs/internal/eyes/_eyes.dm
index b6152e8b0ce6..64285ea5afc1 100644
--- a/code/modules/surgery/organs/internal/eyes/_eyes.dm
+++ b/code/modules/surgery/organs/internal/eyes/_eyes.dm
@@ -286,45 +286,46 @@
return ..() || (owner.stat != DEAD && !HAS_TRAIT(owner, TRAIT_KNOCKEDOUT) && (owner.is_blind() || owner.is_nearsighted()))
/// This proc generates a list of overlays that the eye should be displayed using for the given parent
-/obj/item/organ/eyes/proc/generate_body_overlay(mob/living/carbon/human/parent)
- if(!istype(parent) || parent.get_organ_by_type(/obj/item/organ/eyes) != src)
- CRASH("Generating a body overlay for [src] targeting an invalid parent '[parent]'.")
-
+/obj/item/organ/eyes/proc/generate_body_overlay(mob/living/carbon/human/parent, obj/item/bodypart/limb)
if(isnull(eye_icon_state))
return list()
- var/mutable_appearance/eye_left = mutable_appearance(eye_icon, "[eye_icon_state]_l", -EYES_LAYER, parent)
- var/mutable_appearance/eye_right = mutable_appearance(eye_icon, "[eye_icon_state]_r", -EYES_LAYER, parent)
+ var/mutable_appearance/eye_left = mutable_appearance(eye_icon, "[eye_icon_state]_l", -EYES_LAYER, parent || limb)
+ var/mutable_appearance/eye_right = mutable_appearance(eye_icon, "[eye_icon_state]_r", -EYES_LAYER, parent || limb)
var/list/overlays = list(eye_left, eye_right)
- if(!(parent.obscured_slots & HIDEEYES))
- overlays += get_emissive_overlays(eye_left, eye_right, parent)
-
- var/obj/item/bodypart/head/my_head = parent.get_bodypart(BODY_ZONE_HEAD)
+ if(!(parent?.obscured_slots & HIDEEYES))
+ overlays += get_emissive_overlays(eye_left, eye_right, parent || limb)
- if(!my_head)
+ if(!limb)
return overlays
- if(my_head.head_flags & HEAD_EYECOLOR)
- eye_right.color = parent.get_right_eye_color()
- eye_left.color = parent.get_left_eye_color()
- var/list/eyelids = setup_eyelids(eye_left, eye_right, parent)
- if (LAZYLEN(eyelids))
- overlays += eyelids
+ // Futureproofing for HARS/weird species
+ var/obj/item/bodypart/head/head = astype(limb, /obj/item/bodypart/head)
+ if(head?.head_flags & HEAD_EYECOLOR)
+ if (parent)
+ eye_right.color = parent.get_right_eye_color()
+ eye_left.color = parent.get_left_eye_color()
+ var/list/eyelids = setup_eyelids(eye_left, eye_right, parent)
+ if (LAZYLEN(eyelids))
+ overlays += eyelids
+ else
+ eye_right.color = eye_color_right
+ eye_left.color = eye_color_left
if (scarring & RIGHT_EYE_SCAR)
- var/mutable_appearance/right_scar = mutable_appearance('icons/mob/human/human_eyes.dmi', "eye_scar_right", -EYES_LAYER, parent)
- right_scar.color = my_head.draw_color
+ var/mutable_appearance/right_scar = mutable_appearance('icons/mob/human/human_eyes.dmi', "eye_scar_right", -EYES_LAYER, parent || limb)
+ right_scar.color = limb.draw_color
overlays += right_scar
if (scarring & LEFT_EYE_SCAR)
- var/mutable_appearance/left_scar = mutable_appearance('icons/mob/human/human_eyes.dmi', "eye_scar_left", -EYES_LAYER, parent)
- left_scar.color = my_head.draw_color
+ var/mutable_appearance/left_scar = mutable_appearance('icons/mob/human/human_eyes.dmi', "eye_scar_left", -EYES_LAYER, parent || limb)
+ left_scar.color = limb.draw_color
overlays += left_scar
- if(my_head.worn_face_offset)
+ if(head?.worn_face_offset)
for (var/mutable_appearance/overlay as anything in overlays)
- my_head.worn_face_offset.apply_offset(overlay)
+ head.worn_face_offset.apply_offset(overlay)
return overlays
@@ -336,9 +337,14 @@
emissive_effect = EMISSIVE_BLOOM
else if((owner && HAS_TRAIT(owner, TRAIT_REFLECTIVE_EYES)) || (TRAIT_REFLECTIVE_EYES in organ_traits))
emissive_effect = EMISSIVE_SPECULAR
+
if(emissive_effect)
return_list += emissive_appearance(eye_left.icon, eye_left.icon_state, spokesman, -EYES_LAYER, alpha = eye_left.alpha, effect_type = emissive_effect)
return_list += emissive_appearance(eye_right.icon, eye_right.icon_state, spokesman, -EYES_LAYER, alpha = eye_right.alpha, effect_type = emissive_effect)
+ else
+ return_list += emissive_blocker(eye_left.icon, eye_left.icon_state, spokesman, -EYES_LAYER, alpha = eye_left.alpha)
+ return_list += emissive_blocker(eye_right.icon, eye_right.icon_state, spokesman, -EYES_LAYER, alpha = eye_right.alpha)
+
return return_list
/obj/item/organ/eyes/update_overlays()
@@ -606,604 +612,3 @@
#undef NIGHTVISION_LIGHT_LOW
#undef NIGHTVISION_LIGHT_MID
#undef NIGHTVISION_LIGHT_HIG
-
-/obj/item/organ/eyes/night_vision/mushroom
- name = "fung-eye"
- desc = "While on the outside they look inert and dead, the eyes of mushroom people are actually very advanced."
- low_light_cutoff = list(0, 15, 20)
- medium_light_cutoff = list(0, 20, 35)
- high_light_cutoff = list(0, 40, 50)
- pupils_name = "photosensory openings"
- penlight_message = "are attached to fungal stalks"
-
-/obj/item/organ/eyes/zombie
- name = "undead eyes"
- desc = "Somewhat counterintuitively, these half-rotten eyes actually have superior vision to those of a living human."
- color_cutoffs = list(25, 35, 5)
- penlight_message = "are rotten and decayed!"
-
-/obj/item/organ/eyes/zombie/penlight_examine(mob/living/viewer, obj/item/examtool)
- return span_danger("[owner.p_Their()] eyes [penlight_message]")
-
-/obj/item/organ/eyes/alien
- name = "alien eyes"
- desc = "It turned out they had them after all!"
- sight_flags = SEE_MOBS
- color_cutoffs = list(25, 5, 42)
-
-/obj/item/organ/eyes/golem
- name = "resonating crystal"
- desc = "Golems somehow measure external light levels and detect nearby ore using this sensitive mineral lattice."
- icon_state = "adamantine_cords"
- eye_icon_state = null
- blink_animation = FALSE
- iris_overlay = null
- color = COLOR_GOLEM_GRAY
- visual = FALSE
- organ_flags = ORGAN_MINERAL
- color_cutoffs = list(10, 15, 5)
- actions_types = list(/datum/action/cooldown/golem_ore_sight)
- penlight_message = "glimmer, their crystaline structure refracting light inwards"
- pupils_name = "lensing gems" ///given it says these are a "mineral lattice" that collects light i assume they work like artifical ruby laser foci
-
-/// Send an ore detection pulse on a cooldown
-/datum/action/cooldown/golem_ore_sight
- name = "Ore Resonance"
- desc = "Causes nearby ores to vibrate, revealing their location."
- button_icon = 'icons/obj/devices/scanner.dmi'
- button_icon_state = "manual_mining"
- check_flags = AB_CHECK_CONSCIOUS
- cooldown_time = 10 SECONDS
-
-/datum/action/cooldown/golem_ore_sight/Activate(atom/target)
- . = ..()
- mineral_scan_pulse(get_turf(target), scanner = target)
-
-///Robotic
-
-/obj/item/organ/eyes/robotic
- name = "robotic eyes"
- desc = "Your vision is augmented."
- icon_state = "eyes_cyber"
- organ_flags = ORGAN_ROBOTIC
- failing_desc = "seems to be broken."
- pupils_name = "apertures"
- penlight_message = "are cybernetic, click-whirring as they refocus"
-
-/obj/item/organ/eyes/robotic/emp_act(severity)
- . = ..()
- if((. & EMP_PROTECT_SELF) || !owner)
- return
- if(prob(10 * severity))
- return
- to_chat(owner, span_warning("Static obfuscates your vision!"))
- owner.flash_act(visual = 1)
-
-/obj/item/organ/eyes/robotic/basic
- name = "basic robotic eyes"
- desc = "A pair of basic cybernetic eyes that restore vision, but at some vulnerability to light."
- icon_state = "eyes_cyber_basic"
- iris_overlay = null
- eye_color_left = "#2f3032"
- eye_color_right = "#2f3032"
- flash_protect = FLASH_PROTECTION_SENSITIVE
- penlight_message = "are low grade cybernetics, poorly compensating for the light"
-
-/obj/item/organ/eyes/robotic/basic/emp_act(severity)
- . = ..()
- if(. & EMP_PROTECT_SELF)
- return
- if(prob(10 * severity))
- apply_organ_damage(20 * severity)
- to_chat(owner, span_warning("Your eyes start to fizzle in their sockets!"))
- do_sparks(2, TRUE, owner)
- owner.emote("scream")
-
-/obj/item/organ/eyes/robotic/xray
- name = "x-ray eyes"
- desc = "These cybernetic eyes will give you X-ray vision. Blinking is futile."
- icon_state = "eyes_cyber_xray"
- iris_overlay = null
- eye_color_left = "#3cb8a5"
- eye_color_right = "#3cb8a5"
- sight_flags = SEE_MOBS | SEE_OBJS | SEE_TURFS
- flash_protect = FLASH_PROTECTION_SENSITIVE
- organ_traits = list(TRAIT_XRAY_VISION)
- penlight_message = "are replaced by small radiation emitters and detectors"
-
-/obj/item/organ/eyes/robotic/thermals
- name = "thermal eyes"
- desc = "These cybernetic eye implants will give you thermal vision. Vertical slit pupil included."
- icon_state = "eyes_cyber_thermal"
- iris_overlay = null
- eye_color_left = "#ce2525"
- eye_color_right = "#ce2525"
- // We're gonna downshift green and blue a bit so darkness looks yellow
- color_cutoffs = list(25, 8, 5)
- sight_flags = SEE_MOBS
- flash_protect = FLASH_PROTECTION_SENSITIVE
- pupils_name = "slit aperatures"
- penlight_message = "are cybernetic, with vertically slit metalic lenses."
-
-/obj/item/organ/eyes/robotic/flashlight
- name = "flashlight eyes"
- desc = "It's two flashlights rigged together with some wire. Why would you put these in someone's head?"
- icon_state = "flashlight_eyes"
- eye_color_left = "#fee5a3"
- eye_color_right = "#fee5a3"
- iris_overlay = null
- flash_protect = FLASH_PROTECTION_WELDER
- tint = INFINITY
- custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 2.5, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 1.9)
- var/obj/item/flashlight/eyelight/eye
- light_reactive = FALSE
- pupils_name = "flashlights"
- penlight_message = "are actually two flashlights taped together. ...why"
-
-/obj/item/organ/eyes/robotic/flashlight/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/empprotection, EMP_PROTECT_ALL)
-
-/obj/item/organ/eyes/robotic/flashlight/on_mob_insert(mob/living/carbon/victim)
- . = ..()
- if(!eye)
- eye = new /obj/item/flashlight/eyelight()
- eye.set_light_on(TRUE)
- eye.forceMove(victim)
- eye.update_brightness(victim)
- victim.become_blind(FLASHLIGHT_EYES)
-
-/obj/item/organ/eyes/robotic/flashlight/on_mob_remove(mob/living/carbon/victim)
- . = ..()
- eye.set_light_on(FALSE)
- eye.update_brightness(victim)
- eye.forceMove(src)
- victim.cure_blind(FLASHLIGHT_EYES)
-
-// Welding shield implant
-/obj/item/organ/eyes/robotic/shield
- name = "shielded robotic eyes"
- desc = "These reactive micro-shields will protect you from welders and flashes without obscuring your vision."
- icon_state = "eyes_cyber_shield"
- iris_overlay = null
- eye_color_left = "#353845"
- eye_color_right = "#353845"
- flash_protect = FLASH_PROTECTION_WELDER
- pupils_name = "flash shields"
- penlight_message = "have polarized cybernetic lenses, blocking bright lights"
-
-/obj/item/organ/eyes/robotic/shield/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/empprotection, EMP_PROTECT_ALL)
-
-#define MATCH_LIGHT_COLOR 1
-#define USE_CUSTOM_COLOR 0
-#define UPDATE_LIGHT 0
-#define UPDATE_EYES_LEFT 1
-#define UPDATE_EYES_RIGHT 2
-
-/obj/item/organ/eyes/robotic/glow
- name = "high luminosity eyes"
- desc = "Special glowing eyes, used by snowflakes who want to be special."
- icon_state = "eyes_cyber_glow"
- iris_overlay = "eyes_cyber_glow_iris"
- eye_color_left = "#19191a"
- eye_color_right = "#19191a"
- actions_types = list(/datum/action/item_action/organ_action/use, /datum/action/item_action/organ_action/toggle)
- var/max_light_beam_distance = 5
- var/obj/item/flashlight/eyelight/glow/eye
- /// base icon state for eye overlays
- var/base_eye_state = "eyes_glow_gs"
- /// Whether or not to match the eye color to the light or use a custom selection
- var/eye_color_mode = USE_CUSTOM_COLOR
- /// The selected color for the light beam itself
- var/light_color_string = "#ffffff"
- /// The custom selected eye color for the left eye. Defaults to the mob's natural eye color
- var/left_eye_color_string
- /// The custom selected eye color for the right eye. Defaults to the mob's natural eye color
- var/right_eye_color_string
- penlight_message = "shine back with cybernetic LEDs"
-
-/obj/item/organ/eyes/robotic/glow/Initialize(mapload)
- . = ..()
- eye = new /obj/item/flashlight/eyelight/glow
-
-/obj/item/organ/eyes/robotic/glow/Destroy()
- . = ..()
- deactivate(close_ui = TRUE)
- QDEL_NULL(eye)
-
-/obj/item/organ/eyes/robotic/glow/emp_act(severity)
- . = ..()
- if(!eye.light_on || . & EMP_PROTECT_SELF)
- return
- deactivate(close_ui = TRUE)
-
-/// Set the initial color of the eyes on insert to be the mob's previous eye color.
-/obj/item/organ/eyes/robotic/glow/on_mob_insert(mob/living/carbon/eye_recipient, special = FALSE, movement_flags)
- . = ..()
- left_eye_color_string = eye_color_left
- right_eye_color_string = eye_color_right
- update_mob_eye_color(eye_recipient)
- deactivate(close_ui = TRUE)
- eye.forceMove(eye_recipient)
-
-/obj/item/organ/eyes/robotic/glow/on_mob_remove(mob/living/carbon/eye_owner)
- deactivate(eye_owner, close_ui = TRUE)
- if(!QDELETED(eye))
- eye.forceMove(src)
- return ..()
-
-/obj/item/organ/eyes/robotic/glow/ui_state(mob/user)
- return GLOB.default_state
-
-/obj/item/organ/eyes/robotic/glow/ui_status(mob/user, datum/ui_state/state)
- if(!QDELETED(owner))
- if(owner == user)
- return min(
- ui_status_user_is_abled(user, src),
- ui_status_only_living(user),
- )
- else return UI_CLOSE
- return ..()
-
-/obj/item/organ/eyes/robotic/glow/ui_interact(mob/user, datum/tgui/ui)
- ui = SStgui.try_update_ui(user, src, ui)
- if(!ui)
- ui = new(user, src, "HighLuminosityEyesMenu")
- ui.autoupdate = FALSE
- ui.open()
-
-/obj/item/organ/eyes/robotic/glow/ui_data(mob/user)
- var/list/data = list()
-
- data["eyeColor"] = list(
- mode = eye_color_mode,
- hasOwner = owner ? TRUE : FALSE,
- left = left_eye_color_string,
- right = right_eye_color_string,
- )
- data["lightColor"] = light_color_string
- data["range"] = eye.light_range
-
- return data
-
-/obj/item/organ/eyes/robotic/glow/ui_act(action, list/params, datum/tgui/ui)
- . = ..()
- if(.)
- return
-
- switch(action)
- if("set_range")
- var/new_range = params["new_range"]
- set_beam_range(new_range)
- return TRUE
- if("pick_color")
- var/new_color = tgui_color_picker(
- usr,
- "Choose eye color color:",
- "High Luminosity Eyes Menu",
- light_color_string
- )
- if(new_color)
- var/to_update = params["to_update"]
- set_beam_color(new_color, to_update)
- return TRUE
- if("enter_color")
- var/new_color = LOWER_TEXT(params["new_color"])
- var/to_update = params["to_update"]
- set_beam_color(new_color, to_update, sanitize = TRUE)
- return TRUE
- if("random_color")
- var/to_update = params["to_update"]
- randomize_color(to_update)
- return TRUE
- if("toggle_eye_color")
- toggle_eye_color_mode()
- return TRUE
-
-/obj/item/organ/eyes/robotic/glow/ui_action_click(mob/user, action)
- if(istype(action, /datum/action/item_action/organ_action/toggle))
- toggle_active()
- else if(istype(action, /datum/action/item_action/organ_action/use))
- ui_interact(user)
-
-/**
- * Activates the light
- *
- * Turns on the attached flashlight object, updates the mob overlay to be added.
- */
-/obj/item/organ/eyes/robotic/glow/proc/activate()
- if(eye.light_range)
- eye.set_light_on(TRUE)
- else
- eye.light_on = TRUE // at range 0 we are just going to make the eyes glow emissively, no light overlay
- update_mob_eye_color()
-
-/**
- * Deactivates the light
- *
- * Turns off the attached flashlight object, closes UIs, updates the mob overlay to be removed.
- * Arguments:
- * * mob/living/carbon/eye_owner - the mob who the eyes belong to
- * * close_ui - whether or not to close the ui
- */
-/obj/item/organ/eyes/robotic/glow/proc/deactivate(mob/living/carbon/eye_owner = owner, close_ui = FALSE)
- if(close_ui)
- SStgui.close_uis(src)
- eye.set_light_on(FALSE)
- update_mob_eye_color(eye_owner)
-
-/**
- * Randomizes the light color
- *
- * Picks a random color and sets the beam color to that
- * Arguments:
- * * to_update - whether we are setting the color for the light beam itself, or the individual eyes
- */
-/obj/item/organ/eyes/robotic/glow/proc/randomize_color(to_update = UPDATE_LIGHT)
- var/new_color = "#"
- for(var/i in 1 to 3)
- new_color += num2hex(rand(0, 255), 2)
- set_beam_color(new_color, to_update)
-
-/**
- * Setter function for the light's range
- *
- * Sets the light range of the attached flashlight object
- * Includes some 'unique' logic to accomodate for some quirks of the lighting system
- * Arguments:
- * * new_range - the new range to set
- */
-/obj/item/organ/eyes/robotic/glow/proc/set_beam_range(new_range)
- var/old_light_range = eye.light_range
- if(old_light_range == 0 && new_range > 0 && eye.light_on) // turn bring back the light overlay if we were previously at 0 (aka emissive eyes only)
- eye.light_on = FALSE // this is stupid, but this has to be FALSE for set_light_on() to work.
- eye.set_light_on(TRUE)
- eye.set_light_range(clamp(new_range, 0, max_light_beam_distance))
-
-/**
- * Setter function for the light's color
- *
- * Sets the light color of the attached flashlight object. Sets the eye color vars of this eye organ as well and then updates the mob's eye color.
- * Arguments:
- * * newcolor - the new color hex string to set
- * * to_update - whether we are setting the color for the light beam itself, or the individual eyes
- * * sanitize - whether the hex string should be sanitized
- */
-/obj/item/organ/eyes/robotic/glow/proc/set_beam_color(newcolor, to_update = UPDATE_LIGHT, sanitize = FALSE)
- var/newcolor_string
- if(sanitize)
- newcolor_string = sanitize_hexcolor(newcolor)
- else
- newcolor_string = newcolor
- switch(to_update)
- if(UPDATE_LIGHT)
- light_color_string = newcolor_string
- eye.set_light_color(newcolor_string)
- if(UPDATE_EYES_LEFT)
- left_eye_color_string = newcolor_string
- if(UPDATE_EYES_RIGHT)
- right_eye_color_string = newcolor_string
-
- update_mob_eye_color()
-
-/**
- * Toggle the attached flashlight object on or off
- */
-/obj/item/organ/eyes/robotic/glow/proc/toggle_active()
- if(eye.light_on)
- deactivate()
- else
- activate()
-
-/**
- * Toggles for the eye color mode
- *
- * Toggles the eye color mode on or off and then calls an update on the mob's eye color
- */
-/obj/item/organ/eyes/robotic/glow/proc/toggle_eye_color_mode()
- eye_color_mode = !eye_color_mode
- update_mob_eye_color()
-
-/**
- * Updates the mob eye color
- *
- * Updates the eye color to reflect on the mob's body if it's possible to do so
- * Arguments:
- * * mob/living/carbon/eye_owner - the mob to update the eye color appearance of
- */
-/obj/item/organ/eyes/robotic/glow/proc/update_mob_eye_color(mob/living/carbon/eye_owner = owner)
- switch(eye_color_mode)
- if(MATCH_LIGHT_COLOR)
- eye_color_left = light_color_string
- eye_color_right = light_color_string
- if(USE_CUSTOM_COLOR)
- eye_color_left = left_eye_color_string
- eye_color_right = right_eye_color_string
-
- if(QDELETED(eye_owner) || !ishuman(eye_owner)) //Other carbon mobs don't have eye color.
- return
-
- var/obj/item/bodypart/head/head = eye_owner.get_bodypart(BODY_ZONE_HEAD) //if we have eyes we definently have a head anyway
- var/previous_flags = head.head_flags
- head.head_flags |= HEAD_EYECOLOR
-
- ///enabling and disabling the TRAIT_LUMINESCENT_EYES trait already calls handle_eyes(), in that case, let's skip that call
- var/skip_call = FALSE
- if(!eye.light_on)
- eye_icon_state = initial(eye_icon_state)
- skip_call = HAS_TRAIT_FROM_ONLY(eye_owner, TRAIT_LUMINESCENT_EYES, REF(src))
- remove_organ_trait(TRAIT_LUMINESCENT_EYES)
- else
- skip_call = !HAS_TRAIT(eye_owner, TRAIT_LUMINESCENT_EYES)
- add_organ_trait(TRAIT_LUMINESCENT_EYES)
- eye_icon_state = base_eye_state
-
- if(!skip_call && ishuman(eye_owner))
- var/mob/living/carbon/human/humie = eye_owner
- humie.update_eyes()
-
- head.head_flags = previous_flags
-
-#undef MATCH_LIGHT_COLOR
-#undef USE_CUSTOM_COLOR
-#undef UPDATE_LIGHT
-#undef UPDATE_EYES_LEFT
-#undef UPDATE_EYES_RIGHT
-
-/obj/item/organ/eyes/moth
- name = "moth eyes"
- desc = "These eyes seem to have increased sensitivity to bright light, with no improvement to low light vision."
- icon_state = "eyes_moth"
- eye_icon_state = "motheyes"
- blink_animation = FALSE
- iris_overlay = null
- flash_protect = FLASH_PROTECTION_SENSITIVE
- pupils_name = "ommatidia" //yes i know compound eyes have no pupils shut up
- penlight_message = "are bulbous and insectoid"
-
-/obj/item/organ/eyes/robotic/moth
- name = "robotic moth eyes"
- desc = "Your vision is augmented. Much like actual moth eyes, very sensitive to bright lights."
- icon_state = "eyes_moth_cyber"
- eye_icon_state = "motheyes_cyber"
- flash_protect = FLASH_PROTECTION_SENSITIVE
- pupils_name = "aperture clusters"
- penlight_message = "are metal hemispheres, resembling insect eyes"
-
-/obj/item/organ/eyes/robotic/basic/moth
- name = "basic robotic moth eyes"
- icon_state = "eyes_moth_cyber_basic"
- eye_icon_state = "motheyes_white"
- eye_color_left = "#65686f"
- eye_color_right = "#65686f"
- blink_animation = FALSE
- flash_protect = FLASH_PROTECTION_SENSITIVE
- pupils_name = "aperture clusters"
- penlight_message = "are metal hemispheres, resembling insect eyes"
-
-/obj/item/organ/eyes/robotic/xray/moth
- name = "moth x-ray eyes"
- desc = "These cybernetic imitation moth eyes will give you X-ray vision. Blinking is futile. Much like actual moth eyes, very sensitive to bright lights."
- icon_state = "eyes_moth_cyber_xray"
- eye_icon_state = "motheyes_white"
- eye_color_left = "#3c4e52"
- eye_color_right = "#3c4e52"
- blink_animation = FALSE
- flash_protect = FLASH_PROTECTION_HYPER_SENSITIVE
- pupils_name = "aperture clusters"
-
-/obj/item/organ/eyes/robotic/shield/moth
- name = "shielded robotic moth eyes"
- icon_state = "eyes_moth_cyber_shield"
- eye_icon_state = "motheyes_white"
- eye_color_left = "#353845"
- eye_color_right = "#353845"
- blink_animation = FALSE
- pupils_name = "aperture clusters"
- penlight_message = "have shutters, protecting insectoid compound eyes."
-
-/obj/item/organ/eyes/robotic/glow/moth
- name = "high luminosity moth eyes"
- desc = "Special glowing eyes, to be one with the lamp. Much like actual moth eyes, very sensitive to bright lights."
- icon_state = "eyes_moth_cyber_glow"
- eye_icon_state = "motheyes_cyber"
- iris_overlay = "eyes_moth_cyber_glow_iris"
- blink_animation = FALSE
- base_eye_state = "eyes_mothglow"
- flash_protect = FLASH_PROTECTION_SENSITIVE
- penlight_message = "are bulbous clusters of LEDs and cameras"
- pupils_name = "aperture clusters"
-
-/obj/item/organ/eyes/robotic/thermals/moth
- name = "thermal moth eyes"
- icon_state = "eyes_moth_cyber_thermal"
- eye_icon_state = "motheyes_white"
- eye_color_left = "#901f38"
- eye_color_right = "#901f38"
- blink_animation = FALSE
- flash_protect = FLASH_PROTECTION_HYPER_SENSITIVE
- pupils_name = "sensor clusters"
- penlight_message = "are two clustered hemispheres of thermal sensors"
-
-/obj/item/organ/eyes/ghost
- name = "ghost eyes"
- desc = "Despite lacking pupils, these can see pretty well."
- icon_state = "eyes-ghost"
- blink_animation = FALSE
- movement_type = PHASING
- organ_flags = parent_type::organ_flags | ORGAN_GHOST
-
-/obj/item/organ/eyes/snail
- name = "snail eyes"
- desc = "These eyes seem to have a large range, but might be cumbersome with glasses."
- icon_state = "eyes_snail"
- eye_icon_state = "snail_eyes"
- blink_animation = FALSE
- pupils_name = "eyestalks" //many species of snails can retract their eyes into their face! (my lame science excuse for not having better writing here)
- penlight_message = "are sat upon retractable tentacles"
-
-/obj/item/organ/eyes/jelly
- name = "jelly eyes"
- desc = "These eyes are made of a soft jelly. Unlike all other eyes, though, there are three of them."
- icon_state = "eyes_jelly"
- eye_icon_state = "jelleyes"
- blink_animation = FALSE
- iris_overlay = null
- pupils_name = "lensing bubbles" //imagine a water lens physics demo but with goo. thats how these work.
- penlight_message = "are three bubbles of refractive jelly"
-
-/obj/item/organ/eyes/lizard
- name = "reptile eyes"
- desc = "A pair of reptile eyes with thin vertical slits for pupils."
- icon_state = "lizard_eyes"
- synchronized_blinking = FALSE
- pupils_name = "slit pupils"
- penlight_message = "have vertically slit pupils and tinted whites"
-
-/obj/item/organ/eyes/night_vision/maintenance_adapted
- name = "adapted eyes"
- desc = "These red eyes look like two foggy marbles. They give off a particularly worrying glow in the dark."
- icon_state = "eyes_adapted"
- eye_color_left = "#f74a4d"
- eye_color_right = "#f74a4d"
- eye_icon_state = "eyes_glow"
- iris_overlay = null
- organ_traits = list(TRAIT_UNNATURAL_RED_GLOWY_EYES, TRAIT_LUMINESCENT_EYES)
- flash_protect = FLASH_PROTECTION_HYPER_SENSITIVE
- low_light_cutoff = list(5, 12, 20)
- medium_light_cutoff = list(15, 20, 30)
- high_light_cutoff = list(30, 35, 50)
- penlight_message = "glow a foggy red, sizzling under the light!"
-
-/obj/item/organ/eyes/night_vision/maintenance_adapted/penlight_examine(mob/living/viewer, obj/item/examtool)
- if(!owner.is_blind())
- to_chat(owner, span_danger("Your eyes sizzle agonizingly as light is shone on them!"))
- apply_organ_damage(20 * examtool.light_power) //that's 0.5 lightpower for a penlight, so one penlight shining is equivalent to two seconds in a lit area
- return span_danger("[owner.p_Their()] eyes [penlight_message]")
-
-/obj/item/organ/eyes/night_vision/maintenance_adapted/on_life(seconds_per_tick)
- if(owner.get_eye_protection() <= FLASH_PROTECTION_SENSITIVE && !owner.is_blind() && isturf(owner.loc) && owner.has_light_nearby(light_amount=0.5)) //we allow a little more than usual so we can produce light from the adapted eyes
- to_chat(owner, span_danger("Your eyes! They burn in the light!"))
- apply_organ_damage(10) //blind quickly
- playsound(owner, 'sound/machines/grill/grillsizzle.ogg', 50)
- else
- apply_organ_damage(-10) //heal quickly
- . = ..()
-
-/obj/item/organ/eyes/pod
- name = "pod eyes"
- desc = "Strangest salad you've ever seen."
- icon_state = "eyes_pod"
- eye_color_left = "#375846"
- eye_color_right = "#375846"
- iris_overlay = null
- foodtype_flags = PODPERSON_ORGAN_FOODTYPES
- penlight_message = "are green and plant-like"
-
-/obj/item/organ/eyes/felinid
- name = "felinid eyes"
- desc = "A pair of highly reflective eyes with slit pupils, like those of a cat."
- pupils_name = "slit pupils"
- penlight_message = "shine under the pearly light"
diff --git a/code/modules/surgery/organs/internal/eyes/eyes_augments.dm b/code/modules/surgery/organs/internal/eyes/eyes_augments.dm
new file mode 100644
index 000000000000..276b8437b887
--- /dev/null
+++ b/code/modules/surgery/organs/internal/eyes/eyes_augments.dm
@@ -0,0 +1,965 @@
+/obj/item/organ/eyes/robotic
+ name = "robotic eyes"
+ desc = "Your vision is augmented."
+ icon_state = "eyes_cyber"
+ organ_flags = ORGAN_ROBOTIC
+ failing_desc = "seems to be broken."
+ pupils_name = "apertures"
+ penlight_message = "are cybernetic, click-whirring as they refocus"
+
+/obj/item/organ/eyes/robotic/emp_act(severity)
+ . = ..()
+ if((. & EMP_PROTECT_SELF) || !owner)
+ return
+ if(prob(10 * severity))
+ return
+ to_chat(owner, span_warning("Static obfuscates your vision!"))
+ owner.flash_act(visual = 1)
+
+/obj/item/organ/eyes/robotic/basic
+ name = "basic robotic eyes"
+ desc = "A pair of basic cybernetic eyes that restore vision, but at some vulnerability to light."
+ icon_state = "eyes_cyber_basic"
+ iris_overlay = null
+ eye_color_left = "#2f3032"
+ eye_color_right = "#2f3032"
+ flash_protect = FLASH_PROTECTION_SENSITIVE
+ penlight_message = "are low grade cybernetics, poorly compensating for the light"
+
+/obj/item/organ/eyes/robotic/basic/emp_act(severity)
+ . = ..()
+ if(. & EMP_PROTECT_SELF)
+ return
+ if(prob(10 * severity))
+ apply_organ_damage(20 * severity)
+ to_chat(owner, span_warning("Your eyes start to fizzle in their sockets!"))
+ do_sparks(2, TRUE, owner)
+ owner.emote("scream")
+
+/obj/item/organ/eyes/robotic/xray
+ name = "x-ray eyes"
+ desc = "These cybernetic eyes will give you X-ray vision. Blinking is futile."
+ icon_state = "eyes_cyber_xray"
+ iris_overlay = null
+ eye_color_left = "#3cb8a5"
+ eye_color_right = "#3cb8a5"
+ sight_flags = SEE_MOBS | SEE_OBJS | SEE_TURFS
+ flash_protect = FLASH_PROTECTION_SENSITIVE
+ organ_traits = list(TRAIT_XRAY_VISION)
+ penlight_message = "are replaced by small radiation emitters and detectors"
+
+/obj/item/organ/eyes/robotic/thermals
+ name = "thermal eyes"
+ desc = "These cybernetic eye implants will give you thermal vision. Vertical slit pupil included."
+ icon_state = "eyes_cyber_thermal"
+ iris_overlay = null
+ eye_color_left = "#ce2525"
+ eye_color_right = "#ce2525"
+ // We're gonna downshift green and blue a bit so darkness looks yellow
+ color_cutoffs = list(25, 8, 5)
+ sight_flags = SEE_MOBS
+ flash_protect = FLASH_PROTECTION_SENSITIVE
+ pupils_name = "slit aperatures"
+ penlight_message = "are cybernetic, with vertically slit metalic lenses."
+
+/obj/item/organ/eyes/robotic/flashlight
+ name = "flashlight eyes"
+ desc = "It's two flashlights rigged together with some wire. Why would you put these in someone's head?"
+ icon_state = "flashlight_eyes"
+ eye_color_left = "#fee5a3"
+ eye_color_right = "#fee5a3"
+ iris_overlay = null
+ flash_protect = FLASH_PROTECTION_WELDER
+ tint = INFINITY
+ custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT * 2.5, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 1.9)
+ var/obj/item/flashlight/eyelight/eye
+ light_reactive = FALSE
+ pupils_name = "flashlights"
+ penlight_message = "are actually two flashlights taped together. ...why"
+
+/obj/item/organ/eyes/robotic/flashlight/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/empprotection, EMP_PROTECT_ALL)
+
+/obj/item/organ/eyes/robotic/flashlight/on_mob_insert(mob/living/carbon/victim)
+ . = ..()
+ if(!eye)
+ eye = new /obj/item/flashlight/eyelight()
+ eye.set_light_on(TRUE)
+ eye.forceMove(victim)
+ eye.update_brightness(victim)
+ victim.become_blind(FLASHLIGHT_EYES)
+
+/obj/item/organ/eyes/robotic/flashlight/on_mob_remove(mob/living/carbon/victim)
+ . = ..()
+ eye.set_light_on(FALSE)
+ eye.update_brightness(victim)
+ eye.forceMove(src)
+ victim.cure_blind(FLASHLIGHT_EYES)
+
+// Welding shield implant
+/obj/item/organ/eyes/robotic/shield
+ name = "shielded robotic eyes"
+ desc = "These reactive micro-shields will protect you from welders and flashes without obscuring your vision."
+ icon_state = "eyes_cyber_shield"
+ iris_overlay = null
+ eye_color_left = "#353845"
+ eye_color_right = "#353845"
+ flash_protect = FLASH_PROTECTION_WELDER
+ pupils_name = "flash shields"
+ penlight_message = "have polarized cybernetic lenses, blocking bright lights"
+
+/obj/item/organ/eyes/robotic/shield/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/empprotection, EMP_PROTECT_ALL)
+
+#define MATCH_LIGHT_COLOR 1
+#define USE_CUSTOM_COLOR 0
+#define UPDATE_LIGHT 0
+#define UPDATE_EYES_LEFT 1
+#define UPDATE_EYES_RIGHT 2
+
+/obj/item/organ/eyes/robotic/glow
+ name = "high luminosity eyes"
+ desc = "Special glowing eyes, used by snowflakes who want to be special."
+ icon_state = "eyes_cyber_glow"
+ iris_overlay = "eyes_cyber_glow_iris"
+ eye_color_left = "#19191a"
+ eye_color_right = "#19191a"
+ actions_types = list(/datum/action/item_action/organ_action/use, /datum/action/item_action/organ_action/toggle)
+ var/max_light_beam_distance = 5
+ var/obj/item/flashlight/eyelight/glow/eye
+ /// base icon state for eye overlays
+ var/base_eye_state = "eyes_glow_gs"
+ /// Whether or not to match the eye color to the light or use a custom selection
+ var/eye_color_mode = USE_CUSTOM_COLOR
+ /// The selected color for the light beam itself
+ var/light_color_string = "#ffffff"
+ /// The custom selected eye color for the left eye. Defaults to the mob's natural eye color
+ var/left_eye_color_string
+ /// The custom selected eye color for the right eye. Defaults to the mob's natural eye color
+ var/right_eye_color_string
+ penlight_message = "shine back with cybernetic LEDs"
+
+/obj/item/organ/eyes/robotic/glow/Initialize(mapload)
+ . = ..()
+ eye = new /obj/item/flashlight/eyelight/glow
+
+/obj/item/organ/eyes/robotic/glow/Destroy()
+ . = ..()
+ deactivate(close_ui = TRUE)
+ QDEL_NULL(eye)
+
+/obj/item/organ/eyes/robotic/glow/emp_act(severity)
+ . = ..()
+ if(!eye.light_on || . & EMP_PROTECT_SELF)
+ return
+ deactivate(close_ui = TRUE)
+
+/// Set the initial color of the eyes on insert to be the mob's previous eye color.
+/obj/item/organ/eyes/robotic/glow/on_mob_insert(mob/living/carbon/eye_recipient, special = FALSE, movement_flags)
+ . = ..()
+ left_eye_color_string = eye_color_left
+ right_eye_color_string = eye_color_right
+ update_mob_eye_color(eye_recipient)
+ deactivate(close_ui = TRUE)
+ eye.forceMove(eye_recipient)
+
+/obj/item/organ/eyes/robotic/glow/on_mob_remove(mob/living/carbon/eye_owner)
+ deactivate(eye_owner, close_ui = TRUE)
+ if(!QDELETED(eye))
+ eye.forceMove(src)
+ return ..()
+
+/obj/item/organ/eyes/robotic/glow/ui_state(mob/user)
+ return GLOB.default_state
+
+/obj/item/organ/eyes/robotic/glow/ui_status(mob/user, datum/ui_state/state)
+ if(!QDELETED(owner))
+ if(owner == user)
+ return min(
+ ui_status_user_is_abled(user, src),
+ ui_status_only_living(user),
+ )
+ else return UI_CLOSE
+ return ..()
+
+/obj/item/organ/eyes/robotic/glow/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "HighLuminosityEyesMenu")
+ ui.autoupdate = FALSE
+ ui.open()
+
+/obj/item/organ/eyes/robotic/glow/ui_data(mob/user)
+ var/list/data = list()
+
+ data["eyeColor"] = list(
+ mode = eye_color_mode,
+ hasOwner = owner ? TRUE : FALSE,
+ left = left_eye_color_string,
+ right = right_eye_color_string,
+ )
+ data["lightColor"] = light_color_string
+ data["range"] = eye.light_range
+
+ return data
+
+/obj/item/organ/eyes/robotic/glow/ui_act(action, list/params, datum/tgui/ui)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("set_range")
+ var/new_range = params["new_range"]
+ set_beam_range(new_range)
+ return TRUE
+ if("pick_color")
+ var/new_color = tgui_color_picker(
+ usr,
+ "Choose eye color color:",
+ "High Luminosity Eyes Menu",
+ light_color_string
+ )
+ if(new_color)
+ var/to_update = params["to_update"]
+ set_beam_color(new_color, to_update)
+ return TRUE
+ if("enter_color")
+ var/new_color = LOWER_TEXT(params["new_color"])
+ var/to_update = params["to_update"]
+ set_beam_color(new_color, to_update, sanitize = TRUE)
+ return TRUE
+ if("random_color")
+ var/to_update = params["to_update"]
+ randomize_color(to_update)
+ return TRUE
+ if("toggle_eye_color")
+ toggle_eye_color_mode()
+ return TRUE
+
+/obj/item/organ/eyes/robotic/glow/ui_action_click(mob/user, action)
+ if(istype(action, /datum/action/item_action/organ_action/toggle))
+ toggle_active()
+ else if(istype(action, /datum/action/item_action/organ_action/use))
+ ui_interact(user)
+
+/**
+ * Activates the light
+ *
+ * Turns on the attached flashlight object, updates the mob overlay to be added.
+ */
+/obj/item/organ/eyes/robotic/glow/proc/activate()
+ if(eye.light_range)
+ eye.set_light_on(TRUE)
+ else
+ eye.light_on = TRUE // at range 0 we are just going to make the eyes glow emissively, no light overlay
+ update_mob_eye_color()
+
+/**
+ * Deactivates the light
+ *
+ * Turns off the attached flashlight object, closes UIs, updates the mob overlay to be removed.
+ * Arguments:
+ * * mob/living/carbon/eye_owner - the mob who the eyes belong to
+ * * close_ui - whether or not to close the ui
+ */
+/obj/item/organ/eyes/robotic/glow/proc/deactivate(mob/living/carbon/eye_owner = owner, close_ui = FALSE)
+ if(close_ui)
+ SStgui.close_uis(src)
+ eye.set_light_on(FALSE)
+ update_mob_eye_color(eye_owner)
+
+/**
+ * Randomizes the light color
+ *
+ * Picks a random color and sets the beam color to that
+ * Arguments:
+ * * to_update - whether we are setting the color for the light beam itself, or the individual eyes
+ */
+/obj/item/organ/eyes/robotic/glow/proc/randomize_color(to_update = UPDATE_LIGHT)
+ var/new_color = "#"
+ for(var/i in 1 to 3)
+ new_color += num2hex(rand(0, 255), 2)
+ set_beam_color(new_color, to_update)
+
+/**
+ * Setter function for the light's range
+ *
+ * Sets the light range of the attached flashlight object
+ * Includes some 'unique' logic to accomodate for some quirks of the lighting system
+ * Arguments:
+ * * new_range - the new range to set
+ */
+/obj/item/organ/eyes/robotic/glow/proc/set_beam_range(new_range)
+ var/old_light_range = eye.light_range
+ if(old_light_range == 0 && new_range > 0 && eye.light_on) // turn bring back the light overlay if we were previously at 0 (aka emissive eyes only)
+ eye.light_on = FALSE // this is stupid, but this has to be FALSE for set_light_on() to work.
+ eye.set_light_on(TRUE)
+ eye.set_light_range(clamp(new_range, 0, max_light_beam_distance))
+
+/**
+ * Setter function for the light's color
+ *
+ * Sets the light color of the attached flashlight object. Sets the eye color vars of this eye organ as well and then updates the mob's eye color.
+ * Arguments:
+ * * newcolor - the new color hex string to set
+ * * to_update - whether we are setting the color for the light beam itself, or the individual eyes
+ * * sanitize - whether the hex string should be sanitized
+ */
+/obj/item/organ/eyes/robotic/glow/proc/set_beam_color(newcolor, to_update = UPDATE_LIGHT, sanitize = FALSE)
+ var/newcolor_string
+ if(sanitize)
+ newcolor_string = sanitize_hexcolor(newcolor)
+ else
+ newcolor_string = newcolor
+ switch(to_update)
+ if(UPDATE_LIGHT)
+ light_color_string = newcolor_string
+ eye.set_light_color(newcolor_string)
+ if(UPDATE_EYES_LEFT)
+ left_eye_color_string = newcolor_string
+ if(UPDATE_EYES_RIGHT)
+ right_eye_color_string = newcolor_string
+
+ update_mob_eye_color()
+
+/**
+ * Toggle the attached flashlight object on or off
+ */
+/obj/item/organ/eyes/robotic/glow/proc/toggle_active()
+ if(eye.light_on)
+ deactivate()
+ else
+ activate()
+
+/**
+ * Toggles for the eye color mode
+ *
+ * Toggles the eye color mode on or off and then calls an update on the mob's eye color
+ */
+/obj/item/organ/eyes/robotic/glow/proc/toggle_eye_color_mode()
+ eye_color_mode = !eye_color_mode
+ update_mob_eye_color()
+
+/**
+ * Updates the mob eye color
+ *
+ * Updates the eye color to reflect on the mob's body if it's possible to do so
+ * Arguments:
+ * * mob/living/carbon/eye_owner - the mob to update the eye color appearance of
+ */
+/obj/item/organ/eyes/robotic/glow/proc/update_mob_eye_color(mob/living/carbon/eye_owner = owner)
+ switch(eye_color_mode)
+ if(MATCH_LIGHT_COLOR)
+ eye_color_left = light_color_string
+ eye_color_right = light_color_string
+ if(USE_CUSTOM_COLOR)
+ eye_color_left = left_eye_color_string
+ eye_color_right = right_eye_color_string
+
+ if(QDELETED(eye_owner) || !ishuman(eye_owner)) //Other carbon mobs don't have eye color.
+ return
+
+ var/obj/item/bodypart/head/head = eye_owner.get_bodypart(BODY_ZONE_HEAD) //if we have eyes we definently have a head anyway
+ var/previous_flags = head.head_flags
+ head.head_flags |= HEAD_EYECOLOR
+
+ ///enabling and disabling the TRAIT_LUMINESCENT_EYES trait already calls handle_eyes(), in that case, let's skip that call
+ var/skip_call = FALSE
+ if(!eye.light_on)
+ eye_icon_state = initial(eye_icon_state)
+ skip_call = HAS_TRAIT_FROM_ONLY(eye_owner, TRAIT_LUMINESCENT_EYES, REF(src))
+ remove_organ_trait(TRAIT_LUMINESCENT_EYES)
+ else
+ skip_call = !HAS_TRAIT(eye_owner, TRAIT_LUMINESCENT_EYES)
+ add_organ_trait(TRAIT_LUMINESCENT_EYES)
+ eye_icon_state = base_eye_state
+
+ if(!skip_call && ishuman(eye_owner))
+ var/mob/living/carbon/human/humie = eye_owner
+ humie.update_eyes()
+
+ head.head_flags = previous_flags
+
+#undef MATCH_LIGHT_COLOR
+#undef USE_CUSTOM_COLOR
+#undef UPDATE_LIGHT
+#undef UPDATE_EYES_LEFT
+#undef UPDATE_EYES_RIGHT
+
+// Moth variations
+
+/obj/item/organ/eyes/robotic/moth
+ name = "robotic moth eyes"
+ desc = "Your vision is augmented. Much like actual moth eyes, very sensitive to bright lights."
+ icon_state = "eyes_moth_cyber"
+ eye_icon_state = "motheyes_cyber"
+ flash_protect = FLASH_PROTECTION_SENSITIVE
+ pupils_name = "aperture clusters"
+ penlight_message = "are metal hemispheres, resembling insect eyes"
+
+/obj/item/organ/eyes/robotic/basic/moth
+ name = "basic robotic moth eyes"
+ icon_state = "eyes_moth_cyber_basic"
+ eye_icon_state = "motheyes_white"
+ eye_color_left = "#65686f"
+ eye_color_right = "#65686f"
+ blink_animation = FALSE
+ flash_protect = FLASH_PROTECTION_SENSITIVE
+ pupils_name = "aperture clusters"
+ penlight_message = "are metal hemispheres, resembling insect eyes"
+
+/obj/item/organ/eyes/robotic/xray/moth
+ name = "moth x-ray eyes"
+ desc = "These cybernetic imitation moth eyes will give you X-ray vision. Blinking is futile. Much like actual moth eyes, very sensitive to bright lights."
+ icon_state = "eyes_moth_cyber_xray"
+ eye_icon_state = "motheyes_white"
+ eye_color_left = "#3c4e52"
+ eye_color_right = "#3c4e52"
+ blink_animation = FALSE
+ flash_protect = FLASH_PROTECTION_HYPER_SENSITIVE
+ pupils_name = "aperture clusters"
+
+/obj/item/organ/eyes/robotic/shield/moth
+ name = "shielded robotic moth eyes"
+ icon_state = "eyes_moth_cyber_shield"
+ eye_icon_state = "motheyes_white"
+ eye_color_left = "#353845"
+ eye_color_right = "#353845"
+ blink_animation = FALSE
+ pupils_name = "aperture clusters"
+ penlight_message = "have shutters, protecting insectoid compound eyes."
+
+/obj/item/organ/eyes/robotic/glow/moth
+ name = "high luminosity moth eyes"
+ desc = "Special glowing eyes, to be one with the lamp. Much like actual moth eyes, very sensitive to bright lights."
+ icon_state = "eyes_moth_cyber_glow"
+ eye_icon_state = "motheyes_cyber"
+ iris_overlay = "eyes_moth_cyber_glow_iris"
+ blink_animation = FALSE
+ base_eye_state = "eyes_mothglow"
+ flash_protect = FLASH_PROTECTION_SENSITIVE
+ penlight_message = "are bulbous clusters of LEDs and cameras"
+ pupils_name = "aperture clusters"
+
+/obj/item/organ/eyes/robotic/thermals/moth
+ name = "thermal moth eyes"
+ icon_state = "eyes_moth_cyber_thermal"
+ eye_icon_state = "motheyes_white"
+ eye_color_left = "#901f38"
+ eye_color_right = "#901f38"
+ blink_animation = FALSE
+ flash_protect = FLASH_PROTECTION_HYPER_SENSITIVE
+ pupils_name = "sensor clusters"
+ penlight_message = "are two clustered hemispheres of thermal sensors"
+
+// Chaplain's special boy
+/obj/item/organ/eyes/night_vision/maintenance_adapted
+ name = "adapted eyes"
+ desc = "These red eyes look like two foggy marbles. They give off a particularly worrying glow in the dark."
+ icon_state = "eyes_adapted"
+ eye_color_left = "#f74a4d"
+ eye_color_right = "#f74a4d"
+ eye_icon_state = "eyes_glow"
+ iris_overlay = null
+ organ_traits = list(TRAIT_UNNATURAL_RED_GLOWY_EYES, TRAIT_LUMINESCENT_EYES)
+ flash_protect = FLASH_PROTECTION_HYPER_SENSITIVE
+ low_light_cutoff = list(5, 12, 20)
+ medium_light_cutoff = list(15, 20, 30)
+ high_light_cutoff = list(30, 35, 50)
+ penlight_message = "glow a foggy red, sizzling under the light!"
+
+/obj/item/organ/eyes/night_vision/maintenance_adapted/penlight_examine(mob/living/viewer, obj/item/examtool)
+ if(!owner.is_blind())
+ to_chat(owner, span_danger("Your eyes sizzle agonizingly as light is shone on them!"))
+ apply_organ_damage(20 * examtool.light_power) //that's 0.5 lightpower for a penlight, so one penlight shining is equivalent to two seconds in a lit area
+ return span_danger("[owner.p_Their()] eyes [penlight_message]")
+
+/obj/item/organ/eyes/night_vision/maintenance_adapted/on_life(seconds_per_tick)
+ if(owner.get_eye_protection() <= FLASH_PROTECTION_SENSITIVE && !owner.is_blind() && isturf(owner.loc) && owner.has_light_nearby(light_amount=0.5)) //we allow a little more than usual so we can produce light from the adapted eyes
+ to_chat(owner, span_danger("Your eyes! They burn in the light!"))
+ apply_organ_damage(10) //blind quickly
+ playsound(owner, 'sound/machines/grill/grillsizzle.ogg', 50)
+ else
+ apply_organ_damage(-10) //heal quickly
+ return ..()
+
+#define VISOR_DISPLAY_CROSS "Cross"
+#define VISOR_DISPLAY_EYES "Eyes"
+#define VISOR_DISPLAY_LINE "Line"
+
+#define IFF_HOSTILE 1
+#define IFF_NEUTRAL 2
+#define IFF_FRIENDLY 3
+
+#define IFF_FACTION_NONE "None"
+#define IFF_FACTION_SYNDICATE "Syndicate"
+#define IFF_FACTION_CREW "Crew"
+#define IFF_FACTION_SEC_COMMAND "Security & Command"
+#define IFF_FACTION_CENTCOM "CentCom"
+#define IFF_FACTION_EVERYONE "Non-Allies"
+
+/obj/item/organ/eyes/robotic/tacvisor
+ name = "tactical EFF visor"
+ desc = "A failed attempt at integrating IFF systems directly into soldiers' prefrontal cortex, this complex sensor array has proved to be impractical as the additional load impared the user's ability to recognize people's appearances or voices. The screen is there just for intimidation."
+ icon_state = "eyes_tacvisor"
+ eye_icon_state = "eyes_tacvisor"
+ blink_animation = FALSE
+ no_glasses = TRUE
+ iris_overlay = null
+ eye_color_left = COLOR_WHITE
+ eye_color_right = COLOR_WHITE
+ flash_protect = FLASH_PROTECTION_WELDER
+ lighting_cutoff = LIGHTING_CUTOFF_MEDIUM
+ pupils_name = "faceplate"
+ penlight_message = "are a wide reinforced faceplate with an inbuilt screen and a multitude of combat sensors"
+ light_reactive = FALSE
+ actions_types = list(/datum/action/item_action/organ_action/use)
+ /// Used to detect when unmasked mobs enter range
+ var/datum/proximity_monitor/tacvisor/proximity_monitor
+ /// List of mob refs -> their overlays
+ var/list/mob_overlays = list()
+ /// List of mobs in our direct view range who we need to update dynamically
+ var/list/direct_view_tracking = list()
+ /// Current visual displayed on the screen
+ var/visor_display = VISOR_DISPLAY_CROSS
+ /// Allied faction highlight
+ var/friendly_faction = IFF_FACTION_SEC_COMMAND
+ /// Hostile faction highlight
+ var/hostile_faction = IFF_FACTION_NONE
+ /// Threat flags for hostile detection, if any are chosen
+ var/threat_flags = JUDGE_IDCHECK | JUDGE_WEAPONCHECK | JUDGE_RECORDCHECK
+ /// Can the user change the parameters?
+ var/user_controls = TRUE
+
+ /// Valid options for friendly factions
+ var/static/list/valid_friendly_factions = list(
+ IFF_FACTION_NONE,
+ IFF_FACTION_CREW,
+ IFF_FACTION_SEC_COMMAND,
+ IFF_FACTION_CENTCOM,
+ IFF_FACTION_SYNDICATE,
+ )
+ /// Valid options for hostile factions
+ var/static/list/valid_hostile_factions = list(
+ IFF_FACTION_NONE,
+ IFF_FACTION_CREW,
+ IFF_FACTION_SEC_COMMAND,
+ IFF_FACTION_CENTCOM,
+ IFF_FACTION_SYNDICATE,
+ IFF_FACTION_EVERYONE,
+ )
+ /// Threat flags and their tooltips
+ var/static/list/threat_flag_options = list(
+ "Valid ID" = JUDGE_IDCHECK,
+ "Weapon Permit" = JUDGE_WEAPONCHECK,
+ "Security Status" = JUDGE_RECORDCHECK,
+ )
+
+/obj/item/organ/eyes/robotic/tacvisor/generate_body_overlay(mob/living/carbon/human/parent, obj/item/bodypart/limb)
+ var/mutable_appearance/visor_overlay = mutable_appearance(eye_icon, eye_icon_state, -EYES_LAYER, parent || limb)
+ var/list/eye_overlays = list(visor_overlay)
+
+ if (parent && parent.appears_alive() && !HAS_TRAIT(parent, TRAIT_KNOCKEDOUT))
+ var/mutable_appearance/display_overlay = mutable_appearance(eye_icon, "[eye_icon_state]_[LOWER_TEXT(visor_display)]", -EYES_LAYER, parent)
+ eye_overlays += display_overlay
+ if(!(parent.obscured_slots & HIDEEYES))
+ eye_overlays += emissive_appearance(eye_icon, "[eye_icon_state]_[LOWER_TEXT(visor_display)]", parent, -EYES_LAYER)
+
+ if(!limb)
+ return eye_overlays
+
+ var/obj/item/bodypart/head/head = astype(limb, /obj/item/bodypart/head)
+ if(head?.worn_face_offset)
+ for (var/mutable_appearance/overlay as anything in eye_overlays)
+ head?.worn_face_offset.apply_offset(overlay)
+
+ return eye_overlays
+
+// Hides and mutes all people on the screen
+/obj/item/organ/eyes/robotic/tacvisor/emag_act(mob/user, obj/item/card/emag/emag_card)
+ if(obj_flags & EMAGGED)
+ return FALSE
+ obj_flags |= EMAGGED
+ balloon_alert(user, "perceptual scanners overriden")
+ return TRUE
+
+/obj/item/organ/eyes/robotic/tacvisor/on_mob_insert(mob/living/carbon/receiver, special, movement_flags)
+ . = ..()
+ receiver.add_client_colour(/datum/client_colour/tacvisor, REF(src))
+ receiver.apply_status_effect(/datum/status_effect/grouped/see_no_names, REF(src))
+ proximity_monitor = new(receiver, 9)
+ proximity_monitor.owner = src
+
+ RegisterSignal(receiver, COMSIG_MOB_CLIENT_LOGIN, PROC_REF(on_login))
+ RegisterSignal(receiver, COMSIG_MOVABLE_PRE_HEAR, PROC_REF(on_hear))
+ RegisterSignal(receiver, COMSIG_MOB_EXAMINING, PROC_REF(on_examine))
+ if (receiver.client)
+ create_illusions(receiver)
+
+/obj/item/organ/eyes/robotic/tacvisor/on_mob_remove(mob/living/carbon/organ_owner, special, movement_flags)
+ . = ..()
+ UnregisterSignal(organ_owner, list(COMSIG_MOB_CLIENT_LOGIN, COMSIG_MOVABLE_PRE_HEAR, COMSIG_MOB_EXAMINING))
+ organ_owner.remove_client_colour(REF(src))
+ organ_owner.remove_status_effect(/datum/status_effect/grouped/see_no_names, REF(src))
+ QDEL_NULL(proximity_monitor)
+ organ_owner.client?.images -= assoc_to_values(mob_overlays)
+ for (var/mob/living/thing as anything in mob_overlays)
+ UnregisterSignal(thing, list(
+ COMSIG_MOVABLE_Z_CHANGED,
+ COMSIG_QDELETING,
+ COMSIG_ATOM_UPDATE_APPEARANCE,
+ COMSIG_LIVING_POST_UPDATE_TRANSFORM,
+ COMSIG_MOB_EQUIPPED_ITEM,
+ COMSIG_MOB_UNEQUIPPED_ITEM,
+ COMSIG_LIVING_UPDATE_OFFSETS,
+ ))
+ mob_overlays.Cut()
+ direct_view_tracking.Cut()
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/on_hear(datum/source, list/hearing_args)
+ SIGNAL_HANDLER
+
+ if (hearing_args[HEARING_SPEAKER] == owner || !isliving(hearing_args[HEARING_SPEAKER]))
+ return
+
+ if (obj_flags & EMAGGED)
+ return COMSIG_MOVABLE_CANCEL_HEARING
+
+ var/list/message_mods = hearing_args[HEARING_MESSAGE_MODE]
+ message_mods[MODE_SPEAKER_NAME_OVERRIDE] = "Unknown"
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/on_login(mob/living/source, client/user_client)
+ SIGNAL_HANDLER
+
+ if (!length(mob_overlays))
+ create_illusions(source)
+ else
+ user_client.images |= assoc_to_values(mob_overlays)
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/create_illusions(mob/living/user)
+ for(var/mob/living/target as anything in ((obj_flags & EMAGGED) ? GLOB.mob_living_list : GLOB.carbon_list))
+ if (target == user)
+ continue
+
+ if (get_dist(user, target) <= proximity_monitor.current_range)
+ on_entered(target)
+ else
+ refresh_overlay(target)
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/on_mob_delete(mob/living/source)
+ SIGNAL_HANDLER
+ owner.client?.images -= mob_overlays[source]
+ mob_overlays -= source
+ direct_view_tracking -= source
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/refresh_overlay(mob/living/source)
+ SIGNAL_HANDLER
+
+ if (mob_overlays[source])
+ owner.client?.images -= mob_overlays[source]
+ else
+ RegisterSignal(source, COMSIG_QDELETING, PROC_REF(on_mob_delete))
+ RegisterSignal(source, COMSIG_ATOM_UPDATE_APPEARANCE, PROC_REF(refresh_overlay))
+ RegisterSignal(source, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_z_change))
+ // Lying down/being pushed
+ RegisterSignal(source, COMSIG_LIVING_POST_UPDATE_TRANSFORM, PROC_REF(refresh_overlay))
+ RegisterSignal(source, COMSIG_LIVING_UPDATE_OFFSETS, PROC_REF(refresh_overlay))
+
+ mob_overlays[source] = make_overlay(source)
+ owner.client?.images |= mob_overlays[source]
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/on_z_change(mob/living/source)
+ SIGNAL_HANDLER
+ var/image/overlay = mob_overlays[source]
+ SET_PLANE_EXPLICIT(overlay, ABOVE_GAME_PLANE, source)
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/make_overlay(mob/living/target)
+ if (obj_flags & EMAGGED)
+ var/image/overlay_image = image(mutable_appearance('icons/effects/effects.dmi', "nothing"), target)
+ overlay_image.name = "Unknown"
+ overlay_image.override = TRUE
+ overlay_image.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ SET_PLANE_EXPLICIT(overlay_image, ABOVE_GAME_PLANE, target)
+ return overlay_image
+
+ var/mutable_appearance/appearance_copy = new(target.appearance)
+ appearance_copy.appearance_flags |= KEEP_APART|KEEP_TOGETHER
+ var/outline_color = "#FFD500CC"
+ switch (get_iff_signature(target))
+ if (IFF_FRIENDLY)
+ outline_color = "#0099FFCC"
+ if (IFF_HOSTILE)
+ outline_color = "#FF0022CC"
+ appearance_copy.add_filter("target_lock_outline", 2, outline_filter(1, outline_color))
+ var/mutable_appearance/static_effect = mutable_appearance('icons/effects/effects.dmi', "static_base")
+ static_effect.color = "#373642"
+ static_effect.blend_mode = BLEND_INSET_OVERLAY
+ appearance_copy.overlays += static_effect
+ appearance_copy.override = TRUE
+ var/image/overlay_image = image(appearance_copy, target)
+ overlay_image.name = "Unknown"
+ overlay_image.override = TRUE
+ SET_PLANE_EXPLICIT(overlay_image, ABOVE_GAME_PLANE, target)
+ return overlay_image
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/get_iff_signature(mob/living/target)
+ . = IFF_NEUTRAL
+ if (hostile_faction == IFF_FACTION_EVERYONE)
+ . = IFF_HOSTILE
+
+ if (!istype(target))
+ return .
+
+ // This hasn't been used for like, over a decade, but by god will I make it great again
+ var/lasercolor = null
+ if (istype(owner.get_item_by_slot(ITEM_SLOT_OCLOTHING), /obj/item/clothing/suit/redtag) || istype(owner.get_item_by_slot(ITEM_SLOT_BELT), /obj/item/gun/energy/laser/redtag) || owner.is_holding_item_of_type(/obj/item/gun/energy/laser/redtag))
+ lasercolor = "r"
+ else if (istype(owner.get_item_by_slot(ITEM_SLOT_OCLOTHING), /obj/item/clothing/suit/bluetag) || istype(owner.get_item_by_slot(ITEM_SLOT_BELT), /obj/item/gun/energy/laser/bluetag) || owner.is_holding_item_of_type(/obj/item/gun/energy/laser/bluetag))
+ lasercolor = "b"
+
+ if (threat_flags && target.assess_threat(threat_flags, lasercolor) >= THREAT_ASSESS_DANGEROUS)
+ return IFF_HOSTILE
+
+ var/obj/item/card/id/idcard = target.get_idcard()
+ if (!istype(idcard))
+ return .
+
+ // Ignores RETA access so we directly access access instead of using the wrapper
+ if (ACCESS_SYNDICATE in idcard.access)
+ if (hostile_faction == IFF_FACTION_SYNDICATE)
+ return IFF_HOSTILE
+ if (friendly_faction == IFF_FACTION_SYNDICATE)
+ return IFF_FRIENDLY
+
+ if (istype(idcard.trim, /datum/id_trim/job))
+ // Cham cards get a pass
+ if (hostile_faction == IFF_FACTION_CREW && idcard.trim?.threat_modifier >= 0)
+ return IFF_HOSTILE
+ if (friendly_faction == IFF_FACTION_CREW)
+ return IFF_FRIENDLY
+
+ if ((ACCESS_COMMAND in idcard.access) || (ACCESS_SECURITY in idcard.access))
+ if (hostile_faction == IFF_FACTION_SEC_COMMAND)
+ return IFF_HOSTILE
+ if (friendly_faction == IFF_FACTION_SEC_COMMAND)
+ return IFF_FRIENDLY
+
+ if (ACCESS_CENT_GENERAL in idcard.access)
+ if (hostile_faction == IFF_FACTION_CENTCOM)
+ return IFF_HOSTILE
+ if (friendly_faction == IFF_FACTION_CENTCOM)
+ return IFF_FRIENDLY
+
+ return .
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/on_examine(mob/source, atom/target, list/examine_strings, list/examine_overrides)
+ SIGNAL_HANDLER
+
+ if (target == owner || !iscarbon(target) && !(isliving(target) && (obj_flags & EMAGGED)))
+ return
+
+ var/list/override_strings = list(span_warning("You're struggling to make out any details..."))
+
+ if (!threat_flags && !(obj_flags & EMAGGED))
+ examine_overrides[EXAMINE_OVERRIDE_PRIORITY_IFF] = override_strings
+ return
+
+ var/lasercolor = null
+ var/mob/living/victim = target
+ if (istype(owner.get_item_by_slot(ITEM_SLOT_OCLOTHING), /obj/item/clothing/suit/redtag) || istype(owner.get_item_by_slot(ITEM_SLOT_BELT), /obj/item/gun/energy/laser/redtag) || owner.is_holding_item_of_type(/obj/item/gun/energy/laser/redtag))
+ lasercolor = "r"
+ else if (istype(owner.get_item_by_slot(ITEM_SLOT_OCLOTHING), /obj/item/clothing/suit/bluetag) || istype(owner.get_item_by_slot(ITEM_SLOT_BELT), /obj/item/gun/energy/laser/bluetag) || owner.is_holding_item_of_type(/obj/item/gun/energy/laser/bluetag))
+ lasercolor = "b"
+
+ var/threat_level = victim.assess_threat(threat_flags, lasercolor)
+ switch (threat_level)
+ if (THREAT_ASSESS_MAXIMUM to INFINITY)
+ override_strings += span_boldwarning("Assessed threat level of [threat_level]! Extreme danger of criminal activity!")
+ if (THREAT_ASSESS_DANGEROUS to THREAT_ASSESS_MAXIMUM)
+ override_strings += span_warning("Assessed threat level of [threat_level]. Criminal scum detected!")
+ if (1 to THREAT_ASSESS_DANGEROUS)
+ override_strings += span_notice("Assessed threat level of [threat_level]. Probably not dangerous... yet.")
+ else
+ override_strings += span_notice("Seems to be a trustworthy individual.")
+
+ examine_overrides[EXAMINE_OVERRIDE_PRIORITY_IFF] = override_strings
+
+/obj/item/organ/eyes/robotic/tacvisor/ui_state(mob/user)
+ return GLOB.default_state
+
+/obj/item/organ/eyes/robotic/tacvisor/ui_status(mob/user, datum/ui_state/state)
+ if(!QDELETED(owner))
+ if(owner == user && user_controls)
+ return min(
+ ui_status_user_is_abled(user, src),
+ ui_status_only_living(user),
+ )
+ return UI_CLOSE
+ return ..()
+
+/obj/item/organ/eyes/robotic/tacvisor/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "TacVisorEyesMenu")
+ ui.open()
+
+/obj/item/organ/eyes/robotic/tacvisor/ui_data(mob/user)
+ var/list/data = list()
+ data["friendlyFaction"] = friendly_faction
+ data["hostileFaction"] = hostile_faction
+ data["visorDisplay"] = visor_display
+ data["threatFlags"] = threat_flags
+ return data
+
+/obj/item/organ/eyes/robotic/tacvisor/ui_static_data(mob/user)
+ var/list/data = list()
+ data["validFriendlyFactions"] = valid_friendly_factions
+ data["validHostileFactions"] = valid_hostile_factions
+ data["visorOptions"] = list(VISOR_DISPLAY_CROSS, VISOR_DISPLAY_EYES, VISOR_DISPLAY_LINE)
+ data["threatOptions"] = threat_flag_options
+ return data
+
+/obj/item/organ/eyes/robotic/tacvisor/ui_act(action, list/params, datum/tgui/ui)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if ("set_friendly")
+ var/chosen_faction = params["faction"]
+ if (!(chosen_faction in valid_friendly_factions))
+ chosen_faction = IFF_FACTION_NONE
+ friendly_faction = chosen_faction
+ update_mob_overlays()
+
+ if ("set_hostile")
+ var/chosen_faction = params["faction"]
+ if (!(chosen_faction in valid_hostile_factions))
+ chosen_faction = IFF_FACTION_NONE
+ hostile_faction = chosen_faction
+ update_mob_overlays()
+
+ if ("set_threat_flags")
+ threat_flags = text2num(params["threat_flags"])
+ update_mob_overlays()
+
+ if ("set_display")
+ visor_display = params["display"]
+ if (visor_display != VISOR_DISPLAY_EYES && visor_display != VISOR_DISPLAY_LINE && visor_display != VISOR_DISPLAY_CROSS)
+ visor_display = VISOR_DISPLAY_CROSS
+ owner?.update_body()
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/update_mob_overlays()
+ for (var/mob/living/target as anything in direct_view_tracking)
+ refresh_overlay(target)
+
+/obj/item/organ/eyes/robotic/tacvisor/ui_action_click(mob/user, action)
+ if(istype(action, /datum/action/item_action/organ_action/use) && user_controls)
+ ui_interact(user)
+
+/obj/item/organ/eyes/robotic/tacvisor/multitool_act(mob/living/user, obj/item/tool)
+ ui_interact(user)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/item/organ/eyes/robotic/tacvisor/screwdriver_act(mob/living/user, obj/item/tool)
+ user_controls = !user_controls
+ balloon_alert(user, "user controls [user_controls ? "enabled" : "disabled"]")
+ if (user_controls)
+ add_item_action(/datum/action/item_action/organ_action/use)
+ else
+ for(var/datum/action/action as anything in actions)
+ remove_item_action(action)
+ return ITEM_INTERACT_SUCCESS
+
+/obj/item/organ/eyes/robotic/tacvisor/examine(mob/user)
+ . = ..()
+ . += span_notice("Its settings can be changed with a [EXAMINE_HINT("multitool")].")
+ . += span_notice("User configuration switch is currently in the [user_controls ? "on" : "off"] position, and could be flipped wtih a [EXAMINE_HINT("screwdriver")].")
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/on_entered(mob/living/source)
+ if (source in direct_view_tracking)
+ return
+
+ refresh_overlay(source)
+ direct_view_tracking += source
+ // Track equipping/unequipping items for threat levels/ID identity
+ RegisterSignal(source, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(check_equippped_item))
+ RegisterSignal(source, COMSIG_MOB_UNEQUIPPED_ITEM, PROC_REF(refresh_overlay)) // Can't see the slot of the dropped item so we need to blanket update
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/check_equippped_item(mob/living/source, obj/item/equipped_item, slot)
+ SIGNAL_HANDLER
+
+ // Anywhere where an ID or a gun would count
+ if (slot & (ITEM_SLOT_BELT | ITEM_SLOT_ID | ITEM_SLOT_HANDS | ITEM_SLOT_BACK))
+ refresh_overlay(source)
+
+/obj/item/organ/eyes/robotic/tacvisor/proc/on_exited(mob/living/source)
+ if (!(source in direct_view_tracking))
+ return
+
+ direct_view_tracking -= source
+ UnregisterSignal(source, list(
+ COMSIG_MOB_EQUIPPED_ITEM,
+ COMSIG_MOB_UNEQUIPPED_ITEM,
+ ))
+
+/datum/proximity_monitor/tacvisor
+ var/obj/item/organ/eyes/robotic/tacvisor/owner
+
+/datum/proximity_monitor/tacvisor/Destroy()
+ owner = null
+ return ..()
+
+/datum/proximity_monitor/tacvisor/on_moved(atom/movable/source, atom/old_loc)
+ return
+
+/datum/proximity_monitor/tacvisor/on_entered(atom/source, atom/movable/arrived, turf/old_loc)
+ if (arrived != host && (iscarbon(arrived) || isliving(arrived) && (owner.obj_flags & EMAGGED)))
+ owner.on_entered(arrived)
+
+/datum/proximity_monitor/tacvisor/on_uncrossed/on_uncrossed(turf/old_location, mob/exited, direction)
+ if (exited != host && (iscarbon(exited) || isliving(exited) && (owner.obj_flags & EMAGGED)) && get_dist(exited, host) > current_range)
+ owner.on_exited(exited)
+
+/datum/proximity_monitor/tacvisor/on_initialized(turf/location, atom/created, init_flags)
+ if (created != host && (iscarbon(created) || isliving(created) && (owner.obj_flags & EMAGGED)))
+ owner.on_entered(created)
+
+/datum/client_colour/tacvisor
+ priority = CLIENT_COLOR_ORGAN_PRIORITY
+
+/datum/client_colour/tacvisor/New(mob/owner)
+ . = ..()
+ color = color_matrix_filter(list(
+ 1, 0, 0,
+ 0, 1.75, 0,
+ 0, 0, 0.75,
+ 0, -0.75, 0,
+ ), COLORSPACE_HSL)
+
+/obj/item/organ/eyes/robotic/tacvisor/deathsquad
+ friendly_faction = IFF_FACTION_CENTCOM
+ hostile_faction = IFF_FACTION_EVERYONE
+ actions_types = null
+ user_controls = FALSE
+
+/obj/item/organ/eyes/robotic/tacvisor/deathsquad/ui_status(mob/user, datum/ui_state/state)
+ return UI_CLOSE
+
+#undef VISOR_DISPLAY_CROSS
+#undef VISOR_DISPLAY_EYES
+#undef VISOR_DISPLAY_LINE
+
+#undef IFF_HOSTILE
+#undef IFF_NEUTRAL
+#undef IFF_FRIENDLY
+
+#undef IFF_FACTION_NONE
+#undef IFF_FACTION_SYNDICATE
+#undef IFF_FACTION_CREW
+#undef IFF_FACTION_SEC_COMMAND
+#undef IFF_FACTION_CENTCOM
+#undef IFF_FACTION_EVERYONE
diff --git a/code/modules/surgery/organs/internal/eyes/eyes_species.dm b/code/modules/surgery/organs/internal/eyes/eyes_species.dm
new file mode 100644
index 000000000000..7c3fc298330f
--- /dev/null
+++ b/code/modules/surgery/organs/internal/eyes/eyes_species.dm
@@ -0,0 +1,113 @@
+/obj/item/organ/eyes/night_vision/mushroom
+ name = "fung-eye"
+ desc = "While on the outside they look inert and dead, the eyes of mushroom people are actually very advanced."
+ low_light_cutoff = list(0, 15, 20)
+ medium_light_cutoff = list(0, 20, 35)
+ high_light_cutoff = list(0, 40, 50)
+ pupils_name = "photosensory openings"
+ penlight_message = "are attached to fungal stalks"
+
+/obj/item/organ/eyes/zombie
+ name = "undead eyes"
+ desc = "Somewhat counterintuitively, these half-rotten eyes actually have superior vision to those of a living human."
+ color_cutoffs = list(25, 35, 5)
+ penlight_message = "are rotten and decayed!"
+
+/obj/item/organ/eyes/zombie/penlight_examine(mob/living/viewer, obj/item/examtool)
+ return span_danger("[owner.p_Their()] eyes [penlight_message]")
+
+/obj/item/organ/eyes/alien
+ name = "alien eyes"
+ desc = "It turned out they had them after all!"
+ sight_flags = SEE_MOBS
+ color_cutoffs = list(25, 5, 42)
+
+/obj/item/organ/eyes/golem
+ name = "resonating crystal"
+ desc = "Golems somehow measure external light levels and detect nearby ore using this sensitive mineral lattice."
+ icon_state = "adamantine_cords"
+ eye_icon_state = null
+ blink_animation = FALSE
+ iris_overlay = null
+ color = COLOR_GOLEM_GRAY
+ visual = FALSE
+ organ_flags = ORGAN_MINERAL
+ color_cutoffs = list(10, 15, 5)
+ actions_types = list(/datum/action/cooldown/golem_ore_sight)
+ penlight_message = "glimmer, their crystaline structure refracting light inwards"
+ pupils_name = "lensing gems" // Given it says these are a "mineral lattice" that collects light i assume they work like artifical ruby laser foci
+
+/// Send an ore detection pulse on a cooldown
+/datum/action/cooldown/golem_ore_sight
+ name = "Ore Resonance"
+ desc = "Causes nearby ores to vibrate, revealing their location."
+ button_icon = 'icons/obj/devices/scanner.dmi'
+ button_icon_state = "manual_mining"
+ check_flags = AB_CHECK_CONSCIOUS
+ cooldown_time = 10 SECONDS
+
+/datum/action/cooldown/golem_ore_sight/Activate(atom/target)
+ . = ..()
+ mineral_scan_pulse(get_turf(target), scanner = target)
+
+/obj/item/organ/eyes/moth
+ name = "moth eyes"
+ desc = "These eyes seem to have increased sensitivity to bright light, with no improvement to low light vision."
+ icon_state = "eyes_moth"
+ eye_icon_state = "motheyes"
+ blink_animation = FALSE
+ iris_overlay = null
+ flash_protect = FLASH_PROTECTION_SENSITIVE
+ pupils_name = "ommatidia" //yes i know compound eyes have no pupils shut up
+ penlight_message = "are bulbous and insectoid"
+
+/obj/item/organ/eyes/ghost
+ name = "ghost eyes"
+ desc = "Despite lacking pupils, these can see pretty well."
+ icon_state = "eyes-ghost"
+ blink_animation = FALSE
+ movement_type = PHASING
+ organ_flags = parent_type::organ_flags | ORGAN_GHOST
+
+/obj/item/organ/eyes/snail
+ name = "snail eyes"
+ desc = "These eyes seem to have a large range, but might be cumbersome with glasses."
+ icon_state = "eyes_snail"
+ eye_icon_state = "snail_eyes"
+ blink_animation = FALSE
+ pupils_name = "eyestalks" //many species of snails can retract their eyes into their face! (my lame science excuse for not having better writing here)
+ penlight_message = "are sat upon retractable tentacles"
+
+/obj/item/organ/eyes/jelly
+ name = "jelly eyes"
+ desc = "These eyes are made of a soft jelly. Unlike all other eyes, though, there are three of them."
+ icon_state = "eyes_jelly"
+ eye_icon_state = "jelleyes"
+ blink_animation = FALSE
+ iris_overlay = null
+ pupils_name = "lensing bubbles" //imagine a water lens physics demo but with goo. thats how these work.
+ penlight_message = "are three bubbles of refractive jelly"
+
+/obj/item/organ/eyes/lizard
+ name = "reptile eyes"
+ desc = "A pair of reptile eyes with thin vertical slits for pupils."
+ icon_state = "lizard_eyes"
+ synchronized_blinking = FALSE
+ pupils_name = "slit pupils"
+ penlight_message = "have vertically slit pupils and tinted whites"
+
+/obj/item/organ/eyes/pod
+ name = "pod eyes"
+ desc = "Strangest salad you've ever seen."
+ icon_state = "eyes_pod"
+ eye_color_left = "#375846"
+ eye_color_right = "#375846"
+ iris_overlay = null
+ foodtype_flags = PODPERSON_ORGAN_FOODTYPES
+ penlight_message = "are green and plant-like"
+
+/obj/item/organ/eyes/felinid
+ name = "felinid eyes"
+ desc = "A pair of highly reflective eyes with slit pupils, like those of a cat."
+ pupils_name = "slit pupils"
+ penlight_message = "shine under the pearly light"
diff --git a/code/modules/transport/tram/tram_controller.dm b/code/modules/transport/tram/tram_controller.dm
index 4194493f27d9..a9e52078fe29 100644
--- a/code/modules/transport/tram/tram_controller.dm
+++ b/code/modules/transport/tram/tram_controller.dm
@@ -270,7 +270,7 @@
set_status_code(EMERGENCY_STOP, FALSE)
playsound(paired_cabinet, 'sound/machines/synth/synth_yes.ogg', 40, vary = FALSE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
paired_cabinet.say("Controller reset.")
-
+ nav_beacon.tram_loop.start()
for(var/obj/structure/transport/linear/tram/transport_module as anything in transport_modules) //only thing everyone needs to know is the new location.
if(transport_module.travelling) //wee woo wee woo there was a double action queued. damn multi tile structs
return //we don't care to undo cover_locked controls, though, as that will resolve itself
@@ -360,6 +360,7 @@
/datum/transport_controller/linear/tram/proc/normal_stop()
cycle_doors(CYCLE_OPEN)
log_transport("TC: [specific_transport_id] trip completed. Info: nav_pos ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z]) idle_pos ([destination_platform.x], [destination_platform.y], [destination_platform.z]).")
+ nav_beacon.tram_loop.stop()
addtimer(CALLBACK(src, PROC_REF(unlock_controls)), 2 SECONDS)
if((controller_status & SYSTEM_FAULT) && (nav_beacon.loc == destination_platform.loc)) //position matches between controller and tram, we're back on track
set_status_code(SYSTEM_FAULT, FALSE)
@@ -367,6 +368,29 @@
paired_cabinet.say("Controller reset.")
log_transport("TC: [specific_transport_id] position data successfully reset.")
idle_platform = destination_platform
+ var/our_channel = SSsounds.random_available_channel()
+ var/sound/jingle = sound(
+ idle_platform.arrival_sound,
+ FALSE,
+ 0,
+ our_channel,
+ 60
+ )
+ var/list/hearers = playsound(idle_platform, jingle, 50, FALSE, 0, extrarange= 10, ignore_walls = TRUE)
+ new /datum/threed_sound(
+ idle_platform,
+ jingle,
+ hearers,
+ FALSE,
+ 60,
+ SOUND_RANGE + 7,
+ 12 SECONDS,
+ our_channel,
+ null,
+ null,
+ SOUND_FALLOFF_EXPONENT,
+ 5
+ )
tram_registration.distance_travelled += (travel_trip_length - travel_remaining)
travel_trip_length = 0
current_speed = 0
@@ -379,6 +403,7 @@
/datum/transport_controller/linear/tram/proc/degraded_stop()
crash_fx()
log_transport("TC: [specific_transport_id] trip completed with a degraded status. Info: [TC_TS_STATUS] nav_pos ([nav_beacon.x], [nav_beacon.y], [nav_beacon.z]) idle_pos ([destination_platform.x], [destination_platform.y], [destination_platform.z]).")
+ nav_beacon.tram_loop.stop()
addtimer(CALLBACK(src, PROC_REF(unlock_controls)), 4 SECONDS)
if(controller_status & SYSTEM_FAULT)
set_status_code(SYSTEM_FAULT, FALSE)
@@ -418,7 +443,7 @@
var/throw_direction = travel_direction
for(var/obj/structure/transport/linear/tram/module in transport_modules)
module.estop_throw(throw_direction)
-
+ nav_beacon.tram_loop.stop()
addtimer(CALLBACK(src, PROC_REF(unlock_controls)), 4 SECONDS)
addtimer(CALLBACK(src, PROC_REF(cycle_doors), CYCLE_OPEN), 2 SECONDS)
idle_platform = null
diff --git a/code/modules/transport/tram/tram_controls.dm b/code/modules/transport/tram/tram_controls.dm
index 0cb859cc0ed2..1780b48944c1 100644
--- a/code/modules/transport/tram/tram_controls.dm
+++ b/code/modules/transport/tram/tram_controls.dm
@@ -16,6 +16,7 @@
light_color = COLOR_BLUE_LIGHT
light_range = 0 //we dont want to spam SSlighting with source updates every movement
brightness_on = 0
+ voice_filter = "highpass=f=300,lowpass=f=3500,aecho=0.8:0.9:70|140:0.3|0.15,alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000"
/// What sign face prefixes we have icons for
var/static/list/available_faces = list()
/// The sign face we're displaying
@@ -232,22 +233,26 @@
/obj/machinery/computer/tram_controls/proc/call_response(controller, list/relevant, response_code, response_info)
SIGNAL_HANDLER
- switch(response_code)
- if(REQUEST_SUCCESS)
- say("The next station is: [response_info]")
-
- if(REQUEST_FAIL)
- if(!LAZYFIND(relevant, src))
- return
-
- switch(response_info)
- if(NOT_IN_SERVICE)
- say("The tram is not in service. Please contact the nearest engineer.")
- if(INVALID_PLATFORM)
- say("Configuration error. Please contact the nearest engineer.")
- if(INTERNAL_ERROR)
- say("Tram controller error. Please contact the nearest engineer or crew member with telecommunications access to reset the controller.")
- else
+ var/datum/transport_controller/linear/tram/tram = transport_ref?.resolve()
+ if(tram)
+ if(SStts.tts_enabled)
+ tram.nav_beacon.voice = SStts.tram_voice
+ switch(response_code)
+ if(REQUEST_SUCCESS)
+ tram.nav_beacon.say("The next stop is: [response_info]")
+
+ if(REQUEST_FAIL)
+ if(!LAZYFIND(relevant, src))
return
+ switch(response_info)
+ if(NOT_IN_SERVICE)
+ tram.nav_beacon.say("The tram is not in service. Please contact the nearest engineer.")
+ if(INVALID_PLATFORM)
+ tram.nav_beacon.say("Configuration error. Please contact the nearest engineer.")
+ if(INTERNAL_ERROR)
+ tram.nav_beacon.say("Tram controller error. Please contact the nearest engineer or crew member with telecommunications access to reset the controller.")
+ else
+ return
+
MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/computer/tram_controls, 32)
diff --git a/code/modules/transport/tram/tram_floors.dm b/code/modules/transport/tram/tram_floors.dm
index 0d3f352acde4..e7486fa9bac3 100644
--- a/code/modules/transport/tram/tram_floors.dm
+++ b/code/modules/transport/tram/tram_floors.dm
@@ -232,7 +232,7 @@
/obj/structure/thermoplastic/update_icon_state()
. = ..()
var/ratio = atom_integrity / max_integrity
- ratio = CEILING(ratio * 4, 1) * 25
+ ratio = ceil(ratio * 4) * 25
if(ratio > 75)
icon_state = base_icon_state
return
@@ -324,7 +324,7 @@
. = ..()
if(throwforce && !is_cyborg) //do not want to divide by zero or show the message to borgs who can't throw
var/damage_value
- switch(CEILING(MAX_LIVING_HEALTH / throwforce, 1)) //throws to crit a human
+ switch(ceil(MAX_LIVING_HEALTH / throwforce)) //throws to crit a human
if(1 to 3)
damage_value = "superb"
if(4 to 6)
diff --git a/code/modules/transport/tram/tram_machinery.dm b/code/modules/transport/tram/tram_machinery.dm
index b4d4ac5db7a1..7e265e65fa03 100644
--- a/code/modules/transport/tram/tram_machinery.dm
+++ b/code/modules/transport/tram/tram_machinery.dm
@@ -1,4 +1,5 @@
/obj/item/assembly/control/transport
+ voice_filter = "highpass=f=300,lowpass=f=3500,aecho=0.8:0.9:70|140:0.3|0.15,alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000"
/// The ID of the tram we're linked to
var/specific_transport_id = TRAMSTATION_LINE_1
/// Options to be passed with the requests to the transport subsystem
@@ -47,7 +48,8 @@
SIGNAL_HANDLER
if(!LAZYFIND(relevant, src))
return
-
+ if(SStts.tts_enabled)
+ voice = SStts.tram_voice
switch(response_code)
if(REQUEST_SUCCESS)
say("The tram has been called to the platform.")
diff --git a/code/modules/transport/tram/tram_structures.dm b/code/modules/transport/tram/tram_structures.dm
index 70dcf92d7487..f596fd293dec 100644
--- a/code/modules/transport/tram/tram_structures.dm
+++ b/code/modules/transport/tram/tram_structures.dm
@@ -103,7 +103,7 @@
/obj/structure/tram/update_overlays(updates = ALL)
. = ..()
var/ratio = atom_integrity / max_integrity
- ratio = CEILING(ratio * 4, 1) * 25
+ ratio = ceil(ratio * 4) * 25
cut_overlay(damage_overlay)
if(ratio > 75)
return
diff --git a/code/modules/transport/transport_navigation.dm b/code/modules/transport/transport_navigation.dm
index 3b5c73b5de1a..6695772cb2f6 100644
--- a/code/modules/transport/transport_navigation.dm
+++ b/code/modules/transport/transport_navigation.dm
@@ -4,7 +4,11 @@
/obj/effect/landmark/transport/nav_beacon/tram
name = "tram destination" //the tram buttons will mention this.
icon_state = "tram"
-
+ voice_filter = "alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000"
+ /// the looping sound effect that is played while moving
+ var/datum/looping_sound/tram/tram_loop
+ /// What sound do we play when we arrive at this station?
+ var/arrival_sound = 'sound/machines/tram/other_line_processed.ogg'
/// The ID of the tram we're linked to
var/specific_transport_id = TRAMSTATION_LINE_1
/// The ID of that particular destination
@@ -14,10 +18,12 @@
/obj/effect/landmark/transport/nav_beacon/tram/Initialize(mapload)
. = ..()
+ tram_loop = new(src)
LAZYADDASSOCLIST(SStransport.nav_beacons, specific_transport_id, src)
/obj/effect/landmark/transport/nav_beacon/tram/Destroy()
LAZYREMOVEASSOC(SStransport.nav_beacons, specific_transport_id, src)
+ QDEL_NULL(tram_loop)
return ..()
/obj/effect/landmark/transport/nav_beacon/tram/nav
@@ -45,19 +51,22 @@
dir = WEST
/obj/effect/landmark/transport/nav_beacon/tram/platform/tramstation/west
- name = "West Wing"
+ name = "Arrivals Station"
platform_code = TRAMSTATION_WEST
tgui_icons = list("Arrivals" = "plane-arrival", "Command" = "bullhorn", "Security" = "gavel")
+ arrival_sound = 'sound/machines/tram/arrivals_line_processed.ogg'
/obj/effect/landmark/transport/nav_beacon/tram/platform/tramstation/central
- name = "Central Wing"
+ name = "Medical Station"
platform_code = TRAMSTATION_CENTRAL
tgui_icons = list("Service" = "cocktail", "Medical" = "plus", "Engineering" = "wrench")
+ arrival_sound = 'sound/machines/tram/medical_line_processed.ogg'
/obj/effect/landmark/transport/nav_beacon/tram/platform/tramstation/east
- name = "East Wing"
+ name = "Escape Station"
platform_code = TRAMSTATION_EAST
tgui_icons = list("Departures" = "plane-departure", "Cargo" = "box", "Science" = "flask")
+ arrival_sound = 'sound/machines/tram/escape_line_processed.ogg'
//birdshot
@@ -78,28 +87,32 @@
dir = WEST
/obj/effect/landmark/transport/nav_beacon/tram/platform/birdshot/sec_wing
- name = "Security Wing"
+ name = "Security Station"
specific_transport_id = BIRDSHOT_LINE_1
platform_code = BIRDSHOT_SECURITY_WING
tgui_icons = list("Security" = "gavel")
+ arrival_sound = 'sound/machines/tram/medical_line_processed.ogg'
/obj/effect/landmark/transport/nav_beacon/tram/platform/birdshot/prison_wing
- name = "Prison Wing"
+ name = "Prison Station"
specific_transport_id = BIRDSHOT_LINE_1
platform_code = BIRDSHOT_PRISON_WING
tgui_icons = list("Prison" = "box")
+ arrival_sound = 'sound/machines/tram/other_line_processed.ogg'
/obj/effect/landmark/transport/nav_beacon/tram/platform/birdshot/maint_left
- name = "Port Platform"
+ name = "Escape Station"
specific_transport_id = BIRDSHOT_LINE_2
platform_code = BIRDSHOT_MAINTENANCE_LEFT
tgui_icons = list("Port Platform" = "plane-departure")
+ arrival_sound = 'sound/machines/tram/escape_line_processed.ogg'
/obj/effect/landmark/transport/nav_beacon/tram/platform/birdshot/maint_right
- name = "Starboard Platform"
+ name = "Arrivals Station"
specific_transport_id = BIRDSHOT_LINE_2
platform_code = BRIDSHOT_MAINTENANCE_RIGHT
tgui_icons = list("Starboard Platform" = "plane-arrival")
+ arrival_sound = 'sound/machines/tram/arrivals_line_processed.ogg'
//map-agnostic landmarks
diff --git a/code/modules/tutorials/tutorials/switch_hands.dm b/code/modules/tutorials/tutorials/switch_hands.dm
index 724d99fca81e..affc5d8cbec5 100644
--- a/code/modules/tutorials/tutorials/switch_hands.dm
+++ b/code/modules/tutorials/tutorials/switch_hands.dm
@@ -50,17 +50,9 @@
switch (stage)
if (STAGE_SHOULD_SWAP_HAND)
- var/datum/keybinding/mob/select_hand/hand_keybinding
- var/hand_name
- if (IS_RIGHT_INDEX(hand_to_watch))
- hand_keybinding = /datum/keybinding/mob/select_hand/right
- hand_name = "right"
- else
- hand_keybinding = /datum/keybinding/mob/select_hand/left
- hand_name = "left"
-
+ var/hand_name = IS_RIGHT_INDEX(hand_to_watch) ? "right" : "left"
show_instruction(keybinding_message(
- hand_keybinding,
+ /datum/keybinding/mob/swap_hands,
"Press '%KEY%' to use your [hand_name] hand",
"Click 'SWAP' to use your [hand_name] hand",
))
diff --git a/code/modules/unit_tests/asset_smart_cache.dm b/code/modules/unit_tests/asset_smart_cache.dm
index 297951376a3e..1c10e74bec47 100644
--- a/code/modules/unit_tests/asset_smart_cache.dm
+++ b/code/modules/unit_tests/asset_smart_cache.dm
@@ -3,7 +3,7 @@
load_immediately = TRUE
force_cache = TRUE
// Don't let the asset subsystem load this. This is how we trick it.
- _abstract = /datum/asset/spritesheet_batched/test
+ abstract_type = /datum/asset/spritesheet_batched/test
var/static/list/items = list(/obj/item/binoculars, /obj/item/camera, /obj/item/clothing/under/color/blue, /obj/item/clothing/under/color/black)
/datum/asset/spritesheet_batched/test/create_spritesheets()
diff --git a/code/modules/unit_tests/huds.dm b/code/modules/unit_tests/huds.dm
index 8aadb770260a..ab2a21474bb7 100644
--- a/code/modules/unit_tests/huds.dm
+++ b/code/modules/unit_tests/huds.dm
@@ -16,3 +16,26 @@
REMOVE_TRAIT(dummy, TRAIT_SECURITY_HUD, TRAIT_SOURCE_UNIT_TESTS)
TEST_ASSERT(!testhud.hud_users_all_z_levels[dummy], "HUD not removed when trait of HUD was removed")
+
+///We're gonna give every HUD type to a mob to see if they are missing action intent/health doll.
+///This destroys the HUD of the mob we're using (but it doesn't matter cause it's a test)
+/datum/unit_test/verify_basic_huds
+
+/datum/unit_test/verify_basic_huds/Run()
+ for(var/mob/living/basic/mobs as anything in subtypesof(/mob/living/basic))
+ if(mobs::abstract_type == mobs)
+ continue
+ var/mob/living/basic/dummy = allocate(mobs)
+ var/mob_hud_type = mobs::hud_type
+ var/datum/hud/initialized_hud = new mob_hud_type(dummy)
+ //mobs that don't use combat mode don't need it.
+ var/atom/movable/screen/action_intent = initialized_hud.screen_objects[HUD_MOB_INTENTS]
+ if(!HAS_TRAIT(dummy, TRAIT_COMBAT_MODE_LOCK) && isnull(action_intent))
+ TEST_FAIL("[dummy] using [initialized_hud.type] does not have an Action Intent.")
+ //Mobs that need a health indicator should have at least a healthdoll or healths.
+ var/atom/movable/screen/healthdoll/healthdoll = initialized_hud.screen_objects[HUD_MOB_HEALTHDOLL]
+ var/atom/movable/screen/healths/healths = initialized_hud.screen_objects[HUD_MOB_HEALTH]
+ if(initialized_hud.needs_health_indicator && isnull(healthdoll) && isnull(healths))
+ TEST_FAIL("[dummy] using [initialized_hud.type] does not have a Health Doll or Health HUD.")
+ //Because we're not setting hud_used, the HUD doesn't delete with the mob, let's clear it here.
+ qdel(initialized_hud)
diff --git a/code/modules/unit_tests/spritesheets.dm b/code/modules/unit_tests/spritesheets.dm
index 16666dac17f6..404590125e6a 100644
--- a/code/modules/unit_tests/spritesheets.dm
+++ b/code/modules/unit_tests/spritesheets.dm
@@ -2,22 +2,18 @@
/datum/unit_test/spritesheets
/datum/unit_test/spritesheets/Run()
- for(var/datum/asset/spritesheet/sheet as anything in subtypesof(/datum/asset/spritesheet))
+ for(var/datum/asset/spritesheet/sheet as anything in valid_subtypesof(/datum/asset/spritesheet))
if(!initial(sheet.name)) //Ignore abstract types
continue
- if (sheet == initial(sheet._abstract))
- continue
sheet = get_asset_datum(sheet)
for(var/sprite_name in sheet.sprites)
if(!sprite_name)
TEST_FAIL("Spritesheet [sheet.type] has a nameless icon state.")
// Test IconForge generated sheets as well
- for(var/datum/asset/spritesheet_batched/sheet as anything in subtypesof(/datum/asset/spritesheet_batched))
+ for(var/datum/asset/spritesheet_batched/sheet as anything in valid_subtypesof(/datum/asset/spritesheet_batched))
if(!initial(sheet.name)) //Ignore abstract types
continue
- if (sheet == initial(sheet._abstract))
- continue
sheet = get_asset_datum(sheet)
for(var/sprite_name in sheet.sprites)
if(!sprite_name)
diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm
index 289bfc097bc3..3814df5ef38c 100644
--- a/code/modules/unit_tests/unit_test.dm
+++ b/code/modules/unit_tests/unit_test.dm
@@ -267,6 +267,8 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests())
/obj/structure/holosign/robot_seat,
//Singleton
/mob/dview,
+ //Template type
+ /obj/item/bodypart,
//This is meant to fail extremely loud every single time it occurs in any environment in any context, and it falsely alarms when this unit test iterates it. Let's not spawn it in.
/obj/merge_conflict_marker,
//Not meant to spawn without the machine wand
diff --git a/code/modules/vehicles/lavaboat.dm b/code/modules/vehicles/lavaboat.dm
index f1a6333c8d5e..378f03923a9b 100644
--- a/code/modules/vehicles/lavaboat.dm
+++ b/code/modules/vehicles/lavaboat.dm
@@ -77,6 +77,7 @@
desc = "This boat moves where you will it, without the need for an oar."
icon_state = "dragon_boat"
resistance_flags = LAVA_PROOF | FIRE_PROOF | FREEZE_PROOF
+ key_type = null
/obj/vehicle/ridden/lavaboat/dragon/Initialize(mapload)
. = ..()
diff --git a/code/modules/vehicles/mecha/combat/durand.dm b/code/modules/vehicles/mecha/combat/durand.dm
index 09f56c5146ce..99f32ddc987f 100644
--- a/code/modules/vehicles/mecha/combat/durand.dm
+++ b/code/modules/vehicles/mecha/combat/durand.dm
@@ -141,7 +141,8 @@ Expects a turf. Returns true if the attack should be blocked, false if not.*/
button_icon_state = "mech_defense_mode_off"
/datum/action/vehicle/sealed/mecha/mech_defense_mode/Trigger(mob/clicker, trigger_flags, forced_state = FALSE)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
diff --git a/code/modules/vehicles/mecha/combat/marauder.dm b/code/modules/vehicles/mecha/combat/marauder.dm
index 19a0df1affa5..e0e214242aa3 100644
--- a/code/modules/vehicles/mecha/combat/marauder.dm
+++ b/code/modules/vehicles/mecha/combat/marauder.dm
@@ -59,7 +59,8 @@
button_icon_state = "mech_smoke"
/datum/action/vehicle/sealed/mecha/mech_smoke/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
@@ -73,7 +74,8 @@
button_icon_state = "mech_zoom_off"
/datum/action/vehicle/sealed/mecha/mech_zoom/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!owner.client || !chassis || !(owner in chassis.occupants))
return
diff --git a/code/modules/vehicles/mecha/combat/phazon.dm b/code/modules/vehicles/mecha/combat/phazon.dm
index 30d15edfe179..e193f4e5e1ca 100644
--- a/code/modules/vehicles/mecha/combat/phazon.dm
+++ b/code/modules/vehicles/mecha/combat/phazon.dm
@@ -42,7 +42,8 @@
button_icon_state = "mech_damtype_brute"
/datum/action/vehicle/sealed/mecha/mech_switch_damtype/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
@@ -67,7 +68,8 @@
button_icon_state = "mech_phasing_off"
/datum/action/vehicle/sealed/mecha/mech_toggle_phasing/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
diff --git a/code/modules/vehicles/mecha/combat/savannah_ivanov.dm b/code/modules/vehicles/mecha/combat/savannah_ivanov.dm
index 40f40f621e6e..13f594f243bd 100644
--- a/code/modules/vehicles/mecha/combat/savannah_ivanov.dm
+++ b/code/modules/vehicles/mecha/combat/savannah_ivanov.dm
@@ -81,7 +81,8 @@
var/skyfall_charge_level = 0
/datum/action/vehicle/sealed/mecha/skyfall/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!owner || !chassis || !(owner in chassis.occupants))
return
@@ -253,7 +254,8 @@
return ..()
/datum/action/vehicle/sealed/mecha/ivanov_strike/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
diff --git a/code/modules/vehicles/mecha/mecha_actions.dm b/code/modules/vehicles/mecha/mecha_actions.dm
index 7d40cc3785da..2fd55c14e4b0 100644
--- a/code/modules/vehicles/mecha/mecha_actions.dm
+++ b/code/modules/vehicles/mecha/mecha_actions.dm
@@ -24,7 +24,8 @@
button_icon_state = "mech_eject"
/datum/action/vehicle/sealed/mecha/mech_eject/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
@@ -36,7 +37,8 @@
desc = "Airtight cabin preserves internal air and can be pressurized with a mounted air tank."
/datum/action/vehicle/sealed/mecha/mech_toggle_cabin_seal/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
@@ -47,7 +49,8 @@
button_icon_state = "mech_lights_off"
/datum/action/vehicle/sealed/mecha/mech_toggle_lights/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
@@ -58,7 +61,8 @@
button_icon_state = "mech_view_stats"
/datum/action/vehicle/sealed/mecha/mech_view_stats/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
@@ -74,7 +78,8 @@
RegisterSignal(chassis, COMSIG_MECH_SAFETIES_TOGGLE, PROC_REF(update_action_icon))
/datum/action/vehicle/sealed/mecha/mech_toggle_safeties/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
@@ -94,7 +99,8 @@
button_icon_state = "strafe"
/datum/action/vehicle/sealed/mecha/strafe/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
@@ -124,7 +130,8 @@
button_icon_state = "mech_seat_swap"
/datum/action/vehicle/sealed/mecha/swap_seat/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
@@ -162,7 +169,8 @@
build_all_button_icons()
/datum/action/vehicle/sealed/mecha/mech_overclock/Trigger(mob/clicker, trigger_flags, forced_state = null)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
@@ -187,7 +195,8 @@
return ..()
/datum/action/vehicle/sealed/mecha/equipment/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants) || !equipment)
return
@@ -214,6 +223,7 @@
name = "[equipment.name]"
/datum/action/vehicle/sealed/mecha/equipment/cargo_module/Trigger(mob/clicker, trigger_flags)
+ SHOULD_CALL_PARENT(FALSE) //We are snowflaked from parent
if(!chassis || !(owner in chassis.occupants) || !equipment)
return
if(!istype(equipment, /obj/item/mecha_parts/mecha_equipment/ejector))
@@ -263,6 +273,7 @@
name = "[equipment.name]"
/datum/action/vehicle/sealed/mecha/equipment/extinguisher_action/Trigger(mob/clicker, trigger_flags)
+ SHOULD_CALL_PARENT(FALSE) //We are snowflaked from parent
if(!chassis || !(owner in chassis.occupants) || !equipment)
return
if(!istype(equipment, /obj/item/mecha_parts/mecha_equipment/extinguisher))
diff --git a/code/modules/vehicles/mecha/working/clarke.dm b/code/modules/vehicles/mecha/working/clarke.dm
index 95b920aa047e..98907b8a26ee 100644
--- a/code/modules/vehicles/mecha/working/clarke.dm
+++ b/code/modules/vehicles/mecha/working/clarke.dm
@@ -121,7 +121,8 @@
button_icon_state = "mecha_sleeper_miner"
/datum/action/vehicle/sealed/mecha/clarke_scoop_body/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
var/obj/item/mecha_parts/mecha_equipment/sleeper/clarke/sleeper = locate() in chassis
var/mob/living/carbon/human/human_target
@@ -140,7 +141,8 @@
COOLDOWN_DECLARE(search_cooldown)
/datum/action/vehicle/sealed/mecha/mech_search_ruins/Trigger(mob/clicker, trigger_flags)
- if(!..())
+ . = ..()
+ if(!.)
return
if(!chassis || !(owner in chassis.occupants))
return
diff --git a/code/modules/vehicles/vehicle_actions.dm b/code/modules/vehicles/vehicle_actions.dm
index 5acd193d9232..5c395243caf3 100644
--- a/code/modules/vehicles/vehicle_actions.dm
+++ b/code/modules/vehicles/vehicle_actions.dm
@@ -222,6 +222,9 @@
button_icon_state = "car_removekey"
/datum/action/vehicle/sealed/remove_key/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
vehicle_entered_target.remove_key(owner)
//CLOWN CAR ACTION DATUMS
@@ -232,6 +235,9 @@
var/hornsound = 'sound/items/carhorn.ogg'
/datum/action/vehicle/sealed/horn/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
if(TIMER_COOLDOWN_RUNNING(src, COOLDOWN_CAR_HONK))
return
TIMER_COOLDOWN_START(src, COOLDOWN_CAR_HONK, 2 SECONDS)
@@ -248,6 +254,9 @@
button_icon_state = "car_headlights"
/datum/action/vehicle/sealed/headlights/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
to_chat(owner, span_notice("You flip the switch for the vehicle's headlights."))
vehicle_entered_target.headlights_toggle = !vehicle_entered_target.headlights_toggle
vehicle_entered_target.set_light_on(vehicle_entered_target.headlights_toggle)
@@ -260,6 +269,9 @@
button_icon_state = "car_dump"
/datum/action/vehicle/sealed/dump_kidnapped_mobs/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
vehicle_entered_target.visible_message(span_danger("[vehicle_entered_target] starts dumping the people inside of it."))
vehicle_entered_target.dump_specific_mobs(VEHICLE_CONTROL_KIDNAPPED)
@@ -270,6 +282,9 @@
button_icon_state = "car_rtd"
/datum/action/vehicle/sealed/roll_the_dice/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
if(!istype(vehicle_entered_target, /obj/vehicle/sealed/car/clowncar))
return
var/obj/vehicle/sealed/car/clowncar/C = vehicle_entered_target
@@ -281,6 +296,9 @@
button_icon_state = "car_cannon"
/datum/action/vehicle/sealed/cannon/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
if(!istype(vehicle_entered_target, /obj/vehicle/sealed/car/clowncar))
return
var/obj/vehicle/sealed/car/clowncar/C = vehicle_entered_target
@@ -295,6 +313,9 @@
/datum/action/vehicle/sealed/thank/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
if(!istype(vehicle_entered_target, /obj/vehicle/sealed/car/clowncar))
return
if(!COOLDOWN_FINISHED(src, thank_time_cooldown))
@@ -318,6 +339,9 @@
var/bell_cooldown
/datum/action/vehicle/ridden/wheelchair/bell/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
if(TIMER_COOLDOWN_RUNNING(src, bell_cooldown))
return
TIMER_COOLDOWN_START(src, bell_cooldown, 0.5 SECONDS)
@@ -376,6 +400,9 @@
check_flags = AB_CHECK_CONSCIOUS
/datum/action/vehicle/ridden/scooter/skateboard/kickflip/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return
var/obj/vehicle/ridden/scooter/skateboard/board = vehicle_target
var/mob/living/rider = owner
@@ -426,6 +453,9 @@
var/sound_message = "makes a sound."
/datum/action/vehicle/sealed/noise/Trigger(mob/clicker, trigger_flags)
+ . = ..()
+ if(!.)
+ return FALSE
var/obj/vehicle/sealed/car/vim/vim_mecha = vehicle_entered_target
if(!COOLDOWN_FINISHED(vim_mecha, sound_cooldown))
vim_mecha.balloon_alert(owner, "on cooldown!")
@@ -462,4 +492,6 @@
/datum/action/vehicle/sealed/headlights/vim/Trigger(mob/clicker, trigger_flags)
. = ..()
+ if(!.)
+ return
SEND_SIGNAL(vehicle_entered_target, COMSIG_VIM_HEADLIGHTS_TOGGLED, vehicle_entered_target.headlights_toggle)
diff --git a/code/modules/vending/clothesmate.dm b/code/modules/vending/clothesmate.dm
index f5669329cfde..18a673a61c2e 100644
--- a/code/modules/vending/clothesmate.dm
+++ b/code/modules/vending/clothesmate.dm
@@ -102,6 +102,8 @@
/obj/item/clothing/suit/jacket/oversized = 4,
/obj/item/clothing/suit/jacket/fancy = 4,
/obj/item/clothing/suit/toggle/lawyer/greyscale = 4,
+ /obj/item/clothing/suit/hooded/wintercoat/pullover = 3,
+ /obj/item/clothing/suit/hooded/wintercoat/zipup = 3,
/obj/item/clothing/suit/hooded/wintercoat/custom = 3,
/obj/item/clothing/suit/hooded/wintercoat = 3,
/obj/item/clothing/under/suit/navy = 3,
diff --git a/code/modules/vending/vendor/_vending.dm b/code/modules/vending/vendor/_vending.dm
index 12f71ad7e07d..f9a677faf718 100644
--- a/code/modules/vending/vendor/_vending.dm
+++ b/code/modules/vending/vendor/_vending.dm
@@ -215,7 +215,7 @@
if(SStts.tts_enabled)
var/static/vendor_voice_by_type = list()
if(!vendor_voice_by_type[type])
- vendor_voice_by_type[type] = SStts.radnom_vendor_voice() // //MASSMETA ADD START (vendor one voice) Original: vendor_voice_by_type[type] = pick(SStts.available_speakers)
+ vendor_voice_by_type[type] = SStts.random_vendor_voice() // //MASSMETA ADD START (vendor one voice) Original: vendor_voice_by_type[type] = pick(SStts.available_speakers)
voice = vendor_voice_by_type[type]
diff --git a/code/modules/wiremod/components/action/camera.dm b/code/modules/wiremod/components/action/camera.dm
new file mode 100644
index 000000000000..7a3f9460501c
--- /dev/null
+++ b/code/modules/wiremod/components/action/camera.dm
@@ -0,0 +1,110 @@
+/obj/item/circuit_component/camera
+ display_name = "Camera"
+ desc = "A polaroid camera that takes pictures when triggered. The picture coordinate ports are relative to the position of the camera."
+ circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL
+
+ /// The atom that was photographed from either user click or trigger input.
+ var/datum/port/output/photographed_atom
+ /// The item that was added/removed.
+ var/datum/port/output/picture_taken
+ /// If set, the trigger input will target this atom.
+ var/datum/port/input/picture_target
+ /// If the above is unset, these coordinates will be used.
+ var/datum/port/input/picture_coord_x
+ var/datum/port/input/picture_coord_y
+ /// Adjusts the picture_size_x variable of the camera.
+ var/datum/port/input/adjust_size_x
+ /// Idem but for picture_size_y.
+ var/datum/port/input/adjust_size_y
+
+ /// The camera this circut is attached to.
+ var/obj/item/camera/camera
+
+/obj/item/circuit_component/camera/populate_ports()
+ picture_taken = add_output_port("Picture Taken", PORT_TYPE_SIGNAL)
+ photographed_atom = add_output_port("Photographed Entity", PORT_TYPE_ATOM)
+
+ picture_target = add_input_port("Picture Target", PORT_TYPE_ATOM)
+ picture_coord_x = add_input_port("Picture Coordinate X", PORT_TYPE_NUMBER)
+ picture_coord_y = add_input_port("Picture Coordinate Y", PORT_TYPE_NUMBER)
+ adjust_size_x = add_input_port("Picture Size X", PORT_TYPE_NUMBER, trigger = PROC_REF(sanitize_picture_size))
+ adjust_size_y = add_input_port("Picture Size Y", PORT_TYPE_NUMBER, trigger = PROC_REF(sanitize_picture_size))
+
+/obj/item/circuit_component/camera/register_shell(atom/movable/shell)
+ . = ..()
+ camera = shell
+ RegisterSignal(shell, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_image_captured))
+
+/obj/item/circuit_component/camera/unregister_shell(atom/movable/shell)
+ UnregisterSignal(shell, COMSIG_CAMERA_IMAGE_CAPTURED)
+ camera = null
+ return ..()
+
+///Adjuts the zoom of the camera
+/obj/item/circuit_component/camera/proc/sanitize_picture_size()
+ camera.adjust_zoom(adjust_size_x.value, adjust_size_y.value)
+
+/obj/item/circuit_component/camera/proc/on_image_captured(obj/item/camera/source, atom/target, mob/user)
+ SIGNAL_HANDLER
+ photographed_atom.set_output(target)
+ picture_taken.set_output(COMPONENT_SIGNAL)
+
+/obj/item/circuit_component/camera/input_received(datum/port/input/port)
+ var/atom/target = picture_target.value
+ if(!target)
+ var/turf/our_turf = get_location()
+ target = locate(our_turf.x + picture_coord_x.value, our_turf.y + picture_coord_y.value, our_turf.z)
+ if(!target)
+ return
+ camera.attempt_picture(target)
+
+/obj/item/circuit_component/mod_program/camera
+ associated_program = /datum/computer_file/program/maintenance/camera
+ circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL
+
+ ///A target to take a picture of.
+ var/datum/port/input/picture_target
+ ///The size of the photo to take.
+ var/datum/port/input/picture_size
+ ///The photographed target
+ var/datum/port/output/photographed
+ /**
+ * Pinged when the image has been captured.
+ * I'm not using the default trigger output here because the process is asynced,
+ * even though I'm mostly sure it only sleeps if there's a set user.
+ */
+ var/datum/port/output/photo_taken
+
+/obj/item/circuit_component/mod_program/camera/populate_ports()
+ . = ..()
+ picture_target = add_input_port("Picture Target", PORT_TYPE_ATOM)
+ picture_size = add_input_port("Picture Size", PORT_TYPE_NUMBER)
+ photographed = add_output_port("Photographed Entity", PORT_TYPE_ATOM)
+ photo_taken = add_output_port("Photo Taken", PORT_TYPE_SIGNAL)
+
+/obj/item/circuit_component/mod_program/camera/register_shell(atom/movable/shell)
+ . = ..()
+ var/datum/computer_file/program/maintenance/camera/cam = associated_program
+ RegisterSignal(cam.internal_camera, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_image_captured))
+
+/obj/item/circuit_component/mod_program/camera/unregister_shell()
+ var/datum/computer_file/program/maintenance/camera/cam = associated_program
+ UnregisterSignal(cam.internal_camera, COMSIG_CAMERA_IMAGE_CAPTURED)
+ return ..()
+
+/obj/item/circuit_component/mod_program/camera/input_received(datum/port/input/port)
+ if(!COMPONENT_TRIGGERED_BY(port, trigger_input))
+ return
+ var/atom/target = picture_target.value
+ if(!target)
+ var/turf/our_turf = get_location()
+ target = locate(our_turf.x, our_turf.y, our_turf.z)
+ if(!target)
+ return
+ var/datum/computer_file/program/maintenance/camera/cam = associated_program
+ cam.internal_camera.attempt_picture(target)
+
+/obj/item/circuit_component/mod_program/camera/proc/on_image_captured(obj/item/camera/source, atom/target, mob/user)
+ SIGNAL_HANDLER
+ photographed.set_output(target)
+ photo_taken.set_output(COMPONENT_SIGNAL)
diff --git a/code/modules/wiremod/components/action/mmi.dm b/code/modules/wiremod/components/action/mmi.dm
index 0a7b6a38ae8e..d75219018528 100644
--- a/code/modules/wiremod/components/action/mmi.dm
+++ b/code/modules/wiremod/components/action/mmi.dm
@@ -6,7 +6,7 @@
button_icon = 'icons/mob/actions/actions_AI.dmi'
button_icon_state = "ai_core"
-/datum/action/innate/mmi_comp_disconnect/Trigger(trigger_flags)
+/datum/action/innate/mmi_comp_disconnect/Trigger(mob/clicker, trigger_flags)
. = ..()
if(!.)
return
diff --git a/code/modules/modular_computers/file_system/program_circuit.dm b/code/modules/wiremod/core/program_circuit.dm
similarity index 100%
rename from code/modules/modular_computers/file_system/program_circuit.dm
rename to code/modules/wiremod/core/program_circuit.dm
diff --git a/code/modules/wiremod/shell/brain_computer_interface.dm b/code/modules/wiremod/shell/brain_computer_interface.dm
index 30f4910ec23d..126764cfa938 100644
--- a/code/modules/wiremod/shell/brain_computer_interface.dm
+++ b/code/modules/wiremod/shell/brain_computer_interface.dm
@@ -211,7 +211,7 @@
START_PROCESSING(SSobj, src)
-/datum/action/innate/bci_charge_action/create_button()
+/datum/action/innate/bci_charge_action/create_button(mob/viewer)
var/atom/movable/screen/movable/action_button/button = ..()
button.maptext_x = 2
button.maptext_y = 0
@@ -226,6 +226,8 @@
return ..()
/datum/action/innate/bci_charge_action/Trigger(mob/clicker, trigger_flags)
+ if(!..())
+ return
var/obj/item/stock_parts/power_store/cell/cell = circuit_component.parent.cell
if (isnull(cell))
diff --git a/code/modules/wiremod/shell/implant.dm b/code/modules/wiremod/shell/implant.dm
index 2c719d6017ee..409c1921c6f3 100644
--- a/code/modules/wiremod/shell/implant.dm
+++ b/code/modules/wiremod/shell/implant.dm
@@ -199,7 +199,7 @@
START_PROCESSING(SSobj, src)
-/datum/action/innate/implant_charge_action/create_button()
+/datum/action/innate/implant_charge_action/create_button(mob/viewer)
var/atom/movable/screen/movable/action_button/button = ..()
button.maptext_x = 2
button.maptext_y = 0
@@ -214,6 +214,8 @@
return ..()
/datum/action/innate/implant_charge_action/Trigger(mob/clicker, trigger_flags)
+ if(!..())
+ return
var/obj/item/stock_parts/power_store/cell/cell = circuit_component.parent.cell
if (isnull(cell))
diff --git a/config/config.txt b/config/config.txt
index b35e44ce037c..5dd48f757487 100644
--- a/config/config.txt
+++ b/config/config.txt
@@ -610,9 +610,6 @@ PR_ANNOUNCEMENTS_PER_ROUND 5
## Uncomment to block granting profiling privileges to users with R_DEBUG, for performance purposes
#FORBID_ADMIN_PROFILING
-## Uncomment to disable TTS messages on whispering, such as radio messages or the whisper verb. Useful for reducing your TTS load.
-#TTS_NO_WHISPER
-
## Link to a HTTP server that's been set up on a server. Docker-compose file can be found in tools/tts
#TTS_HTTP_URL http://localhost:5002
@@ -629,6 +626,19 @@ PR_ANNOUNCEMENTS_PER_ROUND 5
#TTS_VOICE_BLACKLIST Sans Undertale
#TTS_VOICE_BLACKLIST Papyrus Undertale
+## Override the voice that the Tram system uses. If left undefined, the tram will pick a random voice.
+#TTS_TRAM_ANNOUNCER_OVERRIDE Sans Undertale
+
+## Override the voice that computers use. If left undefined, the subsystem will pick a random consistent computer voice.
+#TTS_COMPUTER_VOICE_OVERRIDE Papyrus Undertale
+
+## Default centcom voice for station's announcements
+#CENTCOM_VOICE glados
+
+## Voices that are only available to admins.
+#ADMIN_VOICES glados
+#ADMIN_VOICES wheatley
+
## Comment to disable sending a toast notification on the host server when initializations complete.
## Even if this is enabled, a notification will only be sent if there are no clients connected.
TOAST_NOTIFICATION_ON_INIT
@@ -659,5 +669,6 @@ TGUI_MAX_CHUNK_COUNT 32
## It increases initialization time significantly so you'll want to disable this in live environments.
GENERATE_ASSETS_IN_INIT
-## Minimum time before a heretic can ascend, in minutes
-# MINIMUM_ASCENSION_TIME 3
+## Path from the config folder to the policy json
+POLICY_JSON_PATH policy.json
+
diff --git a/config/game_options.txt b/config/game_options.txt
index 6160d74e37a6..3a8319cca9a8 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -593,3 +593,6 @@ SPY_EASY_REWARD_TC_THRESHOLD 5
## Hard bounties grant uplink rewards that cost this much TC (or higher).
## Also functions as the upper end for medium bounties.
SPY_HARD_REWARD_TC_THRESHOLD 15
+
+## Minimum time before a heretic can ascend, in minutes
+# MINIMUM_ASCENSION_TIME 3
diff --git a/html/changelogs/archive/2026-04.yml b/html/changelogs/archive/2026-04.yml
index d04b83950ebb..b0ff79eebe4f 100644
--- a/html/changelogs/archive/2026-04.yml
+++ b/html/changelogs/archive/2026-04.yml
@@ -70,6 +70,237 @@
type, and Meal type. You can now filter between "Moth Pizzas" or "Lizard Soups"
rather than needing to guess if the recipe is in "Pizza" or "Mothic".
- spellcheck: de-capitalized several foods
+ - qol: TTS voice is more appropriately randomized to gender
+ - qol: Getting randomized through means such as mulligan toxin randomizes TTS voices
+ SyncIt21:
+ - bugfix: camera devices work again, camera flash turns on in most instances
+ - refactor: camera code has been refactored. Report bugs on github
TheRyeGuyWhoWillNowDie:
- bugfix: using a dedicated cyborg defib upgrade on a mediborg with a backpack defib
installed will properly eject it
+ lelandkemble:
+ - rscadd: Remote chemical implants can be synced with a remote signalling device
+ to activate without a Prisoner Management Console.
+2026-04-05:
+ SyncIt21:
+ - refactor: ' reagent reaction code has been refactored for better cpu & memory
+ performance. Report broken reactions on github'
+2026-04-06:
+ ArcaneMusic:
+ - admin: Adds a secret to restart the gravity generator.
+ JohnFulpWillard:
+ - qol: Health assemblies now show their health in maptext rather than a stat panel
+ entry.
+ - rscdel: Deleted Object tab, and Server tab for players. Activate Held Object verb
+ is now in the IC tab. Examine and Point To was removed from the stat panel.
+ - admin: Object possession verbs have been moved to Admin Fun.
+ LemonInTheDark:
+ - config: Added a config option for the path to policy.json
+ Melbert:
+ - bugfix: Reset Dislocation operation ignores clothing
+ Rhials:
+ - bugfix: The shuttle loan event will now properly clear itself if not accepted
+ within the time limit, allowing for different loan opportunities to roll.
+ SmArtKar:
+ - balance: Red and black raptors no longer retaliate against their tamer's accidental
+ attacks
+ - bugfix: Fixed raptor shift-hover menu being offset, and other visual offset issues
+ - balance: Raptors and raptor chicks/eggs have had their growth speed increased
+ by 50/33% respectively.
+ rageguy505:
+ - bugfix: Metastations mining and QM disposal bins are no longer Jim Norton branded.
+2026-04-07:
+ Aliceee2ch:
+ - bugfix: Fixed empty message when ejecting an ID card from PDA.
+ - spellcheck: Most of wrenching screentips should be properly capitalized now.
+ JohnFulpWillard:
+ - rscadd: Medbay/Detectives can now add a Cause of Death to people's medical records
+ when set as deceased.
+ Melbert:
+ - bugfix: Fix cyborgs being unable to do surgeries that ask for user input before
+ it begins
+ - refactor: Refactored the SDSM-35 to allow for new books of similar styles, report
+ any oddities with it.
+ - bugfix: Fixes the broken image(s) in the SDSM-35.
+ - rscadd: Adds the IDC-27 in Medbay, the CMO's office, Virology, and possibly the
+ Library. It's a reference book to all viruses you may experience, similar to
+ the SDSM-35.
+ - rscadd: Several diseases/viruses received minor flavor adjustments, primarily
+ to cure text, agent, description, and form.
+ - rscadd: Getting cured of the Flu provides immunity to Spanish Flu (and visa versa)
+ as implied in cure text.
+ - rscadd: Getting cured of a Cold provides immunity to Cold-9 (and visa versa) as
+ implied in cure text.
+ - qol: Surgical states applied by wounds don't appear in examine unless the mob
+ is prepared for surgery
+ - qol: Surgical state examine text is now slightly smaller
+ - qol: '"Fix bones" surgery operation no longer appears if the patient is suffering
+ a bone fracture - you will need to "repair fracture" instead.'
+ - balance: Holy water temporarily pauses some effects of, and helps reverse, cursed
+ painting effects
+ SmArtKar:
+ - bugfix: Fixed boxing or successful blocking not preventing grabs
+ - bugfix: Fixed people crashing into their own mounts if they're thrown together
+ Tsar-Salat:
+ - bugfix: audits action button triggers, more action buttons ingame should respect
+ if the player can use the action button or not.
+2026-04-08:
+ Absolucy:
+ - qol: Added an indicator to chat highlight settings if you've input an invalid
+ regex (a red border around the textbox).
+ - qol: You can now enable/disable individual chat highlights.
+ Maxipat:
+ - rscadd: New css style class for use in config. Now you can notice policy text
+ easier
+ Melbert:
+ - admin: Admins can no longer edit global movespeed modifier datums
+ SmArtKar:
+ - bugfix: Fixed brimdust rendering on wide mobs (like raptors)
+ SyncIt21:
+ - bugfix: rped beam on machine frame renders correctly
+ TealSeer:
+ - bugfix: fixed silicons being unable to use operating computer from afar
+ lelandkemble:
+ - rscdel: Charlie station can no longer print custom shuttle boards
+ - bugfix: Gauze will stay on bleeding wounds it is applied to(but still not stop
+ bloodflow)
+ loganuk:
+ - code_imp: Atmos pipes GPU performance - Station areas with pipes will be less
+ taxing on the client.
+2026-04-09:
+ Melbert:
+ - bugfix: A dirty mask that CAN be pushed out of the way no longer obscures vision
+ if it IS pushed out of the way.
+ - qol: If your mask is dirty from pepper spray, you get some passive chat messages
+ indicating that it can be cleaned off.
+ - balance: Pepper spray clothing dirt is applied piecemeal - smaller dosages, rather
+ than doing nothing, now build up with repeat applications.
+ - balance: Pepper spray clothing dirt now has a tier between "very obscured vision"
+ and "no obscured vision", giving the wearer more of a warning before being thrust
+ into darkness.
+ - balance: Pepper spray can no longer stack blindness infinitely - it is now capped
+ to 6 seconds. This brings it in line with its other effects (confusion capped
+ at 5 seconds, blur capped at 10 seconds, knockdown capped at 3 seconds).
+ - bugfix: Fix zombie powder perma-coma on injection
+ SyncIt21:
+ - bugfix: reactions no longer consume catalysts
+ - code_imp: improved reaction code in other places
+ siliconOpossum:
+ - admin: Adds the jump-to-ghost verb, which brings your body to your aghost, it
+ has a hotkey which defaults to F4.
+2026-04-10:
+ Aliceee2ch:
+ - spellcheck: Renamed golden data disk to syndicate data disk.
+ Meblert:
+ - balance: Wizard Transparence perk's curse is permanent
+ Melbert:
+ - bugfix: Fixes the IDC
+ - bugfix: Carp mask can be used for internals again
+ - qol: Roundstart xeno egg is now mentioned in the roundstart report as an addendum,
+ rather than as a second report (usually) following the roundstart report.
+ lelandkemble:
+ - bugfix: Neurine works in the dead again
+ - bugfix: Escape pod storage wall safes will now be unlocked when they're unlocked
+ and appear locked when they're locked
+2026-04-11:
+ ArcaneMusic:
+ - bugfix: Weakpoints have more safeguards to prevent them from spawning onto space.
+ - image: Weakpoints now have an animation to spawn in with.
+ ElGitificador:
+ - bugfix: Deleted the misleading key request for the dragon lavaboat
+ FalloutFalcon:
+ - bugfix: The swap hand tutorial wont be shown to someone missing a hand
+ - bugfix: A handful of permissions panel querys now work again
+ Neocloudy:
+ - bugfix: DNA mob height doesn't instantly fall back to the base height for all
+ mobs.
+ SmArtKar:
+ - rscadd: Custom material spears can now be crafted using shards or (any) crystal
+ sheets on a wireprod
+ - rscadd: 'Added 3 new material effects: Vampire''s Bane (Silver), Teleporting (BS/Telecrystals)
+ and Penetrating (Telecrystals)'
+ - balance: Spears had their material stat calculations adjusted a bit to account
+ for the new slot system.
+ - refactor: Implemented datum-based material effects and slots
+ TheRyeGuyWhoWillNowDie:
+ - bugfix: the cyborg omnitool upgrades properly increase tool speed
+ - bugfix: the cyborg omnitool engineer upgrade properly increases the welder speed
+ as well
+ Twaticus:
+ - image: Hoodies! Both pullover and zip-up hoodies are available in the clothesmate
+ and in loadouts!
+2026-04-12:
+ ElGitificador:
+ - spellcheck: due to conflicts with the hive empress, lamarr has lost it s diplomatic
+ facehugger title
+ SmArtKar:
+ - bugfix: Fixed weather forcasts randomly backtracking on themselves, and falsely
+ announcing safety right before a storm hits
+ - rscdel: Plasma flower MOD core no longer spawns butterflies around the user
+ - qol: Plasma flower MOD core's pollen visual now spawns less particles
+2026-04-13:
+ 00-Steven:
+ - admin: Holoparasite boobytraps now log when they're set and when they explode.
+ ArcaneMusic:
+ - rscadd: The Civilian bounty pad now has a new tab, where players can collaborate
+ and work together to complete additional, station wide bounties. Reward is split
+ based on the amount contributed. (Between 5-10 based on population to start)
+ - rscadd: The civilian bounty pad can now print out sheets of paper displaying all
+ available bounties, like old times.
+ - bugfix: The ninja holding facility is no longer loaded when civilian bounties
+ are sent.
+ Arturlang:
+ - bugfix: rust heretic robes now become buffed if the tile under you becomes rusted
+ - balance: void ascension and the void chill status effect now respect antimagic
+ - rscadd: heretic aura is now emissive.
+ - bugfix: a exploit where you could exit space phase anywhere by loosing your heretic
+ focus
+ - bugfix: moon ascension aura not affecting anyone else if one of the targets has
+ more than 10 sanity
+ Dolphyuser:
+ - balance: Now you can't handcuff shields
+ Iamgoofball:
+ - qol: Restores keybindings to their original pre-August 2025 defaults, moves the
+ new per-hand bind options to Unbound by default.
+ Rhials:
+ - bugfix: Donk Trading Post cameras no longer show up on the station cameranet
+ SmArtKar:
+ - qol: Ore vents now turn transparent and clickthrough when a mob enters the tile
+ above them. The bottom half of the vent is still clickable.
+ - code_imp: Cleaned up ore vent code
+ TheRyeGuyWhoWillNowDie:
+ - balance: making the handheld iconoclast's repeater now requires learning its recipe,
+ the book for which can spawn in maintenance.
+ lelandkemble:
+ - bugfix: Roundstart revs will no longer choose people who do not want to be headrevs
+ as headrevs
+ - bugfix: Liberator pistol applies point-blank damage modifier no matter how it
+ is fired
+ - bugfix: toiletbong works
+ - bugfix: Smoke no longer exposes mobs to reagents multiple times per tick, significantly
+ lowering the duration of effects applied by chemical smoke
+ - map: Tramstation inner supermatter chamber no longer has a connected APC
+ timothymtorres:
+ - bugfix: Fix admin shuttle recall revealing admin location
+ - rscadd: Added a new positive quirk "Keen Nose". Characters with this quirk can
+ smell chemical contents in open beakers and bottles simply by examining them
+ while held. The smell description is identical to the taste description of the
+ reagents.
+ - rscadd: Getting pepper-sprayed now causes a mob to lose their sense of smell for
+ half a minute.
+2026-04-14:
+ Ben10Omintrix:
+ - bugfix: fixes bot huds randomly dissappearing
+ JohnFulpWillard:
+ - qol: Holoparasites' abilities are now action buttons, so you can rebind or move
+ them.
+ - qol: Holoparasites now have floor change buttons.
+ - bugfix: Holoparasites and Soulscythes now have a combat mode indicator.
+ - bugfix: Dextrous holoparasites now have their inventory slot again while non-dextrous
+ had it taken away (not that they could use it).
+ SmArtKar:
+ - rscadd: Added a new Tactical IFF Visor, available after combat implants have been
+ researched.
+ - refactor: Refactored eye rendering slightly.
+ timothymtorres:
+ - bugfix: Fix fishing random portals not working correctly
diff --git a/icons/hud/64x16_actions.dmi b/icons/hud/64x16_actions.dmi
index 6a54c8e4bb36..9b8e936d32d0 100644
Binary files a/icons/hud/64x16_actions.dmi and b/icons/hud/64x16_actions.dmi differ
diff --git a/icons/hud/guardian.dmi b/icons/hud/guardian.dmi
index 6b6c03912dd7..e39b6681f69c 100644
Binary files a/icons/hud/guardian.dmi and b/icons/hud/guardian.dmi differ
diff --git a/icons/map_icons/clothing/suit/_suit.dmi b/icons/map_icons/clothing/suit/_suit.dmi
index 82feabad49e9..cdbb28e990c8 100644
Binary files a/icons/map_icons/clothing/suit/_suit.dmi and b/icons/map_icons/clothing/suit/_suit.dmi differ
diff --git a/icons/mob/clothing/head/winterhood.dmi b/icons/mob/clothing/head/winterhood.dmi
index 3b28fe1c0a57..64ab5132c8ee 100644
Binary files a/icons/mob/clothing/head/winterhood.dmi and b/icons/mob/clothing/head/winterhood.dmi differ
diff --git a/icons/mob/clothing/suits/wintercoat.dmi b/icons/mob/clothing/suits/wintercoat.dmi
index ee3e3046d289..8a0a06fe06ee 100644
Binary files a/icons/mob/clothing/suits/wintercoat.dmi and b/icons/mob/clothing/suits/wintercoat.dmi differ
diff --git a/icons/mob/human/hair_masks.dmi b/icons/mob/human/hair_masks.dmi
index dd54ce1e8860..703c791cb2b0 100644
Binary files a/icons/mob/human/hair_masks.dmi and b/icons/mob/human/hair_masks.dmi differ
diff --git a/icons/mob/human/human_eyes.dmi b/icons/mob/human/human_eyes.dmi
index 1ed4e37917af..9f8b07cdc97e 100644
Binary files a/icons/mob/human/human_eyes.dmi and b/icons/mob/human/human_eyes.dmi differ
diff --git a/icons/mob/inhands/weapons/polearms_lefthand.dmi b/icons/mob/inhands/weapons/polearms_lefthand.dmi
index a882482331f2..9cf37e16e042 100644
Binary files a/icons/mob/inhands/weapons/polearms_lefthand.dmi and b/icons/mob/inhands/weapons/polearms_lefthand.dmi differ
diff --git a/icons/mob/inhands/weapons/polearms_righthand.dmi b/icons/mob/inhands/weapons/polearms_righthand.dmi
index 985e1188932b..8107d6533a98 100644
Binary files a/icons/mob/inhands/weapons/polearms_righthand.dmi and b/icons/mob/inhands/weapons/polearms_righthand.dmi differ
diff --git a/icons/mob/simple/lavaland/raptor_big.dmi b/icons/mob/simple/lavaland/raptor_big.dmi
index 4e4932f91a4a..84a8a035c0dc 100644
Binary files a/icons/mob/simple/lavaland/raptor_big.dmi and b/icons/mob/simple/lavaland/raptor_big.dmi differ
diff --git a/icons/obj/clothing/head/winterhood.dmi b/icons/obj/clothing/head/winterhood.dmi
index 1ad9f6eca016..35e56fd3c0b2 100644
Binary files a/icons/obj/clothing/head/winterhood.dmi and b/icons/obj/clothing/head/winterhood.dmi differ
diff --git a/icons/obj/clothing/suits/wintercoat.dmi b/icons/obj/clothing/suits/wintercoat.dmi
index 3f8e1bc38351..1e53f07dcfd3 100644
Binary files a/icons/obj/clothing/suits/wintercoat.dmi and b/icons/obj/clothing/suits/wintercoat.dmi differ
diff --git a/icons/obj/medical/organs/organs.dmi b/icons/obj/medical/organs/organs.dmi
index 6197442f6227..24d9ef9cbe28 100644
Binary files a/icons/obj/medical/organs/organs.dmi and b/icons/obj/medical/organs/organs.dmi differ
diff --git a/icons/obj/mining_zones/terrain.dmi b/icons/obj/mining_zones/terrain.dmi
index 5351cde085c3..f46a677296b1 100644
Binary files a/icons/obj/mining_zones/terrain.dmi and b/icons/obj/mining_zones/terrain.dmi differ
diff --git a/icons/obj/stack_objects.dmi b/icons/obj/stack_objects.dmi
index 0daa5fc8dd0d..808453958034 100644
Binary files a/icons/obj/stack_objects.dmi and b/icons/obj/stack_objects.dmi differ
diff --git a/icons/obj/weapons/spear.dmi b/icons/obj/weapons/spear.dmi
index e654cbde28df..6a2f95f78b5c 100644
Binary files a/icons/obj/weapons/spear.dmi and b/icons/obj/weapons/spear.dmi differ
diff --git a/icons/turf/composite.dmi b/icons/turf/composite.dmi
index a40c4121d252..51938161ed1b 100644
Binary files a/icons/turf/composite.dmi and b/icons/turf/composite.dmi differ
diff --git a/interface/stylesheet.dm b/interface/stylesheet.dm
index 99d1f4443f92..7fdf99783b8d 100644
--- a/interface/stylesheet.dm
+++ b/interface/stylesheet.dm
@@ -163,6 +163,7 @@ h1.alert, h2.alert {color: #000000;}
.monkey {color: #975032;}
.swarmer {color: #2C75FF;}
.resonate {color: #298F85;}
+.policy {color: #9730db; font-style: italic; text-align: center; font-size: 2;}
.upside_down {display: inline; -moz-transform: scale(-1, -1); -webkit-transform: scale(-1, -1); -o-transform: scale(-1, -1); -ms-transform: scale(-1, -1); transform: scale(-1, -1);}
"}
diff --git a/modular_meta/features/ntts-nd-tg-tts/code/announcements.dm b/modular_meta/features/ntts-nd-tg-tts/code/announcements.dm
new file mode 100644
index 000000000000..7df001814bd4
--- /dev/null
+++ b/modular_meta/features/ntts-nd-tg-tts/code/announcements.dm
@@ -0,0 +1,181 @@
+/datum/config_entry/string/centcom_voice
+
+/atom/movable
+ var/tts_announcement_effect
+
+/obj/machinery/announcement_system
+ tts_announcement_effect = "radio_machine"
+
+/datum/controller/subsystem/tts
+ var/centcom_voice
+
+/datum/tts_request
+ var/announcement = FALSE
+ var/list/announcement_listeners
+
+/datum/controller/subsystem/tts/proc/set_announcement_voices()
+ centcom_voice = CONFIG_GET(string/centcom_voice) || computer_voice
+ if(!(centcom_voice in available_speakers))
+ centcom_voice = computer_voice
+
+/datum/controller/subsystem/tts/proc/announcement_voice(atom/movable/source, voice_override)
+ var/speaker = voice_override || source?.voice || centcom_voice || computer_voice
+ if(speaker in available_speakers)
+ return speaker
+ if(computer_voice in available_speakers)
+ return computer_voice
+ return length(available_speakers) ? pick(available_speakers) : null
+
+/datum/controller/subsystem/tts/proc/announcement_effect(atom/movable/source, fallback = "centcom")
+ return source?.tts_announcement_effect || fallback
+
+/datum/controller/subsystem/tts/proc/announcement_text(message, title)
+ var/list/parts = list()
+ if(title)
+ parts += html_decode(STRIP_HTML_SIMPLE(title, MAX_MESSAGE_LEN))
+ if(message)
+ parts += html_decode(STRIP_HTML_SIMPLE(message, MAX_MESSAGE_LEN))
+ return parts.Join(". ")
+
+/datum/controller/subsystem/tts/proc/can_hear_announcement_tts(mob/listener, should_play_sound = TRUE)
+ if(!listener || isnewplayer(listener) || HAS_TRAIT(listener, TRAIT_DEAF))
+ return FALSE
+ if(!listener.client)
+ return FALSE
+
+ var/datum/callback/play_check = astype(should_play_sound)
+ if(!should_play_sound || (play_check && !play_check.Invoke(listener)))
+ return FALSE
+ if(!listener.client.prefs.read_preference(/datum/preference/toggle/sound_announcements))
+ return FALSE
+ if(listener.client.prefs.read_preference(/datum/preference/choiced/sound_tts) == TTS_SOUND_OFF)
+ return FALSE
+ return TRUE
+
+/datum/controller/subsystem/tts/proc/announcement_listeners(list/players, should_play_sound = TRUE)
+ var/list/filtered = list()
+ for(var/mob/player as anything in (players || GLOB.player_list))
+ if(can_hear_announcement_tts(player, should_play_sound))
+ filtered += player
+ return filtered
+
+/datum/controller/subsystem/tts/proc/queue_global_announcement(message, list/players, atom/movable/source, voice_override, filter, special_filters, should_play_sound = TRUE, effect)
+ if(!tts_enabled)
+ return
+
+ message = announcement_text(message)
+ if(!message)
+ return
+
+ var/list/listeners = announcement_listeners(players, should_play_sound)
+ if(!length(listeners))
+ return
+
+ var/list/filters = list()
+ if(filter)
+ filters += filter
+ if(source?.voice_filter)
+ filters += source.voice_filter
+
+ var/list/special_filter_list = list()
+ if(special_filters)
+ special_filter_list += special_filters
+
+ var/speaker = voice_override
+ if(!speaker && isliving(source))
+ var/mob/living/living_source = source
+ speaker = living_source.get_tts_voice(filters, special_filter_list)
+ if(!speaker)
+ speaker = announcement_voice(source, voice_override)
+ if(!(speaker in available_speakers))
+ speaker = announcement_voice(source, null)
+ if(!speaker)
+ return
+
+ filter = filters.Join(",")
+ special_filters = special_filter_list.Join("|")
+ var/pitch = source?.pitch || 0
+ var/blip_base = source?.blip_base || "male"
+ var/blip_number = source?.blip_number || "1"
+ effect ||= announcement_effect(source)
+ queue_announcement(message, listeners, speaker, filter, pitch, special_filters, blip_base, blip_number, effect)
+
+/datum/controller/subsystem/tts/proc/queue_announcement(message, list/listeners, speaker, filter = "", pitch = 0, special_filters = "", blip_base = "male", blip_number = "1", effect = "centcom")
+ if(!fexists("tmp/tts/init.txt"))
+ rustg_file_write("rustg HTTP requests can't write to folders that don't exist, so we need to make it exist.", "tmp/tts/init.txt")
+
+ filter ||= ""
+ special_filters ||= ""
+ var/static/regex/contains_alphanumeric = regex("\[а-яА-ЯёЁa-zA-Z0-9]")
+ if(contains_alphanumeric.Find(message) == 0)
+ return
+
+ var/effect_param = effect ? "&announcement_effect=[url_encode(effect)]" : ""
+ var/identifier = "[sha1("[speaker][filter][pitch][special_filters][message][blip_base][blip_number][effect]")].[world.time]"
+ var/input = tts_speech_filter(message)
+ var/list/headers = list(
+ "Content-Type" = "application/json",
+ "Authorization" = CONFIG_GET(string/tts_http_token),
+ )
+
+ var/datum/http_request/request = new()
+ var/file_name = "tmp/tts/[identifier].ogg"
+ request.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts?voice=[speaker]&identifier=[identifier]&filter=[tts_filter_encode(filter, speaker, pitch)]&pitch=[pitch]&special_filters=[url_encode(special_filters)][effect_param]", json_encode(list("text" = input)), headers, file_name, timeout_seconds = CONFIG_GET(number/tts_http_timeout_seconds))
+
+ var/datum/tts_request/current_request = new /datum/tts_request(identifier, request, null, null, null, null, input, null, FALSE, /datum/language/common, INFINITY, 0, listeners, pitch, FALSE)
+ current_request.announcement = TRUE
+ current_request.announcement_listeners = listeners
+
+ var/list/queued = queued_tts_messages[src]
+ if(!queued)
+ queued = list()
+ queued_tts_messages[src] = queued
+ queued += current_request
+ if(length(in_process_http_messages) < max_concurrent_requests)
+ current_request.start_requests()
+ in_process_http_messages += current_request
+ else
+ queued_http_messages.insert(current_request)
+
+/datum/controller/subsystem/tts/proc/prepare_radio_announcement(atom/movable/source, message, effect)
+ if(!tts_enabled)
+ return
+
+ var/tts_text = announcement_text(message)
+ if(!tts_text)
+ return
+
+ var/speaker = announcement_voice(null, computer_voice)
+ if(!speaker)
+ return
+
+ var/filter = source?.voice_filter || ""
+ var/pitch = source?.pitch || 0
+ var/blip_base = source?.blip_base || "male"
+ var/blip_number = source?.blip_number || "1"
+ effect ||= announcement_effect(source, "radio_machine")
+ var/identifier = "[sha1("[speaker][filter][pitch][tts_text][blip_base][blip_number][effect]")].[world.time]"
+ var/language = source ? source.get_selected_language() : /datum/language/common
+ INVOKE_ASYNC(src, PROC_REF(queue_tts_message), source, tts_text, language, speaker, filter, list(), message_range = INFINITY, pitch = pitch, blip_base = blip_base, blip_number = blip_number, identifier = identifier, announcement_effect = effect)
+ return list(MODE_TTS_IDENTIFIER = identifier)
+
+/datum/tts_request/proc/start_announcement_requests()
+ request.begin_async()
+
+/datum/tts_request/proc/announcement_requests_errored()
+ var/datum/http_response/response = request.into_response()
+ return response.errored || response.status_code != 200
+
+/datum/tts_request/proc/announcement_requests_completed()
+ return request.is_complete()
+
+/datum/tts_request/proc/play_announcement()
+ for(var/mob/listener as anything in announcement_listeners)
+ if(!SStts.can_hear_announcement_tts(listener))
+ continue
+ var/volume = listener.client.prefs.read_preference(/datum/preference/numeric/volume/sound_tts_volume)
+ if(!volume)
+ continue
+ var/sound/audio = sound(audio_file)
+ audio.volume = 85 * (volume / 100)
+ SEND_SOUND(listener, audio)
diff --git a/modular_meta/features/ntts-nd-tg-tts/code/middleware.dm b/modular_meta/features/ntts-nd-tg-tts/code/middleware.dm
new file mode 100644
index 000000000000..3b1ec4c51010
--- /dev/null
+++ b/modular_meta/features/ntts-nd-tg-tts/code/middleware.dm
@@ -0,0 +1,57 @@
+/datum/preference_middleware/tts/New(datum/preferences/preferences)
+ . = ..()
+ action_delegations = action_delegations.Copy()
+ action_delegations["open_tts_voice_picker"] = PROC_REF(open_voice_picker)
+
+/datum/preference_middleware/tts/proc/open_voice_picker(list/params, mob/user)
+ var/datum/tts_voice_picker/picker = new(src)
+ picker.ui_interact(user)
+ return TRUE
+
+/datum/preference_middleware/tts/play_voice(list/params, mob/user)
+ if(!COOLDOWN_FINISHED(src, tts_test_cooldown))
+ return TRUE
+ var/speaker = params["voice"]
+ var/message = params["message"]
+ var/pitch = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch)
+ var/blip_base = preferences.read_preference(/datum/preference/choiced/tts_blip_base)
+ if(blip_base == TTS_BLIPS_MASCULINE)
+ blip_base = "male"
+ else
+ blip_base = "female"
+ var/blip_number = preferences.read_preference(/datum/preference/numeric/tts_blip_number)
+ COOLDOWN_START(src, tts_test_cooldown, 0.5 SECONDS)
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, message, speaker = speaker, pitch = pitch, local = TRUE, blip_base = blip_base, blip_number = blip_number)
+ return TRUE
+
+/datum/preference_middleware/tts/play_voice_borg(list/params, mob/user)
+ if(!COOLDOWN_FINISHED(src, tts_test_cooldown))
+ return TRUE
+ var/speaker = params["voice"]
+ var/message = params["message"]
+ var/pitch = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch)
+ var/blip_base = preferences.read_preference(/datum/preference/choiced/tts_blip_base)
+ if(blip_base == TTS_BLIPS_MASCULINE)
+ blip_base = "male"
+ else
+ blip_base = "female"
+ var/blip_number = preferences.read_preference(/datum/preference/numeric/tts_blip_number)
+ COOLDOWN_START(src, tts_test_cooldown, 0.5 SECONDS)
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, message, speaker = speaker, pitch = pitch, special_filters = TTS_FILTER_SILICON, local = TRUE, blip_base = blip_base, blip_number = blip_number)
+ return TRUE
+
+/datum/preference_middleware/tts/play_blips(list/params, mob/user)
+ if(!COOLDOWN_FINISHED(src, tts_test_cooldown))
+ return TRUE
+ var/speaker = params["voice"]
+ var/message = params["message"]
+ var/pitch = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch)
+ var/blip_base = preferences.read_preference(/datum/preference/choiced/tts_blip_base)
+ if(blip_base == TTS_BLIPS_MASCULINE)
+ blip_base = "male"
+ else
+ blip_base = "female"
+ var/blip_number = preferences.read_preference(/datum/preference/numeric/tts_blip_number)
+ COOLDOWN_START(src, tts_test_cooldown, 0.5 SECONDS)
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, message, speaker = speaker, pitch = pitch, local = TRUE, force_blips = TRUE, blip_base = blip_base, blip_number = blip_number)
+ return TRUE
diff --git a/modular_meta/features/ntts-nd-tg-tts/code/picker.dm b/modular_meta/features/ntts-nd-tg-tts/code/picker.dm
new file mode 100644
index 000000000000..33e300c62688
--- /dev/null
+++ b/modular_meta/features/ntts-nd-tg-tts/code/picker.dm
@@ -0,0 +1,111 @@
+/datum/tts_voice_picker
+ var/datum/preference_middleware/tts/middleware
+ var/datum/preferences/preferences
+
+/datum/tts_voice_picker/New(datum/preference_middleware/tts/middleware)
+ . = ..()
+ src.middleware = middleware
+ preferences = middleware.preferences
+
+/datum/tts_voice_picker/Destroy(force)
+ middleware = null
+ preferences = null
+ return ..()
+
+/datum/tts_voice_picker/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "TTSVoicePicker", "TTS Voice", 650, 720)
+ ui.open()
+
+/datum/tts_voice_picker/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/tts_voice_picker/ui_status(mob/user, datum/ui_state/state)
+ return user.client == preferences.parent ? UI_INTERACTIVE : UI_CLOSE
+
+/datum/tts_voice_picker/ui_close(mob/user)
+ qdel(src)
+
+/datum/tts_voice_picker/ui_data(mob/user)
+ return list(
+ "voice" = preferences.read_preference(/datum/preference/choiced/voice),
+ "pitch" = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch),
+ "blip_base" = preferences.read_preference(/datum/preference/choiced/tts_blip_base),
+ "blip_number" = preferences.read_preference(/datum/preference/numeric/tts_blip_number),
+ "voices" = user.client?.holder ? SStts.admin_voices() : SStts.player_voice_choices(),
+ "categories" = SStts.voice_categories,
+ )
+
+/datum/tts_voice_picker/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("set_voice")
+ return set_preference(/datum/preference/choiced/voice, params["value"])
+ if("set_pitch")
+ return set_preference(/datum/preference/numeric/tts_voice_pitch, params["value"])
+ if("set_blip_base")
+ return set_preference(/datum/preference/choiced/tts_blip_base, params["value"])
+ if("set_blip_number")
+ return set_preference(/datum/preference/numeric/tts_blip_number, params["value"])
+ if("play_voice")
+ return middleware.play_voice(params, ui.user)
+ if("play_robot")
+ return middleware.play_voice_borg(params, ui.user)
+ if("play_blips")
+ return middleware.play_blips(params, ui.user)
+ if("play_radio")
+ return play_radio(params, ui.user)
+ /*
+ if("play_radio_ion")
+ return play_radio(params, ui.user, TRUE)
+ */
+ if("play_gas_mask")
+ return play_mask(params, ui.user, /obj/item/clothing/mask/gas)
+ if("play_sec_hailer")
+ return play_mask(params, ui.user, /obj/item/clothing/mask/gas/sechailer)
+
+ return FALSE
+
+/datum/tts_voice_picker/proc/set_preference(preference_type, value)
+ var/datum/preference/preference = GLOB.preference_entries[preference_type]
+ if(!preferences.update_preference(preference, value))
+ return FALSE
+ SStgui.update_uis(preferences)
+ return TRUE
+
+/datum/tts_voice_picker/proc/play_radio(list/params, mob/user, ion = FALSE)
+ if(!COOLDOWN_FINISHED(middleware, tts_test_cooldown))
+ return TRUE
+ var/identifier = "tts_preview_[REF(user)]_[world.time]_[rand(1, 999999)]"
+ var/list/radio_messages = list()
+ radio_messages[TTS_GHOST_RADIO] = list(user)
+ SStts.queued_radio_messages[identifier] = radio_messages
+ SStts.queued_radio_messages_compression[identifier] = 0
+ // SStts.queued_radio_messages_compression[identifier] = ion ? 31 : 0
+ COOLDOWN_START(middleware, tts_test_cooldown, 0.5 SECONDS)
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, params["message"], speaker = params["voice"], pitch = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch), listeners = list(), blip_base = get_blip_base(), blip_number = preferences.read_preference(/datum/preference/numeric/tts_blip_number), identifier = identifier)
+ return TRUE
+
+/datum/tts_voice_picker/proc/play_mask(list/params, mob/user, obj/item/clothing/mask/mask_type)
+ if(!COOLDOWN_FINISHED(middleware, tts_test_cooldown))
+ return TRUE
+ var/speaker = initial(mask_type.voice_override) || params["voice"]
+ var/list/filter = list()
+ if(initial(mask_type.voice_filter))
+ filter += initial(mask_type.voice_filter)
+ var/list/special_filter = list()
+ if(initial(mask_type.use_radio_beeps_tts))
+ special_filter |= TTS_FILTER_RADIO
+ COOLDOWN_START(middleware, tts_test_cooldown, 0.5 SECONDS)
+ INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, params["message"], speaker = speaker, filter = filter.Join(","), pitch = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch), special_filters = special_filter.Join("|"), local = TRUE, blip_base = get_blip_base(), blip_number = preferences.read_preference(/datum/preference/numeric/tts_blip_number))
+ return TRUE
+
+/datum/tts_voice_picker/proc/get_blip_base()
+ var/blip_base = preferences.read_preference(/datum/preference/choiced/tts_blip_base)
+ if(blip_base == TTS_BLIPS_MASCULINE)
+ return "male"
+ return "female"
diff --git a/modular_meta/features/ntts-nd-tg-tts/code/preferences.dm b/modular_meta/features/ntts-nd-tg-tts/code/preferences.dm
new file mode 100644
index 000000000000..58c0d5ec34de
--- /dev/null
+++ b/modular_meta/features/ntts-nd-tg-tts/code/preferences.dm
@@ -0,0 +1,25 @@
+/datum/preference/choiced/voice/init_possible_values()
+ if(SStts.tts_enabled)
+ return SStts.admin_voices()
+ if(fexists("data/cached_tts_voices.json"))
+ var/list/text_data = rustg_file_read("data/cached_tts_voices.json")
+ var/list/cached_data = json_decode(text_data)
+ if(!cached_data)
+ return list("invalid")
+ return cached_data
+ return list("invalid")
+
+/datum/preference/choiced/voice/apply_to_human(mob/living/carbon/human/target, value)
+ if(SStts.tts_enabled)
+ var/is_admin = target.client?.holder
+ var/list/allowed = is_admin ? SStts.admin_voices() : SStts.player_voice_choices()
+ if(!(value in allowed))
+ value = SStts.random_tts_voice(target.gender)
+ target.voice = value
+
+/datum/preference/choiced/voice/compile_constant_data()
+ . = ..()
+ var/list/categories = list()
+ for(var/voice in .["choices"])
+ categories[voice] = SStts.voice_categories[voice] || "other"
+ .["categories"] = categories
diff --git a/modular_meta/features/ntts-nd-tg-tts/code/voices.dm b/modular_meta/features/ntts-nd-tg-tts/code/voices.dm
new file mode 100644
index 000000000000..fea341837c08
--- /dev/null
+++ b/modular_meta/features/ntts-nd-tg-tts/code/voices.dm
@@ -0,0 +1,73 @@
+/datum/config_entry/str_list/admin_voices
+
+/datum/controller/subsystem/tts
+ var/list/player_speakers = list()
+ var/list/admin_speakers = list()
+
+/datum/controller/subsystem/tts/proc/valid_voice_list(list/voices)
+ var/list/valid = list()
+ if(!islist(voices))
+ return valid
+ for(var/voice in voices)
+ if(voice in available_speakers)
+ valid += voice
+ return valid
+
+/datum/controller/subsystem/tts/proc/refresh_voice_pools()
+ var/list/admin_only = valid_voice_list(CONFIG_GET(str_list/admin_voices))
+
+ var/list/reserved = list()
+ for(var/voice in admin_only)
+ reserved |= voice
+
+ player_speakers = list()
+ for(var/voice in available_speakers)
+ if(!(voice in reserved))
+ player_speakers += voice
+ if(!length(player_speakers))
+ player_speakers = available_speakers.Copy()
+
+ admin_speakers = player_speakers.Copy()
+ for(var/voice in admin_only)
+ admin_speakers |= voice
+ if(!length(admin_speakers))
+ admin_speakers = available_speakers.Copy()
+
+/datum/controller/subsystem/tts/proc/player_voice_choices()
+ return length(player_speakers) ? player_speakers.Copy() : available_speakers.Copy()
+
+/datum/controller/subsystem/tts/proc/admin_voices()
+ return length(admin_speakers) ? admin_speakers.Copy() : available_speakers.Copy()
+
+
+
+/datum/controller/subsystem/tts/proc/random_from_voices(list/voices, gender = NEUTER)
+ if(!length(voices))
+ return null
+
+ var/list/gendered = list()
+ for(var/voice in voices)
+ if(gender == MALE && (findtext(voice, "Man") || findtext(voice, "Male")))
+ if(findtext(voice, "Woman")) // someone told me that findtext() will find woman if man is in string
+ continue
+ gendered += voice
+ else if(gender == FEMALE && (findtext(voice, "Woman") || findtext(voice, "Female")))
+ gendered += voice
+
+ if(length(gendered))
+ return pick(gendered)
+ return pick(voices)
+
+/datum/controller/subsystem/tts/random_tts_voice(gender = NEUTER)
+ if(!tts_enabled)
+ return null
+ return random_from_voices(player_voice_choices(), gender)
+
+/datum/controller/subsystem/tts/random_vendor_voice()
+ if(!tts_enabled)
+ return null
+
+ for(var/voice in available_speakers)
+ if(findtext(voice, "Glados"))
+ return voice
+ return random_from_voices(player_voice_choices())
diff --git a/modular_meta/features/ntts-nd-tg-tts/includes.dm b/modular_meta/features/ntts-nd-tg-tts/includes.dm
new file mode 100644
index 000000000000..25474c723a68
--- /dev/null
+++ b/modular_meta/features/ntts-nd-tg-tts/includes.dm
@@ -0,0 +1,12 @@
+#include "code\preferences.dm"
+#include "code\voices.dm"
+#include "code\middleware.dm"
+#include "code\picker.dm"
+#include "code\announcements.dm"
+
+/datum/modpack/ntts_nd_tg_tts
+ id = "ntts-nd-tg-tts"
+ name = "ntts && /tg/tts"
+ group = "Features"
+ desc = "Так звучит мой голос"
+ author = "Bruh24"
diff --git a/modular_meta/main_modular_include.dm b/modular_meta/main_modular_include.dm
index ff0fb4d264a5..869537e7fdde 100644
--- a/modular_meta/main_modular_include.dm
+++ b/modular_meta/main_modular_include.dm
@@ -50,6 +50,7 @@
#include "features\bot_topic\includes.dm"
#include "features\metacoins\includes.dm"
#include "features\spaceman_races\includes.dm"
+#include "features\ntts-nd-tg-tts\includes.dm"
#include "features\meta_redesign\includes.dm"
/* --- Reverts --- */
diff --git a/sound/attributions.txt b/sound/attributions.txt
index 1de71db183f8..ade4c8fe7e7b 100644
--- a/sound/attributions.txt
+++ b/sound/attributions.txt
@@ -245,3 +245,6 @@ sound/music/antag/blobalert.ogg and sound/effects/blob/blobattack.ogg -- "SlimeA
sound/effect/droplet.ogg -- "Drop - Water" By mattfinarelli -- https://freesound.org/people/mattfinarelli/sounds/533146/ -- License: Creative Commons 0
sound/effects/swapper/swap_A.ogg and swap_B.ogg by ArcaneMusic, License: Creative Commons 0, man-made mouth noises, slight cleanup in audacity.
+
+sound/machines/data_transmission.ogg -- Elements from Tim Verberne -- https://freesound.org/people/Tim_Verberne/sounds/558942/ -- License: Creative Commons 0
+ and samples from Ping.ogg from throughout the codebase.
diff --git a/sound/machines/data_transmission.ogg b/sound/machines/data_transmission.ogg
new file mode 100644
index 000000000000..d5261f27ba2f
Binary files /dev/null and b/sound/machines/data_transmission.ogg differ
diff --git a/sound/machines/tram/arrivals_line_processed.ogg b/sound/machines/tram/arrivals_line_processed.ogg
new file mode 100644
index 000000000000..612372d277fd
Binary files /dev/null and b/sound/machines/tram/arrivals_line_processed.ogg differ
diff --git a/sound/machines/tram/escape_line_processed.ogg b/sound/machines/tram/escape_line_processed.ogg
new file mode 100644
index 000000000000..65b3fe802a6a
Binary files /dev/null and b/sound/machines/tram/escape_line_processed.ogg differ
diff --git a/sound/machines/tram/medical_line_processed.ogg b/sound/machines/tram/medical_line_processed.ogg
new file mode 100644
index 000000000000..e0fd05a30706
Binary files /dev/null and b/sound/machines/tram/medical_line_processed.ogg differ
diff --git a/sound/machines/tram/other_line_processed.ogg b/sound/machines/tram/other_line_processed.ogg
new file mode 100644
index 000000000000..4bce47b61006
Binary files /dev/null and b/sound/machines/tram/other_line_processed.ogg differ
diff --git a/sound/machines/tram/tram_loop.ogg b/sound/machines/tram/tram_loop.ogg
new file mode 100644
index 000000000000..0c1dc12280bd
Binary files /dev/null and b/sound/machines/tram/tram_loop.ogg differ
diff --git a/sound/machines/tram/tram_start.ogg b/sound/machines/tram/tram_start.ogg
new file mode 100644
index 000000000000..fb5e3f687ba5
Binary files /dev/null and b/sound/machines/tram/tram_start.ogg differ
diff --git a/sound/machines/tram/tram_stop.ogg b/sound/machines/tram/tram_stop.ogg
new file mode 100644
index 000000000000..e14fd5b2fdd5
Binary files /dev/null and b/sound/machines/tram/tram_stop.ogg differ
diff --git a/tgstation.dme b/tgstation.dme
index b663cf8f4b52..4581e2abf2fc 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -657,7 +657,6 @@
#include "code\_onclick\hud\screen_objects\credits.dm"
#include "code\_onclick\hud\screen_objects\fullscreen.dm"
#include "code\_onclick\hud\screen_objects\ghost.dm"
-#include "code\_onclick\hud\screen_objects\guardian.dm"
#include "code\_onclick\hud\screen_objects\human.dm"
#include "code\_onclick\hud\screen_objects\map_popups.dm"
#include "code\_onclick\hud\screen_objects\map_view.dm"
@@ -904,6 +903,7 @@
#include "code\datums\voice_of_god_command.dm"
#include "code\datums\weakrefs.dm"
#include "code\datums\world_topic.dm"
+#include "code\datums\3d_sounds\_3d_sound.dm"
#include "code\datums\achievements\_achievement_data.dm"
#include "code\datums\achievements\_awards.dm"
#include "code\datums\achievements\admin_panel.dm"
@@ -948,6 +948,7 @@
#include "code\datums\actions\mobs\defensive_mode.dm"
#include "code\datums\actions\mobs\fire_breath.dm"
#include "code\datums\actions\mobs\ground_slam.dm"
+#include "code\datums\actions\mobs\guardians.dm"
#include "code\datums\actions\mobs\lava_swoop.dm"
#include "code\datums\actions\mobs\meteors.dm"
#include "code\datums\actions\mobs\mobcooldown.dm"
@@ -1261,6 +1262,7 @@
#include "code\datums\components\manual_heart.dm"
#include "code\datums\components\marionette.dm"
#include "code\datums\components\martial_art_giver.dm"
+#include "code\datums\components\material_turf_tracking.dm"
#include "code\datums\components\mind_linker.dm"
#include "code\datums\components\mind_martial_art.dm"
#include "code\datums\components\mirv.dm"
@@ -1799,6 +1801,7 @@
#include "code\datums\looping_sounds\machinery_sounds.dm"
#include "code\datums\looping_sounds\music.dm"
#include "code\datums\looping_sounds\projectiles.dm"
+#include "code\datums\looping_sounds\tram.dm"
#include "code\datums\looping_sounds\vents.dm"
#include "code\datums\looping_sounds\weather.dm"
#include "code\datums\mapgen\CaveGenerator.dm"
@@ -1825,6 +1828,8 @@
#include "code\datums\materials\hauntium.dm"
#include "code\datums\materials\meat.dm"
#include "code\datums\materials\pizza.dm"
+#include "code\datums\materials\material_slots\_slot.dm"
+#include "code\datums\materials\material_slots\generic.dm"
#include "code\datums\materials\properties\_properties.dm"
#include "code\datums\materials\properties\derived.dm"
#include "code\datums\materials\properties\optional.dm"
@@ -1981,6 +1986,7 @@
#include "code\datums\quirks\positive_quirks\freerunning.dm"
#include "code\datums\quirks\positive_quirks\friendly.dm"
#include "code\datums\quirks\positive_quirks\jolly.dm"
+#include "code\datums\quirks\positive_quirks\keen_nose.dm"
#include "code\datums\quirks\positive_quirks\light_step.dm"
#include "code\datums\quirks\positive_quirks\mime_fan.dm"
#include "code\datums\quirks\positive_quirks\musician.dm"
@@ -2238,7 +2244,6 @@
#include "code\game\machinery\botlaunchpad.dm"
#include "code\game\machinery\buttons.dm"
#include "code\game\machinery\cell_charger.dm"
-#include "code\game\machinery\civilian_bounties.dm"
#include "code\game\machinery\constructable_frame.dm"
#include "code\game\machinery\dance_machine.dm"
#include "code\game\machinery\defibrillator_mount.dm"
@@ -2317,6 +2322,9 @@
#include "code\game\machinery\camera\presets.dm"
#include "code\game\machinery\camera\silicon_camera.dm"
#include "code\game\machinery\camera\trackable.dm"
+#include "code\game\machinery\civilian_bounty\bounty_cube.dm"
+#include "code\game\machinery\civilian_bounty\bounty_machinery.dm"
+#include "code\game\machinery\civilian_bounty\civilian_bounty_trims.dm"
#include "code\game\machinery\computer\_computer.dm"
#include "code\game\machinery\computer\accounting.dm"
#include "code\game\machinery\computer\aifixer.dm"
@@ -5784,7 +5792,6 @@
#include "code\modules\modular_computers\file_system\data.dm"
#include "code\modules\modular_computers\file_system\image_file.dm"
#include "code\modules\modular_computers\file_system\program.dm"
-#include "code\modules\modular_computers\file_system\program_circuit.dm"
#include "code\modules\modular_computers\file_system\programs\airestorer.dm"
#include "code\modules\modular_computers\file_system\programs\alarm.dm"
#include "code\modules\modular_computers\file_system\programs\arcade.dm"
@@ -6545,6 +6552,8 @@
#include "code\modules\surgery\organs\internal\cyberimp\augments_internal.dm"
#include "code\modules\surgery\organs\internal\ears\_ears.dm"
#include "code\modules\surgery\organs\internal\eyes\_eyes.dm"
+#include "code\modules\surgery\organs\internal\eyes\eyes_augments.dm"
+#include "code\modules\surgery\organs\internal\eyes\eyes_species.dm"
#include "code\modules\surgery\organs\internal\heart\_heart.dm"
#include "code\modules\surgery\organs\internal\heart\heart_anomalock.dm"
#include "code\modules\surgery\organs\internal\heart\heart_ethereal.dm"
@@ -6754,6 +6763,7 @@
#include "code\modules\wiremod\components\abstract\list_variable.dm"
#include "code\modules\wiremod\components\abstract\module.dm"
#include "code\modules\wiremod\components\abstract\variable.dm"
+#include "code\modules\wiremod\components\action\camera.dm"
#include "code\modules\wiremod\components\action\equpiment_action.dm"
#include "code\modules\wiremod\components\action\laserpointer.dm"
#include "code\modules\wiremod\components\action\light.dm"
@@ -6863,6 +6873,7 @@
#include "code\modules\wiremod\core\integrated_circuit.dm"
#include "code\modules\wiremod\core\marker.dm"
#include "code\modules\wiremod\core\port.dm"
+#include "code\modules\wiremod\core\program_circuit.dm"
#include "code\modules\wiremod\core\usb_cable.dm"
#include "code\modules\wiremod\core\variable.dm"
#include "code\modules\wiremod\datatypes\any.dm"
diff --git a/tgui/packages/tgui-panel/chat/renderer.tsx b/tgui/packages/tgui-panel/chat/renderer.tsx
index 18d2f2c6137b..9e2fb95384e7 100644
--- a/tgui/packages/tgui-panel/chat/renderer.tsx
+++ b/tgui/packages/tgui-panel/chat/renderer.tsx
@@ -205,6 +205,7 @@ class ChatRenderer {
const highlightWholeMessage = setting.highlightWholeMessage;
const matchWord = setting.matchWord;
const matchCase = setting.matchCase;
+ const enabled = setting.enabled;
const allowedRegex = /^[a-zа-яё0-9_\-$/^[\s\]\\]+$/gi;
const regexEscapeCharacters = /[!#$%^&*)(+=.<>{}[\]:;'"|~`_\-\\/]/g;
const lines = String(text)
@@ -275,6 +276,7 @@ class ChatRenderer {
this.highlightParsers = [];
}
this.highlightParsers.push({
+ enabled,
highlightWords,
highlightRegex,
highlightColor,
@@ -436,17 +438,19 @@ class ChatRenderer {
// Highlight text
if (!message.avoidHighlighting && this.highlightParsers) {
- this.highlightParsers.forEach((parser) => {
- const highlighted = highlightNode(
- node,
- parser.highlightRegex,
- parser.highlightWords,
- (text) => createHighlightNode(text, parser.highlightColor),
- );
- if (highlighted && parser.highlightWholeMessage) {
- node.className += ' ChatMessage--highlighted';
- }
- });
+ this.highlightParsers
+ .filter((parser) => parser.enabled)
+ .forEach((parser) => {
+ const highlighted = highlightNode(
+ node,
+ parser.highlightRegex,
+ parser.highlightWords,
+ (text) => createHighlightNode(text, parser.highlightColor),
+ );
+ if (highlighted && parser.highlightWholeMessage) {
+ node.className += ' ChatMessage--highlighted';
+ }
+ });
}
// Linkify text
const linkifyNodes = node.querySelectorAll('.linkify');
diff --git a/tgui/packages/tgui-panel/settings/TextHighlight.tsx b/tgui/packages/tgui-panel/settings/TextHighlight.tsx
index 60c942979d97..2b352f5dae18 100644
--- a/tgui/packages/tgui-panel/settings/TextHighlight.tsx
+++ b/tgui/packages/tgui-panel/settings/TextHighlight.tsx
@@ -1,3 +1,4 @@
+import { useMemo } from 'react';
import {
Box,
Button,
@@ -61,6 +62,22 @@ export function TextHighlightSettings(props) {
);
}
+const oneCharacterRegex = /^(\[.*\]|\\.|.)$/;
+
+function extractRegex(highlight: string): string | null {
+ if (
+ highlight.charAt(0) !== '/' ||
+ highlight.charAt(highlight.length - 1) !== '/'
+ ) {
+ return null;
+ }
+ const expr = highlight.substring(1, highlight.length - 1);
+ if (oneCharacterRegex.test(expr)) {
+ return null;
+ }
+ return expr;
+}
+
function TextHighlightSetting(props) {
const { id, ...rest } = props;
const {
@@ -69,6 +86,7 @@ function TextHighlightSetting(props) {
removeHighlight,
} = useHighlights();
const {
+ enabled,
highlightColor,
highlightText,
highlightWholeMessage,
@@ -76,10 +94,37 @@ function TextHighlightSetting(props) {
matchCase,
} = highlightSettingById[id];
+ const highlightRegex = useMemo(
+ () => extractRegex(highlightText),
+ [highlightText],
+ );
+
+ const isRegexValid = useMemo(() => {
+ if (!highlightRegex) return true;
+ try {
+ new RegExp(highlightRegex, 'g');
+ return true;
+ } catch {
+ return false;
+ }
+ }, [highlightRegex]);
+
return (
+
+ updateHighlight({
+ id,
+ enabled: !enabled,
+ })
+ }
+ >
+ Enabled
+
@@ -145,6 +152,28 @@ function AnnouncementSound(props) {
);
}
+//MASSMETA EDIT ADDITION START (ntts && tgtts)
+function AnnouncementVoice(props) {
+ const { act, data } = useBackend();
+ const { tts_voice, tts_voices = [] } = data;
+
+ return (
+
+
+ act('set_tts_voice', {
+ picked_voice: value,
+ })
+ }
+ />
+
+ );
+}
+//MASSMETA EDIT ADDITION END (ntts && tgtts)
+
/** Creates the report textarea with a submit button. */
function ReportText(props) {
const { act, data } = useBackend();
diff --git a/tgui/packages/tgui/interfaces/DSMBook.tsx b/tgui/packages/tgui/interfaces/DSMBook.tsx
index f2884b8e10e9..c61607567b23 100644
--- a/tgui/packages/tgui/interfaces/DSMBook.tsx
+++ b/tgui/packages/tgui/interfaces/DSMBook.tsx
@@ -1,421 +1,122 @@
-import { useState } from 'react';
import { useBackend } from 'tgui/backend';
-import { Box, Button, Section, Stack } from 'tgui-core/components';
-import { Window } from '../layouts';
-import deforest_logo from '../styles/assets/bg-deforest.svg';
+import { Box, Section, Stack } from 'tgui-core/components';
+import { type BookEntry, BookUI, type TOCEntry } from './common/PaginatedBook';
+import '../styles/interfaces/DeForestLogo.scss';
type Trauma = {
- full_name: string; // full "medical" name
- scan_name: string; // shortened name shown on scan
- desc: string; // short description of trauma effect
- symptoms: string; // longer description of how it affects people
- id: string; // typepath
-};
-
-type TraumaPage = Trauma & {
- page: number; // page to put the trauma on
-};
-
-type TOCEntry = {
- trauma: Trauma;
- displayed_page: number; // page to put the toc entry on
- entry_page: number; // page the trauma is on
+ full_name: string;
+ scan_name: string;
+ desc: string;
+ symptoms: string;
+ id: string;
};
type TraumaData = {
traumas: Trauma[];
};
-// fits as many traumas as possible on a page
-function getTOCWithPages(traumas: Trauma[], windowheight: number) {
- const page_height = windowheight - 50;
- const pages: TOCEntry[] = [];
- let current_page = 1;
- let current_page_height = 0;
-
- for (const trauma of traumas) {
- // estimate height of toc entry
- const estimated_height = 30;
-
- if (current_page_height + estimated_height > page_height) {
- // move to next page
- current_page += 1;
- current_page_height = 0;
- }
-
- const pageentry: TOCEntry = {
- trauma: trauma,
- displayed_page: current_page,
- entry_page: -1, // to be filled later
- };
-
- pages.push(pageentry);
-
- current_page_height += estimated_height;
- }
-
- return pages;
+function checkIdenticalTraumaNames(trauma: BookEntry) {
+ return trauma.full_name.toLowerCase() === trauma.scan_name.toLowerCase();
}
-function getTraumasWithPages(
- traumas: Trauma[],
- windowheight: number,
- start_page: number,
-) {
- const page_height = windowheight - 50;
- const pages: TraumaPage[] = [];
- let current_page = start_page;
- let current_page_height = 0;
+function estimateHeight(entry: BookEntry) {
+ const title_height = 40;
+ const subttitle_height = checkIdenticalTraumaNames(entry) ? 0 : 20;
+ const desc_height = Math.ceil(entry.desc.length / 50) * 12.5;
+ const symptoms_height = Math.ceil(entry.symptoms.length / 50) * 12.5;
+ const extra_spacing = 40;
- for (const trauma of traumas) {
- // estimate height of trauma entry
- const title_height = 40;
- const subttitle_height = checkIdenticalTraumaNames(trauma) ? 0 : 20;
- const desc_height = Math.ceil(trauma.desc.length / 50) * 12.5;
- const symptoms_height = Math.ceil(trauma.symptoms.length / 50) * 12.5;
- const extra_spacing = 40;
-
- const estimated_height =
- title_height +
- subttitle_height +
- desc_height +
- symptoms_height +
- extra_spacing;
-
- if (current_page_height + estimated_height > page_height) {
- // move to next page
- current_page += 1;
- current_page_height = 0;
- }
-
- const pageentry: TraumaPage = {
- ...trauma,
- page: current_page,
- };
-
- pages.push(pageentry);
-
- current_page_height += estimated_height;
- }
-
- return pages;
-}
-
-function checkIdenticalTraumaNames(traumas: Trauma) {
- return traumas.full_name.toLowerCase() === traumas.scan_name.toLowerCase();
+ return (
+ title_height +
+ subttitle_height +
+ desc_height +
+ symptoms_height +
+ extra_spacing
+ );
}
-type DSMEntryComponentProps = {
- trauma: TraumaPage;
-};
-
-const DSMEntryComponent = (props: DSMEntryComponentProps) => {
- const { trauma } = props;
-
+function renderDSMEntry(entry: BookEntry) {
return (
- {trauma.full_name}
- {!checkIdenticalTraumaNames(trauma) && (
- ...aka "{trauma.scan_name}"
+ {entry.full_name}
+ {!checkIdenticalTraumaNames(entry) && (
+ ...aka "{entry.scan_name}"
)}
}
>
-
+
Description:
{' '}
- {trauma.desc}
+ {entry.desc}
Diagnosis:
{' '}
- {trauma.symptoms}
+ {entry.symptoms}
);
-};
-
-type TocEntryComponentProps = {
- entry: TOCEntry;
- setPage: (page: number) => void;
-};
-
-const TOCEntryComponent = (props: TocEntryComponentProps) => {
- const { entry, setPage } = props;
+}
+function renderTOCEntry(tocentry: TOCEntry) {
return (
-
-
-
- {entry.trauma.full_name}
- {!checkIdenticalTraumaNames(entry.trauma) && (
-
- ({entry.trauma.scan_name})
-
- )}
-
-
-
-
+ {tocentry.entry.full_name}
+ {!checkIdenticalTraumaNames(tocentry.entry) && (
+
-
-
- setPage(entry.entry_page - (entry.entry_page % 2))}
>
- {entry.entry_page}
-
-
-
- );
-};
-
-type PageTurnProps = {
- page: number;
- setPage: (page: number) => void;
- title: string;
- maxPage: number;
-};
-
-const PageTurn = (props: PageTurnProps) => {
- const { page, setPage, title, maxPage } = props;
- const { act } = useBackend();
-
- return (
-
-
- {
- setPage(1);
- act('play_flip_sound');
- }}
- fluid
- disabled={page <= 1}
- />
-
-
- {
- setPage(page - 2);
- act('play_flip_sound');
- }}
- fluid
- disabled={page <= 1}
- />
-
-
-
-
- {page}
-
- ~ {title} ~
-
- {page + 1}
-
-
-
-
- {
- setPage(page + 2);
- act('play_flip_sound');
- }}
- fluid
- disabled={page + 1 >= maxPage}
- />
-
-
- {
- setPage(maxPage - (maxPage % 2));
- act('play_flip_sound');
- }}
- fluid
- disabled={page + 1 >= maxPage}
- />
-
+ ({tocentry.entry.scan_name})
+
+ )}
);
-};
-
-type FakePageProps = {
- tocDisplay?: TOCEntry[];
- traumaDisplay?: TraumaPage[];
- setPage: (page: number) => void;
-};
-
-const FakePage = (props: FakePageProps) => {
- const { tocDisplay, traumaDisplay, setPage } = props;
-
- return (
-
-
- {tocDisplay?.map((entry) => (
-
-
-
- ))}
- {traumaDisplay?.map((trauma) => (
-
-
-
- ))}
- {!tocDisplay?.length && !traumaDisplay?.length && (
-
-
-
- )}
-
-
- );
-};
-
-type FakePagesProps = {
- tocDisplay?: TOCEntry[];
- traumaDisplay?: TraumaPage[];
- page: number;
- setPage: (page: number) => void;
-};
-
-const FakePages = (props: FakePagesProps) => {
- const { tocDisplay, traumaDisplay, page, setPage } = props;
-
- const leftTOC = tocDisplay?.filter((entry) => entry.displayed_page === page);
- const rightTOC = tocDisplay?.filter(
- (entry) => entry.displayed_page === page + 1,
- );
-
- const leftTrauma = traumaDisplay?.filter((trauma) => trauma.page === page);
- const rightTrauma = traumaDisplay?.filter(
- (trauma) => trauma.page === page + 1,
- );
+}
- return (
-
-
-
-
-
-
-
-
- );
-};
+const blankDSMPage = (
+
+);
export const DSMBook = () => {
const { data } = useBackend();
const { traumas } = data;
- // abc it up
- const traumasSorted = [...traumas].sort((a, b) =>
- a.full_name > b.full_name ? 1 : -1,
- );
-
- const windowheight = 565; // used in measurements below
- const allTOCWithPages = getTOCWithPages(traumasSorted, windowheight);
-
- // determine what page toc ends and traumas begin on
- let finalTOCPage = allTOCWithPages.reduce(
- (maxpage, entry) => Math.max(maxpage, entry.displayed_page),
- 1,
- );
-
- // if final toc page is odd, make it even, so traumas start on the left
- if (finalTOCPage % 2 !== 0) {
- finalTOCPage += 1;
- }
-
- const allTraumaWithPages = getTraumasWithPages(
- traumasSorted,
- windowheight,
- finalTOCPage + 1,
- );
-
- // determine final page number for each TOC entry
- for (const trauma of traumas) {
- const traumaPage = allTraumaWithPages.find((t) => t.id === trauma.id);
- const tocEntry = allTOCWithPages.find((e) => e.trauma.id === trauma.id);
- if (traumaPage && tocEntry) {
- tocEntry.entry_page = traumaPage.page;
- }
- }
-
- const lastPage = allTraumaWithPages.reduce(
- (maxpage, trauma) => Math.max(maxpage, trauma.page),
- finalTOCPage,
- );
-
- // two pages are shown at once, so front page is 1+2
- const [page, setPage] = useState(1);
+ const traumaEntries: BookEntry[] = traumas
+ .map((trauma) => ({
+ id: trauma.id,
+ full_name: trauma.full_name,
+ scan_name: trauma.scan_name,
+ desc: trauma.desc,
+ symptoms: trauma.symptoms,
+ }))
+ .sort((a, b) => (a.full_name > b.full_name ? 1 : -1));
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+ estimateHeight={estimateHeight}
+ renderEntry={renderDSMEntry}
+ renderTOCEntry={renderTOCEntry}
+ blankPage={blankDSMPage}
+ />
);
};
diff --git a/tgui/packages/tgui/interfaces/IDCBook.tsx b/tgui/packages/tgui/interfaces/IDCBook.tsx
new file mode 100644
index 000000000000..44a17acb4f61
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/IDCBook.tsx
@@ -0,0 +1,256 @@
+import { useBackend } from 'tgui/backend';
+import { Box, Section, Stack } from 'tgui-core/components';
+import { type BookEntry, BookUI, type TOCEntry } from './common/PaginatedBook';
+import '../styles/interfaces/DeForestLogo.scss';
+
+type Disease = {
+ name: string; // used by symptoms and diseases - name of the disease or symptom
+ desc: string; // used by symptoms and diseases - description of the disease or symptom
+ form: string; // used by symptoms and diseases - something like symptom, virus, bacteria, etc
+ agent: string; // used by diseases - the agent that causes the disease, like a virus or bacteria name
+ spread_by: string; // used by diseases - how the disease is spread, like "Airborne" or "Contact"
+ cured_by: string | null; // used by symptoms and diseases - how the disease or symptom is cured, like a medicine name or "Unknown"
+ illness: string; // used by symptoms - the common illness associated with the symptom, like "Flu" for "Fever"
+ id: string;
+};
+
+type DiseaseData = {
+ diseases: Disease[];
+};
+
+function isSymptom(entry: BookEntry) {
+ return entry.form.toLowerCase() === 'symptom';
+}
+
+function formToColor(form: string) {
+ switch (form.toLowerCase()) {
+ case 'symptom':
+ return 'lightgreen';
+ case 'condition':
+ case 'infection':
+ case 'parasite':
+ return 'lightsalmon';
+ case 'virus':
+ case 'bacteria':
+ case 'fungus':
+ return 'lightblue';
+ default:
+ return 'white';
+ }
+}
+
+function formatSpreadBy(spread_by: string) {
+ switch (spread_by.toLowerCase()) {
+ case 'airborne':
+ return (
+
+ Airborne
+
+ (e.g. coughing, sneezing)
+
+
+ );
+ case 'skin contact':
+ case 'contact':
+ return (
+
+ Contact
+
+ (e.g. touching infected persons)
+
+
+ );
+ case 'fluid contact':
+ return (
+
+ Liquids
+
+ (e.g. touching contaminated fluids)
+
+
+ );
+ case 'blood':
+ return (
+
+ Blood
+
+ (e.g. tranfusion of infected blood)
+
+
+ );
+ case 'none':
+ return (
+
+ None
+
+ (non-contagious)
+
+
+ );
+ default:
+ return spread_by;
+ }
+}
+
+function estimateHeight(entry: BookEntry) {
+ const title_height = 40;
+ const extra_spacing = 40;
+ if (isSymptom(entry)) {
+ const desc_height = Math.ceil(entry.desc.length / 50) * 14;
+ const illness_height = entry.illness !== 'Unidentified' ? 10 : 0;
+ const cured_by_height = 10;
+
+ return (
+ title_height +
+ desc_height +
+ illness_height +
+ cured_by_height +
+ extra_spacing
+ );
+ }
+
+ const desc_height = Math.ceil(entry.desc.length / 50) * 14;
+ const agent_height = 15;
+ const spread_by_height = 15;
+ const cured_by_height = 15;
+
+ return (
+ title_height +
+ desc_height +
+ agent_height +
+ spread_by_height +
+ cured_by_height +
+ extra_spacing
+ );
+}
+
+function renderDSMEntry(entry: BookEntry) {
+ return (
+
+ {entry.name}
+
+ ({entry.form})
+
+
+ }
+ >
+
+ {isSymptom(entry) ? (
+ <>
+
+
+ Description:
+ {' '}
+ {entry.desc}
+
+ {entry.illness !== 'Unidentified' && (
+
+
+ Common associated illness:
+ {' '}
+ "{entry.illness}"
+
+ )}
+ {!!entry.cured_by && (
+
+
+ Common cures:
+ {' '}
+ {entry.cured_by}
+
+ )}
+ >
+ ) : (
+ <>
+
+
+ Agent:
+ {' '}
+ {entry.agent}
+
+
+
+ Description:
+ {' '}
+ {entry.desc}
+
+
+
+ Spread By:
+ {' '}
+ {formatSpreadBy(entry.spread_by)}
+
+
+
+ Cured By:
+ {' '}
+ {entry.cured_by}
+
+ >
+ )}
+
+
+ );
+}
+
+function renderTOCEntry(tocentry: TOCEntry) {
+ return (
+
+ {tocentry.entry.name}
+
+ ({tocentry.entry.form})
+
+
+ );
+}
+
+const blankIDCPage = (
+
+);
+
+export const IDCBook = () => {
+ const { data } = useBackend();
+ const { diseases } = data;
+
+ const diseaseEntries: BookEntry[] = diseases
+ .map((disease) => ({
+ id: disease.id,
+ name: disease.name,
+ desc: disease.desc,
+ form: disease.form,
+ agent: disease.agent,
+ spread_by: disease.spread_by,
+ cured_by: disease.cured_by,
+ illness: disease.illness,
+ }))
+ .sort((a, b) =>
+ a.form > b.form ? 1 : a.form < b.form ? -1 : a.name > b.name ? 1 : -1,
+ );
+
+ return (
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/MedicalRecords/RecordView.tsx b/tgui/packages/tgui/interfaces/MedicalRecords/RecordView.tsx
index 41b43607803a..da8357294f77 100644
--- a/tgui/packages/tgui/interfaces/MedicalRecords/RecordView.tsx
+++ b/tgui/packages/tgui/interfaces/MedicalRecords/RecordView.tsx
@@ -2,6 +2,7 @@ import { useState } from 'react';
import {
Box,
Button,
+ Input,
LabeledList,
NoticeBox,
RestrictedInput,
@@ -43,6 +44,7 @@ export const MedicalRecordView = (props) => {
major_disabilities,
minor_disabilities,
physical_status,
+ cause_of_death,
mental_status,
name,
quirk_notes,
@@ -166,6 +168,23 @@ export const MedicalRecordView = (props) => {
{physical_status}
+ {physical_status === 'Deceased' && (
+
+
+
+ act('set_cause_of_death', {
+ crew_ref: crew_ref,
+ cause: value,
+ })
+ }
+ />
+
+
+ )}
{
const isSelected = button === mental_status;
diff --git a/tgui/packages/tgui/interfaces/MedicalRecords/types.ts b/tgui/packages/tgui/interfaces/MedicalRecords/types.ts
index 4b6422a92b85..c44cd1048676 100644
--- a/tgui/packages/tgui/interfaces/MedicalRecords/types.ts
+++ b/tgui/packages/tgui/interfaces/MedicalRecords/types.ts
@@ -20,6 +20,7 @@ export type MedicalRecord = {
major_disabilities: string;
minor_disabilities: string;
physical_status: string;
+ cause_of_death: string;
mental_status: string;
name: string;
notes: MedicalNote[];
diff --git a/tgui/packages/tgui/interfaces/NtosCamera.tsx b/tgui/packages/tgui/interfaces/NtosCamera.tsx
index 100bbf8c0d8b..c55c084a3ee4 100644
--- a/tgui/packages/tgui/interfaces/NtosCamera.tsx
+++ b/tgui/packages/tgui/interfaces/NtosCamera.tsx
@@ -13,7 +13,9 @@ import { useBackend } from '../backend';
import { NtosWindow } from '../layouts';
type NtosCameraCommonData = {
- size: number;
+ width: number;
+ height: number;
+ size: string;
minSize: number;
maxSize: number;
maxNameLength: number;
@@ -48,6 +50,8 @@ export const NtosCamera = (props) => {
export const NtosCameraContent = (props) => {
const { act, data } = useBackend();
const {
+ width,
+ height,
size,
minSize,
maxSize,
@@ -74,7 +78,7 @@ export const NtosCameraContent = (props) => {
maxWidth="100%"
height="auto"
fixErrors
- src={photo}
+ src={`data:image/jpeg;base64,${photo}`}
/>
@@ -86,7 +90,11 @@ export const NtosCameraContent = (props) => {
disabled={!canEditMetadata}
value={name}
maxLength={maxNameLength}
- onChange={(value) => act('setName', { value })}
+ onChange={(value) =>
+ act('setName', {
+ value: value,
+ })
+ }
/>
@@ -98,7 +106,11 @@ export const NtosCameraContent = (props) => {
disabled={!canEditMetadata}
value={desc}
maxLength={maxDescLength}
- onChange={(value) => act('setDesc', { value })}
+ onChange={(value) =>
+ act('setDesc', {
+ value: value,
+ })
+ }
/>
@@ -111,7 +123,11 @@ export const NtosCameraContent = (props) => {
disabled={!canEditMetadata}
value={caption}
maxLength={maxCaptionLength}
- onChange={(value) => act('setCaption', { value })}
+ onChange={(value) =>
+ act('setCaption', {
+ value: value,
+ })
+ }
/>
@@ -138,17 +154,39 @@ export const NtosCameraContent = (props) => {
)}
+ {size}
+
+
+ Half Width Aperture:
+
+
+ act('adjustWidth', {
+ value: value,
+ })
+ }
+ />
+
- Photo Size:
+ Half Height Aperture:
act('adjustSize', { value })}
+ onChange={(e, value) =>
+ act('adjustHeight', {
+ value: value,
+ })
+ }
/>
diff --git a/tgui/packages/tgui/interfaces/NtosFileManager.tsx b/tgui/packages/tgui/interfaces/NtosFileManager.tsx
index 2e4af805686f..8bb426d9491e 100644
--- a/tgui/packages/tgui/interfaces/NtosFileManager.tsx
+++ b/tgui/packages/tgui/interfaces/NtosFileManager.tsx
@@ -90,7 +90,7 @@ const PrintDialog = (props: PrintDialogProps) => {
left="5%"
>
-
+
{printTypes.map((type, i) => {
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/MainPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/MainPage.tsx
index 6bacbdfccd39..2906d3b74dd0 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/MainPage.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/MainPage.tsx
@@ -483,6 +483,16 @@ export function MainPage(props: MainPageProps) {
const nonContextualPreferences = {
...data.character_preferences.non_contextual,
};
+ // MASSMETA EDIT START (ntts && /tg/tts)
+ // Those aren't needed anymore. voice pitch, blip are now acessible in dedicated panel
+ for (const preference of [
+ 'tts_voice_pitch',
+ 'tts_blip_base',
+ 'tts_blip_number',
+ ]) {
+ delete nonContextualPreferences[preference];
+ }
+ // MASSMETA EDIT END (ntts && /tg/tts)
if (randomBodyEnabled) {
nonContextualPreferences.random_species =
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/tts_voice.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/tts_voice.tsx
index d8e8ab003267..73871d359c38 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/tts_voice.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/tts_voice.tsx
@@ -1,5 +1,8 @@
import { useBackend } from 'tgui/backend';
-import { Button, Stack } from 'tgui-core/components';
+//MASSMETA EDIT START (ntts && tgtts)
+//original: import { Button, Stack } from 'tgui-core/components';
+import { Box, Button, LabeledList, Stack } from 'tgui-core/components';
+//MASSMETA EDIT START (ntts && tgtts)
import {
type FeatureChoiced,
@@ -14,14 +17,15 @@ function FeatureTTSDropdownInput(
props: FeatureValueProps,
) {
const { act } = useBackend();
+ //MASSMETA EDIT START (ntts && tgtts)
+ const { non_contextual } = props.character_preferences;
+ const blipBase = String(non_contextual.tts_blip_base);
+ const blipNumber = String(non_contextual.tts_blip_number);
+ //MASSMETA EDIT END (ntts && tgtts)
- return (
-
-
-
-
-
- {
act('play_voice');
}}
@@ -40,6 +44,43 @@ function FeatureTTSDropdownInput(
height="100%"
/>
+
+ {
+ act('play_blips');
+ }}
+ icon="leaf"
+ width="100%"
+ height="100%"
+ />
+ */
+ //MASSMETA EDIT REMOVAL END (ntts && tgtts)
+
+ //MASSMETA EDIT START (ntts && tgtts)
+ return (
+
+
+ Current TTS voice
+ {props.value}
+
+
+ {String(non_contextual.tts_voice_pitch)}
+
+ {blipBase}
+
+ {blipBase} {blipNumber}
+
+
+
+
+ act('open_tts_voice_picker')}
+ >
+ Change Voice
+
+ {/*MASSMETA EDIT END (ntts && tgtts) */}
+
);
}
@@ -53,3 +94,13 @@ export const tts_voice_pitch: FeatureNumeric = {
name: 'Voice Pitch Adjustment',
component: FeatureSliderInput,
};
+
+export const tts_blip_base: FeatureChoiced = {
+ name: 'Voice Blip Base',
+ component: FeatureDropdownInput,
+};
+
+export const tts_blip_number: FeatureNumeric = {
+ name: 'Voice Blip Variant',
+ component: FeatureSliderInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/sounds.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/sounds.tsx
index fc4f6942d846..541de5a5a966 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/sounds.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/sounds.tsx
@@ -73,6 +73,24 @@ export const sound_tts: FeatureChoiced = {
component: FeatureDropdownInput,
};
+export const sound_tts_radio: FeatureChoiced = {
+ name: 'Enable TTS Over Radio',
+ category: 'SOUND',
+ description: `
+ When enabled, be able to hear text-to-speech sounds in game over radio channels.
+ When set to "Departmental Radio Only", text to speech over the radio will only play for departmental radio channels. Anything that isn't Common.
+ When disabled, text to speech will not play over radio sources.
+ `,
+ component: FeatureDropdownInput,
+};
+
+export const sound_tts_hear_self_radio: FeatureToggle = {
+ name: 'Enable TTS Hear Self Over Radio',
+ category: 'SOUND',
+ description: 'When enabled, hear yourself over the radio when Text to Speech and TTS Over Radio is enabled.',
+ component: CheckboxInput,
+};
+
export const sound_tts_volume: Feature = {
name: 'TTS Volume',
category: 'SOUND',
@@ -80,6 +98,13 @@ export const sound_tts_volume: Feature = {
component: FeatureSliderInput,
};
+export const sound_tts_radio_volume: Feature = {
+ name: 'TTS Radio Volume',
+ category: 'SOUND',
+ description: 'The volume that radio text-to-speech sounds will play at. This is independent of regular TTS volume.',
+ component: FeatureSliderInput,
+};
+
export const sound_lobby_volume: Feature = {
name: 'Lobby music volume',
category: 'SOUND',
diff --git a/tgui/packages/tgui/interfaces/Secrets.jsx b/tgui/packages/tgui/interfaces/Secrets.jsx
index 63635054d591..b2864483e58e 100644
--- a/tgui/packages/tgui/interfaces/Secrets.jsx
+++ b/tgui/packages/tgui/interfaces/Secrets.jsx
@@ -120,13 +120,13 @@ const HelpfulTab = (props) => {
-
- Your admin button here, coder!
-
+ content="Fix station gravity"
+ onClick={() => act('fix_gravity')}
+ />
;
+ pitch: number;
+ voice: string;
+ voices: string[];
+};
+
+type VoiceAction = {
+ action: string;
+ icon: string;
+ tone: string;
+ tooltip: string;
+};
+
+const VOICE_ACTIONS: VoiceAction[] = [
+ {
+ action: 'play_voice',
+ icon: 'play',
+ tone: 'voice',
+ tooltip: 'Normal voice preview',
+ },
+ {
+ action: 'play_robot',
+ icon: 'robot',
+ tone: 'robot',
+ tooltip: 'Robot voice preview',
+ },
+ {
+ action: 'play_radio',
+ icon: 'broadcast-tower',
+ tone: 'radio',
+ tooltip: 'Radio preview',
+ },
+ {
+ action: 'play_blips',
+ icon: 'leaf',
+ tone: 'blips',
+ tooltip: 'Blips preview',
+ },
+ /*
+ {
+ action: 'play_radio_ion',
+ icon: 'bolt',
+ tone: 'ion',
+ tooltip: 'Ion interference preview',
+ },
+ */
+ {
+ action: 'play_gas_mask',
+ icon: 'theater-masks',
+ tone: 'mask',
+ tooltip: 'Gas mask preview',
+ },
+ {
+ action: 'play_sec_hailer',
+ icon: 'bullhorn',
+ tone: 'hailer',
+ tooltip: 'Security hailer preview',
+ },
+];
+
+function titleize(value: string) {
+ if (value === ALL_CATEGORY) {
+ return 'All';
+ }
+ return value.replace(/[_-]/g, ' ');
+}
+
+export function TTSVoicePicker(props) {
+ const { act, data } = useBackend();
+ const {
+ blip_base,
+ blip_number,
+ categories = {},
+ pitch,
+ voice,
+ voices = [],
+ } = data;
+ const [category, setCategory] = useState(ALL_CATEGORY);
+ const [message, setMessage] = useState(TEST_MESSAGE);
+ const [search, setSearch] = useState('');
+
+ const categoryCounts = useMemo(() => {
+ const counts: Record = {
+ [ALL_CATEGORY]: voices.length,
+ };
+ for (const item of voices) {
+ const voiceCategory = categories[item] || 'other';
+ counts[voiceCategory] = (counts[voiceCategory] || 0) + 1;
+ }
+ return counts;
+ }, [categories, voices]);
+
+ const categoryList = useMemo(() => {
+ return [
+ ALL_CATEGORY,
+ ...Object.keys(categoryCounts)
+ .filter((name) => name !== ALL_CATEGORY)
+ .sort((left, right) => left.localeCompare(right)),
+ ];
+ }, [categoryCounts]);
+
+ const query = search.toLowerCase();
+ const shownVoices = useMemo(() => {
+ return voices.filter((item) => {
+ const voiceCategory = categories[item] || 'other';
+ return (
+ (category === ALL_CATEGORY || voiceCategory === category) &&
+ item.toLowerCase().includes(query)
+ );
+ });
+ }, [categories, category, query, voices]);
+
+ function preview(action: string, previewVoice = voice) {
+ act(action, {
+ message,
+ voice: previewVoice,
+ });
+ }
+
+ return (
+
+
+
+
+
+
+ {categoryList.map((item) => (
+
+ setCategory(item)}
+ >
+
+ {titleize(item)}
+
+
+ {categoryCounts[item]}
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ act('set_pitch', { value })}
+ />
+
+
+ act('set_blip_base', { value })}
+ />
+
+
+
+ act('set_blip_number', { value })
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {shownVoices.map((item) => (
+
+
+
+
+ {item}
+
+
+ {titleize(categories[item] || 'other')}
+
+
+
+ act('set_voice', { value: item })}
+ />
+
+ {VOICE_ACTIONS.map((voiceAction) => (
+
+
+ preview(voiceAction.action, item)
+ }
+ />
+
+ ))}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/tgui/packages/tgui/interfaces/TacVisorEyesMenu.tsx b/tgui/packages/tgui/interfaces/TacVisorEyesMenu.tsx
new file mode 100644
index 000000000000..4ba290916679
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/TacVisorEyesMenu.tsx
@@ -0,0 +1,75 @@
+import { Button, Dropdown, LabeledList, Section } from 'tgui-core/components';
+import { useBackend } from '../backend';
+import { Window } from '../layouts';
+
+type Data = {
+ friendlyFaction: string;
+ hostileFaction: string;
+ visorDisplay: string;
+ threatFlags: number;
+
+ validFriendlyFactions: string[];
+ validHostileFactions: string[];
+ visorOptions: string[];
+ threatOptions: Record;
+};
+
+export const TacVisorEyesMenu = (props) => {
+ const { act, data } = useBackend();
+ const {
+ friendlyFaction,
+ hostileFaction,
+ visorDisplay,
+ threatFlags,
+ validFriendlyFactions,
+ validHostileFactions,
+ visorOptions,
+ threatOptions,
+ } = data;
+ return (
+
+
+
+
+
+ act('set_friendly', { faction: value })}
+ />
+
+
+ act('set_hostile', { faction: value })}
+ />
+
+
+ act('set_display', { display: value })}
+ />
+
+
+
+
+ {Object.keys(threatOptions).map((option) => (
+
+ act('set_threat_flags', {
+ threat_flags: threatFlags ^ threatOptions[option],
+ })
+ }
+ >
+ {option}
+
+ ))}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/common/PaginatedBook.tsx b/tgui/packages/tgui/interfaces/common/PaginatedBook.tsx
new file mode 100644
index 000000000000..b855cd7d8820
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/common/PaginatedBook.tsx
@@ -0,0 +1,404 @@
+import { useState } from 'react';
+import { useBackend } from 'tgui/backend';
+import { Box, Button, Section, Stack } from 'tgui-core/components';
+import { Window } from '../../layouts';
+
+export type BookEntry = {
+ id: string;
+} & Type;
+
+export type TOCEntry = {
+ entry: BookEntry;
+ displayedPage: number;
+ entryPage: number;
+};
+
+type PageEntry = BookEntry & {
+ page: number;
+};
+
+function getTOCWithPages(
+ entries: BookEntry[],
+ windowHeight: number,
+) {
+ const pageHeight = windowHeight - 50;
+ const pages: TOCEntry[] = [];
+ let currentPage = 1;
+ let currentPageHeight = 0;
+
+ for (const entry of entries) {
+ const estimatedHeight = 30;
+
+ if (currentPageHeight + estimatedHeight > pageHeight) {
+ currentPage += 1;
+ currentPageHeight = 0;
+ }
+
+ pages.push({
+ entry,
+ displayedPage: currentPage,
+ entryPage: -1,
+ });
+
+ currentPageHeight += estimatedHeight;
+ }
+
+ return pages;
+}
+
+function getEntriesWithPages(
+ entries: BookEntry[],
+ windowHeight: number,
+ startPage: number,
+ estimateHeight: (entry: BookEntry) => number,
+) {
+ const pageHeight = windowHeight - 50;
+ const pages: PageEntry[] = [];
+ let currentPage = startPage;
+ let currentPageHeight = 0;
+
+ for (const entry of entries) {
+ const estimatedHeight = estimateHeight(entry);
+
+ if (currentPageHeight + estimatedHeight > pageHeight) {
+ currentPage += 1;
+ currentPageHeight = 0;
+ }
+
+ pages.push({
+ ...entry,
+ page: currentPage,
+ });
+
+ currentPageHeight += estimatedHeight;
+ }
+
+ return pages;
+}
+
+type EntryComponentProps = {
+ entry: PageEntry;
+ renderEntry: (entry: PageEntry) => React.ReactNode;
+};
+
+const EntryComponent = (props: EntryComponentProps) => {
+ const { entry, renderEntry } = props;
+ return ;
+};
+
+type TOCEntryComponentProps = {
+ entry: TOCEntry;
+ setPage: (page: number) => void;
+ renderTOCEntry: (entry: TOCEntry) => React.ReactNode;
+};
+
+const TOCEntryComponent = (props: TOCEntryComponentProps) => {
+ const { entry, setPage, renderTOCEntry } = props;
+ return (
+
+ {renderTOCEntry(entry)}
+
+
+
+
+
+ setPage(
+ entry.entryPage % 2 === 0 ? entry.entryPage - 1 : entry.entryPage,
+ )
+ }
+ >
+ {entry.entryPage}
+
+
+
+ );
+};
+
+type FakePageProps = {
+ tocDisplay?: TOCEntry[];
+ entryDisplay?: PageEntry[];
+ setPage: (page: number) => void;
+ renderEntry: (entry: PageEntry) => React.ReactNode;
+ renderTOCEntry: (entry: TOCEntry) => React.ReactNode;
+ blankPage?: React.ReactNode;
+};
+
+const FakePage = (props: FakePageProps) => {
+ const {
+ tocDisplay,
+ entryDisplay,
+ setPage,
+ renderEntry,
+ renderTOCEntry,
+ blankPage,
+ } = props;
+
+ return (
+
+
+ {tocDisplay?.map((entry) => (
+
+
+
+ ))}
+ {entryDisplay?.map((entry) => (
+
+
+
+ ))}
+ {!tocDisplay?.length &&
+ !entryDisplay?.length &&
+ (blankPage || (
+ This page intentionally left blank.
+ ))}
+
+
+ );
+};
+
+type FakePagesProps = {
+ tocDisplay?: TOCEntry[];
+ entryDisplay?: PageEntry[];
+ page: number;
+ setPage: (page: number) => void;
+ renderEntry: (entry: PageEntry) => React.ReactNode;
+ renderTOCEntry: (entry: TOCEntry) => React.ReactNode;
+ blankPage?: React.ReactNode;
+};
+
+const FakePages = (props: FakePagesProps) => {
+ const {
+ tocDisplay,
+ entryDisplay,
+ page,
+ setPage,
+ renderEntry,
+ renderTOCEntry,
+ blankPage,
+ } = props;
+
+ const leftTOC = tocDisplay?.filter((entry) => entry.displayedPage === page);
+ const rightTOC = tocDisplay?.filter(
+ (entry) => entry.displayedPage === page + 1,
+ );
+
+ const leftEntries = entryDisplay?.filter((entry) => entry.page === page);
+ const rightEntries = entryDisplay?.filter((entry) => entry.page === page + 1);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+type PageTurnProps = {
+ page: number;
+ setPage: (page: number) => void;
+ title: string;
+ maxPage: number;
+};
+
+const PageTurn = (props: PageTurnProps) => {
+ const { page, setPage, title, maxPage } = props;
+ const { act } = useBackend();
+
+ return (
+
+
+ {
+ setPage(1);
+ act('play_flip_sound');
+ }}
+ fluid
+ disabled={page <= 1}
+ />
+
+
+ {
+ setPage(page - 2);
+ act('play_flip_sound');
+ }}
+ fluid
+ disabled={page <= 1}
+ />
+
+
+
+
+ {page}
+
+ ~ {title} ~
+
+ {page + 1}
+
+
+
+
+ {
+ setPage(page + 2);
+ act('play_flip_sound');
+ }}
+ fluid
+ disabled={page + 1 >= maxPage}
+ />
+
+
+ {
+ setPage(maxPage - (maxPage % 2));
+ act('play_flip_sound');
+ }}
+ fluid
+ disabled={page + 1 >= maxPage}
+ />
+
+
+ );
+};
+
+type BookUIProps = {
+ bookData: BookEntry[];
+ title: string;
+ theme?: string;
+ estimateHeight: (entry: BookEntry) => number;
+ renderEntry: (entry: PageEntry) => React.ReactNode;
+ renderTOCEntry: (entry: TOCEntry) => React.ReactNode;
+ blankPage?: React.ReactNode;
+};
+
+/**
+ * Provides a basic paginated book UI
+ *
+ * @prop bookData The data to be displayed in the book. Must be a list of BookEntry objects.
+ * @prop title The title of the book, displayed on the page turner.
+ * @prop theme The theme of the book window. Defaults to 'ntos_lightmode'.
+ * @prop estimateHeight A function that estimates the height of a given entry. Used for pagination.
+ * @prop renderEntry A function that renders a given entry. Used for displaying entries on pages.
+ * @prop renderTOCEntry A function that renders a given entry for the table of contents. Used for displaying entries in the TOC.
+ * @prop blankPage An optional React node to display on blank pages. If not provided, blank pages will display "This page intentionally left blank."
+ *
+ */
+export const BookUI = (props: BookUIProps) => {
+ const {
+ bookData,
+ title,
+ theme,
+ estimateHeight,
+ renderEntry,
+ renderTOCEntry,
+ blankPage,
+ } = props;
+
+ const windowHeight = 565;
+ const allTOCWithPages = getTOCWithPages(bookData, windowHeight);
+
+ let finalTOCPage = allTOCWithPages.reduce(
+ (maxPage, entry) => Math.max(maxPage, entry.displayedPage),
+ 1,
+ );
+
+ if (finalTOCPage % 2 !== 0) {
+ finalTOCPage += 1;
+ }
+
+ const allEntriesWithPages = getEntriesWithPages(
+ bookData,
+ windowHeight,
+ finalTOCPage + 1,
+ estimateHeight,
+ );
+
+ for (const entry of bookData) {
+ const entryPage = allEntriesWithPages.find((e) => e.id === entry.id);
+ const tocEntry = allTOCWithPages.find((e) => e.entry.id === entry.id);
+ if (entryPage && tocEntry) {
+ tocEntry.entryPage = entryPage.page;
+ }
+ }
+
+ const lastPage = allEntriesWithPages.reduce(
+ (maxPage, entry) => Math.max(maxPage, entry.page),
+ finalTOCPage,
+ );
+
+ const [page, setPage] = useState(1);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/styles/interfaces/CivCargoHoldTerminal.scss b/tgui/packages/tgui/styles/interfaces/CivCargoHoldTerminal.scss
new file mode 100644
index 000000000000..c3be10fe06e2
--- /dev/null
+++ b/tgui/packages/tgui/styles/interfaces/CivCargoHoldTerminal.scss
@@ -0,0 +1,23 @@
+.Tab_Flash {
+ box-shadow: 0 0 1px 2px #48abe0;
+ animation: shadowFade 0.5s forwards;
+}
+
+@keyframes shadowFade {
+ to {
+ box-shadow: 0 0 0 0 #48abe0;
+ }
+}
+
+.Marquee {
+ animation: marquee 5s linear infinite;
+}
+
+@keyframes marquee {
+ 0% {
+ transform: translateX(100%);
+ }
+ 100% {
+ transform: translateX(-100%);
+ }
+}
diff --git a/tgui/packages/tgui/styles/interfaces/DeForestLogo.scss b/tgui/packages/tgui/styles/interfaces/DeForestLogo.scss
new file mode 100644
index 000000000000..5c247781bd0b
--- /dev/null
+++ b/tgui/packages/tgui/styles/interfaces/DeForestLogo.scss
@@ -0,0 +1,7 @@
+.deforest_logo {
+ background-color: hsl(150, 100%, 18%);
+ -webkit-mask-image: url('../assets/bg-deforest.svg');
+ mask-image: url('../assets/bg-deforest.svg');
+ mask-repeat: no-repeat;
+ mask-position: center;
+}
diff --git a/tgui/packages/tgui/styles/interfaces/TTSVoicePicker.scss b/tgui/packages/tgui/styles/interfaces/TTSVoicePicker.scss
new file mode 100644
index 000000000000..7a347a5743de
--- /dev/null
+++ b/tgui/packages/tgui/styles/interfaces/TTSVoicePicker.scss
@@ -0,0 +1,169 @@
+.TTSVoicePicker {
+ background-image:
+ linear-gradient(180deg, hsla(185, 22%, 12%, 0.58), transparent 220px),
+ repeating-linear-gradient(
+ 0deg,
+ hsla(0, 0%, 100%, 0.022) 0,
+ hsla(0, 0%, 100%, 0.022) 1px,
+ transparent 1px,
+ transparent 4px
+ );
+ box-shadow: inset 0 0 0 1px hsla(165, 45%, 52%, 0.06);
+
+ .Section {
+ background-color: hsla(210, 16%, 8%, 0.82);
+ border: var(--border-thickness-tiny) solid hsla(165, 28%, 48%, 0.18);
+ box-shadow:
+ inset 0 1px 0 hsla(0, 0%, 100%, 0.05),
+ 0 6px 16px hsla(0, 0%, 0%, 0.18);
+ }
+
+ .Section__title {
+ border-bottom: var(--border-thickness-tiny) solid hsla(165, 28%, 48%, 0.16);
+ color: hsla(165, 70%, 76%, 0.94);
+ letter-spacing: 0;
+ text-transform: uppercase;
+ }
+
+ &__categoryButton {
+ background-color: hsla(0, 0%, 100%, 0.035);
+ border-left: 2px solid transparent;
+ text-align: left;
+
+ &:hover {
+ background-color: hsla(165, 24%, 50%, 0.1);
+ border-left-color: hsla(165, 60%, 64%, 0.55);
+ }
+
+ &.Button--selected {
+ background-color: hsla(165, 35%, 36%, 0.18);
+ border-left-color: var(--color-green);
+ box-shadow: inset 0 0 0 1px hsla(165, 60%, 64%, 0.14);
+ }
+ }
+
+ &__categoryCount {
+ background-color: hsla(0, 0%, 0%, 0.24);
+ border: var(--border-thickness-tiny) solid hsla(165, 28%, 48%, 0.16);
+ color: hsla(165, 70%, 78%, 0.86);
+ min-width: 2em;
+ padding: 0 4px;
+ text-align: right;
+ }
+
+ &__summary {
+ background:
+ linear-gradient(90deg, hsla(165, 34%, 26%, 0.22), transparent 58%),
+ hsla(210, 16%, 8%, 0.86);
+ }
+
+ &__currentLabel {
+ color: hsla(165, 70%, 78%, 0.72);
+ font-size: 10px;
+ letter-spacing: 0;
+ text-transform: uppercase;
+ }
+
+ &__currentVoice {
+ color: hsla(0, 0%, 100%, 0.96);
+ font-size: 18px;
+ font-weight: bold;
+ overflow-wrap: anywhere;
+ text-shadow: 0 0 8px hsla(165, 70%, 60%, 0.22);
+ }
+
+ &__voiceRow {
+ background:
+ linear-gradient(90deg, hsla(165, 28%, 32%, 0.08), transparent 46%),
+ hsla(0, 0%, 100%, 0.032);
+ border: var(--border-thickness-tiny) solid hsla(165, 20%, 45%, 0.12);
+ border-left: 2px solid hsla(165, 20%, 45%, 0.2);
+ box-shadow: inset 0 1px 0 hsla(0, 0%, 100%, 0.035);
+ min-height: 38px;
+ padding: 4px 6px;
+ transition:
+ background-color var(--transition-time-fast) ease-out,
+ border-color var(--transition-time-fast) ease-out,
+ box-shadow var(--transition-time-fast) ease-out;
+
+ &:hover {
+ background:
+ linear-gradient(90deg, hsla(165, 38%, 38%, 0.16), transparent 48%),
+ hsla(0, 0%, 100%, 0.055);
+ border-color: hsla(165, 48%, 58%, 0.24);
+ border-left-color: hsla(165, 60%, 64%, 0.72);
+ box-shadow:
+ inset 0 1px 0 hsla(0, 0%, 100%, 0.06),
+ 0 0 12px hsla(165, 50%, 45%, 0.08);
+ }
+
+ &--selected {
+ background:
+ linear-gradient(90deg, hsla(130, 52%, 38%, 0.22), transparent 54%),
+ hsla(120, 50%, 35%, 0.08);
+ border-color: var(--color-green);
+ border-left-color: var(--color-green);
+ box-shadow:
+ inset 0 1px 0 hsla(0, 0%, 100%, 0.08),
+ 0 0 14px hsla(120, 70%, 45%, 0.1);
+ }
+ }
+
+ &__voiceName {
+ color: hsla(0, 0%, 100%, 0.94);
+ font-weight: bold;
+ overflow-wrap: anywhere;
+ }
+
+ &__voiceCategory {
+ color: var(--color-label);
+ font-size: 11px;
+ overflow-wrap: anywhere;
+ }
+
+ &__iconButton {
+ border-color: hsla(165, 20%, 50%, 0.18);
+ min-width: 24px;
+ padding-left: 0;
+ padding-right: 0;
+ width: 24px;
+
+ &:hover {
+ box-shadow: 0 0 9px hsla(165, 48%, 56%, 0.14);
+ }
+
+ &--select.Button--selected {
+ border-color: var(--color-green);
+ color: var(--color-green);
+ }
+
+ &--voice {
+ color: hsla(165, 68%, 78%, 0.94);
+ }
+
+ &--robot {
+ color: hsla(195, 72%, 78%, 0.94);
+ }
+
+ &--radio {
+ color: hsla(48, 85%, 72%, 0.94);
+ }
+
+ &--blips {
+ color: hsla(112, 58%, 72%, 0.94);
+ }
+
+ /*
+ &--ion {
+ color: hsla(286, 72%, 78%, 0.94);
+ }
+ */
+ &--mask {
+ color: hsla(25, 72%, 72%, 0.94);
+ }
+
+ &--hailer {
+ color: hsla(355, 74%, 74%, 0.94);
+ }
+ }
+}
diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss
index 73ba423ab0bd..eae79cc8ef6a 100644
--- a/tgui/packages/tgui/styles/main.scss
+++ b/tgui/packages/tgui/styles/main.scss
@@ -49,6 +49,7 @@
@include meta.load-css('./interfaces/DetectiveBoard.scss');
@include meta.load-css('./interfaces/ColorPicker.scss');
@include meta.load-css('./interfaces/AnomalyTower.scss');
+@include meta.load-css('./interfaces/CivCargoHoldTerminal.scss');
// Layouts
@include meta.load-css('./layouts/Layout.scss');
@@ -74,3 +75,4 @@
// MASSMETA DOWN HERE
@include meta.load-css('./interfaces/MetaCoinSlot.scss');
+@include meta.load-css('./interfaces/TTSVoicePicker.scss');
diff --git a/tools/UpdatePaths/Scripts/95735_xenochitin.txt b/tools/UpdatePaths/Scripts/95735_xenochitin.txt
new file mode 100644
index 000000000000..c90c16f8c618
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/95735_xenochitin.txt
@@ -0,0 +1 @@
+/obj/item/stack/sheet/xenochitin : /obj/item/stack/sheet/animalhide/xeno{@OLD}
\ No newline at end of file
diff --git a/tools/tts/tts/tts.py b/tools/tts/tts/tts.py
index 445c24ec963e..490810ffeebe 100644
--- a/tools/tts/tts/tts.py
+++ b/tools/tts/tts/tts.py
@@ -10,7 +10,11 @@
from pydub.silence import split_on_silence
tts = TTS("tts_models/en/vctk/vits", progress_bar=False, gpu=False)
-letters_to_use = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
+# MASSMETA EDIT START (ntts && /tg/tts)
+# по русски говори йобана в рот
+# ORIGINAL: "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
+letters_to_use = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ1234567890"
+# MASSMETA EDIT END (ntts && /tg/tts)
random_factor = 0.35
os.makedirs('samples', exist_ok=True)
app = Flask(__name__)
@@ -54,16 +58,22 @@ def text_to_speech_blips():
with io.BytesIO() as data_bytes:
with torch.no_grad():
result_sound = None
- if not os.path.exists('samples/' + voice):
- os.makedirs('samples/' + voice, exist_ok=True)
- for i, value in enumerate(letters_to_use):
- tts.tts_to_file(text=value + ".", speaker=voice, file_path="samples/" + voice + "/" + value + ".wav")
- loaded_word = AudioSegment.from_file("samples/" + voice + "/" + value + ".wav")
- audio_chunks = split_on_silence(loaded_word, min_silence_len = 100, silence_thresh = -45, keep_silence = 50)
- combined = AudioSegment.empty()
- for chunk in audio_chunks:
- combined += chunk
- combined.export("samples/" + voice + "/" + value + ".wav", format='wav')
+ # MASSMETA EDIT START (ntts && /tg/tts)
+ samples_dir = "samples/" + voice
+ if not os.path.exists(samples_dir):
+ os.makedirs(samples_dir, exist_ok=True)
+ for value in letters_to_use:
+ letter_file = samples_dir + "/" + value + ".wav"
+ if os.path.isfile(letter_file):
+ continue
+ tts.tts_to_file(text=value + ".", speaker=voice, file_path=letter_file)
+ loaded_word = AudioSegment.from_file(letter_file)
+ audio_chunks = split_on_silence(loaded_word, min_silence_len = 100, silence_thresh = -45, keep_silence = 50)
+ combined = AudioSegment.empty()
+ for chunk in audio_chunks:
+ combined += chunk
+ combined.export(letter_file, format='wav')
+ # MASSMETA EDIT END (ntts && /tg/tts)
for i, letter in enumerate(text):
if not letter.isalpha() or letter.isnumeric() or letter == " ":
continue
diff --git a/tools/tts/tts/tts_rvc_OPEN_AND_READ_ME_BEFORE_USING.py b/tools/tts/tts/tts_rvc_OPEN_AND_READ_ME_BEFORE_USING.py
index a97b1aa27cfa..d5f31a1715ec 100644
--- a/tools/tts/tts/tts_rvc_OPEN_AND_READ_ME_BEFORE_USING.py
+++ b/tools/tts/tts/tts_rvc_OPEN_AND_READ_ME_BEFORE_USING.py
@@ -40,7 +40,7 @@
app = Flask(__name__)
tts = TTS(model_path = "E:/model_output_3/model_no_disc.pth", config_path = "E:/model_output_2/config.json", progress_bar=False, gpu=True)
-letters_to_use = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
+letters_to_use = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ1234567890"
random_factor = 0.35
os.makedirs('samples', exist_ok=True)
trim_leading_silence = lambda x: x[detect_leading_silence(x) :]
@@ -159,13 +159,17 @@ def text_to_speech_blips():
with io.BytesIO() as data_bytes:
with torch.no_grad():
result_sound = AudioSegment.empty()
- if not os.path.exists('samples/' + voice):
- os.makedirs('samples/' + voice, exist_ok=True)
- for i, value in enumerate(letters_to_use):
- tts.tts_to_file(text=value + ".", speaker=voice, file_path="samples/" + voice + "/" + value + ".wav")
- sound = AudioSegment.from_file("samples/" + voice + "/" + value + ".wav", format="wav")
- silenced_word = strip_silence(sound)
- silenced_word.export("samples/" + voice + "/" + value + ".wav", format='wav')
+ samples_dir = "samples/" + voice
+ if not os.path.exists(samples_dir):
+ os.makedirs(samples_dir, exist_ok=True)
+ for value in letters_to_use:
+ letter_file = samples_dir + "/" + value + ".wav"
+ if os.path.isfile(letter_file):
+ continue
+ tts.tts_to_file(text=value + ".", speaker=voice, file_path=letter_file)
+ sound = AudioSegment.from_file(letter_file, format="wav")
+ silenced_word = strip_silence(sound)
+ silenced_word.export(letter_file, format='wav')
speaker_id = "NO SPEAKER"
model_to_use = None
found_model = False