Morale Modifying Skills (#22508)

This PR adds a new Everyday skill called Ministry, which is targeted
both for the Chaplain role, as well as for characters with Theological
training in general. It provides a character with a unique action called
"Offer Blessing", appearing in the action bar on the top-left corner of
the screen. When used on an adjacent character, it offers a series of RP
prompts for the user to write a blessing of their choice to said
character.

If the blessing is accepted and received, the recipient gains a small
Morale Modifier that scales with the user's Ministry skill ranks.
The morale modifier is also increased if the two characters involved
share a religion in common.

<img width="1907" height="971" alt="image"
src="https://github.com/user-attachments/assets/fef51432-2206-4513-a734-fe62a3acfc6c"
/>

<img width="357" height="94" alt="image"
src="https://github.com/user-attachments/assets/2f344d04-10c9-4fbc-9183-3471c4b51e7c"
/>

<img width="322" height="148" alt="image"
src="https://github.com/user-attachments/assets/fb5cc21f-60bb-4c9f-ba5b-45f6e3ec94ff"
/>

<img width="703" height="34" alt="image"
src="https://github.com/user-attachments/assets/8df6b6c3-8ea7-4f64-b8c4-a439ccadc076"
/>

<img width="361" height="83" alt="image"
src="https://github.com/user-attachments/assets/fe7b6d32-7cb5-4e60-89d8-38353981ffb2"
/>

There is also now a similar skill for the combat category, called
Leadership.

<img width="1268" height="987" alt="image"
src="https://github.com/user-attachments/assets/44448560-1ff7-4bae-966b-1b8f7481005d"
/>

<img width="912" height="67" alt="image"
src="https://github.com/user-attachments/assets/30531b15-0aa3-4ab6-b75c-3af26f8b5bdd"
/>


I have fully tested this PR to its completion.

### Asset Licenses
The following assets that **have not** been created by myself are
included in this PR:

| Path | Original Author | License |
| icons/hud/action_buttons/skills.dmi | gabrielgabey (Discord) |
CC-BY-SA-3.0 |
This commit is contained in:
VMSolidus
2026-05-27 15:38:57 +00:00
committed by GitHub
parent e9f1bb5709
commit fe4f84703c
13 changed files with 420 additions and 14 deletions
@@ -26,6 +26,9 @@
RegisterSignal(parent, COMSIG_PSI_CHECK_SENSITIVITY, PROC_REF(modify_sensitivity), override = TRUE)
RegisterSignal(parent, COMSIG_PSI_MIND_POWER, PROC_REF(cancel_power), override = TRUE)
RegisterSignal(parent, COMSIG_GET_MINISTRY_MODIFIERS, PROC_REF(modify_ministry_empathy), override = TRUE)
RegisterSignal(parent, COMSIG_RECEIVE_MINISTRY_MODIFIERS, PROC_REF(modify_ministry_receiving), override = TRUE)
RegisterSignal(parent, COMSIG_GET_LEADERSHIP_MODIFIERS, PROC_REF(modify_leadership_empathy), override = TRUE)
/datum/component/timed_life/psiblock_drugs/Destroy()
owner = null
@@ -39,6 +42,9 @@
UnregisterSignal(parent, COMSIG_PSI_CHECK_SENSITIVITY)
UnregisterSignal(parent, COMSIG_PSI_MIND_POWER)
UnregisterSignal(parent, COMSIG_GET_MINISTRY_MODIFIERS)
UnregisterSignal(parent, COMSIG_RECEIVE_MINISTRY_MODIFIERS)
UnregisterSignal(parent, COMSIG_GET_LEADERSHIP_MODIFIERS)
return ..()
/datum/component/timed_life/psiblock_drugs/proc/modify_sensitivity(parent, effective_sensitivity)
@@ -51,6 +57,31 @@
if (prob(telepathy_cancel_probability))
*cancelled = TRUE
/datum/component/timed_life/psiblock_drugs/proc/modify_ministry_empathy(minister, ministree, moodlet_value)
SIGNAL_HANDLER
if (!(*moodlet_value))
return
to_chat(minister, SPAN_BAD("Why should you care how [ministree] feels?"))
*moodlet_value = 0
/datum/component/timed_life/psiblock_drugs/proc/modify_ministry_receiving(ministree, minister, moodlet_value)
SIGNAL_HANDLER
if (!(*moodlet_value))
return
to_chat(ministree, SPAN_BAD("You feel nothing from [minister]'s words."))
*moodlet_value = 0
/datum/component/timed_life/psiblock_drugs/proc/modify_leadership_empathy(leader, moodlet_value)
SIGNAL_HANDLER
if (!(*moodlet_value))
return
to_chat(leader, SPAN_BAD("Why should you care about how others feel?"))
*moodlet_value = 0
/datum/component/timed_life/psiblock_drugs/process(seconds_per_tick)
. = ..()
@@ -1,6 +1,165 @@
#define GET_SPEECH_TYPE(owner) alert(owner.client, "What type of chat message would you like to use for your inspirational speech?", "Say Type", "Say", "Whisper", "Emote")
#define GET_SPEECH_TEXT(owner) tgui_input_text(owner, "Write what you wish to say for your speech.", "Deliver Speech")
/datum/moodlet/leadership
moodlet_descriptor = SPAN_GOOD("Listened to an encouraging speech")
initial_descriptor = SPAN_GOOD("You have received a morale modifier from hearing words of encouragement.")
duration = 15 MINUTES
/datum/action/leadership
name = "Deliver Speech"
action_type = 6
procname = "queue_click"
button_icon = 'icons/hud/action_buttons/skills.dmi'
button_icon_state = "leadership"
/datum/action/leadership/proc/queue_click()
if (!owner)
return
to_chat(owner, SPAN_NOTICE("You prepare yourself to give a speech. Left click on yourself to target those who can hear you in an area. Left click on another person to instead give them directed words of encouragement. Targeting a single person will give a stronger morale modifier."))
RegisterSignal(owner, COMSIG_MOB_CLICKON, PROC_REF(get_target), override = TRUE)
/datum/action/leadership/proc/get_target(owner, atom/target, modifiers)
SIGNAL_HANDLER
// Both forms of deliver speech will immediately return control to the caller without blocking.
// Unfortunately StrongDMM is apparently incapable of reading that.
if (owner == target)
UNLINT(deliver_speech_area(owner))
else
UNLINT(deliver_speech_target(owner, target))
UnregisterSignal(owner, COMSIG_MOB_CLICKON)
return COMSIG_MOB_CANCEL_CLICKON
/datum/action/leadership/proc/deliver_speech_area(mob/owner)
set waitfor = FALSE // Immediately return control to the caller.
if (!istype(owner))
return
owner.visible_message(SPAN_NOTICE("[owner] prepares to deliver a speech."))
var/speech_text = GET_SPEECH_TEXT(owner)
if (!speech_text)
owner.visible_message(SPAN_NOTICE("[owner] has stopped speaking."))
return
owner.say(speech_text)
var/datum/component/skill/leadership/leadership_skill = owner.GetComponent(LEADERSHIP_SKILL_COMPONENT)
if (!leadership_skill)
return
var/moodlet_value = leadership_skill.moodlet_value_per_rank * (leadership_skill.skill_level - 1)
SEND_SIGNAL(owner, COMSIG_GET_LEADERSHIP_MODIFIERS, &moodlet_value)
if (!moodlet_value)
return
var/list/listening = get_hearers_in_view(world.view, owner)
if (!listening)
return
listening -= owner
for (var/mob/player_mob in listening)
var/datum/component/morale/receiver_morale = player_mob.GetComponent(MORALE_COMPONENT)
if (!receiver_morale)
continue
if (astype(receiver_morale.moodlets[/datum/moodlet/leadership], /datum/moodlet)?.get_morale_modifier() >= moodlet_value)
receiver_morale.load_moodlet(/datum/moodlet/leadership)?.refresh_moodlet()
continue // Don't overwrite stronger moodlets.
var/datum/moodlet/speech = receiver_morale.load_moodlet(/datum/moodlet/leadership, moodlet_value)
speech.refresh_moodlet()
speech.moodlet_descriptor += " from [owner.name]."
/datum/action/leadership/proc/deliver_speech_target(mob/owner, mob/target)
set waitfor = FALSE // Immediately return control to the caller.
if (!istype(owner) || !istype(target))
return
if (!target.client)
to_chat(owner, SPAN_NOTICE("[target] cannot hear your speech."))
return
owner.visible_message(SPAN_NOTICE("[owner] prepares to deliver a speech."))
var/speech_type = GET_SPEECH_TYPE(owner)
var/speech_text = GET_SPEECH_TEXT(owner)
if (!speech_text)
owner.visible_message(SPAN_NOTICE("[owner] has stopped speaking."))
return
switch(speech_type)
if ("Say")
if (get_dist(owner, target) >= 7)
to_chat(owner, SPAN_NOTICE("You must be closer to [target] to give them words of encouragement"))
return
owner.say(speech_text)
if ("Whisper")
if (get_dist(owner, target) >= 2)
to_chat(owner, SPAN_NOTICE("You must be adjacent to [target] to whisper them words of encouragement"))
return
owner.whisper(speech_text)
if ("Emote")
if (get_dist(owner, target) >= 7)
to_chat(owner, SPAN_NOTICE("You must be closer to [target] to give them words of encouragement"))
owner.emote(speech_text)
var/datum/component/skill/leadership/leadership_skill = owner.GetComponent(LEADERSHIP_SKILL_COMPONENT)
if (!leadership_skill)
return
var/moodlet_value = leadership_skill.moodlet_value_per_rank * (leadership_skill.skill_level - 1) * leadership_skill.single_target_bonus_mod
SEND_SIGNAL(owner, COMSIG_GET_LEADERSHIP_MODIFIERS, &moodlet_value)
if (!moodlet_value)
return
var/datum/component/morale/target_morale = target.GetComponent(MORALE_COMPONENT)
if (!target_morale)
return
if (astype(target_morale.moodlets[/datum/moodlet/leadership], /datum/moodlet)?.get_morale_modifier() >= moodlet_value)
target_morale.load_moodlet(/datum/moodlet/leadership)?.refresh_moodlet()
return // Don't overwrite stronger moodlets.
var/datum/moodlet/speech = target_morale.load_moodlet(/datum/moodlet/leadership, moodlet_value)
speech.refresh_moodlet()
speech.moodlet_descriptor += " from [owner.name]."
/**
* Component used for the Leadership Skill. This is not currently implemented, and will be handled in a separate PR to avoid scope creep.
* The way this will be intended to work is that having the component grants access to an "Inspire" action. Activating it will prompt the user to select a person, then prompt them to "Say something inspiring!".
* Component used for the Leadership Skill.
* The way this works is that having the component grants access to an "Inspire" action. Activating it will prompt the user to select a person, then prompt them to "Say something inspiring!".
* If the target can "Hear" the inspirational speech, they gain a morale bonus which scales with the actor's Leadership Skill.
*/
/datum/component/skill/leadership
/// The action icon stored for this ability.
var/datum/action/leadership/leadership_action
/// The value of the moodlet provided by this ability.
var/moodlet_value_per_rank = 10.0 / 3.0
/// Moodlet value multiplier for single target speeches.
var/single_target_bonus_mod = 1.5
/datum/component/skill/leadership/Initialize(level)
. = ..()
if (!parent)
return
leadership_action = new /datum/action/leadership()
leadership_action.SetTarget(leadership_action)
leadership_action.Grant(parent)
RegisterSignal(parent, COMSIG_MOB_AFTER_LOGIN, PROC_REF(setup_action_button), override = TRUE)
/datum/component/skill/leadership/Destroy(force)
if (!parent)
return ..()
UnregisterSignal(parent, COMSIG_MOB_AFTER_LOGIN)
leadership_action?.Remove(parent)
QDEL_NULL(leadership_action)
return ..()
/datum/component/skill/leadership/proc/setup_action_button()
astype(parent, /mob)?.update_action_buttons()
#undef GET_SPEECH_TYPE
#undef GET_SPEECH_TEXT
@@ -0,0 +1,132 @@
/// Moodlet used for the Ministry skill.
/datum/moodlet/ministry_blessing
moodlet_descriptor = SPAN_GOOD("Received a blessing.")
initial_descriptor = SPAN_GOOD("You have received a morale modifier from hearing a blessing.")
/// Action bar object used by the Ministry skill.
/datum/action/ministry
name = "Offer Blessing"
action_type = 6
procname = "queue_click"
button_icon = 'icons/hud/action_buttons/skills.dmi'
button_icon_state = "ministry"
/datum/action/ministry/proc/queue_click()
if (!owner)
return
to_chat(owner, SPAN_NOTICE("You prepare yourself to offer a blessing to another. Left click on an adjacent character to offer them it."))
RegisterSignal(owner, COMSIG_MOB_CLICKON, PROC_REF(get_target), override = TRUE)
/datum/action/ministry/Destroy()
if (!owner)
return ..()
UnregisterSignal(owner, COMSIG_MOB_CLICKON)
return ..()
/datum/action/ministry/proc/get_target(owner, atom/target, modifiers)
SIGNAL_HANDLER
if (owner == target)
to_chat(owner, SPAN_NOTICE("You cannot offer a blessing to yourself."))
return COMSIG_MOB_CANCEL_CLICKON
if (!astype(target, /mob)?.client)
to_chat(owner, SPAN_NOTICE("[target] cannot receive a blessing."))
return COMSIG_MOB_CANCEL_CLICKON
if (get_dist(owner, target) >= 2)
to_chat(owner, SPAN_NOTICE("You must be adjacent to [target] to offer them a blessing."))
return COMSIG_MOB_CANCEL_CLICKON
// StrongDMM for whatever ungodly reason can't tell that this proc won't block the caller.
UNLINT(try_give_blessing(target))
UnregisterSignal(owner, COMSIG_MOB_CLICKON)
return COMSIG_MOB_CANCEL_CLICKON
/datum/action/ministry/proc/try_give_blessing(mob/target)
set waitfor = FALSE
switch(alert(target.client, "Would you like to accept a blessing from [owner]? You will need to remain close to them while they speak it", "Accept Blessing", "Yes", "No"))
if ("Yes")
var/blessing_type = alert(owner.client, "What type of chat message would you like to use for your blessing?", "Say Type", "Say", "Whisper", "Emote")
var/blessing_text = tgui_input_text(owner, "Write what you wish to say as a blessing for [target].", "Offer Blessing")
if (!blessing_text)
to_chat(owner, SPAN_NOTICE("You have stopped speaking."))
to_chat(target, SPAN_NOTICE("[owner] has stopped speaking."))
return
if (get_dist(owner, target) >= 2)
to_chat(owner, SPAN_NOTICE("[target] is too far away to receive a blessing."))
to_chat(target, SPAN_NOTICE("[owner] tried to offer a blessing, but you were too far away to receive it."))
return
switch(blessing_type)
if ("Say")
owner.say(blessing_text)
if ("Whisper")
owner.whisper(blessing_text)
if ("Emote")
owner.emote(blessing_text)
var/datum/component/skill/ministry/ministry_skill = owner.GetComponent(MINISTRY_SKILL_COMPONENT)
if (!ministry_skill)
return
var/datum/component/morale/target_morale = target.GetComponent(MORALE_COMPONENT)
if (!target_morale)
return
var/moodlet_value = ministry_skill.moodlet_value_per_rank * (ministry_skill.skill_level - 1)
if (astype(owner, /mob/living/carbon/human)?.religion == astype(target, /mob/living/carbon/human)?.religion)
moodlet_value *= ministry_skill.same_religion_bonus_mod
SEND_SIGNAL(owner, COMSIG_GET_MINISTRY_MODIFIERS, target, &moodlet_value)
SEND_SIGNAL(target, COMSIG_RECEIVE_MINISTRY_MODIFIERS, owner, &moodlet_value)
if (!moodlet_value)
return
if (astype(target_morale.moodlets[/datum/moodlet/ministry_blessing], /datum/moodlet)?.get_morale_modifier() >= moodlet_value)
target_morale.load_moodlet(/datum/moodlet/ministry_blessing)?.refresh_moodlet()
return // Don't overwrite stronger moodlets.
var/datum/moodlet/blessing = target_morale.load_moodlet(/datum/moodlet/ministry_blessing, moodlet_value)
blessing.refresh_moodlet()
blessing.moodlet_descriptor += " from [owner.name]."
if ("No")
to_chat(owner, SPAN_NOTICE("[target] has refused your blessing."))
return
/datum/component/skill/ministry
/// The action icon stored for this ability.
var/datum/action/ministry/ministry_action
/// The value of the moodlet provided by this ability.
var/moodlet_value_per_rank = 10.0 / 3.0
/// The moodlet value multiplier for a target having the same religion as the performer.
var/same_religion_bonus_mod = 1.5
/datum/component/skill/ministry/Initialize(level)
. = ..()
if (!parent)
return
ministry_action = new /datum/action/ministry()
ministry_action.SetTarget(ministry_action)
ministry_action.Grant(parent)
RegisterSignal(parent, COMSIG_MOB_AFTER_LOGIN, PROC_REF(setup_action_button), override = TRUE)
/datum/component/skill/ministry/Destroy(force)
if (!parent)
return ..()
UnregisterSignal(parent, COMSIG_MOB_AFTER_LOGIN)
ministry_action?.Remove(parent)
QDEL_NULL(ministry_action)
return ..()
/datum/component/skill/ministry/proc/setup_action_button()
astype(parent, /mob)?.update_action_buttons()
+20 -8
View File
@@ -69,14 +69,26 @@
+ " - Firearms you shoot have a 30 degree spread-angle decrease, making them somewhat more accurate. This generally doesn't apply to weapons fired in semi-auto, but will make burst and automatic fire more manageable." \
)
// Temporarily commented because this is a little too complicated to catch in the initial release.
// /singleton/skill/leadership
// name = "Leadership"
// description = "Leadership skill grants access to a unique 'Inspire' action, which lets you say something inspiring and give the target a positive moodlet."
// category = /singleton/skill_category/combat
// subcategory = SKILL_SUBCATEGORY_SUPPORT
// required = TRUE
// component_type = LEADERSHIP_SKILL_COMPONENT
/singleton/skill/leadership
name = "Leadership"
description = "Leadership represents a characters skill with inspiring others. Having ranks in this skill grants access to the \"Deliver Speech\" action. Which can be used to give a morale modifier either in an area, or to a single person. " \
+ "Additional ranks increase the morale modifier provided."
maximum_level = SKILL_LEVEL_PROFESSIONAL
category = /singleton/skill_category/combat
subcategory = SKILL_SUBCATEGORY_SUPPORT
component_type = LEADERSHIP_SKILL_COMPONENT
skill_level_descriptions = alist(
SKILL_LEVEL_UNFAMILIAR = "You have no skill with motivational speeches.",
SKILL_LEVEL_FAMILIAR = "You gain the \"Deliver Speech\" ability, which provides a small morale bonus.",
SKILL_LEVEL_TRAINED = "You gain the \"Deliver Speech\" ability, which provides a modest morale bonus.",
SKILL_LEVEL_PROFESSIONAL = "You gain the \"Deliver Speech\" ability, which provides a moderate morale bonus."
)
skill_cost_map = alist(
SKILL_LEVEL_UNFAMILIAR = 0,
SKILL_LEVEL_FAMILIAR = 1,
SKILL_LEVEL_TRAINED = 2,
SKILL_LEVEL_PROFESSIONAL = 4
)
/singleton/skill/tenacity
name = "Tenacity"
+15
View File
@@ -57,3 +57,18 @@
SKILL_LEVEL_TRAINED = "Your liver is 10% more effective at filtering drugs and alcohol.",
SKILL_LEVEL_PROFESSIONAL = "Your liver is 15% more effective at filtering drugs and alcohol."
)
/singleton/skill/ministry
name = "Ministry"
description = "Represents a characters training in religious counseling. Having ranks in this skill unlocks the \"Offer Blessing\" ability, which offers a small morale modifier to an adjacent character. " \
+ "Additional ranks increase the morale modifier, which is empowered further if the recipient shares a religion with you."
maximum_level = SKILL_LEVEL_PROFESSIONAL
category = /singleton/skill_category/everyday
subcategory = SKILL_SUBCATEGORY_SERVICE
component_type = MINISTRY_SKILL_COMPONENT
skill_level_descriptions = alist(
SKILL_LEVEL_UNFAMILIAR = "You have no training in religious counseling.",
SKILL_LEVEL_FAMILIAR = "You gain the \"Offer Blessing\" ability, which provides a small morale bonus to a recipient.",
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."
)