Resolve Skill

This commit is contained in:
VMSolidus
2026-07-03 15:31:16 -04:00
parent 92e4ea7f7d
commit a81df5e16e
4 changed files with 73 additions and 18 deletions
@@ -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
+1 -1
View File
@@ -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
+29
View File
@@ -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))
@@ -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."