From 8e53577ee732e5117426f6e11179a39b2516bdff Mon Sep 17 00:00:00 2001
From: Casper3667 <8396443+Casper3667@users.noreply.github.com>
Date: Mon, 1 Jun 2026 12:36:57 +0200
Subject: [PATCH] Taj rebalance patch (#22451)
This is based on requests from the taj lore team. A future PR will do
more for the sensitive hearing as well, but the PR was growing by quite
a lot so it was best to split it up.
New:
- Tajaran eyes can now extend their vision by 3 tiles while they stand
still.
- Taj can hear people whispering and talking into radios from 1 tiles
further away, if they are listening intently.
Bugfixes:
- Flashbangs no longer fuck taj up as much unless they got intent listen
active.
- The message for autakh farseer eyes no longer gives the wrong message
if it is disabled by moving.
Balance & Change:
- Taj have new heights. Before it was 150 to 190 cm
- Hharar: 145-180
- M'sai: 155-175
- Zhan: 160-195
- The brute mod have been adjusted on tajara
- Zhan: 10% -> 5%
- Hharar: 20% -> 10%
- M'sai: 30% -> 15%
Backend stuff:
- Action buttons were changed to accept lists, so multiple actions can
be given to a single item.
---
code/_onclick/hud/action.dm | 16 +++++++
code/game/atoms.dm | 12 +++++
code/game/objects/items.dm | 18 +++++--
.../items/weapons/grenades/flashbang.dm | 18 +++++--
.../carbon/human/species/outsider/revenant.dm | 1 +
.../human/species/station/tajara/tajara.dm | 6 +--
.../station/tajara/tajaran_subspecies.dm | 8 +++-
code/modules/mob/living/living_defense.dm | 47 +++++++++++++++----
code/modules/mob/living/say.dm | 3 ++
code/modules/mob/living/whisper.dm | 9 ++--
.../modules/organs/internal/species/tajara.dm | 42 +++++++++++++----
.../modules/organs/internal/species/vaurca.dm | 2 +
code/modules/organs/subtypes/autakh.dm | 1 +
html/changelogs/TajChange.yml | 11 +++++
14 files changed, 162 insertions(+), 32 deletions(-)
create mode 100644 html/changelogs/TajChange.yml
diff --git a/code/_onclick/hud/action.dm b/code/_onclick/hud/action.dm
index 69e323792aa..a5eb04343f3 100644
--- a/code/_onclick/hud/action.dm
+++ b/code/_onclick/hud/action.dm
@@ -284,6 +284,22 @@
check_flags = AB_CHECK_STUNNED|AB_CHECK_ALIVE|AB_CHECK_INSIDE
button_icon_state = "night_eyes"
+/datum/action/item_action/organ/night_eyes/Trigger()
+ if(!Checks())
+ return
+ var/obj/item/organ/internal/eyes/night/target_eyes = target
+ target_eyes.night_vision()
+
+/datum/action/item_action/organ/extended_eyes
+ check_flags = AB_CHECK_STUNNED|AB_CHECK_ALIVE|AB_CHECK_INSIDE
+ button_icon_state = "night_eyes"
+
+/datum/action/item_action/organ/extended_eyes/Trigger()
+ if(!Checks())
+ return
+ var/obj/item/organ/internal/eyes/night/target_eyes = target
+ target_eyes.extended_vision()
+
/datum/action/item_action/organ/night_eyes/rev
check_flags = AB_CHECK_ALIVE|AB_CHECK_INSIDE
button_icon_state = "rev_eyes"
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 13fcc2b8e92..72ba517426e 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -543,6 +543,18 @@
if(src.z == H.z && get_dist(src, H) <= range)
H.intent_listen(src, message)
+/proc/get_intent_listeners(var/atom/source, var/range = 7, var/list/hearers = list())
+ SHOULD_NOT_SLEEP(TRUE)
+ var/list/listeners = list()
+ if(air_sound(source))
+ if(!length(hearers))
+ hearers = get_hearers_in_view(range, source)
+ for(var/mob/living/carbon/human/H as anything in GLOB.intent_listener)
+ if((H in hearers))
+ if(source.z == H.z && get_dist(source, H) <= range)
+ listeners += H
+ return listeners
+
/atom/movable/proc/dropInto(var/atom/destination)
while(istype(destination))
var/atom/drop_destination = destination.onDropInto(src)
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index a98d7e1950c..a0f440f2516 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -136,6 +136,8 @@
var/zoomdevicename
/// Boolean, `TRUE` if item is actively being used to zoom. For scoped guns and binoculars.
var/zoom = FALSE
+ /// The message used when stopping looking through a pair of binoculars/scope
+ var/zoom_out_message
/// Boolean, if item_state, lefthand, righthand, and worn sprite are all in one dmi
var/contained_sprite = FALSE
@@ -259,8 +261,16 @@
var/mob/m = loc
m.drop_from_inventory(src, null)
- if(!QDELETED(action))
+ if(islist(action))
+ var/list/action_list = action
+ for(var/i in 1 to action_list.len)
+ var/datum/action/A = action_list[i]
+ if(!QDELETED(A))
+ QDEL_NULL(A)
+ action_list.Cut()
+ else if(!QDELETED(action))
QDEL_NULL(action) // /mob/living/proc/handle_actions() creates it, for ungodly reasons
+
action = null
if(!QDELETED(hidden_uplink))
@@ -1005,9 +1015,11 @@ modules/mob/living/carbon/human/life.dm if you die, you will be zoomed out.
M.client.pixel_x = 0
M.client.pixel_y = 0
- if(!cannotzoom)
- if(show_zoom_message)
+ if(!cannotzoom && show_zoom_message)
+ if(!zoom_out_message)
M.visible_message("[zoomdevicename ? "[M] looks up from \the [src.name]" : "[M] lowers \the [src.name]"].")
+ else
+ M.visible_message("[zoomdevicename ? "[M][zoom_out_message] \the [src.name]." : "[M][zoom_out_message]"]")
if(ishuman(M))
var/mob/living/carbon/human/H = M
diff --git a/code/game/objects/items/weapons/grenades/flashbang.dm b/code/game/objects/items/weapons/grenades/flashbang.dm
index cd749b4f00e..b04398747aa 100644
--- a/code/game/objects/items/weapons/grenades/flashbang.dm
+++ b/code/game/objects/items/weapons/grenades/flashbang.dm
@@ -36,13 +36,21 @@
if(M.flash_act(ignore_inherent = TRUE))
M.Weaken(10)
- // 1 - 9x/70 gives us 100% at zero, 87% at 1 turf, all the way to 10% at 7
- var/bang_intensity = 1 - (9 * get_dist(T, M) / 70)
- M.noise_act(intensity = EAR_PROTECTION_MODERATE, damage_pwr = 10 * bang_intensity, deafen_pwr = 15 * (1 - bang_intensity))
+ var/distance_to_grenade = get_dist(T, M)
- if(M.get_hearing_sensitivity()) // we only stun if they've got sensitive ears
+ // 1 - 9x/70 gives us 100% at zero, 87% at 1 turf, all the way to 10% at 7
+ var/bang_intensity = 1 - (9 * distance_to_grenade / 70)
+
+ var/ear_damage = 10 * bang_intensity
+ var/deafen_damage = 15 * (1 - bang_intensity)
+
+
+ // we only stun if they've got sensitive ears and got it active as well as if it is close enough.
+ if(M.get_hearing_sensitivity() && astype(M, /mob/living/carbon/human)?.is_listening() && distance_to_grenade < 5)
// checking for protection is handled by noise_act
- M.noise_act(intensity = EAR_PROTECTION_MAJOR, stun_pwr = 2)
+ M.noise_act(intensity = EAR_PROTECTION_MAJOR, stun_pwr = 2, damage_pwr = ear_damage, deafen_pwr = deafen_damage)
+ else
+ M.noise_act(intensity = EAR_PROTECTION_MODERATE, damage_pwr = ear_damage, deafen_pwr = deafen_damage)
M.disable_cloaking_device()
M.update_icon()
diff --git a/code/modules/mob/living/carbon/human/species/outsider/revenant.dm b/code/modules/mob/living/carbon/human/species/outsider/revenant.dm
index b0b020a15e5..2a38a09bc5a 100644
--- a/code/modules/mob/living/carbon/human/species/outsider/revenant.dm
+++ b/code/modules/mob/living/carbon/human/species/outsider/revenant.dm
@@ -166,6 +166,7 @@
icon_state = "revenant_eyes"
eye_emote = null
vision_color = null
+ action_button_name = list("Activate Low Light Vision")
default_action_type = /datum/action/item_action/organ/night_eyes/rev
/obj/item/organ/internal/eyes/night/revenant/flash_act()
diff --git a/code/modules/mob/living/carbon/human/species/station/tajara/tajara.dm b/code/modules/mob/living/carbon/human/species/station/tajara/tajara.dm
index 5b58a8692a2..3ab0ac68452 100644
--- a/code/modules/mob/living/carbon/human/species/station/tajara/tajara.dm
+++ b/code/modules/mob/living/carbon/human/species/station/tajara/tajara.dm
@@ -5,8 +5,8 @@
category_name = "Tajara"
bodytype = BODYTYPE_TAJARA
species_height = HEIGHT_CLASS_SHORT
- height_min = 150
- height_max = 190
+ height_min = 145 // human default: 145
+ height_max = 180 // human default: 203
icobase = 'icons/mob/human_races/tajara/r_tajaran.dmi'
deform = 'icons/mob/human_races/tajara/r_def_tajaran.dmi'
preview_icon = 'icons/mob/human_races/tajara/tajaran_preview.dmi'
@@ -27,7 +27,7 @@
darksight = 8
slowdown = -0.4
- brute_mod = 1.2
+ brute_mod = 1.1
fall_mod = 0.5
grab_mod = 1.25 // Fur easy to cling onto
diff --git a/code/modules/mob/living/carbon/human/species/station/tajara/tajaran_subspecies.dm b/code/modules/mob/living/carbon/human/species/station/tajara/tajaran_subspecies.dm
index deed857b277..bc6b87ed4e9 100644
--- a/code/modules/mob/living/carbon/human/species/station/tajara/tajaran_subspecies.dm
+++ b/code/modules/mob/living/carbon/human/species/station/tajara/tajaran_subspecies.dm
@@ -10,6 +10,8 @@
an easy path to education. Zhan-Khazan are in a transformative state in modern Tajaran society, \
but still deal with the many barriers from centuries of the caste system."
species_height = HEIGHT_CLASS_TALL
+ height_min = 160 // taj default: 145
+ height_max = 195 // taj default: 180
secondary_langs = list(LANGUAGE_SIIK_TAJR, LANGUAGE_DELVAHII)
@@ -17,7 +19,7 @@
sprint_speed_factor = 0.55
standing_jump_range = 2
stamina = 100 // As opposed to 90
- brute_mod = 1.1 // Less Brute Damage
+ brute_mod = 1.05 // Less Brute Damage
ethanol_resistance = 1 // Default value
climb_coeff = 1.1
@@ -59,12 +61,14 @@
When these roles were shrunken down however, the M'sai commonly found themselves taking \
similar work to the Zhan-Khazan. As a result, the two races share much in common with their culture."
species_height = HEIGHT_CLASS_AVERAGE
+ height_min = 155 // taj default: 145
+ height_max = 175 // taj default: 180
slowdown = -0.6
sprint_speed_factor = 0.75
standing_jump_range = 3
stamina = 80 // As opposed to 90
- brute_mod = 1.3 // More Brute Damage
+ brute_mod = 1.15 // More Brute Damage
ethanol_resistance = 0.7 // Species Default 0.9
maneuvers = list(
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 47edd11ff1d..65cddec66bb 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -503,19 +503,50 @@
A.Remove(src)
for(var/obj/item/I in src)
- if(I.action_button_name)
+ if(!I.action_button_name)
+ continue
- //If the item_action object does not exist, try to create it
+ var/list/action_names = islist(I.action_button_name) ? I.action_button_name : list(I.action_button_name)
+ var/list/action_types = islist(I.default_action_type) ? I.default_action_type : list(I.default_action_type)
+
+ if(action_names.len > 1)
+ if(!islist(I.action))
+ I.action = I.action ? list(I.action) : list()
+
+ var/list/actions_list = I.action
+
+ for(var/i in 1 to action_names.len)
+ var/action_name = action_names[i]
+ if(!action_name)
+ continue
+
+ var/datum/action/item_action = (i <= actions_list.len) ? actions_list[i] : null
+
+ if(!item_action)
+ var/action_type = action_types.len >= i ? action_types[i] : action_types[1]
+ if(!action_type)
+ continue
+
+ item_action = new action_type
+ actions_list.Add(item_action)
+
+ item_action.name = action_name
+ item_action.SetTarget(I)
+ item_action.Grant(src)
+
+ else
if(!I.action)
- //Try to use the default action type, if there is none, skip this implant
- if(I.default_action_type)
- I.action = new I.default_action_type
+ var/action_type = action_types.len ? action_types[1] : null
+ if(action_type)
+ I.action = new action_type
else
continue
- I.action.name = I.action_button_name
- I.action.SetTarget(I)
- I.action.Grant(src)
+ var/datum/action/item_action = I.action
+ item_action.name = action_names[1]
+ item_action.SetTarget(I)
+ item_action.Grant(src)
+
return
/mob/living/update_action_buttons()
diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm
index 5658b3ed41d..caf2a24e2ba 100644
--- a/code/modules/mob/living/say.dm
+++ b/code/modules/mob/living/say.dm
@@ -330,6 +330,9 @@ var/list/channel_to_radio_key = new
listening = get_hearers_in_view(message_range, src)
+ var/sensitive_listener = get_intent_listeners(src, message_range + 1)
+ listening = mergelists(listening, sensitive_listener, TRUE)
+
if(client)
for (var/mob/player_mob in GLOB.player_list)
if(!player_mob || player_mob.stat != DEAD || (player_mob in listening))
diff --git a/code/modules/mob/living/whisper.dm b/code/modules/mob/living/whisper.dm
index 07a75cf42a5..cc5e294edf2 100644
--- a/code/modules/mob/living/whisper.dm
+++ b/code/modules/mob/living/whisper.dm
@@ -60,11 +60,14 @@
var/list/observers = list()
var/list/all_in_range = hearers(watching_range, src)
for(var/mob/M as anything in all_in_range)
- if(M.stat == DEAD && M.client && (M.client.prefs.toggles & CHAT_GHOSTEARS)) //Preventing duplicate messages to ghostear'd observers
+ // If they got sensitive hearing enabled, increase the whisper hearing range by one
+ var/sensitive = astype(M, /mob/living/carbon/human)?.is_listening() ? 1 : 0
+ //Preventing duplicate messages to ghostear'd observers
+ if(M.stat == DEAD && M.client && (M.client.prefs.toggles & CHAT_GHOSTEARS))
continue
- if(get_dist(src, M) <= message_range) //In range to hear
+ if(get_dist(src, M) <= (message_range + sensitive)) //In range to hear
continue
- else if(get_dist(src, M) <= eavesdropping_range)
+ else if(get_dist(src, M) <= (eavesdropping_range + sensitive))
if(M.stat == DEAD && M.client)
observers += M
else
diff --git a/code/modules/organs/internal/species/tajara.dm b/code/modules/organs/internal/species/tajara.dm
index 061fa190cab..48bc6617370 100644
--- a/code/modules/organs/internal/species/tajara.dm
+++ b/code/modules/organs/internal/species/tajara.dm
@@ -2,14 +2,16 @@
name = "light sensitive eyes"
desc = "A pair of Tajaran eyes accustomed to the low light conditions of Adhomai."
icon = 'icons/obj/organs/tajara_organs.dmi'
- action_button_name = "Activate Low Light Vision"
- default_action_type = /datum/action/item_action/organ/night_eyes
+ action_button_name = list("Activate Low Light Vision", "Extend vision")
+ default_action_type = list(/datum/action/item_action/organ/night_eyes, /datum/action/item_action/organ/extended_eyes)
relative_size = 8
var/night_vision = FALSE
var/datum/client_color/vision_color = /datum/client_color/monochrome
var/datum/client_color/vision_mechanical_color = /datum/client_color/monochrome
var/eye_emote = "'s eyes dilate!"
var/allowed_model = PROSTHETIC_TESLA //what robotic model allows this eyes to use the night vision
+ var/cooldown = 30
+ zoom_out_message = "'s eyes widens as they stop focusing ahead."
/obj/item/organ/internal/eyes/night/Destroy()
disable_night_vision()
@@ -25,25 +27,49 @@
/obj/item/organ/internal/eyes/night/attack_self(var/mob/user)
. = ..()
+
+/obj/item/organ/internal/eyes/night/proc/ability_check()
if(owner.last_special > world.time)
- return
+ return FALSE
if(owner.stat || owner.paralysis || owner.stunned || owner.weakened)
- return
+ return FALSE
if(is_broken())
- return
+ return FALSE
if(status & ORGAN_ROBOT)
if(!robotic_check())
- return
+ return FALSE
+ owner.last_special = world.time + 20
+ return TRUE
+
+/obj/item/organ/internal/eyes/night/proc/night_vision()
+ if(attack_self())
+ return
+ if(!ability_check())
+ return
if(!night_vision)
enable_night_vision()
else
disable_night_vision()
- owner.last_special = world.time + 20
+/obj/item/organ/internal/eyes/night/proc/extended_vision()
+ if(attack_self())
+ return
+ if(!ability_check())
+ return
+ if(zoom)
+ owner.visible_message("[SPAN_BOLD("\The [owner]")]'s eyes widens as they stop focusing ahead.", range = 3)
+ else
+ owner.visible_message("[SPAN_BOLD("\The [owner]")]'s eyes begins to narrow as they focus on something in the distance.", range = 3)
+ if(!do_after(owner, 1.5 SECONDS))
+ owner.visible_message("[SPAN_BOLD("\The [owner]")]'s eyes stop focusing.", range = 3)
+ return
+ owner.visible_message("[SPAN_BOLD("\The [owner]")]'s eyes narrows as their lenses shift to focus ahead.", range = 3)
+
+ zoom(owner, 3, 7, FALSE, FALSE)
/obj/item/organ/internal/eyes/night/proc/robotic_check(var/mob/user)
if(robotize_type == allowed_model)
@@ -89,7 +115,7 @@
show_message = FALSE
break
if(show_message && eye_emote)
- owner.visible_message("[owner][eye_emote]")
+ owner.visible_message("[SPAN_BOLD("\The [owner]")][eye_emote]")
night_vision = TRUE
if(can_change_invisible())
diff --git a/code/modules/organs/internal/species/vaurca.dm b/code/modules/organs/internal/species/vaurca.dm
index 5a2aa355f9f..aa38e35f771 100644
--- a/code/modules/organs/internal/species/vaurca.dm
+++ b/code/modules/organs/internal/species/vaurca.dm
@@ -3,6 +3,8 @@
desc = "A set of four vaurcaesian eyes, adapted to the low or no light tunnels of Sedantis."
icon = 'icons/obj/organs/vaurca_organs.dmi'
robotic_sprite = FALSE
+ action_button_name = list("Activate Low Light Vision")
+ default_action_type = list(/datum/action/item_action/organ/night_eyes)
vision_color = /datum/client_color/vaurca
vision_mechanical_color = /datum/client_color/monochrome
eye_emote = "'s eyes gently shift."
diff --git a/code/modules/organs/subtypes/autakh.dm b/code/modules/organs/subtypes/autakh.dm
index bbb66b5d8c8..afcf7616c9c 100644
--- a/code/modules/organs/subtypes/autakh.dm
+++ b/code/modules/organs/subtypes/autakh.dm
@@ -190,6 +190,7 @@
action_button_icon = "hunterseye"
cooldown = 30
activable = TRUE
+ zoom_out_message = " eyes whirrs loudly as the zoom lenses retract."
/obj/item/organ/internal/augment/farseer_eye/attack_self(var/mob/user)
. = ..()
diff --git a/html/changelogs/TajChange.yml b/html/changelogs/TajChange.yml
new file mode 100644
index 00000000000..f24d08b67d4
--- /dev/null
+++ b/html/changelogs/TajChange.yml
@@ -0,0 +1,11 @@
+author: TheGreyWolf
+
+delete-after: True
+
+changes:
+ - balance: "Tajara brute mods were changed from 10%, 20% and 30% for zhan, base tajara and m'sai to 5%, 10%, 15%."
+ - balance: "Flashbangs no longer stun tajara unless they have sensitive hearing on."
+ - rscadd: "The height difference between hharar, m'sai and zhan were tweaked to not be identical."
+ - rscadd: "Tajara eyes can now extend their vision, somewhat similarly to using a binocular."
+ - rscadd: "Tajara with sensitive hearing on can now hear whispers and people talking into radios from 2 turfs away."
+ - bugfix: "Fixed a bug with the autakh farseer eyes causing them to give the improper message if retracted by movement or other sources."