diff --git a/code/_onclick/hud/_defines.dm b/code/_onclick/hud/_defines.dm index 9ec19e5f057..60b5035a09c 100644 --- a/code/_onclick/hud/_defines.dm +++ b/code/_onclick/hud/_defines.dm @@ -96,6 +96,11 @@ #define ui_health_east_loc "EAST-1:28" // used to manipulate the position of the healths screen element, must be same as the one above #define ui_health_east_template "EAST-1:" // ditto #define ui_internal "EAST-1:28,CENTER+1:17" +/** + * Location that the morale component will place its HUD element, + * which is defined here for your sanity as a maintainer so that you know where it is in reference to everything else. + */ +#define UI_MORALE_LOCATION "EAST-1:28,CENTER+2:19" //Upper-middle right (alerts) #define ui_alert1 "EAST-1:28,CENTER+5:27" @@ -170,3 +175,6 @@ #define ui_ai_view_images "SOUTH:6,WEST+11:16" #define ui_ai_move_up "SOUTH:6,WEST+12:16" #define ui_ai_move_down "SOUTH:6,WEST+13:16" + +// HUD element related signals +#define COMSIG_GET_HUD_ELEMENTS "get_hud_elements" diff --git a/code/datums/components/morale/moodlets.dm b/code/datums/components/morale/moodlets.dm index 749470fdb28..6d04bcc19ff 100644 --- a/code/datums/components/morale/moodlets.dm +++ b/code/datums/components/morale/moodlets.dm @@ -15,10 +15,11 @@ ABSTRACT_TYPE(/datum/moodlet) */ VAR_PRIVATE/morale_modifier = 0.0 // Positive and negative floating points are allowed. - /** - * - */ - var/moodlet_descriptor = "It's a moodlet!" + /// The moodlet description to display to chat when someone with a morale component clicks on their morale icon. + var/moodlet_descriptor = SPAN_GOOD("It's a moodlet!") + + /// The moodlet description that gets sent to the chat of someone when they first obtain a moodlet. + var/initial_descriptor = SPAN_GOOD("You have gained a morale modifier from a moodlet!") /** * How long this moodlet will last if not refreshed. For Aurora's purposes, moodlets are targeted as having "Small effect, extreme duration". @@ -45,20 +46,24 @@ ABSTRACT_TYPE(/datum/moodlet) morale_component = WEAKREF(_morale_component) if (set_points) morale_modifier = set_points _morale_component.add_morale_points(morale_modifier) + send_initial_description(_morale_component.parent) /datum/moodlet/Destroy(force) if (force) // This will be forced when a Morale Component is deleted directly, as it QDEL_NULL_LIST's its own moodlets. + morale_component = null return ..() // Else if the moodlet is deleted directly rather than its parent. var/datum/component/morale/parent = morale_component.resolve() if (!parent || !parent.moodlets[src]) + morale_component = null return ..() // Clean the effects of this moodlet from the parent. parent.moodlets -= src parent.add_morale_points(-morale_modifier) + morale_component = null return ..() /datum/moodlet/proc/get_morale_modifier() @@ -81,3 +86,25 @@ ABSTRACT_TYPE(/datum/moodlet) /datum/moodlet/proc/refresh_moodlet() time_to_die = REALTIMEOFDAY + duration + +/** + * Returns a descriptor for the moodlet. + * + * Before you ask why this exists, + * know that some moodlets will override this with more complicated logic. + */ +/datum/moodlet/proc/get_moodlet_descriptor() + var/remaining_duration = time_to_die - REALTIMEOFDAY + var/seconds = round(remaining_duration % 600) + var/minutes = round((remaining_duration - seconds) / 600) + + return "\t [moodlet_descriptor]\n" \ + + "\t - worth [morale_modifier] points\n" \ + + "\t - remaining duration: [minutes] minutes, [round(seconds / 10)] seconds" + +/** + * Similarly to get_moodlet_descriptor(), some moodlets are going to want to override this with more complicated effects. + * By default, this just sends an initial descriptor. + */ +/datum/moodlet/proc/send_initial_description(datum/owner) + to_chat(owner, initial_descriptor) diff --git a/code/datums/components/morale/morale_component.dm b/code/datums/components/morale/morale_component.dm index 610837e1d97..ddf6eedf277 100644 --- a/code/datums/components/morale/morale_component.dm +++ b/code/datums/components/morale/morale_component.dm @@ -1,3 +1,30 @@ +/// Screen space movable object for the morale component. +/atom/movable/screen/morale + name = "morale" + icon = 'icons/mob/screen/morale_ui.dmi' + icon_state = "morale_hidden" + screen_loc = UI_MORALE_LOCATION + +/atom/movable/screen/morale/Click(location, control, params) + if (!istype(usr)) + return + + var/datum/component/morale/morale_comp = usr.GetComponent(MORALE_COMPONENT) + if (!morale_comp) + qdel(src) // whoops the attached mob doesn't have a morale component. + return + + if (!length(morale_comp.moodlets)) + to_chat(usr, SPAN_NOTICE("You currently have no morale modifiers.")) + return + + // This section is Unlinted since we need to access a same-file PRIVATE var + // and for whatever ungodly reason strongdmm doesn't allow same-file to touch VAR_PRIVATE + UNLINT(to_chat(usr, SPAN_NOTICE("Your current morale bonus is [morale_comp.morale_ratio * 100]%"))) + to_chat(usr, SPAN_NOTICE("You have the following morale modifiers: ")) + for (var/datum/moodlet/moodlet as anything in morale_comp.moodlets) + to_chat(usr, moodlet.get_moodlet_descriptor()) + /** * Having a Morale Component allows a character to receive and benefit from Moodlets, providing a variety of buffs(or debuffs) depending on the total morale points. * This component acts as both proof a mob can be affected by morale, as well as a method of tracking the effects of morale on each system. @@ -38,6 +65,15 @@ */ VAR_PRIVATE/beta_value = 0.0195 + /** + * UI element stored for the morale component, + * which is attached to the screen of a client controlling a character with this component. + * Its lifecycle is strictly controlled by this component, do not under any circumstances touch it from outside this file. + * + * You have been warned. + */ + VAR_PRIVATE/atom/movable/screen/morale/morale_ui + // By default, all of these values are roughly equivalent to "up to half" a skill rank. /// How much this morale component contributes to signal based unarmed values. var/unarmed_chance_contribution = 2.5 @@ -64,6 +100,12 @@ morale_points += input morale_ratio = ftanh(beta_value * morale_points) + // I get to do this freakishly compact state setting because I can + // logically prove via VAR_PRIVATE that this is only ever set via a hyperbolic tangent + // And that because a hyperbolic tangent will only ever return a value between -1 and 1, + // the range of this equation becomes the set of integers between 1 and 9 inclusive. + morale_ui.icon_state = ((morale_ratio > -0.0001 && morale_ratio < 0.0001) ? "morale_hidden" : "morale" + "[round(morale_ratio * 4) + 5]") + /datum/component/morale/proc/set_beta_value(input) beta_value = input morale_ratio = ftanh(beta_value * morale_points) @@ -78,6 +120,7 @@ if (!loaded_moodlet) loaded_moodlet = new moodlet_type(src, set_points) moodlets.Add(loaded_moodlet) + START_PROCESSING(SSprocessing, src) return loaded_moodlet if (set_points) loaded_moodlet.set_moodlet(set_points) @@ -88,6 +131,11 @@ if (!parent) return + // Generate the morale UI in advance. + morale_ui = new /atom/movable/screen/morale() + // Separate from the general morale interactions, this one is special for generating the morale HUD elements. + RegisterSignal(parent, COMSIG_MOB_UPDATE_VISION, PROC_REF(get_morale_hud), override = TRUE) + // Behold my wall of RegisterSignal() RegisterSignal(parent, COMSIG_APPLY_HIT_EFFECT, PROC_REF(modify_hit_effect), override = TRUE) RegisterSignal(parent, COMSIG_BEFORE_GUN_FIRE, PROC_REF(handle_accuracy), override = TRUE) @@ -106,6 +154,10 @@ if (!parent) return ..() + // Cleanup morale HUD elements. + UnregisterSignal(parent, COMSIG_MOB_UPDATE_VISION) + QDEL_NULL(morale_ui) + // Behold my wall of UnregisterSignal() UnregisterSignal(parent, COMSIG_APPLY_HIT_EFFECT) UnregisterSignal(parent, COMSIG_BEFORE_GUN_FIRE) @@ -121,10 +173,13 @@ return ..() /datum/component/morale/process(seconds_per_tick) + if (!length(moodlets)) + return PROCESS_KILL + var/current_time = REALTIMEOFDAY var/list_trimmed = FALSE for (var/datum/moodlet/moodlet as anything in moodlets) - if (moodlet.time_to_die < current_time || QDELING(moodlet)) + if (moodlet.time_to_die > current_time || QDELING(moodlet)) continue morale_points -= moodlet.get_morale_modifier() @@ -237,3 +292,10 @@ /datum/component/morale/proc/handle_surgery_modifiers(mob/living/user, success_rate) SIGNAL_HANDLER *success_rate = *success_rate + surgery_success_contribution * morale_ratio + +/datum/component/morale/proc/get_morale_hud(mob/living/carbon/human/user) + SIGNAL_HANDLER + if (QDELING(src)) + return // draw nothing. + + user.client?.screen |= morale_ui diff --git a/code/datums/components/skills/service/bartending_skill_component.dm b/code/datums/components/skills/service/bartending_skill_component.dm index 2377499dcf3..044ee518317 100644 --- a/code/datums/components/skills/service/bartending_skill_component.dm +++ b/code/datums/components/skills/service/bartending_skill_component.dm @@ -1,6 +1,8 @@ /datum/component/skill/bartending /datum/moodlet/bartender_drink + moodlet_descriptor = SPAN_GOOD("Consumed a skillfully prepared drink.") + initial_descriptor = SPAN_GOOD("You have gained a morale modifier from consuming a skillfully prepared drink.") /datum/component/drink_moodlet_provider /// The morale boosting value of the moodlet this drink will provide. diff --git a/html/changelogs/hellfirejag-moodlet-ui.yml b/html/changelogs/hellfirejag-moodlet-ui.yml new file mode 100644 index 00000000000..1a3d5a260b8 --- /dev/null +++ b/html/changelogs/hellfirejag-moodlet-ui.yml @@ -0,0 +1,4 @@ +author: Hellfirejag +delete-after: True +changes: + - rscadd: "Added a UI indicator for Morale modifiers." diff --git a/icons/mob/screen/morale_ui.dmi b/icons/mob/screen/morale_ui.dmi new file mode 100644 index 00000000000..ce8b1910c97 Binary files /dev/null and b/icons/mob/screen/morale_ui.dmi differ