diff --git a/code/datums/components/morale/morale_component.dm b/code/datums/components/morale/morale_component.dm index 9a0a2593dcb..19c34bfc8b3 100644 --- a/code/datums/components/morale/morale_component.dm +++ b/code/datums/components/morale/morale_component.dm @@ -1,3 +1,8 @@ +#define UPDATE_MORALE \ + morale_ratio = ftanh(beta_value * (morale_points + phi_value)); \ + morale_ui.icon_state = ((morale_ratio > -0.01 && morale_ratio < 0.01) ? "morale_hidden" : "morale" + "[round(morale_ratio * 4) + 5]"); \ + morale_ui.update_icon(); + /// Screen space movable object for the morale component. /atom/movable/screen/morale name = "morale" @@ -14,13 +19,18 @@ qdel(src) // whoops the attached mob doesn't have a morale component. return - if (!length(morale_comp.moodlets)) + // 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 + var/morale_percent = UNLINT(morale_comp.morale_ratio * 100) + if (!morale_percent) 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("Your current morale bonus is [morale_percent]%")) + + if (!length(morale_comp.moodlets)) + return + 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()) @@ -54,17 +64,25 @@ VAR_PRIVATE/morale_ratio = 0.0 // Positive and negative floating points are allowed. /** - * The "B" constant in the equation for y = Atanh(Bx + C). + * The "B" constant in the equation for y = Atanh(B(x + C)). * This constant is not arbitrary, it was carefully selected such that the equation will give "75% of its effect" at 50 morale points, and "96% of its effect" at 100 morale. * This allows for there to be an effect of diminishing returns for chasing ever increasingly more morale points, while front-loading the bulk of the effects at a specific amount of moodlets. * Since the effects of morale are a "Logistic Curve", "100% of the morale effect" is only ever obtained at +INFINITY. * This also goes for the opposite direction, morale penalties max out only at -INFINITY points, but get to "75% of the penalty effect" at -50 points. * - * The actual "Effects" of morale are to be per-signal, and are defined by the A value in y = Atanh(Bx + C) + * The actual "Effects" of morale are to be per-signal, and are defined by the A value in y = Atanh(B(x + C)) * This is private for a reason, if you need to change it, do so by using set_beta_value(), which will also make the component recalculate its morale ratio. */ VAR_PRIVATE/beta_value = 0.0195 + /** + * The "C" constant in the equation for y = Atanh(B(x + C)) + * This constant acts like a "permanent" moodlet with a morale bonus equal to what its set as. + * Please set this extremely sparingly. You won't necessarily break anything by setting it, since the system will tolerate any number between +/- Infinity. + * But making a habit of bypassing the morale system like this can easily trivialize it. + */ + VAR_PRIVATE/phi_value = 0.0 + /** * UI element stored for the morale component, * which is attached to the screen of a client controlling a character with this component. @@ -129,20 +147,20 @@ /datum/component/morale/proc/add_morale_points(input) morale_points += input - morale_ratio = ftanh(beta_value * morale_points) + UPDATE_MORALE - // 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.01 && morale_ratio < 0.01) ? "morale_hidden" : "morale" + "[round(morale_ratio * 4) + 5]") - morale_ui.update_icon() +/datum/component/morale/proc/get_beta_value() + return beta_value /datum/component/morale/proc/set_beta_value(input) beta_value = input - morale_ratio = ftanh(beta_value * morale_points) - morale_ui.icon_state = ((morale_ratio > -0.01 && morale_ratio < 0.01) ? "morale_hidden" : "morale" + "[round(morale_ratio * 4) + 5]") - morale_ui.update_icon() + +/datum/component/morale/proc/get_phi_value() + return phi_value + +/datum/component/morale/proc/set_phi_value(input) + phi_value = input + UPDATE_MORALE /** * Your one-stop-shop for making moodlets. This proc returns the pre-existing moodlet of a given type. @@ -184,6 +202,7 @@ RegisterSignal(parent, COMSIG_GET_SURGERY_SUCCESS_MODIFIERS, PROC_REF(handle_surgery_modifiers), override = TRUE) RegisterSignal(parent, COMSIG_GET_CRAFTING_MODIFIERS, PROC_REF(handle_crafting_speed), override = TRUE) RegisterSignal(parent, COMSIG_PLANT_HARVESTER, PROC_REF(modify_yield), override = TRUE) + UPDATE_MORALE /datum/component/morale/Destroy() QDEL_LIST_FORCE(moodlets) @@ -228,7 +247,7 @@ if (!list_trimmed) return - morale_ratio = ftanh(morale_points) + UPDATE_MORALE /* AND NOW THE GIANT WALL OF SIGNAL HANDLERS @@ -347,3 +366,5 @@ SIGNAL_HANDLER *total_yield = *total_yield + plant_yield_contribution * morale_ratio *doafter = *doafter * (1 - (harvest_speed_contribution * morale_ratio)) + +#undef UPDATE_MORALE diff --git a/code/datums/skills/_skills.dm b/code/datums/skills/_skills.dm index 29d8b1ab319..e07eb0f2f30 100644 --- a/code/datums/skills/_skills.dm +++ b/code/datums/skills/_skills.dm @@ -81,7 +81,7 @@ * It will be called during the process of spawning a player character in. */ /singleton/skill/proc/on_spawn(mob/owner, skill_level) - SHOULD_CALL_PARENT(TRUE) + // Note that this can be directly overridden for skills that wish to use the ECS hook to provide an effect other than the standard "adding a component". if (!owner || !component_type || (!required && skill_level == SKILL_LEVEL_UNFAMILIAR)) return diff --git a/code/datums/skills/everyday/service.dm b/code/datums/skills/everyday/service.dm index 56962d34c1f..5d69c64777d 100644 --- a/code/datums/skills/everyday/service.dm +++ b/code/datums/skills/everyday/service.dm @@ -79,3 +79,32 @@ SKILL_LEVEL_TRAINED = "You gain the \"Offer Blessing\" ability, which provides a modest morale bonus to a recipient.", SKILL_LEVEL_PROFESSIONAL = "You gain the \"Offer Blessing\" ability, which provides a moderate morale bonus to a recipient." ) + +/singleton/skill/resolve + name = "Resolve" + description = "Represents a character's mental resilience, emotional stability, and ability to remain steadfast under stress. " \ + + "Characters with greater Resolve receive a permanent passive bonus to their effective morale value. This can provide small bonuses to a variety of interactions. " \ + + "This morale bonus can also act as a pool of resistance towards psychic damage. " \ + + "This skill has no effect species which does not interact with Morale." + maximum_level = SKILL_LEVEL_PROFESSIONAL + category = /singleton/skill_category/everyday + subcategory = SKILL_SUBCATEGORY_PHYSICAL + component_type = null // No component, this skill is flexing the ECS hook in a different way entirely. + skill_level_descriptions = alist( + SKILL_LEVEL_UNFAMILIAR = "You have no modifiers from Resolve.", + SKILL_LEVEL_FAMILIAR = "You have a passive permanent bonus of +5 morale points.", + SKILL_LEVEL_TRAINED = "You have a passive permanent bonus of +10 morale points.", + SKILL_LEVEL_PROFESSIONAL = "You have a passive permanent bonus of +15 morale points." + ) + +/singleton/skill/resolve/on_spawn(mob/owner, skill_level) + var/mob/living/carbon/character = astype(owner) + if (!character || skill_level == SKILL_LEVEL_UNFAMILIAR || !character.species || !character.species.has_morale) + return + + // Using LoadComponent here lets me defeat any possible race conditions coming out of character creation. + // Uniquely among skills, this skill works by modifying a pre-existing component used by player characters, rather than adding its own. + // If you're a contributor reading up on skills, consider this your tutorial on how to make a skill modify some statistic directly. + var/datum/component/morale/morale_comp = character.LoadComponent(MORALE_COMPONENT) + morale_comp.set_phi_value(morale_comp.get_phi_value() + (5 * skill_level)) + diff --git a/html/changelogs/hellfirejag-resolve-skill.yml b/html/changelogs/hellfirejag-resolve-skill.yml new file mode 100644 index 00000000000..461b96b9353 --- /dev/null +++ b/html/changelogs/hellfirejag-resolve-skill.yml @@ -0,0 +1,5 @@ +author: Hellfirejag +delete-after: True +changes: + - rscadd: "Added a new Resolve skill to the Everyday category. Resolve as a skill provides a small passive bonus to a characters Morale statistic." + - bugfix: "Fixed Morale not properly updating its effects when a moodlet (such as Psychic Damage) expires."