All Key Bindings: "
dat += "Reset to Default "
@@ -806,10 +753,10 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
/datum/preferences/proc/get_gear_metadata(var/datum/gear/G)
- . = loadout_gear[G.display_name]
+ . = loadout_gear[G.index_name]
if(!.)
. = list()
- loadout_gear[G.display_name] = .
+ loadout_gear[G.index_name] = .
/datum/preferences/proc/get_tweak_metadata(var/datum/gear/G, var/datum/gear_tweak/tweak)
var/list/metadata = get_gear_metadata(G)
@@ -1191,6 +1138,8 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
HTML += ShowDisabilityState(user, DISABILITY_FLAG_ALCOHOLE_ADDICT, "Alcohole addict")
if(!(S.blacklisted_disabilities & DISABILITY_FLAG_PARAPLEGIA))
HTML += ShowDisabilityState(user, DISABILITY_FLAG_PARAPLEGIA, "Paraplegia")
+ if(!(S.blacklisted_disabilities & DISABILITY_FLAG_APHASIA))
+ HTML += ShowDisabilityState(user, DISABILITY_FLAG_APHASIA, "Aphasia")
HTML += {"
\[Done\]
@@ -1280,6 +1229,60 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
SetChoices(user)
return 1
+/**
+ * Rebuilds the `loadout_gear` list of the [active_character], and returns the total end cost.
+ *
+ * Caches and cuts the existing [/datum/character_save/var/loadout_gear] list and remakes it, checking the `subtype_selection_cost` and overall cost validity of each item.
+ *
+ * If the item's [/datum/gear/var/subtype_selection_cost] is `FALSE`, any future items with the same [/datum/gear/var/main_typepath] will have their cost skipped.
+ * If adding the item will take the total cost over the maximum, it won't be added to the list.
+ *
+ * Arguments:
+ * * new_item - A new [/datum/gear] item to be added to the `loadout_gear` list.
+ */
+/datum/preferences/proc/build_loadout(datum/gear/new_item)
+ var/total_cost = 0
+ var/list/type_blacklist = list()
+ var/list/loadout_cache = loadout_gear.Copy()
+ loadout_gear.Cut()
+ tgui_loadout_gear.Cut()
+ choosen_gears.Cut()
+ if(new_item)
+ loadout_cache += "[new_item.index_name]"
+
+ for(var/item in loadout_cache)
+ var/datum/gear/gear = GLOB.gear_datums[item]
+ if(!gear)
+ continue
+ var/added_cost = gear.cost
+ if(!gear.subtype_cost_overlap) // If listings of the same subtype shouldn't have their cost added.
+ if(gear.path in type_blacklist)
+ added_cost = 0
+ else
+ type_blacklist += gear.path
+ if((total_cost + added_cost) > max_gear_slots)
+ continue // If the final cost is too high, don't add the item.
+ var/item_cache = loadout_cache[item]
+ loadout_gear[item] = item_cache ? item_cache : list()
+ var/tgui_data = list()
+ for(var/datum/gear_tweak/tweak in gear.gear_tweaks)
+ var/text_path = "[tweak.type]"
+ if(!(text_path in item_cache))
+ continue
+ var/params = item_cache[text_path]
+ var/list/data =tweak?.get_tgui_data(params)
+ if (!data)
+ continue
+ tgui_data[text_path] = data["display_param"]
+ tgui_data["name"] = data["name"]
+ tgui_data["icon"] = data["icon"]
+ tgui_data["icon_file"] = data["icon_file"]
+ tgui_data["icon_state"] = data["icon_state"]
+ tgui_loadout_gear[gear] = tgui_data
+ choosen_gears[item] = gear
+ total_cost += added_cost
+ return total_cost
+
/datum/preferences/proc/ResetJobs()
job_support_high = 0
job_support_med = 0
@@ -1491,47 +1494,6 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
SetRecords(user)
- if(href_list["preference"] == "gear")
- if(href_list["toggle_gear"])
- var/datum/gear/TG = GLOB.gear_datums[href_list["toggle_gear"]]
- if(TG.display_name in loadout_gear)
- loadout_gear -= TG.display_name
- choosen_gears -= TG.display_name
- else
- if(!TG.can_select(cl = user.client, species_name = S.name)) // all gear checks there, no jobs while prefs
- return
- var/total_cost = 0
- var/list/type_blacklist = list()
- for(var/gear_name in loadout_gear)
- var/datum/gear/G = GLOB.gear_datums[gear_name]
- if(istype(G))
- if(!G.subtype_cost_overlap)
- if(G.subtype_path in type_blacklist)
- continue
- type_blacklist += G.subtype_path
- total_cost += G.cost
-
- if((total_cost + TG.cost) <= max_gear_slots)
- loadout_gear += TG.display_name
- choosen_gears[TG.display_name] += new TG.type
- else if(href_list["gear"] && href_list["tweak"])
- var/datum/gear/gear = choosen_gears[href_list["gear"]]
- var/datum/gear_tweak/tweak = locate(href_list["tweak"])
- if(!tweak || !istype(gear) || !(tweak in gear.gear_tweaks))
- return
- var/metadata = tweak.get_metadata(user, get_tweak_metadata(gear, tweak))
- if(!metadata)
- return
- set_tweak_metadata(gear, tweak, metadata)
- else if(href_list["select_category"])
- gear_tab = href_list["select_category"]
- else if(href_list["clear_loadout"])
- loadout_gear.Cut()
- choosen_gears.Cut()
-
- ShowChoices(user)
- return
-
switch(href_list["task"])
if("random")
var/datum/robolimb/robohead
@@ -1763,16 +1725,16 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
if("hair")
if(species in list(SPECIES_HUMAN, SPECIES_UNATHI, SPECIES_TAJARAN, SPECIES_SKRELL, SPECIES_MACNINEPERSON, SPECIES_VULPKANIN, SPECIES_VOX, SPECIES_WRYN)) //Species that have hair. (No HAS_HAIR flag)
var/input = "Choose your character's hair colour:"
- var/new_hair = input(user, input, "Character Preference", h_colour) as color|null
- if(new_hair)
+ var/new_hair = tgui_input_color(user, input, "Character Preference", h_colour)
+ if(!isnull(new_hair))
h_colour = new_hair
if("secondary_hair")
if(species in list(SPECIES_HUMAN, SPECIES_UNATHI, SPECIES_TAJARAN, SPECIES_SKRELL, SPECIES_MACNINEPERSON, SPECIES_VULPKANIN, SPECIES_VOX))
var/datum/sprite_accessory/hair_style = GLOB.hair_styles_public_list[h_style]
if(hair_style.secondary_theme && !hair_style.no_sec_colour)
- var/new_hair = input(user, "Choose your character's secondary hair colour:", "Character Preference", h_sec_colour) as color|null
- if(new_hair)
+ var/new_hair = tgui_input_color(user, "Choose your character's secondary hair colour:", "Character Preference", h_sec_colour)
+ if(!isnull(new_hair))
h_sec_colour = new_hair
if("h_style")
@@ -1820,8 +1782,8 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
h_grad_offset_y = clamp(text2num(expl[2]) || 0, -16, 16)
if("h_grad_colour")
- var/result = input(user, "Choose your character's hair gradient colour:", "Character Preference", h_grad_colour) as color|null
- if(result)
+ var/result = tgui_input_color(user, "Choose your character's hair gradient colour:", "Character Preference", h_grad_colour)
+ if(!isnull(result))
h_grad_colour = result
if("h_grad_alpha")
@@ -1833,8 +1795,8 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
if("headaccessory")
if(S.bodyflags & HAS_HEAD_ACCESSORY) //Species with head accessories.
var/input = "Choose the colour of your your character's head accessory:"
- var/new_head_accessory = input(user, input, "Character Preference", hacc_colour) as color|null
- if(new_head_accessory)
+ var/new_head_accessory = tgui_input_color(user, input, "Character Preference", hacc_colour)
+ if(!isnull(new_head_accessory))
hacc_colour = new_head_accessory
if("ha_style")
@@ -1913,8 +1875,8 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
if("m_head_colour")
if(S.bodyflags & HAS_HEAD_MARKINGS) //Species with head markings.
var/input = "Choose the colour of your your character's head markings:"
- var/new_markings = input(user, input, "Character Preference", m_colours["head"]) as color|null
- if(new_markings)
+ var/new_markings = tgui_input_color(user, input, "Character Preference", m_colours["head"])
+ if(!isnull(new_markings))
m_colours["head"] = new_markings
if("m_style_body")
@@ -1939,8 +1901,8 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
if("m_body_colour")
if(S.bodyflags & HAS_BODY_MARKINGS) //Species with body markings/tattoos.
var/input = "Choose the colour of your your character's body markings:"
- var/new_markings = input(user, input, "Character Preference", m_colours["body"]) as color|null
- if(new_markings)
+ var/new_markings = tgui_input_color(user, input, "Character Preference", m_colours["body"])
+ if(!isnull(new_markings))
m_colours["body"] = new_markings
if("m_style_tail")
@@ -1969,8 +1931,8 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
if("m_tail_colour")
if(S.bodyflags & HAS_TAIL_MARKINGS) //Species with tail markings.
var/input = "Choose the colour of your your character's tail markings:"
- var/new_markings = input(user, input, "Character Preference", m_colours["tail"]) as color|null
- if(new_markings)
+ var/new_markings = tgui_input_color(user, input, "Character Preference", m_colours["tail"])
+ if(!isnull(new_markings))
m_colours["tail"] = new_markings
if("body_accessory")
@@ -1994,17 +1956,17 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
if("facial")
- if(species in list(SPECIES_HUMAN, SPECIES_UNATHI, SPECIES_TAJARAN, SPECIES_SKRELL, SPECIES_MACNINEPERSON, SPECIES_VULPKANIN, SPECIES_VOX, SPECIES_WRYN)) //Species that have facial hair. (No HAS_HAIR_FACIAL flag)
- var/new_facial = input(user, "Choose your character's facial-hair colour:", "Character Preference", f_colour) as color|null
- if(new_facial)
+ if(species in list(SPECIES_HUMAN, SPECIES_UNATHI, SPECIES_TAJARAN, SPECIES_SKRELL, SPECIES_MACNINEPERSON, SPECIES_VULPKANIN, SPECIES_VOX)) //Species that have facial hair. (No HAS_HAIR_FACIAL flag)
+ var/new_facial = tgui_input_color(user, "Choose your character's facial-hair colour:", "Character Preference", f_colour)
+ if(!isnull(new_facial))
f_colour = new_facial
if("secondary_facial")
if(species in list(SPECIES_HUMAN, SPECIES_UNATHI, SPECIES_TAJARAN, SPECIES_SKRELL, SPECIES_MACNINEPERSON, SPECIES_VULPKANIN, SPECIES_VOX))
var/datum/sprite_accessory/facial_hair_style = GLOB.facial_hair_styles_list[f_style]
if(facial_hair_style.secondary_theme && !facial_hair_style.no_sec_colour)
- var/new_facial = input(user, "Choose your character's secondary facial-hair colour:", "Character Preference", f_sec_colour) as color|null
- if(new_facial)
+ var/new_facial = tgui_input_color(user, "Choose your character's secondary facial-hair colour:", "Character Preference", f_sec_colour)
+ if(!isnull(new_facial))
f_sec_colour = new_facial
if("f_style")
@@ -2054,8 +2016,8 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
underwear = new_underwear
if("underwear_color")
- var/new_uwear_color = input(user, "Choose your character's underwear colour:", "Character Preference", underwear_color) as color|null
- if(new_uwear_color)
+ var/new_uwear_color = tgui_input_color(user, "Choose your character's underwear colour:", "Character Preference", underwear_color)
+ if(!isnull(new_uwear_color))
underwear_color = new_uwear_color
if("undershirt")
@@ -2074,8 +2036,8 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
undershirt = new_undershirt
if("undershirt_color")
- var/new_ushirt_color = input(user, "Choose your character's undershirt colour:", "Character Preference", undershirt_color) as color|null
- if(new_ushirt_color)
+ var/new_ushirt_color = tgui_input_color(user, "Choose your character's undershirt colour:", "Character Preference", undershirt_color)
+ if(!isnull(new_ushirt_color))
undershirt_color = new_ushirt_color
if("socks")
@@ -2094,8 +2056,8 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
socks = new_socks
if("eyes")
- var/new_eyes = input(user, "Choose your character's eye colour:", "Character Preference", e_colour) as color|null
- if(new_eyes)
+ var/new_eyes = tgui_input_color(user, "Choose your character's eye colour:", "Character Preference", e_colour)
+ if(!isnull(new_eyes))
e_colour = new_eyes
if("s_tone")
@@ -2121,13 +2083,13 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
if("skin")
if((S.bodyflags & HAS_SKIN_COLOR) || ((S.bodyflags & HAS_BODYACC_COLOR) && GLOB.body_accessory_by_species[species]) || check_rights(R_ADMIN, 0, user))
- var/new_skin = input(user, "Choose your character's skin colour: ", "Character Preference", s_colour) as color|null
- if(new_skin)
+ var/new_skin = tgui_input_color(user, "Choose your character's skin colour: ", "Character Preference", s_colour)
+ if(!isnull(new_skin))
s_colour = new_skin
if("ooccolor")
- var/new_ooccolor = input(user, "Choose your OOC colour:", "Game Preference", ooccolor) as color|null
- if(new_ooccolor)
+ var/new_ooccolor = tgui_input_color(user, "Choose your OOC colour:", "Game Preference", ooccolor)
+ if(!isnull(new_ooccolor))
ooccolor = new_ooccolor
if("bag")
@@ -2135,6 +2097,11 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
if(new_backbag)
backbag = new_backbag
+ if("loadout")
+ var/datum/ui_module/loadout/loadout = new()
+ loadout.ui_interact(user)
+ return FALSE
+
if("nt_relation")
var/new_relation = tgui_input_list(user, "Choose your relation to NT. Note that this represents what others can find out about your character by researching your background, not what your character actually thinks.", "Character Preference", list("Loyal", "Supportive", "Neutral", "Skeptical", "Opposed"))
if(new_relation)
@@ -2489,8 +2456,8 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
toggles2 ^= PREFTOGGLE_2_AFKWATCH
if("UIcolor")
- var/UI_style_color_new = input(user, "Choose your UI color, dark colors are not recommended!", UI_style_color) as color|null
- if(!UI_style_color_new) return
+ var/UI_style_color_new = tgui_input_color(user, "Choose your UI color, dark colors are not recommended!", UI_style_color)
+ if(isnull(UI_style_color_new)) return
UI_style_color = UI_style_color_new
if(ishuman(usr)) //mid-round preference changes, for aesthetics
@@ -2959,6 +2926,9 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts
if((disabilities & DISABILITY_FLAG_PARAPLEGIA) && !(new_species.blacklisted_disabilities & DISABILITY_FLAG_PARAPLEGIA))
character.force_gene_block(GLOB.paraplegiablock, TRUE, TRUE)
+ if((disabilities & DISABILITY_FLAG_APHASIA) && !(new_species.blacklisted_disabilities & DISABILITY_FLAG_APHASIA))
+ character.force_gene_block(GLOB.aphasiablock, TRUE, TRUE)
+
character.dna.species.handle_dna(character)
if(character.dna.dirtySE)
diff --git a/code/modules/client/preference/preferences_toggles.dm b/code/modules/client/preference/preferences_toggles.dm
index d047654588f..4bbd63e44ca 100644
--- a/code/modules/client/preference/preferences_toggles.dm
+++ b/code/modules/client/preference/preferences_toggles.dm
@@ -434,8 +434,8 @@
blackbox_message = "Set Own OOC"
/datum/preference_toggle/special_toggle/set_ooc_color/set_toggles(client/user)
- var/new_ooccolor = input(usr, "Please select your OOC color.", "OOC color", user.prefs.ooccolor) as color|null
- if(new_ooccolor)
+ var/new_ooccolor = tgui_input_color(usr, "Please select your OOC color.", "OOC color", user.prefs.ooccolor)
+ if(!isnull(new_ooccolor))
user.prefs.ooccolor = new_ooccolor
to_chat(usr, "Your OOC color has been set to [new_ooccolor].")
else
diff --git a/code/modules/clothing/glasses/hud.dm b/code/modules/clothing/glasses/hud.dm
index de7194f995e..0dc22fed05d 100644
--- a/code/modules/clothing/glasses/hud.dm
+++ b/code/modules/clothing/glasses/hud.dm
@@ -489,3 +489,39 @@ SKILLS
/obj/item/clothing/glasses/hud/skills/tajblind/attack_self(mob/user)
toggle_veil(user)
+
+/obj/item/clothing/glasses/hud/blueshield
+ name = "multi-mode HUD glasses"
+ desc = "Солнечные очки с многорежимным проекционным дисплеем."
+ ru_names = list(
+ NOMINATIVE = "много-режимные HUD-очки",
+ GENITIVE = "много-режимных HUD-очков",
+ DATIVE = "много-режимным HUD-очкам",
+ ACCUSATIVE = "много-режимные HUD-очки",
+ INSTRUMENTAL = "много-режимными HUD-очками",
+ PREPOSITIONAL = "много-режимных HUD-очках"
+ )
+ actions_types = list(/datum/action/item_action/switch_hud)
+ icon_state = "sunhudmed"
+ origin_tech = "magnets=4;combat=4;engineering=4;biotech=4"
+ see_in_dark = 1
+ flash_protect = FLASH_PROTECTION_FLASH
+ tint = 1
+ HUDType = DATA_HUD_MEDICAL_ADVANCED
+
+/obj/item/clothing/glasses/hud/blueshield/attack_self(mob/user)
+ if(HUDType)
+ var/datum/atom_hud/H = GLOB.huds[HUDType]
+ H.remove_hud_from(user)
+ switch(HUDType)
+ if(DATA_HUD_MEDICAL_ADVANCED)
+ HUDType = DATA_HUD_SECURITY_BASIC
+ examine_extensions = EXAMINE_HUD_SKILLS
+ if(DATA_HUD_SECURITY_ADVANCED)
+ HUDType = DATA_HUD_MEDICAL_ADVANCED
+ examine_extensions = EXAMINE_HUD_MEDICAL
+ else
+ HUDType = DATA_HUD_SECURITY_ADVANCED
+ examine_extensions = EXAMINE_HUD_SECURITY_READ | EXAMINE_HUD_SECURITY_WRITE
+ balloon_alert(user, "режим переключён")
+ return
diff --git a/code/modules/clothing/gloves/color.dm b/code/modules/clothing/gloves/color.dm
index 492bcc975dc..0aa3eb6a812 100644
--- a/code/modules/clothing/gloves/color.dm
+++ b/code/modules/clothing/gloves/color.dm
@@ -145,7 +145,7 @@
desc = "Pair of gloves with some protection"
icon_state = "armored_gloves"
item_state = "armored_gloves"
- armor = list("melee" = 5, "bullet" = 5, "laser" = 5, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 0, "acid" = 0)
+ armor = list("melee" = 5, "bullet" = 25, "laser" = 10, "energy" = 5, "bomb" = 5, "bio" = 0, "rad" = 0, "fire" = 75, "acid" = 75)
can_be_cut = FALSE
sprite_sheets = list(
SPECIES_VOX = 'icons/mob/clothing/species/vox/gloves.dmi',
diff --git a/code/modules/clothing/gloves/miscellaneous.dm b/code/modules/clothing/gloves/miscellaneous.dm
index 259052c4dfc..13fd4ae3c03 100644
--- a/code/modules/clothing/gloves/miscellaneous.dm
+++ b/code/modules/clothing/gloves/miscellaneous.dm
@@ -35,8 +35,8 @@
transfer_prints = FALSE
/obj/item/clothing/gloves/combat
- desc = "These tactical gloves are both insulated and offer protection from heat sources."
name = "combat gloves"
+ desc = "These tactical gloves are both insulated and offer melee protection."
icon_state = "combat"
item_state = "swat_gl"
siemens_coefficient = 0
@@ -47,7 +47,22 @@
heat_protection = HANDS
max_heat_protection_temperature = GLOVES_MAX_TEMP_PROTECT
resistance_flags = NONE
- armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 80, "acid" = 50)
+ armor = list("melee" = 25, "bullet" = 5, "laser" = 5, "energy" = 10, "bomb" = 10, "bio" = 0, "rad" = 0, "fire" = 70, "acid" = 70)
+
+/obj/item/clothing/gloves/combat/riot
+ name = "riot gloves"
+ desc = "These riot gloves are both insulated and offer melee protection."
+ icon_state = "riotgloves"
+ item_state = "riotgloves"
+ sprite_sheets = list(
+ SPECIES_VOX = 'icons/mob/clothing/species/vox/gloves.dmi',
+ SPECIES_DRASK = 'icons/mob/clothing/species/drask/gloves.dmi',
+ SPECIES_MONKEY = 'icons/mob/clothing/species/monkey/gloves.dmi',
+ SPECIES_FARWA = 'icons/mob/clothing/species/monkey/gloves.dmi',
+ SPECIES_WOLPIN = 'icons/mob/clothing/species/monkey/gloves.dmi',
+ SPECIES_NEARA = 'icons/mob/clothing/species/monkey/gloves.dmi',
+ SPECIES_STOK = 'icons/mob/clothing/species/monkey/gloves.dmi'
+ )
/obj/item/clothing/gloves/bracer
name = "bone bracers"
diff --git a/code/modules/clothing/head/helmet.dm b/code/modules/clothing/head/helmet.dm
index 56db1c4aadf..792386969be 100644
--- a/code/modules/clothing/head/helmet.dm
+++ b/code/modules/clothing/head/helmet.dm
@@ -126,8 +126,9 @@
dog_fashion = null
sprite_sheets = list(
SPECIES_VOX = 'icons/mob/clothing/species/vox/helmet.dmi',
+ SPECIES_GREY = 'icons/mob/clothing/species/grey/helmet.dmi',
SPECIES_VULPKANIN = 'icons/mob/clothing/species/vulpkanin/helmet.dmi'
- )
+ )
/obj/item/clothing/head/helmet/riot/knight
name = "medieval helmet"
diff --git a/code/modules/clothing/shoes/miscellaneous.dm b/code/modules/clothing/shoes/miscellaneous.dm
index e17f94aa6a6..1195c8a3a2f 100644
--- a/code/modules/clothing/shoes/miscellaneous.dm
+++ b/code/modules/clothing/shoes/miscellaneous.dm
@@ -16,6 +16,26 @@
pickup_sound = 'sound/items/handling/boots_pickup.ogg'
drop_sound = 'sound/items/handling/boots_drop.ogg'
+/obj/item/clothing/shoes/combat/riot
+ name = "riot boots"
+ desc = "High speed, low drag riot boots."
+ can_cut_open = FALSE
+ icon_state = "riotboots"
+ item_state = "riotboots"
+ sprite_sheets = list(
+ SPECIES_VOX = 'icons/mob/clothing/species/vox/shoes.dmi',
+ SPECIES_DRASK = 'icons/mob/clothing/species/drask/shoes.dmi',
+ SPECIES_MONKEY = 'icons/mob/clothing/species/monkey/shoes.dmi',
+ SPECIES_FARWA = 'icons/mob/clothing/species/monkey/shoes.dmi',
+ SPECIES_WOLPIN = 'icons/mob/clothing/species/monkey/shoes.dmi',
+ SPECIES_NEARA = 'icons/mob/clothing/species/monkey/shoes.dmi',
+ SPECIES_STOK = 'icons/mob/clothing/species/monkey/shoes.dmi',
+ SPECIES_UNATHI = 'icons/mob/clothing/species/unathi/shoes.dmi',
+ SPECIES_ASHWALKER_BASIC = 'icons/mob/clothing/species/unathi/shoes.dmi',
+ SPECIES_ASHWALKER_SHAMAN = 'icons/mob/clothing/species/unathi/shoes.dmi',
+ SPECIES_DRACONOID = 'icons/mob/clothing/species/unathi/shoes.dmi'
+ )
+
/obj/item/clothing/shoes/combat/swat //overpowered boots for death squads
name = "\improper SWAT shoes"
desc = "High speed, no drag combat boots."
@@ -155,7 +175,7 @@
icon_state = "armored_shoes"
item_color = "armored_shoes"
item_state = "armored_shoes"
- armor = list("melee" = 5, "bullet" = 5, "laser" = 5, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 0, "acid" = 0)
+ armor = list("melee" = 5, "bullet" = 25, "laser" = 10, "energy" = 5, "bomb" = 5, "bio" = 0, "rad" = 0, "fire" = 75, "acid" = 75)
sprite_sheets = list(
SPECIES_VOX = 'icons/mob/clothing/species/vox/shoes.dmi',
SPECIES_DRASK = 'icons/mob/clothing/species/drask/shoes.dmi',
diff --git a/code/modules/crafting/recipes.dm b/code/modules/crafting/recipes.dm
index 4dcd10981ba..c97a20f0941 100644
--- a/code/modules/crafting/recipes.dm
+++ b/code/modules/crafting/recipes.dm
@@ -684,7 +684,7 @@
/datum/crafting_recipe/bonesword
name = "Bone Sword"
- result = /obj/item/claymore/bone
+ result = /obj/item/melee/claymore/bone
time = 4 SECONDS
reqs = list(/obj/item/stack/sheet/bone = 3,
/obj/item/stack/sheet/sinew = 2)
@@ -1405,6 +1405,14 @@
/obj/item/toy/crayon/spraycan = 1)
category = CAT_MISC
+/datum/crafting_recipe/ashedlockerpaint
+ name = "Ashed customisation kit"
+ result = /obj/item/paintkit/lockermech_ashed
+ time = 35
+ reqs = list(/obj/item/stack/sheet/cardboard = 5,
+ /obj/item/toy/crayon/spraycan = 1)
+ category = CAT_MISC
+
/datum/crafting_recipe/stacklifter
name = "The weight stacklifter"
result = /obj/structure/weightmachine/stacklifter
@@ -1551,7 +1559,7 @@
/obj/item/stack/sheet/mineral/diamond = 5
)
result = list(/obj/item/pickaxe/diamond)
-
+
/datum/crafting_recipe/drone
name = "Inactive Drone"
result = list(/obj/item/inactive_drone)
diff --git a/code/modules/customitems/item_defines.dm b/code/modules/customitems/item_defines.dm
index cb5b6bf4768..450342c2f90 100644
--- a/code/modules/customitems/item_defines.dm
+++ b/code/modules/customitems/item_defines.dm
@@ -111,8 +111,8 @@
/obj/item/fluff/tattoo_gun/elliot_cybernetic_tat/attack_self(mob/user as mob)
if(!used)
- var/ink_color = input("Please select an ink color.", "Tattoo Ink Color", rgb(tattoo_r, tattoo_g, tattoo_b)) as color|null
- if(ink_color && !(user.incapacitated() || used) )
+ var/ink_color = tgui_input_color("Please select an ink color.", "Tattoo Ink Color", rgb(tattoo_r, tattoo_g, tattoo_b))
+ if(!isnull(ink_color) && !(user.incapacitated() || used) )
tattoo_r = color2R(ink_color)
tattoo_g = color2G(ink_color)
tattoo_b = color2B(ink_color)
@@ -138,13 +138,13 @@
to_chat(user, "You use [src] on yourself.")
qdel(src)
-/obj/item/claymore/fluff // MrBarrelrolll: Maximus Greenwood
+/obj/item/melee/claymore/fluff // MrBarrelrolll: Maximus Greenwood
name = "Greenwood's Blade"
desc = "A replica claymore with strange markings scratched into the blade."
force = 5
sharp = 0
-/obj/item/claymore/fluff/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = ITEM_ATTACK)
+/obj/item/melee/claymore/fluff/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = ITEM_ATTACK)
return 0
/obj/item/fluff/rsik_katana //Xydonus: Rsik Ugsharki Atan
diff --git a/code/modules/economy/robotic_quests/mech_types.dm b/code/modules/economy/robotic_quests/mech_types.dm
index edd66d9c5c7..36b019a782f 100644
--- a/code/modules/economy/robotic_quests/mech_types.dm
+++ b/code/modules/economy/robotic_quests/mech_types.dm
@@ -96,6 +96,7 @@
/obj/item/mecha_parts/mecha_equipment/medical/syringe_gun_upgrade, //You can't put this without syringe gun
/obj/item/mecha_parts/mecha_equipment/servo_hydra_actuator,
/obj/item/mecha_parts/mecha_equipment/improved_exosuit_control_system,
+ /obj/item/mecha_parts/mecha_equipment/medical/beamgun,
)
/datum/quest_mech/gygax
diff --git a/code/modules/hallucination/_hallucination.dm b/code/modules/hallucination/_hallucination.dm
new file mode 100644
index 00000000000..c59b1af61a2
--- /dev/null
+++ b/code/modules/hallucination/_hallucination.dm
@@ -0,0 +1,123 @@
+/**
+ * Simple effect that holds an image
+ * to be shown to one or multiple clients only.
+ *
+ * Pass a list of mobs in initialize() that corresponds to all mobs that can see it.
+ */
+/obj/effect/client_image_holder
+ invisibility = INVISIBILITY_OBSERVER
+ anchored = TRUE
+
+ /// A list of mobs which can see us.
+ var/list/mob/who_sees_us
+ /// The created image, what we look like.
+ var/image/shown_image
+ /// The icon file the image uses. If null, we have no image
+ var/image_icon
+ /// The icon state the image uses
+ var/image_state
+ /// The x pixel offset of the image
+ var/image_pixel_x = 0
+ /// The y pixel offset of the image
+ var/image_pixel_y = 0
+ /// Optional, the color of the image
+ var/image_color
+ /// The layer of the image
+ var/image_layer = MOB_LAYER
+ /// The plane of the image
+ var/image_plane = GAME_PLANE
+
+/obj/effect/client_image_holder/Initialize(mapload, list/mobs_which_see_us)
+ . = ..()
+ if(isnull(mobs_which_see_us))
+ stack_trace("Client image holder was created with no mobs to see it.")
+ return INITIALIZE_HINT_QDEL
+
+ shown_image = generate_image()
+
+ if(!islist(mobs_which_see_us))
+ mobs_which_see_us = list(mobs_which_see_us)
+
+ who_sees_us = list()
+ for(var/mob/seer as anything in mobs_which_see_us)
+ RegisterSignal(seer, COMSIG_MOB_LOGIN, PROC_REF(show_image_to))
+ RegisterSignal(seer, COMSIG_QDELETING, PROC_REF(remove_seer))
+ who_sees_us += seer
+ show_image_to(seer)
+
+/obj/effect/client_image_holder/Destroy(force)
+ if(shown_image)
+ for(var/mob/seer as anything in who_sees_us)
+ remove_seer(seer)
+ shown_image = null
+
+ who_sees_us.Cut() // probably not needed but who knows
+ return ..()
+
+/obj/effect/client_image_holder/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents)
+ . = ..()
+ if(QDELETED(src) || same_z_layer)
+ return
+ SET_PLANE(shown_image, PLANE_TO_TRUE(shown_image.plane), new_turf)
+
+/// Signal proc to clean up references if people who see us are deleted.
+/obj/effect/client_image_holder/proc/remove_seer(mob/source)
+ SIGNAL_HANDLER
+
+ UnregisterSignal(source, list(COMSIG_MOB_LOGIN, COMSIG_QDELETING))
+ hide_image_from(source)
+ who_sees_us -= source
+
+ // No reason to exist, anymore
+ if(!QDELETED(src) && !length(who_sees_us))
+ qdel(src)
+
+/// Generates the image which we take on.
+/obj/effect/client_image_holder/proc/generate_image()
+ var/image/created = image(image_icon, src, image_state, image_layer, dir = src.dir)
+ SET_PLANE_EXPLICIT(created, image_plane, src)
+ created.pixel_x = image_pixel_x
+ created.pixel_y = image_pixel_y
+ if(image_color)
+ created.color = image_color
+ return created
+
+/// Shows the image we generated to the passed mob
+/obj/effect/client_image_holder/proc/show_image_to(mob/show_to)
+ SIGNAL_HANDLER
+
+ show_to.client?.images |= shown_image
+
+/// Hides the image we generated from the passed mob
+/obj/effect/client_image_holder/proc/hide_image_from(mob/hide_from)
+ SIGNAL_HANDLER
+
+ hide_from.client?.images -= shown_image
+
+/// Simple helper for refreshing / showing the image to everyone in our list.
+/obj/effect/client_image_holder/proc/regenerate_image()
+ for(var/mob/seer as anything in who_sees_us)
+ hide_image_from(seer)
+
+ shown_image = generate_image()
+
+ for(var/mob/seer as anything in who_sees_us)
+ show_image_to(seer)
+
+// Whenever we perform icon updates, regenerate our image
+/obj/effect/client_image_holder/update_icon(updates = ALL)
+ . = ..()
+ regenerate_image()
+
+// If we move for some reason, regenerate our image
+/obj/effect/client_image_holder/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
+ . = ..()
+ if(!loc)
+ return
+ regenerate_image()
+
+/obj/effect/client_image_holder/singularity_pull()
+ return
+
+/obj/effect/client_image_holder/singularity_act()
+ return
diff --git a/code/modules/mini_games/thunderdome/gamemodes/gamemode.dm b/code/modules/mini_games/thunderdome/gamemodes/gamemode.dm
index 12e097e4459..03de50171b3 100644
--- a/code/modules/mini_games/thunderdome/gamemodes/gamemode.dm
+++ b/code/modules/mini_games/thunderdome/gamemodes/gamemode.dm
@@ -60,7 +60,7 @@
/obj/item/twohanded/spear/bonespear/chitinspear = 1,
/obj/item/twohanded/garrote = 1,
/obj/item/melee/rapier/syndie = 1,
- /obj/item/claymore/bone = 1,
+ /obj/item/melee/claymore/bone = 1,
/obj/item/gun/magic/staff/spellblade = 1,
/obj/item/spellbook/oneuse/goliath_dash = 1,
)
@@ -75,6 +75,7 @@
random_items_count = 3
item_pool = list(
/obj/item/gun/energy/immolator/multi = 2,
+ /obj/item/gun/energy/gun/minigun = 1,
/obj/item/gun/projectile/automatic/mini_uzi = 2,
/obj/item/gun/projectile/automatic/pistol/deagle = 2,
/obj/item/gun/projectile/automatic/wt550 = 2,
@@ -139,6 +140,7 @@
random_items_count = 3
item_pool = list(
/obj/item/gun/energy/immolator/multi = 1,
+ /obj/item/gun/energy/gun/minigun = 1,
/obj/item/gun/projectile/automatic/mini_uzi = 1,
/obj/item/gun/projectile/automatic/pistol/deagle = 1,
/obj/item/gun/projectile/automatic/wt550 = 1,
@@ -225,7 +227,7 @@
/obj/item/twohanded/spear/bonespear/chitinspear = 1,
/obj/item/twohanded/garrote = 1,
/obj/item/melee/rapier/syndie = 1,
- /obj/item/claymore/bone = 1,
+ /obj/item/melee/claymore/bone = 1,
/obj/item/gun/magic/staff/spellblade = 1,
/obj/item/spellbook/oneuse/goliath_dash = 1,
/obj/item/spellbook/oneuse/forcewall = 1,
diff --git a/code/modules/mini_games/thunderdome/thunderdome_battle.dm b/code/modules/mini_games/thunderdome/thunderdome_battle.dm
index 8d233c1c83c..0821efba0dc 100644
--- a/code/modules/mini_games/thunderdome/thunderdome_battle.dm
+++ b/code/modules/mini_games/thunderdome/thunderdome_battle.dm
@@ -5,7 +5,7 @@
#define ARENA_COOLDOWN 5 MINUTES //After which time thunderdome will be once again allowed to use
#define CQC_ARENA_RADIUS 6 //how much tiles away from a center players will spawn
#define RANGED_ARENA_RADIUS 10
-#define VOTING_POLL_TIME 30 SECONDS
+#define VOTING_POLL_TIME 10 SECONDS
#define MAX_PLAYERS_COUNT 16
#define MIN_PLAYERS_COUNT 2
#define SPAWN_COEFFICENT 0.85 //how many (polled * spawn_coefficent) players will go brawling
diff --git a/code/modules/mining/abandonedcrates.dm b/code/modules/mining/abandonedcrates.dm
index 8d08d07bb8e..70070c1d37b 100644
--- a/code/modules/mining/abandonedcrates.dm
+++ b/code/modules/mining/abandonedcrates.dm
@@ -115,7 +115,7 @@
if(91)
new /obj/item/soulstone/anybody(src)
if(92)
- new /obj/item/katana(src)
+ new /obj/item/melee/katana(src)
if(93)
new /obj/item/dnainjector/xraymut(src)
if(94)
diff --git a/code/modules/mining/equipment/kinetic_crusher.dm b/code/modules/mining/equipment/kinetic_crusher.dm
index 822765b8858..3f48c11605f 100644
--- a/code/modules/mining/equipment/kinetic_crusher.dm
+++ b/code/modules/mining/equipment/kinetic_crusher.dm
@@ -4,8 +4,16 @@
icon_state = "crusher"
item_state = "crusher0"
name = "proto-kinetic crusher"
- desc = "An early design of the proto-kinetic accelerator, it is little more than a combination of various mining tools cobbled together, forming a high-tech club. \
- While it is an effective mining tool, it did little to aid any but the most skilled and/or suicidal miners against local fauna."
+ desc = "Ранний дизайн прото-кинетического акселератора, лишь немногим отличающийся от кучи различных шахтёрских инструментов, прибитых друг к другу, формирующих высокотехнологичный топор. \
+ Хоть это и является эффективным шахтёрским инструментом, для борьбы с местной фауной его могут использовать либо самые опытные, либо самые сумасшедшие шахтёры."
+ ru_names = list(
+ NOMINATIVE = "прото-кинетический крушитель",
+ GENITIVE = "прото-кинетического крушителя",
+ DATIVE = "прото-кинетическому крушителю",
+ ACCUSATIVE = "прото-кинетический крушитель",
+ INSTRUMENTAL = "прото-кинетическим крушителем",
+ PREPOSITIONAL = "прото-кинетическом крушителе"
+ )
force = 0 //You can't hit stuff unless wielded
w_class = WEIGHT_CLASS_BULKY
slot_flags = ITEM_SLOT_BACK
@@ -37,11 +45,11 @@
/obj/item/twohanded/kinetic_crusher/examine(mob/living/user)
. = ..()
- . += "Mark a large creature with the destabilizing force, then hit them in melee to do [force + detonation_damage] damage."
- . += "Does [force + detonation_damage + backstab_bonus] damage if the target is backstabbed, instead of [force + detonation_damage]."
+ . += span_notice("Отметьте существо дестабилизирующим полем, затем нанесите удар в ближнем бою, чтобы нанести [force + detonation_damage] единиц[declension_ru(force + detonation_damage, "у", "ы", "")] урона.")
+ . += span_notice("Наносит [force + detonation_damage + backstab_bonus] единиц[declension_ru(force + detonation_damage + backstab_bonus, "у", "ы", "")] урона вместо [force + detonation_damage], если удар был нанесён в спину.")
for(var/t in trophies)
var/obj/item/crusher_trophy/T = t
- . += "It has \a [T] attached, which causes [T.effect_desc()]."
+ . += span_notice("К нему прикреплён[genderize_ru(T.gender, "", "а", "о", "ы")] [T.declent_ru(NOMINATIVE)], что вызывает следующий эффект: [T.effect_desc()].")
/obj/item/twohanded/kinetic_crusher/attackby(obj/item/I, mob/user, params)
@@ -69,9 +77,9 @@
/obj/item/twohanded/kinetic_crusher/attack(mob/living/target, mob/living/user, params, def_zone, skip_attack_anim = FALSE)
if(!HAS_TRAIT(src, TRAIT_WIELDED))
- var/warn_message = "The [name] is too heavy to use with one hand."
+ var/warn_message = "[capitalize(declent_ru(NOMINATIVE))] слишком тяжёл, чтобы использовать его одной рукой."
if(user.drop_item_ground(src))
- warn_message += " You fumble and drop it."
+ warn_message += "Вы роняете [declent_ru(ACCUSATIVE)] на землю."
to_chat(user, span_warning(warn_message))
return ATTACK_CHAIN_BLOCKED_ALL
var/datum/status_effect/crusher_damage/damage_track = target.has_status_effect(STATUS_EFFECT_CRUSHERDAMAGETRACKING)
@@ -104,9 +112,9 @@
if(user.has_status_effect(STATUS_EFFECT_DASH) && user.a_intent == INTENT_HELP)
if(user.throw_at(target, range = 3, speed = 3, spin = FALSE, diagonals_first = TRUE))
playsound(src, 'sound/effects/stealthoff.ogg', 50, 1, 1)
- user.visible_message("[user] dashes!")
+ user.visible_message(span_warning("[user] соверша[pluralize_ru(user, "ет", "ют")] рывок!"))
else
- to_chat(user, "Something prevents you from dashing!")
+ to_chat(user, span_warning("Что-то не даёт вам совершить рывок!"))
user.remove_status_effect(STATUS_EFFECT_DASH)
return
if(!proximity_flag && charged)//Mark a target, or mine a tile.
@@ -214,7 +222,7 @@
var/target_turf = get_turf(target)
if(ismineralturf(target_turf))
if(isancientturf(target_turf))
- visible_message("This rock appears to be resistant to all mining tools except pickaxes!")
+ visible_message(span_notice("Похоже, что эту породу возьмёт только кирка!"))
else
var/turf/simulated/mineral/M = target_turf
new /obj/effect/temp_visual/kinetic_blast(M)
@@ -224,7 +232,7 @@
//trophies
/obj/item/crusher_trophy
name = "tail spike"
- desc = "A strange spike with no usage."
+ desc = "Странный шип без применений."
icon = 'icons/obj/lavaland/artefacts.dmi'
icon_state = "tail_spike"
var/bonus_value = 10 //if it has a bonus effect, this is how much that effect is
@@ -232,7 +240,7 @@
/obj/item/crusher_trophy/examine(mob/living/user)
. = ..()
- . += "Causes [effect_desc()] when attached to a kinetic crusher."
+ . += span_notice("Когда прикреплено к крушителю, вызывает следующий эффект: [effect_desc()].")
/obj/item/crusher_trophy/proc/effect_desc()
return "errors"
@@ -250,7 +258,7 @@
/obj/item/crusher_trophy/proc/add_to(obj/item/twohanded/kinetic_crusher/crusher, mob/living/user)
for(var/obj/item/crusher_trophy/crusher_trophy as anything in crusher.trophies)
if(istype(crusher_trophy, denied_type) || istype(src, crusher_trophy.denied_type))
- to_chat(user, span_warning("You cannot attach [src] to [crusher]. Try to remove a few trophies first."))
+ balloon_alert(user, "нет места!")
return FALSE
if(loc == user)
if(!user.drop_transfer_item_to_loc(src, crusher))
@@ -258,7 +266,7 @@
else
forceMove(crusher)
crusher.trophies += src
- to_chat(user, span_notice("You have attached [src] to [crusher]."))
+ balloon_alert(user, "прикреплено")
return TRUE
/obj/item/crusher_trophy/proc/remove_from(obj/item/twohanded/kinetic_crusher/H, mob/living/user)
@@ -277,7 +285,15 @@
//goliath
/obj/item/crusher_trophy/goliath_tentacle
name = "goliath tentacle"
- desc = "A sliced-off goliath tentacle. Suitable as a trophy for a kinetic crusher."
+ desc = "Отрубленное щупальце голиафа. Может быть установлено на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "щупальце голиафа",
+ GENITIVE = "щупальца голиафа",
+ DATIVE = "щупальцу голиафа",
+ ACCUSATIVE = "щупальце голиафа",
+ INSTRUMENTAL = "щупальцем голиафа",
+ PREPOSITIONAL = "щупальце голиафа"
+ )
icon_state = "goliath_tentacle"
denied_type = /obj/item/crusher_trophy/goliath_tentacle
bonus_value = 2
@@ -285,7 +301,7 @@
var/missing_health_desc = 10
/obj/item/crusher_trophy/goliath_tentacle/effect_desc()
- return "mark detonation to do [bonus_value] more damage for every [missing_health_desc] health you are missing"
+ return "детонация метки дестабилизатора наносит на [bonus_value] единиц[declension_ru(bonus_value, "у", "ы", "")] урона больше за каждые [missing_health_desc] единиц[declension_ru(missing_health_desc, "у", "ы", "")] недостающего у вас здоровья"
/obj/item/crusher_trophy/goliath_tentacle/on_mark_detonation(mob/living/target, mob/living/user)
var/missing_health = user.health - user.maxHealth
@@ -297,13 +313,21 @@
//watcher
/obj/item/crusher_trophy/watcher_wing
name = "watcher wing"
- desc = "A wing ripped from a watcher. Suitable as a trophy for a kinetic crusher."
+ desc = "Оторванное крыло наблюдателя. Может быть установлено на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "крыло наблюдателя",
+ GENITIVE = "крыла наблюдателя",
+ DATIVE = "крылу наблюдателя",
+ ACCUSATIVE = "крыло наблюдателя",
+ INSTRUMENTAL = "крылом наблюдателя",
+ PREPOSITIONAL = "крыле наблюдателя"
+ )
icon_state = "watcher_wing"
denied_type = /obj/item/crusher_trophy/watcher_wing
bonus_value = 5
/obj/item/crusher_trophy/watcher_wing/effect_desc()
- return "mark detonation to prevent certain creatures from using certain attacks for [bonus_value*0.1] second\s"
+ return "детонация метки дестабилизатора не позволяет некоторым существам использовать дальнобойные атаки в течении [bonus_value * 0.1] секунд[declension_ru(bonus_value * 0.1, "ы", "", "")]"
/obj/item/crusher_trophy/watcher_wing/on_mark_detonation(mob/living/target, mob/living/user)
if(ishostile(target))
@@ -317,13 +341,21 @@
//magmawing watcher
/obj/item/crusher_trophy/blaster_tubes/magma_wing
name = "magmawing watcher wing"
- desc = "A still-searing wing from a magmawing watcher. Suitable as a trophy for a kinetic crusher."
+ desc = "Всё ещё пылающее крыло магмакрылого наблюдателя. Может быть установлено на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "крыло магмакрылого наблюдателя",
+ GENITIVE = "крыла магмакрылого наблюдателя",
+ DATIVE = "крылу магмакрылого наблюдателя",
+ ACCUSATIVE = "крыло магмакрылого наблюдателя",
+ INSTRUMENTAL = "крылом магмакрылого наблюдателя",
+ PREPOSITIONAL = "крыле магмакрылого наблюдателя"
+ )
icon_state = "magma_wing"
gender = NEUTER
bonus_value = 5
/obj/item/crusher_trophy/blaster_tubes/magma_wing/effect_desc()
- return "mark detonation to make the next destabilizer shot deal [bonus_value] damage"
+ return "детонация метки дестабилизатора позволяет следующему выстрелу дестабилизатора нанести [bonus_value] единиц[declension_ru(bonus_value, "у", "ы", "")] урона"
/obj/item/crusher_trophy/blaster_tubes/magma_wing/on_projectile_fire(obj/item/projectile/destabilizer/marker, mob/living/user)
if(deadly_shot)
@@ -336,20 +368,36 @@
//icewing watcher
/obj/item/crusher_trophy/watcher_wing/ice_wing
name = "icewing watcher wing"
- desc = "A carefully preserved frozen wing from an icewing watcher. Suitable as a trophy for a kinetic crusher."
+ desc = "Хрупкое, замороженное крыло ледокрылого наблюдателя. Может быть установлено на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "крыло ледокрылого наблюдателя",
+ GENITIVE = "крыла ледокрылого наблюдателя",
+ DATIVE = "крылу ледокрылого наблюдателя",
+ ACCUSATIVE = "крыло ледокрылого наблюдателя",
+ INSTRUMENTAL = "крылом ледокрылого наблюдателя",
+ PREPOSITIONAL = "крыле ледокрылого наблюдателя"
+ )
icon_state = "ice_wing"
bonus_value = 8
//legion
/obj/item/crusher_trophy/legion_skull
name = "legion skull"
- desc = "A dead and lifeless legion skull. Suitable as a trophy for a kinetic crusher."
+ desc = "Разбитый, безжизненный череп легиона. Может быть установлен на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "череп легиона",
+ GENITIVE = "черепа легиона",
+ DATIVE = "черепу легиона",
+ ACCUSATIVE = "череп легиона",
+ INSTRUMENTAL = "черепом легиона",
+ PREPOSITIONAL = "черепе легиона"
+ )
icon_state = "legion_skull"
denied_type = /obj/item/crusher_trophy/legion_skull
bonus_value = 3
/obj/item/crusher_trophy/legion_skull/effect_desc()
- return "a kinetic crusher to recharge [bonus_value*0.1] second\s faster"
+ return "выстрел дестабилизатора перезаряжается на [bonus_value * 0.1] секунд[declension_ru(bonus_value * 0.1, "у", "ы", "")] быстрее"
/obj/item/crusher_trophy/legion_skull/add_to(obj/item/twohanded/kinetic_crusher/H, mob/living/user)
. = ..()
@@ -364,13 +412,21 @@
/// Massive eyed tentacle
/obj/item/crusher_trophy/eyed_tentacle
name = "Massive eyed tentacle"
- desc = "Большое и глазастое щупальце древнего голиафа. Может быть установлено как трофей крашера."
+ desc = "Большое и глазастое щупальце древнего голиафа. Может быть установлено на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "огромное щупальце голиафа",
+ GENITIVE = "огромного щупальца голиафа",
+ DATIVE = "огромному щупальцу голиафа",
+ ACCUSATIVE = "огромное щупальце голиафа",
+ INSTRUMENTAL = "огромным щупальцем голиафа",
+ PREPOSITIONAL = "огромном щупальце голиафа"
+ )
icon_state = "ancient_goliath_tentacle"
denied_type = /obj/item/crusher_trophy/eyed_tentacle
bonus_value = 1
/obj/item/crusher_trophy/eyed_tentacle/effect_desc()
- return "causes kinetic crusher to deal 50% more damage if target has more than 90% HP"
+ return "крушитель наносит на 50% больше урона, если у цели больше 90% здоровья"
/obj/item/crusher_trophy/eyed_tentacle/on_melee_hit(mob/living/target, mob/living/user)
var/procent = (target.health / target.maxHealth) * 100
@@ -386,13 +442,21 @@
/// Poison fang
/obj/item/crusher_trophy/fang
name = "Poison fang"
- desc = "Уродливый и отравленный коготь. Может быть установлен как трофей крашера."
+ desc = "Уродливый и отравленный клык. Может быть установлен на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "отравленный клык",
+ GENITIVE = "отравленного клыка",
+ DATIVE = "отравленному клыку",
+ ACCUSATIVE = "отравленный клык",
+ INSTRUMENTAL = "отравленным клыком",
+ PREPOSITIONAL = "отравленном клыке"
+ )
icon_state = "ob_gniga"
denied_type = /obj/item/crusher_trophy/fang
bonus_value = 1.1
/obj/item/crusher_trophy/fang/effect_desc()
- return "causes fauna to get 10% more damage after mark destroyed for 2 seconds"
+ return "фауна получает на 10% больше урона в течении 2 секунд после детонации метки дестабилизатора"
/obj/item/crusher_trophy/fang/on_mark_detonation(mob/living/target, mob/living/user)
target.apply_status_effect(STATUS_EFFECT_FANG_EXHAUSTION, bonus_value)
@@ -400,13 +464,21 @@
/// Frost gland
/obj/item/crusher_trophy/gland
name = "Frost gland"
- desc = "Замороженная железа. Может быть установлена как трофей крашера."
+ desc = "Замороженная железа. Может быть установлена на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "морозная железа",
+ GENITIVE = "морозной железы",
+ DATIVE = "морозной железе",
+ ACCUSATIVE = "морозную железу",
+ INSTRUMENTAL = "морозной железой",
+ PREPOSITIONAL = "морозной железе"
+ )
icon_state = "ice_gniga"
denied_type = /obj/item/crusher_trophy/gland
bonus_value = 0.9
/obj/item/crusher_trophy/gland/effect_desc()
- return "causes fauna to deal 10% less damage when marked"
+ return "фауна наносит на 10% меньше урона, пока на неё установлена метка дестабилизатора"
/obj/item/crusher_trophy/gland/on_mark_application(mob/living/simple_animal/target, datum/status_effect/crusher_mark/mark, had_mark)
if(had_mark)
@@ -428,24 +500,40 @@
//blood-drunk hunter
/obj/item/crusher_trophy/miner_eye
name = "eye of a blood-drunk hunter"
- desc = "Its pupil is collapsed and turned to mush. Suitable as a trophy for a kinetic crusher."
+ desc = "Человеческий глаз с раздробленным в кашу зрачком. Может быть установлено на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "глаз кровожадного шахтёра",
+ GENITIVE = "глаза кровожадного шахтёра",
+ DATIVE = "глазу кровожадного шахтёра",
+ ACCUSATIVE = "глаз кровожадного шахтёра",
+ INSTRUMENTAL = "глазом кровожадного шахтёра",
+ PREPOSITIONAL = "глазе кровожадного шахтёра"
+ )
icon_state = "hunter_eye"
denied_type = /obj/item/crusher_trophy/miner_eye
/obj/item/crusher_trophy/miner_eye/effect_desc()
- return "mark detonation to grant stun immunity and 90% damage reduction for 1 second"
+ return "детонация метки дестабилизатора даёт вам иммунитет к оглушению и уменьшение получаемого урона на 90%, на 1 секунду"
/obj/item/crusher_trophy/miner_eye/on_mark_detonation(mob/living/target, mob/living/user)
user.apply_status_effect(STATUS_EFFECT_BLOODDRUNK)
//ash drake
/obj/item/crusher_trophy/tail_spike
- desc = "A spike taken from an ash drake's tail. Suitable as a trophy for a kinetic crusher."
+ desc = "Шип, срезанный с хвоста пепельного дрейка. Может быть установлено на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "хвостновой шип",
+ GENITIVE = "хвостового шипа",
+ DATIVE = "хвостовому шипу",
+ ACCUSATIVE = "хвостовой шип",
+ INSTRUMENTAL = "хвостовым шипом",
+ PREPOSITIONAL = "хвостовом шипе"
+ )
denied_type = /obj/item/crusher_trophy/tail_spike
bonus_value = 5
/obj/item/crusher_trophy/tail_spike/effect_desc()
- return "mark detonation to do [bonus_value] damage to nearby creatures and push them back"
+ return "детонация метки дестабилизатора взрывает врага, нанося [bonus_value] единиц[declension_ru(bonus_value, "у", "ы", "")] урона близлежащим врагам и отталкивая их"
/obj/item/crusher_trophy/tail_spike/on_mark_detonation(mob/living/target, mob/living/user)
for(var/mob/living/L in oview(2, user))
@@ -463,7 +551,15 @@
//bubblegum
/obj/item/crusher_trophy/demon_claws
name = "demon claws"
- desc = "A set of blood-drenched claws from a massive demon's hand. Suitable as a trophy for a kinetic crusher."
+ desc = "Набор окровавленных когтей, вырванных с руки огромного демона. Может быть установлено на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "демонические когти",
+ GENITIVE = "демонических когтей",
+ DATIVE = "демоническим когтям",
+ ACCUSATIVE = "демонические когти",
+ INSTRUMENTAL = "демоническими когтями",
+ PREPOSITIONAL = "демонических когтях"
+ )
icon_state = "demon_claws"
gender = PLURAL
denied_type = /obj/item/crusher_trophy/demon_claws
@@ -471,7 +567,7 @@
var/static/list/damage_heal_order = list(BRUTE, BURN, OXY)
/obj/item/crusher_trophy/demon_claws/effect_desc()
- return "melee hits to do [bonus_value * 0.2] more damage and heal you for [bonus_value * 0.1], with 5X effect on mark detonation"
+ return "удары в ближнем бою наносят на [bonus_value * 0.2] единиц[declension_ru(bonus_value * 0.2, "у", "ы", "")] урона больше и лечат вас на [bonus_value * 0.1] единиц[declension_ru(bonus_value * 0.1, "у", "ы", "")] здоровья, с пятерным эффектом при детонации метки"
/obj/item/crusher_trophy/demon_claws/add_to(obj/item/twohanded/kinetic_crusher/H, mob/living/user)
. = ..()
@@ -499,7 +595,15 @@
//colossus
/obj/item/crusher_trophy/blaster_tubes
name = "blaster tubes"
- desc = "The blaster tubes from a colossus's arm. Suitable as a trophy for a kinetic crusher."
+ desc = "Бластерные трубки, взятые с руки колосса. Может быть установлено на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "бластерные трубки",
+ GENITIVE = "бластерных трубок",
+ DATIVE = "бластерным трубкам",
+ ACCUSATIVE = "бластерные трубки",
+ INSTRUMENTAL = "бластерными трубками",
+ PREPOSITIONAL = "бластерных трубках"
+ )
icon_state = "blaster_tubes"
gender = PLURAL
denied_type = /obj/item/crusher_trophy/blaster_tubes
@@ -507,7 +611,7 @@
var/deadly_shot = FALSE
/obj/item/crusher_trophy/blaster_tubes/effect_desc()
- return "mark detonation to make the next destabilizer shot deal [bonus_value] damage but move slower"
+ return "следующий выстрел дестабилизатора после детонации метки дестабилизатора будет лететь медленнее, но нанесёт [bonus_value] единиц[declension_ru(bonus_value, "у", "ы", "")] урона"
/obj/item/crusher_trophy/blaster_tubes/on_projectile_fire(obj/item/projectile/destabilizer/marker, mob/living/user)
if(deadly_shot)
@@ -528,12 +632,20 @@
//hierophant
/obj/item/crusher_trophy/vortex_talisman
name = "vortex talisman"
- desc = "A glowing trinket that was originally the Hierophant's beacon. Suitable as a trophy for a kinetic crusher."
+ desc = "Мерцающий талисман, ранее бывший маяком Иерофанта. Может быть установлено на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "талисман вихря",
+ GENITIVE = "талисмана вихря",
+ DATIVE = "талисману вихря",
+ ACCUSATIVE = "талисман вихря",
+ INSTRUMENTAL = "талисманом вихря",
+ PREPOSITIONAL = "талисмане вихря"
+ )
icon_state = "vortex_talisman"
denied_type = /obj/item/crusher_trophy/vortex_talisman
/obj/item/crusher_trophy/vortex_talisman/effect_desc()
- return "mark detonation to create a homing hierophant chaser" //Wall was way too cheesy and allowed miners to be nearly invincible while dumb mob AI just rubbed its face on the wall.
+ return "детонация метки дестабилизатора призывает самонаводящуюся гончую Иерофанта" //Wall was way too cheesy and allowed miners to be nearly invincible while dumb mob AI just rubbed its face on the wall.
/obj/item/crusher_trophy/vortex_talisman/on_mark_detonation(mob/living/target, mob/living/user)
if(isliving(target))
@@ -545,13 +657,21 @@
//vetus
/obj/item/crusher_trophy/adaptive_intelligence_core
name = "adaptive intelligence core"
- desc = "Seems to be one of the cores from a massive robot. Suitable as a trophy for a kinetic crusher."
+ desc = "Кажется, это одно из ядер огромного робота. Может быть установлено на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "адаптивное ядро ИИ",
+ GENITIVE = "адаптивного ядра ИИ",
+ DATIVE = "адаптивному ядру ИИ",
+ ACCUSATIVE = "адаптивное ядро ИИ",
+ INSTRUMENTAL = "адаптивным ядром ИИ",
+ PREPOSITIONAL = "адаптивном ядре ИИ"
+ )
icon_state = "adaptive_core"
denied_type = /obj/item/crusher_trophy/adaptive_intelligence_core
bonus_value = 2
/obj/item/crusher_trophy/adaptive_intelligence_core/effect_desc()
- return "melee hits deal [bonus_value] more damage per hit after hitting a target, up to [bonus_value * 10] extra damage to that target"
+ return "удары в ближнем бою наносят на [bonus_value] единиц[declension_ru(bonus_value, "у", "ы", "")] урона больше после атаки по противнику, с пределом в [bonus_value * 10] единиц[declension_ru(bonus_value, "у", "ы", "")] урона"
/obj/item/crusher_trophy/adaptive_intelligence_core/add_to(obj/item/twohanded/kinetic_crusher/H, mob/living/user)
. = ..()
@@ -567,12 +687,20 @@
/obj/item/crusher_trophy/empowered_legion_skull
name = "empowered legion skull"
- desc = "A powerful looking skull with glowing red eyes."
+ desc = "Устрашающий череп с горящими красными глазами. Может быть установлено на крушитель в качестве трофея."
+ ru_names = list(
+ NOMINATIVE = "усиленный череп легиона",
+ GENITIVE = "усиленного черепа легиона",
+ DATIVE = "усиленному черепу легиона",
+ ACCUSATIVE = "усиленный череп легиона",
+ INSTRUMENTAL = "усиленным черепом легиона",
+ PREPOSITIONAL = "усиленном черепе легиона"
+ )
icon_state = "ashen_skull"
denied_type = /obj/item/crusher_trophy/empowered_legion_skull
/obj/item/crusher_trophy/empowered_legion_skull/effect_desc()
- return "mark detonation grants the ability to dash a short distance on help intent"
+ return "детонация метки дестабилизатора позволяет вам сделать рывок на небольшую дистанцию, если выбрано намерение помощи"
/obj/item/crusher_trophy/empowered_legion_skull/on_mark_detonation(mob/living/target, mob/living/user)
user.apply_status_effect(STATUS_EFFECT_DASH)
@@ -583,7 +711,15 @@
icon_state = "magmite_crusher"
item_state = "magmite_crusher0"
name = "magmite proto-kinetic crusher"
- desc = "An early design of the proto-kinetic accelerator, it is now a combination of various mining tools infused with magmite, forming a high-tech club, increasing its capacity as a mining tool."
+ desc = "Ранний дизайн прото-кинетического акселератора, теперь являющийся кучей различных шахтёрских иструментов приваренных друг к другу плазменным магмитом, формирующих высокотехнологичный топор. Магмит улучшает шахтёрские возможности крушителя."
+ ru_names = list(
+ NOMINATIVE = "магмитовый прото-кинетический крушитель",
+ GENITIVE = "магмитового прото-кинетического крушителя",
+ DATIVE = "магмитовому прото-кинетическому крушителю",
+ ACCUSATIVE = "магмитовый прото-кинетический крушитель",
+ INSTRUMENTAL = "магмитовым прото-кинетическим крушителем",
+ PREPOSITIONAL = "магмитовом прото-кинетическом крушителе"
+ )
destab = /obj/item/projectile/destabilizer/mega
upgraded = TRUE
@@ -595,7 +731,7 @@
var/target_turf = get_turf(target)
if(ismineralturf(target_turf))
if(isancientturf(target_turf))
- visible_message("This rock appears to be resistant to all mining tools except pickaxes!")
+ visible_message(span_notice("Похоже, что эту породу возьмёт только кирка!"))
forcedodge = 0
else
var/turf/simulated/mineral/M = target_turf
@@ -611,9 +747,17 @@
icon_state = "magmite_crusher"
item_state = "magmite_crusher0"
name = "unfinished proto-kinetic crusher"
- desc = "An early design of the proto-kinetic accelerator, it is now a combination of various mining tools infused with magmite, forming a new design, but there is not enough magmite to upgrade it's destabilizer."
+ desc = "Ранний дизайн прото-кинетического акселератора, теперь являющийся кучей различных шахтёрских иструментов приваренных друг к другу плазменным магмитом. Судя по всему, магмитовых деталей на улучшение его дестабилизатора было недостаточно."
+ ru_names = list(
+ NOMINATIVE = "незавершенный прото-кинетический крушитель",
+ GENITIVE = "незавершенного прото-кинетического крушителя",
+ DATIVE = "незавершенному прото-кинетическому крушителю",
+ ACCUSATIVE = "незавершенный прото-кинетический крушитель",
+ INSTRUMENTAL = "незавершенным прото-кинетическим крушителем",
+ PREPOSITIONAL = "незавершенном прото-кинетическом крушителе"
+ )
upgraded = TRUE
/obj/item/twohanded/kinetic_crusher/almost/examine(mob/living/user)
. = ..()
- . += "Perhaps you could use another magmite upgrade part to fully upgrade your crusher."
+ . += span_notice("Возможно, вы можете применить еще немного магмитовых деталей, чтобы полностью улучшить ваш крушитель.")
diff --git a/code/modules/mining/lavaland/loot/ashdragon_loot.dm b/code/modules/mining/lavaland/loot/ashdragon_loot.dm
index a36867067e5..f33eebb2084 100644
--- a/code/modules/mining/lavaland/loot/ashdragon_loot.dm
+++ b/code/modules/mining/lavaland/loot/ashdragon_loot.dm
@@ -185,6 +185,8 @@
name = "staff of lava"
desc = "The power of fire and rocks in your hands!"
icon_state = "lavastaff"
+ lefthand_file = 'icons/mob/inhands/staff_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/staff_righthand.dmi'
item_state = "lavastaff"
icon = 'icons/obj/weapons/magic.dmi'
slot_flags = ITEM_SLOT_BACK
diff --git a/code/modules/mining/mint.dm b/code/modules/mining/mint.dm
index 1cc12eb483a..65fecf1434e 100644
--- a/code/modules/mining/mint.dm
+++ b/code/modules/mining/mint.dm
@@ -1,104 +1,213 @@
-/**********************Mint**************************/
-
+#define COIN_COST MINERAL_MATERIAL_AMOUNT * 0.2
/obj/machinery/mineral/mint
name = "coin press"
+ desc = "Массивная, слегка шумящая машина с тяжелыми стальными прессами. \
+ Она используется для чеканки монет из различных матриалов. \
+ На корпусе расположена панель управления с настройками для выбора металла и нанесения уникальных штампов."
+
+ ru_names = list(
+ NOMINATIVE = "монетный пресс",
+ GENITIVE = "монетного пресса",
+ DATIVE = "монетному прессу",
+ ACCUSATIVE = "монетный пресс",
+ INSTRUMENTAL = "монетным прессом",
+ PREPOSITIONAL = "монетном прессе",
+ )
+
icon = 'icons/obj/economy.dmi'
- icon_state = "coinpress0"
+ icon_state = "coin_press"
density = TRUE
anchored = TRUE
- var/newCoins = 0 //how many coins the machine made in it's last load
- var/processing = FALSE
- var/chosen = MAT_METAL //which material will be used to make coins
- var/coinsToProduce = 10
- speed_process = TRUE
-/obj/machinery/mineral/mint/New()
- ..()
- AddComponent(/datum/component/material_container, list(MAT_METAL, MAT_PLASMA, MAT_SILVER, MAT_GOLD, MAT_URANIUM, MAT_DIAMOND, MAT_BANANIUM, MAT_TRANQUILLITE), MINERAL_MATERIAL_AMOUNT * 50, FALSE, /obj/item/stack)
+ /// How many coins did the machine make in total.
+ var/total_coins = 0
+ /// Is it creating coins now?
+ var/active = FALSE
+ /// Which material will be used to make coins or for ejecting.
+ var/chosen_material
+ /// Inserted money bag.
+ var/obj/item/storage/bag/money/money_bag
-/obj/machinery/mineral/mint/process()
- var/turf/T = get_step(src, input_dir)
- if(!T)
- return
+/obj/machinery/mineral/mint/Initialize(mapload)
+ . = ..()
+ var/static/list/coin_materials = list()
+ if(!length(coin_materials))
+ for(var/datum/material/coin_mat as anything in subtypesof(/datum/material))
+ var/obj/item/coin/coin_type = coin_mat.coin_type
+ if(!coin_type)
+ continue
+ coin_materials += coin_mat.id
+
+ AddComponent(/datum/component/material_container, coin_materials, MINERAL_MATERIAL_AMOUNT * 50, FALSE, /obj/item/stack, _after_insert = CALLBACK(src, PROC_REF(material_insert)))
+ chosen_material = pick(coin_materials[1])
+
+/obj/machinery/mineral/mint/Destroy()
var/datum/component/material_container/materials = GetComponent(/datum/component/material_container)
- for(var/obj/item/stack/sheet/O in T)
- materials.insert_stack(O, O.amount)
+ materials.retrieve_all()
+ return ..()
+
+/obj/machinery/mineral/mint/update_icon_state()
+ if(active)
+ icon_state = "coin_press-active"
+ else
+ icon_state = "coin_press"
+
+/obj/machinery/mineral/mint/wrench_act(mob/user, obj/item/I)
+ default_unfasten_wrench(user, I, time = 4 SECONDS)
+ return TRUE
/obj/machinery/mineral/mint/attack_hand(mob/user)
- if(..())
- return
- var/dat = {"Coin Press "}
+ add_fingerprint(user)
+ ui_interact(user)
+
+/obj/machinery/mineral/mint/attack_ghost(mob/user)
+ ui_interact(user)
+
+/obj/machinery/mineral/mint/ui_state(mob/user)
+ return GLOB.default_state
+
+/obj/machinery/mineral/mint/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "CoinMint")
+ ui.set_autoupdate(FALSE)
+ ui.open()
+
+/obj/machinery/mineral/mint/ui_assets(mob/user)
+ return list(
+ get_asset_datum(/datum/asset/spritesheet/materials)
+ )
+
+/obj/machinery/mineral/mint/ui_data(mob/user)
+ var/list/data = list()
+
+ data["active"] = active
+ data["chosenMaterial"] = chosen_material
+ data["totalCoins"] = total_coins
+ data["moneyBag"] = !!money_bag
+
+ if(money_bag)
+ data["moneyBagContent"] = length(money_bag.contents)
+ data["moneyBagMaxContent"] = money_bag.storage_slots
var/datum/component/material_container/materials = GetComponent(/datum/component/material_container)
+ data["totalMaterials"] = materials.total_amount
+ data["maxMaterials"] = materials.max_amount
+
+ var/list/material_list = list()
for(var/mat_id in materials.materials)
- var/datum/material/M = materials.materials[mat_id]
- if(!M.amount && chosen != mat_id)
- continue
- dat += " [M.name] amount: [M.amount] cm3 "
- if(chosen == mat_id)
- dat += "Chosen"
- else
- dat += "Choose"
-
- var/datum/material/M = materials.materials[chosen]
-
- dat += "
Will produce [coinsToProduce] [lowertext(M.name)] coins if enough materials are available. "
- dat += "-10 "
- dat += "-5 "
- dat += "-1 "
- dat += "+1 "
- dat += "+5 "
- dat += "+10 "
-
- dat += "
In total this machine produced [newCoins] coins."
- dat += " Make coins"
- user << browse(dat, "window=mint")
-
-/obj/machinery/mineral/mint/Topic(href, href_list)
+ var/datum/material/material = materials.materials[mat_id]
+ material_list += list(list(
+ "name" = material.name,
+ "amount" = material.amount / MINERAL_MATERIAL_AMOUNT,
+ "id" = material.id
+ ))
+ data["materials"] = material_list
+
+ return data
+
+/obj/machinery/mineral/mint/ui_act(action, params, datum/tgui/ui)
if(..())
return
- usr.set_machine(src)
- add_fingerprint(usr)
- if(processing == 1)
- to_chat(usr, "The machine is processing.")
+ . = TRUE
+
+ var/datum/component/material_container/materials = GetComponent(/datum/component/material_container)
+ switch(action)
+ if("selectMaterial")
+ if(!materials.materials[params["material"]])
+ return
+ chosen_material = params["material"]
+ if("activate")
+ if(active)
+ active = FALSE
+ else
+ try_make_coins()
+ update_icon(UPDATE_ICON_STATE)
+ if("ejectMat")
+ var/datum/material/material = materials.materials[chosen_material]
+ if(material.amount < MINERAL_MATERIAL_AMOUNT)
+ to_chat(usr, span_warning("Недостаточно [material.name] для извлечения!"))
+ return
+ var/num_sheets = tgui_input_number(usr, "Сколько кусков вы хотите извлечь?", "Извлечь [material.name]", max_value = round(material.amount / MINERAL_MATERIAL_AMOUNT), min_value = 1)
+ if(isnull(num_sheets))
+ return
+ materials.retrieve_sheets(num_sheets, chosen_material)
+ if("ejectBag")
+ eject_bag(usr)
+
+/obj/machinery/mineral/mint/attackby(obj/item/I, mob/user, params)
+ if(istype(I, /obj/item/storage/bag/money))
+ if(money_bag)
+ to_chat(user, span_notice("Внутри уже есть [money_bag.declent_ru(NOMINATIVE)]!"))
+ balloon_alert(usr, "место уже занято!")
+ return ATTACK_CHAIN_PROCEED
+ if(!user.drop_from_active_hand())
+ return ATTACK_CHAIN_PROCEED
+ to_chat(user, span_notice("Вы помещаете [I.declent_ru(ACCUSATIVE)] в [declent_ru(ACCUSATIVE)]."))
+ balloon_alert(usr, "мешок помещен")
+ I.forceMove(src)
+ money_bag = I
+ SStgui.update_uis(src)
+ return ATTACK_CHAIN_PROCEED_SUCCESS
+
+ return ..()
+
+/obj/machinery/mineral/mint/process()
+ if(!active)
+ return
+ if(length(money_bag.contents) >= money_bag.storage_slots)
+ active = FALSE
+ visible_message(span_notice("[capitalize(declent_ru(NOMINATIVE))] прекращает производство, чтобы избежать переполнения."))
+ balloon_alert_to_viewers("мешок переполнен")
+ update_icon(UPDATE_ICON_STATE)
+ SStgui.update_uis(src)
+ return
+
+ var/datum/component/material_container/materials = GetComponent(/datum/component/material_container)
+ var/datum/material/material = materials.materials[chosen_material]
+ if(!materials.can_use_amount(COIN_COST, chosen_material))
+ active = FALSE
+ visible_message(span_notice("[capitalize(declent_ru(NOMINATIVE))] прекращает производство из-за нехватки материала."))
+ balloon_alert_to_viewers("материал кончился")
+ update_icon(UPDATE_ICON_STATE)
+ SStgui.update_uis(src)
return
+
+ materials.use_amount_type(COIN_COST, chosen_material)
+ new material.coin_type(money_bag)
+ total_coins++
+ SStgui.update_uis(src)
+
+/obj/machinery/mineral/mint/proc/try_make_coins(mob/user)
var/datum/component/material_container/materials = GetComponent(/datum/component/material_container)
- if(href_list["choose"])
- if(materials.materials[href_list["choose"]])
- chosen = href_list["choose"]
- if(href_list["chooseAmt"])
- coinsToProduce = clamp(coinsToProduce + text2num(href_list["chooseAmt"]), 0, 1000)
- if(href_list["makeCoins"])
- var/temp_coins = coinsToProduce
- processing = TRUE
- icon_state = "coinpress1"
- var/coin_mat = MINERAL_MATERIAL_AMOUNT * 0.2
- var/datum/material/M = materials.materials[chosen]
- if(!M || !M.coin_type)
- updateUsrDialog()
- return
-
- while(coinsToProduce > 0 && materials.use_amount_type(coin_mat, chosen))
- create_coins(M.coin_type)
- coinsToProduce--
- newCoins++
- updateUsrDialog()
- sleep(5)
-
- icon_state = "coinpress0"
- processing = FALSE
- coinsToProduce = temp_coins
- updateUsrDialog()
-
-/obj/machinery/mineral/mint/proc/create_coins(P)
- var/turf/T = get_step(src,output_dir)
- if(T)
- var/obj/item/O = new P(src)
- var/obj/item/storage/bag/money/M = locate(/obj/item/storage/bag/money, T)
- if(!M)
- M = new /obj/item/storage/bag/money(src)
- unload_mineral(M)
- O.forceMove(M)
+ if(!money_bag)
+ visible_message(span_warning("[capitalize(declent_ru(NOMINATIVE))] не может работать без денежного мешка!"))
+ return
+ if(length(money_bag.contents) == money_bag.storage_slots)
+ visible_message(span_warning("[capitalize(money_bag.declent_ru(NOMINATIVE))] полон!"))
+ return
+ if(!materials.can_use_amount(COIN_COST, chosen_material))
+ visible_message(span_warning("Недостаточно выбранного материала для производства!"))
+ return
+ active = TRUE
+
+/obj/machinery/mineral/mint/proc/eject_bag(mob/user)
+ if(!money_bag || !(user && iscarbon(user) && user.Adjacent(src)))
+ return
+ if(active)
+ active = FALSE
+ if(user.put_in_hands(money_bag))
+ to_chat(user, span_notice("Вы забираете [money_bag.declent_ru(ACCUSATIVE)] из [declent_ru(GENITIVE)]."))
+ else
+ var/turf/T = get_step(src, output_dir)
+ money_bag.forceMove(T)
+ money_bag = null
+ SStgui.update_uis(src)
+
+/obj/machinery/mineral/mint/proc/material_insert()
+ SStgui.update_uis(src)
+
+#undef COIN_COST
diff --git a/code/modules/mining/money_bag.dm b/code/modules/mining/money_bag.dm
index 8a25b21b22d..340f7630394 100644
--- a/code/modules/mining/money_bag.dm
+++ b/code/modules/mining/money_bag.dm
@@ -2,6 +2,17 @@
/obj/item/storage/bag/money
name = "money bag"
+ desc = "Просторный мешок из плотной ткани, украшенный крупным символом доллара. \
+ Идеально подходит для хранения монет или банкнот. "
+
+ ru_names = list(
+ NOMINATIVE = "денежный мешок",
+ GENITIVE = "денежного мешка",
+ DATIVE = "денежному мешку",
+ ACCUSATIVE = "денежный мешок",
+ INSTRUMENTAL = "денежным мешком",
+ PREPOSITIONAL = "денежном мешке",
+ )
icon_state = "moneybag"
item_state = "moneybag"
force = 10
@@ -14,6 +25,7 @@
max_combined_w_class = 40
can_hold = list(/obj/item/coin, /obj/item/stack/spacecash)
+
/obj/item/storage/bag/money/vault/populate_contents()
new /obj/item/coin/silver(src)
new /obj/item/coin/silver(src)
diff --git a/code/modules/mining/ores_coins.dm b/code/modules/mining/ores_coins.dm
index 04ed72018ef..9966f669e5f 100644
--- a/code/modules/mining/ores_coins.dm
+++ b/code/modules/mining/ores_coins.dm
@@ -237,7 +237,7 @@ GLOBAL_LIST_INIT(sand_recipes, list(\
desc = "Extremely explosive if struck with mining equipment, Gibtonite is often used by miners to speed up their work by using it as a mining charge. This material is illegal to possess by unauthorized personnel under space law."
icon = 'icons/obj/mining.dmi'
icon_state = "Gibtonite ore"
- item_state = "Gibtonite ore"
+ item_state = "gibtonite"
w_class = WEIGHT_CLASS_BULKY
throw_range = 0
var/primed = FALSE
diff --git a/code/modules/mob/dead/dead.dm b/code/modules/mob/dead/dead.dm
index baac7eb2ac8..c6c0b535e1d 100644
--- a/code/modules/mob/dead/dead.dm
+++ b/code/modules/mob/dead/dead.dm
@@ -22,7 +22,7 @@
* updates the Z level for dead players
* If they don't have a new z, we'll keep the old one, preventing bugs from ghosting and re-entering, among others
*/
-/mob/dead/proc/update_z(new_z)
+/mob/dead/update_z(new_z)
if(registered_z == new_z)
return
if(registered_z)
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 469f5cff6cb..128b427e089 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -21,6 +21,7 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER)
light_system = NO_LIGHT_SUPPORT
invisibility = INVISIBILITY_OBSERVER
pass_flags = PASSEVERYTHING
+ hud_type = /datum/hud/ghost
var/can_reenter_corpse
var/bootime = FALSE
var/started_as_observer //This variable is set to 1 when you enter the game as an observer.
diff --git a/code/modules/mob/living/alpha.dm b/code/modules/mob/living/alpha.dm
new file mode 100644
index 00000000000..d50b49746ac
--- /dev/null
+++ b/code/modules/mob/living/alpha.dm
@@ -0,0 +1,35 @@
+/mob/living/proc/alpha_update()
+ var/result = 1
+ for(var/source in alphas)
+ result *= alphas[source]
+
+ alpha = LIGHTING_PLANE_ALPHA_VISIBLE * result
+
+/mob/living/proc/alpha_prepare(source)
+ if(!(source in alphas))
+ alphas[source] = 1
+
+/mob/living/proc/alpha_finalise(source)
+ alphas[source] = clamp(alphas[source], 0, 1)
+ if(alphas[source] == 1 && source != ALPHA_SOURCE_DEFAULT)
+ alphas.Remove(source)
+
+ alpha_update()
+
+/mob/living/proc/alpha_add(val, source = ALPHA_SOURCE_DEFAULT)
+ alpha_prepare(source)
+ alphas[source] += val
+ alpha_finalise(source)
+
+/mob/living/proc/alpha_multiply(val, source = ALPHA_SOURCE_DEFAULT)
+ alpha_prepare(source)
+ alphas[source] *= val
+ alpha_finalise(source)
+
+/mob/living/proc/alpha_set(val, source = ALPHA_SOURCE_DEFAULT)
+ alpha_prepare(source)
+ alphas[source] = val
+ alpha_finalise(source)
+
+/mob/living/proc/alpha_get(source = ALPHA_SOURCE_DEFAULT)
+ return alphas[source]
diff --git a/code/modules/mob/living/carbon/alien/alien.dm b/code/modules/mob/living/carbon/alien/alien.dm
index dbfbb236453..d3fd0680bab 100644
--- a/code/modules/mob/living/carbon/alien/alien.dm
+++ b/code/modules/mob/living/carbon/alien/alien.dm
@@ -14,7 +14,12 @@
var/nightvision_enabled = FALSE
nightvision = 4
-
+
+ verb_say = "hisses"
+ verb_ask = "hisses curiously"
+ verb_exclaim = "roars"
+ verb_yell = "roars"
+
var/obj/item/card/id/wear_id = null // Fix for station bounced radios -- Skie
var/has_fine_manipulation = FALSE
var/move_delay_add = 0 // movement delay to add
@@ -97,17 +102,12 @@
return GLOB.all_languages[LANGUAGE_XENOS]
/mob/living/carbon/alien/say_quote(var/message, var/datum/language/speaking = null)
- var/verb = "hisses"
var/ending = copytext(message, length(message))
-
+
if(speaking && (speaking.name != "Galactic Common")) //this is so adminbooze xenos speaking common have their custom verbs,
- verb = speaking.get_spoken_verb(ending) //and use normal verbs for their own languages and non-common languages
+ return speaking.get_spoken_verb(ending) //and use normal verbs for their own languages and non-common languages
else
- if(ending=="!")
- verb = "roars"
- else if(ending=="?")
- verb = "hisses curiously"
- return verb
+ return ..()
/mob/living/carbon/alien/adjustToxLoss(
diff --git a/code/modules/mob/living/carbon/alien/death.dm b/code/modules/mob/living/carbon/alien/death.dm
index 777c180bd57..9d0605f5dde 100644
--- a/code/modules/mob/living/carbon/alien/death.dm
+++ b/code/modules/mob/living/carbon/alien/death.dm
@@ -16,7 +16,7 @@
flick("gibbed-a", animation)
xgibs(loc)
- GLOB.dead_mob_list -= src
+ remove_from_dead_mob_list()
QDEL_IN(animation, 15)
QDEL_IN(src, 15)
@@ -30,7 +30,7 @@
invisibility = INVISIBILITY_ABSTRACT
dust_animation()
new /obj/effect/decal/remains/xeno(loc)
- GLOB.dead_mob_list -= src
+ remove_from_dead_mob_list()
QDEL_IN(src, 15)
return TRUE
@@ -42,7 +42,7 @@
animation.master = src
flick("dust-a", animation)
new /obj/effect/decal/remains/xeno(loc)
- GLOB.dead_mob_list -= src
+ remove_from_dead_mob_list()
QDEL_IN(animation, 15)
/mob/living/carbon/alien/death(gibbed)
diff --git a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
index 1026d476882..b34ba7137e1 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
@@ -5,6 +5,7 @@
max_grab = GRAB_KILL
slowed_by_pull_and_push = FALSE
butcher_results = list(/obj/item/reagent_containers/food/snacks/monstermeat/xenomeat= 5, /obj/item/stack/sheet/animalhide/xeno = 1)
+ hud_type = /datum/hud/alien
var/obj/item/r_store = null
var/obj/item/l_store = null
var/caste = ""
diff --git a/code/modules/mob/living/carbon/alien/larva/larva.dm b/code/modules/mob/living/carbon/alien/larva/larva.dm
index 2ff4dc5b667..0e11a27fc79 100644
--- a/code/modules/mob/living/carbon/alien/larva/larva.dm
+++ b/code/modules/mob/living/carbon/alien/larva/larva.dm
@@ -18,6 +18,8 @@
death_message = "с тошнотворным шипением выдыха%(ет,ют)% воздух и пада%(ет,ют)% на пол..."
death_sound = null
+ hud_type = /datum/hud/larva
+
var/datum/action/innate/hide/alien_larva/hide_action
diff --git a/code/modules/mob/living/carbon/brain/MMI.dm b/code/modules/mob/living/carbon/brain/MMI.dm
index 7bf60ec50dc..1b92f72d71c 100644
--- a/code/modules/mob/living/carbon/brain/MMI.dm
+++ b/code/modules/mob/living/carbon/brain/MMI.dm
@@ -186,7 +186,7 @@
brainmob.container = null//Reset brainmob mmi var.
brainmob.forceMove(held_brain) //Throw mob into brain.
GLOB.respawnable_list += brainmob
- GLOB.alive_mob_list -= brainmob//Get outta here
+ brainmob.remove_from_alive_mob_list()//Get outta here
held_brain.brainmob = brainmob//Set the brain to use the brainmob
held_brain.brainmob.cancel_camera()
REMOVE_TRAIT(brainmob, TRAIT_NO_SPELLS, UNIQUE_TRAIT_SOURCE(src))
diff --git a/code/modules/mob/living/carbon/brain/brain.dm b/code/modules/mob/living/carbon/brain/brain.dm
index f16f77bb016..d5417f35dca 100644
--- a/code/modules/mob/living/carbon/brain/brain.dm
+++ b/code/modules/mob/living/carbon/brain/brain.dm
@@ -39,6 +39,13 @@
CRASH("Brainmob without container.")
forceMove(container)
+/mob/living/carbon/brain/update_mouse_pointer()
+ if (!client)
+ return
+ client.mouse_pointer_icon = initial(client.mouse_pointer_icon)
+ if(!container)
+ return
+
/*
This will return true if the brain has a container that leaves it less helpless than a naked brain
diff --git a/code/modules/mob/living/carbon/brain/brain_item.dm b/code/modules/mob/living/carbon/brain/brain_item.dm
index 18ac00640da..12e997c9fde 100644
--- a/code/modules/mob/living/carbon/brain/brain_item.dm
+++ b/code/modules/mob/living/carbon/brain/brain_item.dm
@@ -142,6 +142,7 @@
icon = 'icons/mob/slimes.dmi'
icon_state = "green slime extract"
mmi_icon_state = "slime_mmi"
+ parent_organ_zone = BODY_ZONE_CHEST
/obj/item/organ/internal/brain/golem
name = "Runic mind"
diff --git a/code/modules/mob/living/carbon/brain/robotic_brain.dm b/code/modules/mob/living/carbon/brain/robotic_brain.dm
index 6a5b1baf56e..a3f7db308b6 100644
--- a/code/modules/mob/living/carbon/brain/robotic_brain.dm
+++ b/code/modules/mob/living/carbon/brain/robotic_brain.dm
@@ -265,7 +265,7 @@
brainmob.dna.species = new /datum/species/machine() // Else it will default to human. And we don't want to clone IRC humans now do we?
brainmob.dna.ResetSE()
brainmob.dna.ResetUI()
- GLOB.dead_mob_list -= brainmob
+ brainmob.remove_from_dead_mob_list()
..()
/obj/item/mmi/robotic_brain/attack_ghost(mob/dead/observer/O)
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index b0d89a6afa2..d4f89e28eb6 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -28,7 +28,7 @@
if(stat == DEAD)
return
else
- show_message("Блоб атакует!")
+ show_message(span_userdanger("Блоб атакует!"))
adjustBruteLoss(10)
diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm
index 6152cecfe17..1cfac94b198 100644
--- a/code/modules/mob/living/carbon/human/examine.dm
+++ b/code/modules/mob/living/carbon/human/examine.dm
@@ -426,10 +426,6 @@
pose = addtext(pose,".") //Makes sure all emotes end with a period.
msg += "\n[p_they(TRUE)] [p_are()] [pose]"
- if(client && mind && !mind.offstation_role && user.mind?.special_role) // No ashwalkers, monkeys etc
- var/permission_granted = client.prefs.toggles2 & PREFTOGGLE_2_GIB_WITHOUT_OBJECTIVE
- msg += "\n
[span_info("Вы[permission_granted ? "" : " [span_warning("НЕ")]"] можете вывести этого игрока из игры не имея соответствующей цели.")]
"
-
. = list(msg)
SEND_SIGNAL(src, COMSIG_PARENT_EXAMINE, user, .)
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 63f34de85cc..8fe2746464a 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -335,7 +335,7 @@
if(stat == DEAD)
return
SEND_SIGNAL(src, COMSIG_ATOM_BLOB_ACT, B)
- show_message("The blob attacks you!")
+ show_message(span_userdanger("The blob attacks you!"))
var/dam_zone = list(
BODY_ZONE_CHEST,
BODY_ZONE_PRECISE_GROIN,
@@ -350,8 +350,7 @@
BODY_ZONE_PRECISE_R_FOOT,
)
var/obj/item/organ/external/affecting = get_organ(ran_zone(dam_zone))
- apply_damage(5, BRUTE, affecting, run_armor_check(affecting, "melee"))
-
+ apply_damage(5, BRUTE, affecting, run_armor_check(affecting, MELEE))
// Get rank from ID from hands, wear_id, pda, and then from uniform
/mob/living/carbon/human/proc/get_authentification_rank(var/if_no_id = "No id", var/if_no_job = "No job")
diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm
index b4059f4a60f..040145b9b64 100644
--- a/code/modules/mob/living/carbon/human/human_defines.dm
+++ b/code/modules/mob/living/carbon/human/human_defines.dm
@@ -15,6 +15,7 @@
num_hands = 0 //Populated on init through list/bodyparts
usable_hands = 0 //Populated on init through list/bodyparts
status_flags = parent_type::status_flags|CANSTAMCRIT
+ hud_type = /datum/hud/human
//Marking colour and style
var/list/m_colours = DEFAULT_MARKING_COLOURS //All colours set to #000000.
var/list/m_styles = DEFAULT_MARKING_STYLES //All markings set to None.
diff --git a/code/modules/mob/living/carbon/human/human_interaction.dm b/code/modules/mob/living/carbon/human/human_interaction.dm
index f731c0d8912..4a858d8abf8 100644
--- a/code/modules/mob/living/carbon/human/human_interaction.dm
+++ b/code/modules/mob/living/carbon/human/human_interaction.dm
@@ -1,241 +1,225 @@
/mob/living/carbon/human/Topic(href, href_list)
///////Interactions!!///////
- if(href_list["interaction"])
- if(usr.incapacitated() || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED))
- return
-
- //CONDITIONS
- var/mob/living/carbon/human/H = usr
- var/mob/living/carbon/human/P = H.partner
- if (!(P in view(H.loc)))
- return
- var/obj/item/organ/external/temp = H.bodyparts_by_name[BODY_ZONE_PRECISE_R_HAND]
- var/hashands = (temp?.is_usable())
- if (!hashands)
- temp = H.bodyparts_by_name[BODY_ZONE_PRECISE_L_HAND]
- hashands = (temp?.is_usable())
- temp = P.bodyparts_by_name[BODY_ZONE_PRECISE_R_HAND]
- var/hashands_p = (temp?.is_usable())
- if (!hashands_p)
- temp = P.bodyparts_by_name[BODY_ZONE_PRECISE_L_HAND]
- hashands = (temp?.is_usable())
- var/mouthfree = !((H.head && (H.head.flags_cover & HEADCOVERSMOUTH)) || (H.wear_mask && (H.wear_mask.flags_cover & MASKCOVERSMOUTH)))
- var/mouthfree_p = !((P.head && (P.head.flags_cover & HEADCOVERSMOUTH)) || (P.wear_mask && (P.wear_mask.flags_cover & MASKCOVERSMOUTH)))
-
- if(world.time <= H.last_interract + 1 SECONDS)
- return
- else
- H.last_interract = world.time
-
- if (href_list["interaction"] == "bow")
- H.custom_emote(message = "кланя[pluralize_ru(H.gender,"ет","ют")]ся [P].")
- if (istype(P.loc, /obj/structure/closet) && P.loc == H.loc)
- P.custom_emote(message = "кланя[pluralize_ru(H.gender,"ет","ют")]ся [P].")
-
- else if (href_list["interaction"] == "pet")
- if(((!istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && hashands && H.Adjacent(P))
- H.custom_emote(message = "[pick("глад[pluralize_ru(H.gender,"ит","ят")]", "поглажива[pluralize_ru(H.gender,"ет","ют")]")] [P].")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "[pick("глад[pluralize_ru(H.gender,"ит","ят")]", "поглажива[pluralize_ru(H.gender,"ет","ют")]")] [P].")
-
- else if (href_list["interaction"] == "scratch")
- if(((!istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && hashands && H.Adjacent(P))
- if(H.zone_selected == BODY_ZONE_HEAD && !((P.dna.species.name == SPECIES_MACNINEPERSON) || (P.dna.species.name == SPECIES_GREY) || (P.dna.species.name == SPECIES_UNATHI)))
- H.custom_emote(message = "[pick("чеш[pluralize_ru(H.gender,"ет","ут")] за ухом", "чеш[pluralize_ru(H.gender,"ет","ут")] голову")] [P].")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "[pick("чеш[pluralize_ru(H.gender,"ет","ут")] за ухом", "чеш[pluralize_ru(H.gender,"ет","ут")] голову")] [P].")
- else
- H.custom_emote(message = "[pick("чеш[pluralize_ru(H.gender,"ет","ут")]")] [P].")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "[pick("чеш[pluralize_ru(H.gender,"ет","ут")]")] [P].")
-
- else if (href_list["interaction"] == "give")
- if(H.Adjacent(P))
- if (((!istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && hashands)
- H.give(P)
-
- else if (href_list["interaction"] == "kiss")
- if( ((H.Adjacent(P) && !istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)))
- H.custom_emote(message = "целу[pluralize_ru(H.gender,"ет","ют")] [P].")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "целу[pluralize_ru(H.gender,"ет","ют")] [P].")
- else if (mouthfree)
- H.custom_emote(message = "посыла[pluralize_ru(H.gender,"ет","ют")] [P] воздушный поцелуй.")
-
- else if (href_list["interaction"] == "lick")
- if( ((H.Adjacent(P) && !istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && mouthfree && mouthfree_p)
- if (prob(90))
- H.custom_emote(message = "лизнул[genderize_ru(H.gender,"","а","о","и")] [P] в щеку.")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "лизнул[genderize_ru(H.gender,"","а","о","и")] [P] в щеку.")
- else
- H.custom_emote(message = "особо тщательно лизнул[genderize_ru(H.gender,"","а","о","и")] [P].")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "особо тщательно лизнул[genderize_ru(H.gender,"","а","о","и")] [P].")
-
- else if (href_list["interaction"] == "hug")
- if(((H.Adjacent(P) && !istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && hashands)
- H.custom_emote(message = "обнима[pluralize_ru(H.gender,"ет","ют")] [P].")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "обнима[pluralize_ru(H.gender,"ет","ют")] [P].")
- playsound(loc, 'sound/weapons/thudswoosh.ogg', 50, 1, -1)
-
- else if (href_list["interaction"] == "cheer")
- if(((H.Adjacent(P) && !istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && hashands)
- H.custom_emote(message = "похлопыва[pluralize_ru(H.gender,"ет","ют")] [P] по плечу.")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "похлопыва[pluralize_ru(H.gender,"ет","ют")] [P] по плечу.")
-
- else if (href_list["interaction"] == "five")
- if(((H.Adjacent(P) && !istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && hashands)
- H.custom_emote(message = "да[pluralize_ru(H.gender,"ёт","ют")] [P] пять.")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "да[pluralize_ru(H.gender,"ёт","ют")] [P] пять.")
- playsound(loc, 'sound/effects/snap.ogg', 25, 1, -1)
-
- else if (href_list["interaction"] == "handshake")
- if(((H.Adjacent(P) && !istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && hashands && hashands_p)
- H.custom_emote(message = "жм[pluralize_ru(H.gender,"ёт","ут")] руку [P].")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "жм[pluralize_ru(H.gender,"ёт","ут")] руку [P].")
-
- else if (href_list["interaction"] == "bow_affably")
- H.custom_emote(message = "приветливо кивнул[genderize_ru(H.gender,"","а","о","и")] в сторону [P].")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "приветливо кивнул[genderize_ru(H.gender,"","а","о","и")] в сторону [P].")
-
- else if (href_list["interaction"] == "wave")
- if (!(H.Adjacent(P)) && hashands)
- H.custom_emote(message = "приветливо маш[pluralize_ru(H.gender,"ет","ут")] в сторону [P].")
+ if(!href_list["interaction"])
+ return ..()
+
+ if(usr.incapacitated() || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED))
+ return
+
+ //CONDITIONS
+ var/mob/living/carbon/human/H = usr
+ var/mob/living/carbon/human/P = H.partner
+ if(!(P in view(H.loc)))
+ return
+
+ if(world.time <= H.last_interract + 1 SECONDS)
+ return
+
+ H.last_interract = world.time
+
+ switch(href_list["interaction"])
+ if("bow")
+ H.custom_emote(message = "кланя[pluralize_ru(H.gender, "ет", "ют")]ся [P].")
+
+ if("pet")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED) || !P.Adjacent(H.loc))
+ return
+
+ H.custom_emote(message = "[pick("глад[pluralize_ru(H.gender, "ит", "ят")]", "поглажива[pluralize_ru(H.gender, "ет", "ют")]")] [P].")
+
+ if("scratch")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED) || !P.Adjacent(H.loc))
+ return
+
+ if(H.zone_selected != BODY_ZONE_HEAD || ismachineperson(P) || isunathi(P) || isgrey(P))
+ H.custom_emote(message = "[pick("чеш[pluralize_ru(H.gender, "ет", "ут")]")] [P].")
+
else
- H.custom_emote(message = "приветливо маш[pluralize_ru(H.gender,"ет","ут")] в сторону [P].")
-
-
- else if (href_list["interaction"] == "slap")
- if(((H.Adjacent(P) && !istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && hashands)
- switch(H.zone_selected)
- if(BODY_ZONE_HEAD)
- H.custom_emote(message = "да[pluralize_ru(H.gender,"ет","ют")] [P] пощечину!")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "да[pluralize_ru(H.gender,"ет","ют")] [P] пощечину!")
- playsound(loc, 'sound/effects/snap.ogg', 50, 1, -1)
- var/obj/item/organ/external/head/head = P.get_organ(BODY_ZONE_HEAD)
- if(head?.brute_dam < 5)
- P.apply_damage(1, def_zone = head)
- H.do_attack_animation(P)
-
- if(BODY_ZONE_PRECISE_GROIN)
- H.custom_emote(message = "шлёпа[pluralize_ru(H.gender,"ет","ют")] [P] по заднице!")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "шлёпа[pluralize_ru(H.gender,"ет","ют")] [P] по заднице!")
- playsound(loc, 'sound/effects/snap.ogg', 50, 1, -1)
- var/obj/item/organ/external/groin/groin = P.get_organ(BODY_ZONE_PRECISE_GROIN)
- if(groin?.brute_dam < 5)
- P.apply_damage(1, def_zone = groin)
- H.do_attack_animation(P)
-
- if(BODY_ZONE_PRECISE_MOUTH)
- H.custom_emote(message = "да[pluralize_ru(H.gender,"ет","ют")] [P] по губе!")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "да[pluralize_ru(H.gender,"ет","ют")] [P] по губе!")
- playsound(loc, 'sound/effects/snap.ogg', 50, 1, -1)
- H.do_attack_animation(P)
-
- else if (href_list["interaction"] == "fuckyou")
- if(hashands)
- H.custom_emote(message = "показыва[pluralize_ru(H.gender,"ет","ют")] [P] средний палец!")
- if (istype(P.loc, /obj/structure/closet) && P.loc == H.loc)
- P.custom_emote(message = "показыва[pluralize_ru(H.gender,"ет","ют")] [P] средний палец!")
-
- else if (href_list["interaction"] == "knock")
- if(((H.Adjacent(P) && !istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && hashands)
- H.custom_emote(message = "да[pluralize_ru(H.gender,"ет","ют")] [P] подзатыльник!")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "да[pluralize_ru(H.gender,"ет","ют")] [P] подзатыльник!")
- playsound(loc, 'sound/weapons/throwtap.ogg', 50, 1, -1)
- var/obj/item/organ/external/head/head = P.get_organ(BODY_ZONE_HEAD)
- if(head?.brute_dam < 3)
- P.apply_damage(1, def_zone = head)
- H.do_attack_animation(P)
-
- else if (href_list["interaction"] == "spit")
- if(((H.Adjacent(P) && !istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && mouthfree)
- H.custom_emote(message = "плю[pluralize_ru(H.gender,"ёт","ют")] в [P]!")
- if(prob(20))
- P.AdjustEyeBlurry(3 SECONDS)
- if(istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "плю[pluralize_ru(H.gender,"ёт","ют")] в [P]!")
-
- else if (href_list["interaction"] == "threaten")
- if(hashands)
- H.custom_emote(message = "гроз[pluralize_ru(H.gender,"ит","ят")] [P] кулаком!")
- if (istype(P.loc, /obj/structure/closet) && H.loc == P.loc)
- P.custom_emote(message = "гроз[pluralize_ru(H.gender,"ит","ят")] [P] кулаком!")
-
- else if (href_list["interaction"] == "tongue")
- if(mouthfree)
- H.custom_emote(message = "показыва[pluralize_ru(H.gender,"ет","ют")] [P] язык!")
- if (istype(P.loc, /obj/structure/closet) && H.loc == P.loc)
- P.custom_emote(message = "показыва[pluralize_ru(H.gender,"ет","ют")] [P] язык!")
-
- else if (href_list["interaction"] == "pullwing")
- if(((H.Adjacent(P) && !istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && hashands && !HAS_TRAIT(H, TRAIT_HANDS_BLOCKED))
- if(!P.bodyparts_by_name[BODY_ZONE_WING])
- H.custom_emote(message = "пыта[pluralize_ru(H.gender,"ет","ют")]ся поймать [P] за крылья КОТОРЫХ НЕТ!!!")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "пыта[pluralize_ru(H.gender,"ет","ют")]ся поймать [P] за крылья КОТОРЫХ НЕТ!!!")
- return
- if (prob(30))
- var/obj/item/organ/external/wing/wing = P.get_organ(BODY_ZONE_WING)
- if ((wing.brute_dam == wing.max_damage || wing.is_dead() || wing.has_fracture()) && prob(20))
- H.custom_emote(message = "отрыва[pluralize_ru(H.gender,"ет","ют")] [P] крылья!")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "отрыва[pluralize_ru(H.gender,"ет","ют")] [P] крылья!")
- wing.droplimb()
- return
- H.custom_emote(message = "дёрга[pluralize_ru(H.gender,"ет","ют")] [P] за крылья!")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "дёрга[pluralize_ru(H.gender,"ет","ют")] [P] за крылья!")
- if(wing.brute_dam < 10)
- P.apply_damage(1, def_zone = wing)
+ H.custom_emote(message = "[pick("чеш[pluralize_ru(H.gender, "ет", "ут")] за ухом", "чеш[pluralize_ru(H.gender, "ет", "ут")] голову")] [P].")
+
+ if("give")
+ if(!P.Adjacent(H.loc))
+ return
+
+ H.give(P)
+
+ if("kiss")
+ if(!get_location_accessible(H, BODY_ZONE_PRECISE_MOUTH))
+ return
+
+ if(!P.Adjacent(H.loc))
+ H.custom_emote(message = "посыла[pluralize_ru(H.gender, "ет", "ют")] [P] воздушный поцелуй.")
+
+ else if(get_location_accessible(P, BODY_ZONE_PRECISE_MOUTH))
+ H.custom_emote(message = "целу[pluralize_ru(H.gender, "ет", "ют")] [P].")
+
+ if("lick")
+ if(!P.Adjacent(H.loc) || !get_location_accessible(H, BODY_ZONE_PRECISE_MOUTH) || !get_location_accessible(P, BODY_ZONE_PRECISE_MOUTH))
+ return
+
+ if(prob(90))
+ H.custom_emote(message = "лизнул[genderize_ru(H.gender, "", "а", "о", "и")] [P] в щеку.")
+
+ else
+ H.custom_emote(message = "особо тщательно лизнул[genderize_ru(H.gender, "", "а", "о", "и")] [P].")
+
+ if("hug")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED) || !P.Adjacent(H.loc))
+ return
+
+ H.custom_emote(message = "обнима[pluralize_ru(H.gender, "ет", "ют")] [P].")
+ playsound(loc, 'sound/weapons/thudswoosh.ogg', 50, TRUE, -1)
+
+ if("cheer")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED) || !P.Adjacent(H.loc))
+ return
+
+ H.custom_emote(message = "похлопыва[pluralize_ru(H.gender, "ет", "ют")] [P] по плечу.")
+
+ if("five")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED) || !P.Adjacent(H.loc))
+ return
+
+ H.custom_emote(message = "да[pluralize_ru(H.gender, "ёт", "ют")] [P] пять.")
+ playsound(loc, 'sound/effects/snap.ogg', 25, TRUE, -1)
+
+ if("handshake")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED) || HAS_TRAIT(P, TRAIT_HANDS_BLOCKED) || !P.Adjacent(H.loc))
+ return
+
+ H.custom_emote(message = "жм[pluralize_ru(H.gender, "ёт", "ут")] руку [P].")
+
+ if("bow_affably")
+ H.custom_emote(message = "приветливо кивнул[genderize_ru(H.gender, "", "а", "о", "и")] в сторону [P].")
+
+ if("wave")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED))
+ return
+
+ H.custom_emote(message = "приветливо маш[pluralize_ru(H.gender, "ет", "ут")] в сторону [P].")
+
+ if("slap")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED) || !P.Adjacent(H.loc))
+ return
+
+ var/obj/item/organ/external/targeted_organ = P.get_organ(H.zone_selected)
+ if(!targeted_organ)
+ return
+
+ switch(H.zone_selected)
+ if(BODY_ZONE_HEAD)
+ H.custom_emote(message = span_danger("да[pluralize_ru(H.gender, "ет", "ют")] [P] пощечину!"))
+
+ if(BODY_ZONE_PRECISE_GROIN)
+ H.custom_emote(message = span_danger("шлёпа[pluralize_ru(H.gender, "ет", "ют")] [P] по заднице!"))
+
+ if(BODY_ZONE_PRECISE_MOUTH)
+ H.custom_emote(message = span_danger("да[pluralize_ru(H.gender, "ет", "ют")] [P] по губе!"))
+
else
- H.custom_emote(message = "пыта[pluralize_ru(H.gender,"ет","ют")]ся поймать [P] за крылья!")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "пыта[pluralize_ru(H.gender,"ет","ют")]ся поймать [P] за крылья!")
-
- else if (href_list["interaction"] == "pull")
- if(((H.Adjacent(P) && !istype(P.loc, /obj/structure/closet)) || (H.loc == P.loc)) && hashands && !HAS_TRAIT(H, TRAIT_HANDS_BLOCKED))
- if(!P.bodyparts_by_name[BODY_ZONE_TAIL])
- H.custom_emote(message = "пыта[pluralize_ru(H.gender,"ет","ют")]ся поймать [P] за хвост КОТОРОГО НЕТ!!!")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "пыта[pluralize_ru(H.gender,"ет","ют")]ся поймать [P] за хвост КОТОРОГО НЕТ!!!")
return
- var/obj/item/organ/internal/cyberimp/tail/blade/implant = P.get_organ_slot(INTERNAL_ORGAN_TAIL_DEVICE)
- if(istype(implant) && implant.activated) // KEEP YOUR HANDS AWAY FROM ME!
- H.custom_emote(message = span_danger("пыта[pluralize_ru(H.gender,"ет","ют")]ся дёрнуть [P] за хвост, но резко одёргива[pluralize_ru(H.gender,"ет","ют")] руки!"))
- if(H.has_pain())
- H.emote("scream")
- H.apply_damage(5, implant.damage_type, BODY_ZONE_PRECISE_R_HAND)
- H.apply_damage(5, implant.damage_type, BODY_ZONE_PRECISE_L_HAND)
- return
+ if(targeted_organ.brute_dam < 5)
+ P.apply_damage(1, def_zone = targeted_organ)
- if (prob(30))
- var/obj/item/organ/external/tail/tail = P.get_organ(BODY_ZONE_TAIL)
- if ((tail.brute_dam == tail.max_damage || tail.is_dead() || tail.has_fracture()) && prob(20))
- H.custom_emote(message = "отрыва[pluralize_ru(H.gender,"ет","ют")] [P] хвост!")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "отрыва[pluralize_ru(H.gender,"ет","ют")] [P] хвост!")
- tail.droplimb()
- return
- H.custom_emote(message = "дёрга[pluralize_ru(H.gender,"ет","ют")] [P] за хвост!")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "дёрга[pluralize_ru(H.gender,"ет","ют")] [P] за хвост!")
- if(tail.brute_dam < 10)
- P.apply_damage(1, def_zone = tail)
- else
- H.custom_emote(message = "пыта[pluralize_ru(H.gender,"ет","ют")]ся поймать [P] за хвост!")
- if (istype(P.loc, /obj/structure/closet))
- P.custom_emote(message = "пыта[pluralize_ru(H.gender,"ет","ют")]ся поймать [P] за хвост!")
- return
- ..()
+ playsound(loc, 'sound/effects/snap.ogg', 50, TRUE, -1)
+ H.do_attack_animation(P)
+
+
+ if("fuckyou")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED))
+ return
+
+ H.custom_emote(message = span_danger("показыва[pluralize_ru(H.gender, "ет", "ют")] [P] средний палец!"))
+
+ if("knock")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED) || !P.Adjacent(H.loc))
+ return
+
+ var/obj/item/organ/external/head/head = P.get_organ(BODY_ZONE_HEAD)
+ if(!head)
+ return
+
+ if(head.brute_dam < 5)
+ P.apply_damage(1, def_zone = head)
+
+ H.custom_emote(message = span_danger("да[pluralize_ru(H.gender, "ет", "ют")] [P] подзатыльник!"))
+ playsound(loc, 'sound/weapons/throwtap.ogg', 50, TRUE, -1)
+ H.do_attack_animation(P)
+
+ if("spit")
+ if(!P.Adjacent(H.loc) || !get_location_accessible(H, BODY_ZONE_PRECISE_MOUTH))
+ return
+
+ H.custom_emote(message = span_danger("плю[pluralize_ru(H.gender, "ёт", "ют")] в [P]!"))
+
+ if(prob(20))
+ P.AdjustEyeBlurry(3 SECONDS)
+
+ if("threaten")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED))
+ return
+
+ H.custom_emote(message = span_danger("гроз[pluralize_ru(H.gender, "ит", "ят")] [P] кулаком!"))
+
+ if("tongue")
+ if(!get_location_accessible(H, BODY_ZONE_PRECISE_MOUTH))
+ return
+
+ H.custom_emote(message = span_danger("показыва[pluralize_ru(H.gender, "ет", "ют")] [P] язык!"))
+
+ if("pullwing")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED) || !P.Adjacent(H.loc))
+ return
+
+ var/obj/item/organ/external/wing/wing = P.get_organ(BODY_ZONE_WING)
+ if(!wing)
+ H.custom_emote(message = "пыта[pluralize_ru(H.gender, "ет", "ют")]ся поймать [P] за крылья, [span_danger("КОТОРЫХ НЕТ!!!")]")
+ return
+
+ if(!prob(30))
+ H.custom_emote(message = "пыта[pluralize_ru(H.gender, "ет", "ют")]ся поймать [P] за крылья!")
+ return
+
+ if((wing.is_dead() || wing.has_fracture()) && prob(20))
+ H.custom_emote(message = span_danger("отрыва[pluralize_ru(H.gender, "ет", "ют")] [P] крылья!"))
+ wing.droplimb()
+ return
+
+ if(wing.brute_dam < 10)
+ P.apply_damage(1, def_zone = wing)
+
+ H.custom_emote(message = span_danger("дёрга[pluralize_ru(H.gender, "ет", "ют")] [P] за крылья!"))
+
+ if("pull")
+ if(HAS_TRAIT(H, TRAIT_HANDS_BLOCKED) || !P.Adjacent(H.loc))
+ return
+
+ var/obj/item/organ/external/tail/tail = P.get_organ(BODY_ZONE_TAIL)
+ if(!tail)
+ H.custom_emote(message = "пыта[pluralize_ru(H.gender, "ет", "ют")]ся поймать [P] за хвост, [span_danger("КОТОРОГО НЕТ!!!")]")
+ return
+
+ var/obj/item/organ/internal/cyberimp/tail/blade/implant = P.get_organ_slot(INTERNAL_ORGAN_TAIL_DEVICE)
+ if(istype(implant) && implant.activated) // KEEP YOUR HANDS AWAY FROM ME!
+ if(H.has_pain())
+ H.emote("scream")
+
+ H.custom_emote(message = span_danger("пыта[pluralize_ru(H.gender, "ет", "ют")]ся дёрнуть [P] за хвост, но резко одёргива[pluralize_ru(H.gender, "ет", "ют")] руки!"))
+ H.apply_damage(5, implant.damage_type, BODY_ZONE_PRECISE_R_HAND)
+ H.apply_damage(5, implant.damage_type, BODY_ZONE_PRECISE_L_HAND)
+ return
+
+ if(prob(70))
+ H.custom_emote(message = "пыта[pluralize_ru(H.gender, "ет", "ют")]ся поймать [P] за хвост!")
+ return
+
+ if((tail.is_dead() || tail.has_fracture()) && prob(20))
+ H.custom_emote(message = span_danger("отрыва[pluralize_ru(H.gender, "ет", "ют")] [P] хвост!"))
+ tail.droplimb()
+ return
+
+ if(tail.brute_dam < 10)
+ P.apply_damage(1, def_zone = tail)
+
+ H.custom_emote(message = span_danger("дёрга[pluralize_ru(H.gender, "ет", "ют")] [P] за хвост!"))
diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm
index b6a5786543c..d625200111c 100644
--- a/code/modules/mob/living/carbon/human/life.dm
+++ b/code/modules/mob/living/carbon/human/life.dm
@@ -291,6 +291,8 @@
if(!environment)
return
+ SEND_SIGNAL(src, COMSIG_HUMAN_EARLY_HANDLE_ENVIRONMENT, environment)
+
var/loc_temp = get_temperature(environment)
// to_chat(world, "Loc temp: [loc_temp] - Body temp: [bodytemperature] - Fireloss: [getFireLoss()] - Thermal protection: [get_main_thermal_protection()] - Fire protection: [thermal_protection + add_fire_protection(loc_temp)] - Heat capacity: [environment_heat_capacity] - Location: [loc] - src: [src]")
@@ -788,7 +790,8 @@
if(dna.species.update_health_hud())
return
else
-
+ if(SEND_SIGNAL(src, COMSIG_HUMAN_UPDATING_HEALTH_HUD, health) & COMPONENT_OVERRIDE_HEALTH_HUD)
+ return
var/shock_reduction = 0
if(HAS_TRAIT(src, TRAIT_NO_PAIN_HUD))
shock_reduction = INFINITY
diff --git a/code/modules/mob/living/carbon/human/species/drask.dm b/code/modules/mob/living/carbon/human/species/drask.dm
index 2d2b25dc886..04b47fd6e14 100644
--- a/code/modules/mob/living/carbon/human/species/drask.dm
+++ b/code/modules/mob/living/carbon/human/species/drask.dm
@@ -1,5 +1,3 @@
-#define DRASK_COOLINGSTARTTEMP 280
-#define ENVIRONMENT_COOLINGSTOPTEMP 400
#define DRASK_PITCH_SHIFT -0.1 // a bit lower emotes
/datum/species/drask
@@ -95,28 +93,40 @@
var/obj/item/organ/internal/eyes/E = H.get_int_organ(/obj/item/organ/internal/eyes)
return E.eye_colour
-/datum/species/drask/on_species_gain(mob/living/carbon/human/H)
+/datum/species/drask/on_species_gain(mob/living/carbon/human/human)
. = ..()
- add_verb(H, /mob/living/carbon/human/proc/emote_hum)
-/datum/species/drask/on_species_loss(mob/living/carbon/human/H)
+ var/datum/action/innate/drask/coma/coma = locate() in human.actions
+
+ if(!coma)
+ coma = new
+ coma.Grant(human)
+
+ add_verb(human, /mob/living/carbon/human/proc/emote_hum)
+
+/datum/species/drask/on_species_loss(mob/living/carbon/human/human)
+ . = ..()
+
+ var/datum/action/innate/drask/coma/coma = locate() in human.actions
+ coma?.Remove(human)
+
+ remove_verb(human, /mob/living/carbon/human/proc/emote_hum)
+
+/datum/species/drask/handle_life(mob/living/carbon/human/human)
. = ..()
- remove_verb(H, /mob/living/carbon/human/proc/emote_hum)
-/datum/species/drask/handle_life(mob/living/carbon/human/H)
- ..()
- if(H.stat == DEAD)
+ if(human.stat == DEAD)
return
- var/datum/gas_mixture/environment = H.return_air()
- if(environment && H.bodytemperature > DRASK_COOLINGSTARTTEMP && environment.temperature <= ENVIRONMENT_COOLINGSTOPTEMP)
- H.adjust_bodytemperature(-5)
- if(H.bodytemperature < TCRYO)
+
+ if(human.bodytemperature < TCRYO)
var/update = NONE
- update |= H.heal_overall_damage(2, 4, updating_health = FALSE)
- update |= H.heal_damages(tox = 0.5, oxy = 2, clone = 1, updating_health = FALSE)
+ update |= human.heal_overall_damage(2, 4, updating_health = FALSE)
+ update |= human.heal_damages(tox = 0.5, oxy = 2, clone = 1, updating_health = FALSE)
+
if(update)
- H.updatehealth()
- var/obj/item/organ/external/head/head = H.get_organ(BODY_ZONE_HEAD)
+ human.updatehealth()
+
+ var/obj/item/organ/external/head/head = human.get_organ(BODY_ZONE_HEAD)
head?.undisfigure()
/datum/species/drask/handle_reagents(mob/living/carbon/human/H, datum/reagent/R)
@@ -127,15 +137,65 @@
if("salglu_solution")
if(prob(33))
H.heal_overall_damage(1, 1, updating_health = FALSE)
+
H.reagents.remove_reagent(R.id, REAGENTS_METABOLISM * H.metabolism_efficiency * H.digestion_ratio)
return FALSE
+
return ..()
+/datum/action/innate/drask
+
+/datum/action/innate/drask/Grant(mob/user)
+ . = ..()
+
+ if(!. || !isliving(user))
+ return FALSE
+
+ return .
+
+/datum/action/innate/drask/coma
+ name = "Enter coma"
+ desc = "Постепенно вводит в состояние комы, понижает температуру тела. Повторная активация способности позволит прервать вход в кому, либо выйти из нее."
+
+ button_icon_state = "heal"
+
+ COOLDOWN_DECLARE(wake_up_cooldown)
+
+/datum/action/innate/drask/coma/Activate()
+ var/mob/living/living = owner
+
+ if(!living.has_status_effect(STATUS_EFFECT_DRASK_COMA))
+ handle_activation(living)
+ return
+
+ handle_deactivation(living)
+
+/datum/action/innate/drask/coma/proc/handle_activation(mob/living/living)
+ if(living.stat)
+ return FALSE
+
+ if(!do_after(living, 5 SECONDS, living, ALL, cancel_on_max = TRUE, max_interact_count = 1))
+ return FALSE
+
+ living.apply_status_effect(STATUS_EFFECT_DRASK_COMA)
+ COOLDOWN_START(src, wake_up_cooldown, 10 SECONDS)
+
+ return TRUE
+
+/datum/action/innate/drask/coma/proc/handle_deactivation(mob/living/living)
+ if(!COOLDOWN_FINISHED(src, wake_up_cooldown))
+ to_chat(living, span_warning("Вы не можете пробудиться сейчас."))
+ return FALSE
+
+ if(!do_after(living, 10 SECONDS, living, ALL, cancel_on_max = TRUE, max_interact_count = 1))
+ return FALSE
+
+ living.remove_status_effect(STATUS_EFFECT_DRASK_COMA)
+
+ return TRUE
+
/datum/species/drask/get_emote_pitch(mob/living/carbon/human/H, tolerance)
. = ..()
. += DRASK_PITCH_SHIFT
-
-#undef DRASK_COOLINGSTARTTEMP
-#undef ENVIRONMENT_COOLINGSTOPTEMP
#undef DRASK_PITCH_SHIFT
diff --git a/code/modules/mob/living/carbon/human/species/machine.dm b/code/modules/mob/living/carbon/human/species/machine.dm
index a8b58784216..c8e23ad1f82 100644
--- a/code/modules/mob/living/carbon/human/species/machine.dm
+++ b/code/modules/mob/living/carbon/human/species/machine.dm
@@ -167,11 +167,11 @@
if(!head_organ)
return
if(!robohead.is_monitor) //If they've got a prosthetic head and it isn't a monitor, they've no screen to adjust. Instead, let them change the colour of their optics!
- var/optic_colour = input(H, "Select optic colour", H.m_colours["head"]) as color|null
+ var/optic_colour = tgui_input_color(H, "Select optic colour", H.m_colours["head"])
if(H.incapacitated(INC_IGNORE_RESTRAINED|INC_IGNORE_GRABBED))
to_chat(H, "Ваша попытка сменить отображаемый цвет была прервана.")
return
- if(optic_colour)
+ if(!isnull(optic_colour))
H.change_markings(optic_colour, "head")
else if(robohead.is_monitor) //Means that the character's head is a monitor (has a screen). Time to customize.
diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm
index 3d8ab5a1387..cf013ce2c08 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -283,6 +283,8 @@
if(healths)
if(stat != DEAD)
. = TRUE
+ if(SEND_SIGNAL(src, COMSIG_CARBON_UPDATING_HEALTH_HUD, shown_health_amount) & COMPONENT_OVERRIDE_HEALTH_HUD)
+ return
if(shown_health_amount == null)
shown_health_amount = health
if(shown_health_amount >= maxHealth)
diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm
index 765ff34e0f8..a6ea4f598d3 100644
--- a/code/modules/mob/living/life.dm
+++ b/code/modules/mob/living/life.dm
@@ -4,15 +4,7 @@
SEND_SIGNAL(src, COMSIG_LIVING_LIFE, seconds, times_fired)
- if(client || registered_z) // This is a temporary error tracker to make sure we've caught everything
- var/turf/T = get_turf(src)
- if(client && registered_z != T.z)
- message_admins("[src] [ADMIN_FLW(src, "FLW")] has somehow ended up in Z-level [T.z] despite being registered in Z-level [registered_z]. If you could ask them how that happened and notify the coders, it would be appreciated.")
- add_misc_logs(src, "Z-TRACKING: [src] has somehow ended up in Z-level [T.z] despite being registered in Z-level [registered_z].")
- update_z(T.z)
- else if (!client && registered_z)
- add_misc_logs(src, "Z-TRACKING: [src] of type [src.type] has a Z-registration despite not having a client.")
- update_z(null)
+ track_z()
if(HAS_TRAIT(src, TRAIT_NO_TRANSFORM))
return FALSE
@@ -229,13 +221,13 @@
severity = 6
livingdoll.icon_state = "living[severity]"
if(!livingdoll.filtered)
- livingdoll.filtered = TRUE
var/icon/mob_mask = icon(icon, icon_state)
if(mob_mask.Height() > world.icon_size || mob_mask.Width() > world.icon_size)
var/health_doll_icon_state = health_doll_icon ? health_doll_icon : "megasprite"
mob_mask = icon('icons/mob/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.filtered = TRUE
if(severity > 0)
overlay_fullscreen("brute", /atom/movable/screen/fullscreen/brute, severity)
else
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 7ef73f42ec5..c01b8ec42ea 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -7,6 +7,8 @@
faction += "\ref[src]"
determine_move_and_pull_forces()
gravity_setup()
+ if(unique_name)
+ set_name()
if(ventcrawler_trait)
var/static/list/ventcrawler_sanity = list(
TRAIT_VENTCRAWLER_ALWAYS,
@@ -756,6 +758,7 @@
ExtinguishMob()
CureAllDiseases(FALSE)
fire_stacks = 0
+ fire_stacks = 0
on_fire = 0
suiciding = 0
if(buckled) //Unbuckle the mob and clear the alerts.
@@ -1640,35 +1643,6 @@
target.devoured(grabber)
-/mob/living/proc/update_z(new_z) // 1+ to register, null to unregister
- if(registered_z == new_z)
- return
- if(registered_z)
- SSmobs.clients_by_zlevel[registered_z] -= src
- if(isnull(client))
- registered_z = null
- return
- if(!new_z)
- registered_z = new_z
- return
- //Figure out how many clients were here before
- var/oldlen = SSmobs.clients_by_zlevel[new_z].len
- SSmobs.clients_by_zlevel[new_z] += src
- for(var/index in length(SSidlenpcpool.idle_mobs_by_zlevel[new_z]) to 1 step -1) //Backwards loop because we're removing (guarantees optimal rather than worst-case performance), it's fine to use .len here but doesn't compile on 511
- var/mob/living/simple_animal/animal = SSidlenpcpool.idle_mobs_by_zlevel[new_z][index]
- if(animal)
- if(!oldlen)
- //Start AI idle if nobody else was on this z level before (mobs will switch off when this is the case)
- animal.toggle_ai(AI_IDLE)
- //If they are also within a close distance ask the AI if it wants to wake up
- if(get_dist(get_turf(src), get_turf(animal)) < MAX_SIMPLEMOB_WAKEUP_RANGE)
- animal.consider_wakeup() // Ask the mob if it wants to turn on it's AI
- //They should clean up in destroy, but often don't so we get them here
- else
- SSidlenpcpool.idle_mobs_by_zlevel[new_z] -= animal
- registered_z = new_z
-
-
/mob/living/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents = TRUE)
..()
update_z(new_turf?.z)
@@ -1851,6 +1825,9 @@
return TRUE
return FALSE
+/mob/living/examine(mob/user, infix, suffix)
+ . = ..()
+ SEND_SIGNAL(src, COMSIG_LIVING_EXAMINE, user, .)
/**
* Sets the mob's direction lock towards a given atom.
@@ -2178,8 +2155,8 @@
update_blind_effects()
update_blurry_effects()
update_unconscious_overlay()
- GLOB.alive_mob_list += src
- GLOB.dead_mob_list -= src
+ add_to_alive_mob_list()
+ remove_from_dead_mob_list()
switch(stat) //Current stat.
if(CONSCIOUS)
@@ -2192,8 +2169,8 @@
SetLoseBreath(0)
SetDisgust(0)
SetEyeBlurry(0)
- GLOB.alive_mob_list -= src
- GLOB.dead_mob_list += src
+ remove_from_alive_mob_list()
+ add_to_dead_mob_list()
/// Updates hands HUD element.
@@ -2335,3 +2312,9 @@
. |= RECHARGE_SUCCESSFUL
to_chat(src, span_notice("You feel [(. & RECHARGE_SUCCESSFUL) ? "raw magical energy flowing through you, it feels good!" : "very strange for a moment, but then it passes."]"))
+
+/mob/living/proc/set_name()
+ if(numba == 0)
+ numba = rand(1, 1000)
+ name = "[name] ([numba])"
+ real_name = name
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 26a49da0a8e..791cde5c3ec 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -85,6 +85,13 @@
)
return shock_damage
+/mob/living/blob_vore_act(obj/structure/blob/special/core/voring_core)
+ . = ..()
+ if(HAS_TRAIT(src, TRAIT_BLOB_ZOMBIFIED) || QDELETED(src))
+ return FALSE
+ if(stat == DEAD)
+ forceMove(voring_core)
+
/mob/living/emp_act(severity)
..()
@@ -185,8 +192,8 @@
/mob/living/proc/IgniteMob()
if(fire_stacks > 0 && !on_fire)
on_fire = TRUE
- visible_message("[src.declent_ru(NOMINATIVE)] загора[pluralize_ru(src.gender,"ется","ются")]!", \
- "[pluralize_ru(src.gender,"Ты загораешься","Вы загораетесь")]!")
+ visible_message(span_warning("[src.declent_ru(NOMINATIVE)] загора[pluralize_ru(src.gender,"ется","ются")]!"), \
+ span_userdanger("[pluralize_ru(src.gender,"Ты загораешься","Вы загораетесь")]!"))
set_light_range(light_range + 3)
set_light_color("#ED9200")
throw_alert("fire", /atom/movable/screen/alert/fire)
@@ -212,6 +219,8 @@
/mob/living/proc/adjust_fire_stacks(add_fire_stacks) //Adjusting the amount of fire_stacks we have on person
SEND_SIGNAL(src, COMSIG_MOB_ADJUST_FIRE)
fire_stacks = clamp(fire_stacks + add_fire_stacks, -20, 20)
+ var/datum/status_effect/stacking/wet/wet_effect = has_status_effect(/datum/status_effect/stacking/wet)
+ wet_effect?.combine_wet_and_fire()
if(on_fire && fire_stacks <= 0)
ExtinguishMob()
@@ -239,6 +248,24 @@
SEND_SIGNAL(src, COMSIG_LIVING_FIRE_TICK)
return TRUE
+/mob/living/proc/WetMob(wet_type = /datum/status_effect/stacking/wet)
+ var/datum/status_effect/stacking/wet/effect = has_status_effect(wet_type)
+ return effect?.WetMob()
+
+
+/mob/living/proc/adjust_wet_stacks(add_wet_stacks, wet_type = /datum/status_effect/stacking/wet) //Adjusting the amount of fire_stacks we have on person
+ var/datum/status_effect/stacking/wet/effect = has_status_effect(wet_type)
+ if(effect)
+ effect.add_stacks(add_wet_stacks)
+ else
+ apply_status_effect(wet_type, add_wet_stacks)
+
+
+/mob/living/proc/DryMob(wet_type = /datum/status_effect/stacking/wet)
+ var/datum/status_effect/stacking/wet/effect = has_status_effect(wet_type)
+ return effect?.DryMob()
+
+
/mob/living/fire_act(datum/gas_mixture/air, exposed_temperature, exposed_volume, global_overlay = TRUE)
..()
adjust_fire_stacks(3)
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index 0ee33dffb53..ede980b09be 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -38,6 +38,7 @@
var/on_fire = 0 //The "Are we on fire?" var
var/fire_stacks = 0 //Tracks how many stacks of fire we have on, max is usually 20
+
var/mob_size = MOB_SIZE_HUMAN
var/metabolism_efficiency = 1 //more or less efficiency to metabolize helpful/harmful reagents and regulate body temperature..
var/digestion_ratio = 1 //controls how quickly reagents metabolize; largely governered by species attributes.
@@ -66,6 +67,9 @@
var/gene_stability = DEFAULT_GENE_STABILITY
var/ignore_gene_stability = 0
+ /// the id a mob gets when it's created
+ var/numba = 0
+ var/unique_name = FALSE
/// A log of what we've said, plain text, no spans or junk, essentially just each individual "message"
var/list/say_log
@@ -141,3 +145,6 @@
/// Famous last words -- if succumbing, what the user's last words were
var/last_words
+
+ /// List of alpha changelog from various sources
+ var/list/alphas = list(ALPHA_SOURCE_DEFAULT = 1)
diff --git a/code/modules/mob/living/living_infected_blob_mobs.dm b/code/modules/mob/living/living_infected_blob_mobs.dm
index 65970863476..b86bf2aac47 100644
--- a/code/modules/mob/living/living_infected_blob_mobs.dm
+++ b/code/modules/mob/living/living_infected_blob_mobs.dm
@@ -101,6 +101,10 @@
return FALSE
+/mob/living/simple_animal/hostile/illusion/can_be_blob()
+ return FALSE
+
+
/mob/living/simple_animal/hostile/asteroid/can_be_blob()
return FALSE
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index 67316cdb170..2e0ad06133e 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -205,6 +205,10 @@ GLOBAL_LIST_EMPTY(channel_to_radio_key)
if(check_mute(client.ckey, MUTE_IC))
to_chat(src, span_danger("You cannot speak in IC (Muted)."))
return FALSE
+
+ var/sigreturn = SEND_SIGNAL(src, COMSIG_MOB_TRY_SPEECH, message)
+ if(sigreturn & COMPONENT_CANNOT_SPEAK)
+ return FALSE
if(sanitize)
message = trim_strip_html_properly(message, 512)
@@ -234,6 +238,17 @@ GLOBAL_LIST_EMPTY(channel_to_radio_key)
var/datum/multilingual_say_piece/first_piece = message_pieces[1]
+ if(SEND_SIGNAL( \
+ src, \
+ COMSIG_LIVING_EARLY_SAY, \
+ message, \
+ verb, \
+ ignore_speech_problems, \
+ ignore_atmospherics, \
+ ignore_languages, \
+ first_piece) & COMPONENT_PREVENT_SPEAKING)
+ return FALSE
+
if(first_piece.speaking?.flags & HIVEMIND)
first_piece.speaking.broadcast(src, first_piece.message)
return TRUE
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 74bb56e8bd9..c2f2b0ed6cd 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -47,6 +47,7 @@ GLOBAL_LIST_INIT(ai_verbs_default, list(
sight = SEE_TURFS | SEE_MOBS | SEE_OBJS
nightvision = 8
can_buckle_to = FALSE
+ hud_type = /datum/hud/ai
var/list/network = list("SS13","Telecomms","Research Outpost","Mining Outpost")
var/obj/machinery/camera/current = null
var/list/connected_robots = list()
@@ -659,9 +660,8 @@ GLOBAL_LIST_INIT(ai_verbs_default, list(
/mob/living/silicon/ai/blob_act(obj/structure/blob/B)
if(stat != DEAD)
adjustBruteLoss(60)
- return 1
- return 0
-
+ return TRUE
+ return TRUE
/mob/living/silicon/ai/emp_act(severity)
..()
@@ -1351,7 +1351,7 @@ GLOBAL_LIST_INIT(ai_verbs_default, list(
return TRUE
-/mob/living/silicon/ai/proc/can_see(atom/A)
+/mob/living/silicon/ai/can_see(atom/A)
if(isturf(loc)) //AI in core, check if on cameras
//get_turf_pixel() is because APCs in maint aren't actually in view of the inner camera
//apc_override is needed here because AIs use their own APC when depowered
diff --git a/code/modules/mob/living/silicon/death.dm b/code/modules/mob/living/silicon/death.dm
index e3383f980b5..df12411bfd4 100644
--- a/code/modules/mob/living/silicon/death.dm
+++ b/code/modules/mob/living/silicon/death.dm
@@ -16,7 +16,7 @@
drop_hat()
- GLOB.dead_mob_list -= src
+ remove_from_dead_mob_list()
spawn(15)
if(animation) qdel(animation)
if(src) qdel(src)
@@ -28,7 +28,7 @@
icon = null
invisibility = INVISIBILITY_ABSTRACT
dust_animation()
- GLOB.dead_mob_list -= src
+ remove_from_dead_mob_list()
QDEL_IN(src, 15)
return TRUE
diff --git a/code/modules/mob/living/silicon/pai/pai.dm b/code/modules/mob/living/silicon/pai/pai.dm
index 709c20446e5..8efe5264e71 100644
--- a/code/modules/mob/living/silicon/pai/pai.dm
+++ b/code/modules/mob/living/silicon/pai/pai.dm
@@ -230,8 +230,8 @@
/mob/living/silicon/pai/blob_act()
if(stat != DEAD)
adjustBruteLoss(60)
- return 1
- return 0
+ return TRUE
+ return FALSE
/mob/living/silicon/pai/emp_act(severity)
diff --git a/code/modules/mob/living/silicon/robot/death.dm b/code/modules/mob/living/silicon/robot/death.dm
index b58059c48a3..3ce665ce098 100644
--- a/code/modules/mob/living/silicon/robot/death.dm
+++ b/code/modules/mob/living/silicon/robot/death.dm
@@ -22,8 +22,8 @@
drop_hat()
- GLOB.alive_mob_list -= src
- GLOB.dead_mob_list -= src
+ remove_from_alive_mob_list()
+ remove_from_dead_mob_list()
QDEL_IN(animation, 15)
QDEL_IN(src, 15)
return TRUE
@@ -36,7 +36,7 @@
invisibility = INVISIBILITY_ABSTRACT
if(mmi)
qdel(mmi) //Delete the MMI first so that it won't go popping out.
- GLOB.dead_mob_list -= src
+ remove_from_dead_mob_list()
QDEL_IN(src, 15)
return TRUE
diff --git a/code/modules/mob/living/silicon/robot/drone/drone_items.dm b/code/modules/mob/living/silicon/robot/drone/drone_items.dm
index ed9a968742d..3df009b8cd6 100644
--- a/code/modules/mob/living/silicon/robot/drone/drone_items.dm
+++ b/code/modules/mob/living/silicon/robot/drone/drone_items.dm
@@ -109,6 +109,54 @@
)
..()
+/obj/item/gripper/universal
+ name = "Universal gripper"
+ desc = "Универсальный захватывающий инструмент, используемый для выполнения сверх секретных заданий клана паука."
+ icon_state = "diskgripper"
+ can_hold = list(/obj/item/firealarm_electronics,
+ /obj/item/airalarm_electronics,
+ /obj/item/airlock_electronics,
+ /obj/item/firelock_electronics,
+ /obj/item/intercom_electronics,
+ /obj/item/apc_electronics,
+ /obj/item/access_control,
+ /obj/item/tracker_electronics,
+ /obj/item/stock_parts,
+ /obj/item/vending_refill,
+ /obj/item/mounted/frame/light_fixture,
+ /obj/item/mounted/frame/apc_frame,
+ /obj/item/mounted/frame/alarm_frame,
+ /obj/item/mounted/frame/firealarm,
+ /obj/item/mounted/frame/newscaster_frame,
+ /obj/item/mounted/frame/intercom,
+ /obj/item/mounted/frame/extinguisher,
+ /obj/item/mounted/frame/light_switch,
+ /obj/item/mounted/frame/door_control,
+ /obj/item/assembly/control,
+ /obj/item/rack_parts,
+ /obj/item/camera_assembly,
+ /obj/item/tank,
+ /obj/item/circuitboard,
+ /obj/item/stack/tile/light,
+ /obj/item/stack/ore/bluespace_crystal,
+ /obj/item/organ,
+ /obj/item/reagent_containers/iv_bag,
+ /obj/item/robot_parts/head,
+ /obj/item/robot_parts/l_arm,
+ /obj/item/robot_parts/r_arm,
+ /obj/item/robot_parts/l_leg,
+ /obj/item/robot_parts/r_leg,
+ /obj/item/robot_parts/chest,
+ /obj/item/stack/sheet/mineral/plasma,
+ /obj/item/card,
+ /obj/item/camera_film,
+ /obj/item/paper,
+ /obj/item/photo,
+ /obj/item/toy/plushie,
+ /obj/item/reagent_containers/food,
+ /obj/item/seeds,
+ /obj/item/disk/plantgene)
+
/obj/item/gripper/nuclear
name = "Nuclear gripper"
desc = "Designed for all your nuclear needs."
diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm
index 5deaf581453..4304861f5c6 100644
--- a/code/modules/mob/living/silicon/robot/robot.dm
+++ b/code/modules/mob/living/silicon/robot/robot.dm
@@ -110,6 +110,7 @@ GLOBAL_LIST_INIT(robot_verbs_default, list(
var/updating = 0 //portable camera camerachunk update
hud_possible = list(SPECIALROLE_HUD, DIAG_STAT_HUD, DIAG_HUD, DIAG_BATT_HUD)
+ hud_type = /datum/hud/robot
var/default_cell_type = /obj/item/stock_parts/cell/high
///Jetpack-like effect.
diff --git a/code/modules/mob/living/silicon/robot/robot_modules.dm b/code/modules/mob/living/silicon/robot/robot_modules.dm
index c97076e5903..8b84ed89c77 100644
--- a/code/modules/mob/living/silicon/robot/robot_modules.dm
+++ b/code/modules/mob/living/silicon/robot/robot_modules.dm
@@ -405,8 +405,11 @@
if(!robot.weapons_unlock)
var/count_secborgs = 0
- for(var/mob/living/silicon/robot/R in GLOB.alive_mob_list)
- if(R && R.stat != DEAD && R.module && istype(R.module, /obj/item/robot_module/security))
+ for(var/mob/living/silicon/robot/silicon in GLOB.alive_mob_list)
+ if(silicon == robot)
+ continue
+
+ if(silicon.stat != DEAD && silicon.module && istype(silicon.module, /obj/item/robot_module/security))
count_secborgs++
var/max_secborgs = 2
@@ -624,7 +627,7 @@
if(!istype(D, /obj/item/pickaxe/drill/cyborg/diamond))
qdel(D)
modules -= D // Remove it from this list so it doesn't get added in the rebuild.
-
+
modules += new /obj/item/pickaxe/drill/cyborg/diamond(src)
rebuild()
@@ -890,7 +893,7 @@
/obj/item/robot_module/hunter/New()
..()
- modules += new /obj/item/melee/energy/alien/claws(src)
+ modules += new /obj/item/melee/energy/alien_claws(src)
modules += new /obj/item/flash/cyborg/alien(src)
var/obj/item/reagent_containers/spray/alien/stun/S = new /obj/item/reagent_containers/spray/alien/stun(src)
S.reagents.add_reagent("cryogenic_liquid",250) //nerfed to sleeptoxin to make it less instant drop.
@@ -1076,7 +1079,7 @@
modules += new /obj/item/handheld_defibrillator(src)
modules += new /obj/item/twohanded/shockpaddles/borg(src)
modules += new /obj/item/restraints/handcuffs/cable/zipties(src)
- modules += new /obj/item/gripper(src)
+ modules += new /obj/item/gripper/universal(src)
modules += new /obj/item/flash/cyborg(src)
modules += new /obj/item/scalpel/laser/laser1(src)
modules += new /obj/item/hemostat(src)
@@ -1144,7 +1147,7 @@
return TRUE
return TRUE
-
+
else
return FALSE
diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm
index 06714b5a316..62c4b4b530a 100644
--- a/code/modules/mob/living/silicon/silicon.dm
+++ b/code/modules/mob/living/silicon/silicon.dm
@@ -50,6 +50,8 @@
diag_hud_set_status()
diag_hud_set_health()
+ ADD_TRAIT(src, TRAIT_WET_IMMUNITY, INNATE_TRAIT)
+
RegisterSignal(SSalarm, COMSIG_TRIGGERED_ALARM, PROC_REF(alarm_triggered))
RegisterSignal(SSalarm, COMSIG_CANCELLED_ALARM, PROC_REF(alarm_cancelled))
diff --git a/code/modules/mob/living/simple_animal/animal_defense.dm b/code/modules/mob/living/simple_animal/animal_defense.dm
index 70bdf1172f2..2c7a527d58f 100644
--- a/code/modules/mob/living/simple_animal/animal_defense.dm
+++ b/code/modules/mob/living/simple_animal/animal_defense.dm
@@ -156,7 +156,10 @@
adjustBruteLoss(bloss)
/mob/living/simple_animal/blob_act(obj/structure/blob/B)
- adjustBruteLoss(20)
+ var/result = ..()
+ if(result)
+ adjustBruteLoss(20)
+ return result
/mob/living/simple_animal/do_attack_animation(atom/A, visual_effect_icon, used_item, no_effect)
if(!no_effect && !visual_effect_icon && melee_damage_upper)
diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm
index 2199f6168da..d6c4e093865 100644
--- a/code/modules/mob/living/simple_animal/bot/bot.dm
+++ b/code/modules/mob/living/simple_animal/bot/bot.dm
@@ -24,6 +24,9 @@
light_system = MOVABLE_LIGHT
+ hud_type = /datum/hud/bot
+
+
var/obj/machinery/bot_core/bot_core = null
var/bot_core_type = /obj/machinery/bot_core
var/list/users = list() //for dialog updates
@@ -218,6 +221,8 @@
bot_core = new bot_core_type(src)
addtimer(CALLBACK(src, PROC_REF(add_bot_filter)), 3 SECONDS)
+ ADD_TRAIT(src, TRAIT_WET_IMMUNITY, INNATE_TRAIT)
+
prepare_huds()
for(var/datum/atom_hud/data/diagnostic/diag_hud in GLOB.huds)
diag_hud.add_to_hud(src)
@@ -240,6 +245,9 @@
/mob/living/simple_animal/bot/can_strip()
return FALSE
+/mob/living/simple_animal/bot/can_unarmed_attack()
+ return on
+
/mob/living/simple_animal/bot/med_hud_set_health()
return diag_hud_set_bothealth() //we use a different hud
diff --git a/code/modules/mob/living/simple_animal/bot/cleanbot.dm b/code/modules/mob/living/simple_animal/bot/cleanbot.dm
index baccfd4f241..c2cf432cfdf 100644
--- a/code/modules/mob/living/simple_animal/bot/cleanbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/cleanbot.dm
@@ -293,9 +293,7 @@
ejectpai()
-/mob/living/simple_animal/bot/cleanbot/UnarmedAttack(atom/A)
- if(!can_unarmed_attack())
- return
+/mob/living/simple_animal/bot/cleanbot/OnUnarmedAttack(atom/A)
if(istype(A,/obj/effect/decal/cleanable))
start_clean(A)
else
diff --git a/code/modules/mob/living/simple_animal/bot/ed209bot.dm b/code/modules/mob/living/simple_animal/bot/ed209bot.dm
index 361a97b2184..6b7b2378837 100644
--- a/code/modules/mob/living/simple_animal/bot/ed209bot.dm
+++ b/code/modules/mob/living/simple_animal/bot/ed209bot.dm
@@ -595,9 +595,7 @@
lasercolor = "r"
-/mob/living/simple_animal/bot/ed209/UnarmedAttack(atom/A)
- if(!on || !can_unarmed_attack())
- return
+/mob/living/simple_animal/bot/ed209/OnUnarmedAttack(atom/A)
if(iscarbon(A))
var/mob/living/carbon/C = A
if(C.staminaloss < 110 || arrest_type && !baton_delayed)
diff --git a/code/modules/mob/living/simple_animal/bot/floorbot.dm b/code/modules/mob/living/simple_animal/bot/floorbot.dm
index 2dc92554766..3f12a46f25f 100644
--- a/code/modules/mob/living/simple_animal/bot/floorbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/floorbot.dm
@@ -471,9 +471,7 @@
..()
-/mob/living/simple_animal/bot/floorbot/UnarmedAttack(atom/A)
- if(!can_unarmed_attack())
- return
+/mob/living/simple_animal/bot/floorbot/OnUnarmedAttack(atom/A)
if(isturf(A))
repair(A)
else if(istype(A,/obj/item/stack/tile/plasteel))
diff --git a/code/modules/mob/living/simple_animal/bot/griefsky.dm b/code/modules/mob/living/simple_animal/bot/griefsky.dm
index 865dc05a854..937b9932a3b 100644
--- a/code/modules/mob/living/simple_animal/bot/griefsky.dm
+++ b/code/modules/mob/living/simple_animal/bot/griefsky.dm
@@ -118,12 +118,12 @@
arrived.Weaken(4 SECONDS)
-/mob/living/simple_animal/bot/secbot/griefsky/UnarmedAttack(atom/A) //like secbots its only possible with admin intervention
- if(!on || !can_unarmed_attack())
+/mob/living/simple_animal/bot/secbot/griefsky/OnUnarmedAttack(atom/atom) //like secbots its only possible with admin intervention
+ if(!iscarbon(atom))
return
- if(iscarbon(A))
- var/mob/living/carbon/C = A
- sword_attack(C)
+
+ var/mob/living/carbon/carbon = atom
+ sword_attack(carbon)
/mob/living/simple_animal/bot/secbot/griefsky/bullet_act(obj/item/projectile/P) //so uncivilized
diff --git a/code/modules/mob/living/simple_animal/bot/honkbot.dm b/code/modules/mob/living/simple_animal/bot/honkbot.dm
index eb6296117a1..ec507703105 100644
--- a/code/modules/mob/living/simple_animal/bot/honkbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/honkbot.dm
@@ -139,9 +139,7 @@
..()
-/mob/living/simple_animal/bot/honkbot/UnarmedAttack(atom/A)
- if(!on || !can_unarmed_attack())
- return
+/mob/living/simple_animal/bot/honkbot/OnUnarmedAttack(atom/A)
if(iscarbon(A))
var/mob/living/carbon/C = A
if(emagged <= 1)
diff --git a/code/modules/mob/living/simple_animal/bot/medbot.dm b/code/modules/mob/living/simple_animal/bot/medbot.dm
index 49b6e53e178..6aa3304f30f 100644
--- a/code/modules/mob/living/simple_animal/bot/medbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/medbot.dm
@@ -498,9 +498,7 @@
return TRUE //If a valid medicine option for the patient exists, they require treatment
-/mob/living/simple_animal/bot/medbot/UnarmedAttack(atom/A)
- if(!can_unarmed_attack())
- return
+/mob/living/simple_animal/bot/medbot/OnUnarmedAttack(atom/A)
if(iscarbon(A))
var/mob/living/carbon/C = A
patient = C
diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm
index 61659f90974..4d72b9f5d95 100644
--- a/code/modules/mob/living/simple_animal/bot/mulebot.dm
+++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm
@@ -979,9 +979,7 @@
unload()
-/mob/living/simple_animal/bot/mulebot/UnarmedAttack(atom/A)
- if(!can_unarmed_attack())
- return
+/mob/living/simple_animal/bot/mulebot/OnUnarmedAttack(atom/A)
if(isturf(A) && isturf(loc) && loc.Adjacent(A) && load)
unload(get_dir(loc, A))
else
diff --git a/code/modules/mob/living/simple_animal/bot/secbot.dm b/code/modules/mob/living/simple_animal/bot/secbot.dm
index 9f9c4ed393c..327910850bd 100644
--- a/code/modules/mob/living/simple_animal/bot/secbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/secbot.dm
@@ -322,9 +322,7 @@
..()
-/mob/living/simple_animal/bot/secbot/UnarmedAttack(atom/A)
- if(!on || !can_unarmed_attack())
- return
+/mob/living/simple_animal/bot/secbot/OnUnarmedAttack(atom/A)
if(iscarbon(A))
var/mob/living/carbon/C = A
if((C.staminaloss < 110 || arrest_type) && !baton_delayed)
diff --git a/code/modules/mob/living/simple_animal/bot/syndicate.dm b/code/modules/mob/living/simple_animal/bot/syndicate.dm
index c8bcbcd2c71..a7a43bd133d 100644
--- a/code/modules/mob/living/simple_animal/bot/syndicate.dm
+++ b/code/modules/mob/living/simple_animal/bot/syndicate.dm
@@ -209,10 +209,8 @@
return
-/mob/living/simple_animal/bot/ed209/syndicate/UnarmedAttack(atom/A)
- if(!on || !can_unarmed_attack())
- return
- shootAt(A)
+/mob/living/simple_animal/bot/ed209/syndicate/OnUnarmedAttack(atom/A)
+ return shootAt(A)
/mob/living/simple_animal/bot/ed209/syndicate/start_cuffing(mob/living/carbon/C)
diff --git a/code/modules/mob/living/simple_animal/constructs.dm b/code/modules/mob/living/simple_animal/constructs.dm
index 3f4e0f0ccac..089dc0ef118 100644
--- a/code/modules/mob/living/simple_animal/constructs.dm
+++ b/code/modules/mob/living/simple_animal/constructs.dm
@@ -127,6 +127,7 @@
force_threshold = 11
playstyle_string = "You are a Juggernaut. Though slow, your shell can withstand extreme punishment, \
create shield walls, rip apart enemies and walls alike, and even deflect energy weapons."
+ hud_type = /datum/hud/construct/armoured
/mob/living/simple_animal/hostile/construct/armoured/hostile //actually hostile, will move around, hit things
AIStatus = AI_ON
@@ -177,6 +178,7 @@
retreat_distance = 2 //AI wraiths will move in and out of combat
playstyle_string = "You are a Wraith. Though relatively fragile, you are fast, deadly, and even able to phase through walls."
tts_seed = "Kelthuzad"
+ hud_type = /datum/hud/construct/wraith
/mob/living/simple_animal/hostile/construct/wraith/hostile //actually hostile, will move around, hit things
AIStatus = AI_ON
@@ -228,6 +230,7 @@
use magic missile, repair allied constructs (by clicking on them), \
and, most important of all, create new constructs by producing soulstones to capture souls, \
and shells to place those soulstones into."
+ hud_type = /datum/hud/construct/builder
/mob/living/simple_animal/hostile/construct/builder/Found(atom/A) //what have we found here?
@@ -309,6 +312,7 @@
attack_sound = 'sound/weapons/punch4.ogg'
force_threshold = 11
construct_type = "behemoth"
+ hud_type = /datum/hud/construct/armoured
var/energy = 0
var/max_energy = 1000
@@ -341,6 +345,7 @@
retreat_distance = 2 //AI harvesters will move in and out of combat, like wraiths, but shittier
playstyle_string = "You are a Harvester. You are not strong, but your powers of domination will assist you in your role: \
Bring those who still cling to this world of illusion back to the master so they may know Truth."
+ hud_type = /datum/hud/construct/harvester
/mob/living/simple_animal/hostile/construct/harvester/Process_Spacemove(movement_dir = NONE, continuous_move = FALSE)
diff --git a/code/modules/mob/living/simple_animal/friendly/animals_named.dm b/code/modules/mob/living/simple_animal/friendly/animals_named.dm
index a8b8f873064..cba5533f316 100644
--- a/code/modules/mob/living/simple_animal/friendly/animals_named.dm
+++ b/code/modules/mob/living/simple_animal/friendly/animals_named.dm
@@ -59,6 +59,8 @@
unique_pet = TRUE
gold_core_spawnable = NO_SPAWN
resting = TRUE
+ gender = FEMALE
+ tts_seed = "Widowmaker"
/mob/living/simple_animal/pet/cat/birman/Crusher
name = "Бедокур" //Не цель для воров
diff --git a/code/modules/mob/living/simple_animal/friendly/diona.dm b/code/modules/mob/living/simple_animal/friendly/diona.dm
index 188ed783e26..81bfddd6c38 100644
--- a/code/modules/mob/living/simple_animal/friendly/diona.dm
+++ b/code/modules/mob/living/simple_animal/friendly/diona.dm
@@ -96,9 +96,7 @@
evolve_action.Grant(src)
steal_blood_action.Grant(src)
-/mob/living/simple_animal/diona/UnarmedAttack(atom/A)
- if(!can_unarmed_attack())
- return
+/mob/living/simple_animal/diona/OnUnarmedAttack(atom/A)
if(isdiona(A) && (src in A.contents)) //can't attack your gestalt
visible_message("[src] wiggles around a bit.")
else
diff --git a/code/modules/mob/living/simple_animal/friendly/dog.dm b/code/modules/mob/living/simple_animal/friendly/dog.dm
index 1b3978b8299..84c6470ba4e 100644
--- a/code/modules/mob/living/simple_animal/friendly/dog.dm
+++ b/code/modules/mob/living/simple_animal/friendly/dog.dm
@@ -25,6 +25,7 @@
turns_per_move = 10
mob_size = MOB_SIZE_SMALL
gold_core_spawnable = FRIENDLY_SPAWN
+ hud_type = /datum/hud/corgi
var/bark_sound = list('sound/creatures/dog_bark1.ogg','sound/creatures/dog_bark2.ogg') //Used in emote.
var/bark_emote = list("ла%(ет,ют)%.", "гавка%(ет,ют)%.") // used in emote.
var/growl_sound = list('sound/creatures/dog_grawl1.ogg','sound/creatures/dog_grawl2.ogg') //Used in emote.
diff --git a/code/modules/mob/living/simple_animal/gondolas/gondola.dm b/code/modules/mob/living/simple_animal/gondolas/gondola.dm
new file mode 100644
index 00000000000..e12dbe4e15a
--- /dev/null
+++ b/code/modules/mob/living/simple_animal/gondolas/gondola.dm
@@ -0,0 +1,74 @@
+#define GONDOLA_HEIGHT pick(list("gondola_body_long", "gondola_body_medium", "gondola_body_short"))
+#define GONDOLA_COLOR pick(list("A87855", "915E48", "683E2C"))
+#define GONDOLA_MOUSTACHE pick(list("gondola_moustache_large", "gondola_moustache_small"))
+#define GONDOLA_EYES pick(list("gondola_eyes_close", "gondola_eyes_far"))
+
+/mob/living/simple_animal/pet/gondola
+ name = "gondola"
+ real_name = "gondola"
+ desc = "Гондола — бесшумный ходок. \
+ Не имея рук, он воплощает даосский принцип у-вэй (недействие), а его улыбающееся \
+ выражение лица показывает его полное и полное принятие мира таким, какой он есть."
+ icon = 'icons/mob/gondolas.dmi'
+ icon_state = "gondola"
+ icon_living = "gondola"
+
+ maxHealth = 200
+ health = 200
+ faction = list("gandola")
+ response_help = "pets"
+ response_disarm = "bops"
+ response_harm = "kicks"
+
+ //Gondolas aren't affected by cold.
+ unsuitable_atmos_damage = 0
+ del_on_death = TRUE
+
+ ///List of loot drops on death, since it deletes itself on death (like trooper).
+ loot = list(
+ /obj/effect/decal/cleanable/blood/gibs = 1,
+ )
+
+ ru_names = list(
+ NOMINATIVE = "гандола",
+ GENITIVE = "гандолы",
+ DATIVE = "гандоле",
+ ACCUSATIVE = "гандолу",
+ INSTRUMENTAL = "гандолой",
+ PREPOSITIONAL = "гандоле"
+ )
+
+
+/mob/living/simple_animal/pet/gondola/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_MUTE, INNATE_TRAIT)
+ create_gondola()
+
+/mob/living/simple_animal/pet/gondola/proc/create_gondola()
+ icon_state = null
+ icon_living = null
+ var/height = GONDOLA_HEIGHT
+ var/mutable_appearance/body_overlay = mutable_appearance(icon, height)
+ var/mutable_appearance/eyes_overlay = mutable_appearance(icon, GONDOLA_EYES)
+ var/mutable_appearance/moustache_overlay = mutable_appearance(icon, GONDOLA_MOUSTACHE)
+ body_overlay.color = ("#[GONDOLA_COLOR]")
+
+ //Offset the face to match the Gondola's height.
+ switch(height)
+ if("gondola_body_medium")
+ eyes_overlay.pixel_y = -4
+ moustache_overlay.pixel_y = -4
+ if("gondola_body_short")
+ eyes_overlay.pixel_y = -8
+ moustache_overlay.pixel_y = -8
+
+ cut_overlays(TRUE)
+ add_overlay(body_overlay)
+ add_overlay(eyes_overlay)
+ add_overlay(moustache_overlay)
+
+
+#undef GONDOLA_HEIGHT
+#undef GONDOLA_COLOR
+#undef GONDOLA_MOUSTACHE
+#undef GONDOLA_EYES
diff --git a/code/modules/mob/living/simple_animal/gondolas/gondolapod.dm b/code/modules/mob/living/simple_animal/gondolas/gondolapod.dm
new file mode 100644
index 00000000000..496605b4920
--- /dev/null
+++ b/code/modules/mob/living/simple_animal/gondolas/gondolapod.dm
@@ -0,0 +1,95 @@
+/mob/living/simple_animal/pet/gondola/gondolapod
+ name = "gondola"
+ real_name = "gondola"
+ desc = "Бесшумный ходок. Кажется, это сотрудник агентства доставки."
+ icon = 'icons/obj/supplypods.dmi'
+ icon_state = "gondola"
+ icon_living = "gondola"
+ SET_BASE_PIXEL(-16, -5) //2x2 sprite
+ layer = TABLE_LAYER //so that deliveries dont appear underneath it
+
+ ///Boolean on whether the pod is currently open, and should appear such.
+ var/opened = FALSE
+ ///The supply pod attached to the gondola, that actually holds the contents of our delivery.
+ var/obj/structure/closet/supplypod/centcompod/linked_pod
+
+ ///Static list of actions the gondola is given on creation, and taken away when it successfully delivers.
+ var/static/list/gondola_delivering_actions = list(
+ /datum/action/innate/deliver_gondola_package,
+ /datum/action/innate/check_gondola_contents,
+ )
+
+/mob/living/simple_animal/pet/gondola/gondolapod/Initialize(mapload, pod)
+ linked_pod = pod || new(src)
+ name = linked_pod.name
+ desc = linked_pod.desc
+ if(!linked_pod.stay_after_drop || !linked_pod.opened)
+ grant_actions_by_list(gondola_delivering_actions)
+ return ..()
+
+/mob/living/simple_animal/pet/gondola/gondolapod/death(gibbed)
+ QDEL_NULL(linked_pod) //Will cause the open() proc for the linked supplypod to be called with the "broken" parameter set to true, meaning that it will dump its contents on death
+ return ..()
+
+/mob/living/simple_animal/pet/gondola/gondolapod/create_gondola()
+ return
+
+/mob/living/simple_animal/pet/gondola/gondolapod/update_overlays()
+ . = ..()
+ if(opened)
+ . += "[icon_state]_open"
+
+/mob/living/simple_animal/pet/gondola/gondolapod/examine(mob/user)
+ . = ..()
+ if (contents.len)
+ . += span_notice("Похоже, посылка еще не доставлена.")
+ else
+ . += span_notice("Судя по всему, доставку уже осуществили.")
+
+/mob/living/simple_animal/pet/gondola/gondolapod/setOpened()
+ opened = TRUE
+ layer = initial(layer)
+ update_appearance()
+ addtimer(CALLBACK(src, TYPE_PROC_REF(/atom/, setClosed)), 5 SECONDS)
+
+/mob/living/simple_animal/pet/gondola/gondolapod/setClosed()
+ opened = FALSE
+ layer = OBJ_LAYER
+ update_appearance()
+
+///Opens the gondola pod and delivers its package, one-time use as it removes all delivery-related actions.
+/datum/action/innate/deliver_gondola_package
+ name = "Доставить"
+ desc = "Откройте хранилище и освободите все содержимое, хранящееся внутри."
+ button_icon_state = "arrow"
+
+/datum/action/innate/deliver_gondola_package/Trigger(left_click)
+ . = ..()
+ if(!.)
+ return
+
+ var/mob/living/simple_animal/pet/gondola/gondolapod/gondola_owner = owner
+ gondola_owner.linked_pod.open_pod(gondola_owner, forced = TRUE)
+ for(var/datum/action/actions as anything in gondola_owner.actions)
+ if(actions.type in gondola_owner.gondola_delivering_actions)
+ actions.Remove(gondola_owner)
+ return TRUE
+
+///Checks the contents of the gondola and lets them know what they're holding.
+/datum/action/innate/check_gondola_contents
+ name = "Проверить содержимое"
+ desc = "Посмотрите, сколько предметов вы сейчас держите в капсуле."
+ button_icon_state = "storage"
+
+/datum/action/innate/check_gondola_contents/Trigger(left_click)
+ . = ..()
+ if(!.)
+ return
+
+ var/mob/living/simple_animal/pet/gondola/gondolapod/gondola_owner = owner
+ var/total = gondola_owner.contents.len
+ if (total)
+ to_chat(gondola_owner, span_notice("You detect [total] object\s within your incredibly vast belly."))
+ else
+ to_chat(gondola_owner, span_notice("A closer look inside yourself reveals... nothing."))
+ return TRUE
diff --git a/code/modules/mob/living/simple_animal/hostile/bees.dm b/code/modules/mob/living/simple_animal/hostile/bees.dm
index 44a5af40f2f..fec8fa967ce 100644
--- a/code/modules/mob/living/simple_animal/hostile/bees.dm
+++ b/code/modules/mob/living/simple_animal/hostile/bees.dm
@@ -59,6 +59,7 @@
regenerate_icons()
AddComponent(/datum/component/swarming)
AddElement(/datum/element/simple_flying)
+ AddElement(/datum/element/reagent_attack/bee)
/mob/living/simple_animal/hostile/poison/bees/ComponentInitialize()
AddComponent( \
@@ -156,14 +157,6 @@
return //no don't attack the goddamm box
else
. = ..()
- if(. && isliving(target) && (!client || a_intent == INTENT_HARM))
- var/mob/living/L = target
- if(L.reagents)
- if(beegent)
- beegent.reaction_mob(L, REAGENT_INGEST)
- L.reagents.add_reagent(beegent.id, rand(1, 5))
- else
- L.reagents.add_reagent("beetoxin", 5)
/mob/living/simple_animal/hostile/poison/bees/proc/assign_reagent(datum/reagent/R)
if(istype(R))
diff --git a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
index 55d751ee2b4..f6c657164bd 100644
--- a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
+++ b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
@@ -37,10 +37,10 @@
talk_sound = list('sound/creatures/spider_talk1.ogg', 'sound/creatures/spider_talk2.ogg')
damaged_sound = list('sound/creatures/spider_attack1.ogg', 'sound/creatures/spider_attack2.ogg')
gold_core_spawnable = HOSTILE_SPAWN
- var/venom_per_bite = 0 // While the /poison/ type path remains as-is for consistency reasons, we're really talking about venom, not poison.
var/busy = 0
footstep_type = FOOTSTEP_MOB_CLAW
AI_delay_max = 0.5 SECONDS
+ hud_type = /datum/hud/simple_animal/spider
/mob/living/simple_animal/hostile/poison/giant_spider/ComponentInitialize()
AddComponent( \
@@ -49,15 +49,6 @@
cold_damage = 20, \
)
-/mob/living/simple_animal/hostile/poison/giant_spider/AttackingTarget()
- // This is placed here, NOT on /poison, because the other subtypes of /poison/ already override AttackingTarget() completely, and as such it would do nothing but confuse people there.
- . = ..()
- if(. && venom_per_bite > 0 && iscarbon(target) && (!client || a_intent == INTENT_HARM))
- var/mob/living/carbon/C = target
- var/inject_target = pick(BODY_ZONE_CHEST, BODY_ZONE_HEAD)
- if(C.can_inject(null, FALSE, inject_target, FALSE))
- C.reagents.add_reagent("spidertoxin", venom_per_bite)
-
/mob/living/simple_animal/hostile/poison/giant_spider/get_spacemove_backup(moving_direction, continuous_move)
. = ..()
// If we don't find any normal thing to use, attempt to use any nearby spider structure instead.
@@ -77,10 +68,21 @@
health = 40
melee_damage_lower = 5
melee_damage_upper = 10
- venom_per_bite = 30
var/atom/cocoon_target
var/fed = 0
+/mob/living/simple_animal/hostile/poison/giant_spider/nurse/Initialize(mapload)
+ . = ..()
+
+ AddElement( \
+ /datum/element/reagent_attack, \
+ "spidertoxin", \
+ 30, \
+ FALSE, \
+ null, \
+ list(BODY_ZONE_CHEST, BODY_ZONE_HEAD), \
+ )
+
//hunters have the most poison and move the fastest, so they can find prey
/mob/living/simple_animal/hostile/poison/giant_spider/hunter
desc = "Furry and dark purple, it makes you shudder to look at it. This one has sparkling purple eyes."
@@ -91,9 +93,19 @@
health = 120
melee_damage_lower = 10
melee_damage_upper = 20
- venom_per_bite = 10
move_to_delay = 5
+/mob/living/simple_animal/hostile/poison/giant_spider/hunter/Initialize(mapload)
+ . = ..()
+
+ AddElement(
+ /datum/element/reagent_attack, \
+ "spidertoxin", \
+ 10, \
+ FALSE, \
+ null, \
+ list(BODY_ZONE_CHEST, BODY_ZONE_HEAD), \
+ )
/mob/living/simple_animal/hostile/poison/giant_spider/handle_automated_movement() //Hacky and ugly.
. = ..()
diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm
index 32908854ec8..f25826fb2bd 100644
--- a/code/modules/mob/living/simple_animal/hostile/hostile.dm
+++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm
@@ -171,16 +171,39 @@
if(!search_objects)
. = hearers(vision_range, targets_from) - src //Remove self, so we don't suicide
- var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/spacepod))
-
- for(var/HM in typecache_filter_list(range(vision_range, targets_from), hostile_machines))
- if(can_see(targets_from, HM, vision_range))
+ var/static/possible_targets = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/spacepod, /mob/living))
+ for(var/HM in typecache_filter_list(range(vision_range, targets_from), possible_targets))
+ if(targets_from.can_see(HM, vision_range))
. += HM
else
. = oview(vision_range, targets_from)
if(retaliate_only)
return . &= enemies // Remove all entries that aren't in enemies
+/mob/living/simple_animal/hostile/can_see(atom/target, length)
+ if(!target || target.invisibility > see_invisible)
+ return FALSE
+ var/turf/current_turf = get_turf(src)
+ var/turf/target_turf = get_turf(target)
+ if(!current_turf || !target_turf) // nullspace
+ return FALSE
+ if(get_dist(current_turf, target_turf) > length)
+ return FALSE
+ if(current_turf == target_turf)//they are on the same turf, source can see the target
+ return TRUE
+ if(isliving(target) && (sight & SEE_MOBS))//if a mob sees mobs through walls, it always sees the target mob within line of sight
+ return TRUE
+ var/steps = 1
+ current_turf = get_step_towards(current_turf, target_turf)
+ while(current_turf != target_turf)
+ if(steps > length)
+ return FALSE
+ if(IS_OPAQUE_TURF(current_turf))
+ return FALSE
+ current_turf = get_step_towards(current_turf, target_turf)
+ steps++
+ return TRUE
+
/mob/living/simple_animal/hostile/proc/FindTarget(list/possible_targets)//Step 2, filter down possible targets to things we actually care about
if(QDELETED(src))
@@ -316,7 +339,7 @@
if(L in friends)
return FALSE
else
- if((faction_check && !attack_same) || L.stat)
+ if((faction_check && !attack_same) || L.stat > stat_attack)
return FALSE
return TRUE
@@ -463,7 +486,9 @@
SEND_SIGNAL(src, COMSIG_HOSTILE_ATTACKINGTARGET, target)
if(!client)
mob_attack_logs += "[time_stamp()] Attacked [target] at [COORD(src)]"
- return target.attack_animal(src)
+ var/result = target.attack_animal(src)
+ SEND_SIGNAL(src, COMSIG_HOSTILE_POST_ATTACKINGTARGET, target, result)
+ return result
/mob/living/simple_animal/hostile/proc/Aggro()
diff --git a/code/modules/mob/living/simple_animal/hostile/illusion.dm b/code/modules/mob/living/simple_animal/hostile/illusion.dm
index d3437761665..5282ea0e811 100644
--- a/code/modules/mob/living/simple_animal/hostile/illusion.dm
+++ b/code/modules/mob/living/simple_animal/hostile/illusion.dm
@@ -20,6 +20,11 @@
del_on_death = 1
+/mob/living/simple_animal/hostile/illusion/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_WET_IMMUNITY, INNATE_TRAIT)
+
+
/mob/living/simple_animal/hostile/illusion/Life()
..()
if(world.time > life_span)
diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/pet.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/pet.dm
index 833ef4de31a..71c05d106ec 100644
--- a/code/modules/mob/living/simple_animal/hostile/retaliate/pet.dm
+++ b/code/modules/mob/living/simple_animal/hostile/retaliate/pet.dm
@@ -20,4 +20,5 @@
unique_pet = TRUE
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 2, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
gender = FEMALE
+ hud_type = /datum/hud/simple_animal/spider
diff --git a/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_spiders.dm b/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_spiders.dm
index 593dbc8434b..9f5dda6e667 100644
--- a/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_spiders.dm
+++ b/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_spiders.dm
@@ -88,6 +88,9 @@ GLOBAL_LIST_EMPTY(ts_spiderling_list)
lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
sight = SEE_TURFS|SEE_MOBS|SEE_OBJS
+ // HUD
+ hud_type = /datum/hud/simple_animal/spider
+
// AI aggression settings
var/ai_target_method = TS_DAMAGE_SIMPLE
diff --git a/code/modules/mob/living/simple_animal/hostile/terror_spiders/widow.dm b/code/modules/mob/living/simple_animal/hostile/terror_spiders/widow.dm
index 0e5b72c0b2f..cfe298d286c 100644
--- a/code/modules/mob/living/simple_animal/hostile/terror_spiders/widow.dm
+++ b/code/modules/mob/living/simple_animal/hostile/terror_spiders/widow.dm
@@ -34,6 +34,10 @@
tts_seed = "Karastamper"
spider_intro_text = "Будучи Вдовой Ужаса, ваша цель - внести хаос на поле боя при помощи своих плевков, вы также смертоносны вблизи и с каждым укусом вводите в противников опасный яд. Несмотря на скорость и смертоносность, вы довольно хрупки, поэтому не стоит атаковать тяжело вооружённых противников!"
+/mob/living/simple_animal/hostile/poison/terror_spider/widow/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/reagent_attack/widow)
+
/mob/living/simple_animal/hostile/poison/terror_spider/widow/spider_specialattack(mob/living/carbon/human/L, poisonable)
. = ..()
if(!.)
@@ -41,15 +45,6 @@
L.AdjustSilence(10 SECONDS)
if(!poisonable)
return TRUE
- if(L.reagents.has_reagent("terror_black_toxin", 100))
- return TRUE
- var/inject_target = pick(BODY_ZONE_CHEST, BODY_ZONE_HEAD)
- if(HAS_TRAIT(L, TRAIT_INCAPACITATED) || L.can_inject(null, FALSE, inject_target, FALSE))
- L.reagents.add_reagent("terror_black_toxin", 33) // inject our special poison
- visible_message(span_danger("[src] buries its long fangs deep into the [inject_target] of [target]!"))
- else
- L.reagents.add_reagent("terror_black_toxin", 20)
- visible_message(span_danger("[src] pierces armour and buries its long fangs deep into the [inject_target] of [target]!"))
if(!ckey && (!(target in enemies) || L.reagents.has_reagent("terror_black_toxin", 60)))
step_away(src, L)
step_away(src, L)
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index 49f45d9499b..5d5286d9e71 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -9,6 +9,8 @@
universal_speak = 0
status_flags = CANPUSH
+ hud_type = /datum/hud/simple_animal
+
var/icon_living = ""
var/icon_dead = ""
var/icon_resting = ""
@@ -369,12 +371,9 @@
/mob/living/simple_animal/say_quote(message)
- var/verb = "says"
-
- if(speak_emote.len)
- verb = pick(speak_emote)
-
- return verb
+ if(speak_emote?.len)
+ return get_verb(speak_emote)
+ return ..()
/mob/living/simple_animal/proc/set_varspeed(var_value)
@@ -697,6 +696,11 @@
/mob/living/simple_animal/Login()
..()
SSmove_manager.stop_looping(src) // if mob is moving under ai control, then stop AI movement
+ toggle_ai(AI_OFF)
+
+/mob/living/simple_animal/Logout()
+ . = ..()
+ toggle_ai(AI_ON)
/mob/living/simple_animal/say(message, verb = "says", sanitize = TRUE, ignore_speech_problems = FALSE, ignore_atmospherics = FALSE, ignore_languages = FALSE)
diff --git a/code/modules/mob/living/simple_animal/slime/say.dm b/code/modules/mob/living/simple_animal/slime/say.dm
index a921c499bb1..f61609ffd48 100644
--- a/code/modules/mob/living/simple_animal/slime/say.dm
+++ b/code/modules/mob/living/simple_animal/slime/say.dm
@@ -1,14 +1,3 @@
-/mob/living/simple_animal/slime/say_quote(text, datum/language/speaking)
- var/verb = "blorbles"
- var/ending = copytext(text, length(text))
-
- if(ending == "?")
- verb = "inquisitively blorbles"
- else if(ending == "!")
- verb = "loudly blorbles"
-
- return verb
-
/mob/living/simple_animal/slime/hear_say(list/message_pieces, verb = "says", italics = 0, mob/speaker = null, sound/speech_sound, sound_vol, sound_frequency, use_voice = TRUE, is_whisper = FALSE)
if(speaker != src && !stat)
if(speaker in Friends)
diff --git a/code/modules/mob/living/simple_animal/slime/slime.dm b/code/modules/mob/living/simple_animal/slime/slime.dm
index 6b51b2320a1..997dc6e86ad 100644
--- a/code/modules/mob/living/simple_animal/slime/slime.dm
+++ b/code/modules/mob/living/simple_animal/slime/slime.dm
@@ -20,7 +20,10 @@
response_disarm = "shoos"
response_harm = "stomps on"
emote_see = list("jiggles", "bounces in place")
- speak_emote = list("blorbles")
+ verb_say = "blorbles"
+ verb_ask = "inquisitively blorbles"
+ verb_exclaim = "loudly blorbles"
+ verb_yell = "loudly blorbles"
bubble_icon = "slime"
tts_seed = "Chen"
@@ -40,6 +43,8 @@
footstep_type = FOOTSTEP_MOB_SLIME
+ hud_type = /datum/hud/slime
+
var/cores = 1 // the number of /obj/item/slime_extract's the slime has left inside
var/mutation_chance = 30 // Chance of mutating, should be between 25 and 35
var/chance_reproduce = 80
diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm
index d4134aae515..0e9bd6c5f23 100644
--- a/code/modules/mob/login.dm
+++ b/code/modules/mob/login.dm
@@ -29,8 +29,7 @@
if(!client)
return FALSE
canon_client = client
- GLOB.player_list |= src
- GLOB.keyloop_list |= src
+ add_to_player_list()
last_known_ckey = ckey
update_Login_details()
world.update_status()
diff --git a/code/modules/mob/logout.dm b/code/modules/mob/logout.dm
index 44d97c84b3a..4ad2bd7fd1f 100644
--- a/code/modules/mob/logout.dm
+++ b/code/modules/mob/logout.dm
@@ -3,8 +3,7 @@
set_typing_indicator(FALSE)
SStgui.on_logout(src) // Cleanup any TGUIs the user has open
unset_machine()
- GLOB.player_list -= src
- GLOB.keyloop_list -= src
+ remove_from_player_list()
log_access_out(src)
add_game_logs("OWNERSHIP: [key_name(src)] is no longer owning mob [src]([src.type])")
// `holder` is nil'd out by now, so we check the `admin_datums` array directly
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 2d71404ccba..02b62e4adf1 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -1,7 +1,7 @@
/mob/Destroy()//This makes sure that mobs with clients/keys are not just deleted from the game.
- GLOB.mob_list -= src
- GLOB.dead_mob_list -= src
- GLOB.alive_mob_list -= src
+ remove_from_mob_list()
+ remove_from_alive_mob_list()
+ remove_from_dead_mob_list()
focus = null
QDEL_NULL(hud_used)
if(mind && mind.current == src)
@@ -25,11 +25,11 @@
return ..()
/mob/Initialize(mapload)
- GLOB.mob_list += src
+ add_to_mob_list()
if(stat == DEAD)
- GLOB.dead_mob_list += src
+ add_to_dead_mob_list()
else
- GLOB.alive_mob_list += src
+ add_to_alive_mob_list()
set_focus(src)
prepare_huds()
. = ..()
@@ -344,8 +344,10 @@
DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(run_examinate), A))
-/mob/proc/run_examinate(atom/A)
- var/list/result = A.examine(src)
+/mob/proc/run_examinate(atom/target)
+ var/list/result = target.examine(src)
+ SEND_SIGNAL(src, COMSIG_MOB_RUN_EXAMINATE, target, result)
+
to_chat(src, chat_box_examine(result.Join("\n")), MESSAGE_TYPE_INFO, confidential = TRUE)
@@ -591,8 +593,9 @@
/mob/proc/get_status_tab_items()
SHOULD_CALL_PARENT(TRUE)
- var/list/status_tab_data = list()
- return status_tab_data
+ . = list()
+ SEND_SIGNAL(src, COMSIG_MOB_GET_STATUS_TAB_ITEMS, .)
+ return .
// facing verbs
/mob/proc/canface()
@@ -1000,6 +1003,17 @@
SEND_SIGNAL(src, COMSIG_MOB_UPDATE_SIGHT)
sync_lighting_plane_alpha()
+///Update the mouse pointer of the attached client in this mob
+/mob/proc/update_mouse_pointer()
+ if(!client)
+ return
+
+ if(client.mouse_pointer_icon != initial(client.mouse_pointer_icon))//only send changes to the client if theyre needed
+ client.mouse_pointer_icon = initial(client.mouse_pointer_icon)
+
+ if(client.mouse_override_icon)
+ client.mouse_pointer_icon = client.mouse_override_icon
+
/mob/proc/set_vision_override(datum/vision_override/O)
QDEL_NULL(vision_type)
if(O) //in case of null
@@ -1164,3 +1178,43 @@ GLOBAL_LIST_INIT(holy_areas, typecacheof(list(
if(update)
update_actionspeed()
+/mob/proc/update_z(new_z) // 1+ to register, null to unregister
+ if(registered_z == new_z)
+ return
+ if(registered_z)
+ SSmobs.clients_by_zlevel[registered_z] -= src
+ if(isnull(client))
+ registered_z = null
+ return
+ if(!new_z)
+ registered_z = new_z
+ return
+ //Figure out how many clients were here before
+ var/oldlen = SSmobs.clients_by_zlevel[new_z].len
+ SSmobs.clients_by_zlevel[new_z] += src
+ for(var/index in length(SSidlenpcpool.idle_mobs_by_zlevel[new_z]) to 1 step -1) //Backwards loop because we're removing (guarantees optimal rather than worst-case performance), it's fine to use .len here but doesn't compile on 511
+ var/mob/living/simple_animal/animal = SSidlenpcpool.idle_mobs_by_zlevel[new_z][index]
+ if(animal)
+ if(!oldlen)
+ //Start AI idle if nobody else was on this z level before (mobs will switch off when this is the case)
+ animal.toggle_ai(AI_IDLE)
+ //If they are also within a close distance ask the AI if it wants to wake up
+ if(get_dist(get_turf(src), get_turf(animal)) < MAX_SIMPLEMOB_WAKEUP_RANGE)
+ animal.consider_wakeup() // Ask the mob if it wants to turn on it's AI
+ //They should clean up in destroy, but often don't so we get them here
+ else
+ SSidlenpcpool.idle_mobs_by_zlevel[new_z] -= animal
+ registered_z = new_z
+
+/mob/proc/track_z()
+ if(client || registered_z) // This is a temporary error tracker to make sure we've caught everything
+ var/turf/T = get_turf(src)
+ if(client && registered_z != T.z)
+ message_admins("[src] [ADMIN_FLW(src, "FLW")] has somehow ended up in Z-level [T.z] despite being registered in Z-level [registered_z]. If you could ask them how that happened and notify the coders, it would be appreciated.")
+ add_misc_logs(src, "Z-TRACKING: [src] has somehow ended up in Z-level [T.z] despite being registered in Z-level [registered_z].")
+ update_z(T.z)
+ else if(!client && registered_z)
+ add_misc_logs(src, "Z-TRACKING: [src] of type [src.type] has a Z-registration despite not having a client.")
+ update_z(null)
+
+
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index a76ad7796fa..66ecf8a68a2 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -93,7 +93,11 @@
var/list/datum/language/languages
/// For reagents that grant language knowlege.
var/list/temporary_languages
- var/list/speak_emote = list("says") // Verbs used when speaking. Defaults to 'say' if speak_emote is null.
+ var/list/speak_emote = list() // Verbs used when speaking. Defaults to 'say' if speak_emote is null.
+ var/verb_say = "says"
+ var/verb_ask = "asks"
+ var/verb_exclaim = list("exclaims", "shouts")
+ var/verb_yell = "yells"
/// Define emote default type, EMOTE_VISIBLE for seen emotes, EMOTE_AUDIBLE for heard emotes.
var/emote_type = EMOTE_VISIBLE
var/name_archive //For admin things like possession
@@ -123,6 +127,8 @@
var/obj/item/clothing/mask/wear_mask = null //Carbon
var/datum/hud/hud_used = null
+ /// Mob hud type
+ var/hud_type = /datum/hud
hud_possible = list(SPECIALROLE_HUD)
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index 075c44d9ffd..fd397aea8e3 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -850,3 +850,14 @@ GLOBAL_LIST_INIT(intents, list(INTENT_HELP,INTENT_DISARM,INTENT_GRAB,INTENT_HARM
return FALSE
return TRUE
+
+/// Takes in an associated list (key `/datum/action` typepaths, value is the AI blackboard key) and handles granting the action and adding it to the mob's AI controller blackboard.
+/// This is only useful in instances where you don't want to store the reference to the action on a variable on the mob.
+/mob/proc/grant_actions_by_list(list/input)
+ if(length(input) <= 0)
+ return
+
+ for(var/action in input)
+ var/datum/action/ability = new action(src)
+ ability.Grant(src)
+
diff --git a/code/modules/mob/mob_lists.dm b/code/modules/mob/mob_lists.dm
new file mode 100644
index 00000000000..d2e76b8f96f
--- /dev/null
+++ b/code/modules/mob/mob_lists.dm
@@ -0,0 +1,90 @@
+///Adds the mob reference to the list and directory of all mobs. Called on Initialize().
+/mob/proc/add_to_mob_list()
+ GLOB.mob_list |= src
+
+///Removes the mob reference from the list and directory of all mobs. Called on Destroy().
+/mob/proc/remove_from_mob_list()
+ GLOB.mob_list -= src
+
+///Adds the mob reference to the list of all mobs alive. If mob is cliented, it adds it to the list of all living player-mobs.
+/mob/proc/add_to_alive_mob_list()
+ if(QDELETED(src))
+ return
+ GLOB.alive_mob_list |= src
+ if(client)
+ add_to_current_living_players()
+
+///Removes the mob reference from the list of all mobs alive. If mob is cliented, it removes it from the list of all living player-mobs.
+/mob/proc/remove_from_alive_mob_list()
+ GLOB.alive_mob_list -= src
+ if(client)
+ remove_from_current_living_players()
+
+///Adds the mob reference to the list of all the dead mobs. If mob is cliented, it adds it to the list of all dead player-mobs.
+/mob/proc/add_to_dead_mob_list()
+ if(QDELETED(src))
+ return
+ GLOB.dead_mob_list |= src
+ if(client)
+ add_to_current_dead_players()
+
+///Remvoes the mob reference from list of all the dead mobs. If mob is cliented, it adds it to the list of all dead player-mobs.
+/mob/proc/remove_from_dead_mob_list()
+ GLOB.dead_mob_list -= src
+ if(client)
+ remove_from_current_dead_players()
+
+
+///Adds the cliented mob reference to the list of all player-mobs, besides to either the of dead or alive player-mob lists, as appropriate. Called on Login().
+/mob/proc/add_to_player_list()
+ SHOULD_CALL_PARENT(TRUE)
+ GLOB.player_list |= src
+ GLOB.keyloop_list |= src
+ if(stat == DEAD)
+ add_to_current_dead_players()
+ else
+ add_to_current_living_players()
+
+///Removes the mob reference from the list of all player-mobs, besides from either the of dead or alive player-mob lists, as appropriate. Called on Logout().
+/mob/proc/remove_from_player_list()
+ SHOULD_CALL_PARENT(TRUE)
+ GLOB.player_list -= src
+ GLOB.keyloop_list -= src
+ if(stat == DEAD)
+ remove_from_current_dead_players()
+ else
+ remove_from_current_living_players()
+
+
+///Adds the cliented mob reference to either the list of dead player-mobs or to the list of observers, depending on how they joined the game.
+/mob/proc/add_to_current_dead_players()
+ GLOB.dead_player_list |= src
+
+/mob/dead/observer/add_to_current_dead_players()
+ if(started_as_observer)
+ GLOB.current_observers_list |= src
+ return
+ return ..()
+
+/mob/dead/new_player/add_to_current_dead_players()
+ return
+
+///Removes the mob reference from either the list of dead player-mobs or from the list of observers, depending on how they joined the game.
+/mob/proc/remove_from_current_dead_players()
+ GLOB.dead_player_list -= src
+
+/mob/dead/observer/remove_from_current_dead_players()
+ if(started_as_observer)
+ GLOB.current_observers_list -= src
+ return
+ return ..()
+
+
+///Adds the cliented mob reference to the list of living player-mobs. If the mob is an antag, it adds it to the list of living antag player-mobs.
+/mob/proc/add_to_current_living_players()
+ GLOB.alive_player_list |= src
+
+///Removes the mob reference from the list of living player-mobs. If the mob is an antag, it removes it from the list of living antag player-mobs.
+/mob/proc/remove_from_current_living_players()
+ GLOB.alive_player_list -= src
+
diff --git a/code/modules/mob/mob_say.dm b/code/modules/mob/mob_say.dm
index 34adbed0045..50f0010cdf2 100644
--- a/code/modules/mob/mob_say.dm
+++ b/code/modules/mob/mob_say.dm
@@ -121,17 +121,23 @@
/mob/proc/say_quote(message, datum/language/speaking = null)
- var/verb = "says"
- var/ending = copytext(message, length(message))
-
+ var/ending = copytext_char(message, -1)
if(speaking)
- verb = genderize_decode(src, speaking.get_spoken_verb(ending))
- else
- if(ending == "!")
- verb = pick("exclaims", "shouts", "yells")
- else if(ending == "?")
- verb = "asks"
- return verb
+ return genderize_decode(src, speaking.get_spoken_verb(ending))
+ else if(ending == "!")
+ return get_verb(verb_exclaim)
+ else if(ending == "?")
+ return get_verb(verb_ask)
+ else if(copytext_char(message, -2) == "!!")
+ return get_verb(verb_yell)
+ return get_verb(verb_say)
+
+/mob/proc/get_verb(list/verbs)
+ if(!verbs)
+ return ""
+ if(!istype(verbs))
+ return verbs
+ return pick(verbs)
/// Transforms the speech emphasis mods from [/atom/movable/proc/say_emphasis] into the appropriate HTML tags
#define ENCODE_HTML_EMPHASIS(input, char, html, varname) \
diff --git a/code/modules/mob/mob_transformation_simple.dm b/code/modules/mob/mob_transformation_simple.dm
index 5e66d1a35f4..4f22cfe517f 100644
--- a/code/modules/mob/mob_transformation_simple.dm
+++ b/code/modules/mob/mob_transformation_simple.dm
@@ -47,7 +47,9 @@
mind.transfer_to(M)
else
M.key = key
-
+
+ SEND_SIGNAL(src, COMSIG_MOB_CHANGED_TYPE, M)
+
if(delete_old_mob)
spawn(1)
qdel(src)
diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm
index 404cb514df6..5fba35858c8 100644
--- a/code/modules/mob/new_player/new_player.dm
+++ b/code/modules/mob/new_player/new_player.dm
@@ -16,7 +16,7 @@
if(flags & INITIALIZED)
stack_trace("Warning: [src]([type]) initialized multiple times!")
flags |= INITIALIZED
- GLOB.mob_list += src
+ add_to_mob_list()
return INITIALIZE_HINT_NORMAL
/mob/new_player/proc/privacy_consent()
diff --git a/code/modules/pda/PDA.dm b/code/modules/pda/PDA.dm
index 5cf60cd6afc..b636cbc1aaa 100755
--- a/code/modules/pda/PDA.dm
+++ b/code/modules/pda/PDA.dm
@@ -12,6 +12,8 @@ GLOBAL_LIST_EMPTY(PDAs)
desc = "A portable microcomputer by Thinktronic Systems, LTD. Functionality determined by a preprogrammed ROM cartridge."
icon = 'icons/obj/pda.dmi'
icon_state = "pda"
+ lefthand_file = 'icons/mob/inhands/pda_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/pda_righthand.dmi'
w_class = WEIGHT_CLASS_TINY
item_flags = DENY_UI_BLOCKED
slot_flags = ITEM_SLOT_ID|ITEM_SLOT_PDA|ITEM_SLOT_BELT
diff --git a/code/modules/power/cable_coil.dm b/code/modules/power/cable_coil.dm
index c51cb95ab69..c94392cfc22 100644
--- a/code/modules/power/cable_coil.dm
+++ b/code/modules/power/cable_coil.dm
@@ -5,6 +5,8 @@
singular_name = "cable"
icon = 'icons/obj/engines_and_power/power.dmi'
icon_state = "coil"
+ righthand_file = 'icons/mob/inhands/tools_righthand.dmi'
+ lefthand_file = 'icons/mob/inhands/tools_lefthand.dmi'
item_state = "coil_red"
belt_icon = "cable_coil"
amount = MAXCOIL
@@ -20,11 +22,21 @@
materials = list(MAT_METAL=10, MAT_GLASS=5)
flags = CONDUCT
slot_flags = ITEM_SLOT_BELT
- item_state = "coil"
attack_verb = list("whipped", "lashed", "disciplined", "flogged")
usesound = 'sound/items/deconstruct.ogg'
toolspeed = 1
+ var/static/list/wire_colors = list(
+ WIRE_COLOR_BLUE = "blue",
+ WIRE_COLOR_CYAN = "cyan",
+ WIRE_COLOR_GREEN = "green",
+ WIRE_COLOR_ORANGE = "orange",
+ WIRE_COLOR_PINK = "pink",
+ WIRE_COLOR_RED = "red",
+ WIRE_COLOR_WHITE = "white",
+ WIRE_COLOR_YELLOW = "yellow"
+ )
+
/obj/item/stack/cable_coil/Initialize(mapload, new_amount, merge = TRUE, cable_color = null)
. = ..()
@@ -59,7 +71,7 @@
icon_state = "coil2"
else
icon_state = "coil"
-
+ item_state = wire_colors[color]
/obj/item/stack/cable_coil/update_weight()
if(amount == 1)
diff --git a/code/modules/power/cell.dm b/code/modules/power/cell.dm
index 8617697874b..692dcab1425 100644
--- a/code/modules/power/cell.dm
+++ b/code/modules/power/cell.dm
@@ -23,6 +23,9 @@
/obj/item/stock_parts/cell/laser
maxcharge = 1500
+/obj/item/stock_parts/cell/laser/gatling
+ maxcharge = 9000
+
/obj/item/stock_parts/cell/get_cell()
return src
diff --git a/code/modules/power/gravitygenerator.dm b/code/modules/power/gravitygenerator.dm
index 598647c2b75..f8dc37b811a 100644
--- a/code/modules/power/gravitygenerator.dm
+++ b/code/modules/power/gravitygenerator.dm
@@ -14,6 +14,8 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
#define GRAV_NEEDS_PLASTEEL 2
#define GRAV_NEEDS_WRENCH 3
+#define BLOB_HITS_NEED 4
+
//
// Abstract Generator
//
@@ -27,6 +29,8 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
use_power = NO_POWER_USE
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | NO_MALF_EFFECT
var/sprite_number = 0
+ /// Number of successful blob hits
+ var/blob_hits = 0
/obj/machinery/gravity_generator/ex_act(severity)
@@ -35,7 +39,8 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
/obj/machinery/gravity_generator/blob_act(obj/structure/blob/B)
- if(prob(20))
+ blob_hits++
+ if(blob_hits >= BLOB_HITS_NEED)
set_broken()
diff --git a/code/modules/power/port_gen.dm b/code/modules/power/port_gen.dm
index c3bc21edaa9..b84f766eb14 100644
--- a/code/modules/power/port_gen.dm
+++ b/code/modules/power/port_gen.dm
@@ -9,6 +9,7 @@
density = TRUE
anchored = FALSE
use_power = NO_POWER_USE
+ var/datum/looping_sound/port_gen/soundloop
var/active = 0
var/power_gen = 5000
@@ -17,6 +18,14 @@
var/power_output = 1
var/base_icon = "portgen0"
+/obj/machinery/power/port_gen/Initialize(mapload)
+ . = ..()
+ soundloop = new(list(src), active)
+
+/obj/machinery/power/port_gen/Destroy()
+ QDEL_NULL(soundloop)
+ return ..()
+
/obj/machinery/power/port_gen/proc/IsBroken()
return (stat & (BROKEN|EMPED))
@@ -39,10 +48,12 @@
if(active && HasFuel() && !IsBroken() && anchored && powernet)
add_avail(power_gen * power_output)
UseFuel()
+ soundloop.start()
else
active = 0
handleInactive()
update_icon(UPDATE_ICON_STATE)
+ soundloop.stop()
/obj/machinery/power/powered()
return TRUE //doesn't require an external power source
diff --git a/code/modules/power/singularity/field_generator.dm b/code/modules/power/singularity/field_generator.dm
index 0ed692764d0..75371cdce4f 100644
--- a/code/modules/power/singularity/field_generator.dm
+++ b/code/modules/power/singularity/field_generator.dm
@@ -146,7 +146,7 @@ field_generator power level display
/obj/machinery/field/generator/blob_act(obj/structure/blob/B)
if(active)
- return 0
+ return FALSE
else
..()
diff --git a/code/modules/power/singularity/particle_accelerator/particle_accelerator.dm b/code/modules/power/singularity/particle_accelerator/particle_accelerator.dm
index bcd6b2b01ab..61f5602019e 100644
--- a/code/modules/power/singularity/particle_accelerator/particle_accelerator.dm
+++ b/code/modules/power/singularity/particle_accelerator/particle_accelerator.dm
@@ -134,10 +134,6 @@ So, hopefully this is helpful if any more icons are to be added/changed/wonderin
master.toggle_power()
investigate_log("was moved whilst active; it powered down.", INVESTIGATE_ENGINE)
-/obj/machinery/particle_accelerator/control_box/blob_act(obj/structure/blob/B)
- if(prob(50) && !QDELETED(src))
- qdel(src)
-
/obj/structure/particle_accelerator/update_icon_state()
switch(construction_state)
if(ACCELERATOR_UNWRENCHED, ACCELERATOR_WRENCHED)
diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm
index 2973373acc0..3954a0bb1d7 100644
--- a/code/modules/power/supermatter/supermatter.dm
+++ b/code/modules/power/supermatter/supermatter.dm
@@ -355,7 +355,7 @@
consume(user)
-/obj/machinery/power/supermatter_shard/proc/get_integrity()
+/obj/machinery/power/supermatter_shard/proc/get_internal_integrity()
var/integrity = damage / explosion_point
integrity = round(100 - integrity * 100)
integrity = integrity < 0 ? 0 : integrity
@@ -541,16 +541,16 @@
if(!air)
return SUPERMATTER_ERROR
- if(get_integrity() < 25)
+ if(get_internal_integrity() < 25)
return SUPERMATTER_DELAMINATING
- if(get_integrity() < 50)
+ if(get_internal_integrity() < 50)
return SUPERMATTER_EMERGENCY
- if(get_integrity() < 75)
+ if(get_internal_integrity() < 75)
return SUPERMATTER_DANGER
- if((get_integrity() < 100) || (air.temperature > CRITICAL_TEMPERATURE))
+ if((get_internal_integrity() < 100) || (air.temperature > CRITICAL_TEMPERATURE))
return SUPERMATTER_WARNING
if(air.temperature > (CRITICAL_TEMPERATURE * 0.8))
diff --git a/code/modules/projectiles/ammunition/energy.dm b/code/modules/projectiles/ammunition/energy.dm
index 4f961b593bf..9a62d5890c8 100644
--- a/code/modules/projectiles/ammunition/energy.dm
+++ b/code/modules/projectiles/ammunition/energy.dm
@@ -18,6 +18,10 @@
muzzle_flash_color = LIGHT_COLOR_DARKRED
select_name = "kill"
+/obj/item/ammo_casing/energy/laser/light
+ projectile_type = /obj/item/projectile/beam/laser/light
+ delay = 0.9
+
/obj/item/ammo_casing/energy/laser/cyborg //to balance cyborg energy cost seperately
e_cost = 250
diff --git a/code/modules/projectiles/firing.dm b/code/modules/projectiles/firing.dm
index d17d0e9141e..dbd7faf36b9 100644
--- a/code/modules/projectiles/firing.dm
+++ b/code/modules/projectiles/firing.dm
@@ -18,6 +18,7 @@
user.changeNext_move(CLICK_CD_RANGE)
user.newtonian_move(get_dir(target, user))
update_icon()
+ SEND_SIGNAL(src, COMSIG_FIRE_CASING, target, user, firer_source_atom, randomspread, spread, zone_override, params, distro)
return TRUE
@@ -78,7 +79,7 @@
* If the user is holding a weapon in telekinesis grab,
* use a starting location from the firer source
*/
- var/fire_from_tk_grab = !isnull(firer_source_atom) && user.tkgrabbed_objects[firer_source_atom]
+ var/fire_from_tk_grab = !isnull(firer_source_atom) && ismob(user) && user.tkgrabbed_objects[firer_source_atom]
if (fire_from_tk_grab)
curloc = get_turf(firer_source_atom)
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index b6e4a1fb8f1..f0a5ce3190a 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -14,7 +14,7 @@
throw_range = 5
force = 5
origin_tech = "combat=1"
- needs_permit = 1
+ needs_permit = TRUE
attack_verb = list("struck", "hit", "bashed")
pickup_sound = 'sound/items/handling/gun_pickup.ogg'
drop_sound = 'sound/items/handling/gun_drop.ogg'
diff --git a/code/modules/projectiles/guns/energy.dm b/code/modules/projectiles/guns/energy.dm
index b3bc18b5fb7..40f2ee9cc98 100644
--- a/code/modules/projectiles/guns/energy.dm
+++ b/code/modules/projectiles/guns/energy.dm
@@ -14,6 +14,8 @@
var/modifystate = FALSE
var/shaded_charge = FALSE //if this gun uses a stateful charge bar for more detail
var/selfcharge = FALSE
+ /// Recharge rate if self-charging
+ var/recharge_rate = 100
var/can_charge = TRUE
var/charge_sections = 4
var/charge_tick = 0
@@ -184,7 +186,7 @@
charge_tick = 0
if(!cell)
return // check if we actually need to recharge
- cell.give(100) //... to recharge the shot
+ cell.give(recharge_rate) // to recharge the shot
on_recharge()
update_icon()
@@ -199,14 +201,14 @@
update_icon()
-/obj/item/gun/energy/can_shoot(mob/living/user)
+/obj/item/gun/energy/can_shoot(mob/living/user, silent = FALSE)
if(user && sibyl_mod && !sibyl_mod.check_auth(user))
return FALSE
var/obj/item/ammo_casing/energy/shot = ammo_type[select]
. = cell.charge >= shot.e_cost
- if(!.)
+ if(!. && !silent)
sibyl_mod?.sibyl_sound(user, 'sound/voice/dominator/battery.ogg', 5 SECONDS)
diff --git a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
index 31574f8430a..57fd110b524 100644
--- a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
+++ b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
@@ -798,7 +798,10 @@
/obj/item/borg/upgrade/modkit/tracer/adjustable/attack_self(mob/user)
- bolt_color = input(user,"","Choose Color",bolt_color) as color|null
+ var/color = tgui_input_color(user,"","Choose Color",bolt_color)
+ if(isnull(color))
+ return
+ bolt_color = color
#undef COMPATIBILITY_STANDART
diff --git a/code/modules/projectiles/guns/energy/laser.dm b/code/modules/projectiles/guns/energy/laser.dm
index fade0a3224b..a0ce4eaeabb 100644
--- a/code/modules/projectiles/guns/energy/laser.dm
+++ b/code/modules/projectiles/guns/energy/laser.dm
@@ -18,7 +18,7 @@
origin_tech = "combat=2;magnets=2"
ammo_type = list(/obj/item/ammo_casing/energy/laser/practice)
clumsy_check = 0
- needs_permit = 0
+ needs_permit = FALSE
/obj/item/gun/energy/laser/retro
name ="retro laser gun"
diff --git a/code/modules/projectiles/guns/energy/nuclear.dm b/code/modules/projectiles/guns/energy/nuclear.dm
index 6c49b4a29af..3e0f3670414 100644
--- a/code/modules/projectiles/guns/energy/nuclear.dm
+++ b/code/modules/projectiles/guns/energy/nuclear.dm
@@ -109,3 +109,62 @@
ammo_x_offset = 1
ammo_type = list(/obj/item/ammo_casing/energy/electrode, /obj/item/ammo_casing/energy/disabler, /obj/item/ammo_casing/energy/laser)
selfcharge = TRUE
+
+/obj/item/gun/energy/gun/minigun
+ name = "Laser gatling gun"
+ desc = "Огромное лазерное орудие, обладающее выдающейся скорострельностью и поражающей силой. Говорят, что 12 секунд стрельбы из этой малышки обойдутся вам в 400 тысяч кредитов."
+ ru_names = list(
+ NOMINATIVE = "Гатлинг-лазер",
+ GENITIVE = "Гатлинг-лазера",
+ DATIVE = "Гатлинг-лазеру",
+ ACCUSATIVE = "Гатлинг-лазер",
+ INSTRUMENTAL = "Гатлинг-лазером",
+ PREPOSITIONAL = "Гатлинг-лазере"
+ )
+ icon_state = "gatling"
+ item_state = "gatling"
+ fire_sound = "lasergatling"
+ origin_tech = "combat=7;magnets=6;powerstorage=6"
+ slot_flags = FALSE
+ resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
+ weapon_weight = WEAPON_MEDIUM
+ w_class = WEIGHT_CLASS_GIGANTIC
+ throw_range = 0
+ burst_size = 6
+ spread = 45
+ can_charge = FALSE
+ cell_type = /obj/item/stock_parts/cell/laser/gatling
+ ammo_type = list(/obj/item/ammo_casing/energy/laser/light)
+ selfcharge = TRUE
+ charge_delay = 5
+ recharge_rate = 600
+ slowdown = 0.2
+ var/force_unwielded = 10
+ var/force_wielded = 20
+
+/obj/item/gun/energy/gun/minigun/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/two_handed, \
+ force_unwielded = src.force_unwielded, \
+ force_wielded = src.force_wielded, \
+ require_twohands = TRUE \
+ )
+
+/obj/item/gun/energy/gun/minigun/can_be_pulled(atom/movable/user, force, show_message = FALSE)
+ ..()
+ balloon_alert(user, "слишком тяжело!")
+
+/obj/item/gun/energy/gun/minigun/update_icon_state()
+ item_state = !cell ? initial(item_state) : "gatling[!can_shoot(silent = TRUE) ? "1" : ""]"
+ icon_state = !cell ? initial(icon_state) : "gatling[!can_shoot(silent = TRUE) ? "1" : ""]"
+
+/obj/item/gun/energy/gun/minigun/examine(mob/user)
+ . = ..()
+
+ if(!cell)
+ return .
+
+ var/obj/item/ammo_casing/energy/shot = ammo_type[select]
+ var/charge_amount = round(cell.charge / (shot.e_cost * burst_size))
+
+ . += span_notice("Индикатор батареи сообщает: заряда хватит на [charge_amount] [declension_ru(charge_amount, "выстрел", "выстрела", "выстрелов")].")
diff --git a/code/modules/projectiles/guns/magic.dm b/code/modules/projectiles/guns/magic.dm
index 5651a0815c9..0514260bd3b 100644
--- a/code/modules/projectiles/guns/magic.dm
+++ b/code/modules/projectiles/guns/magic.dm
@@ -21,8 +21,8 @@
clumsy_check = 0
trigger_guard = TRIGGER_GUARD_ALLOW_ALL // Has no trigger at all, uses magic instead
- lefthand_file = 'icons/mob/inhands/items_lefthand.dmi' //not really a gun and some toys use these inhands
- righthand_file = 'icons/mob/inhands/items_righthand.dmi'
+ lefthand_file = 'icons/mob/inhands/staff_lefthand.dmi' //not really a gun and some toys use these inhands
+ righthand_file = 'icons/mob/inhands/staff_righthand.dmi'
/obj/item/gun/magic/afterattack(atom/target, mob/living/user, flag, params)
if(no_den_usage)
diff --git a/code/modules/projectiles/guns/magic/staff.dm b/code/modules/projectiles/guns/magic/staff.dm
index 8b6744f62ec..a3f0a957ff5 100644
--- a/code/modules/projectiles/guns/magic/staff.dm
+++ b/code/modules/projectiles/guns/magic/staff.dm
@@ -2,6 +2,8 @@
slot_flags = ITEM_SLOT_BACK
ammo_type = /obj/item/ammo_casing/magic
item_flags = NO_MAT_REDEMPTION
+ lefthand_file = 'icons/mob/inhands/staff_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/staff_righthand.dmi'
/obj/item/gun/magic/staff/change
name = "staff of change"
diff --git a/code/modules/projectiles/guns/medbeam.dm b/code/modules/projectiles/guns/medbeam.dm
index 46b2b95a7d3..ff0e97b4e2b 100644
--- a/code/modules/projectiles/guns/medbeam.dm
+++ b/code/modules/projectiles/guns/medbeam.dm
@@ -1,19 +1,33 @@
/obj/item/gun/medbeam
name = "Medical Beamgun"
- desc = "Delivers volatile medical nanites in a focused beam. Don't cross the beams!"
+ ru_names = list(
+ NOMINATIVE = "Медицинская Лучпушка",
+ GENITIVE = "Медицинской Лучпушки",
+ DATIVE = "Медицинской Лучпушке",
+ ACCUSATIVE = "Медицинскую Лучпушку",
+ INSTRUMENTAL = "Медицинской Лучпушкой",
+ PREPOSITIONAL = "Медицинской Лучпушку"
+ )
+
+ desc = "Передает целебные наниты своим сфокусированным лучом. Не скрещивайте лучи!"
+
icon = 'icons/obj/chronos.dmi'
icon_state = "chronogun"
item_state = "chronogun"
+
w_class = WEIGHT_CLASS_NORMAL
+ weapon_weight = WEAPON_MEDIUM
var/mob/living/current_target
- var/last_check = 0
- var/check_delay = 10 //Check los as often as possible, max resolution is SSobj tick though
+ var/datum/beam/current_beam
+
+ COOLDOWN_DECLARE(last_check)
+ var/check_delay = 1 SECONDS
+
var/max_range = 8
- var/active = FALSE
- var/datum/beam/current_beam = null
- weapon_weight = WEAPON_MEDIUM
+ var/active = FALSE
+ var/mounted = FALSE
/obj/item/gun/medbeam/Initialize(mapload)
@@ -49,6 +63,7 @@
active = FALSE
QDEL_NULL(current_beam)
on_beam_release(current_target)
+
current_target = null
@@ -61,10 +76,10 @@
SIGNAL_HANDLER
if(active && isliving(loc))
- to_chat(loc, span_warning("You lose control of the beam!"))
+ balloon_alert(loc, "контроль над лучом потерян")
current_beam = null
- active = FALSE //skip qdelling the beam again if we're doing this proc
+ active = FALSE // skip qdelling the beam again if we're doing this proc
LoseTarget()
@@ -77,18 +92,18 @@
LoseTarget()
if(old_target == target || !isliving(target))
- return
+ return FALSE
current_target = target
active = TRUE
current_beam = user.Beam(current_target, icon_state = "medbeam", time = 10 MINUTES, maxdistance = max_range, beam_type = /obj/effect/ebeam/medical)
- RegisterSignal(current_beam, COMSIG_QDELETING, PROC_REF(beam_died))//this is a WAY better rangecheck than what was done before (process check)
+ RegisterSignal(current_beam, COMSIG_QDELETING, PROC_REF(beam_died)) // this is a WAY better rangecheck than what was done before (process check)
SSblackbox.record_feedback("tally", "gun_fired", 1, type)
-
+ return TRUE
/obj/item/gun/medbeam/process()
- if(!ishuman(loc) && !isrobot(loc))
+ if(!mounted && !isliving(loc))
LoseTarget()
return
@@ -96,10 +111,10 @@
LoseTarget()
return
- if(world.time <= last_check + check_delay)
+ if(!COOLDOWN_FINISHED(src, last_check))
return
- last_check = world.time
+ COOLDOWN_START(src, last_check, check_delay)
if(!los_check(loc, current_target))
QDEL_NULL(current_beam)//this will give the target lost message
@@ -111,47 +126,69 @@
/obj/item/gun/medbeam/proc/los_check(atom/movable/user, mob/target)
var/turf/user_turf = user.loc
+
+ if(mounted)
+ user_turf = get_turf(user)
+
if(!istype(user_turf))
return FALSE
+
var/obj/dummy = new(user_turf)
- dummy.pass_flags |= (PASSTABLE|PASSGLASS|PASSGRILLE|PASSFENCE) //Grille/Glass so it can be used through common windows
+ dummy.pass_flags |= (PASSTABLE | PASSGLASS | PASSGRILLE | PASSFENCE) // Grille/Glass so it can be used through common windows
+
var/turf/previous_step = user_turf
var/first_step = TRUE
+
for(var/turf/next_step as anything in (get_line(user_turf, target) - user_turf))
if(first_step)
for(var/obj/blocker in user_turf)
if(!blocker.density || !(blocker.flags & ON_BORDER))
continue
+
if(blocker.CanPass(dummy, get_dir(user_turf, next_step)))
continue
+
qdel(dummy)
return FALSE // Could not leave the first turf.
+
first_step = FALSE
+
+ if(mounted && next_step == user_turf)
+ continue // Mechs are dense and thus fail the check
+
if(next_step.density)
qdel(dummy)
return FALSE
+
for(var/atom/movable/movable as anything in next_step)
if(!movable.CanPass(dummy, get_dir(next_step, previous_step)))
qdel(dummy)
return FALSE
+
for(var/obj/effect/ebeam/medical/B in next_step)// Don't cross the str-beams!
if(QDELETED(current_beam))
- break //We shouldn't be processing anymore.
+ break // We shouldn't be processing anymore.
+
if(QDELETED(B))
continue
+
if(!B.owner)
stack_trace("beam without an owner! [B]")
continue
+
if(B.owner.origin != current_beam.origin)
- next_step.visible_message(span_boldwarning("The medbeams cross and EXPLODE!"))
+ next_step.visible_message(span_boldwarning("Лучи пересекаются и ПРОИСХОДИТ ВЗРЫВ!"))
explosion(B.loc, heavy_impact_range = 3, light_impact_range = 5, flash_range = 8, cause = src)
qdel(dummy)
return FALSE
+
previous_step = next_step
+
qdel(dummy)
return TRUE
+
/obj/item/gun/medbeam/proc/on_beam_hit(mob/living/target)
return
@@ -160,11 +197,13 @@
var/prev_health = target.health
target.heal_overall_damage(4, 4)
var/bones_mended = FALSE
+
if(ishuman(target))
for(var/obj/item/organ/external/bodypart as anything in target.bodyparts)
if(bodypart.has_fracture() && prob(10))
bones_mended = TRUE
bodypart.mend_fracture()
+
if(target.health != prev_health || bones_mended)
new /obj/effect/temp_visual/heal(get_turf(target), "#80F5FF")
@@ -172,3 +211,5 @@
/obj/item/gun/medbeam/proc/on_beam_release(mob/living/target)
return
+/obj/item/gun/medbeam/mech
+ mounted = TRUE
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index 74e1e0e6f30..ffd8adfc77d 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -287,6 +287,10 @@
return FALSE
prehit(bumped_atom)
+ if(HAS_TRAIT(src, TRAIT_SHRAPNEL))
+ bumped_atom.hitby(src, TRUE)
+ qdel(src)
+
var/permutation = bumped_atom.bullet_act(src, def_zone) // searches for return value, could be deleted after run so check A isn't null
if(permutation == -1 || forcedodge)// the bullet passes through a dense object!
if(forcedodge > 0)
@@ -393,7 +397,7 @@
Angle = round(get_angle(src, current))
if(spread)
Angle += (rand() - 0.5) * spread
- if(firer)
+ if(firer && ismob(firer))
hit_crawling_mobs_chance = firer.a_intent == INTENT_HELP ? 0 : 100
// Turn right away
var/matrix/M = new
@@ -459,8 +463,12 @@
/obj/item/projectile/proc/check_ricochet_flag(atom/A)
- if(A.flags & CHECK_RICOCHET)
+ if((flag in list(ENERGY, LASER)) && (A.flags_ricochet & RICOCHET_SHINY))
+ return TRUE
+
+ if((flag in list(BOMB, BULLET)) && (A.flags_ricochet & RICOCHET_HARD))
return TRUE
+
return FALSE
diff --git a/code/modules/projectiles/projectile/beams.dm b/code/modules/projectiles/projectile/beams.dm
index ad9ddc0a321..0fe6b485d8d 100644
--- a/code/modules/projectiles/projectile/beams.dm
+++ b/code/modules/projectiles/projectile/beams.dm
@@ -19,6 +19,9 @@
/obj/item/projectile/beam/laser
+/obj/item/projectile/beam/laser/light
+ damage = 15
+
/obj/item/projectile/beam/laser/heavylaser
name = "heavy laser"
icon_state = "heavylaser"
diff --git a/code/modules/projectiles/projectile/shrapnel.dm b/code/modules/projectiles/projectile/shrapnel.dm
new file mode 100644
index 00000000000..15b004b4b4b
--- /dev/null
+++ b/code/modules/projectiles/projectile/shrapnel.dm
@@ -0,0 +1,28 @@
+/obj/item/projectile/shrapnel
+ name = "shrapnel"
+ icon = 'icons/obj/shards.dmi'
+ throwforce = 14
+ throw_speed = EMBED_THROWSPEED_THRESHOLD
+ embed_chance = 100
+ embedded_fall_chance = 0
+ w_class = WEIGHT_CLASS_SMALL
+ sharp = TRUE
+ damage = 14
+ range = 20
+ dismemberment = 5
+ ricochets_max = 2
+ ricochet_chance = 70
+ hitsound = 'sound/weapons/pierce.ogg'
+ ru_names = list(
+ NOMINATIVE = "шрапнель",
+ GENITIVE = "шрапнели",
+ DATIVE = "шрапнели",
+ ACCUSATIVE = "шрапнель",
+ INSTRUMENTAL = "шрапнелью",
+ PREPOSITIONAL = "шрапнели"
+ )
+
+/obj/item/projectile/shrapnel/Initialize(mapload)
+ . = ..()
+ icon_state = pick("shrapnel1", "shrapnel2", "shrapnel3")
+ ADD_TRAIT(src, TRAIT_SHRAPNEL, INNATE_TRAIT)
diff --git a/code/modules/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm
index 31326844b87..e30257381bf 100644
--- a/code/modules/reagents/chemistry/holder.dm
+++ b/code/modules/reagents/chemistry/holder.dm
@@ -137,9 +137,12 @@
/datum/reagents/proc/copy_to(obj/target, amount = 1, multiplier = 1, preserve_data = TRUE, safety = FALSE)
if(!target)
return
- if(!target.reagents || total_volume <= 0)
+ if(total_volume <= 0)
+ return
+
+ var/datum/reagents/R =(istype(target, /datum/reagents))? target : target?.reagents
+ if(!R || !istype(R))
return
- var/datum/reagents/R = target.reagents
amount = min(min(amount, total_volume), R.maximum_volume - R.total_volume)
var/part = amount / total_volume
var/trans_data = null
@@ -222,6 +225,18 @@
return transfered
+/datum/reagents/proc/can_metabolize(mob/living/carbon/human/H, datum/reagent/R)
+ if(!H.dna.species || !H.dna.species.reagent_tag)
+ return FALSE
+ if((R.process_flags & SYNTHETIC) && (H.dna.species.reagent_tag & PROCESS_SYN)) //SYNTHETIC-oriented reagents require PROCESS_SYN
+ return TRUE
+ if((R.process_flags & ORGANIC) && (H.dna.species.reagent_tag & PROCESS_ORG)) //ORGANIC-oriented reagents require PROCESS_ORG
+ return TRUE
+ //Species with PROCESS_DUO are only affected by reagents that affect both organics and synthetics, like acid and hellwater
+ if((R.process_flags & ORGANIC) && (R.process_flags & SYNTHETIC) && (H.dna.species.reagent_tag & PROCESS_DUO))
+ return TRUE
+
+
/datum/reagents/proc/metabolize(mob/living/M)
if(M)
temperature_reagents(M.bodytemperature - 30)
@@ -244,17 +259,7 @@
if(ishuman(M))
var/mob/living/carbon/human/H = M
//Check if this mob's species is set and can process this type of reagent
- var/can_process = FALSE
- //If we somehow avoided getting a species or reagent_tag set, we'll assume we aren't meant to process ANY reagents (CODERS: SET YOUR SPECIES AND TAG!)
- if(H.dna.species && H.dna.species.reagent_tag)
- if((R.process_flags & SYNTHETIC) && (H.dna.species.reagent_tag & PROCESS_SYN)) //SYNTHETIC-oriented reagents require PROCESS_SYN
- can_process = TRUE
- if((R.process_flags & ORGANIC) && (H.dna.species.reagent_tag & PROCESS_ORG)) //ORGANIC-oriented reagents require PROCESS_ORG
- can_process = TRUE
- //Species with PROCESS_DUO are only affected by reagents that affect both organics and synthetics, like acid and hellwater
- if((R.process_flags & ORGANIC) && (R.process_flags & SYNTHETIC) && (H.dna.species.reagent_tag & PROCESS_DUO))
- can_process = TRUE
-
+ var/can_process = can_metabolize(H, R)
//If handle_reagents returns 0, it's doing the reagent removal on its own
var/species_handled = !(H.dna.species.handle_reagents(H, R))
can_process = can_process && !species_handled
@@ -648,7 +653,7 @@
handle_reactions()
return FALSE
- var/datum/reagent/D = GLOB.chemical_reagents_list[reagent]
+ var/datum/reagent/D = (ispath(reagent))? new reagent() : GLOB.chemical_reagents_list[reagent]
if(D)
var/datum/reagent/R = new D.type()
@@ -746,6 +751,15 @@
/datum/reagents/proc/get_reagent(type)
. = locate(type) in reagent_list
+/datum/reagents/proc/get_reagent_by_id(id)
+ var/list/cached_reagents = reagent_list
+ for(var/A in cached_reagents)
+ var/datum/reagent/R = A
+ if(R.id == id)
+ return R
+
+ return
+
/datum/reagents/proc/remove_all_type(reagent_type, amount, strict = FALSE, safety = TRUE) // Removes all reagent of X type. @strict set to 1 determines whether the childs of the type are included.
if(!isnum(amount))
return TRUE
diff --git a/code/modules/reagents/chemistry/machinery/chem_master.dm b/code/modules/reagents/chemistry/machinery/chem_master.dm
index 3348df63932..150cfbdf9c4 100644
--- a/code/modules/reagents/chemistry/machinery/chem_master.dm
+++ b/code/modules/reagents/chemistry/machinery/chem_master.dm
@@ -112,10 +112,6 @@
if(powered())
. += "waitlight"
-/obj/machinery/chem_master/blob_act(obj/structure/blob/B)
- if(prob(50) && !QDELETED(src))
- qdel(src)
-
/obj/machinery/chem_master/power_change()
if(!..())
return
diff --git a/code/modules/reagents/chemistry/reagents.dm b/code/modules/reagents/chemistry/reagents.dm
index d22721d6f5f..96b7e362f07 100644
--- a/code/modules/reagents/chemistry/reagents.dm
+++ b/code/modules/reagents/chemistry/reagents.dm
@@ -68,6 +68,8 @@
return
/datum/reagent/proc/on_mob_life(mob/living/M)
+ if(current_cycle == 1)
+ on_mob_start_metabolize(M)
current_cycle++
var/total_depletion_rate = metabolization_rate * M.metabolism_efficiency * M.digestion_ratio // Cache it
@@ -75,8 +77,16 @@
sate_addiction(M)
holder.remove_reagent(id, total_depletion_rate) //By default it slowly disappears.
+ if(volume <= 0)
+ on_mob_end_metabolize(M)
return STATUS_UPDATE_NONE
+/datum/reagent/proc/on_mob_start_metabolize(mob/living/metabolizer)
+ return
+
+/datum/reagent/proc/on_mob_end_metabolize(mob/living/metabolizer)
+ return
+
/datum/reagent/proc/handle_addiction(mob/living/M, consumption_rate)
if(addiction_chance && count_by_type(M.reagents.addiction_list, addict_supertype) < 1)
var/datum/reagent/new_reagent = new addict_supertype()
diff --git a/code/modules/reagents/chemistry/reagents/blob.dm b/code/modules/reagents/chemistry/reagents/blob.dm
deleted file mode 100644
index b1a7952973e..00000000000
--- a/code/modules/reagents/chemistry/reagents/blob.dm
+++ /dev/null
@@ -1,195 +0,0 @@
-// These can only be applied by blobs. They are what blobs are made out of.
-// The 4 damage
-/datum/reagent/blob
- description = ""
- var/complementary_color = "#000000"
- var/message = "Блоб наносит вам удар" //message sent to any mob hit by the blob
- var/message_living = null //extension to first mob sent to only living mobs i.e. silicons have no skin to be burnt
- can_synth = FALSE
-
-/datum/reagent/blob/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume, show_message, touch_protection)
- return round(volume * min(1.5 - touch_protection, 1), 0.1) //full touch protection means 50% volume, any prot below 0.5 means 100% volume.
-
-/datum/reagent/blob/proc/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag) //when the blob takes damage, do this
- return damage
-
-/datum/reagent/blob/ripping_tendrils //does brute and a little stamina damage
- name = "Разрывающие щупальца"
- description = "Наносит высокий урон травмами, а также урон выносливости."
- id = "ripping_tendrils"
- color = "#7F0000"
- complementary_color = "#a15656"
- message_living = ", и вы чувствуете, как ваша кожа рвется и слезает."
-
-/datum/reagent/blob/ripping_tendrils/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- M.apply_damage(0.6*volume, BRUTE)
- M.adjustStaminaLoss(volume)
- if(iscarbon(M))
- M.emote("scream")
-
-/datum/reagent/blob/boiling_oil //sets you on fire, does burn damage
- name = "Кипящее масло"
- description = "Наносит высокий урон ожогами и поджигает жертву."
- id = "boiling_oil"
- color = "#B68D00"
- complementary_color = "#c0a856"
- message = "Блоб обдает вас горящим маслом"
- message_living = ", и вы чувствуете, как ваша кожа обугливается и плавится"
-
-/datum/reagent/blob/boiling_oil/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- M.adjust_fire_stacks(round(volume/10))
- volume = ..()
- M.apply_damage(0.6*volume, BURN)
- M.IgniteMob()
- M.emote("scream")
-
-/datum/reagent/blob/envenomed_filaments //toxin, hallucination, and some bonus spore toxin
- name = "Ядовитые нити"
- description = "Наносит высокий урон токсинами, вызывает галлюцинации и вводит споры в кровоток."
- id = "envenomed_filaments"
- color = "#9ACD32"
- complementary_color = "#b0cd73"
- message_living = ", и вы чувствуете себя плохо. Вас тошнит"
-
-/datum/reagent/blob/envenomed_filaments/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- M.apply_damage(0.6 * volume, TOX)
- M.AdjustHallucinate(1.2 SECONDS * volume)
- if(M.reagents)
- M.reagents.add_reagent("spore", 0.4*volume)
-
-/datum/reagent/blob/lexorin_jelly //does tons of oxygen damage and a little brute
- name = "Лексориновое желе"
- description = "Наносит средний урон травмами, но огромный урон гипоксией."
- id = "lexorin_jelly"
- color = "#00FFC5"
- complementary_color = "#56ebc9"
- message_living = ", и ваши легкие кажутся тяжелыми и слабыми"
-
-/datum/reagent/blob/lexorin_jelly/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- M.apply_damage(0.4*volume, BRUTE)
- M.apply_damage(1*volume, OXY)
- M.AdjustLoseBreath(round(0.6 SECONDS * volume))
-
-
-/datum/reagent/blob/kinetic //does semi-random brute damage
- name = "Кинетический желатин"
- description = "Наносит случайный урон травмами, в 0,33–2,33 раза превышающий стандартное количество."
- id = "kinetic"
- color = "#FFA500"
- complementary_color = "#ebb756"
- message = "Блоб избивает вас"
-
-/datum/reagent/blob/kinetic/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- var/damage = rand(5, 35)/25
- M.apply_damage(damage*volume, BRUTE)
-
-/datum/reagent/blob/cryogenic_liquid //does low burn damage and stamina damage and cools targets down
- name = "Криогенная жидкость"
- description = "Наносит средний урон травмами, урон выносливости и вводит в жертв ледяное масло, замораживая их до смерти."
- id = "cryogenic_liquid"
- color = "#8BA6E9"
- complementary_color = "#a8b7df"
- message = "Блоб обливает вас ледяной жидкостью"
- message_living = ", и вы чувствуете себя холодным и усталым"
-
-/datum/reagent/blob/cryogenic_liquid/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- M.apply_damage(0.4*volume, BURN)
- M.adjustStaminaLoss(volume)
- if(M.reagents)
- M.reagents.add_reagent("frostoil", 0.4*volume)
-
-/datum/reagent/blob/b_sorium
- name = "Сорий"
- description = "Наносит высокий урон травмами и отбрасывает людей в стороны."
- id = "b_sorium"
- color = "#808000"
- complementary_color = "#a2a256"
- message = "Блоб врезается в вас и отбрасывает в сторону."
-
-/datum/reagent/blob/b_sorium/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- reagent_vortex(M, 1, volume)
- volume = ..()
- M.apply_damage(0.6*volume, BRUTE)
-
-/datum/reagent/blob/proc/reagent_vortex(mob/living/M, setting_type, volume)
- var/turf/pull = get_turf(M)
- var/range_power = clamp(round(volume/5, 1), 1, 5)
- for(var/atom/movable/X in range(range_power,pull))
- if(iseffect(X))
- continue
- if(X.move_resist <= MOVE_FORCE_DEFAULT && !X.anchored)
- var/distance = get_dist(X, pull)
- var/moving_power = max(range_power - distance, 1)
- spawn(0)
- if(moving_power > 2) //if the vortex is powerful and we're close, we get thrown
- if(setting_type)
- var/atom/throw_target = get_edge_target_turf(X, get_dir(X, get_step_away(X, pull)))
- var/throw_range = 5 - distance
- X.throw_at(throw_target, throw_range, 1)
- else
- X.throw_at(pull, distance, 1)
- else
- if(setting_type)
- for(var/i = 0, i < moving_power, i++)
- sleep(2)
- if(!step_away(X, pull))
- break
- else
- for(var/i = 0, i < moving_power, i++)
- sleep(2)
- if(!step_towards(X, pull))
- break
-
-/datum/reagent/blob/radioactive_gel
- name = "Радиоактивный гель"
- description = "Наносит средний урон токсинами и небольшой урон травмами, но облучает тех, кого задевает."
- id = "radioactive_gel"
- color = "#2476f0"
- complementary_color = "#24f0f0"
- message_living = ", и вы чувствуете странное тепло изнутри"
-
-/datum/reagent/blob/radioactive_gel/reaction_mob(mob/living/M, method = REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- M.apply_damage(0.3 * volume, TOX)
- M.apply_damage(0.2 * volume, BRUTE) // lets not have IPC / plasmaman only take 7.5 damage from this
- if(M.reagents)
- M.reagents.add_reagent("uranium", 0.3 * volume)
-
-/datum/reagent/blob/teslium_paste
- name = "Теслиевая паста"
- description = "Наносит средний урон ожогами и вызывает удары током у тех, кого задевает, со временем."
- id = "teslium_paste"
- color = "#20324D"
- complementary_color = "#412968"
- message_living = ", и вы чувствуете удар статическим электричеством"
-
-/datum/reagent/blob/teslium_paste/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- M.apply_damage(0.4 * volume, BURN)
- if(M.reagents)
- if(M.reagents.has_reagent("teslium") && prob(0.6 * volume))
- M.electrocute_act((0.5 * volume), "разряда блоба", flags = SHOCK_NOGLOVES)
- M.reagents.del_reagent("teslium")
- return //don't add more teslium after you shock it out of someone.
- M.reagents.add_reagent("teslium", 0.125 * volume) // a little goes a long way
-
-/datum/reagent/blob/proc/send_message(mob/living/M)
- var/totalmessage = message
- if(message_living && !issilicon(M))
- totalmessage += message_living
- totalmessage += "!"
- to_chat(M, "[totalmessage]")
diff --git a/code/modules/reagents/chemistry/reagents/toxins.dm b/code/modules/reagents/chemistry/reagents/toxins.dm
index 9840b80713b..e42b541ba67 100644
--- a/code/modules/reagents/chemistry/reagents/toxins.dm
+++ b/code/modules/reagents/chemistry/reagents/toxins.dm
@@ -6,10 +6,11 @@
color = "#CF3600" // rgb: 207, 54, 0
taste_mult = 1.2
taste_description = "bitterness"
+ var/toxpwr = 2
/datum/reagent/toxin/on_mob_life(mob/living/M)
var/update_flags = STATUS_UPDATE_NONE
- update_flags |= M.adjustToxLoss(2, FALSE)
+ update_flags |= M.adjustToxLoss(toxpwr, FALSE)
return ..() | update_flags
/datum/reagent/spider_venom
@@ -511,19 +512,35 @@
return ..() | update_flags
-/datum/reagent/spore
+/datum/reagent/toxin/spore
name = "Spore Toxin"
- id = "spore"
description = "A natural toxin produced by blob spores that inhibits vision when ingested."
color = "#9ACD32"
+ id = "spore"
+ toxpwr = 1
+ can_synth = FALSE
taste_description = "bitterness"
-/datum/reagent/spore/on_mob_life(mob/living/M)
- var/update_flags = STATUS_UPDATE_NONE
- update_flags |= M.adjustToxLoss(1, FALSE)
- M.damageoverlaytemp = 60
- M.EyeBlurry(6 SECONDS)
- return ..() | update_flags
+/datum/reagent/toxin/spore/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ affected_mob.damageoverlaytemp = 60
+ affected_mob.update_damage_hud()
+ affected_mob.EyeBlurry(6 SECONDS * REM * seconds_per_tick)
+
+/datum/reagent/toxin/spore_burning
+ name = "Burning Spore Toxin"
+ description = "A natural toxin produced by blob spores that induces combustion in its victim."
+ color = "#9ACD32"
+ id = "spore_burn"
+ toxpwr = 0.5
+ taste_description = "burning"
+ can_synth = FALSE
+
+/datum/reagent/toxin/spore_burning/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ affected_mob.adjust_fire_stacks(2 * REM * seconds_per_tick)
+ affected_mob.IgniteMob()
+
/datum/reagent/beer2 //disguised as normal beer for use by emagged service borgs
name = "Beer"
diff --git a/code/modules/reagents/reagent_containers/bottle.dm b/code/modules/reagents/reagent_containers/bottle.dm
index 1daf2b9acac..2b5025254e2 100644
--- a/code/modules/reagents/reagent_containers/bottle.dm
+++ b/code/modules/reagents/reagent_containers/bottle.dm
@@ -6,7 +6,7 @@
desc = "A small bottle."
icon = 'icons/obj/chemical.dmi'
icon_state = "round_bottle"
- item_state = "atoxinbottle"
+ item_state = "round_bottle"
amount_per_transfer_from_this = 10
possible_transfer_amounts = list(5,10,15,25,30)
container_type = OPENCONTAINER
diff --git a/code/modules/reagents/reagent_dispenser.dm b/code/modules/reagents/reagent_dispenser.dm
index 965ac4fc1e3..0f959176c05 100644
--- a/code/modules/reagents/reagent_dispenser.dm
+++ b/code/modules/reagents/reagent_dispenser.dm
@@ -106,6 +106,7 @@
investigate_log("[key_name_log(P.firer)] triggered a fueltank explosion with [P.name]", INVESTIGATE_BOMB)
..()
+
/obj/structure/reagent_dispensers/fueltank/boom(rigtrigger = FALSE, log_attack = FALSE) // Prevent case where someone who rigged the tank is blamed for the explosion when the rig isn't what triggered the explosion
if(rigtrigger) // If the explosion is triggered by an assembly holder
add_attack_logs(lastrigger, src, "rigged fuel tank exploded", ATKLOG_FEW)
@@ -151,6 +152,11 @@
/obj/structure/reagent_dispensers/fueltank/attackby(obj/item/I, mob/user, params)
+ if(istype(I, /obj/item/weldingtool/sword))
+ if(I.tool_enabled)
+ boom(FALSE, TRUE)
+ return ATTACK_CHAIN_BLOCKED_ALL
+
if(istype(I, /obj/item/assembly_holder))
add_fingerprint(user)
var/obj/item/assembly_holder/assembly = I
diff --git a/code/modules/research/designs/mechfabricator_designs.dm b/code/modules/research/designs/mechfabricator_designs.dm
index e361291f828..33b08853dbc 100644
--- a/code/modules/research/designs/mechfabricator_designs.dm
+++ b/code/modules/research/designs/mechfabricator_designs.dm
@@ -1004,6 +1004,17 @@
construction_time = 20 SECONDS
category = list("Exosuit Equipment")
+/datum/design/medbeamgun
+ name = "Exosuit Medical Equipment (Mecha Medbeam)"
+ id = "mech_medical_beamgun"
+ build_type = MECHFAB
+ build_path = /obj/item/mecha_parts/mecha_equipment/medical/beamgun
+ req_tech = list("biotech" = 7, "bluespace" = 7, "powerstorage" = 7)
+ materials = list(MAT_METAL=5000,MAT_DIAMOND=600,MAT_GLASS=600,MAT_GOLD=600,MAT_URANIUM=300,MAT_BLUESPACE=650)
+ construction_time = 20 SECONDS
+ category = list("Exosuit Equipment")
+
+
/datum/design/improved_exosuit_control_system
name = "Exosuit Common Equipment (Control System Upgrade)"
id = "mech_improved_exosuit_control_system"
diff --git a/code/modules/research/designs/weapon_designs.dm b/code/modules/research/designs/weapon_designs.dm
index 6b473664872..8bb3ba0bc77 100644
--- a/code/modules/research/designs/weapon_designs.dm
+++ b/code/modules/research/designs/weapon_designs.dm
@@ -576,6 +576,20 @@
build_path = /obj/item/clothing/gloves/color/black/pyro_claws
category = list("Weapons")
+/* uncomment when every tech is 90 lvl, too op for now
+/datum/design/laserminigun
+ name = "Laser gatling gun"
+ desc = "Огромное лазерное орудие, обладающее выдающейся скорострельностью и поражающей силой. Говорят, что 12 секунд стрельбы из этой малышки обойдутся вам в 400 тысяч кредитов."
+ id = "laser_gatling"
+ build_type = PROTOLATHE
+ req_tech = list("combat" = 8, "materials" = 7, "magnets" = 7, "powerstorage" = 7)
+ materials = list(MAT_METAL = 12000, MAT_GLASS = 2400, MAT_URANIUM = 1200, MAT_TITANIUM = 1200, MAT_DIAMOND = 1200)
+ locked = TRUE
+ build_path = /obj/item/gun/energy/gun/minigun
+ category = list("Weapons")
+ lathe_time_factor = 0.5
+*/
+
/datum/design/real_plasma_pistol
name = "Plasma Pistol"
desc = "HA specialized firearm designed to fire heated bolts of plasma. Can be overloaded for a high damage shield breaking shot."
diff --git a/code/modules/research/rdconsole.dm b/code/modules/research/rdconsole.dm
index a14e5fac182..ec3f40e3379 100644
--- a/code/modules/research/rdconsole.dm
+++ b/code/modules/research/rdconsole.dm
@@ -496,19 +496,19 @@ won't update every console in existence) but it's more of a hassle to do. Also,
var/obj/item/new_item_item = new_item
new_item_item.update_materials_coeff(coeff)
- if(locked)
- var/obj/item/storage/lockbox/research/L = new/obj/item/storage/lockbox/research(machine.loc)
- new_item.forceMove(L)
- L.name += " ([new_item.name])"
- L.origin_tech = new_item.origin_tech
- L.req_access = being_built.access_requirement
+ if(locked && isitem(new_item))
+ var/obj/item/real_item = new_item
+ var/obj/item/storage/lockbox/research/lockbox = new /obj/item/storage/lockbox/research(machine.loc)
+ real_item.forceMove(lockbox)
+ lockbox.name += " ([real_item.name])"
+ lockbox.origin_tech = real_item.origin_tech
+ lockbox.req_access = being_built.access_requirement
+ lockbox.w_class = real_item.w_class > lockbox.w_class ? real_item.w_class : lockbox.w_class
var/list/lockbox_access
- for(var/A in L.req_access)
+ for(var/A in lockbox.req_access)
lockbox_access += "[get_access_desc(A)] "
-
- L.desc = "A locked box. It is locked to [lockbox_access]access."
-
+ lockbox.desc = "A locked box. It is locked to [lockbox_access]access."
else
new_item.loc = machine.loc
diff --git a/code/modules/research/research.dm b/code/modules/research/research.dm
index a6e7f227e8b..9cc2a08c205 100644
--- a/code/modules/research/research.dm
+++ b/code/modules/research/research.dm
@@ -237,78 +237,80 @@ research holder datum.
name = "Materials Research"
desc = "Development of new and improved materials."
id = "materials"
- max_level = 7
+ max_level = 8
/datum/tech/engineering
name = "Engineering Research"
desc = "Development of new and improved engineering parts and methods."
id = "engineering"
- max_level = 7
+ max_level = 8
/datum/tech/plasmatech
name = "Plasma Research"
desc = "Research into the mysterious substance colloqually known as 'plasma'."
id = "plasmatech"
- max_level = 7
+ max_level = 8
rare = 3
/datum/tech/powerstorage
name = "Power Manipulation Technology"
desc = "The various technologies behind the storage and generation of electicity."
id = "powerstorage"
- max_level = 7
+ max_level = 8
/datum/tech/bluespace
name = "'Blue-space' Research"
desc = "Research into the sub-reality known as 'blue-space'."
id = "bluespace"
- max_level = 7
+ max_level = 8
rare = 2
/datum/tech/biotech
name = "Biological Technology"
desc = "Research into the deeper mysteries of life and organic substances."
id = "biotech"
- max_level = 7
+ max_level = 8
/datum/tech/combat
name = "Combat Systems Research"
desc = "The development of offensive and defensive systems."
id = "combat"
- max_level = 7
+ max_level = 8
/datum/tech/magnets
name = "Electromagnetic Spectrum Research"
desc = "Research into the electromagnetic spectrum. No clue how they actually work, though."
id = "magnets"
- max_level = 7
+ max_level = 8
/datum/tech/programming
name = "Data Theory Research"
desc = "The development of new computer and artificial intelligence and data storage systems."
id = "programming"
- max_level = 7
+ max_level = 8
/datum/tech/toxins //not meant to be raised by deconstruction, do not give objects toxins as an origin_tech
name = "Toxins Research"
desc = "Research into plasma based explosive devices. Upgrade through testing explosives in the toxins lab."
id = "toxins"
- max_level = 7
+ max_level = 8
rare = 2
/datum/tech/syndicate
name = "Illegal Technologies Research"
desc = "The study of technologies that violate standard Nanotrasen regulations."
id = "syndicate"
- max_level = 0 // Don't count towards maxed research, since it's illegal.
+ level = 0 // Illegal tech level dont need to show in roundstart on console
+ max_level = 8 // Used for admin button so need max level like other tech
rare = 4
/datum/tech/abductor
name = "Alien Technologies Research"
desc = "The study of technologies used by the advanced alien race known as Abductors."
id = "abductor"
+ level = 0 // Alien tech level hide roundstart like illegal
+ max_level = 8
rare = 5
- level = 0
/*
datum/tech/arcane
diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm
index df0112bc7ed..b7b8bedf1cd 100644
--- a/code/modules/research/xenobiology/xenobiology.dm
+++ b/code/modules/research/xenobiology/xenobiology.dm
@@ -100,6 +100,10 @@
name = "oil slime extract"
icon_state = "oil slime extract"
+/obj/item/slime_extract/oil/blob_vore_act(obj/structure/blob/special/core/voring_core)
+ obj_destruction(MELEE)
+
+
/obj/item/slime_extract/adamantine
name = "adamantine slime extract"
icon_state = "adamantine slime extract"
diff --git a/code/modules/ruins/ruin_areas.dm b/code/modules/ruins/ruin_areas.dm
index 2c7933ca8a8..7a6ce119091 100644
--- a/code/modules/ruins/ruin_areas.dm
+++ b/code/modules/ruins/ruin_areas.dm
@@ -9,6 +9,9 @@
ambientsounds = RUINS_SOUNDS
sound_environment = SOUND_ENVIRONMENT_STONEROOM
+/area/ruin/space
+ area_flags = NONE
+
/area/ruin/unpowered
always_unpowered = FALSE
@@ -31,6 +34,7 @@
/area/ruin/powered/space_bar
name = "Space Bar"
+ area_flags = NONE
/area/ruin/powered/shuttle
name = "Shuttle"
@@ -56,3 +60,4 @@
/area/ruin/spaceprison
name = "Space Prison"
icon_state = "spaceprison"
+ area_flags = NONE
diff --git a/code/modules/spacepods/spacepod.dm b/code/modules/spacepods/spacepod.dm
index 4973d3ce5a3..bac31213c75 100644
--- a/code/modules/spacepods/spacepod.dm
+++ b/code/modules/spacepods/spacepod.dm
@@ -103,7 +103,9 @@
if("Windows")
part_type = WINDOW
else
- var/coloradd = input(user, "Choose a color", "Color") as color
+ var/coloradd = tgui_input_color(user, "Choose a color", "Color")
+ if(isnull(coloradd))
+ return
colors[part_type] = coloradd
if(!has_paint)
has_paint = 1
@@ -291,7 +293,7 @@
update_icons()
-/obj/spacepod/proc/repair_damage(var/repair_amount)
+/obj/spacepod/repair_damage(repair_amount)
if(health)
health = min(initial(health), health + repair_amount)
update_icons()
diff --git a/code/modules/surgery/generic.dm b/code/modules/surgery/generic.dm
index d639b945111..36a1be677b4 100644
--- a/code/modules/surgery/generic.dm
+++ b/code/modules/surgery/generic.dm
@@ -17,7 +17,7 @@
/obj/item/shard = 60,
/obj/item/scissors = 12,
/obj/item/twohanded/chainsaw = 1,
- /obj/item/claymore = 6,
+ /obj/item/melee/claymore = 6,
/obj/item/melee/energy = 6,
/obj/item/pen/edagger = 6,
)
diff --git a/code/modules/surgery/organs/augments_arms.dm b/code/modules/surgery/organs/augments_arms.dm
index ae6d740baef..58e42b02049 100644
--- a/code/modules/surgery/organs/augments_arms.dm
+++ b/code/modules/surgery/organs/augments_arms.dm
@@ -208,12 +208,6 @@
/obj/item/organ/internal/cyberimp/arm/gun/laser/l
parent_organ_zone = BODY_ZONE_L_ARM
-/obj/item/organ/internal/cyberimp/arm/gun/laser/Initialize(mapload)
- . = ..()
- var/obj/item/organ/internal/cyberimp/arm/gun/laser/laserphasergun = locate(/obj/item/gun/energy/laser/mounted) in contents
- laserphasergun.icon = icon //No invisible laser guns kthx
- laserphasergun.icon_state = icon_state
-
/obj/item/organ/internal/cyberimp/arm/gun/taser
name = "arm-mounted taser implant"
desc = "A variant of the arm cannon implant that fires electrodes and disabler shots. The cannon emerges from the subject's arm and remains inside when not in use."
diff --git a/code/modules/surgery/organs/blood.dm b/code/modules/surgery/organs/blood.dm
index ca107009fec..e4b3e75dc3a 100644
--- a/code/modules/surgery/organs/blood.dm
+++ b/code/modules/surgery/organs/blood.dm
@@ -198,7 +198,7 @@
if(!blood_id)
return 0
- AdjustBlood(amount)
+ AdjustBlood(-amount)
var/list/blood_data = get_blood_data(blood_id)
diff --git a/code/modules/surgery/organs/lungs.dm b/code/modules/surgery/organs/lungs.dm
index f348dd3cd8a..97e26f68deb 100644
--- a/code/modules/surgery/organs/lungs.dm
+++ b/code/modules/surgery/organs/lungs.dm
@@ -334,6 +334,30 @@
cold_level_3_damage = -COLD_GAS_DAMAGE_LEVEL_3
cold_damage_types = list(BRUTE = 0.5, BURN = 0.25)
+ var/cooling_start_temp = DRASK_LUNGS_COOLING_START_TEMP
+ var/cooling_stop_temp = DRASK_LUNGS_COOLING_STOP_TEMP
+
+/obj/item/organ/internal/lungs/drask/insert(mob/living/carbon/target, special = ORGAN_MANIPULATION_DEFAULT)
+ . = ..()
+
+ if(!.)
+ return FALSE
+
+ RegisterSignal(owner, COMSIG_HUMAN_EARLY_HANDLE_ENVIRONMENT, PROC_REF(regulate_temperature))
+
+/obj/item/organ/internal/lungs/drask/proc/regulate_temperature(mob/living/source, datum/gas_mixture/environment)
+ SIGNAL_HANDLER
+
+ if(source.stat == DEAD)
+ return
+
+ if(owner.bodytemperature > cooling_start_temp && environment.temperature <= cooling_stop_temp)
+ owner.adjust_bodytemperature(-5)
+
+/obj/item/organ/internal/lungs/drask/remove(mob/living/user, special = ORGAN_MANIPULATION_DEFAULT)
+ UnregisterSignal(owner, COMSIG_HUMAN_EARLY_HANDLE_ENVIRONMENT)
+ return ..()
+
/obj/item/organ/internal/lungs/cybernetic
name = "cybernetic lungs"
desc = "A cybernetic version of the lungs found in traditional humanoid entities. It functions the same as an organic lung and is merely meant as a replacement."
diff --git a/code/modules/surgery/organs/organ.dm b/code/modules/surgery/organs/organ.dm
index 462f89ebc09..39475908614 100644
--- a/code/modules/surgery/organs/organ.dm
+++ b/code/modules/surgery/organs/organ.dm
@@ -66,11 +66,15 @@
/obj/item/organ/Destroy()
STOP_PROCESSING(SSobj, src)
+
if(owner)
remove(owner, ORGAN_MANIPULATION_NOEFFECT)
+
QDEL_LIST_ASSOC_VAL(autopsy_data)
+
if(dna)
QDEL_NULL(dna)
+
return ..()
@@ -87,6 +91,7 @@
if(is_robotic() && !species_type) // no DNA for cybernetics, except IPC parts
if(update_blood)
update_blood()
+
return
if(!dna)
@@ -118,6 +123,7 @@
/obj/item/organ/proc/update_blood()
if(!dna || (TRAIT_NO_BLOOD in dna.species.inherent_traits))
return
+
LAZYSET(blood_DNA, dna.unique_enzymes, dna.blood_type)
@@ -128,13 +134,17 @@
/obj/item/organ/proc/necrotize(silent = FALSE)
if(status & (ORGAN_ROBOT|ORGAN_DEAD))
return FALSE
+
damage = max_damage
status |= ORGAN_DEAD
STOP_PROCESSING(SSobj, src)
+
if(dead_icon && !is_robotic())
icon_state = dead_icon
+
if(owner && vital)
owner.death()
+
return TRUE
@@ -145,6 +155,7 @@
/obj/item/organ/proc/unnecrotize()
if(!is_dead())
return FALSE
+
status &= ~ORGAN_DEAD
return TRUE
@@ -153,12 +164,15 @@
if(istype(I, /obj/item/stack/nanopaste))
add_fingerprint(user)
var/obj/item/stack/nanopaste/nanopaste = I
+
if(!is_robotic())
to_chat(user, span_warning("The [nanopaste.name] can only be used on robotic bodyparts."))
return ATTACK_CHAIN_PROCEED
+
if(!nanopaste.use(1))
to_chat(user, span_warning("You need at least one unit of [nanopaste] to proceed."))
return ATTACK_CHAIN_PROCEED
+
to_chat(user, span_notice("You have repaired the damage on [src]."))
rejuvenate()
return ATTACK_CHAIN_PROCEED_SUCCESS
@@ -184,10 +198,13 @@
// Maybe scale it down a bit, have it REALLY kick in once past the basic infection threshold
// Another mercy for surgeons preparing transplant organs
germ_level++
+
if(germ_level >= INFECTION_LEVEL_ONE)
germ_level += rand(2,6)
+
if(germ_level >= INFECTION_LEVEL_TWO)
germ_level += rand(2,6)
+
if(germ_level >= INFECTION_LEVEL_THREE)
necrotize()
@@ -211,12 +228,15 @@
for(var/typepath in preserved_holders)
if(is_found_within(typepath))
return TRUE
+
if(istype(loc,/obj/item/mmi)) // So a brain can slowly recover from being left out of an MMI
germ_level = max(0, germ_level - 1)
return TRUE
+
if(istype(loc, /mob/living/simple_animal/hostile/headslug) || istype(loc, /obj/item/organ/internal/body_egg/changeling_egg))
germ_level = 0 // weird stuff might happen, best to be safe
return TRUE
+
if(isturf(loc))
var/is_in_freezer = FALSE
if(world.time - last_freezer_update_time > freezer_update_period)
@@ -342,6 +362,7 @@
/obj/item/organ/proc/heal_internal_damage(amount, robo_repair = FALSE)
if(is_robotic() && !robo_repair)
return
+
damage = max(damage - amount, 0)
@@ -372,12 +393,13 @@
if(owner?.stat != DEAD && vital && !special)
add_attack_logs(user, owner, "Removed vital organ ([src])")
owner.death()
+
owner = null
return src
/obj/item/organ/proc/replaced(mob/living/carbon/human/target, special = ORGAN_MANIPULATION_DEFAULT)
- return // Nothing uses this, it is always overridden
+ return
// A version of `replaced` that "flattens" the process of insertion, making organs "Plug'n'play"
@@ -396,6 +418,7 @@
/obj/item/organ/proc/has_damage()
if(damage)
return TRUE
+
return FALSE
/obj/item/organ/proc/is_robotic()
@@ -404,6 +427,7 @@
/obj/item/organ/serialize()
var/data = ..()
+
if(status != 0)
data["status"] = status
@@ -411,6 +435,7 @@
// the owner
if(!(owner && dna.unique_enzymes == owner.dna.unique_enzymes))
data["dna"] = dna.serialize()
+
return data
diff --git a/code/modules/surgery/organs/organ_external.dm b/code/modules/surgery/organs/organ_external.dm
index acb5ccfd3d3..08749b1b317 100644
--- a/code/modules/surgery/organs/organ_external.dm
+++ b/code/modules/surgery/organs/organ_external.dm
@@ -182,8 +182,10 @@
return
var/obj/item/organ/external/replaced = owner.bodyparts_by_name[limb_zone]
+
if(!isnull(replaced))
replaced.remove(target, ORGAN_MANIPULATION_NOEFFECT)
+
owner.bodyparts_by_name[limb_zone] = src
owner.bodyparts |= src
@@ -439,6 +441,9 @@
return update_state()
+/obj/item/organ/external/blob_act()
+ external_receive_damage(max_damage, forced = TRUE)
+
/obj/item/organ/external/emp_act(severity)
if(!is_robotic() || emp_proof)
return
diff --git a/code/modules/surgery/organs/organ_internal.dm b/code/modules/surgery/organs/organ_internal.dm
index e929d3fe9e6..08ca28a9282 100644
--- a/code/modules/surgery/organs/organ_internal.dm
+++ b/code/modules/surgery/organs/organ_internal.dm
@@ -18,6 +18,7 @@
if(iscarbon(loc))
insert(loc)
+
if(species_type == /datum/species/diona)
AddComponent(/datum/component/diona_internals)
@@ -31,6 +32,7 @@
do_pickup_animation(src, target)
var/obj/item/organ/internal/replaced = target.get_organ_slot(slot)
+
if(replaced)
replaced.remove(target, ORGAN_MANIPULATION_NOEFFECT)
@@ -45,6 +47,7 @@
stack_trace("[src] attempted to insert into a [parent_organ_zone], but [parent_organ_zone] wasn't an organ! [atom_loc_line(h_target)]")
else
LAZYOR(parent.internal_organs, src)
+
h_target.update_int_organs()
loc = null
@@ -76,10 +79,13 @@
if(iscarbon(organ_owner))
organ_owner.internal_organs -= src
+
if(organ_owner.internal_organs_slot[slot] == src)
organ_owner.internal_organs_slot[slot] = null
+
if(!special)
send_signal = TRUE
+
if(vital && !special && organ_owner.stat != DEAD)
organ_owner.death()
@@ -107,6 +113,7 @@
/obj/item/organ/internal/emp_act(severity)
if(!is_robotic() || emp_proof)
return
+
switch(severity)
if(1)
internal_receive_damage(20, silent = TRUE)
@@ -138,6 +145,7 @@
/obj/item/organ/internal/proc/prepare_eat()
if(is_robotic())
return //no eating cybernetic implants!
+
var/obj/item/reagent_containers/food/snacks/organ/S = new
S.name = name
S.desc = desc
@@ -151,6 +159,7 @@
/obj/item/organ/internal/attempt_become_organ(obj/item/organ/external/parent, mob/living/carbon/human/target, special = ORGAN_MANIPULATION_DEFAULT)
if(parent_organ_zone != parent.limb_zone)
return FALSE
+
insert(target, special)
return TRUE
@@ -172,6 +181,7 @@
return ..()
var/obj/item/reagent_containers/food/snacks/snack = prepare_eat()
+
if(!snack)
return ATTACK_CHAIN_PROCEED
@@ -198,9 +208,11 @@
H.icon_base = "[slot]-c"
H.dead_icon = "[slot]-c-off"
H.update_icon()
+
else if("[slot]-c" in states) //Give the robotic organ its robotic organ icons if they exist.
icon = icon('icons/obj/surgery.dmi')
icon_state = "[slot]-c"
+
name = "cybernetic [slot]"
..() //Go apply all the organ flags/robotic statuses.
@@ -217,12 +229,14 @@
for(var/datum/disease/appendicitis/A in M.diseases)
A.cure()
inflamed = TRUE
+
update_icon()
. = ..()
/obj/item/organ/internal/appendix/insert(mob/living/carbon/M, special = ORGAN_MANIPULATION_DEFAULT)
..()
+
if(inflamed)
var/datum/disease/appendicitis/D = new
D.Contract(M)
@@ -230,8 +244,10 @@
/obj/item/organ/internal/appendix/prepare_eat()
var/obj/S = ..()
+
if(inflamed)
S.reagents.add_reagent("????", 5)
+
return S
@@ -263,8 +279,10 @@
var/light_count = T.get_lumcount()*10
if(light_count > 4 && obj_integrity > 0) //Die in the light
obj_integrity--
+
else if(light_count < 2 && obj_integrity < max_integrity) //Heal in the dark
obj_integrity++
+
if(obj_integrity <= 0)
visible_message(span_warning("[src] collapses in on itself!"))
qdel(src)
@@ -287,6 +305,7 @@
/obj/item/organ/internal/honktumor/insert(mob/living/carbon/M, special = ORGAN_MANIPULATION_DEFAULT)
..()
+
M.force_gene_block(GLOB.clumsyblock, TRUE)
M.force_gene_block(GLOB.comicblock, TRUE)
organhonked = world.time
@@ -334,6 +353,7 @@
/obj/item/organ/internal/honktumor/cursed/on_life() //No matter what you do, no matter who you are, no matter where you go, you're always going to be a fat, stuttering dimwit.
..()
+
owner.setBrainLoss(80)
owner.set_nutrition(9000)
owner.overeatduration = 9000
@@ -379,13 +399,16 @@
if(ishuman(owner))
var/mob/living/carbon/human/H = owner
var/obj/item/organ/external/head/head_organ = H.get_organ(BODY_ZONE_HEAD)
+
if(!(head_organ.h_style == "Very Long Hair" || head_organ.h_style == "Mohawk"))
if(prob(10))
head_organ.h_style = "Mohawk"
else
head_organ.h_style = "Very Long Hair"
+
head_organ.hair_colour = "#D8C078"
H.update_hair()
+
if(!(head_organ.f_style == "Very Long Beard"))
head_organ.f_style = "Very Long Beard"
head_organ.facial_colour = "#D8C078"
@@ -396,7 +419,9 @@
..()
if(!ishuman(owner))
return
+
var/germs_mod = owner.dna.species.germs_growth_mod * owner.physiology.germs_growth_mod
+
if(germ_level >= INFECTION_LEVEL_TWO && prob(3 * germs_mod))
// big message from every 1 damage is not good. If germs growth rate is big, it will spam the chat.
internal_receive_damage(1, silent = prob(30 * germs_mod))
@@ -404,16 +429,19 @@
/mob/living/carbon/human/proc/check_infections()
var/list/infections = list()
+
for(var/obj/item/organ/internal/organ as anything in internal_organs)
if(organ.germ_level > 0)
infections.Add(organ)
+
return infections
/mob/living/carbon/human/proc/check_damaged_organs()
var/list/damaged = list()
+
for(var/obj/item/organ/internal/organ as anything in internal_organs)
if(organ.damage > 0)
damaged.Add(organ)
- return damaged
+ return damaged
diff --git a/code/modules/surgery/organs_internal.dm b/code/modules/surgery/organs_internal.dm
index 77a40efd088..27f8b753b7a 100644
--- a/code/modules/surgery/organs_internal.dm
+++ b/code/modules/surgery/organs_internal.dm
@@ -853,7 +853,7 @@
/obj/item/shard = 60,
/obj/item/scissors = 12,
/obj/item/twohanded/chainsaw = 1,
- /obj/item/claymore = 6,
+ /obj/item/melee/claymore = 6,
/obj/item/melee/energy = 6,
/obj/item/pen/edagger = 6
)
diff --git a/code/modules/tgui/modules/appearance_changer.dm b/code/modules/tgui/modules/appearance_changer.dm
index 7983d32b064..ab1719a0a54 100644
--- a/code/modules/tgui/modules/appearance_changer.dm
+++ b/code/modules/tgui/modules/appearance_changer.dm
@@ -72,8 +72,8 @@
if("skin_color")
if(can_change_skin_color())
- var/new_skin = input(usr, "Choose your character's skin colour: ", "Skin Color", owner.skin_colour) as color|null
- if(new_skin && (!..()) && owner.change_skin_color(new_skin))
+ var/new_skin = tgui_input_color(usr, "Choose your character's skin colour: ", "Skin Color", owner.skin_colour)
+ if(!isnull(new_skin) && (!..()) && owner.change_skin_color(new_skin))
update_dna()
if("hair")
@@ -83,14 +83,14 @@
if("hair_color")
if(can_change(APPEARANCE_HAIR_COLOR))
- var/new_hair = input("Please select hair color.", "Hair Color", head_organ.hair_colour) as color|null
- if(new_hair && (!..()) && owner.change_hair_color(new_hair))
+ var/new_hair = tgui_input_color("Please select hair color.", "Hair Color", head_organ.hair_colour)
+ if(!isnull(new_hair) && (!..()) && owner.change_hair_color(new_hair))
update_dna()
if("secondary_hair_color")
if(can_change(APPEARANCE_SECONDARY_HAIR_COLOR))
- var/new_hair = input("Please select secondary hair color.", "Secondary Hair Color", head_organ.sec_hair_colour) as color|null
- if(new_hair && (!..()) && owner.change_hair_color(new_hair, 1))
+ var/new_hair = tgui_input_color("Please select secondary hair color.", "Secondary Hair Color", head_organ.sec_hair_colour)
+ if(!isnull(new_hair) && (!..()) && owner.change_hair_color(new_hair, 1))
update_dna()
if("hair_gradient")
@@ -124,21 +124,21 @@
if("facial_hair_color")
if(can_change(APPEARANCE_FACIAL_HAIR_COLOR))
- var/new_facial = input("Please select facial hair color.", "Facial Hair Color", head_organ.facial_colour) as color|null
- if(new_facial && (!..()) && owner.change_facial_hair_color(new_facial))
+ var/new_facial = tgui_input_color("Please select facial hair color.", "Facial Hair Color", head_organ.facial_colour)
+ if(!isnull(new_facial) && (!..()) && owner.change_facial_hair_color(new_facial))
update_dna()
if("secondary_facial_hair_color")
if(can_change(APPEARANCE_SECONDARY_FACIAL_HAIR_COLOR))
- var/new_facial = input("Please select secondary facial hair color.", "Secondary Facial Hair Color", head_organ.sec_facial_colour) as color|null
- if(new_facial && (!..()) && owner.change_facial_hair_color(new_facial, 1))
+ var/new_facial = tgui_input_color("Please select secondary facial hair color.", "Secondary Facial Hair Color", head_organ.sec_facial_colour)
+ if(!isnull(new_facial) && (!..()) && owner.change_facial_hair_color(new_facial, 1))
update_dna()
if("eye_color")
if(can_change(APPEARANCE_EYE_COLOR))
var/obj/item/organ/internal/eyes/eyes_organ = owner.get_int_organ(/obj/item/organ/internal/eyes)
- var/new_eyes = input("Please select eye color.", "Eye Color", eyes_organ.eye_colour) as color|null
- if(new_eyes && (!..()) && owner.change_eye_color(new_eyes))
+ var/new_eyes = tgui_input_color("Please select eye color.", "Eye Color", eyes_organ.eye_colour)
+ if(!isnull(new_eyes) && (!..()) && owner.change_eye_color(new_eyes))
update_dna()
if("head_accessory")
@@ -148,8 +148,8 @@
if("head_accessory_color")
if(can_change_head_accessory())
- var/new_head_accessory = input("Please select head accessory color.", "Head Accessory Color", head_organ.headacc_colour) as color|null
- if(new_head_accessory && (!..()) && owner.change_head_accessory_color(new_head_accessory))
+ var/new_head_accessory = tgui_input_color("Please select head accessory color.", "Head Accessory Color", head_organ.headacc_colour)
+ if(!isnull(new_head_accessory) && (!..()) && owner.change_head_accessory_color(new_head_accessory))
update_dna()
if("head_marking")
@@ -159,8 +159,8 @@
if("head_marking_color")
if(can_change_markings("head"))
- var/new_markings = input("Please select head marking color.", "Marking Color", owner.m_colours["head"]) as color|null
- if(new_markings && (!..()) && owner.change_marking_color(new_markings, "head"))
+ var/new_markings = tgui_input_color("Please select head marking color.", "Marking Color", owner.m_colours["head"])
+ if(!isnull(new_markings) && (!..()) && owner.change_marking_color(new_markings, "head"))
update_dna()
if("body_marking")
@@ -170,8 +170,8 @@
if("body_marking_color")
if(can_change_markings("body"))
- var/new_markings = input("Please select body marking color.", "Marking Color", owner.m_colours["body"]) as color|null
- if(new_markings && (!..()) && owner.change_marking_color(new_markings, "body"))
+ var/new_markings = tgui_input_color("Please select body marking color.", "Marking Color", owner.m_colours["body"])
+ if(!isnull(new_markings) && (!..()) && owner.change_marking_color(new_markings, "body"))
update_dna()
if("tail_marking")
@@ -181,8 +181,8 @@
if("tail_marking_color")
if(can_change_markings("tail"))
- var/new_markings = input("Please select tail marking color.", "Marking Color", owner.m_colours["tail"]) as color|null
- if(new_markings && (!..()) && owner.change_marking_color(new_markings, "tail"))
+ var/new_markings = tgui_input_color("Please select tail marking color.", "Marking Color", owner.m_colours["tail"])
+ if(!isnull(new_markings) && (!..()) && owner.change_marking_color(new_markings, "tail"))
update_dna()
if("body_accessory")
diff --git a/code/modules/tgui/tgui_datum.dm b/code/modules/tgui/tgui_datum.dm
index 90372951958..960930d03b3 100644
--- a/code/modules/tgui/tgui_datum.dm
+++ b/code/modules/tgui/tgui_datum.dm
@@ -103,6 +103,8 @@
/datum/tgui/proc/send_assets()
var/flushqueue = window.send_asset(get_asset_datum(
/datum/asset/simple/namespaced/fontawesome))
+ flushqueue |= window.send_asset(get_asset_datum(
+ /datum/asset/json/icon_ref_map))
for(var/datum/asset/asset in src_object.ui_assets(user))
flushqueue |= window.send_asset(asset)
if(flushqueue)
diff --git a/code/modules/tgui/tgui_input/color_input.dm b/code/modules/tgui/tgui_input/color_input.dm
new file mode 100644
index 00000000000..7ba4b9ed3e0
--- /dev/null
+++ b/code/modules/tgui/tgui_input/color_input.dm
@@ -0,0 +1,132 @@
+/**
+ * Creates a TGUI color picker window and returns the user's response.
+ *
+ * This proc should be used to create a color picker that the caller will wait for a response from.
+ * Arguments:
+ * * user - The user to show the picker to.
+ * * title - The of the picker modal, shown on the top of the TGUI window.
+ * * timeout - The timeout of the picker, after which the modal will close and qdel itself. Set to zero for no timeout.
+ * * autofocus - The bool that controls if this picker should grab window focus.
+ */
+/proc/tgui_input_color(mob/user, message, title, default = "#000000", timeout = 0, autofocus = TRUE, ui_state = GLOB.always_state)
+ if(!user)
+ user = usr
+ if(!istype(user))
+ if(!isclient(user))
+ CRASH("We passed something that wasn't a user/client in a TGUI Input Color! The passed thing was [user]!")
+ var/client/client = user
+ user = client.mob
+
+ if(isnull(user.client))
+ return
+
+ // Client does NOT have tgui_input on: Returns regular input
+ if(user.client?.prefs?.toggles2 & PREFTOGGLE_2_DISABLE_TGUI_INPUT)
+ return input(user, message, title, default) as color|null
+
+ var/datum/tgui_input_color/picker = new(user, message, title, default, timeout, autofocus, ui_state)
+ picker.ui_interact(user)
+ picker.wait()
+ if(picker)
+ . = picker.choice
+ qdel(picker)
+
+/**
+ * tgui_input_color
+ *
+ * Datum used for instantiating and using a TGUI-controlled color picker.
+ */
+/datum/tgui_input_color
+ /// The title of the TGUI window
+ var/title
+ /// The message to show the user
+ var/message
+ /// The default choice, used if there is an existing value
+ var/default
+ /// The color the user selected, null if no selection has been made
+ var/choice
+ /// The time at which the tgui_input_color was created, for displaying timeout progress.
+ var/start_time
+ /// The lifespan of the tgui_input_color, after which the window will close and delete itself.
+ var/timeout
+ /// The bool that controls if this modal should grab window focus
+ var/autofocus
+ /// Boolean field describing if the tgui_input_color was closed by the user.
+ var/closed
+ /// The attached timer that handles this objects timeout deletion
+ var/deletion_timer
+ /// The TGUI UI state that will be returned in ui_state(). Default: always_state
+ var/datum/ui_state/state
+
+/datum/tgui_input_color/New(mob/user, message, title, default, timeout, autofocus, ui_state)
+ src.autofocus = autofocus
+ src.title = title
+ src.default = default
+ src.message = message
+ src.state = ui_state
+
+ if(timeout)
+ src.timeout = timeout
+ start_time = world.time
+ deletion_timer = QDEL_IN(src, timeout)
+
+/datum/tgui_input_color/Destroy(force, ...)
+ SStgui.close_uis(src)
+ state = null
+ deltimer(deletion_timer)
+ return ..()
+
+/**
+ * Waits for a user's response to the tgui_input_color's prompt before returning. Returns early if
+ * the window was closed by the user.
+ */
+/datum/tgui_input_color/proc/wait()
+ while(!choice && !closed && !QDELETED(src))
+ stoplag(1)
+
+/datum/tgui_input_color/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "ColorPickerModal")
+ ui.open()
+ ui.set_autoupdate(timeout > 0)
+
+/datum/tgui_input_color/ui_close(mob/user)
+ closed = TRUE
+
+/datum/tgui_input_color/ui_state(mob/user)
+ return state
+
+/datum/tgui_input_color/ui_static_data(mob/user)
+ var/list/data = list()
+ data["autofocus"] = autofocus
+ data["large_buttons"] = !user.client?.prefs || (user.client.prefs.toggles2 & PREFTOGGLE_2_LARGE_INPUT_BUTTONS)
+ data["swapped_buttons"] = !user.client?.prefs || (user.client.prefs.toggles2 & PREFTOGGLE_2_SWAP_INPUT_BUTTONS)
+ data["title"] = title
+ data["default_color"] = default
+ data["message"] = message
+ return data
+
+/datum/tgui_input_color/ui_data(mob/user)
+ var/list/data = list()
+ if(timeout)
+ data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+ return data
+
+/datum/tgui_input_color/ui_act(action, list/params)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("submit")
+ if(!findtext(params["entry"], GLOB.is_color))
+ return
+ choice = params["entry"]
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+ if("cancel")
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
diff --git a/code/modules/tgui/tgui_input/input_checkbox.dm b/code/modules/tgui/tgui_input/input_checkbox.dm
new file mode 100644
index 00000000000..d7bb27f49eb
--- /dev/null
+++ b/code/modules/tgui/tgui_input/input_checkbox.dm
@@ -0,0 +1,73 @@
+/**
+ * Creates a TGUI input list window and returns the user's response in a ranked order.
+ *
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the input box, shown on the top of the TGUI window.
+ * * items - The options that can be chosen by the user, each string is assigned a button on the UI.
+ * * default - If an option is already preselected on the UI. Current values, etc.
+ * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_checkbox_list(mob/user, message, title = "Select", list/items, default, timeout = 0, ui_state = GLOB.always_state)
+ if(!user)
+ user = usr
+
+ if(!length(items))
+ CRASH("[user] tried to open an empty TGUI Input Checkbox List. Contents are: [items]")
+
+ if(!istype(user))
+ if(!isclient(user))
+ CRASH("We passed something that wasn't a user/client in a TGUI Input Checkbox List! The passed user was [user]!")
+ var/client/client = user
+ user = client.mob
+
+ if(isnull(user.client))
+ return
+
+ var/datum/tgui_list_input/checkbox/input = new(user, message, title, items, default, timeout, ui_state)
+
+ if(input.invalid)
+ qdel(input)
+ return
+
+ input.ui_interact(user)
+ input.wait()
+ if(input)
+ . = input.choice
+ qdel(input)
+
+/**
+ * # tgui_list_input/ranked
+ *
+ * Datum used for allowing a user to sort a TGUI-controlled list input that prompts the user with
+ * a message and shows a list of rankable options
+ */
+/datum/tgui_list_input/checkbox
+ modal_type = "CheckboxListInputModal"
+
+/datum/tgui_list_input/checkbox/handle_new_items(list/_items)
+ var/list/repeat_items = list()
+ // Gets rid of illegal characters
+ var/static/regex/blacklisted_words = regex(@{"([^\u0020-\u8000]+)"})
+
+ for(var/key in _items)
+ var/string_key = blacklisted_words.Replace("[key]", "")
+
+ // Avoids duplicated keys E.g: when areas have the same name
+ string_key = avoid_assoc_duplicate_keys(string_key, repeat_items)
+ src.items += list(list(
+ "key" = string_key,
+ "checked" = (_items[key] ? TRUE : FALSE)
+ ))
+ src.items_map = _items // we use this differently
+
+/datum/tgui_list_input/checkbox/handle_submit_action(params)
+ var/list/associated = list()
+ for(var/list/sublist in params["entry"])
+ associated[sublist["key"]] = (sublist["checked"] in list(1, "1", "true"))
+
+ if(!lists_equal_unordered(associated, items_map))
+ return FALSE
+ set_choice(associated)
+ return TRUE
diff --git a/code/modules/tgui/tgui_input/list_input.dm b/code/modules/tgui/tgui_input/list_input.dm
index f43ceffdaaf..639e3df359c 100644
--- a/code/modules/tgui/tgui_input/list_input.dm
+++ b/code/modules/tgui/tgui_input/list_input.dm
@@ -73,26 +73,18 @@
var/datum/ui_state/state
/// Whether the tgui list input is invalid or not (i.e. due to all list entries being null)
var/invalid = FALSE
+ /// The TGUI modal to use for this popup
+ var/modal_type = "ListInputModal"
-/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout, ui_state)
+/datum/tgui_list_input/New(mob/user, message, title, list/_items, default, timeout, ui_state)
src.title = title
src.message = message
src.items = list()
src.items_map = list()
src.default = default
src.state = ui_state
- var/list/repeat_items = list()
-
- // Gets rid of illegal characters
- var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"})
- for(var/i in items)
- var/string_key = whitelistedWords.Replace("[i]", "")
-
- // Avoids duplicated keys E.g: when areas have the same name
- string_key = avoid_assoc_duplicate_keys(string_key, repeat_items)
- src.items += string_key
- src.items_map[string_key] = i
+ handle_new_items(_items)
if(length(src.items) == 0)
invalid = TRUE
@@ -122,7 +114,7 @@
/datum/tgui_list_input/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
- ui = new(user, src, "ListInputModal")
+ ui = new(user, src, modal_type)
ui.set_autoupdate(FALSE)
ui.open()
@@ -152,9 +144,8 @@
switch(action)
if("submit")
- if(!(params["entry"] in items))
+ if(!handle_submit_action(params))
return
- set_choice(items_map[params["entry"]])
closed = TRUE
SStgui.close_uis(src)
return TRUE
@@ -163,5 +154,24 @@
SStgui.close_uis(src)
return TRUE
+/datum/tgui_list_input/proc/handle_submit_action(params)
+ if(!(params["entry"] in items))
+ return FALSE
+ set_choice(items_map[params["entry"]])
+ return TRUE
+
/datum/tgui_list_input/proc/set_choice(choice)
src.choice = choice
+
+/datum/tgui_list_input/proc/handle_new_items(list/_items)
+ var/list/repeat_items = list()
+ // Gets rid of illegal characters
+ var/static/regex/blacklisted_words = regex(@{"([^\u0020-\u8000]+)"})
+
+ for(var/i in _items)
+ var/string_key = blacklisted_words.Replace("[i]", "")
+
+ // Avoids duplicated keys E.g: when areas have the same name
+ string_key = avoid_assoc_duplicate_keys(string_key, repeat_items)
+ items += string_key
+ items_map[string_key] = i
diff --git a/code/modules/tgui/tgui_input/ranked_list_input.dm b/code/modules/tgui/tgui_input/ranked_list_input.dm
new file mode 100644
index 00000000000..80631d56823
--- /dev/null
+++ b/code/modules/tgui/tgui_input/ranked_list_input.dm
@@ -0,0 +1,56 @@
+/**
+ * Creates a TGUI input list window and returns the user's response.
+ *
+ * This proc should be used to create alerts that the caller will wait for a response from.
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the input box, shown on the top of the TGUI window.
+ * * items - The options that can be chosen by the user, each string is assigned a button on the UI.
+ * * default - If an option is already preselected on the UI. Current values, etc.
+ * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_ranked_list(mob/user, message, title = "Select", list/items, default, timeout = 0, ui_state = GLOB.always_state)
+ if(!user)
+ user = usr
+
+ if(!length(items))
+ CRASH("[user] tried to open an empty TGUI Input List. Contents are: [items]")
+
+ if(!istype(user))
+ if(!isclient(user))
+ CRASH("We passed something that wasn't a user/client in a TGUI Input List! The passed user was [user]!")
+ var/client/client = user
+ user = client.mob
+
+ if(isnull(user.client))
+ return
+
+ // We don't support disabled TGUI input (PREFTOGGLE_2_DISABLE_TGUI_INPUT), get with the times old man
+
+ var/datum/tgui_list_input/ranked/input = new(user, message, title, items, default, timeout, ui_state)
+
+ if(input.invalid)
+ qdel(input)
+ return
+
+ input.ui_interact(user)
+ input.wait()
+ if(input)
+ . = input.choice
+ qdel(input)
+
+/**
+ * # tgui_list_input/ranked
+ *
+ * Datum used for allowing a user to sort a TGUI-controlled list input that prompts the user with
+ * a message and shows a list of rankable options
+ */
+/datum/tgui_list_input/ranked
+ modal_type = "RankedListInputModal"
+
+/datum/tgui_list_input/ranked/handle_submit_action(params)
+ if(!lists_equal_unordered(params["entry"], items))
+ return FALSE
+ set_choice(params["entry"])
+ return TRUE
diff --git a/icons/_nanomaps/Celestation_nanomap_z2.png b/icons/_nanomaps/Celestation_nanomap_z2.png
index c2bc8f31bde..3339d6728af 100644
Binary files a/icons/_nanomaps/Celestation_nanomap_z2.png and b/icons/_nanomaps/Celestation_nanomap_z2.png differ
diff --git a/icons/_nanomaps/Cerestation_nanomap_z1.png b/icons/_nanomaps/Cerestation_nanomap_z1.png
index 7e49b7db91e..e0ab63984a5 100644
Binary files a/icons/_nanomaps/Cerestation_nanomap_z1.png and b/icons/_nanomaps/Cerestation_nanomap_z1.png differ
diff --git a/icons/_nanomaps/Cyberiad_nanomap_z1.png b/icons/_nanomaps/Cyberiad_nanomap_z1.png
index 2aade9d253a..8950fdd2a2a 100644
Binary files a/icons/_nanomaps/Cyberiad_nanomap_z1.png and b/icons/_nanomaps/Cyberiad_nanomap_z1.png differ
diff --git a/icons/_nanomaps/Delta_nanomap_z1.png b/icons/_nanomaps/Delta_nanomap_z1.png
index 4fdb7243124..dc650a6f93b 100644
Binary files a/icons/_nanomaps/Delta_nanomap_z1.png and b/icons/_nanomaps/Delta_nanomap_z1.png differ
diff --git a/icons/_nanomaps/Nova_nanomap_z1.png b/icons/_nanomaps/Nova_nanomap_z1.png
index e3b86ddbadf..24e2d58aec6 100644
Binary files a/icons/_nanomaps/Nova_nanomap_z1.png and b/icons/_nanomaps/Nova_nanomap_z1.png differ
diff --git a/icons/_nanomaps/Nova_nanomap_z2.png b/icons/_nanomaps/Nova_nanomap_z2.png
index 5edd5a29f1b..cb359d4a128 100644
Binary files a/icons/_nanomaps/Nova_nanomap_z2.png and b/icons/_nanomaps/Nova_nanomap_z2.png differ
diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi
index e825e855573..e87749c7242 100644
Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ
diff --git a/icons/effects/mouse_pointers/supplypod_down_target.dmi b/icons/effects/mouse_pointers/supplypod_down_target.dmi
new file mode 100644
index 00000000000..53a3bee0a78
Binary files /dev/null and b/icons/effects/mouse_pointers/supplypod_down_target.dmi differ
diff --git a/icons/effects/mouse_pointers/supplypod_pickturf.dmi b/icons/effects/mouse_pointers/supplypod_pickturf.dmi
new file mode 100644
index 00000000000..3ca1131e1a8
Binary files /dev/null and b/icons/effects/mouse_pointers/supplypod_pickturf.dmi differ
diff --git a/icons/effects/mouse_pointers/supplypod_pickturf_down.dmi b/icons/effects/mouse_pointers/supplypod_pickturf_down.dmi
new file mode 100644
index 00000000000..113fe47540c
Binary files /dev/null and b/icons/effects/mouse_pointers/supplypod_pickturf_down.dmi differ
diff --git a/icons/effects/mouse_pointers/supplypod_target.dmi b/icons/effects/mouse_pointers/supplypod_target.dmi
new file mode 100644
index 00000000000..94401d7a8ae
Binary files /dev/null and b/icons/effects/mouse_pointers/supplypod_target.dmi differ
diff --git a/icons/effects/particles/bonfire.dmi b/icons/effects/particles/bonfire.dmi
new file mode 100644
index 00000000000..e8e2e36346d
Binary files /dev/null and b/icons/effects/particles/bonfire.dmi differ
diff --git a/icons/effects/particles/echo.dmi b/icons/effects/particles/echo.dmi
new file mode 100644
index 00000000000..60a243a8a7b
Binary files /dev/null and b/icons/effects/particles/echo.dmi differ
diff --git a/icons/effects/particles/generic.dmi b/icons/effects/particles/generic.dmi
new file mode 100644
index 00000000000..41776efdbfd
Binary files /dev/null and b/icons/effects/particles/generic.dmi differ
diff --git a/icons/effects/particles/goop.dmi b/icons/effects/particles/goop.dmi
new file mode 100644
index 00000000000..673c1a7ad5b
Binary files /dev/null and b/icons/effects/particles/goop.dmi differ
diff --git a/icons/effects/particles/pollen.dmi b/icons/effects/particles/pollen.dmi
new file mode 100644
index 00000000000..559c4d1846f
Binary files /dev/null and b/icons/effects/particles/pollen.dmi differ
diff --git a/icons/effects/particles/smoke.dmi b/icons/effects/particles/smoke.dmi
new file mode 100644
index 00000000000..99123beeb59
Binary files /dev/null and b/icons/effects/particles/smoke.dmi differ
diff --git a/icons/effects/particles/stink.dmi b/icons/effects/particles/stink.dmi
new file mode 100644
index 00000000000..29b92acbe67
Binary files /dev/null and b/icons/effects/particles/stink.dmi differ
diff --git a/icons/effects/particles/voidwalker.dmi b/icons/effects/particles/voidwalker.dmi
new file mode 100644
index 00000000000..d7f94c98797
Binary files /dev/null and b/icons/effects/particles/voidwalker.dmi differ
diff --git a/icons/effects/weather_effects.dmi b/icons/effects/weather_effects.dmi
index 00083c464a2..7cc1ce758a3 100644
Binary files a/icons/effects/weather_effects.dmi and b/icons/effects/weather_effects.dmi differ
diff --git a/icons/hud/blob.dmi b/icons/hud/blob.dmi
new file mode 100644
index 00000000000..552f511004f
Binary files /dev/null and b/icons/hud/blob.dmi differ
diff --git a/icons/mob/actions/actions.dmi b/icons/mob/actions/actions.dmi
index f2db1779130..faba7f17d23 100644
Binary files a/icons/mob/actions/actions.dmi and b/icons/mob/actions/actions.dmi differ
diff --git a/icons/mob/blob.dmi b/icons/mob/blob.dmi
index 3a73ccf0994..6313a92db0f 100644
Binary files a/icons/mob/blob.dmi and b/icons/mob/blob.dmi differ
diff --git a/icons/mob/clothing/feet.dmi b/icons/mob/clothing/feet.dmi
index 601e7162ee6..a18e9d2fc36 100644
Binary files a/icons/mob/clothing/feet.dmi and b/icons/mob/clothing/feet.dmi differ
diff --git a/icons/mob/clothing/hands.dmi b/icons/mob/clothing/hands.dmi
index 8ff67b1f4a7..5cfeccfa9c9 100644
Binary files a/icons/mob/clothing/hands.dmi and b/icons/mob/clothing/hands.dmi differ
diff --git a/icons/mob/clothing/species/drask/gloves.dmi b/icons/mob/clothing/species/drask/gloves.dmi
index b8320dcc8b5..c7be0d0f961 100644
Binary files a/icons/mob/clothing/species/drask/gloves.dmi and b/icons/mob/clothing/species/drask/gloves.dmi differ
diff --git a/icons/mob/clothing/species/drask/shoes.dmi b/icons/mob/clothing/species/drask/shoes.dmi
index 287b8b3b905..f64d6359692 100644
Binary files a/icons/mob/clothing/species/drask/shoes.dmi and b/icons/mob/clothing/species/drask/shoes.dmi differ
diff --git a/icons/mob/clothing/species/monkey/gloves.dmi b/icons/mob/clothing/species/monkey/gloves.dmi
index 4d51121aed5..98c738ff3af 100644
Binary files a/icons/mob/clothing/species/monkey/gloves.dmi and b/icons/mob/clothing/species/monkey/gloves.dmi differ
diff --git a/icons/mob/clothing/species/monkey/shoes.dmi b/icons/mob/clothing/species/monkey/shoes.dmi
index 5e9f38cfb99..7f08b7132f9 100644
Binary files a/icons/mob/clothing/species/monkey/shoes.dmi and b/icons/mob/clothing/species/monkey/shoes.dmi differ
diff --git a/icons/mob/clothing/species/unathi/shoes.dmi b/icons/mob/clothing/species/unathi/shoes.dmi
index 2d428a7d85b..8dcf1748cef 100644
Binary files a/icons/mob/clothing/species/unathi/shoes.dmi and b/icons/mob/clothing/species/unathi/shoes.dmi differ
diff --git a/icons/mob/clothing/species/vox/gloves.dmi b/icons/mob/clothing/species/vox/gloves.dmi
index 30f5c61de26..56320852d9b 100644
Binary files a/icons/mob/clothing/species/vox/gloves.dmi and b/icons/mob/clothing/species/vox/gloves.dmi differ
diff --git a/icons/mob/clothing/species/vox/shoes.dmi b/icons/mob/clothing/species/vox/shoes.dmi
index c5460f4b8f1..766d3a4c916 100644
Binary files a/icons/mob/clothing/species/vox/shoes.dmi and b/icons/mob/clothing/species/vox/shoes.dmi differ
diff --git a/icons/mob/gondolas.dmi b/icons/mob/gondolas.dmi
new file mode 100644
index 00000000000..c8540fbac0b
Binary files /dev/null and b/icons/mob/gondolas.dmi differ
diff --git a/icons/mob/inhands/chaplain_lefthand.dmi b/icons/mob/inhands/chaplain_lefthand.dmi
new file mode 100644
index 00000000000..448b4aac44b
Binary files /dev/null and b/icons/mob/inhands/chaplain_lefthand.dmi differ
diff --git a/icons/mob/inhands/chaplain_righthand.dmi b/icons/mob/inhands/chaplain_righthand.dmi
new file mode 100644
index 00000000000..120ee0bdee8
Binary files /dev/null and b/icons/mob/inhands/chaplain_righthand.dmi differ
diff --git a/icons/mob/inhands/clothing_lefthand.dmi b/icons/mob/inhands/clothing_lefthand.dmi
index b7af09ade06..ff13a5f65f8 100644
Binary files a/icons/mob/inhands/clothing_lefthand.dmi and b/icons/mob/inhands/clothing_lefthand.dmi differ
diff --git a/icons/mob/inhands/clothing_righthand.dmi b/icons/mob/inhands/clothing_righthand.dmi
index 10fc756e2aa..71303398149 100644
Binary files a/icons/mob/inhands/clothing_righthand.dmi and b/icons/mob/inhands/clothing_righthand.dmi differ
diff --git a/icons/mob/inhands/fluff_lefthand.dmi b/icons/mob/inhands/fluff_lefthand.dmi
index daed1b89256..694c58776ce 100644
Binary files a/icons/mob/inhands/fluff_lefthand.dmi and b/icons/mob/inhands/fluff_lefthand.dmi differ
diff --git a/icons/mob/inhands/fluff_righthand.dmi b/icons/mob/inhands/fluff_righthand.dmi
index 83666d38747..2fdd42b0435 100644
Binary files a/icons/mob/inhands/fluff_righthand.dmi and b/icons/mob/inhands/fluff_righthand.dmi differ
diff --git a/icons/mob/inhands/foods_lefthand.dmi b/icons/mob/inhands/foods_lefthand.dmi
index d5a0c01b1d1..46107265e3d 100644
Binary files a/icons/mob/inhands/foods_lefthand.dmi and b/icons/mob/inhands/foods_lefthand.dmi differ
diff --git a/icons/mob/inhands/foods_righthand.dmi b/icons/mob/inhands/foods_righthand.dmi
index fff5fbee21d..e2a56894e56 100644
Binary files a/icons/mob/inhands/foods_righthand.dmi and b/icons/mob/inhands/foods_righthand.dmi differ
diff --git a/icons/mob/inhands/guns_lefthand.dmi b/icons/mob/inhands/guns_lefthand.dmi
index 47e1359325f..63bf85b4542 100644
Binary files a/icons/mob/inhands/guns_lefthand.dmi and b/icons/mob/inhands/guns_lefthand.dmi differ
diff --git a/icons/mob/inhands/guns_righthand.dmi b/icons/mob/inhands/guns_righthand.dmi
index 005f11926b3..ef08babed38 100644
Binary files a/icons/mob/inhands/guns_righthand.dmi and b/icons/mob/inhands/guns_righthand.dmi differ
diff --git a/icons/mob/inhands/id_lefthand.dmi b/icons/mob/inhands/id_lefthand.dmi
new file mode 100644
index 00000000000..8d7f7871bd4
Binary files /dev/null and b/icons/mob/inhands/id_lefthand.dmi differ
diff --git a/icons/mob/inhands/id_righthand.dmi b/icons/mob/inhands/id_righthand.dmi
new file mode 100644
index 00000000000..9db0a696b49
Binary files /dev/null and b/icons/mob/inhands/id_righthand.dmi differ
diff --git a/icons/mob/inhands/items_lefthand.dmi b/icons/mob/inhands/items_lefthand.dmi
index df670a6fada..e4cdce90ea7 100755
Binary files a/icons/mob/inhands/items_lefthand.dmi and b/icons/mob/inhands/items_lefthand.dmi differ
diff --git a/icons/mob/inhands/items_righthand.dmi b/icons/mob/inhands/items_righthand.dmi
index 10723f454a9..bc5ed56ef56 100755
Binary files a/icons/mob/inhands/items_righthand.dmi and b/icons/mob/inhands/items_righthand.dmi differ
diff --git a/icons/mob/inhands/melee_lefthand.dmi b/icons/mob/inhands/melee_lefthand.dmi
new file mode 100644
index 00000000000..2adec08f235
Binary files /dev/null and b/icons/mob/inhands/melee_lefthand.dmi differ
diff --git a/icons/mob/inhands/melee_righthand.dmi b/icons/mob/inhands/melee_righthand.dmi
new file mode 100644
index 00000000000..19bd0038c6b
Binary files /dev/null and b/icons/mob/inhands/melee_righthand.dmi differ
diff --git a/icons/mob/inhands/pda_lefthand.dmi b/icons/mob/inhands/pda_lefthand.dmi
new file mode 100644
index 00000000000..9f875f6e2d6
Binary files /dev/null and b/icons/mob/inhands/pda_lefthand.dmi differ
diff --git a/icons/mob/inhands/pda_righthand.dmi b/icons/mob/inhands/pda_righthand.dmi
new file mode 100644
index 00000000000..adff23b47f6
Binary files /dev/null and b/icons/mob/inhands/pda_righthand.dmi differ
diff --git a/icons/mob/inhands/sheet_lefthand.dmi b/icons/mob/inhands/sheet_lefthand.dmi
new file mode 100644
index 00000000000..fb1e7df92c9
Binary files /dev/null and b/icons/mob/inhands/sheet_lefthand.dmi differ
diff --git a/icons/mob/inhands/sheet_righthand.dmi b/icons/mob/inhands/sheet_righthand.dmi
new file mode 100644
index 00000000000..16e01936de3
Binary files /dev/null and b/icons/mob/inhands/sheet_righthand.dmi differ
diff --git a/icons/mob/inhands/staff_lefthand.dmi b/icons/mob/inhands/staff_lefthand.dmi
new file mode 100644
index 00000000000..5f4cf88d0c3
Binary files /dev/null and b/icons/mob/inhands/staff_lefthand.dmi differ
diff --git a/icons/mob/inhands/staff_righthand.dmi b/icons/mob/inhands/staff_righthand.dmi
new file mode 100644
index 00000000000..378da59edad
Binary files /dev/null and b/icons/mob/inhands/staff_righthand.dmi differ
diff --git a/icons/mob/inhands/tools_lefthand.dmi b/icons/mob/inhands/tools_lefthand.dmi
new file mode 100644
index 00000000000..d2c95848c26
Binary files /dev/null and b/icons/mob/inhands/tools_lefthand.dmi differ
diff --git a/icons/mob/inhands/tools_righthand.dmi b/icons/mob/inhands/tools_righthand.dmi
new file mode 100644
index 00000000000..011c9e94a9c
Binary files /dev/null and b/icons/mob/inhands/tools_righthand.dmi differ
diff --git a/icons/mob/inhands/twohanded_lefthand.dmi b/icons/mob/inhands/twohanded_lefthand.dmi
new file mode 100644
index 00000000000..1845bbb24be
Binary files /dev/null and b/icons/mob/inhands/twohanded_lefthand.dmi differ
diff --git a/icons/mob/inhands/twohanded_righthand.dmi b/icons/mob/inhands/twohanded_righthand.dmi
new file mode 100644
index 00000000000..77aaf5016cd
Binary files /dev/null and b/icons/mob/inhands/twohanded_righthand.dmi differ
diff --git a/icons/mob/inhands/zippo_lefthand.dmi b/icons/mob/inhands/zippo_lefthand.dmi
new file mode 100644
index 00000000000..15a3cb63d85
Binary files /dev/null and b/icons/mob/inhands/zippo_lefthand.dmi differ
diff --git a/icons/mob/inhands/zippo_righthand.dmi b/icons/mob/inhands/zippo_righthand.dmi
new file mode 100644
index 00000000000..8faea7d03ea
Binary files /dev/null and b/icons/mob/inhands/zippo_righthand.dmi differ
diff --git a/icons/obj/clothing/gloves.dmi b/icons/obj/clothing/gloves.dmi
index b2829a92ec0..2ad0edbd285 100644
Binary files a/icons/obj/clothing/gloves.dmi and b/icons/obj/clothing/gloves.dmi differ
diff --git a/icons/obj/clothing/shoes.dmi b/icons/obj/clothing/shoes.dmi
index 2f796a0ca27..52cf675c5d3 100644
Binary files a/icons/obj/clothing/shoes.dmi and b/icons/obj/clothing/shoes.dmi differ
diff --git a/icons/obj/economy.dmi b/icons/obj/economy.dmi
index ea1bab2622f..34a9422cb68 100644
Binary files a/icons/obj/economy.dmi and b/icons/obj/economy.dmi differ
diff --git a/icons/obj/items.dmi b/icons/obj/items.dmi
index a7291fdee7f..bc70bbe144d 100644
Binary files a/icons/obj/items.dmi and b/icons/obj/items.dmi differ
diff --git a/icons/obj/mecha/lockermech.dmi b/icons/obj/mecha/lockermech.dmi
index ea99827ee41..a2ad9c80a1b 100644
Binary files a/icons/obj/mecha/lockermech.dmi and b/icons/obj/mecha/lockermech.dmi differ
diff --git a/icons/obj/mecha/mecha.dmi b/icons/obj/mecha/mecha.dmi
index 379e9875dea..d3315d18e7a 100644
Binary files a/icons/obj/mecha/mecha.dmi and b/icons/obj/mecha/mecha.dmi differ
diff --git a/icons/obj/mecha/mecha_equipment.dmi b/icons/obj/mecha/mecha_equipment.dmi
index d31ccb93fa1..b8112616f58 100644
Binary files a/icons/obj/mecha/mecha_equipment.dmi and b/icons/obj/mecha/mecha_equipment.dmi differ
diff --git a/icons/obj/statue.dmi b/icons/obj/statue.dmi
index c55db97561f..d59456ef187 100644
Binary files a/icons/obj/statue.dmi and b/icons/obj/statue.dmi differ
diff --git a/icons/obj/supplypods.dmi b/icons/obj/supplypods.dmi
new file mode 100644
index 00000000000..156d3cea245
Binary files /dev/null and b/icons/obj/supplypods.dmi differ
diff --git a/icons/obj/supplypods_32x32.dmi b/icons/obj/supplypods_32x32.dmi
new file mode 100644
index 00000000000..855132f6494
Binary files /dev/null and b/icons/obj/supplypods_32x32.dmi differ
diff --git a/icons/obj/weapons/energy.dmi b/icons/obj/weapons/energy.dmi
index 76045167bac..39ca844dd16 100644
Binary files a/icons/obj/weapons/energy.dmi and b/icons/obj/weapons/energy.dmi differ
diff --git a/icons/turf/areas.dmi b/icons/turf/areas.dmi
index 1d56391f9d3..3bd88590db8 100755
Binary files a/icons/turf/areas.dmi and b/icons/turf/areas.dmi differ
diff --git a/paradise.dme b/paradise.dme
index 31a005dc9a4..17870ec3db3 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -41,6 +41,7 @@
#include "code\__DEFINES\bots.dm"
#include "code\__DEFINES\byond_tracy.dm"
#include "code\__DEFINES\callbacks.dm"
+#include "code\__DEFINES\cargo.dm"
#include "code\__DEFINES\cargo_quests.dm"
#include "code\__DEFINES\chat.dm"
#include "code\__DEFINES\chat_box_defines.dm"
@@ -68,6 +69,7 @@
#include "code\__DEFINES\footstep.dm"
#include "code\__DEFINES\game.dm"
#include "code\__DEFINES\gamemode.dm"
+#include "code\__DEFINES\generators.dm"
#include "code\__DEFINES\genetics.dm"
#include "code\__DEFINES\gravity.dm"
#include "code\__DEFINES\hud.dm"
@@ -104,6 +106,7 @@
#include "code\__DEFINES\obj_flags.dm"
#include "code\__DEFINES\organ_defines.dm"
#include "code\__DEFINES\overlays.dm"
+#include "code\__DEFINES\particles.dm"
#include "code\__DEFINES\path.dm"
#include "code\__DEFINES\pda.dm"
#include "code\__DEFINES\pipes.dm"
@@ -118,8 +121,10 @@
#include "code\__DEFINES\rituals.dm"
#include "code\__DEFINES\role_preferences.dm"
#include "code\__DEFINES\rolebans.dm"
+#include "code\__DEFINES\ru_lang_rules.dm"
#include "code\__DEFINES\rust_g.dm"
#include "code\__DEFINES\rust_g_overrides.dm"
+#include "code\__DEFINES\say.dm"
#include "code\__DEFINES\secret_documents.dm"
#include "code\__DEFINES\sensor_devices.dm"
#include "code\__DEFINES\shuttle.dm"
@@ -156,6 +161,7 @@
#include "code\__DEFINES\dcs\helpers.dm"
#include "code\__DEFINES\dcs\mapping.dm"
#include "code\__DEFINES\dcs\signals.dm"
+#include "code\__DEFINES\dcs\signals_blob.dm"
#include "code\__DEFINES\dcs\signals_lazy_templates.dm"
#include "code\__DEFINES\dcs\signals_object.dm"
#include "code\__DEFINES\dcs\signals_turf.dm"
@@ -171,6 +177,8 @@
#include "code\__HELPERS\atmospherics.dm"
#include "code\__HELPERS\atoms.dm"
#include "code\__HELPERS\bitflag_lists.dm"
+#include "code\__HELPERS\bitflags.dm"
+#include "code\__HELPERS\chat.dm"
#include "code\__HELPERS\cmp.dm"
#include "code\__HELPERS\constants.dm"
#include "code\__HELPERS\experimental.dm"
@@ -192,6 +200,7 @@
#include "code\__HELPERS\pronouns.dm"
#include "code\__HELPERS\qdel.dm"
#include "code\__HELPERS\reagents_helpers.dm"
+#include "code\__HELPERS\ref.dm"
#include "code\__HELPERS\russian.dm"
#include "code\__HELPERS\sanitize_values.dm"
#include "code\__HELPERS\shell.dm"
@@ -202,6 +211,7 @@
#include "code\__HELPERS\time.dm"
#include "code\__HELPERS\tool_helpers.dm"
#include "code\__HELPERS\traits.dm"
+#include "code\__HELPERS\turfs.dm"
#include "code\__HELPERS\type2type.dm"
#include "code\__HELPERS\typelists.dm"
#include "code\__HELPERS\unique_ids.dm"
@@ -216,7 +226,6 @@
#include "code\__HELPERS\sorts\MergeSort.dm"
#include "code\__HELPERS\sorts\TimSort.dm"
#include "code\_globalvars\_regexes.dm"
-#include "code\_globalvars\bitfields.dm"
#include "code\_globalvars\configuration.dm"
#include "code\_globalvars\game_modes.dm"
#include "code\_globalvars\genetics.dm"
@@ -225,6 +234,16 @@
#include "code\_globalvars\misc.dm"
#include "code\_globalvars\sensitive.dm"
#include "code\_globalvars\traits.dm"
+#include "code\_globalvars\bitfields\admin.dm"
+#include "code\_globalvars\bitfields\bitfields.dm"
+#include "code\_globalvars\bitfields\declarations.dm"
+#include "code\_globalvars\bitfields\food.dm"
+#include "code\_globalvars\bitfields\icon_smoothing.dm"
+#include "code\_globalvars\bitfields\jobs.dm"
+#include "code\_globalvars\bitfields\mecha.dm"
+#include "code\_globalvars\bitfields\mobs.dm"
+#include "code\_globalvars\bitfields\objs.dm"
+#include "code\_globalvars\bitfields\sight.dm"
#include "code\_globalvars\lists\flavor_misc.dm"
#include "code\_globalvars\lists\fortunes.dm"
#include "code\_globalvars\lists\keybindings.dm"
@@ -255,6 +274,7 @@
#include "code\_onclick\hud\alien.dm"
#include "code\_onclick\hud\alien_larva.dm"
#include "code\_onclick\hud\blob_overmind.dm"
+#include "code\_onclick\hud\blobbernaut.dm"
#include "code\_onclick\hud\bot.dm"
#include "code\_onclick\hud\cogscarab.dm"
#include "code\_onclick\hud\constructs.dm"
@@ -309,6 +329,7 @@
#include "code\controllers\subsystem\early_assets.dm"
#include "code\controllers\subsystem\events.dm"
#include "code\controllers\subsystem\fires.dm"
+#include "code\controllers\subsystem\fluids.dm"
#include "code\controllers\subsystem\game_events.dm"
#include "code\controllers\subsystem\garbage.dm"
#include "code\controllers\subsystem\ghost_spawns.dm"
@@ -403,6 +424,7 @@
#include "code\datums\mutable_appearance.dm"
#include "code\datums\periodic_news.dm"
#include "code\datums\pipe_datums.dm"
+#include "code\datums\pod_style.dm"
#include "code\datums\position_point_vector.dm"
#include "code\datums\progressbar.dm"
#include "code\datums\radio.dm"
@@ -439,18 +461,22 @@
#include "code\datums\components\after_attacks_hub.dm"
#include "code\datums\components\animal_temperature.dm"
#include "code\datums\components\aura_healing.dm"
+#include "code\datums\components\blob_minion.dm"
+#include "code\datums\components\blob_turf_consuming.dm"
#include "code\datums\components\boomerang.dm"
#include "code\datums\components\boss_music.dm"
#include "code\datums\components\caltrop.dm"
#include "code\datums\components\chasm.dm"
#include "code\datums\components\codeword_hearing.dm"
#include "code\datums\components\combo_attacks.dm"
+#include "code\datums\components\connect_containers.dm"
#include "code\datums\components\connect_loc_behalf.dm"
#include "code\datums\components\connect_mob_behalf.dm"
#include "code\datums\components\contsruction_regenerate.dm"
#include "code\datums\components\conveyor_movement.dm"
#include "code\datums\components\cross_shock.dm"
#include "code\datums\components\decal.dm"
+#include "code\datums\components\death_linked.dm"
#include "code\datums\components\defibrillator.dm"
#include "code\datums\components\drift.dm"
#include "code\datums\components\ducttape.dm"
@@ -459,14 +485,18 @@
#include "code\datums\components\examine_override.dm"
#include "code\datums\components\force_move.dm"
#include "code\datums\components\fullauto.dm"
+#include "code\datums\components\ghost_direct_control.dm"
#include "code\datums\components\hide_highest_offset.dm"
+#include "code\datums\components\holderloving.dm"
#include "code\datums\components\jackboots.dm"
#include "code\datums\components\jetpack.dm"
#include "code\datums\components\label.dm"
#include "code\datums\components\material_container.dm"
#include "code\datums\components\overlay_lighting.dm"
#include "code\datums\components\paintable.dm"
+#include "code\datums\components\pellet_cloud.dm"
#include "code\datums\components\persistent_overlay.dm"
+#include "code\datums\components\pref_viewer.dm"
#include "code\datums\components\proximity_monitor.dm"
#include "code\datums\components\radioactivity.dm"
#include "code\datums\components\ritual_object.dm"
@@ -475,6 +505,7 @@
#include "code\datums\components\spawner.dm"
#include "code\datums\components\spooky.dm"
#include "code\datums\components\squeak.dm"
+#include "code\datums\components\stationloving.dm"
#include "code\datums\components\surgery_initiator.dm"
#include "code\datums\components\swarming.dm"
#include "code\datums\components\transforming.dm"
@@ -562,6 +593,7 @@
#include "code\datums\elements\light_blocking.dm"
#include "code\datums\elements\movetype_handler.dm"
#include "code\datums\elements\openspace_item_click_handler.dm"
+#include "code\datums\elements\reagent_attack.dm"
#include "code\datums\elements\ridable.dm"
#include "code\datums\elements\simple_flying.dm"
#include "code\datums\elements\squish.dm"
@@ -688,8 +720,10 @@
#include "code\datums\status_effects\debuffs.dm"
#include "code\datums\status_effects\gas.dm"
#include "code\datums\status_effects\neutral.dm"
+#include "code\datums\status_effects\screwy_hud.dm"
#include "code\datums\status_effects\status_effect.dm"
#include "code\datums\status_effects\status_effects_absorption.dm"
+#include "code\datums\status_effects\wet_stacks.dm"
#include "code\datums\weather\weather.dm"
#include "code\datums\weather\weather_types\ash_storm.dm"
#include "code\datums\weather\weather_types\blob_storm.dm"
@@ -760,17 +794,6 @@
#include "code\game\gamemodes\blob\blob.dm"
#include "code\game\gamemodes\blob\blob_finish.dm"
#include "code\game\gamemodes\blob\blob_report.dm"
-#include "code\game\gamemodes\blob\overmind.dm"
-#include "code\game\gamemodes\blob\powers.dm"
-#include "code\game\gamemodes\blob\theblob.dm"
-#include "code\game\gamemodes\blob\blobs\blob_mobs.dm"
-#include "code\game\gamemodes\blob\blobs\captured_nuke.dm"
-#include "code\game\gamemodes\blob\blobs\core.dm"
-#include "code\game\gamemodes\blob\blobs\factory.dm"
-#include "code\game\gamemodes\blob\blobs\node.dm"
-#include "code\game\gamemodes\blob\blobs\resource.dm"
-#include "code\game\gamemodes\blob\blobs\shield.dm"
-#include "code\game\gamemodes\blob\blobs\storage.dm"
#include "code\game\gamemodes\changeling\changeling.dm"
#include "code\game\gamemodes\changeling\thief_chan.dm"
#include "code\game\gamemodes\changeling\traitor_chan.dm"
@@ -1087,6 +1110,7 @@
#include "code\game\objects\effects\mines.dm"
#include "code\game\objects\effects\misc.dm"
#include "code\game\objects\effects\overlays.dm"
+#include "code\game\objects\effects\particle_holder.dm"
#include "code\game\objects\effects\portals.dm"
#include "code\game\objects\effects\snowcloud.dm"
#include "code\game\objects\effects\spiders.dm"
@@ -1117,6 +1141,9 @@
#include "code\game\objects\effects\effect_system\effects_smoke.dm"
#include "code\game\objects\effects\effect_system\effects_sparks.dm"
#include "code\game\objects\effects\effect_system\effects_water.dm"
+#include "code\game\objects\effects\effect_system\fluid_spread\_fluid_spread.dm"
+#include "code\game\objects\effects\effect_system\fluid_spread\effects_smoke.dm"
+#include "code\game\objects\effects\particles\water.dm"
#include "code\game\objects\effects\spawners\airlock_spawner.dm"
#include "code\game\objects\effects\spawners\bombspawner.dm"
#include "code\game\objects\effects\spawners\gibspawner.dm"
@@ -1297,6 +1324,7 @@
#include "code\game\objects\items\weapons\twohanded.dm"
#include "code\game\objects\items\weapons\vending_items.dm"
#include "code\game\objects\items\weapons\weaponry.dm"
+#include "code\game\objects\items\weapons\welder_sword.dm"
#include "code\game\objects\items\weapons\whetstone.dm"
#include "code\game\objects\items\weapons\grenades\atmosgrenade.dm"
#include "code\game\objects\items\weapons\grenades\bananade.dm"
@@ -1547,6 +1575,7 @@
#include "code\modules\admin\verbs\atmosdebug.dm"
#include "code\modules\admin\verbs\borgpanel.dm"
#include "code\modules\admin\verbs\BrokenInhands.dm"
+#include "code\modules\admin\verbs\cantcomm_cargo.dm"
#include "code\modules\admin\verbs\cinematic.dm"
#include "code\modules\admin\verbs\custom_event.dm"
#include "code\modules\admin\verbs\deadsay.dm"
@@ -1571,6 +1600,7 @@
#include "code\modules\admin\verbs\possess.dm"
#include "code\modules\admin\verbs\pray.dm"
#include "code\modules\admin\verbs\randomverbs.dm"
+#include "code\modules\admin\verbs\reagents_editor.dm"
#include "code\modules\admin\verbs\requests.dm"
#include "code\modules\admin\verbs\serialization.dm"
#include "code\modules\admin\verbs\space_transitions.dm"
@@ -1590,7 +1620,45 @@
#include "code\modules\antagonists\_common\antag_team.dm"
#include "code\modules\antagonists\blob\blob_actions.dm"
#include "code\modules\antagonists\blob\blob_infected_datum.dm"
+#include "code\modules\antagonists\blob\blob_minion.dm"
#include "code\modules\antagonists\blob\blob_overmind_datum.dm"
+#include "code\modules\antagonists\blob\blobs_attack.dm"
+#include "code\modules\antagonists\blob\overmind.dm"
+#include "code\modules\antagonists\blob\powers.dm"
+#include "code\modules\antagonists\blob\powers_verbs.dm"
+#include "code\modules\antagonists\blob\blob_minions\blob_mob.dm"
+#include "code\modules\antagonists\blob\blob_minions\blob_spore.dm"
+#include "code\modules\antagonists\blob\blob_minions\blob_zombie.dm"
+#include "code\modules\antagonists\blob\blob_minions\blobbernaut.dm"
+#include "code\modules\antagonists\blob\blobstrains\_blobstrain.dm"
+#include "code\modules\antagonists\blob\blobstrains\_reagent.dm"
+#include "code\modules\antagonists\blob\blobstrains\blazing_oil.dm"
+#include "code\modules\antagonists\blob\blobstrains\blob_sorium.dm"
+#include "code\modules\antagonists\blob\blobstrains\cryogenic_poison.dm"
+#include "code\modules\antagonists\blob\blobstrains\debris_devourer.dm"
+#include "code\modules\antagonists\blob\blobstrains\distributed_neurons.dm"
+#include "code\modules\antagonists\blob\blobstrains\electromagnetic_web.dm"
+#include "code\modules\antagonists\blob\blobstrains\energized_jelly.dm"
+#include "code\modules\antagonists\blob\blobstrains\explosive_lattice.dm"
+#include "code\modules\antagonists\blob\blobstrains\multiplex.dm"
+#include "code\modules\antagonists\blob\blobstrains\networked_fibers.dm"
+#include "code\modules\antagonists\blob\blobstrains\pressurized_slime.dm"
+#include "code\modules\antagonists\blob\blobstrains\radioactive_gel.dm"
+#include "code\modules\antagonists\blob\blobstrains\reactive_spines.dm"
+#include "code\modules\antagonists\blob\blobstrains\regenerative_materia.dm"
+#include "code\modules\antagonists\blob\blobstrains\replicating_foam.dm"
+#include "code\modules\antagonists\blob\blobstrains\shifting_fragments.dm"
+#include "code\modules\antagonists\blob\blobstrains\synchronous_mesh.dm"
+#include "code\modules\antagonists\blob\structures\_blob.dm"
+#include "code\modules\antagonists\blob\structures\captured_nuke.dm"
+#include "code\modules\antagonists\blob\structures\core.dm"
+#include "code\modules\antagonists\blob\structures\factory.dm"
+#include "code\modules\antagonists\blob\structures\node.dm"
+#include "code\modules\antagonists\blob\structures\normal.dm"
+#include "code\modules\antagonists\blob\structures\resource.dm"
+#include "code\modules\antagonists\blob\structures\shield.dm"
+#include "code\modules\antagonists\blob\structures\special.dm"
+#include "code\modules\antagonists\blob\structures\storage.dm"
#include "code\modules\antagonists\borer\borer_action.dm"
#include "code\modules\antagonists\borer\borer_datum.dm"
#include "code\modules\antagonists\borer\borer_focus.dm"
@@ -1758,6 +1826,7 @@
#include "code\modules\asset_cache\assets\asset_cloning.dm"
#include "code\modules\asset_cache\assets\asset_common.dm"
#include "code\modules\asset_cache\assets\asset_emoji.dm"
+#include "code\modules\asset_cache\assets\asset_icon_ref_map.dm"
#include "code\modules\asset_cache\assets\asset_id_card.dm"
#include "code\modules\asset_cache\assets\asset_jquery.dm"
#include "code\modules\asset_cache\assets\asset_lobby.dm"
@@ -1772,6 +1841,7 @@
#include "code\modules\asset_cache\assets\asset_seeds.dm"
#include "code\modules\asset_cache\assets\asset_strip.dm"
#include "code\modules\asset_cache\assets\asset_tgui.dm"
+#include "code\modules\asset_cache\assets\supplypods.dm"
#include "code\modules\asset_cache\transports\asset_transport.dm"
#include "code\modules\asset_cache\transports\webroot_transport.dm"
#include "code\modules\atmospherics\enviromental\LINDA_fire.dm"
@@ -1874,6 +1944,8 @@
#include "code\modules\buildmode\submodes\save.dm"
#include "code\modules\buildmode\submodes\throwing.dm"
#include "code\modules\buildmode\submodes\variable_edit.dm"
+#include "code\modules\cargo\centcom_podlauncher.dm"
+#include "code\modules\cargo\supplypod.dm"
#include "code\modules\client\client_defines.dm"
#include "code\modules\client\client_procs.dm"
#include "code\modules\client\geoip.dm"
@@ -1881,6 +1953,7 @@
#include "code\modules\client\ping.dm"
#include "code\modules\client\view.dm"
#include "code\modules\client\preference\preferences.dm"
+#include "code\modules\client\preference\preference_info.dm"
#include "code\modules\client\preference\preferences_mysql.dm"
#include "code\modules\client\preference\preferences_spawnpoints.dm"
#include "code\modules\client\preference\preferences_toggles.dm"
@@ -1899,6 +1972,7 @@
#include "code\modules\client\preference\loadout\loadout_racial.dm"
#include "code\modules\client\preference\loadout\loadout_shoes.dm"
#include "code\modules\client\preference\loadout\loadout_suit.dm"
+#include "code\modules\client\preference\loadout\loadout_tgui.dm"
#include "code\modules\client\preference\loadout\loadout_uniform.dm"
#include "code\modules\clothing\clothing.dm"
#include "code\modules\clothing\chameleon\_chameleon_actions.dm"
@@ -2133,6 +2207,7 @@
#include "code\modules\games\cards.dm"
#include "code\modules\games\tarot.dm"
#include "code\modules\games\unum.dm"
+#include "code\modules\hallucination\_hallucination.dm"
#include "code\modules\holiday\christmas.dm"
#include "code\modules\holiday\holiday.dm"
#include "code\modules\holiday\new_year.dm"
@@ -2345,6 +2420,7 @@
#include "code\modules\mob\mob_defines.dm"
#include "code\modules\mob\mob_emote.dm"
#include "code\modules\mob\mob_helpers.dm"
+#include "code\modules\mob\mob_lists.dm"
#include "code\modules\mob\mob_movement.dm"
#include "code\modules\mob\mob_say.dm"
#include "code\modules\mob\mob_transformation_simple.dm"
@@ -2363,6 +2439,7 @@
#include "code\modules\mob\dead\observer\observer_say.dm"
#include "code\modules\mob\dead\observer\orbit.dm"
#include "code\modules\mob\dead\observer\spells.dm"
+#include "code\modules\mob\living\alpha.dm"
#include "code\modules\mob\living\autohiss.dm"
#include "code\modules\mob\living\damage_procs.dm"
#include "code\modules\mob\living\death.dm"
@@ -2599,6 +2676,8 @@
#include "code\modules\mob\living\simple_animal\friendly\snake.dm"
#include "code\modules\mob\living\simple_animal\friendly\snake_stripping.dm"
#include "code\modules\mob\living\simple_animal\friendly\spiderbot.dm"
+#include "code\modules\mob\living\simple_animal\gondolas\gondola.dm"
+#include "code\modules\mob\living\simple_animal\gondolas\gondolapod.dm"
#include "code\modules\mob\living\simple_animal\hostile\alien.dm"
#include "code\modules\mob\living\simple_animal\hostile\bat.dm"
#include "code\modules\mob\living\simple_animal\hostile\bear.dm"
@@ -2744,8 +2823,8 @@
#include "code\modules\mob\new_player\sprite_accessories\vulpkanin\vulpkanin_head_accessories.dm"
#include "code\modules\mob\new_player\sprite_accessories\vulpkanin\vulpkanin_head_markings.dm"
#include "code\modules\mob\new_player\sprite_accessories\vulpkanin\vulpkanin_tail_markings.dm"
-#include "code\modules\mob\new_player\sprite_accessories\wryn\wryn_hair.dm"
#include "code\modules\mob\new_player\sprite_accessories\wryn\wryn_facial_hair.dm"
+#include "code\modules\mob\new_player\sprite_accessories\wryn\wryn_hair.dm"
#include "code\modules\movespeed\_movespeed_modifier.dm"
#include "code\modules\movespeed\modifiers\components.dm"
#include "code\modules\movespeed\modifiers\innate.dm"
@@ -2896,6 +2975,7 @@
#include "code\modules\projectiles\projectile\force.dm"
#include "code\modules\projectiles\projectile\magic.dm"
#include "code\modules\projectiles\projectile\reusable.dm"
+#include "code\modules\projectiles\projectile\shrapnel.dm"
#include "code\modules\projectiles\projectile\special.dm"
#include "code\modules\projectiles\sibyl\sibyl_system_mod.dm"
#include "code\modules\projectiles\sibyl\sibyl_weapons.dm"
@@ -2914,7 +2994,6 @@
#include "code\modules\reagents\chemistry\machinery\reagentgrinder.dm"
#include "code\modules\reagents\chemistry\reagents\admin.dm"
#include "code\modules\reagents\chemistry\reagents\alcohol.dm"
-#include "code\modules\reagents\chemistry\reagents\blob.dm"
#include "code\modules\reagents\chemistry\reagents\disease.dm"
#include "code\modules\reagents\chemistry\reagents\drink_base.dm"
#include "code\modules\reagents\chemistry\reagents\drink_cold.dm"
@@ -3170,9 +3249,12 @@
#include "code\modules\tgui\states\strippable_state.dm"
#include "code\modules\tgui\states\zlevel.dm"
#include "code\modules\tgui\tgui_input\alert_input.dm"
+#include "code\modules\tgui\tgui_input\input_checkbox.dm"
+#include "code\modules\tgui\tgui_input\color_input.dm"
#include "code\modules\tgui\tgui_input\keycombo_input.dm"
#include "code\modules\tgui\tgui_input\list_input.dm"
#include "code\modules\tgui\tgui_input\number_input.dm"
+#include "code\modules\tgui\tgui_input\ranked_list_input.dm"
#include "code\modules\tgui\tgui_input\text_input.dm"
#include "code\modules\tgui\tgui_panel\audio.dm"
#include "code\modules\tgui\tgui_panel\telemetry.dm"
diff --git a/sound/effects/podwoosh.ogg b/sound/effects/podwoosh.ogg
new file mode 100644
index 00000000000..6edcba62737
Binary files /dev/null and b/sound/effects/podwoosh.ogg differ
diff --git a/sound/machines/generator/generator_end.ogg b/sound/machines/generator/generator_end.ogg
new file mode 100644
index 00000000000..2b2c97ee744
Binary files /dev/null and b/sound/machines/generator/generator_end.ogg differ
diff --git a/sound/machines/generator/generator_mid1.ogg b/sound/machines/generator/generator_mid1.ogg
new file mode 100644
index 00000000000..332b5af9a0e
Binary files /dev/null and b/sound/machines/generator/generator_mid1.ogg differ
diff --git a/sound/machines/generator/generator_mid2.ogg b/sound/machines/generator/generator_mid2.ogg
new file mode 100644
index 00000000000..d71c7b2ae0a
Binary files /dev/null and b/sound/machines/generator/generator_mid2.ogg differ
diff --git a/sound/machines/generator/generator_mid3.ogg b/sound/machines/generator/generator_mid3.ogg
new file mode 100644
index 00000000000..7ee161824d0
Binary files /dev/null and b/sound/machines/generator/generator_mid3.ogg differ
diff --git a/sound/machines/generator/generator_start.ogg b/sound/machines/generator/generator_start.ogg
new file mode 100644
index 00000000000..a9087bd3a7a
Binary files /dev/null and b/sound/machines/generator/generator_start.ogg differ
diff --git a/sound/weapons/gunshots/lasergatling.ogg b/sound/weapons/gunshots/lasergatling.ogg
new file mode 100644
index 00000000000..02504bf94ec
Binary files /dev/null and b/sound/weapons/gunshots/lasergatling.ogg differ
diff --git a/sound/weapons/mortar_long_whistle.ogg b/sound/weapons/mortar_long_whistle.ogg
new file mode 100644
index 00000000000..646d37d8ab6
Binary files /dev/null and b/sound/weapons/mortar_long_whistle.ogg differ
diff --git a/sound/weapons/mortar_whistle.ogg b/sound/weapons/mortar_whistle.ogg
new file mode 100644
index 00000000000..2d7e19d85da
Binary files /dev/null and b/sound/weapons/mortar_whistle.ogg differ
diff --git a/tgui/docs/component-reference.md b/tgui/docs/component-reference.md
index af744b61793..c02ec546d68 100644
--- a/tgui/docs/component-reference.md
+++ b/tgui/docs/component-reference.md
@@ -31,6 +31,7 @@ Make sure to add new items to this list if you document new components.
- [`Icon.Stack`](#iconstack)
- [`ImageButton`](#imagebutton)
- [`ImageButton.Item`](#imagebuttonitem)
+ - [`ImageButtonTS`](#imagebuttonts)
- [`Input`](#input)
- [`Knob`](#knob)
- [`LabeledControls`](#labeledcontrols)
@@ -70,16 +71,16 @@ Event handlers are callbacks that you can attack to various element to
listen for browser events. Inferno supports camelcase (`onClick`) and
lowercase (`onclick`) event names.
-- Camel case names are what's called *synthetic* events, and are the
-**preferred way** of handling events in React, for efficiency and
-performance reasons. Please read
-[Inferno Event Handling](https://infernojs.org/docs/guides/event-handling)
-to understand what this is about.
+- Camel case names are what's called _synthetic_ events, and are the
+ **preferred way** of handling events in React, for efficiency and
+ performance reasons. Please read
+ [Inferno Event Handling](https://infernojs.org/docs/guides/event-handling)
+ to understand what this is about.
- Lower case names are native browser events and should be used sparingly,
-for example when you need an explicit IE8 support. **DO NOT** use
-lowercase event handlers unless you really know what you are doing.
+ for example when you need an explicit IE8 support. **DO NOT** use
+ lowercase event handlers unless you really know what you are doing.
- [Button](#button) component does not support the lowercase `onclick` event.
-Use the camel case `onClick` instead.
+ Use the camel case `onClick` instead.
## `tgui/components`
@@ -91,13 +92,13 @@ This component provides animations for numeric values.
- `value: number` - Value to animate.
- `initial: number` - Initial value to use in animation when element
-first appears. If you set initial to `0` for example, number will always
-animate starting from `0`, and if omitted, it will not play an initial
-animation.
+ first appears. If you set initial to `0` for example, number will always
+ animate starting from `0`, and if omitted, it will not play an initial
+ animation.
- `format: value => value` - Output formatter.
- Example: `value => Math.round(value)`.
- `children: (formattedValue, rawValue) => any` - Pull the animated number to
-animate more complex things deeper in the DOM tree.
+ animate more complex things deeper in the DOM tree.
- Example: `(_, value) => `
### `BlockQuote`
@@ -133,9 +134,7 @@ To workaround this problem, the Box children accept a render props function.
This way, `Button` can pull out the `className` generated by the `Box`.
```jsx
-
- {props => }
-
+{(props) => }
```
**Box Units**
@@ -166,10 +165,10 @@ Default font size (`1rem`) is equal to `12px`.
- `fontSize: number` - Font size.
- `fontFamily: string` - Font family.
- `lineHeight: number` - Directly affects the height of text lines.
-Useful for adjusting button height.
+ Useful for adjusting button height.
- `inline: boolean` - Forces the `Box` to appear as an `inline-block`,
-or in other words, makes the `Box` flow with the text instead of taking
-all available horizontal space.
+ or in other words, makes the `Box` flow with the text instead of taking
+ all available horizontal space.
- `m: number` - Margin on all sides.
- `mx: number` - Horizontal margin.
- `my: number` - Vertical margin.
@@ -201,7 +200,7 @@ all available horizontal space.
- `#ffffff` - Hex format
- `rgba(255, 255, 255, 1)` - RGB format
- `purple` - Applies an atomic `color-` class to the element.
- See `styles/color-map.scss`.
+ See `styles/color-map.scss`.
- `backgroundColor: string` - Sets background color.
- `#ffffff` - Hex format
- `rgba(255, 255, 255, 1)` - RGB format
@@ -217,17 +216,17 @@ Buttons allow users to take actions, and make choices, with a single click.
- `icon: string` - Adds an icon to the button.
- `color: string` - Button color, as defined in `variables.scss`.
- There is also a special color `transparent` - makes the button
- transparent and slightly dim when inactive.
+ transparent and slightly dim when inactive.
- `disabled: boolean` - Disables and greys out the button.
- `selected: boolean` - Activates the button (gives it a green color).
- `tooltip: string` - A fancy, boxy tooltip, which appears when hovering
-over the button.
+ over the button.
- `tooltipPosition?: string` - Position of the tooltip. See [`Popper`](#Popper) for valid options.
- `ellipsis: boolean` - If button width is constrained, button text will
-be truncated with an ellipsis. Be careful however, because this prop breaks
-the baseline alignment.
+ be truncated with an ellipsis. Be careful however, because this prop breaks
+ the baseline alignment.
- `title: string` - A native browser tooltip, which appears when hovering
-over the button.
+ over the button.
- `children: any` - Content to render inside the button.
- `onClick: function` - Called when element is clicked.
@@ -261,11 +260,11 @@ commit, while escape cancels.
- See inherited props: [Box](#box)
- `fluid`: fill available horizontal space
- `onCommit: (e, value) => void`: function that is called after the user
-defocuses the input or presses enter
+ defocuses the input or presses enter
- `currentValue: string`: default string to display when the input is shown
- `defaultValue: string`: default value emitted if the user leaves the box
-blank when hitting enter or defocusing. If left undefined, will cancel the
-change on a blank defocus/enter
+ blank when hitting enter or defocusing. If left undefined, will cancel the
+ change on a blank defocus/enter
### `ByondUi`
@@ -302,8 +301,8 @@ It supports a full set of `Box` properties for layout purposes.
- See inherited props: [Box](#box)
- `params: any` - An object with parameters, which are directly passed to
-the `winset` proc call. You can find a full reference of these parameters
-in [BYOND controls and parameters guide](https://secure.byond.com/docs/ref/skinparams.html).
+ the `winset` proc call. You can find a full reference of these parameters
+ in [BYOND controls and parameters guide](https://secure.byond.com/docs/ref/skinparams.html).
### `Collapsible`
@@ -350,7 +349,7 @@ Works like the good old `` element, but it's fancier.
- `vertical: boolean` - Divide content vertically.
- `hidden: boolean` - Divider can divide content without creating a dividing
-line.
+ line.
### `Dropdown`
@@ -362,7 +361,7 @@ and displays selected entry.
- See inherited props: [Box](#box)
- See inherited props: [Icon](#icon)
- `options: string[] | DropdownEntry[]` - An array of strings which will be displayed in the
-dropdown when open. See Dropdown.tsx for more adcanced usage with DropdownEntry
+ dropdown when open. See Dropdown.tsx for more adcanced usage with DropdownEntry
- `selected: any` - Currently selected entry
- `width: string` - Width of dropdown button and resulting menu; css width value
- `over: boolean` - Dropdown renders over instead of below
@@ -388,13 +387,9 @@ to the left, and certain elements to the right:
```jsx
-
- Button description
-
+ Button description
-
+
```
@@ -407,17 +402,17 @@ effectively places the last flex item to the very end of the flex container.
- See inherited props: [Box](#box)
- ~~`spacing: number`~~ - **Removed in tgui 4.3**,
-use [Stack](#stack) instead.
+ use [Stack](#stack) instead.
- `inline: boolean` - Makes flexbox container inline, with similar behavior
-to an `inline` property on a `Box`.
+ to an `inline` property on a `Box`.
- `direction: string` - This establishes the main-axis, thus defining the
-direction flex items are placed in the flex container.
+ direction flex items are placed in the flex container.
- `row` (default) - left to right.
- `row-reverse` - right to left.
- `column` - top to bottom.
- `column-reverse` - bottom to top.
- `wrap: string` - By default, flex items will all try to fit onto one line.
-You can change that and allow the items to wrap as needed with this property.
+ You can change that and allow the items to wrap as needed with this property.
- `nowrap` (default) - all flex items will be on one line
- `wrap` - flex items will wrap onto multiple lines, from top to bottom.
- `wrap-reverse` - flex items will wrap onto multiple lines from bottom to top.
@@ -428,22 +423,22 @@ You can change that and allow the items to wrap as needed with this property.
- `center` - items are centered on the cross axis.
- `baseline` - items are aligned such as their baselines align.
- `justify: string` - This defines the alignment along the main axis.
-It helps distribute extra free space leftover when either all the flex
-items on a line are inflexible, or are flexible but have reached their
-maximum size. It also exerts some control over the alignment of items
-when they overflow the line.
+ It helps distribute extra free space leftover when either all the flex
+ items on a line are inflexible, or are flexible but have reached their
+ maximum size. It also exerts some control over the alignment of items
+ when they overflow the line.
- `flex-start` (default) - items are packed toward the start of the
- flex-direction.
+ flex-direction.
- `flex-end` - items are packed toward the end of the flex-direction.
- `space-between` - items are evenly distributed in the line; first item is
- on the start line, last item on the end line
+ on the start line, last item on the end line
- `space-around` - items are evenly distributed in the line with equal space
- around them. Note that visually the spaces aren't equal, since all the items
- have equal space on both sides. The first item will have one unit of space
- against the container edge, but two units of space between the next item
- because that next item has its own spacing that applies.
+ around them. Note that visually the spaces aren't equal, since all the items
+ have equal space on both sides. The first item will have one unit of space
+ against the container edge, but two units of space between the next item
+ because that next item has its own spacing that applies.
- `space-evenly` - items are distributed so that the spacing between any two
- items (and the space to the edges) is equal.
+ items (and the space to the edges) is equal.
- TBD (not all properties are supported in IE11).
### `Flex.Item`
@@ -452,24 +447,24 @@ when they overflow the line.
- See inherited props: [Box](#box)
- `order: number` - By default, flex items are laid out in the source order.
-However, the order property controls the order in which they appear in the
-flex container.
+ However, the order property controls the order in which they appear in the
+ flex container.
- `grow: number | boolean` - This defines the ability for a flex item to grow
-if necessary. It accepts a unitless value that serves as a proportion. It
-dictates what amount of the available space inside the flex container the
-item should take up. This number is unit-less and is relative to other
-siblings.
+ if necessary. It accepts a unitless value that serves as a proportion. It
+ dictates what amount of the available space inside the flex container the
+ item should take up. This number is unit-less and is relative to other
+ siblings.
- `shrink: number | boolean` - This defines the ability for a flex item to
-shrink if necessary. Inverse of `grow`.
+ shrink if necessary. Inverse of `grow`.
- `basis: number | string` - This defines the default size of an element
-before any flex-related calculations are done. Has to be a length
-(e.g. `20%`, `5rem`), an `auto` or `content` keyword.
+ before any flex-related calculations are done. Has to be a length
+ (e.g. `20%`, `5rem`), an `auto` or `content` keyword.
- **Important:** IE11 flex is buggy, and auto width/height calculations
- can sometimes end up in a circular dependency. This usually happens, when
- working with tables inside flex (they have wacky internal widths and such).
- Setting basis to `0` breaks the loop and fixes all of the problems.
+ can sometimes end up in a circular dependency. This usually happens, when
+ working with tables inside flex (they have wacky internal widths and such).
+ Setting basis to `0` breaks the loop and fixes all of the problems.
- `align: string` - This allows the default alignment (or the one specified by
-align-items) to be overridden for individual flex items. See: [Flex](#flex).
+ align-items) to be overridden for individual flex items. See: [Flex](#flex).
### `Grid`
@@ -485,14 +480,10 @@ Example:
```jsx
-
- Hello world!
-
+ Hello world!
-
- Hello world!
-
+ Hello world!
```
@@ -518,6 +509,7 @@ Renders one of the FontAwesome icons of your choice.
To smoothen the transition from v4 to v5, we have added a v4 semantic to
transform names with `-o` suffixes to FA Regular icons. For example:
+
- `square` will get transformed to `fas square`
- `square-o` will get transformed to `far square`
@@ -526,10 +518,10 @@ transform names with `-o` suffixes to FA Regular icons. For example:
- See inherited props: [Box](#box)
- `name: string` - Icon name.
- `size: number` - Icon size. `1` is normal size, `2` is two times bigger.
-Fractional numbers are supported.
+ Fractional numbers are supported.
- `rotation: number` - Icon rotation, in degrees.
- `spin: boolean` - Whether an icon should be spinning. Good for load
-indicators.
+ indicators.
### `Icon.Stack`
@@ -558,26 +550,26 @@ Has support for base64, spritesheets and URLs.
- `asset: boolean` - Enables spritesheets support.
- `vertical: boolean` - Makes the button a inlined vertical rectangle.
- `color: string` - By default, the button is semi-transparent. You can change the overall colour,
-all colours are available in KitchenSink in the corresponding section.
+ all colours are available in KitchenSink in the corresponding section.
- `title: string` - The top text, it will always be bold, and also adds a divider between title and content.
-Disabled if there is no content.
+ Disabled if there is no content.
- `content: string|any` - All main content, usually text, but you can put in other components if you like.
-Makes the vertical button square if empty.
+ Makes the vertical button square if empty.
- `selected: boolean` - Makes button selected (green) if true.
- `disabled: boolean` - Makes button disabled (red) if true. Also disables onClick.
- `disabledContent: string` - If button disabled and disabledContent filled, it will be used instead content.
- `image: string` - Base64 image, simple. Disabled if asset support enabled.
- `imageUrl: string` - PNG image or other asset. Make sure you use existing simple asset! Example: imageUrl={'image.png'}
- `imageAsset: string` - If you have enabled asset support, write here which spritesheet to use.
-Example: imageAsset={'spritesheet_name64x64'}
+ Example: imageAsset={'spritesheet_name64x64'}
- `imageSize: string` - Sets the size of the image and adjusts the size of the button itself accordingly.
-Example: imageSize={'64px'}
+ Example: imageSize={'64px'}
- `tooltip: string` - A fancy, boxy tooltip, which appears when hovering
-over the button.
+ over the button.
- `tooltipPosition: string` - Position of the tooltip. See [`Popper`](#Popper) for valid options.
- `ellipsis: boolean` - If button width is constrained, button text will
-be truncated with an ellipsis. Be careful however, because this prop breaks
-the baseline alignment.
+ be truncated with an ellipsis. Be careful however, because this prop breaks
+ the baseline alignment.
- `children: ImageButton.Item|any` - Items that are added to the right of the horizontal button.
- `onClick: function` - Called when element is clicked. Also enables hover effects.
@@ -589,25 +581,60 @@ Additional button/s for ImageButton.
> Available only in horizontal mode, if you try add it to vertical, you're gonna be disappointed
**Props:**
+
- See inherited props: [Box](#box)
- `color: string` - By default, the button is semi-transparent. You can change the overall colour,
-all colours are available in KitchenSink in the corresponding section.
+ all colours are available in KitchenSink in the corresponding section.
- `content: string|any` - All main content, usually text, but you can put in other components if you like.
-Try to not make it too long.
+ Try to not make it too long.
- `selected: boolean` - Makes button selected (green) if true.
- `disabled: boolean` - Makes button disabled (red) if true. Also disables onClick.
- `disabledContent: string` - If button disabled and disabledContent filled, it will be used instead content.
- `tooltip: string` - A fancy, boxy tooltip, which appears when hovering
-over the button.
+ over the button.
- `tooltipPosition: string` - Position of the tooltip. See [`Popper`](#Popper) for valid options.
- `icon: string` - Adds an icon to the button. By default it will be under content.
- `iconColor: string` - Paints icon if it used.
- `iconPosition: string` - You can make an icon above the content.
-Example: iconPosition={'top'}
+ Example: iconPosition={'top'}
- `iconSize: number` - Adjusts the size of the icon.
- `children: any` - Similar to content.
- `onClick: function` - Called when element is clicked.
+### `ImageButtonTS`
+
+A Robust button is specifically for sticking a picture in it.
+
+**Props:**
+
+- See inherited props: [Box](#box)
+- `asset: string[]` - Asset cache. Example: `asset={`assetname32x32, ${thing.key}`}`
+- `base64: string` - Classic way to put images. Example: `base64={thing.image}`
+- `buttons: any` - Special section for any component, or, content.
+ Quite a small area at the bottom of the image in non-fluid mode.
+ Has a style overrides, best to use [Button](#button) inside.
+- `buttonsAlt: boolean` - Enables alternative buttons layout.
+ With fluid, makes buttons like a humburger.
+ Without, moves it to top, and disables pointer-events.
+- `children: any` - Content under image.
+- `className: string` - Applies a CSS class to the element.
+- `color: string` - Color of the button, but without `transparent`; see [Button](#button)
+- `disabled: boolean` - Makes button disabled and dark red if true.
+ Also disables onClick & onRightClick.
+- `selected: boolean` - Makes button selected and green if true.
+- `dmFallback: any` - Optional. Adds a "stub" when loading DmIcon.
+- `dmIcon: string` - Parameter `icon` of component `DmIcon`.
+- `dmIconState: string` - Parameter `icon_state` of component `DmIcon`.
+ For proper work of `DmIcon` it is necessary that both parameters are filled in!
+- `fluid: boolean` - Changes the layout of the button, making it fill the entire horizontally available space.
+ Allows the use of `title`
+- `imageSize: number` - Parameter responsible for the size of the image, component and standard "stubs".
+ Measured in pixels. `imageSize={64}` = 64px.
+- `imageSrc: string` - Prop `src` of . Example: `imageSrc={resolveAsset(thing.image)}`
+- `onClick: (e) => void` - Called when button is clicked with LMB.
+- `onRightClick: (e) => void` - Called when button is clicked with RMB.
+- `title: string` - Requires `fluid` for work. Bold text with divider betwen content.
+
### `Input`
A basic text input, which allow users to enter text into a UI.
@@ -620,12 +647,12 @@ A basic text input, which allow users to enter text into a UI.
- See inherited props: [Box](#box)
- `value: string` - Value of an input.
- `placeholder: string` - Text placed into Input box when it's empty,
-otherwise nothing. Clears automatically when focused.
+ otherwise nothing. Clears automatically when focused.
- `fluid: boolean` - Fill all available horizontal space.
- `selfClear: boolean` - Clear after hitting enter, as well as remain focused
-when this happens. Useful for things like chat inputs.
+ when this happens. Useful for things like chat inputs.
- `onChange: (e, value) => void` - An event, which fires when you commit
-the text by either unfocusing the input box, or by pressing the Enter key.
+ the text by either unfocusing the input box, or by pressing the Enter key.
- `onInput: (e, value) => void` - An event, which fires on every keypress.
### `Knob`
@@ -641,30 +668,30 @@ Single click opens an input box to manually type in a number.
- `animated: boolean` - Animates the value if it was changed externally.
- `bipolar: boolean` - Knob can be bipolar or unipolar.
- `size: number` - Relative size of the knob. `1` is normal size, `2` is two
-times bigger. Fractional numbers are supported.
+ times bigger. Fractional numbers are supported.
- `color: string` - Color of the outer ring around the knob.
- `value: number` - Value itself, controls the position of the cursor.
- `unit: string` - Unit to display to the right of value.
- `minValue: number` - Lowest possible value.
- `maxValue: number` - Highest possible value.
- `fillValue: number` - If set, this value will be used to set the fill
-percentage of the outer ring independently of the main value.
+ percentage of the outer ring independently of the main value.
- `ranges: { color: [from, to] }` - Applies a `color` to the outer ring around
-the knob based on whether the value lands in the range between `from` and `to`.
-See an example of this prop in [ProgressBar](#progressbar).
+ the knob based on whether the value lands in the range between `from` and `to`.
+ See an example of this prop in [ProgressBar](#progressbar).
- `step: number` (default: 1) - Adjust value by this amount when
-dragging the input.
+ dragging the input.
- `stepPixelSize: number` (default: 1) - Screen distance mouse needs
-to travel to adjust value by one `step`.
+ to travel to adjust value by one `step`.
- `format: value => value` - Format value using this function before
-displaying it.
+ displaying it.
- `suppressFlicker: number` - A number in milliseconds, for which the input
-will hold off from updating while events propagate through the backend.
-Default is about 250ms, increase it if you still see flickering.
+ will hold off from updating while events propagate through the backend.
+ Default is about 250ms, increase it if you still see flickering.
- `onChange: (e, value) => void` - An event, which fires when you release
-the input, or successfully enter a number.
+ the input, or successfully enter a number.
- `onDrag: (e, value) => void` - An event, which fires about every 500ms
-when you drag the input up and down, on release and on manual editing.
+ when you drag the input up and down, on release and on manual editing.
### `Popper`
@@ -702,9 +729,7 @@ column is labels, and second column is content.
```jsx
-
- Content
-
+ Content
```
@@ -713,13 +738,7 @@ to perform some sort of action), there is a way to do that:
```jsx
-
- Click me!
-
- )}>
+ Click me!}>
Content
@@ -746,9 +765,7 @@ Example:
```jsx
-
- Content
-
+ Content
```
@@ -794,22 +811,22 @@ to fine tune the value, or single click it to manually type a number.
- `minValue: number` - Lowest possible value.
- `maxValue: number` - Highest possible value.
- `step: number` (default: 1) - Adjust value by this amount when
-dragging the input.
+ dragging the input.
- `stepPixelSize: number` (default: 1) - Screen distance mouse needs
-to travel to adjust value by one `step`.
+ to travel to adjust value by one `step`.
- `width: string|number` - Width of the element, in `Box` units or pixels.
- `height: string|numer` - Height of the element, in `Box` units or pixels.
- `lineHeight: string|number` - lineHeight of the element, in `Box` units or pixels.
- `fontSize: string|number` - fontSize of the element, in `Box` units or pixels.
- `format: value => value` - Format value using this function before
-displaying it.
+ displaying it.
- `suppressFlicker: number` - A number in milliseconds, for which the input
-will hold off from updating while events propagate through the backend.
-Default is about 250ms, increase it if you still see flickering.
+ will hold off from updating while events propagate through the backend.
+ Default is about 250ms, increase it if you still see flickering.
- `onChange: (e, value) => void` - An event, which fires when you release
-the input, or successfully enter a number.
+ the input, or successfully enter a number.
- `onDrag: (e, value) => void` - An event, which fires about every 500ms
-when you drag the input up and down, on release and on manual editing.
+ when you drag the input up and down, on release and on manual editing.
### `ProgressBar`
@@ -828,18 +845,19 @@ Usage of `ranges` prop:
average: [0.25, 0.5],
bad: [-Infinity, 0.25],
}}
- value={0.6} />
+ value={0.6}
+/>
```
**Props:**
- `value: number` - Current progress as a floating point number between
-`minValue` (default: 0) and `maxValue` (default: 1). Determines the
-percentage and how filled the bar is.
+ `minValue` (default: 0) and `maxValue` (default: 1). Determines the
+ percentage and how filled the bar is.
- `minValue: number` - Lowest possible value.
- `maxValue: number` - Highest possible value.
- `ranges: { color: [from, to] }` - Applies a `color` to the progress bar
-based on whether the value lands in the range between `from` and `to`.
+ based on whether the value lands in the range between `from` and `to`.
- `color: string` - Color of the progress bar.
- `children: any` - Content to render inside the progress bar.
@@ -853,13 +871,14 @@ The RoundGauge component provides a visual representation of a single metric, as
value={tankPressure}
minValue={0}
maxValue={pressureLimit}
- alertAfter={pressureLimit * 0.70}
+ alertAfter={pressureLimit * 0.7}
ranges={{
- "good": [0, pressureLimit * 0.70],
- "average": [pressureLimit * 0.70, pressureLimit * 0.85],
- "bad": [pressureLimit * 0.85, pressureLimit],
+ 'good': [0, pressureLimit * 0.7],
+ 'average': [pressureLimit * 0.7, pressureLimit * 0.85],
+ 'bad': [pressureLimit * 0.85, pressureLimit],
}}
- format={formatPressure} />
+ format={formatPressure}
+/>
```
The alert on the gauge is optional, and will only be shown if the `alertAfter` prop is defined. When defined, the alert will begin to flash the respective color upon which the needle currently rests, as defined in the `ranges` prop.
@@ -886,22 +905,14 @@ clearly indicates hierarchy.
Section can also be titled to clearly define its purpose.
```jsx
-
- Here you can order supply crates.
-
+Here you can order supply crates.
```
If you want to have a button on the right side of an section title
(for example, to perform some sort of action), there is a way to do that:
```jsx
-
- Send shuttle
-
- )}>
+Send shuttle}>
Here you can order supply crates.
```
@@ -909,7 +920,7 @@ If you want to have a button on the right side of an section title
- See inherited props: [Box](#box)
- `title: string` - Title of the section.
- `level: number` - Section level in hierarchy. Default is 1, higher number
-means deeper level of nesting. Must be an integer number.
+ means deeper level of nesting. Must be an integer number.
- `buttons: any` - Buttons to render aside the section title.
- `fill: boolean` - If true, fills all available vertical space.
- `fitted: boolean` - If true, removes all section padding.
@@ -933,23 +944,23 @@ Single click opens an input box to manually type in a number.
- `minValue: number` - Lowest possible value.
- `maxValue: number` - Highest possible value.
- `fillValue: number` - If set, this value will be used to set the fill
-percentage of the progress bar filler independently of the main value.
+ percentage of the progress bar filler independently of the main value.
- `ranges: { color: [from, to] }` - Applies a `color` to the slider
-based on whether the value lands in the range between `from` and `to`.
-See an example of this prop in [ProgressBar](#progressbar).
+ based on whether the value lands in the range between `from` and `to`.
+ See an example of this prop in [ProgressBar](#progressbar).
- `step: number` (default: 1) - Adjust value by this amount when
-dragging the input.
+ dragging the input.
- `stepPixelSize: number` (default: 1) - Screen distance mouse needs
-to travel to adjust value by one `step`.
+ to travel to adjust value by one `step`.
- `format: value => value` - Format value using this function before
-displaying it.
+ displaying it.
- `suppressFlicker: number` - A number in milliseconds, for which the input
-will hold off from updating while events propagate through the backend.
-Default is about 250ms, increase it if you still see flickering.
+ will hold off from updating while events propagate through the backend.
+ Default is about 250ms, increase it if you still see flickering.
- `onChange: (e, value) => void` - An event, which fires when you release
-the input, or successfully enter a number.
+ the input, or successfully enter a number.
- `onDrag: (e, value) => void` - An event, which fires about every 500ms
-when you drag the input up and down, on release and on manual editing.
+ when you drag the input up and down, on release and on manual editing.
### `Stack`
@@ -965,13 +976,9 @@ Stacks can be vertical by adding a `vertical` property.
```jsx
-
- Button description
-
+ Button description
-
+
```
@@ -986,9 +993,7 @@ Make sure to use the `fill` property.
-
- Sidebar
-
+ Sidebar
@@ -998,9 +1003,7 @@ Make sure to use the `fill` property.
-
- Bottom pane
-
+ Bottom pane
@@ -1032,9 +1035,7 @@ Example:
```jsx
-
- Hello world!
-
+ Hello world!
Label
@@ -1063,7 +1064,7 @@ A straight forward mapping to `
` element.
- See inherited props: [Box](#box)
- `collapsing: boolean` - Collapses table cell to the smallest possible size,
-and stops any text inside from wrapping.
+ and stops any text inside from wrapping.
### `Tabs`
@@ -1099,9 +1100,7 @@ Tabs also support a vertical configuration. This is usually paired with a
```jsx
-
- ...
-
+ ...
Tab content.
@@ -1113,7 +1112,7 @@ Tabs also support a vertical configuration. This is usually paired with a
- See inherited props: [Box](#box)
- `vertical: boolean` - Use a vertical configuration, where tabs will be
-stacked vertically.
+ stacked vertically.
- `children: Tab[]` - This component only accepts tabs as its children.
### `Tabs.Tab`
@@ -1125,8 +1124,8 @@ a lot of `Button` props.
- See inherited props: [Button](#button)
- `altSelection` - Whether the tab buttons select via standard select (color
-change) or by adding a white indicator to the selected tab.
-Intended for usage on interfaces where tab color has relevance.
+ change) or by adding a white indicator to the selected tab.
+ Intended for usage on interfaces where tab color has relevance.
- `icon: string` - Tab icon.
- `children: any` - Tab text.
- `onClick: function` - Called when element is clicked.
@@ -1143,9 +1142,7 @@ Usage:
```jsx
-
- Sample text.
-
+ Sample text.
```
@@ -1153,7 +1150,7 @@ Usage:
- `position?: string` - Tooltip position. See [`Popper`](#Popper) for valid options. Defaults to "auto".
- `content: string` - Content of the tooltip. Must be a plain string.
-Fragments or other elements are **not** supported.
+ Fragments or other elements are **not** supported.
## `tgui/layouts`
@@ -1167,9 +1164,7 @@ Example:
```jsx
-
- Hello, world!
-
+ Hello, world!
```
@@ -1184,9 +1179,9 @@ Example:
- `height: number` - Window height.
- `noClose: boolean` - Controls the ability to close the window.
- `children: any` - Child elements, which are rendered directly inside the
-window. If you use a [Dimmer](#dimmer) or [Modal](#modal) in your UI,
-they should be put as direct childs of a Window, otherwise you should be
-putting your content into [Window.Content](#windowcontent).
+ window. If you use a [Dimmer](#dimmer) or [Modal](#modal) in your UI,
+ they should be put as direct childs of a Window, otherwise you should be
+ putting your content into [Window.Content](#windowcontent).
### `Window.Content`
diff --git a/tgui/global.d.ts b/tgui/global.d.ts
index 213a04b0fc2..225d1a9dd39 100644
--- a/tgui/global.d.ts
+++ b/tgui/global.d.ts
@@ -174,6 +174,11 @@ type ByondType = {
* Loads a script into the document.
*/
loadJs(url: string): void;
+
+ /**
+ * Maps icons to their ref
+ */
+ iconRefMap: Record;
};
/**
diff --git a/tgui/packages/common/color.js b/tgui/packages/common/color.js
deleted file mode 100644
index 913f50747af..00000000000
--- a/tgui/packages/common/color.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-const EPSILON = 0.0001;
-
-export class Color {
- constructor(r = 0, g = 0, b = 0, a = 1) {
- this.r = r;
- this.g = g;
- this.b = b;
- this.a = a;
- }
-
- toString() {
- return `rgba(${this.r | 0}, ${this.g | 0}, ${this.b | 0}, ${this.a | 0})`;
- }
-}
-
-/**
- * Creates a color from the CSS hex color notation.
- */
-Color.fromHex = (hex) =>
- new Color(
- parseInt(hex.substr(1, 2), 16),
- parseInt(hex.substr(3, 2), 16),
- parseInt(hex.substr(5, 2), 16)
- );
-
-/**
- * Linear interpolation of two colors.
- */
-Color.lerp = (c1, c2, n) =>
- new Color(
- (c2.r - c1.r) * n + c1.r,
- (c2.g - c1.g) * n + c1.g,
- (c2.b - c1.b) * n + c1.b,
- (c2.a - c1.a) * n + c1.a
- );
-
-/**
- * Loops up the color in the provided list of colors
- * with linear interpolation.
- */
-Color.lookup = (value, colors = []) => {
- const len = colors.length;
- if (len < 2) {
- throw new Error('Needs at least two colors!');
- }
- const scaled = value * (len - 1);
- if (value < EPSILON) {
- return colors[0];
- }
- if (value >= 1 - EPSILON) {
- return colors[len - 1];
- }
- const ratio = scaled % 1;
- const index = scaled | 0;
- return Color.lerp(colors[index], colors[index + 1], ratio);
-};
diff --git a/tgui/packages/common/color.ts b/tgui/packages/common/color.ts
new file mode 100644
index 00000000000..9022cccfdc2
--- /dev/null
+++ b/tgui/packages/common/color.ts
@@ -0,0 +1,359 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+const EPSILON = 0.0001;
+
+export class Color {
+ r: number;
+ g: number;
+ b: number;
+ a: number;
+
+ constructor(r = 0, g = 0, b = 0, a = 1) {
+ this.r = r;
+ this.g = g;
+ this.b = b;
+ this.a = a;
+ }
+
+ toString() {
+ return `rgba(${this.r | 0}, ${this.g | 0}, ${this.b | 0}, ${this.a | 0})`;
+ }
+
+ /**
+ * Creates a color from the CSS hex color notation.
+ */
+ static fromHex(hex: string): Color {
+ return new Color(
+ parseInt(hex.substr(1, 2), 16),
+ parseInt(hex.substr(3, 2), 16),
+ parseInt(hex.substr(5, 2), 16)
+ );
+ }
+
+ /**
+ * Linear interpolation of two colors.
+ */
+ static lerp(c1: Color, c2: Color, n: number): Color {
+ return new Color(
+ (c2.r - c1.r) * n + c1.r,
+ (c2.g - c1.g) * n + c1.g,
+ (c2.b - c1.b) * n + c1.b,
+ (c2.a - c1.a) * n + c1.a
+ );
+ }
+
+ /**
+ * Loops up the color in the provided list of colors
+ * with linear interpolation.
+ */
+ static lookup(value: number, colors: Color[] = []): Color {
+ const len = colors.length;
+ if (len < 2) {
+ throw new Error('Needs at least two colors!');
+ }
+ const scaled = value * (len - 1);
+ if (value < EPSILON) {
+ return colors[0];
+ }
+ if (value >= 1 - EPSILON) {
+ return colors[len - 1];
+ }
+ const ratio = scaled % 1;
+ const index = scaled | 0;
+ return Color.lerp(colors[index], colors[index + 1], ratio);
+ }
+}
+
+/*
+ * MIT License
+ * https://github.com/omgovich/react-colorful/
+ *
+ * Copyright (c) 2020 Vlad Shilov
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+const round = (
+ number: number,
+ digits = 0,
+ base = Math.pow(10, digits)
+): number => {
+ return Math.round(base * number) / base;
+};
+
+export interface RgbColor {
+ r: number;
+ g: number;
+ b: number;
+}
+
+export interface RgbaColor extends RgbColor {
+ a: number;
+}
+
+export interface HslColor {
+ h: number;
+ s: number;
+ l: number;
+}
+
+export interface HslaColor extends HslColor {
+ a: number;
+}
+
+export interface HsvColor {
+ h: number;
+ s: number;
+ v: number;
+}
+
+export interface HsvaColor extends HsvColor {
+ a: number;
+}
+
+export type ObjectColor =
+ | RgbColor
+ | HslColor
+ | HsvColor
+ | RgbaColor
+ | HslaColor
+ | HsvaColor;
+
+export type AnyColor = string | ObjectColor;
+
+/**
+ * Valid CSS units.
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/angle
+ */
+const angleUnits: Record = {
+ grad: 360 / 400,
+ turn: 360,
+ rad: 360 / (Math.PI * 2),
+};
+
+export const hexToHsva = (hex: string): HsvaColor => rgbaToHsva(hexToRgba(hex));
+
+export const hexToRgba = (hex: string): RgbaColor => {
+ if (hex[0] === '#') hex = hex.substring(1);
+
+ if (hex.length < 6) {
+ return {
+ r: parseInt(hex[0] + hex[0], 16),
+ g: parseInt(hex[1] + hex[1], 16),
+ b: parseInt(hex[2] + hex[2], 16),
+ a: hex.length === 4 ? round(parseInt(hex[3] + hex[3], 16) / 255, 2) : 1,
+ };
+ }
+
+ return {
+ r: parseInt(hex.substring(0, 2), 16),
+ g: parseInt(hex.substring(2, 4), 16),
+ b: parseInt(hex.substring(4, 6), 16),
+ a: hex.length === 8 ? round(parseInt(hex.substring(6, 8), 16) / 255, 2) : 1,
+ };
+};
+
+export const parseHue = (value: string, unit = 'deg'): number => {
+ return Number(value) * (angleUnits[unit] || 1);
+};
+
+export const hslaStringToHsva = (hslString: string): HsvaColor => {
+ const matcher =
+ /hsla?\(?\s*(-?\d*\.?\d+)(deg|rad|grad|turn)?[,\s]+(-?\d*\.?\d+)%?[,\s]+(-?\d*\.?\d+)%?,?\s*[/\s]*(-?\d*\.?\d+)?(%)?\s*\)?/i;
+ const match = matcher.exec(hslString);
+
+ if (!match) return { h: 0, s: 0, v: 0, a: 1 };
+
+ return hslaToHsva({
+ h: parseHue(match[1], match[2]),
+ s: Number(match[3]),
+ l: Number(match[4]),
+ a: match[5] === undefined ? 1 : Number(match[5]) / (match[6] ? 100 : 1),
+ });
+};
+
+export const hslStringToHsva = hslaStringToHsva;
+
+export const hslaToHsva = ({ h, s, l, a }: HslaColor): HsvaColor => {
+ s *= (l < 50 ? l : 100 - l) / 100;
+
+ return {
+ h: h,
+ s: s > 0 ? ((2 * s) / (l + s)) * 100 : 0,
+ v: l + s,
+ a,
+ };
+};
+
+export const hsvaToHex = (hsva: HsvaColor): string =>
+ rgbaToHex(hsvaToRgba(hsva));
+
+export const hsvaToHsla = ({ h, s, v, a }: HsvaColor): HslaColor => {
+ const hh = ((200 - s) * v) / 100;
+
+ return {
+ h: round(h),
+ s: round(
+ hh > 0 && hh < 200
+ ? ((s * v) / 100 / (hh <= 100 ? hh : 200 - hh)) * 100
+ : 0
+ ),
+ l: round(hh / 2),
+ a: round(a, 2),
+ };
+};
+
+export const hsvaToHslString = (hsva: HsvaColor): string => {
+ const { h, s, l } = hsvaToHsla(hsva);
+ return `hsl(${h}, ${s}%, ${l}%)`;
+};
+
+export const hsvaToHsvString = (hsva: HsvaColor): string => {
+ const { h, s, v } = roundHsva(hsva);
+ return `hsv(${h}, ${s}%, ${v}%)`;
+};
+
+export const hsvaToHsvaString = (hsva: HsvaColor): string => {
+ const { h, s, v, a } = roundHsva(hsva);
+ return `hsva(${h}, ${s}%, ${v}%, ${a})`;
+};
+
+export const hsvaToHslaString = (hsva: HsvaColor): string => {
+ const { h, s, l, a } = hsvaToHsla(hsva);
+ return `hsla(${h}, ${s}%, ${l}%, ${a})`;
+};
+
+export const hsvaToRgba = ({ h, s, v, a }: HsvaColor): RgbaColor => {
+ h = (h / 360) * 6;
+ s = s / 100;
+ v = v / 100;
+
+ const hh = Math.floor(h),
+ b = v * (1 - s),
+ c = v * (1 - (h - hh) * s),
+ d = v * (1 - (1 - h + hh) * s),
+ module = hh % 6;
+
+ return {
+ r: [v, c, b, b, d, v][module] * 255,
+ g: [d, v, v, c, b, b][module] * 255,
+ b: [b, b, d, v, v, c][module] * 255,
+ a: round(a, 2),
+ };
+};
+
+export const hsvaToRgbString = (hsva: HsvaColor): string => {
+ const { r, g, b } = hsvaToRgba(hsva);
+ return `rgb(${round(r)}, ${round(g)}, ${round(b)})`;
+};
+
+export const hsvaToRgbaString = (hsva: HsvaColor): string => {
+ const { r, g, b, a } = hsvaToRgba(hsva);
+ return `rgba(${round(r)}, ${round(g)}, ${round(b)}, ${round(a, 2)})`;
+};
+
+export const hsvaStringToHsva = (hsvString: string): HsvaColor => {
+ const matcher =
+ /hsva?\(?\s*(-?\d*\.?\d+)(deg|rad|grad|turn)?[,\s]+(-?\d*\.?\d+)%?[,\s]+(-?\d*\.?\d+)%?,?\s*[/\s]*(-?\d*\.?\d+)?(%)?\s*\)?/i;
+ const match = matcher.exec(hsvString);
+
+ if (!match) return { h: 0, s: 0, v: 0, a: 1 };
+
+ return roundHsva({
+ h: parseHue(match[1], match[2]),
+ s: Number(match[3]),
+ v: Number(match[4]),
+ a: match[5] === undefined ? 1 : Number(match[5]) / (match[6] ? 100 : 1),
+ });
+};
+
+export const hsvStringToHsva = hsvaStringToHsva;
+
+export const rgbaStringToHsva = (rgbaString: string): HsvaColor => {
+ const matcher =
+ /rgba?\(?\s*(-?\d*\.?\d+)(%)?[,\s]+(-?\d*\.?\d+)(%)?[,\s]+(-?\d*\.?\d+)(%)?,?\s*[/\s]*(-?\d*\.?\d+)?(%)?\s*\)?/i;
+ const match = matcher.exec(rgbaString);
+
+ if (!match) return { h: 0, s: 0, v: 0, a: 1 };
+
+ return rgbaToHsva({
+ r: Number(match[1]) / (match[2] ? 100 / 255 : 1),
+ g: Number(match[3]) / (match[4] ? 100 / 255 : 1),
+ b: Number(match[5]) / (match[6] ? 100 / 255 : 1),
+ a: match[7] === undefined ? 1 : Number(match[7]) / (match[8] ? 100 : 1),
+ });
+};
+
+export const rgbStringToHsva = rgbaStringToHsva;
+
+const format = (number: number) => {
+ const hex = number.toString(16);
+ return hex.length < 2 ? '0' + hex : hex;
+};
+
+export const rgbaToHex = ({ r, g, b, a }: RgbaColor): string => {
+ const alphaHex = a < 1 ? format(round(a * 255)) : '';
+ return (
+ '#' + format(round(r)) + format(round(g)) + format(round(b)) + alphaHex
+ );
+};
+
+export const rgbaToHsva = ({ r, g, b, a }: RgbaColor): HsvaColor => {
+ const max = Math.max(r, g, b);
+ const delta = max - Math.min(r, g, b);
+
+ // prettier-ignore
+ const hh = delta
+ ? max === r
+ ? (g - b) / delta
+ : max === g
+ ? 2 + (b - r) / delta
+ : 4 + (r - g) / delta
+ : 0;
+
+ return {
+ h: 60 * (hh < 0 ? hh + 6 : hh),
+ s: max ? (delta / max) * 100 : 0,
+ v: (max / 255) * 100,
+ a,
+ };
+};
+
+export const roundHsva = (hsva: HsvaColor): HsvaColor => ({
+ h: round(hsva.h),
+ s: round(hsva.s),
+ v: round(hsva.v),
+ a: round(hsva.a, 2),
+});
+
+export const rgbaToRgb = ({ r, g, b }: RgbaColor): RgbColor => ({ r, g, b });
+
+export const hslaToHsl = ({ h, s, l }: HslaColor): HslColor => ({ h, s, l });
+
+export const hsvaToHsv = (hsva: HsvaColor): HsvColor => {
+ const { h, s, v } = roundHsva(hsva);
+ return { h, s, v };
+};
+
+const hexMatcher = /^#?([0-9A-F]{3,8})$/i;
+
+export const validHex = (value: string, alpha?: boolean): boolean => {
+ const match = hexMatcher.exec(value);
+ const length = match ? match[1].length : 0;
+
+ return (
+ length === 3 || // '#rgb' format
+ length === 6 || // '#rrggbb' format
+ (!!alpha && length === 4) || // '#rgba' format
+ (!!alpha && length === 8) // '#rrggbbaa' format
+ );
+};
diff --git a/tgui/packages/tgui/components/ByondUi.js b/tgui/packages/tgui/components/ByondUi.js
index dfc611c5e9c..5e39410743a 100644
--- a/tgui/packages/tgui/components/ByondUi.js
+++ b/tgui/packages/tgui/components/ByondUi.js
@@ -64,13 +64,17 @@ window.addEventListener('beforeunload', () => {
});
/**
- * Get the bounding box of the DOM element.
+ * Get the bounding box of the DOM element in display-pixels.
*/
const getBoundingBox = (element) => {
+ const pixelRatio = window.devicePixelRatio ?? 1;
const rect = element.getBoundingClientRect();
return {
- pos: [rect.left, rect.top],
- size: [rect.right - rect.left, rect.bottom - rect.top],
+ pos: [rect.left * pixelRatio, rect.top * pixelRatio],
+ size: [
+ (rect.right - rect.left) * pixelRatio,
+ (rect.bottom - rect.top) * pixelRatio,
+ ],
};
};
diff --git a/tgui/packages/tgui/components/DmIcon.tsx b/tgui/packages/tgui/components/DmIcon.tsx
new file mode 100644
index 00000000000..c77dc8a8ff9
--- /dev/null
+++ b/tgui/packages/tgui/components/DmIcon.tsx
@@ -0,0 +1,92 @@
+import { Component, InfernoNode } from 'inferno';
+import { resolveAsset } from '../assets';
+import { fetchRetry } from '../http';
+import { BoxProps } from './Box';
+import { Image } from './Image';
+
+enum Direction {
+ NORTH = 1,
+ SOUTH = 2,
+ EAST = 4,
+ WEST = 8,
+ NORTHEAST = NORTH | EAST,
+ NORTHWEST = NORTH | WEST,
+ SOUTHEAST = SOUTH | EAST,
+ SOUTHWEST = SOUTH | WEST,
+}
+
+type Props = {
+ /** Required: The path of the icon */
+ icon: string;
+ /** Required: The state of the icon */
+ icon_state: string;
+} & Partial<{
+ /** Facing direction. See direction enum. Default is South */
+ direction: Direction;
+ /** Fallback icon. */
+ fallback: InfernoNode;
+ /** Frame number. Default is 1 */
+ frame: number;
+ /** Movement state. Default is false */
+ movement: any;
+}> &
+ BoxProps;
+
+let refMap: Record | undefined;
+
+export class DmIcon extends Component {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ iconRef: '',
+ };
+ }
+
+ async fetchRefMap() {
+ try {
+ const response = await fetchRetry(resolveAsset('icon_ref_map.json'));
+ const data = await response.json();
+ refMap = data;
+ this.setState({ iconRef: data[this.props.icon] || '' });
+ } catch (err) {
+ return;
+ }
+ }
+
+ componentDidMount() {
+ if (!refMap) {
+ this.fetchRefMap();
+ } else {
+ this.setState({ iconRef: refMap[this.props.icon] });
+ }
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.icon !== this.props.icon) {
+ if (refMap) {
+ this.setState({ iconRef: refMap[this.props.icon] });
+ } else {
+ this.fetchRefMap();
+ }
+ }
+ }
+
+ render() {
+ const {
+ className,
+ direction = Direction.SOUTH,
+ fallback,
+ frame = 1,
+ icon_state,
+ movement = false,
+ ...rest
+ } = this.props;
+ const { iconRef } = this.state;
+
+ const query = `${iconRef}?state=${icon_state}&dir=${direction}&movement=${!!movement}&frame=${frame}`;
+
+ if (!iconRef) return fallback || null;
+
+ return ;
+ }
+}
diff --git a/tgui/packages/tgui/components/Image.tsx b/tgui/packages/tgui/components/Image.tsx
new file mode 100644
index 00000000000..40730da594d
--- /dev/null
+++ b/tgui/packages/tgui/components/Image.tsx
@@ -0,0 +1,70 @@
+import { Component } from 'inferno';
+import { BoxProps, computeBoxProps } from './Box';
+
+type Props = Partial<{
+ /** True is default, this fixes an ie thing */
+ fixBlur: boolean;
+ /** False by default. Good if you're fetching images on UIs that do not auto update. This will attempt to fix the 'x' icon 5 times. */
+ fixErrors: boolean;
+ /** Fill is default. */
+ objectFit: 'contain' | 'cover';
+}> &
+ IconUnion &
+ BoxProps;
+
+// at least one of these is required
+type IconUnion =
+ | {
+ className?: string;
+ src: string;
+ }
+ | {
+ className: string;
+ src?: string;
+ };
+
+const maxAttempts = 5;
+
+/** Image component. Use this instead of Box as="img". */
+export class Image extends Component {
+ attempts: number = 0;
+
+ handleError = (event) => {
+ const { fixErrors, src } = this.props;
+ if (fixErrors && this.attempts < maxAttempts) {
+ const imgElement = event.currentTarget;
+
+ setTimeout(() => {
+ imgElement.src = `${src}?attempt=${this.attempts}`;
+ this.attempts++;
+ }, 1000);
+ }
+ };
+
+ render() {
+ const {
+ fixBlur = true,
+ fixErrors = false,
+ objectFit = 'fill',
+ src,
+ ...rest
+ } = this.props;
+
+ /* Remove -ms-interpolation-mode with Byond 516. -webkit-optimize-contrast is better than pixelated */
+ const computedProps = computeBoxProps({
+ style: {
+ '-ms-interpolation-mode': `${fixBlur ? 'nearest-neighbor' : 'auto'}`,
+ 'image-rendering': `${fixBlur ? 'pixelated' : 'auto'}`,
+ 'object-fit': `${objectFit}`,
+ },
+ ...rest,
+ });
+
+ /* Use div instead img if used asset, cause img with class leaves white border on 516 */
+ if (computedProps.className) {
+ return ;
+ }
+
+ return ;
+ }
+}
diff --git a/tgui/packages/tgui/components/ImageButtonTS.tsx b/tgui/packages/tgui/components/ImageButtonTS.tsx
new file mode 100644
index 00000000000..565f31a2d58
--- /dev/null
+++ b/tgui/packages/tgui/components/ImageButtonTS.tsx
@@ -0,0 +1,243 @@
+/**
+ * @file
+ * @copyright 2024 Aylong (https://github.com/AyIong)
+ * @license MIT
+ */
+
+import { Placement } from '@popperjs/core';
+
+import { InfernoNode } from 'inferno';
+import { BooleanLike, classes } from 'common/react';
+import { BoxProps, computeBoxProps } from './Box';
+import { Icon } from './Icon';
+import { Image } from './Image';
+import { DmIcon } from './DmIcon';
+import { Stack } from './Stack';
+import { Tooltip } from './Tooltip';
+
+type Props = Partial<{
+ /** Asset cache. Example: `asset={`assetname32x32, ${thing.key}`}` */
+ asset: string[];
+ /** Classic way to put images. Example: `base64={thing.image}` */
+ base64: string;
+ /**
+ * Special container for buttons.
+ * You can put any other component here.
+ * Has some special stylings!
+ * Example: `buttons={}`
+ */
+ buttons: InfernoNode;
+ /**
+ * Same as buttons, but. Have disabled pointer-events on content inside if non-fluid.
+ * Fluid version have humburger layout.
+ */
+ buttonsAlt: InfernoNode;
+ /** Content under image. Or on the right if fluid. */
+ children: InfernoNode;
+ /** Applies a CSS class to the element. */
+ className: string;
+ /** Color of the button. See [Button](#button) but without `transparent`. */
+ color: string;
+ /** Makes button disabled and dark red if true. Also disables onClick. */
+ disabled: BooleanLike;
+ /** Optional. Adds a "stub" when loading DmIcon. */
+ dmFallback: InfernoNode;
+ /** Parameter `icon` of component `DmIcon`. */
+ dmIcon: string | null;
+ /** Parameter `icon_state` of component `DmIcon`. */
+ dmIconState: string | null;
+ /** Parameter `direction` of component `DmIcon`. */
+ dmDirection: number | null;
+ /**
+ * Changes the layout of the button, making it fill the entire horizontally available space.
+ * Allows the use of `title`
+ */
+ fluid: boolean;
+ /** Parameter responsible for the size of the image, component and standard "stubs". */
+ imageSize: number;
+ /** Prop `src` of . Example: `imageSrc={resolveAsset(thing.image}` */
+ imageSrc: string;
+ /** Called when button is clicked with LMB. */
+ onClick: (e: any) => void;
+ /** Called when button is clicked with RMB. */
+ onRightClick: (e: any) => void;
+ /** Makes button selected and green if true. */
+ selected: BooleanLike;
+ /** Requires `fluid` for work. Bold text with divider betwen content. */
+ title: string;
+ /** A fancy, boxy tooltip, which appears when hovering over the button */
+ tooltip: InfernoNode;
+ /** Position of the tooltip. See [`Popper`](#Popper) for valid options. */
+ tooltipPosition: Placement;
+}> &
+ BoxProps;
+
+export const ImageButtonTS = (props: Props) => {
+ const {
+ asset,
+ base64,
+ buttons,
+ buttonsAlt,
+ children,
+ className,
+ color,
+ disabled,
+ dmFallback,
+ dmDirection,
+ dmIcon,
+ dmIconState,
+ fluid,
+ imageSize = 64,
+ imageSrc,
+ onClick,
+ onRightClick,
+ selected,
+ title,
+ tooltip,
+ tooltipPosition,
+ ...rest
+ } = props;
+
+ const getFallback = (iconName: string, iconSpin: boolean) => {
+ return (
+
+
+
+
+
+ );
+ };
+
+ let buttonContent = (
+
+ );
+};
diff --git a/tgui/packages/tgui/components/Interactive.tsx b/tgui/packages/tgui/components/Interactive.tsx
new file mode 100644
index 00000000000..f0a0dfe1389
--- /dev/null
+++ b/tgui/packages/tgui/components/Interactive.tsx
@@ -0,0 +1,153 @@
+/**
+ * MIT License
+ * https://github.com/omgovich/react-colorful/
+ *
+ * Copyright (c) 2020 Vlad Shilov
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { clamp } from 'common/math';
+import { Component, InfernoNode, createRef, RefObject } from 'inferno';
+
+export interface Interaction {
+ left: number;
+ top: number;
+}
+
+// Finds the proper window object to fix iframe embedding issues
+const getParentWindow = (node?: HTMLDivElement | null): Window => {
+ return (node && node.ownerDocument.defaultView) || self;
+};
+
+// Returns a relative position of the pointer inside the node's bounding box
+const getRelativePosition = (
+ node: HTMLDivElement,
+ event: MouseEvent
+): Interaction => {
+ const rect = node.getBoundingClientRect();
+ const pointer = event as MouseEvent;
+ return {
+ left: clamp(
+ (pointer.pageX - (rect.left + getParentWindow(node).pageXOffset)) /
+ rect.width,
+ 0,
+ 1
+ ),
+ top: clamp(
+ (pointer.pageY - (rect.top + getParentWindow(node).pageYOffset)) /
+ rect.height,
+ 0,
+ 1
+ ),
+ };
+};
+
+export interface InteractiveProps {
+ onMove: (interaction: Interaction) => void;
+ onKey: (offset: Interaction) => void;
+ children: InfernoNode[];
+ style?: any;
+}
+
+export class Interactive extends Component {
+ containerRef: RefObject;
+ props: InteractiveProps;
+
+ constructor(props: InteractiveProps) {
+ super();
+ this.props = props;
+ this.containerRef = createRef();
+ }
+
+ handleMoveStart = (event: MouseEvent) => {
+ const el = this.containerRef?.current;
+ if (!el) return;
+
+ // Prevent text selection
+ event.preventDefault();
+ el.focus();
+ this.props.onMove(getRelativePosition(el, event));
+ this.toggleDocumentEvents(true);
+ };
+
+ handleMove = (event: MouseEvent) => {
+ // Prevent text selection
+ event.preventDefault();
+
+ // If user moves the pointer outside of the window or iframe bounds and release it there,
+ // `mouseup`/`touchend` won't be fired. In order to stop the picker from following the cursor
+ // after the user has moved the mouse/finger back to the document, we check `event.buttons`
+ // and `event.touches`. It allows us to detect that the user is just moving his pointer
+ // without pressing it down
+ const isDown = event.buttons > 0;
+
+ if (isDown && this.containerRef?.current) {
+ this.props.onMove(getRelativePosition(this.containerRef.current, event));
+ } else {
+ this.toggleDocumentEvents(false);
+ }
+ };
+
+ handleMoveEnd = () => {
+ this.toggleDocumentEvents(false);
+ };
+
+ handleKeyDown = (event: KeyboardEvent) => {
+ const keyCode = event.which || event.keyCode;
+
+ // Ignore all keys except arrow ones
+ if (keyCode < 37 || keyCode > 40) return;
+ // Do not scroll page by arrow keys when document is focused on the element
+ event.preventDefault();
+ // Send relative offset to the parent component.
+ // We use codes (37←, 38↑, 39→, 40↓) instead of keys ('ArrowRight', 'ArrowDown', etc)
+ // to reduce the size of the library
+ this.props.onKey({
+ left: keyCode === 39 ? 0.05 : keyCode === 37 ? -0.05 : 0,
+ top: keyCode === 40 ? 0.05 : keyCode === 38 ? -0.05 : 0,
+ });
+ };
+
+ toggleDocumentEvents(state?: boolean) {
+ const el = this.containerRef?.current;
+ const parentWindow = getParentWindow(el);
+
+ // Add or remove additional pointer event listeners
+ const toggleEvent = state
+ ? parentWindow.addEventListener
+ : parentWindow.removeEventListener;
+ toggleEvent('mousemove', this.handleMove);
+ toggleEvent('mouseup', this.handleMoveEnd);
+ }
+
+ componentDidMount() {
+ this.toggleDocumentEvents(true);
+ }
+
+ componentWillUnmount() {
+ this.toggleDocumentEvents(false);
+ }
+
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
diff --git a/tgui/packages/tgui/components/Pointer.tsx b/tgui/packages/tgui/components/Pointer.tsx
new file mode 100644
index 00000000000..409972a7dfc
--- /dev/null
+++ b/tgui/packages/tgui/components/Pointer.tsx
@@ -0,0 +1,46 @@
+/**
+ * MIT License
+ * https://github.com/omgovich/react-colorful/
+ *
+ * Copyright (c) 2020 Vlad Shilov
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import { classes } from 'common/react';
+import { InfernoNode } from 'inferno';
+
+interface PointerProps {
+ className?: string;
+ top?: number;
+ left: number;
+ color: string;
+}
+
+export const Pointer = ({
+ className,
+ color,
+ left,
+ top = 0.5,
+}: PointerProps): InfernoNode => {
+ const nodeClassName = classes(['react-colorful__pointer', className]);
+
+ const style = {
+ top: `${top * 100}%`,
+ left: `${left * 100}%`,
+ };
+
+ return (
+