mirror of
https://github.com/Aurorastation/Aurora.3.git
synced 2026-08-26 14:31:59 +01:00
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:
co-authored by
Matt Atlas
FabianK3
Matt Atlas
parent
2948b1dc87
commit
260f744906
@@ -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
|
||||
Reference in New Issue
Block a user