diff --git a/code/__DEFINES/~~bubber_defines/status_indicator_defines.dm b/code/__DEFINES/~~bubber_defines/status_indicator_defines.dm new file mode 100644 index 00000000000..57abd103b0d --- /dev/null +++ b/code/__DEFINES/~~bubber_defines/status_indicator_defines.dm @@ -0,0 +1,11 @@ +#define STUNNED "stunned" +#define WEAKEN "weakened" +#define PARALYSIS "paralysis" +#define SLEEPING "sleeping" +#define CONFUSED "confused" +#define PLANE_STATUS_INDICATOR -12 //Status Indicators that show over mobs' heads when certain things like stuns affect them. +#define STATUS_LAYER -2.1 +#define STATUS_INDICATOR_Y_OFFSET 2 // Offset from the edge of the icon sprite, so 32 pixels plus whatever number is here. +#define STATUS_INDICATOR_ICON_X_SIZE 0 // Don't need to care about the Y size due to the origin being on the bottom side. +#define STATUS_INDICATOR_ICON_MARGIN 0 // The space between two status indicators. We don't do this with the current icons. +#define DEFAULT_MOB_SCALE 1 diff --git a/modular_zubbers/code/modules/mob/living/living.dm b/modular_zubbers/code/modules/mob/living/living.dm new file mode 100644 index 00000000000..840c112204e --- /dev/null +++ b/modular_zubbers/code/modules/mob/living/living.dm @@ -0,0 +1,3 @@ +/mob/living/Initialize(mapload) + . = ..() + AddComponent(/datum/component/status_indicator) diff --git a/modular_zubbers/code/modules/status_indicators/status_indicator.dm b/modular_zubbers/code/modules/status_indicators/status_indicator.dm new file mode 100644 index 00000000000..43e1319adb4 --- /dev/null +++ b/modular_zubbers/code/modules/status_indicators/status_indicator.dm @@ -0,0 +1,220 @@ + +GLOBAL_LIST_INIT(potential_indicators, list( + STUNNED = image(icon = 'modular_zubbers/icons/mob/status_indicators.dmi', icon_state = STUNNED), + WEAKEN = image(icon = 'modular_zubbers/icons/mob/status_indicators.dmi', icon_state = WEAKEN), + PARALYSIS = image(icon = 'modular_zubbers/icons/mob/status_indicators.dmi', icon_state = PARALYSIS), + SLEEPING = image(icon = 'modular_zubbers/icons/mob/status_indicators.dmi', icon_state = SLEEPING), + CONFUSED = image(icon = 'modular_zubbers/icons/mob/status_indicators.dmi', icon_state = CONFUSED), +)) + +/datum/component/status_indicator + var/list/status_indicators = null // Will become a list as needed. Contains our status indicator objects. Note, they are actually added to overlays, this just keeps track of what exists. + var/mob/living/attached_mob + +/// Returns true if the mob is weakened. Also known as floored. +/datum/component/status_indicator/proc/is_weakened() + if(!indicator_fakeouts() && \ + attached_mob.IsKnockdown() || \ + HAS_TRAIT(attached_mob, TRAIT_FLOORED) && \ + !HAS_TRAIT_FROM(attached_mob, TRAIT_FLOORED, BUCKLED_TRAIT) + ) + return TRUE + +/// Returns true if the mob is stunned. +/datum/component/status_indicator/proc/is_stunned() + if(!indicator_fakeouts() && \ + HAS_TRAIT_FROM(attached_mob, TRAIT_INCAPACITATED, TRAIT_STATUS_EFFECT(STAT_TRAIT)) || \ + HAS_TRAIT(attached_mob, TRAIT_CRITICAL_CONDITION) || \ + HAS_TRAIT_FROM(attached_mob, TRAIT_IMMOBILIZED, TRAIT_STATUS_EFFECT(STAT_TRAIT)) || \ + HAS_TRAIT_FROM(attached_mob, TRAIT_IMMOBILIZED, CHOKEHOLD_TRAIT) || \ + HAS_TRAIT_FROM(attached_mob, TRAIT_INCAPACITATED, TRAIT_STATUS_EFFECT(STAT_TRAIT)) || \ + HAS_TRAIT_FROM(attached_mob, TRAIT_IMMOBILIZED, TRAIT_STATUS_EFFECT(STAT_TRAIT)) + ) + return TRUE + +/// Returns true if the mob is paralyzed - for can't fight back purposes. +/datum/component/status_indicator/proc/is_paralyzed() + if(!indicator_fakeouts() && \ + attached_mob.IsParalyzed() || \ + HAS_TRAIT_FROM(attached_mob, TRAIT_FLOORED, CHOKEHOLD_TRAIT) || \ + HAS_TRAIT_FROM(attached_mob, TRAIT_IMMOBILIZED, TRAIT_STATUS_EFFECT(STAT_TRAIT)) || \ + HAS_TRAIT(attached_mob, TRAIT_CRITICAL_CONDITION) || \ + HAS_TRAIT_FROM(attached_mob, TRAIT_INCAPACITATED, STAMINA)) + return TRUE + +/// Returns true if the mob is unconcious for any reason. +/datum/component/status_indicator/proc/is_unconcious() + if(!indicator_fakeouts() && HAS_TRAIT(attached_mob, TRAIT_KNOCKEDOUT)) + return TRUE + +/// Returns true if the mob has confusion. +/datum/component/status_indicator/proc/is_confused() + if(!indicator_fakeouts() && attached_mob.has_status_effect(/datum/status_effect/confusion)) + return TRUE + +/datum/component/status_indicator/RegisterWithParent() + attached_mob = parent + // The Basics + RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(cut_indicators_overlays)) + RegisterSignal(parent, COMSIG_LIVING_LIFE, PROC_REF(status_indicator_evaluate)) + // When things actually happen + RegisterSignal(parent, COMSIG_LIVING_STATUS_STUN, PROC_REF(status_indicator_evaluate)) + RegisterSignal(parent, COMSIG_LIVING_STATUS_KNOCKDOWN, PROC_REF(status_indicator_evaluate)) + RegisterSignal(parent, COMSIG_LIVING_STATUS_PARALYZE, PROC_REF(status_indicator_evaluate)) + RegisterSignal(parent, COMSIG_LIVING_STATUS_IMMOBILIZE, PROC_REF(status_indicator_evaluate)) + RegisterSignal(parent, COMSIG_LIVING_STATUS_UNCONSCIOUS, PROC_REF(status_indicator_evaluate)) + RegisterSignal(parent, COMSIG_MOB_LOGIN, PROC_REF(apply_pref_on_login)) + +/datum/component/status_indicator/proc/apply_pref_on_login() + var/atom/movable/screen/plane_master/game_world_upper_fov_hidden/status_indicator/local_status = locate() in attached_mob.client.screen + if(local_status) + . = attached_mob.client.prefs.read_preference(/datum/preference/toggle/enable_status_indicators) + local_status.alpha = (.) ? 255 : 0 + +/datum/component/status_indicator/UnregisterFromParent() + QDEL_NULL(status_indicators) + UnregisterSignal(attached_mob, COMSIG_LIVING_DEATH) + UnregisterSignal(attached_mob, COMSIG_LIVING_LIFE) + UnregisterSignal(attached_mob, COMSIG_LIVING_STATUS_STUN) + UnregisterSignal(attached_mob, COMSIG_LIVING_STATUS_KNOCKDOWN) + UnregisterSignal(attached_mob, COMSIG_LIVING_STATUS_PARALYZE) + UnregisterSignal(attached_mob, COMSIG_LIVING_STATUS_IMMOBILIZE) + UnregisterSignal(attached_mob, COMSIG_LIVING_STATUS_UNCONSCIOUS) + attached_mob = null +/// This proc makes it so that mobs that have status indicators are checked to remove them, especially in fakeout situations. +/datum/component/status_indicator/proc/check_indicators() + if(status_indicators) + status_indicator_evaluate() + +/// Receives signals to update on carbon health updates. Checks if the mob is dead - if true, removes all the indicators. Then, we determine what status indicators the mob should carry or remove. +/datum/component/status_indicator/proc/status_indicator_evaluate() + SIGNAL_HANDLER + if(attached_mob.stat == DEAD) + return + else + weaken_indicator_update() + paralyzed_indicator_update() + stunned_indicator_update() + unconcious_indicator_update() + confused_indicator_update() + return +/// Cases in which no status indicators should appear above a mob, such as changeling revive and regen coma. +/datum/component/status_indicator/proc/indicator_fakeouts() + if(HAS_TRAIT(attached_mob, TRAIT_DEATHCOMA)) + return TRUE + return FALSE + +/datum/component/status_indicator/proc/weaken_indicator_update() + SIGNAL_HANDLER + is_weakened() ? add_status_indicator(WEAKEN) : remove_status_indicator(WEAKEN) +/datum/component/status_indicator/proc/paralyzed_indicator_update() + SIGNAL_HANDLER + is_paralyzed() ? add_status_indicator(PARALYSIS) : remove_status_indicator(PARALYSIS) +/datum/component/status_indicator/proc/stunned_indicator_update() + SIGNAL_HANDLER + is_stunned() ? add_status_indicator(STUNNED) : remove_status_indicator(STUNNED) +/datum/component/status_indicator/proc/unconcious_indicator_update() + SIGNAL_HANDLER + is_unconcious() ? add_status_indicator(SLEEPING) : remove_status_indicator(SLEEPING) +/datum/component/status_indicator/proc/confused_indicator_update() + SIGNAL_HANDLER + is_confused() ? add_status_indicator(CONFUSED) : remove_status_indicator(CONFUSED) + +/// Adds a status indicator to the mob. Takes an image as an argument. If it exists, it won't dupe it. +/datum/component/status_indicator/proc/add_status_indicator(image/prospective_indicator) + if(get_status_indicator(prospective_indicator)) // No duplicates, please. + return + + prospective_indicator = GLOB.potential_indicators[prospective_indicator] + prospective_indicator.loc = src + LAZYADD(status_indicators, prospective_indicator) + handle_status_indicators(prospective_indicator) + +/// Similar to add_status_indicator() but removes it instead, and nulls the list if it becomes empty as a result. +/datum/component/status_indicator/proc/remove_status_indicator(image/prospective_indicator) + prospective_indicator = get_status_indicator(prospective_indicator) + + attached_mob.cut_overlay(prospective_indicator) + LAZYREMOVE(status_indicators, prospective_indicator) + handle_status_indicators(prospective_indicator) + +/// Finds a status indicator on a mob. +/datum/component/status_indicator/proc/get_status_indicator(image/prospective_indicator) + + for(var/image/indicator in status_indicators) + if(indicator.icon_state == prospective_indicator) + return indicator + return LAZYACCESS(status_indicators, LAZYFIND(status_indicators, prospective_indicator)) + +/// Cuts all the indicators on a mob in a loop. +/datum/component/status_indicator/proc/cut_indicators_overlays() + SIGNAL_HANDLER + for(var/prospective_indicator in status_indicators) + attached_mob.cut_overlay(prospective_indicator) + +/// Refreshes the indicators over a mob's head. Should only be called when adding or removing a status indicator with the above procs, +/// or when the mob changes size visually for some reason. +/datum/component/status_indicator/proc/handle_status_indicators(image/prospective_indicator) + // First, get rid of all the overlays. + + cut_indicators_overlays() + + if(!LAZYLEN(status_indicators)) + return + + var/mob/living/carbon/my_carbon_mob = attached_mob + + var/icon_scale = get_icon_scale(my_carbon_mob) + + if(my_carbon_mob.stat == DEAD) + cut_indicators_overlays() + return + + // Now put them back on in the right spot. + var/our_sprite_x = 16 * icon_scale + var/our_sprite_y = 24 * icon_scale + + var/x_offset = our_sprite_x // Add your own offset here later if you want. + var/y_offset = our_sprite_y + STATUS_INDICATOR_Y_OFFSET + + // Calculates how 'long' the row of indicators and the margin between them should be. + // The goal is to have the center of that row be horizontally aligned with the sprite's center. + var/expected_status_indicator_length = (STATUS_INDICATOR_ICON_X_SIZE * status_indicators.len) + (STATUS_INDICATOR_ICON_MARGIN * max(status_indicators.len - 1, 0)) + var/current_x_position = (x_offset / 2) - (expected_status_indicator_length / 2) + + // In /mob/living's `update_transform()`, the sprite is horizontally shifted when scaled up, so that the center of the sprite doesn't move to the right. + // Because of that, this adjustment needs to happen with the future indicator row as well, or it will look bad. + current_x_position -= 16 * (icon_scale - DEFAULT_MOB_SCALE) + + // Now the indicator row can actually be built. + for(var/all_indicators in status_indicators) + var/image/indicator = all_indicators + + // This is a semi-HUD element, in a similar manner as medHUDs, in that they're 'above' everything else in the world, + // but don't pierce obfuscation layers such as blindness or darkness, unlike actual HUD elements like inventory slots. + indicator.plane = GAME_PLANE_UPPER_FOV_HIDDEN + indicator.layer = STATUS_LAYER + indicator.appearance_flags = PIXEL_SCALE|TILE_BOUND|NO_CLIENT_COLOR|RESET_COLOR|RESET_ALPHA|RESET_TRANSFORM|KEEP_APART + indicator.pixel_y = y_offset + indicator.pixel_x = current_x_position + my_carbon_mob.add_overlay(indicator) + // Adding the margin space every time saves a conditional check on the last iteration, + // and it won't cause any issues since no more icons will be added, and the var is not used for anything else. + current_x_position += STATUS_INDICATOR_ICON_X_SIZE + STATUS_INDICATOR_ICON_MARGIN + +/datum/component/status_indicator/proc/get_icon_scale(livingmob) + if(!iscarbon(livingmob)) // normal mobs are always 1 for scale - hopefully all borgs and simplemobs get this one + return DEFAULT_MOB_SCALE + var/mob/living/carbon/passed_mob = livingmob // we're possibly a player! We have size prefs! + var/mysize = (passed_mob.dna?.current_body_size ? passed_mob.dna.current_body_size : DEFAULT_MOB_SCALE) + return mysize + +/atom/movable/screen/plane_master/game_world_upper_fov_hidden/status_indicator + name = "Status Indicator Plane" + documentation = "Status Indicator Plane" + plane = GAME_PLANE_UPPER_FOV_HIDDEN + start_hidden = FALSE + +#undef STATUS_INDICATOR_Y_OFFSET +#undef STATUS_INDICATOR_ICON_X_SIZE +#undef STATUS_INDICATOR_ICON_MARGIN diff --git a/modular_zubbers/code/modules/status_indicators/status_indicator_pref.dm b/modular_zubbers/code/modules/status_indicators/status_indicator_pref.dm new file mode 100644 index 00000000000..87a8fd20f18 --- /dev/null +++ b/modular_zubbers/code/modules/status_indicators/status_indicator_pref.dm @@ -0,0 +1,26 @@ +/datum/preference/toggle/enable_status_indicators + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "enable_status_indicators" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/enable_status_indicators/create_default_value() + return TRUE + + +/datum/preference/toggle/enable_status_indicators/apply_to_client(client/client, value) + if(client.mob) + var/datum/component/status_indicator/mystatus = client.mob.GetComponent(/datum/component/status_indicator) + if(mystatus) + mystatus.apply_pref_on_login() + +/* if(client && client.prefs) + . = client?.prefs?.read_preference(/datum/preference/toggle/enable_status_indicators) + var/atom/movable/screen/plane_master/status/status_indicators = locate() in client.screen + if(isnull(status_indicators)) + new /atom/movable/screen/plane_master/status in client.screen + status_indicators = locate() in client.screen + if(length(status_indicators) < 1) + pop(status_indicators) + (.) ? (status_indicators.alpha = 255) : (status_indicators.alpha = 0) + + */ diff --git a/modular_zubbers/icons/mob/status_indicators.dmi b/modular_zubbers/icons/mob/status_indicators.dmi new file mode 100644 index 00000000000..9c80c2bf10a Binary files /dev/null and b/modular_zubbers/icons/mob/status_indicators.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 90317860438..e65bc73ba9d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -390,6 +390,7 @@ #include "code\__DEFINES\~~bubber_defines\access.dm" #include "code\__DEFINES\~~bubber_defines\quirk_whitelist.dm" #include "code\__DEFINES\~~bubber_defines\species.dm" +#include "code\__DEFINES\~~bubber_defines\status_indicator_defines.dm" #include "code\__DEFINES\~~bubber_defines\traits.dm" #include "code\__HELPERS\_auxtools_api.dm" #include "code\__HELPERS\_lists.dm" @@ -6724,6 +6725,8 @@ #include "modular_zubbers\code\modules\mod\mod_theme.dm" #include "modular_zubbers\code\modules\mod\mod_types.dm" #include "modular_zubbers\code\modules\projectiles\guns\energy\pulse.dm" +#include "modular_zubbers\code\modules\status_indicators\status_indicator.dm" +#include "modular_zubbers\code\modules\status_indicators\status_indicator_pref.dm" #include "modular_zubbers\code\modules\surgery\bodyparts\species_parts\misc_bodyparts.dm" #include "modular_zubbers\code\modules\vending\wardrobe.dm" // END_INCLUDE diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/status_indicators.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/status_indicators.tsx new file mode 100644 index 00000000000..bb1eac18f2c --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/status_indicators.tsx @@ -0,0 +1,8 @@ +import { CheckboxInput, FeatureToggle } from '../../base'; + +export const enable_status_indicators: FeatureToggle = { + name: 'Display Status Indicators', + category: 'GAMEPLAY', + description: 'This toggles whether or not you will see status indicators.', + component: CheckboxInput, +};