Skills System Revival: The Things She Remembered Had Never Been Her Own (#21853)

This PR is a revisit to the previously derelict PR #20159 that has been
unfinished for sometime now. More details about it in general can be
found here:
https://github.com/orgs/Aurorastation/projects/2?pane=issue&itemId=53167153

For awhile I've been talking about "Things I've been doing but it would
be really nice to do them with a skills system", or "And here's how I
would put this into the skills system when it's done". The main thing
that was stopping me from building it myself was having poor real life
skills in UI code and in DB code. However, I've gotten permission to
resume this PR, which has already completed the steps I would not have
been able to do myself. The rest of the PR fits well into my skillset as
a dev.

I'm opening this PR as a draft so as to enable my dev environment to
locally track all the previously modified files. I'll take this PR out
of draft and give this a full writeup when I have more work to show for
the PR this weekend.

### TODO

- [x] Rework a decent chunk of the currently existing skills to no
longer require hardcoded inserts into other systems. EG, converting from
classical ss13 methods, to modern /tg/-style ECS coding methods that
work off of component-signal patterns.
- [x] Make sure all of the existing skills have actual game
functionality (I won't PR a 2016 Baystation12 situation where 90% of the
skills are fluff only)
- [x] Add the various skills not yet made but are necessary for
completion sake, EG: Pilot (Spacecraft), Gunnery, Pilot (Walkers).
- [x] Examine each existing job in the game and assess whether it should
have a skill made with it in mind, or if it's covered by an existing
skill.
- [x] TO DISCUSS, BUT NOT ESSENTIAL: Additional skill proposals not
currently in the pre-existing TODO list, proposing subcategories.
- [x] Ensure that the previous TODO list is completed.

### Current Skills
The current list of skills, checkmarked for if I've completed them/they
have actual game mechanics. Or if we're just relegating them to separate
PRs. Originally this list was going to be forced to visit for a bare
minimum "does at least one thing" requirement, but now that is being
forgone due to this PR ballooning out of control and in complexity, as
well as development time overruns.

- [x] Bartending
- [x] Cooking
- [x] Gardening
- [x] Entertaining
- [x] Electrical Engineering
- [x] Mechanical Engineering
- [x] Atmospherics Systems
- [x] Reactor Systems
- [x] Medicine
- [x] Surgery
- [x] Pharmacology
- [x] Anatomy
- [x] Forensics
- [x] Robotics
- [x] Pilot: Spacecraft
- [x] Pilot: Exosuits
- [x] Research
- [x] Xenobotany
- [x] Xenoarchaeology
- [x] Xenobiology
- [x] Unarmed Combat
- [x] Armed Combat
- [x] Firearms
- [x] Leadership

---------

Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: Matt Atlas <liermattia@gmail.com>
Co-authored-by: FabianK3 <21039694+FabianK3@users.noreply.github.com>
Co-authored-by: Matt Atlas <mattiathebest2000@hotmail.it>
This commit is contained in:
VMSolidus
2026-04-18 14:33:48 +00:00
committed by GitHub
co-authored by Matt Atlas FabianK3 Matt Atlas
parent 2948b1dc87
commit 260f744906
107 changed files with 2965 additions and 185 deletions
+83
View File
@@ -0,0 +1,83 @@
/**
* Container for a single moodlet to be associated with a Morale Component.
* Only one of each type of moodlet is allowed to exist in a mood component at a time.
* If you're making a new source of moodlets, add a new child of this type.
*/
ABSTRACT_TYPE(/datum/moodlet)
/**
* How much this moodlet modifies its owner's morale by.
* This is simple summed exactly once when the moodlet is created.
* If anything needs to Set the value of a moodlet, they have to do so by calling set_moodlet().
* You may only Get this value by calling get_morale_modifier().
*
* This is similar to { get; private set; }. Under no circumstances are outside functions ever allowed to directly change this var because other vars depend on it.
* But there are public procs available to interact with it which obey its own internal rules.
*/
VAR_PRIVATE/morale_modifier = 0.0 // Positive and negative floating points are allowed.
/**
*
*/
var/moodlet_descriptor = "It's a moodlet!"
/**
* How long this moodlet will last if not refreshed. For Aurora's purposes, moodlets are targeted as having "Small effect, extreme duration".
* This is by design to encourage players to "Visit the chef at least once a round to do a little RP", while avoiding having moodlet collection interrupt the flow of expeditions.
*/
var/duration = 2.0 HOURS
/**
* The target time (in real life seconds) that the moodlet will self-terminate on.
* This is set automatically during the New() creation of moodlets.
* This can be updated by calling refresh_moodlet() to reset the time to die.
*/
var/time_to_die = 0.0
/**
* Moodlets should always have an associated morale component, but the component owns the moodlets, not the other way around.
* For the convenience of refreshing individual moodlets, they hold a weakref to their owner.
* This is set to private here so that we can assert that it will always be a morale component.
*/
VAR_PRIVATE/datum/weakref/morale_component
/datum/moodlet/New(datum/component/morale/_morale_component, set_points)
time_to_die = duration + REALTIMEOFDAY
morale_component = WEAKREF(_morale_component)
if (set_points) morale_modifier = set_points
_morale_component.add_morale_points(morale_modifier)
/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.
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])
return ..()
// Clean the effects of this moodlet from the parent.
parent.moodlets -= src
parent.add_morale_points(-morale_modifier)
return ..()
/datum/moodlet/proc/get_morale_modifier()
return morale_modifier
/datum/moodlet/proc/set_moodlet(new_modifier)
// We can skip the istype() in this case since the held weakref is asserted by VAR_PRIVATE to always be a morale component.
var/datum/component/morale/possible_morale = morale_component.resolve()
if (!possible_morale && !QDELING(src))
// Owner didn't exist, the moodlet has no need to exist either.
qdel(src)
return
// Add the difference between the new modifier and the old morale points.
// This should come out to no change if old and new are the same.
possible_morale.add_morale_points(new_modifier - morale_modifier)
// Then set the current morale points to the new ones.
morale_modifier = new_modifier
/datum/moodlet/proc/refresh_moodlet()
time_to_die = REALTIMEOFDAY + duration
@@ -0,0 +1,230 @@
/**
* 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.
*/
/datum/component/morale
/**
* The set of all moodlets associated with this Morale Component. These are also tightly controlled in their initialization, but are not private.
* That doesn't mean you need to be setting them anywhere other than in moodlets.dm.
* load_moodlet() is your best bet for "Add or Get" a moodlet, and is 100% of the time what you want to use if you're trying to make sure someone has a moodlet.
*/
var/list/datum/moodlet/moodlets = list()
/**
* The current sum total of morale_points. This var is intentionally private because it is self-managed by the component, and should never be set directly.
* If you need the contents of this var outside of the component, you MUST use get_morale_points().
*/
VAR_PRIVATE/morale_points = 0.0 // Positive and negative floating points are allowed.
/**
* The current "Morale Ratio" calculated in advance as the Hyperbolic Tangent of morale_points. This var is self-managed by the component and should never be set directly.
* This gets updated whenever morale_points are changed, and is used by the Morale Component to handle fast calculations of its various effects.
* If you need the contents of this var outside of the component, you MUST use get_morale_ratio().
*
* morale_ratio is NEVER to be set by anything outside of the component.
*/
VAR_PRIVATE/morale_ratio = 0.0 // Positive and negative floating points are allowed.
/**
* The "B" constant in the equation for y = Atanh(Bx + 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)
* 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
// 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
/// How many effective ranks of unarmed combat skill this morale component can contribute
var/unarmed_rank_contribution = 0.5
/**
* The maximum possible panic chance from negative morale point sums.
* Since negative moodlets are unique to psychic damage, the effects of psychically induced panic are uniquely stronger than simply being unskilled.
*/
var/panic_chance_ceiling = 10
/datum/component/morale/proc/get_morale_ratio()
return morale_ratio
/datum/component/morale/proc/get_morale_points()
return morale_points
/datum/component/morale/proc/add_morale_points(input)
morale_points += input
morale_ratio = ftanh(beta_value * morale_points)
/datum/component/morale/proc/set_beta_value(input)
beta_value = input
morale_ratio = ftanh(beta_value * morale_points)
/**
* Your one-stop-shop for making moodlets. This proc returns the pre-existing moodlet of a given type.
* If it doesn't already exist, then one will be created.
*/
/datum/component/morale/proc/load_moodlet(datum/moodlet/moodlet_type, set_points)
RETURN_TYPE(moodlet_type)
var/datum/moodlet/loaded_moodlet = locate(moodlet_type) in moodlets
if (!loaded_moodlet)
loaded_moodlet = new moodlet_type(src, set_points)
moodlets.Add(loaded_moodlet)
return loaded_moodlet
if (set_points) loaded_moodlet.set_moodlet(set_points)
return loaded_moodlet
/datum/component/morale/Initialize()
. = ..()
if (!parent)
return
// 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)
RegisterSignal(parent, COMSIG_GUN_TOGGLE_FIRING_MODE, PROC_REF(safety_fumble), override = TRUE)
RegisterSignal(parent, COMSIG_UNARMED_HARM_ATTACKER, PROC_REF(handle_harm_attack), override = TRUE)
RegisterSignal(parent, COMSIG_UNARMED_HARM_DEFENDER, PROC_REF(handle_harm_defend), override = TRUE)
RegisterSignal(parent, COMSIG_UNARMED_DISARM_ATTACKER, PROC_REF(handle_disarm_attack), override = TRUE)
RegisterSignal(parent, COMSIG_UNARMED_DISARM_DEFENDER, PROC_REF(handle_disarm_defend), override = TRUE)
RegisterSignal(parent, COMSIG_MECH_MOVE_WASD, PROC_REF(handle_user_move), override = TRUE)
RegisterSignal(parent, COMSIG_MECH_MOVE_STRAFE, PROC_REF(handle_user_strafe), override = TRUE)
RegisterSignal(parent, COMSIG_MECH_TOGGLE_POWER, PROC_REF(handle_mech_toggle_power), override = TRUE)
/datum/component/morale/Destroy()
QDEL_LIST_FORCE(moodlets)
if (!parent)
return ..()
// Behold my wall of UnregisterSignal()
UnregisterSignal(parent, COMSIG_APPLY_HIT_EFFECT)
UnregisterSignal(parent, COMSIG_BEFORE_GUN_FIRE)
UnregisterSignal(parent, COMSIG_GUN_TOGGLE_FIRING_MODE)
UnregisterSignal(parent, COMSIG_UNARMED_HARM_ATTACKER)
UnregisterSignal(parent, COMSIG_UNARMED_HARM_DEFENDER)
UnregisterSignal(parent, COMSIG_UNARMED_DISARM_ATTACKER)
UnregisterSignal(parent, COMSIG_UNARMED_DISARM_DEFENDER)
UnregisterSignal(parent, COMSIG_MECH_MOVE_WASD)
UnregisterSignal(parent, COMSIG_MECH_MOVE_STRAFE)
UnregisterSignal(parent, COMSIG_MECH_TOGGLE_POWER)
return ..()
/datum/component/morale/process(seconds_per_tick)
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))
continue
morale_points -= moodlet.get_morale_modifier()
qdel(moodlet, TRUE)
moodlets.Remove(moodlet)
list_trimmed = TRUE
if (!list_trimmed)
return
morale_ratio = ftanh(morale_points)
/*
AND NOW THE GIANT WALL OF SIGNAL HANDLERS
LOOK UPON MY SIGNALS HANDLERS YE MIGHTY AND DESPAIR
Remember how I said low morale will exclusively come from psychic damage?
The Night has shed a tear to tell you of fear and of sorrow and pain that you shall never outgrow.
Oh yea, the negative morale effects will be !!!FUN!!! in exchange for being extremely rare
Otherwise, morale effects are generally equivalent to "up to half a skill rank"
for a large variety of numerical effects related to skills.
*/
/datum/component/morale/proc/modify_hit_effect(owner, mob/living/target, obj/item/weapon, power, hit_zone)
SIGNAL_HANDLER
*power = *power * (1 + (0.05 * morale_ratio))
/datum/component/morale/proc/handle_accuracy(mob/shooter, accuracy_decrease, dispersion_increase)
SIGNAL_HANDLER
*accuracy_decrease = *accuracy_decrease - morale_ratio
*dispersion_increase = *dispersion_increase - 10 * morale_ratio
/datum/component/morale/proc/safety_fumble(mob/shooter, obj/item/gun/shoota, cancelled)
SIGNAL_HANDLER
// Up to 50% chance to fumble a safety when in a psionically-induced panic.
if (cancelled || morale_points >= 0 || !prob(floor(5 * panic_chance_ceiling * -morale_ratio)))
return
*cancelled = TRUE
shooter.visible_message(
SPAN_DANGER("\The [shooter] fumbles with \the [shoota]'s safety in a blind panic!"),
SPAN_DANGER("You fumble with \the [shoota]'s safety in a blind panic!"))
/datum/component/morale/proc/handle_harm_attack(mob/attacker, mob/defender, attacker_skill_level, miss_chance, rand_damage, block_chance)
SIGNAL_HANDLER
*attacker_skill_level = *attacker_skill_level + (unarmed_rank_contribution * morale_ratio)
*miss_chance = *miss_chance - unarmed_chance_contribution * morale_ratio
*block_chance = *block_chance - unarmed_chance_contribution * morale_ratio
/datum/component/morale/proc/handle_harm_defend(mob/defender, mob/attacker, defender_skill_level, miss_chance, rand_damage, block_chance)
SIGNAL_HANDLER
*defender_skill_level = *defender_skill_level - (unarmed_rank_contribution * morale_ratio)
*miss_chance = *miss_chance + unarmed_chance_contribution * morale_ratio
*block_chance = *block_chance + unarmed_chance_contribution * morale_ratio
/datum/component/morale/proc/handle_disarm_attack(mob/attacker, mob/defender, attacker_skill_level, disarm_cost, push_chance, disarm_chance)
SIGNAL_HANDLER
*attacker_skill_level = *attacker_skill_level + (unarmed_rank_contribution * morale_ratio)
*push_chance = *push_chance - unarmed_chance_contribution * morale_ratio
*disarm_chance = *disarm_chance - unarmed_chance_contribution * morale_ratio
/datum/component/morale/proc/handle_disarm_defend(mob/defender, mob/attacker, defender_skill_level, disarm_cost, push_chance, disarm_chance)
SIGNAL_HANDLER
*defender_skill_level = *defender_skill_level + (unarmed_rank_contribution * morale_ratio)
*push_chance = *push_chance + unarmed_chance_contribution * morale_ratio
*disarm_chance = *disarm_chance + unarmed_chance_contribution * morale_ratio
/datum/component/morale/proc/handle_user_move(mob/living/user, direction, delay_modifier)
SIGNAL_HANDLER
if (parent != user)
return
if (morale_points < 0 && prob(floor(panic_chance_ceiling * -morale_ratio)))
user.visible_message(
SPAN_DANGER("\The [user] fumbles with their mech's controls in a blind panic!"),
SPAN_DANGER("You fumble with your mech's controls in a blind panic!"))
*direction = pick(GLOB.cardinals)
if (direction == NORTH)
return
*delay_modifier = *delay_modifier - 0.5 * morale_ratio
/datum/component/morale/proc/handle_user_strafe(mob/living/user, direction, delay_modifier)
SIGNAL_HANDLER
if (parent != user)
return
if (morale_points < 0 && prob(floor(panic_chance_ceiling * -morale_ratio)))
user.visible_message(
SPAN_DANGER("\The [user] fumbles with their mech's controls in a blind panic!"),
SPAN_DANGER("You fumble with your mech's controls in a blind panic!"))
*direction = angle2dir(dir2angle(direction) + 180)
*delay_modifier = *delay_modifier - 0.5 * morale_ratio
/datum/component/morale/proc/handle_mech_toggle_power(mob/user, cancelled, delay)
SIGNAL_HANDLER
if (parent != user || cancelled || morale_points >= 0)
return
if (prob(floor(panic_chance_ceiling * -morale_ratio)))
to_chat(user, SPAN_DANGER("The pressure on your mind overwhelms you completely. You can't even think to find the power switch..."))
*cancelled = TRUE
return
*delay = *delay - (5 * morale_ratio) SECONDS
to_chat(user, SPAN_WARNING("The pressure on your mind causes you to stumble in searching for the power switch..."))
@@ -0,0 +1,23 @@
/**
* Component used for the Armed Combat skill. For its initial implementation, this component works by *slightly* modifying the damage dealt by attacks done with melee weapons.
* For the majority of non-security crew, this basically means a small nerf to damage if they didn't invest any points into the skill.
*/
/datum/component/skill/armed_combat
/datum/component/skill/armed_combat/Initialize(level)
. = ..()
if (!parent)
return
RegisterSignal(parent, COMSIG_APPLY_HIT_EFFECT, PROC_REF(modify_hit_effect), override = TRUE)
/datum/component/skill/armed_combat/Destroy(force)
if (!parent)
return ..()
UnregisterSignal(parent, COMSIG_APPLY_HIT_EFFECT)
return ..()
/datum/component/skill/armed_combat/proc/modify_hit_effect(owner, mob/living/target, obj/item/weapon, power, hit_zone)
SIGNAL_HANDLER
*power = *power * (1 + 0.1 * (skill_level - skill_diff_reference))
@@ -0,0 +1,63 @@
/**
* Component used for the Firearms skill. Mobs with this component will have their weapon handling characteristics modified by their skill rank
* but only if this component is present. Essentially it provides a penalty to gun accuracy at ranks below the "Skill Diff", and a bonus for ranks above it.
*/
/datum/component/skill/firearms
/**
* Accuracy modifier to fired guns per point of "Skill Diff".
* As an "Effective increase" in tiles to the target being shot.
*/
var/accuracy_per_skill_diff = 2
/**
* Dispersion modifier to fired guns per point of "Skill Diff".
* As an arc-length in Degrees.
*/
var/dispersion_per_skill_diff = 30
/// %chance per point of "Skill Diff" to fumble changing a weapon's safety.
var/safety_fumble_per_skill_diff = 15
/// %chance for a completely untrained person to shoot themself in the foot accidentally.
var/footgun_chance = 1
/datum/component/skill/firearms/Initialize(var/level = SKILL_LEVEL_UNFAMILIAR)
. = ..()
if (!parent)
return
RegisterSignal(parent, COMSIG_BEFORE_GUN_FIRE, PROC_REF(handle_accuracy), override = TRUE)
RegisterSignal(parent, COMSIG_GUN_TOGGLE_FIRING_MODE, PROC_REF(safety_fumble), override = TRUE)
/datum/component/skill/firearms/Destroy()
if (!parent)
return ..()
UnregisterSignal(parent, COMSIG_BEFORE_GUN_FIRE)
UnregisterSignal(parent, COMSIG_GUN_TOGGLE_FIRING_MODE)
return ..()
/datum/component/skill/firearms/proc/handle_accuracy(mob/shooter, accuracy_decrease, dispersion_increase)
SIGNAL_HANDLER
var/skill_diff = skill_diff_reference - skill_level
// Count the target as being one tile further away from the shooter
// per the difference between a skilled professional, and the shooter's skill level
*accuracy_decrease = *accuracy_decrease + accuracy_per_skill_diff * skill_diff
// Unskilled shooters get an increased firing arc for their guns
// to a maximum of 30 degrees when fully untrained.
*dispersion_increase = *dispersion_increase + dispersion_per_skill_diff * skill_diff
/datum/component/skill/firearms/proc/safety_fumble(mob/shooter, obj/item/gun/shoota, cancelled)
SIGNAL_HANDLER
if (cancelled || skill_level >= skill_diff_reference)
return // Trained and up will never fumble the safety. Except if morale has anything to say about that...
if (!prob(safety_fumble_per_skill_diff * (skill_diff_reference - skill_level)))
return // if they pass the skill check.
*cancelled = TRUE
shooter.visible_message(
SPAN_DANGER("\The [shooter] fumbles with \the [shoota]'s safety!"),
SPAN_DANGER("You fumble with \the [shoota]'s safety!"))
@@ -0,0 +1,6 @@
/**
* 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!".
* 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
@@ -0,0 +1,53 @@
/datum/component/skill/unarmed_combat
/// Percent chance modifier for harm intent
var/harm_miss_chance_per_skill_diff = 2
/// Percent chance modifier for blocking unarmed attacks
var/block_chance_per_skill_diff = 2
/// Push chance modifier for disarm intent
var/push_chance_per_skill_diff = 2
/// Disarm chance modifier for disarm intent
var/disarm_chance_per_skill_diff = 2
/datum/component/skill/unarmed_combat/Initialize()
. = ..()
if (!parent)
return
RegisterSignal(parent, COMSIG_UNARMED_HARM_ATTACKER, PROC_REF(handle_harm_attack), override = TRUE)
RegisterSignal(parent, COMSIG_UNARMED_HARM_DEFENDER, PROC_REF(handle_harm_defend), override = TRUE)
RegisterSignal(parent, COMSIG_UNARMED_DISARM_ATTACKER, PROC_REF(handle_disarm_attack), override = TRUE)
RegisterSignal(parent, COMSIG_UNARMED_DISARM_DEFENDER, PROC_REF(handle_disarm_defend), override = TRUE)
/datum/component/skill/unarmed_combat/Destroy(force)
if (!parent)
return ..()
UnregisterSignal(parent, COMSIG_UNARMED_HARM_ATTACKER)
UnregisterSignal(parent, COMSIG_UNARMED_HARM_DEFENDER)
UnregisterSignal(parent, COMSIG_UNARMED_DISARM_ATTACKER)
UnregisterSignal(parent, COMSIG_UNARMED_DISARM_DEFENDER)
return ..()
/datum/component/skill/unarmed_combat/proc/handle_harm_attack(mob/attacker, mob/defender, attacker_skill_level, miss_chance, rand_damage, block_chance)
SIGNAL_HANDLER
*attacker_skill_level = *attacker_skill_level + skill_level - 1
*miss_chance = *miss_chance + (skill_diff_reference - skill_level) * harm_miss_chance_per_skill_diff
*block_chance = *block_chance + (skill_diff_reference - skill_level) * block_chance_per_skill_diff
/datum/component/skill/unarmed_combat/proc/handle_harm_defend(mob/defender, mob/attacker, defender_skill_level, miss_chance, rand_damage, block_chance)
SIGNAL_HANDLER
*defender_skill_level = *defender_skill_level + skill_level - 1
*miss_chance = *miss_chance - (skill_diff_reference - skill_level) * harm_miss_chance_per_skill_diff
*block_chance = *block_chance - (skill_diff_reference - skill_level) * block_chance_per_skill_diff
/datum/component/skill/unarmed_combat/proc/handle_disarm_attack(mob/attacker, mob/defender, attacker_skill_level, disarm_cost, push_chance, disarm_chance)
SIGNAL_HANDLER
*attacker_skill_level = *attacker_skill_level + skill_level - 1
*push_chance = *push_chance - (skill_diff_reference - skill_level) * push_chance_per_skill_diff
*disarm_chance = *disarm_chance - (skill_diff_reference - skill_level) * disarm_chance_per_skill_diff
/datum/component/skill/unarmed_combat/proc/handle_disarm_defend(mob/defender, mob/attacker, defender_skill_level, disarm_cost, push_chance, disarm_chance)
SIGNAL_HANDLER
*defender_skill_level = *defender_skill_level + skill_level - 1
*push_chance = *push_chance + (skill_diff_reference - skill_level) * push_chance_per_skill_diff
*disarm_chance = *disarm_chance + (skill_diff_reference - skill_level) * push_chance_per_skill_diff
@@ -0,0 +1 @@
/datum/component/skill/atmospherics_systems
@@ -0,0 +1 @@
/datum/component/skill/electrical_engineering
@@ -0,0 +1 @@
/datum/component/skill/mechanical_engineering
@@ -0,0 +1,23 @@
/datum/component/skill/reactor_systems
/datum/component/skill/reactor_systems/Initialize()
. = ..()
if (!parent)
return
RegisterSignal(parent, COMSIG_USE_REACTOR_COMPUTER, PROC_REF(use_reactor_computer), override = TRUE)
/datum/component/skill/reactor_systems/Destroy(force)
if (!parent)
return ..()
UnregisterSignal(parent, COMSIG_USE_REACTOR_COMPUTER)
return ..()
/datum/component/skill/reactor_systems/proc/use_reactor_computer(mob/user, cancelled)
SIGNAL_HANDLER
if (skill_level >= SKILL_LEVEL_TRAINED)
return
*cancelled = TRUE
to_chat(user, SPAN_WARNING("There's just so many buttons... You have no idea where to even begin with using this machine."))
@@ -0,0 +1,7 @@
/**
* Not currently implemented anywhere. Finish this in its own PR so as to avoid Scope Creep.
*
* This skill should influence the information a character receives when medically examining another person (with or without a health analyzer).
* With extremely high ranks in the skill giving more detailed information about a character's injuries at a glance to make diagnosing injuries easier.
*/
/datum/component/skill/anatomy
@@ -0,0 +1,6 @@
/**
* Not currently implemented anywhere. Finish this in its own PR so as to avoid Scope Creep.
*
* To be honest just port and componentize Baystation12's Forensics.
*/
/datum/component/skill/forensics
@@ -0,0 +1,5 @@
/**
* Component used for the Medicine Skill.
* This skill is meant to influence a character's effectiveness when performing first aid treatments, such as bandaging, CPR, etc.
*/
/datum/component/skill/medicine
@@ -0,0 +1 @@
/datum/component/skill/pharmacology
@@ -0,0 +1,5 @@
/**
* Component used for the Surgery Skill. A character's rank in this component is used to determine which surgical procedures they can perform.
* This skill only governs "Organic" surgery. IPC surgery is instead handled by the Robotics skill.
*/
/datum/component/skill/surgery
@@ -0,0 +1,62 @@
/datum/component/skill/pilot_mechs
/// The %chance per move input to scramble the input into a random direction. This only applies for Unfamiliar mech pilots.
var/move_scramble_chance = 10
/// Extra move delay added to
var/move_delay_per_skill_diff = 0.5
/datum/component/skill/pilot_mechs/Initialize()
. = ..()
if (!parent)
return
RegisterSignal(parent, COMSIG_MECH_MOVE_WASD, PROC_REF(handle_user_move), override = TRUE)
RegisterSignal(parent, COMSIG_MECH_MOVE_STRAFE, PROC_REF(handle_user_strafe), override = TRUE)
RegisterSignal(parent, COMSIG_MECH_TOGGLE_POWER, PROC_REF(handle_toggle_power), override = TRUE)
/datum/component/skill/pilot_mechs/Destroy(force)
if (!parent)
return ..()
UnregisterSignal(parent, COMSIG_MECH_MOVE_WASD)
UnregisterSignal(parent, COMSIG_MECH_MOVE_STRAFE)
UnregisterSignal(parent, COMSIG_MECH_TOGGLE_POWER)
return ..()
/datum/component/skill/pilot_mechs/proc/handle_user_move(mob/living/user, direction, delay_modifier)
SIGNAL_HANDLER
if (parent != user)
return
// Potentially scramble the direction if the pilot is minimally skilled.
if (skill_level == SKILL_LEVEL_UNFAMILIAR && prob(move_scramble_chance))
to_chat(user, SPAN_WARNING("You fumble with the controls!"))
*direction = pick(GLOB.cardinals)
// Don't modify the "Forward" throttle no matter the skill level. All other directions are slower than this for mechs.
if (direction == NORTH)
return
*delay_modifier = *delay_modifier + (skill_diff_reference - skill_level) * move_delay_per_skill_diff
/datum/component/skill/pilot_mechs/proc/handle_user_strafe(mob/living/user, direction, delay_modifier)
SIGNAL_HANDLER
if (parent != user)
return
// Potentially flip the strafe direction if the pilot is minimally skilled.
if (skill_level == SKILL_LEVEL_UNFAMILIAR && prob(move_scramble_chance))
to_chat(user, SPAN_WARNING("You fumble with the controls!"))
*direction = angle2dir(dir2angle(direction) + 180)
*delay_modifier = *delay_modifier + (skill_diff_reference - skill_level) * move_delay_per_skill_diff
/datum/component/skill/pilot_mechs/proc/handle_toggle_power(mob/user, cancelled, delay)
SIGNAL_HANDLER
if (parent != user || cancelled || skill_level != SKILL_LEVEL_UNFAMILIAR)
return
*delay = *delay + (5 SECONDS)
to_chat(user, SPAN_NOTICE("You struggle with searching all these buttons for the power switch."))
playsound(user.loc, SFX_KEYBOARD, 30, TRUE)
@@ -0,0 +1 @@
/datum/component/skill/pilot_spacecraft
@@ -0,0 +1,27 @@
/**
* Component used for the Robotics skill. Like the Surgery skill, this component is used to determine which surgical procedures a character is allowed to perform on IPCs.
* This skill does not apply to surgeries performed on "Organics", but it can allow for repairs to prosthetic limbs.
*/
/datum/component/skill/robotics
/datum/component/skill/robotics/Initialize()
. = ..()
if (!parent)
return
RegisterSignal(parent, COMSIG_USE_MECH_FAB, PROC_REF(use_mech_fab), override = TRUE)
/datum/component/skill/robotics/Destroy()
if (!parent)
return ..()
UnregisterSignal(parent, COMSIG_USE_MECH_FAB)
return ..()
/datum/component/skill/robotics/proc/use_mech_fab(mob/user, cancelled)
SIGNAL_HANDLER
if (cancelled || skill_level >= SKILL_LEVEL_TRAINED)
return
*cancelled = TRUE
to_chat(user, SPAN_WARNING("You have no idea how this machine works."))
@@ -0,0 +1 @@
/datum/component/skill/archaology
@@ -0,0 +1 @@
/datum/component/skill/research
@@ -0,0 +1 @@
/datum/component/skill/xenobiology
@@ -0,0 +1 @@
/datum/component/skill/xenobotany
@@ -0,0 +1,69 @@
/datum/component/skill/bartending
/datum/moodlet/bartender_drink
/datum/component/drink_moodlet_provider
/// The morale boosting value of the moodlet this drink will provide.
var/moodlet_value = 0
/// Whether the DrinkMoodletProvider is allowed to overwrite stronger moodlets with weaker moodlets.
var/overwrite_moodlet = FALSE
/// Original name of the drink before the component changed it.
var/initial_name
/datum/component/drink_moodlet_provider/Initialize(value = 5.0, overwrite = FALSE, drink_quality)
. = ..()
if (!parent)
return
moodlet_value = value
overwrite_moodlet = overwrite
RegisterSignal(parent, COMSIG_CONTAINER_DRANK, PROC_REF(handle_drank), override = TRUE)
if (!isatom(parent))
return
var/atom/owner = parent
initial_name = owner.name
switch (drink_quality)
if (-INFINITY to 5)
owner.name = "inferior " + initial_name
if (5 to 10)
owner.name = "cheap " + initial_name
if (10 to 15)
owner.name = "finely-mixed " + initial_name
if (15 to 20)
owner.name = "superior quality " + initial_name
if (20 to INFINITY)
owner.name = "masterful " + initial_name
/datum/component/drink_moodlet_provider/Destroy()
if (!parent)
return ..()
UnregisterSignal(parent, COMSIG_CONTAINER_DRANK)
if (initial_name && istype(parent, /atom))
parent:name = initial_name
return ..()
/datum/component/drink_moodlet_provider/proc/handle_drank(obj/item/reagent_containers/owner, mob/user)
SIGNAL_HANDLER
if (QDELING(src))
return
if (!owner.reagents.total_volume)
qdel(src)
// No return here because total volume can be empty at this step (if the person drank the last of a cup)
var/datum/component/morale/morale_comp = user.GetComponent(MORALE_COMPONENT)
if (!morale_comp)
return
if (!overwrite_moodlet && astype(morale_comp.moodlets[/datum/moodlet/bartender_drink], /datum/moodlet)?.get_morale_modifier() > moodlet_value)
// Return if they already have a better drink moodlet.
return
var/datum/moodlet/new_moodlet = morale_comp.load_moodlet(/datum/moodlet/bartender_drink, moodlet_value)
new_moodlet.refresh_moodlet() // Reset the duration when they drink it.
@@ -0,0 +1 @@
/datum/component/skill/cooking
@@ -0,0 +1 @@
/datum/component/skill/entertaining
@@ -0,0 +1,22 @@
/datum/component/skill/gardening
var/bonus_yield_per_rank = 1
var/harvest_speedup_per_rank = 0.333 SECONDS
/datum/component/skill/gardening/Initialize(level)
. = ..()
if(!parent)
return
RegisterSignal(parent, COMSIG_PLANT_HARVESTER, PROC_REF(modify_yield), override = TRUE)
/datum/component/skill/gardening/Destroy(force)
if (!parent)
return ..()
UnregisterSignal(parent, COMSIG_PLANT_HARVESTER)
return ..()
/datum/component/skill/gardening/proc/modify_yield(owner, datum/seed/plant, total_yield, cancelled, doafter)
SIGNAL_HANDLER
*total_yield = *total_yield + bonus_yield_per_rank * (skill_level - 1)
*doafter = *doafter - harvest_speedup_per_rank * (skill_level - 1)
@@ -0,0 +1,31 @@
/**
* The base type for Componentized skills, containing only the information extracted from Skill preferences that would be required to function.
* Children of this component can be added to a character from skill singletons by overriding that singleton's on_spawn() proc.
*/
ABSTRACT_TYPE(/datum/component/skill)
/**
* How many ranks a player has purchased in a given skill.
* How this is actually used is entirely up to the implementation of individual components.
*/
var/skill_level = SKILL_LEVEL_UNFAMILIAR
/**
* Reference value used for checking "Skill Diff"
* "Skill Diff" is the distance from the actual skill level to the reference.
*
* This can essentially be thought of as the "baseline competence" for how a vanilla character would function prior to the introduction of Skill Components.
* Any datum that is *missing* a given skill component can be logically assumed to be at this skill level.
* Therefore, characters who both have the component, and are at a level lower than this can be assumed to be "less competent".
* While characters at a level above this can be assumed to be "more competent".
*/
var/skill_diff_reference = SKILL_LEVEL_TRAINED
/**
* Always use . = ..() at the start of a NameSkillComponent's Initialize() proc.
* Skills MUST have their skill_level set first during initialization.
* Do this by setting var/level to the 2nd arg of AddComponent()
*/
/datum/component/skill/Initialize(var/level = SKILL_LEVEL_UNFAMILIAR)
SHOULD_CALL_PARENT(TRUE)
. = ..()
skill_level = level