diff --git a/SQL/migrate-2023/V020__skills_education.sql b/SQL/migrate-2023/V020__skills_education.sql
new file mode 100644
index 00000000000..b0d66f14520
--- /dev/null
+++ b/SQL/migrate-2023/V020__skills_education.sql
@@ -0,0 +1,7 @@
+--
+-- Implemented in PR #21853.
+-- Add education and skills.
+--
+
+ALTER TABLE `ss13_characters` ADD COLUMN `education` VARCHAR(48) DEFAULT NULL AFTER `origin`;
+ALTER TABLE `ss13_characters` ADD COLUMN `skills` VARCHAR(256) DEFAULT NULL AFTER `education`;
diff --git a/SQL/migrate-2023/V021__increase_skill_size.sql b/SQL/migrate-2023/V021__increase_skill_size.sql
new file mode 100644
index 00000000000..a63dd7d311a
--- /dev/null
+++ b/SQL/migrate-2023/V021__increase_skill_size.sql
@@ -0,0 +1,12 @@
+--
+-- Implemented in PR #21853.
+-- Changed skills column to TEXT to prevent size overflow
+--
+
+UPDATE `ss13_characters`
+ SET `skills` = NULL
+ WHERE `skills` IS NOT NULL AND NOT JSON_VALID(`skills`);
+
+ALTER TABLE `ss13_characters`
+ MODIFY COLUMN `skills` LONGTEXT DEFAULT NULL,
+ ADD CONSTRAINT `chk_skills_json_valid` CHECK (`skills` IS NULL OR JSON_VALID(`skills`));
diff --git a/aurorastation.dme b/aurorastation.dme
index d1aecb7179e..4569a2b653b 100644
--- a/aurorastation.dme
+++ b/aurorastation.dme
@@ -137,6 +137,7 @@
#include "code\__DEFINES\shuttle.dm"
#include "code\__DEFINES\si.dm"
#include "code\__DEFINES\singletons.dm"
+#include "code\__DEFINES\skills.dm"
#include "code\__DEFINES\smart_token_bucket.dm"
#include "code\__DEFINES\solar.dm"
#include "code\__DEFINES\sound.dm"
@@ -388,6 +389,7 @@
#include "code\controllers\subsystems\records.dm"
#include "code\controllers\subsystems\responseteam.dm"
#include "code\controllers\subsystems\runechat.dm"
+#include "code\controllers\subsystems\skills.dm"
#include "code\controllers\subsystems\skybox.dm"
#include "code\controllers\subsystems\sound_loops.dm"
#include "code\controllers\subsystems\sounds.dm"
@@ -501,6 +503,8 @@
#include "code\datums\components\eye\base_planner.dm"
#include "code\datums\components\eye\blueprints.dm"
#include "code\datums\components\eye\freelook.dm"
+#include "code\datums\components\morale\moodlets.dm"
+#include "code\datums\components\morale\morale_component.dm"
#include "code\datums\components\multitool\_multitool.dm"
#include "code\datums\components\multitool\multitool.dm"
#include "code\datums\components\multitool\circuitboards\circuitboards.dm"
@@ -508,6 +512,31 @@
#include "code\datums\components\overhead_emote\overhead_emote_singleton.dm"
#include "code\datums\components\psionics\psi_sensitivity.dm"
#include "code\datums\components\psionics\psi_suppression.dm"
+#include "code\datums\components\skills\skill_component.dm"
+#include "code\datums\components\skills\combat\armed_combat_skill_component.dm"
+#include "code\datums\components\skills\combat\firearms_skill_component.dm"
+#include "code\datums\components\skills\combat\leadership_skill_component.dm"
+#include "code\datums\components\skills\combat\unarmed_combat_skill_component.dm"
+#include "code\datums\components\skills\engineering\atmospherics_systems_skill_component.dm"
+#include "code\datums\components\skills\engineering\electrical_engineering_skill_component.dm"
+#include "code\datums\components\skills\engineering\mechanical_engineering_skill_component.dm"
+#include "code\datums\components\skills\engineering\reactor_systems_skill_component.dm"
+#include "code\datums\components\skills\medical\anatomy_skill_component.dm"
+#include "code\datums\components\skills\medical\forensics_skill_component.dm"
+#include "code\datums\components\skills\medical\medicine_skill_component.dm"
+#include "code\datums\components\skills\medical\pharmacology_skill_component.dm"
+#include "code\datums\components\skills\medical\surgery_skill_component.dm"
+#include "code\datums\components\skills\operations\pilot_mechs_skill_component.dm"
+#include "code\datums\components\skills\operations\pilot_spacecraft_skill_component.dm"
+#include "code\datums\components\skills\operations\robotics_skill_component.dm"
+#include "code\datums\components\skills\science\archaology_skill_component.dm"
+#include "code\datums\components\skills\science\research_skill_component.dm"
+#include "code\datums\components\skills\science\xenobiology_skill_component.dm"
+#include "code\datums\components\skills\science\xenobotany_skill_component.dm"
+#include "code\datums\components\skills\service\bartending_skill_component.dm"
+#include "code\datums\components\skills\service\cooking_skill_component.dm"
+#include "code\datums\components\skills\service\entertaining_skill_component.dm"
+#include "code\datums\components\skills\service\gardening_skill_component.dm"
#include "code\datums\components\synthetic_endoskeleton\synthetic_endoskeleton.dm"
#include "code\datums\components\turf_click\turf_hand.dm"
#include "code\datums\elements\_element.dm"
@@ -594,6 +623,14 @@
#include "code\datums\repositories\unique.dm"
#include "code\datums\scenarios\roles.dm"
#include "code\datums\scenarios\scenario.dm"
+#include "code\datums\skills\_skill_categories.dm"
+#include "code\datums\skills\_skills.dm"
+#include "code\datums\skills\combat\combat.dm"
+#include "code\datums\skills\everyday\service.dm"
+#include "code\datums\skills\occupational\engineering.dm"
+#include "code\datums\skills\occupational\medical.dm"
+#include "code\datums\skills\occupational\operations.dm"
+#include "code\datums\skills\occupational\science.dm"
#include "code\datums\state_machine\state.dm"
#include "code\datums\state_machine\transition.dm"
#include "code\datums\synthetic_internals\_synthetic_internals.dm"
@@ -1788,6 +1825,13 @@
#include "code\modules\background\citizenship\tajara.dm"
#include "code\modules\background\citizenship\unathi.dm"
#include "code\modules\background\citizenship\vaurca.dm"
+#include "code\modules\background\education\_education.dm"
+#include "code\modules\background\education\engineering.dm"
+#include "code\modules\background\education\medical.dm"
+#include "code\modules\background\education\misc.dm"
+#include "code\modules\background\education\science.dm"
+#include "code\modules\background\education\security.dm"
+#include "code\modules\background\education\service.dm"
#include "code\modules\background\origins\_origins.dm"
#include "code\modules\background\origins\origins\diona\biesel.dm"
#include "code\modules\background\origins\origins\diona\coalition.dm"
@@ -1975,6 +2019,7 @@
#include "code\modules\client\preference_setup\occupation\occupation.dm"
#include "code\modules\client\preference_setup\origin\origin.dm"
#include "code\modules\client\preference_setup\other\01_incidents.dm"
+#include "code\modules\client\preference_setup\skills\skills.dm"
#include "code\modules\client\verbs\ping.dm"
#include "code\modules\clothing\chameleon.dm"
#include "code\modules\clothing\clothing.dm"
diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm
index fe660255484..ca2f937a854 100644
--- a/code/__DEFINES/components.dm
+++ b/code/__DEFINES/components.dm
@@ -1,2 +1,35 @@
#define COMSIG_BASENAME_RENAME "base_name_rename"
#define COMSIG_BASENAME_SETNAME "base_name_setname"
+#define ARMED_COMBAT_SKILL_COMPONENT /datum/component/skill/armed_combat
+#define FIREARMS_SKILL_COMPONENT /datum/component/skill/firearms
+#define UNARMED_COMBAT_SKILL_COMPONENT /datum/component/skill/unarmed_combat
+#define PILOT_MECHS_SKILL_COMPONENT /datum/component/skill/pilot_mechs
+#define PILOT_SPACECRAFT_SKILL_COMPONENT /datum/component/skill/pilot_spacecraft
+#define ROBOTICS_SKILL_COMPONENT /datum/component/skill/robotics
+#define ARCHAOLOGY_SKILL_COMPONENT /datum/component/skill/archaology
+#define RESEARCH_SKILL_COMPONENT /datum/component/skill/research
+#define XENOBIOLOGY_SKILL_COMPONENT /datum/component/skill/xenobiology
+#define XENOBOTANY_SKILL_COMPONENT /datum/component/skill/xenobotany
+#define BARTENDING_SKILL_COMPONENT /datum/component/skill/bartending
+#define COOKING_SKILL_COMPONENT /datum/component/skill/cooking
+#define ENTERTAINING_SKILL_COMPONENT /datum/component/skill/entertaining
+#define GARDENING_SKILL_COMPONENT /datum/component/skill/gardening
+#define MEDICINE_SKILL_COMPONENT /datum/component/skill/medicine
+#define SKILL_COMPONENT /datum/component/skill
+#define MORALE_COMPONENT /datum/component/morale
+#define ELECTRICAL_ENGINEERING_SKILL_COMPONENT /datum/component/skill/electrical_engineering
+#define MECHANICAL_ENGINEERING_SKILL_COMPONENT /datum/component/skill/electrical_engineering
+#define ATMOSPHERICS_SYSTEMS_SKILL_COMPONENT /datum/component/skill/atmospherics_systems
+#define REACTOR_SYSTEMS_SKILL_COMPONENT /datum/component/skill/reactor_systems
+#define LEADERSHIP_SKILL_COMPONENT /datum/component/skill/leadership
+#define ANATOMY_SKILL_COMPONENT /datum/component/skill/anatomy
+#define FORENSICS_SKILL_COMPONENT /datum/component/skill/forensics
+#define PHARMACOLOGY_SKILL_COMPONENT /datum/component/skill/pharmacology
+#define SURGERY_SKILL_COMPONENT /datum/component/skill/surgery
+
+/**
+ * Trinary-Boolean helper that either returns null, or the skill level of a given skill component(which can be zero).
+ * It will ALWAYS be null if you try to use this to check for something that isn't a skill component, so just don't. Refer to the list above this function to see what your options are.
+ * Therefore, merely checking if (!this) is not sufficient to use this function, and instead you should be using isnull() on it to determine whether or not the skill exists.
+ */
+#define GET_SKILL_LEVEL(user, comp) astype(user.GetComponent(comp), SKILL_COMPONENT)?.skill_level
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 56d5bf9ed51..786b6d7b184 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -89,3 +89,40 @@
/*******Component Specific Signals*******/
+// /obj/item/gun signals
+
+#define COMSIG_GUN_TOGGLE_FIRING_MODE "gun_toggle_firing_mode"
+
+#define COMSIG_BEFORE_GUN_FIRE "before_gun_fire"
+
+// Mech signals
+
+#define COMSIG_MECH_MOVE_WASD "mech_move_wasd"
+
+#define COMSIG_MECH_MOVE_STRAFE "mech_move_strafe"
+
+#define COMSIG_MECH_TOGGLE_POWER "mech_toggle_power"
+
+// Unarmed Combat Signals
+
+#define COMSIG_UNARMED_HARM_ATTACKER "unarmed_harm_attacker"
+#define COMSIG_UNARMED_HARM_DEFENDER "unarmed_harm_defender"
+#define COMSIG_UNARMED_DISARM_ATTACKER "unarmed_disarm_attacker"
+#define COMSIG_UNARMED_DISARM_DEFENDER "unarmed_disarm_defender"
+
+// Bartending Skill Signals
+
+#define COMSIG_CONTAINER_DRANK "container_drank"
+
+// Hydroponics Signals
+/// Signal raised against the harvester of a plant during the Harvest() proc.
+#define COMSIG_PLANT_HARVESTER "plant_harvester"
+/// Signal raised against the plant being harvested during the Harvest() proc.
+#define COMSIG_PLANT_HARVESTED "plant_harvested"
+
+// Armed Combat Signals
+#define COMSIG_APPLY_HIT_EFFECT "apply_hit_effect"
+
+// Various computer signals for interrupting via skill or other effects.
+#define COMSIG_USE_REACTOR_COMPUTER "use_reactor_computer"
+#define COMSIG_USE_MECH_FAB "use_mech_fab"
diff --git a/code/__DEFINES/qdel.dm b/code/__DEFINES/qdel.dm
index abc0cfc7ba0..59ea465f0a1 100644
--- a/code/__DEFINES/qdel.dm
+++ b/code/__DEFINES/qdel.dm
@@ -73,6 +73,8 @@
#define QDEL_NULL_LIST(x) if(x) { for(var/y in x) { qdel(y) }}; if(x) {x.Cut(); x = null } // Second x check to handle items that LAZYREMOVE on qdel.
#define QDEL_LIST(L) if(L) { for(var/I in L) qdel(I); L.Cut(); }
+/// Variant on QDEL_LIST that applies the force optional input to the qdel() args.
+#define QDEL_LIST_FORCE(L) if(L) { for(var/I in L) qdel(I, force = TRUE); L.Cut(); }
#define QDEL_LIST_IN(L, time) addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(______qdel_list_wrapper), L), time, TIMER_STOPPABLE)
#define QDEL_LIST_ASSOC(L) if(L) { for(var/I in L) { qdel(L[I]); qdel(I); } L.Cut(); }
#define QDEL_LIST_ASSOC_VAL(L) if(L) { for(var/I in L) qdel(L[I]); L.Cut(); }
diff --git a/code/__DEFINES/skills.dm b/code/__DEFINES/skills.dm
new file mode 100644
index 00000000000..f3c1ea175c0
--- /dev/null
+++ b/code/__DEFINES/skills.dm
@@ -0,0 +1,39 @@
+/// You don't know anything about this subject.
+#define SKILL_LEVEL_UNFAMILIAR 1
+/// You're familiar with this subject, either by reading into it or by doing some courses.
+#define SKILL_LEVEL_FAMILIAR 2
+/// You've been formally trained in this subject. Typically, this is the minimum level for a job.
+#define SKILL_LEVEL_TRAINED 3
+/// You have some training and a good amount of experience in this subject.
+#define SKILL_LEVEL_PROFESSIONAL 4
+
+/// An everyday skill is a skill that can be picked up normally. Think like mixing a drink, growing a plant, cooking, and so on.
+#define SKILL_CATEGORY_EVERYDAY "Everyday"
+ #define SKILL_SUBCATEGORY_SERVICE "Service"
+ #define SKILL_SUBCATEGORY_MUNDANE "Mundane"
+
+/// Occupational skills are the skills necessary for you to do your job to a minimum degree.
+#define SKILL_CATEGORY_OCCUPATIONAL "Occupational"
+ #define SKILL_SUBCATEGORY_MEDICAL "Medical"
+ #define SKILL_SUBCATEGORY_ENGINEERING "Engineering"
+ #define SKILL_SUBCATEGORY_SCIENCE "Science"
+ #define SKILL_SUBCATEGORY_OPERATIONS "Operations"
+
+///A combat skill is a skill that has a direct effect in combat. These have an increased cost.
+#define SKILL_CATEGORY_COMBAT "Combat"
+ #define SKILL_SUBCATEGORY_RANGED "Ranged"
+ #define SKILL_SUBCATEGORY_MELEE "Melee"
+ #define SKILL_SUBCATEGORY_SUPPORT "Support"
+
+#define BASE_SKILL_POINTS_COMBAT 4
+#define BASE_SKILL_POINTS_OCCUPATIONAL 8
+#define BASE_SKILL_POINTS_EVERYDAY 8
+
+/// Critical success on a difficulty class. Obtained by rolling a nat 20 or +10 over the DC. Used for skills at the moment, will be used for more later.
+#define ROLL_RESULT_CRITICAL_SUCCESS 2
+/// Normal success on a difficulty class. Obtained by rolling equal or higher than the DC. Used for skills at the moment, will be used for more later.
+#define ROLL_RESULT_SUCCESS 1
+/// Normal failure on a difficulty class. Obtained by rolling less than the DC. Used for skills at the moment, will be used for more later.
+#define ROLL_RESULT_FAILURE 0
+/// Critical failure on a difficulty class. Obtained by rolling a nat 1 or -10 under the DC. Used for skills at the moment, will be used for more later.
+#define ROLL_RESULT_CRITICAL_FAILURE -1
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 3bb3be93463..dc615accad4 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -1228,3 +1228,18 @@ GLOBAL_LIST_INIT(wall_items, typecacheof(list(
if(final_x || final_y)
return locate(final_x, final_y, T.z)
+
+/**
+ * Basically just transforms the ROLL_RESULT defines into text.
+ */
+/proc/roll_result_text(roll)
+ switch(roll)
+ if(ROLL_RESULT_CRITICAL_SUCCESS)
+ return "critical success"
+ if(ROLL_RESULT_SUCCESS)
+ return "success"
+ if(ROLL_RESULT_FAILURE)
+ return "failure"
+ if(ROLL_RESULT_CRITICAL_FAILURE)
+ return "critical failure"
+ crash_with("Roll result given invalid roll: [roll]")
diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm
index aa70c4106ce..6ee66f8150f 100644
--- a/code/_onclick/item_attack.dm
+++ b/code/_onclick/item_attack.dm
@@ -188,6 +188,7 @@ This calls [atom/proc/tool_act], among others.
//Called when a weapon is used to make a successful melee attack on a mob. Returns whether damage was dealt.
/obj/item/proc/apply_hit_effect(mob/living/target, mob/living/user, var/hit_zone)
var/power = force
+ SEND_SIGNAL(user, COMSIG_APPLY_HIT_EFFECT, target, src, &power, &hit_zone)
if((user.mutations & HULK))
power *= 1.25
if(user.is_berserk())
diff --git a/code/controllers/subsystems/skills.dm b/code/controllers/subsystems/skills.dm
new file mode 100644
index 00000000000..289611aa5fe
--- /dev/null
+++ b/code/controllers/subsystems/skills.dm
@@ -0,0 +1,32 @@
+SUBSYSTEM_DEF(skills)
+ name = "Skills"
+ flags = SS_NO_FIRE
+
+ /// This is essentially the list we use to read skills in the character setup.
+ var/list/skill_tree = list()
+
+ /// The set of all skills that are "forced" in order to guarantee necessary components are applied.
+ var/list/required_skills = list()
+
+/datum/controller/subsystem/skills/Initialize()
+ // Initialize the skill category lists first.
+ // This creates linked lists as follows: "Science" -> empty list
+ for(var/singleton/skill_category/skill_category as anything in GET_SINGLETON_SUBTYPE_LIST(/singleton/skill_category))
+ skill_tree[skill_category] = list()
+
+ // Now, initialize all the skills.
+ // What actually goes on here: we want a tree that we can traverse programmatically.
+ // To do that, we first of all make empty lists above with all the categories (they're singletons so we can easily iterate over them).
+ // Next, we add the empty subcategory lists if they're not present. At this point, the tree would look like "Combat" -> "Melee" -> empty list
+ // After that's done, if our skill is not present, add it to the empty list of the subcategory.
+ for(var/singleton/skill/skill as anything in GET_SINGLETON_SUBTYPE_LIST(/singleton/skill))
+ if (skill.required)
+ required_skills += skill.type
+ var/singleton/skill_category/skill_category = GET_SINGLETON(skill.category)
+ if(!(skill.subcategory in skill_tree[skill_category]))
+ skill_tree[skill_category] |= skill.subcategory
+ skill_tree[skill_category][skill.subcategory] = list()
+
+ if(!(skill in skill_tree[skill_category][skill.subcategory]))
+ skill_tree[skill_category][skill.subcategory] |= skill
+ return SS_INIT_SUCCESS
diff --git a/code/datums/components/morale/moodlets.dm b/code/datums/components/morale/moodlets.dm
new file mode 100644
index 00000000000..749470fdb28
--- /dev/null
+++ b/code/datums/components/morale/moodlets.dm
@@ -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
diff --git a/code/datums/components/morale/morale_component.dm b/code/datums/components/morale/morale_component.dm
new file mode 100644
index 00000000000..5e3cbe6a142
--- /dev/null
+++ b/code/datums/components/morale/morale_component.dm
@@ -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..."))
diff --git a/code/datums/components/skills/combat/armed_combat_skill_component.dm b/code/datums/components/skills/combat/armed_combat_skill_component.dm
new file mode 100644
index 00000000000..4da146b54d6
--- /dev/null
+++ b/code/datums/components/skills/combat/armed_combat_skill_component.dm
@@ -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))
diff --git a/code/datums/components/skills/combat/firearms_skill_component.dm b/code/datums/components/skills/combat/firearms_skill_component.dm
new file mode 100644
index 00000000000..8e0c0b01721
--- /dev/null
+++ b/code/datums/components/skills/combat/firearms_skill_component.dm
@@ -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!"))
diff --git a/code/datums/components/skills/combat/leadership_skill_component.dm b/code/datums/components/skills/combat/leadership_skill_component.dm
new file mode 100644
index 00000000000..e995b06e520
--- /dev/null
+++ b/code/datums/components/skills/combat/leadership_skill_component.dm
@@ -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
diff --git a/code/datums/components/skills/combat/unarmed_combat_skill_component.dm b/code/datums/components/skills/combat/unarmed_combat_skill_component.dm
new file mode 100644
index 00000000000..e95540c197e
--- /dev/null
+++ b/code/datums/components/skills/combat/unarmed_combat_skill_component.dm
@@ -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
diff --git a/code/datums/components/skills/engineering/atmospherics_systems_skill_component.dm b/code/datums/components/skills/engineering/atmospherics_systems_skill_component.dm
new file mode 100644
index 00000000000..77a1b0a0c71
--- /dev/null
+++ b/code/datums/components/skills/engineering/atmospherics_systems_skill_component.dm
@@ -0,0 +1 @@
+/datum/component/skill/atmospherics_systems
diff --git a/code/datums/components/skills/engineering/electrical_engineering_skill_component.dm b/code/datums/components/skills/engineering/electrical_engineering_skill_component.dm
new file mode 100644
index 00000000000..d0356ba99a4
--- /dev/null
+++ b/code/datums/components/skills/engineering/electrical_engineering_skill_component.dm
@@ -0,0 +1 @@
+/datum/component/skill/electrical_engineering
diff --git a/code/datums/components/skills/engineering/mechanical_engineering_skill_component.dm b/code/datums/components/skills/engineering/mechanical_engineering_skill_component.dm
new file mode 100644
index 00000000000..a3207c66fa0
--- /dev/null
+++ b/code/datums/components/skills/engineering/mechanical_engineering_skill_component.dm
@@ -0,0 +1 @@
+/datum/component/skill/mechanical_engineering
diff --git a/code/datums/components/skills/engineering/reactor_systems_skill_component.dm b/code/datums/components/skills/engineering/reactor_systems_skill_component.dm
new file mode 100644
index 00000000000..a402cc1ca24
--- /dev/null
+++ b/code/datums/components/skills/engineering/reactor_systems_skill_component.dm
@@ -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."))
diff --git a/code/datums/components/skills/medical/anatomy_skill_component.dm b/code/datums/components/skills/medical/anatomy_skill_component.dm
new file mode 100644
index 00000000000..7f89c1e17b0
--- /dev/null
+++ b/code/datums/components/skills/medical/anatomy_skill_component.dm
@@ -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
diff --git a/code/datums/components/skills/medical/forensics_skill_component.dm b/code/datums/components/skills/medical/forensics_skill_component.dm
new file mode 100644
index 00000000000..40fb4fff1d2
--- /dev/null
+++ b/code/datums/components/skills/medical/forensics_skill_component.dm
@@ -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
diff --git a/code/datums/components/skills/medical/medicine_skill_component.dm b/code/datums/components/skills/medical/medicine_skill_component.dm
new file mode 100644
index 00000000000..d58a1bb2fdd
--- /dev/null
+++ b/code/datums/components/skills/medical/medicine_skill_component.dm
@@ -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
diff --git a/code/datums/components/skills/medical/pharmacology_skill_component.dm b/code/datums/components/skills/medical/pharmacology_skill_component.dm
new file mode 100644
index 00000000000..232b9f45bc3
--- /dev/null
+++ b/code/datums/components/skills/medical/pharmacology_skill_component.dm
@@ -0,0 +1 @@
+/datum/component/skill/pharmacology
diff --git a/code/datums/components/skills/medical/surgery_skill_component.dm b/code/datums/components/skills/medical/surgery_skill_component.dm
new file mode 100644
index 00000000000..081fcd78e8f
--- /dev/null
+++ b/code/datums/components/skills/medical/surgery_skill_component.dm
@@ -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
diff --git a/code/datums/components/skills/operations/pilot_mechs_skill_component.dm b/code/datums/components/skills/operations/pilot_mechs_skill_component.dm
new file mode 100644
index 00000000000..a1ee2a9ac75
--- /dev/null
+++ b/code/datums/components/skills/operations/pilot_mechs_skill_component.dm
@@ -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)
diff --git a/code/datums/components/skills/operations/pilot_spacecraft_skill_component.dm b/code/datums/components/skills/operations/pilot_spacecraft_skill_component.dm
new file mode 100644
index 00000000000..ac17e51ccb0
--- /dev/null
+++ b/code/datums/components/skills/operations/pilot_spacecraft_skill_component.dm
@@ -0,0 +1 @@
+/datum/component/skill/pilot_spacecraft
diff --git a/code/datums/components/skills/operations/robotics_skill_component.dm b/code/datums/components/skills/operations/robotics_skill_component.dm
new file mode 100644
index 00000000000..622449b9b9d
--- /dev/null
+++ b/code/datums/components/skills/operations/robotics_skill_component.dm
@@ -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."))
diff --git a/code/datums/components/skills/science/archaology_skill_component.dm b/code/datums/components/skills/science/archaology_skill_component.dm
new file mode 100644
index 00000000000..916653ba8b9
--- /dev/null
+++ b/code/datums/components/skills/science/archaology_skill_component.dm
@@ -0,0 +1 @@
+/datum/component/skill/archaology
diff --git a/code/datums/components/skills/science/research_skill_component.dm b/code/datums/components/skills/science/research_skill_component.dm
new file mode 100644
index 00000000000..409f2d9fa02
--- /dev/null
+++ b/code/datums/components/skills/science/research_skill_component.dm
@@ -0,0 +1 @@
+/datum/component/skill/research
diff --git a/code/datums/components/skills/science/xenobiology_skill_component.dm b/code/datums/components/skills/science/xenobiology_skill_component.dm
new file mode 100644
index 00000000000..b5f5fd26f02
--- /dev/null
+++ b/code/datums/components/skills/science/xenobiology_skill_component.dm
@@ -0,0 +1 @@
+/datum/component/skill/xenobiology
diff --git a/code/datums/components/skills/science/xenobotany_skill_component.dm b/code/datums/components/skills/science/xenobotany_skill_component.dm
new file mode 100644
index 00000000000..69c2fd5068e
--- /dev/null
+++ b/code/datums/components/skills/science/xenobotany_skill_component.dm
@@ -0,0 +1 @@
+/datum/component/skill/xenobotany
diff --git a/code/datums/components/skills/service/bartending_skill_component.dm b/code/datums/components/skills/service/bartending_skill_component.dm
new file mode 100644
index 00000000000..2377499dcf3
--- /dev/null
+++ b/code/datums/components/skills/service/bartending_skill_component.dm
@@ -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.
diff --git a/code/datums/components/skills/service/cooking_skill_component.dm b/code/datums/components/skills/service/cooking_skill_component.dm
new file mode 100644
index 00000000000..bcfc1bde97e
--- /dev/null
+++ b/code/datums/components/skills/service/cooking_skill_component.dm
@@ -0,0 +1 @@
+/datum/component/skill/cooking
diff --git a/code/datums/components/skills/service/entertaining_skill_component.dm b/code/datums/components/skills/service/entertaining_skill_component.dm
new file mode 100644
index 00000000000..5af0227e08e
--- /dev/null
+++ b/code/datums/components/skills/service/entertaining_skill_component.dm
@@ -0,0 +1 @@
+/datum/component/skill/entertaining
diff --git a/code/datums/components/skills/service/gardening_skill_component.dm b/code/datums/components/skills/service/gardening_skill_component.dm
new file mode 100644
index 00000000000..451b6eae3c0
--- /dev/null
+++ b/code/datums/components/skills/service/gardening_skill_component.dm
@@ -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)
diff --git a/code/datums/components/skills/skill_component.dm b/code/datums/components/skills/skill_component.dm
new file mode 100644
index 00000000000..abdcfa536b6
--- /dev/null
+++ b/code/datums/components/skills/skill_component.dm
@@ -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
diff --git a/code/datums/skills/_skill_categories.dm b/code/datums/skills/_skill_categories.dm
new file mode 100644
index 00000000000..683a62477b1
--- /dev/null
+++ b/code/datums/skills/_skill_categories.dm
@@ -0,0 +1,29 @@
+/singleton/skill_category
+ /// The name of the skill category.
+ var/name
+ /// The description and purpose of the skill category, displayed in the info tab.
+ var/desc
+ /// The hex colour of the skill category. Shows as background on the skills tab.
+ var/color
+ /// The number of base points a character gets in this category.
+ var/base_skill_points
+
+/singleton/skill_category/proc/calculate_skill_points(datum/species/species, age, singleton/origin_item/culture, singleton/origin_item/origin)
+ var/list/species_modifiers = species.modify_skill_points(src, age)
+ return base_skill_points * species_modifiers[name]
+
+/singleton/skill_category/everyday
+ name = SKILL_CATEGORY_EVERYDAY
+ desc = "An everyday skill is a skill that can be picked up normally. Think like mixing a drink, growing a plant, cooking, and so on."
+ base_skill_points = BASE_SKILL_POINTS_EVERYDAY
+
+/singleton/skill_category/occupational
+ name = SKILL_CATEGORY_OCCUPATIONAL
+ desc = "Occupational skills are the skills necessary for you to do your job. They typically lock certain aspects of your department if you aren't proficient enough."
+ base_skill_points = BASE_SKILL_POINTS_OCCUPATIONAL
+
+/singleton/skill_category/combat
+ name = SKILL_CATEGORY_COMBAT
+ desc = "A combat skill is a skill that has a direct effect in combat. These have an increased cost."
+ base_skill_points = BASE_SKILL_POINTS_COMBAT
+
diff --git a/code/datums/skills/_skills.dm b/code/datums/skills/_skills.dm
new file mode 100644
index 00000000000..e8c7db67bf4
--- /dev/null
+++ b/code/datums/skills/_skills.dm
@@ -0,0 +1,80 @@
+/singleton/skill
+ /// The displayed name of the skill.
+ var/name
+ /// A description of what this skill entails.
+ var/description
+ /// A detailed description of what a character should expect with their current level in this skill. Assoc list of skill level to string.
+ var/alist/skill_level_descriptions = alist(
+ SKILL_LEVEL_UNFAMILIAR = "You are clueless.",
+ SKILL_LEVEL_FAMILIAR = "You have read up on the subject or have prior real experience.",
+ SKILL_LEVEL_TRAINED = "You have received some degree of official training on the subject, whether through certifications or courses.",
+ SKILL_LEVEL_PROFESSIONAL = "You are an expert in this field, devoting many years of study or practice to it."
+ )
+ /// Map of skill names and descriptions by their index.
+ var/list/skill_level_map = list(
+ "Unfamiliar",
+ "Familiar",
+ "Trained",
+ "Professional"
+ )
+ /// The maximum level someone with no education can reach in this skill. Typically, this should be FAMILIAR on occupational skills.
+ /// If null, then there is no cap.
+ var/uneducated_skill_cap
+ /// The maximum level this skill can reach.
+ var/maximum_level = SKILL_LEVEL_TRAINED
+ /// The category of this skill. Used for sorting, typically.
+ var/category
+ /// The sub-category of this skill. Used to better sort skills.
+ var/subcategory
+ /**
+ * Required skills are always included in the user's saved skills preference, even if it's at the lowest rank.
+ * This is needed for skills that rely on components.
+ */
+ var/required = FALSE
+ /// The component datum that this skill will add during character spawning
+ var/component_type = null
+ /// Map of skill levels to level costs. How many skill points it takes to purchase this rank in a skill.
+ var/alist/skill_cost_map = alist(
+ SKILL_LEVEL_UNFAMILIAR = 0,
+ SKILL_LEVEL_FAMILIAR = 2,
+ SKILL_LEVEL_TRAINED = 4,
+ SKILL_LEVEL_PROFESSIONAL = 8
+ )
+
+/**
+ * Returns the maximum level a character can have in this skill depending on education.
+ */
+/singleton/skill/proc/get_maximum_level(singleton/education/education)
+ if(!istype(education))
+ crash_with("SKILL: Invalid [education] fed to get_maximum_level!")
+
+ // If there is no uneducated skill cap, it means we can always pick the maximum level.
+ if(!uneducated_skill_cap)
+ return maximum_level
+
+ // Otherwise, we need to check the education...
+ if(type in education.skills)
+ return education.skills[type]
+
+
+ return uneducated_skill_cap
+
+/**
+ * Returns the cost of this skill, modified by its difficulty modifier.
+ */
+/singleton/skill/proc/get_cost(level)
+ return skill_cost_map[level]
+
+/**
+ * ECS hook for Skills, based on a one-shot-on-spawn pattern common to traits/disabilities/loadouts, etc.
+ * Skills may optionally include this, but are not required to.
+ * This is primarily useful for making skills that offload all of their functional logic to a Component.
+ *
+ * It will be called during the process of spawning a player character in.
+ */
+/singleton/skill/proc/on_spawn(mob/owner, skill_level)
+ SHOULD_CALL_PARENT(TRUE)
+ if (!owner || !component_type)
+ return
+
+ owner.AddComponent(component_type, skill_level)
diff --git a/code/datums/skills/combat/combat.dm b/code/datums/skills/combat/combat.dm
new file mode 100644
index 00000000000..cfc654b9f82
--- /dev/null
+++ b/code/datums/skills/combat/combat.dm
@@ -0,0 +1,80 @@
+/*
+ All of the combat skills in this file are meant to be presented to the players as a "Bonus" to certain aspects of combat. However,
+ factually they are actually a penalty to combat skills for anyone not in Security. This lets us have better balancing levers against antags,
+ giving non-security leeway in being allowed to fight antags since they will be mechanically far weaker than actual security. Players can then be encouraged,
+ rather than discouraged from picking a rifle up off the floor to fight mercs, since they get hit with a huge accuracy penalty when using it for not being security.
+
+ Players are much happier anyways if you tell them something provides a bonus if they invest in it, even if the baseline before the bonus was less than what they had before.
+ A lot like the "Rested Experience Effect" in video game design.
+
+ As a general rule of thumb, having rank 3 in a combat skill is equivalent to "Vanilla" stats. No penalties, no bonuses.
+ Rank 1 and 2 give penalties to combat characteristics. Rank 4; which isn't accessible to crew and will come in a later PR via implants/modifiers,
+ instead provides a bonus to combat characteristics. All 3 combat skills and their relevant checks are also influenced by the Morale component,
+ which provides up to an equivalent +/- 0.5 ranks in combat skills depending on how much positive or negative morale has been accumulated.
+ */
+/singleton/skill/unarmed_combat
+ name = "Unarmed Combat"
+ description = "Unarmed combat represents your training in hand-to-hand combat, or without a weapon. It also influences your ability to resist actions like disarms or martial arts. " \
+ + "Higher ranks in Unarmed Combat provide bonuses when attacking with your fists, as well as bonuses to defending against others unarmed attacks and disarms."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ skill_level_descriptions = alist(
+ SKILL_LEVEL_UNFAMILIAR = "You have rarely, if ever, fought someone in your life.
" \
+ + " - You are more likely to miss when attempting to punch anywhere not the torso.
" \
+ + " - You are less likely to block unarmed attacks that you are aware of.
" \
+ + " - Enemies are more likely to block your unarmed attacks.",
+ SKILL_LEVEL_FAMILIAR = "You have some experience throwing punches. You are no stranger to a close-quarters fight, though anyone with real training is likely to overwhelm you.
" \
+ + " - You are slightly more likely to miss when attempting to punch anywhere not the torso.
" \
+ + " - You are slightly less likely to block unarmed attacks that you are aware of.
" \
+ + " - Enemies are slightly more likely to block your unarmed attacks.",
+ SKILL_LEVEL_TRAINED = "You have been trained, whether by being in the military, taking close-quarters-combat classes or simply through experience, to both keep calm in a close-quarters fight and also fight well.
" \
+ + " - You suffer no maluses to your close-quarters combat.
" \
+ + " - You have no bonuses to unarmed fighting either.",
+ SKILL_LEVEL_PROFESSIONAL = "You have had many years of martial arts experience. Probably having a Black Belt equivalent in one or more martial arts.
" \
+ + " - You are slightly less likely to miss when attempting to punch anywhere not the torso.
" \
+ + " - You are slightly more likely to block unarmed attacks that you are aware of.
" \
+ + " - Enemies are slightly less likely to block your unarmed attacks.",
+ )
+ category = /singleton/skill_category/combat
+ subcategory = SKILL_SUBCATEGORY_MELEE
+ required = TRUE
+ component_type = UNARMED_COMBAT_SKILL_COMPONENT
+
+/// Currently just a simple percent modifier to melee weapon damage. -20% at rank 1, -10% at rank 2, 0% at rank 3, +10% at rank 4.
+/singleton/skill/armed_combat
+ name = "Armed Combat"
+ description = "Armed Combat influences your effectiveness when fighting with any melee weapon. Having low ranks in this skill slightly decreases damage dealt with melee weapons, while higher ranks can slightly increase it."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ category = /singleton/skill_category/combat
+ subcategory = SKILL_SUBCATEGORY_MELEE
+ component_type = ARMED_COMBAT_SKILL_COMPONENT
+
+/singleton/skill/firearms
+ name = "Firearms"
+ description = "Firearms represents your training in using all forms of ranged weapons. Having a high rank in in this skill provides bonuses to accuracy when shooting."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ category = /singleton/skill_category/combat
+ subcategory = SKILL_SUBCATEGORY_RANGED
+ required = TRUE
+ component_type = FIREARMS_SKILL_COMPONENT
+ skill_level_descriptions = alist(
+ SKILL_LEVEL_UNFAMILIAR = "You have no knowledge of how firearms work at all.
" \
+ + " - Firearms you shoot have a 60 degree spread-angle increase, making them very inaccurate.
" \
+ + " - You have a decent chance of failing to find the safety when attempting to switch it on or off.",
+ SKILL_LEVEL_FAMILIAR = "You have fired a gun once or twice in your life, but are by no means fully trained. At least you know how to not shoot yourself in the foot or get scope-eye.
" \
+ + " - Firearms you shoot have a 30 degree spread-angle increase, making them somewhat less accurate.",
+ SKILL_LEVEL_TRAINED = "You have both training and actual experience with firearms. Equivalent to a few years of experience in roles such as military, police, or armed security.
" \
+ + " - You suffer no maluses to firearms use.
" \
+ + " - You have no bonuses to firearms use either.",
+ SKILL_LEVEL_PROFESSIONAL = "You have many years of experience with firearms, potentially even in actual combat. Retired special forces, police marksmen, and hardened mercenaries fall under this category.
" \
+ + " - 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
+
diff --git a/code/datums/skills/everyday/service.dm b/code/datums/skills/everyday/service.dm
new file mode 100644
index 00000000000..4caa2c77ce7
--- /dev/null
+++ b/code/datums/skills/everyday/service.dm
@@ -0,0 +1,44 @@
+/singleton/skill/bartending
+ name = "Bartending"
+ description = "Users of this skill can create mixed drinks of varying quality, which provide a long lasting morale bonus to anyone who consumes them. This skill is activated by first mixing a drink in a Drink Mixer, then pressing Z to shake it."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ category = /singleton/skill_category/everyday
+ subcategory = SKILL_SUBCATEGORY_SERVICE
+ component_type = BARTENDING_SKILL_COMPONENT
+ skill_level_descriptions = alist(
+ SKILL_LEVEL_UNFAMILIAR = "Drinks you produce have no effects.",
+ SKILL_LEVEL_FAMILIAR = "Drinks you produce are slightly higher quality.",
+ SKILL_LEVEL_TRAINED = "Drinks you produce are of moderately higher quality.",
+ SKILL_LEVEL_PROFESSIONAL = "Drinks you produce are of a significantly higher quality."
+ )
+
+/singleton/skill/cooking
+ name = "Cooking"
+ description = "Currently unimplemented."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ category = /singleton/skill_category/everyday
+ subcategory = SKILL_SUBCATEGORY_SERVICE
+ component_type = COOKING_SKILL_COMPONENT
+
+/singleton/skill/gardening
+ name = "Gardening"
+ description = "Users of this skill gain bonuses when harvesting grown plants."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ category = /singleton/skill_category/everyday
+ subcategory = SKILL_SUBCATEGORY_SERVICE
+ component_type = GARDENING_SKILL_COMPONENT
+ skill_level_descriptions = alist(
+ SKILL_LEVEL_UNFAMILIAR = "You have no modifiers to gardening.",
+ SKILL_LEVEL_FAMILIAR = "You gain +1 produce when harvesting plants",
+ SKILL_LEVEL_TRAINED = "You gain +2 produce when harvesting plants",
+ SKILL_LEVEL_PROFESSIONAL = "You gain +3 produce when harvesting plants"
+ )
+
+// Temporarily commented because this is a little too complicated to catch in the initial PR release.
+// /singleton/skill/entertaining
+// name = "Entertaining"
+// description = "Entertainers are able to generate positive moodlets by playing instruments."
+// maximum_level = SKILL_LEVEL_PROFESSIONAL
+// category = /singleton/skill_category/everyday
+// subcategory = SKILL_SUBCATEGORY_SERVICE
+// component_type = ENTERTAINING_SKILL_COMPONENT
diff --git a/code/datums/skills/occupational/engineering.dm b/code/datums/skills/occupational/engineering.dm
new file mode 100644
index 00000000000..aec3d92e62c
--- /dev/null
+++ b/code/datums/skills/occupational/engineering.dm
@@ -0,0 +1,69 @@
+/singleton/skill/electrical_engineering
+ name = "Electrical Engineering"
+ description = "Not currently implemented"
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_FAMILIAR
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_ENGINEERING
+ required = TRUE
+ component_type = ELECTRICAL_ENGINEERING_SKILL_COMPONENT
+
+/singleton/skill/mechanical_engineering
+ name = "Mechanical Engineering"
+ description = "Mechanical engineering has to do with general construction of objects, walls, windows, and so on. It is also necessary for the usage of heavy machinery \
+ such as emitters. This skill is also commonly used for crafting items out of raw materials, and heavily governs the quality of said objects."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_FAMILIAR
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_ENGINEERING
+ required = TRUE
+ component_type = MECHANICAL_ENGINEERING_SKILL_COMPONENT
+ skill_level_descriptions = alist(
+ SKILL_LEVEL_UNFAMILIAR = "You have little to no experience working with physical machinery.
" \
+ + " - Items you craft will typically be of low quality.
" \
+ + " - You take twice as long to craft anything.
" \
+ + " - You cannot craft any object that requires this skill.
" \
+ + " - You cannot interact with most Engineering machinery.",
+ SKILL_LEVEL_FAMILIAR = "You have some experience working with physical machinery, though are not formally trained.
" \
+ + " - Items you craft will be of slightly lower than average quality.
" \
+ + " - You take 50% longer to craft anything.
" \
+ + " - You can interact with some Engineering-related machinery.",
+ SKILL_LEVEL_TRAINED = "You have formal training in general Engineering concepts, equivalent to a Bachelor's Degree.
" \
+ + " - You can craft items at the standard speed.
" \
+ + " - Items you craft are generally at average quality.
" \
+ + " - You can interact with most if not all Engineering equipment.",
+ SKILL_LEVEL_PROFESSIONAL = "You have many years of training in general Engineering concepts, equivalent to a Master's Degree or better.
" \
+ + " - You can craft items significantly faster.
" \
+ + " - Items you craft are of much higher quality on average.
" \
+ )
+
+/singleton/skill/atmospherics_systems
+ name = "Atmospherics Systems"
+ description = "Not currently implemented."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_FAMILIAR
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_ENGINEERING
+ required = TRUE
+ component_type = ATMOSPHERICS_SYSTEMS_SKILL_COMPONENT
+
+/singleton/skill/reactor_systems
+ name = "Reactor Systems"
+ description = "Reactor systems envelops anything used for reactors, such as the computers and gyrotrons for the INDRA. It is also necessary to correctly interpret information \
+ from reactor monitoring programs."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_FAMILIAR
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_ENGINEERING
+ required = TRUE
+ component_type = REACTOR_SYSTEMS_SKILL_COMPONENT
+ skill_level_descriptions = alist(
+ SKILL_LEVEL_UNFAMILIAR = "You have little to no experience working with Reactor systems.
" \
+ + " - You cannot interact with Reactor computers such as the INDRA controllers.",
+ SKILL_LEVEL_FAMILIAR = "You have some experience working with physical machinery, though are not formally trained.
" \
+ + " - You cannot interact with Reactor computers such as the INDRA controller.",
+ SKILL_LEVEL_TRAINED = "You have formal training in general Engineering concepts, equivalent to a Bachelor's Degree.
" \
+ + " - This is the minimum skill level required to operate the INDRA.",
+ SKILL_LEVEL_PROFESSIONAL = "You have many years of training in general Engineering concepts, equivalent to a Master's Degree or better.
" \
+ + " - Not currently implemented differently from Trained." \
+ )
diff --git a/code/datums/skills/occupational/medical.dm b/code/datums/skills/occupational/medical.dm
new file mode 100644
index 00000000000..edf9f4a4717
--- /dev/null
+++ b/code/datums/skills/occupational/medical.dm
@@ -0,0 +1,83 @@
+/singleton/skill/medicine
+ name = "Medicine"
+ description = "Governs the user's ability to perform basic first-aid, as well as operate medical equipment."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_TRAINED
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_MEDICAL
+ required = TRUE
+ component_type = MEDICINE_SKILL_COMPONENT
+
+/singleton/skill/surgery
+ name = "Surgery"
+ description = "Governs the user's ability to perform surgical procedures on organic humanoids, as well as what complexity of procedures can be performed. " \
+ + "This does not affect \"surgeries\" performed on mechanical prosthetics, robots, or synthetics in general."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_FAMILIAR // Only the most basic of all surgeries could be bought into, you'll need a real doctor education to do anything more.
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_MEDICAL
+ component_type = SURGERY_SKILL_COMPONENT
+ required = TRUE
+ skill_level_descriptions = alist(
+ SKILL_LEVEL_UNFAMILIAR = "You have zero training or experience with surgery.
" \
+ + " - You cannot perform any surgical procedures on organic humanoids.",
+ SKILL_LEVEL_FAMILIAR = "You have minimal training on the basics of surgery. This is equivalent to a fresh med school graduate, or a military corpsman.
" \
+ + "You can perform the following procedures:
" \
+ + " - Opening or Closing incisions.
" \
+ + " - Clamp bleeders.
" \
+ + " - Fixing Arterial Bleeding
" \
+ + " - Amputating a limb.",
+ SKILL_LEVEL_TRAINED = "You have years of formal training and experience with surgery. This is equivalent to a fully licensed surgeon.
" \
+ + "You can perform the following procedures:
" \
+ + " - Opening or Closing incisions.
" \
+ + " - Clamp bleeders.
" \
+ + " - Fixing Arterial Bleeding
" \
+ + " - Amputating a limb." \
+ + " - Repairing broken bones or muscle tendons.
" \
+ + " - Sawing and retracting a ribcage.
" \
+ + " - Facial Reconstructions only on organic humanoids, not synthetics.
" \
+ + " - Remove or Insert implants.
" \
+ + " - Re-attach (organic) limbs. Robotic limbs require the Robotics skill instead.
" \
+ + " - Repair non-necrotic organs other than the brain, or mechanical prosthetics.
",
+ SKILL_LEVEL_PROFESSIONAL = "You are a world class surgeon with decades worth of training and experience.
" \
+ + "You can perform the following procedures:
" \
+ + " - Opening or Closing incisions.
" \
+ + " - Clamp bleeders.
" \
+ + " - Fixing Arterial Bleeding
" \
+ + " - Amputating a limb." \
+ + " - Repairing broken bones or muscle tendons.
" \
+ + " - Sawing and retracting a ribcage.
" \
+ + " - Facial Reconstructions only on organic humanoids, not synthetics.
" \
+ + " - Remove or Insert implants.
" \
+ + " - Re-attach (organic) limbs. Robotic limbs require the Robotics skill instead.
" \
+ + " - Repair non-necrotic organs other than the brain, or mechanical prosthetics.
" \
+ + " - Repair and debride necrotic organs.
" \
+ + " - Repair damaged brains."
+ )
+
+/singleton/skill/pharmacology
+ name = "Pharmacology"
+ description = "Not currently implemented."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_TRAINED
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_MEDICAL
+ component_type = PHARMACOLOGY_SKILL_COMPONENT
+
+/singleton/skill/anatomy
+ name = "Anatomy"
+ description = "Not currently implemented."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_TRAINED
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_MEDICAL
+ component_type = ANATOMY_SKILL_COMPONENT
+
+/singleton/skill/forensics
+ name = "Forensics"
+ description = "Not currently implemented."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_TRAINED
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_MEDICAL
+ component_type = FORENSICS_SKILL_COMPONENT
diff --git a/code/datums/skills/occupational/operations.dm b/code/datums/skills/occupational/operations.dm
new file mode 100644
index 00000000000..71728bbfc7c
--- /dev/null
+++ b/code/datums/skills/occupational/operations.dm
@@ -0,0 +1,89 @@
+/singleton/skill/robotics
+ name = "Robotics"
+ description = "Governs the user's ability to perform surgical procedures on synthetics, as well as the complexity of what procedures can be performed."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_FAMILIAR
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_OPERATIONS
+ component_type = ROBOTICS_SKILL_COMPONENT
+ required = TRUE
+ skill_level_descriptions = alist(
+ SKILL_LEVEL_UNFAMILIAR = "You have zero training or experience with synthetics.
" \
+ + " - You cannot perform any surgical procedures on synthetics.",
+ SKILL_LEVEL_FAMILIAR = "You have minimal training on the basics of synthetic repair and maintenance. This could be the level of a hobbyist, or someone currently pursuing a degree in robotics.
" \
+ + "You can perform the following procedures:
" \
+ + " - Opening or closing external maintenance panels to make superficial repairs.
" \
+ + " - Repairing basic damage with a welder or cables.
" \
+ + " - Repairing external damage to mechanical limbs.
" \
+ + " - Cutting someone out of a hardsuit.",
+ SKILL_LEVEL_TRAINED = "You have years of formal training or experience on repairing and maintaining synthetics equivalent to a Bachelor's degree in Robotics.
" \
+ + "You can perform the following procedures:
" \
+ + " - Opening or closing external maintenance panels to make superficial repairs.
" \
+ + " - Repairing basic damage with a welder or cables.
" \
+ + " - Repairing external damage to mechanical limbs.
" \
+ + " - Cutting someone out of a hardsuit." \
+ + " - Fully opening or closing mechanical parts to access internal systems, allowing repairs to any amount of damage.
" \
+ + " - Repairing, removing, or adding mechanical organs.
" \
+ + " - Facial Reconstructions performed on Shell IPCs.
" \
+ + " - Attaching mechanical limbs (including to organics).
" \
+ + " - Re-attach (organic) limbs. Robotic limbs require the Robotics skill instead.
" \
+ + " - Perform all forms of internal repairs to IPCs.
" \
+ + " - Prepare an MMI for cyborgification.",
+ SKILL_LEVEL_PROFESSIONAL = "Not currently implemented, functions exactly as per Trained.
" \
+ + "You can perform the following procedures:
" \
+ + " - Opening or closing external maintenance panels to make superficial repairs.
" \
+ + " - Repairing basic damage with a welder or cables.
" \
+ + " - Repairing external damage to mechanical limbs.
" \
+ + " - Cutting someone out of a hardsuit." \
+ + " - Fully opening or closing mechanical parts to access internal systems, allowing repairs to any amount of damage.
" \
+ + " - Repairing, removing, or adding mechanical organs.
" \
+ + " - Facial Reconstructions performed on Shell IPCs.
" \
+ + " - Attaching mechanical limbs (including to organics).
" \
+ + " - Re-attach (organic) limbs. Robotic limbs require the Robotics skill instead.
" \
+ + " - Perform all forms of internal repairs to IPCs.
" \
+ + " - Prepare an MMI for cyborgification."
+ )
+
+/singleton/skill/pilot_spacecraft
+ name = "Pilot: Spacecraft"
+ description = "Governs the user's ability to pilot spacecraft of any size, and is required to do so in the first place."
+ maximum_level = SKILL_LEVEL_FAMILIAR
+ uneducated_skill_cap = SKILL_LEVEL_FAMILIAR
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_OPERATIONS
+ skill_level_map = list(
+ "Unlicensed",
+ "Licensed Pilot"
+ )
+ skill_level_descriptions = alist(
+ SKILL_LEVEL_UNFAMILIAR = "You are incapable of piloting spacecraft.",
+ SKILL_LEVEL_FAMILIAR = "You are capable of piloting spacecraft."
+ )
+ skill_cost_map = alist(
+ SKILL_LEVEL_UNFAMILIAR = 0,
+ SKILL_LEVEL_FAMILIAR = 6
+ )
+ required = TRUE
+ component_type = PILOT_SPACECRAFT_SKILL_COMPONENT
+
+/singleton/skill/pilot_mechs
+ name = "Pilot: Exosuits"
+ description = "Governs the user's ability to pilot mechs of any kind."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_TRAINED
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_OPERATIONS
+ skill_level_descriptions = alist(
+ SKILL_LEVEL_UNFAMILIAR = "What's a mech?
" \
+ + " - You have a 10% chance to move in a wrong direction when controlling mechs.
" \
+ + " - Mech handling characteristics such as strafing and reverse speeds are significantly worse for you.
" \
+ + " - It takes you awhile to find the power switch in a mech.",
+ SKILL_LEVEL_FAMILIAR = "You have a license to pilot mechs, but are relatively inexperienced.
" \
+ + " - You have a small penalty to mech handling characteristics, such as slightly slower strafe and reverse speeds.",
+ SKILL_LEVEL_TRAINED = "You have a decent amount of experience piloting mechs.
" \
+ + " - You have no penalties or bonuses for piloting mechs.",
+ SKILL_LEVEL_PROFESSIONAL = "Look on the bright side kid, you get to keep all the money.
" \
+ + " - You have a small bonus to mech handling characteristics. Turning, Strafing, and Reverse speeds are all improved when piloting mechs. Forward speeds are still unchanged."
+ )
+ required = TRUE
+ component_type = PILOT_MECHS_SKILL_COMPONENT
diff --git a/code/datums/skills/occupational/science.dm b/code/datums/skills/occupational/science.dm
new file mode 100644
index 00000000000..b6606ea5716
--- /dev/null
+++ b/code/datums/skills/occupational/science.dm
@@ -0,0 +1,38 @@
+/singleton/skill/research
+ name = "Research"
+ description = "Not currently implemented"
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_TRAINED
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_SCIENCE
+ component_type = RESEARCH_SKILL_COMPONENT
+
+/singleton/skill/xenobotany
+ name = "Xenobotany"
+ description = "Not currently implemented."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_TRAINED
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_SCIENCE
+ component_type = XENOBOTANY_SKILL_COMPONENT
+
+/singleton/skill/archaeology
+ name = "Xenoarchaeology"
+ description = "Not currently implemented."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_TRAINED
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_SCIENCE
+ component_type = ARCHAOLOGY_SKILL_COMPONENT
+
+/singleton/skill/xenobiology
+ name = "Xenobiology"
+ description = "Xenobiology is the study of the research and cataloguing of alien lifeforms. It is necessary not only for the proper detailing of \
+ alien creatures, but also for their processing, such as with slimes. " \
+ + "Having this skill at least at the \"Trained\" rank is required to extract cores from slimes."
+ maximum_level = SKILL_LEVEL_PROFESSIONAL
+ uneducated_skill_cap = SKILL_LEVEL_TRAINED
+ category = /singleton/skill_category/occupational
+ subcategory = SKILL_SUBCATEGORY_SCIENCE
+ component_type = XENOBOTANY_SKILL_COMPONENT
+ required = TRUE
diff --git a/code/game/antagonist/outsider/actor.dm b/code/game/antagonist/outsider/actor.dm
index 36ff159815a..93df4220a7c 100644
--- a/code/game/antagonist/outsider/actor.dm
+++ b/code/game/antagonist/outsider/actor.dm
@@ -43,6 +43,10 @@ GLOBAL_DATUM(actors, /datum/antagonist/actor)
player.update_eyes()
player.regenerate_icons()
+ // Offship antags wipe skill components so that they can bypass skill restrictions entirely.
+ for(var/datum/component/skill/skill in player._datum_components)
+ qdel(skill)
+
return TRUE
/obj/effect/landmark/actor_spawn
diff --git a/code/game/antagonist/outsider/burglar.dm b/code/game/antagonist/outsider/burglar.dm
index 8a930a2c0a7..c70eadf9eba 100644
--- a/code/game/antagonist/outsider/burglar.dm
+++ b/code/game/antagonist/outsider/burglar.dm
@@ -52,6 +52,10 @@ GLOBAL_DATUM(burglars, /datum/antagonist/burglar)
player.update_eyes()
player.regenerate_icons()
+ // Offship antags wipe skill components so that they can bypass skill restrictions entirely.
+ for(var/datum/component/skill/skill in player._datum_components)
+ qdel(skill)
+
return TRUE
/datum/antagonist/burglar/get_antag_radio()
diff --git a/code/game/antagonist/outsider/jockey.dm b/code/game/antagonist/outsider/jockey.dm
index d1069faff1f..848eb21cca3 100644
--- a/code/game/antagonist/outsider/jockey.dm
+++ b/code/game/antagonist/outsider/jockey.dm
@@ -51,6 +51,10 @@ GLOBAL_DATUM(jockeys, /datum/antagonist/jockey)
player.update_eyes()
player.regenerate_icons()
+ // Offship antags wipe skill components so that they can bypass skill restrictions entirely.
+ for(var/datum/component/skill/skill in player._datum_components)
+ qdel(skill)
+
return TRUE
/datum/antagonist/jockey/get_antag_radio()
diff --git a/code/game/antagonist/outsider/loner.dm b/code/game/antagonist/outsider/loner.dm
index a5624520e4e..a49fa487137 100644
--- a/code/game/antagonist/outsider/loner.dm
+++ b/code/game/antagonist/outsider/loner.dm
@@ -54,4 +54,8 @@ GLOBAL_DATUM(loners, /datum/antagonist/loner)
player.update_eyes()
player.regenerate_icons()
+ // Offship antags wipe skill components so that they can bypass skill restrictions entirely.
+ for(var/datum/component/skill/skill in player._datum_components)
+ qdel(skill)
+
return TRUE
diff --git a/code/game/antagonist/outsider/mercenary.dm b/code/game/antagonist/outsider/mercenary.dm
index 8ef1d2a6e81..b1cffc1b04b 100644
--- a/code/game/antagonist/outsider/mercenary.dm
+++ b/code/game/antagonist/outsider/mercenary.dm
@@ -53,6 +53,11 @@ GLOBAL_DATUM(mercs, /datum/antagonist/mercenary)
player.regenerate_icons()
give_codewords(player)
+
+ // Offship antags wipe skill components so that they can bypass skill restrictions entirely.
+ for(var/datum/component/skill/skill in player._datum_components)
+ qdel(skill)
+
return TRUE
/datum/antagonist/mercenary/get_antag_radio()
diff --git a/code/game/antagonist/outsider/ninja.dm b/code/game/antagonist/outsider/ninja.dm
index a35d8bb87d3..b09fc740ee8 100644
--- a/code/game/antagonist/outsider/ninja.dm
+++ b/code/game/antagonist/outsider/ninja.dm
@@ -116,6 +116,11 @@ GLOBAL_DATUM(ninjas, /datum/antagonist/ninja)
player.force_update_limbs()
player.update_eyes()
player.regenerate_icons()
+
+ // Offship antags wipe skill components so that they can bypass skill restrictions entirely.
+ for(var/datum/component/skill/skill in player._datum_components)
+ qdel(skill)
+
return TRUE
/datum/antagonist/ninja/proc/generate_ninja_directive(side)
diff --git a/code/game/antagonist/outsider/raider.dm b/code/game/antagonist/outsider/raider.dm
index 701d1e3e872..f1aec952d09 100644
--- a/code/game/antagonist/outsider/raider.dm
+++ b/code/game/antagonist/outsider/raider.dm
@@ -138,6 +138,10 @@ GLOBAL_DATUM(raiders, /datum/antagonist/raider)
give_codewords(player)
+ // Offship antags wipe skill components so that they can bypass skill restrictions entirely.
+ for(var/datum/component/skill/skill in player._datum_components)
+ qdel(skill)
+
return 1
/datum/antagonist/raider/get_antag_radio()
diff --git a/code/game/antagonist/outsider/raider_techno.dm b/code/game/antagonist/outsider/raider_techno.dm
index 86e5afc1a9e..77a190cc6b5 100644
--- a/code/game/antagonist/outsider/raider_techno.dm
+++ b/code/game/antagonist/outsider/raider_techno.dm
@@ -59,6 +59,11 @@ GLOBAL_DATUM(raider_techno, /datum/antagonist/raider_techno)
player.force_update_limbs()
player.update_eyes()
player.regenerate_icons()
+
+ // Offship antags wipe skill components so that they can bypass skill restrictions entirely.
+ for(var/datum/component/skill/skill in player._datum_components)
+ qdel(skill)
+
return TRUE
/datum/antagonist/raider_techno/get_antag_radio()
diff --git a/code/game/antagonist/outsider/technomancer.dm b/code/game/antagonist/outsider/technomancer.dm
index d511888198e..0cd483004c7 100644
--- a/code/game/antagonist/outsider/technomancer.dm
+++ b/code/game/antagonist/outsider/technomancer.dm
@@ -40,6 +40,10 @@ GLOBAL_DATUM(technomancers, /datum/antagonist/technomancer)
technomancer_mob.preEquipOutfit(/obj/outfit/admin/techomancer, FALSE)
technomancer_mob.equipOutfit(/obj/outfit/admin/techomancer, FALSE)
+ // Offship antags wipe skill components so that they can bypass skill restrictions entirely.
+ for(var/datum/component/skill/skill in technomancer_mob._datum_components)
+ qdel(skill)
+
return TRUE
/datum/antagonist/technomancer/proc/equip_apprentice(var/mob/living/carbon/human/technomancer_mob)
diff --git a/code/game/antagonist/station/traitor.dm b/code/game/antagonist/station/traitor.dm
index 7bd61293b42..b391e79da43 100644
--- a/code/game/antagonist/station/traitor.dm
+++ b/code/game/antagonist/station/traitor.dm
@@ -113,6 +113,10 @@ GLOBAL_DATUM(traitors, /datum/antagonist/traitor)
//Begin code phrase.
give_codewords(traitor_mob)
+ // Offship antags wipe skill components so that they can bypass skill restrictions entirely.
+ for(var/datum/component/skill/skill in traitor_mob._datum_components)
+ qdel(skill)
+
/datum/antagonist/traitor/proc/spawn_uplink(var/mob/living/carbon/human/traitor_mob)
if(!istype(traitor_mob))
return
diff --git a/code/game/jobs/job/captain.dm b/code/game/jobs/job/captain.dm
index 3df4564e804..cfd4a63dc0b 100644
--- a/code/game/jobs/job/captain.dm
+++ b/code/game/jobs/job/captain.dm
@@ -26,6 +26,10 @@ GLOBAL_DATUM_INIT(captain_announcement, /datum/announcement/minor, new(do_newsca
blacklisted_species = list(SPECIES_TAJARA, SPECIES_TAJARA_MSAI, SPECIES_TAJARA_ZHAN, SPECIES_UNATHI, SPECIES_DIONA, SPECIES_IPC, SPECIES_IPC_G1, SPECIES_IPC_G2, SPECIES_IPC_XION, SPECIES_IPC_ZENGHU, SPECIES_IPC_BISHOP, SPECIES_IPC_SHELL, SPECIES_VAURCA_WORKER, SPECIES_VAURCA_WARRIOR, SPECIES_VAURCA_ATTENDANT, SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER, SPECIES_DIONA, SPECIES_DIONA_COEUS)
+ skill_requirements = alist(
+ /singleton/skill/pilot_spacecraft = SKILL_LEVEL_FAMILIAR
+ )
+
/obj/outfit/job/captain
name = "Captain"
jobtype = /datum/job/captain
@@ -114,6 +118,10 @@ GLOBAL_DATUM_INIT(captain_announcement, /datum/announcement/minor, new(do_newsca
blacklisted_species = list(SPECIES_TAJARA_ZHAN, SPECIES_VAURCA_WORKER, SPECIES_VAURCA_WARRIOR, SPECIES_VAURCA_ATTENDANT, SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/pilot_spacecraft = SKILL_LEVEL_FAMILIAR
+ )
+
/obj/outfit/job/xo
name = "Executive Officer"
jobtype = /datum/job/xo
@@ -172,6 +180,10 @@ GLOBAL_DATUM_INIT(captain_announcement, /datum/announcement/minor, new(do_newsca
blacklisted_species = list(SPECIES_TAJARA_ZHAN, SPECIES_VAURCA_WORKER, SPECIES_VAURCA_WARRIOR, SPECIES_VAURCA_ATTENDANT, SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/pilot_spacecraft = SKILL_LEVEL_FAMILIAR
+ )
+
/obj/outfit/job/bridge_crew
name = "Bridge Crew"
jobtype = /datum/job/bridge_crew
diff --git a/code/game/jobs/job/civilian.dm b/code/game/jobs/job/civilian.dm
index ef6edde12a0..5cd3f3d95d6 100644
--- a/code/game/jobs/job/civilian.dm
+++ b/code/game/jobs/job/civilian.dm
@@ -362,6 +362,11 @@
blacklisted_species = list(SPECIES_TAJARA_MSAI, SPECIES_VAURCA_WORKER, SPECIES_VAURCA_WARRIOR, SPECIES_VAURCA_ATTENDANT, SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/pilot_spacecraft = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/pilot_mechs = SKILL_LEVEL_FAMILIAR
+ )
+
/obj/outfit/job/operations_manager
name = "Operations Manager"
jobtype = /datum/job/operations_manager
@@ -414,6 +419,10 @@
blacklisted_species = list(SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/pilot_mechs = SKILL_LEVEL_FAMILIAR
+ )
+
/obj/outfit/job/hangar_tech
name = "Hangar Technician"
jobtype = /datum/job/hangar_tech
@@ -467,6 +476,10 @@
blacklisted_species = list(SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/pilot_spacecraft = SKILL_LEVEL_FAMILIAR
+ )
+
/obj/outfit/job/mining
name = "Shaft Miner"
jobtype = /datum/job/mining
@@ -527,6 +540,10 @@
blacklisted_species = list(SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/robotics = SKILL_LEVEL_TRAINED
+ )
+
/obj/outfit/job/machinist
name = "Machinist"
jobtype = /datum/job/machinist
diff --git a/code/game/jobs/job/engineering.dm b/code/game/jobs/job/engineering.dm
index e5b1043f922..bcd01bc1322 100644
--- a/code/game/jobs/job/engineering.dm
+++ b/code/game/jobs/job/engineering.dm
@@ -36,6 +36,13 @@
blacklisted_species = list(SPECIES_TAJARA_MSAI, SPECIES_VAURCA_WORKER, SPECIES_VAURCA_WARRIOR, SPECIES_VAURCA_ATTENDANT, SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/mechanical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/electrical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/atmospherics_systems = SKILL_LEVEL_TRAINED,
+ /singleton/skill/reactor_systems = SKILL_LEVEL_TRAINED
+ )
+
/obj/outfit/job/chief_engineer
name = "Chief Engineer"
jobtype = /datum/job/chief_engineer
@@ -104,6 +111,13 @@
blacklisted_species = list(SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/mechanical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/electrical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/atmospherics_systems = SKILL_LEVEL_TRAINED,
+ /singleton/skill/reactor_systems = SKILL_LEVEL_TRAINED
+ )
+
/obj/outfit/job/engineer
name = "Ship Engineer"
jobtype = /datum/job/engineer
@@ -178,6 +192,13 @@
outfit = /obj/outfit/job/atmos
blacklisted_species = list(SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/mechanical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/electrical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/atmospherics_systems = SKILL_LEVEL_TRAINED,
+ /singleton/skill/reactor_systems = SKILL_LEVEL_TRAINED
+ )
+
/obj/outfit/job/atmos
name = "Atmospheric Technician"
jobtype = /datum/job/atmos
@@ -251,6 +272,13 @@
SPECIES_SKRELL_AXIORI = 58
)
+ skill_requirements = alist(
+ /singleton/skill/mechanical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/electrical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/atmospherics_systems = SKILL_LEVEL_TRAINED,
+ /singleton/skill/reactor_systems = SKILL_LEVEL_TRAINED
+ )
+
/obj/outfit/job/intern_eng
name = "Engineering Apprentice"
jobtype = /datum/job/intern_eng
diff --git a/code/game/jobs/job/job.dm b/code/game/jobs/job/job.dm
index 37c56e36c54..5fa1c6b17a2 100644
--- a/code/game/jobs/job/job.dm
+++ b/code/game/jobs/job/job.dm
@@ -76,6 +76,11 @@
var/aide_job
var/bodyguard_job
+ /**
+ * Associated list of /singleton/skill/skill_name = skill_level that this job requires.
+ */
+ var/alist/skill_requirements = alist()
+
//Only override this proc
/datum/job/proc/pre_spawn(mob/abstract/new_player/player)
return
diff --git a/code/game/jobs/job/medical.dm b/code/game/jobs/job/medical.dm
index aae6d23be0b..4be042182e9 100644
--- a/code/game/jobs/job/medical.dm
+++ b/code/game/jobs/job/medical.dm
@@ -35,6 +35,10 @@
blacklisted_species = list(SPECIES_TAJARA_MSAI, SPECIES_TAJARA_ZHAN, SPECIES_VAURCA_WORKER, SPECIES_VAURCA_WARRIOR, SPECIES_VAURCA_ATTENDANT, SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/medicine = SKILL_LEVEL_TRAINED
+ )
+
/obj/outfit/job/cmo
name = "Chief Medical Officer"
jobtype = /datum/job/cmo
@@ -83,6 +87,11 @@
minimal_access = list(ACCESS_MEDICAL, ACCESS_MEDICAL_EQUIP, ACCESS_MORGUE, ACCESS_SURGERY, ACCESS_GENETICS, ACCESS_EVA)
outfit = /obj/outfit/job/doctor
blacklisted_species = list(SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/surgery = SKILL_LEVEL_TRAINED,
+ /singleton/skill/medicine = SKILL_LEVEL_PROFESSIONAL,
+ /singleton/skill/anatomy = SKILL_LEVEL_TRAINED
+ )
/datum/job/surgeon
title = "Surgeon"
@@ -107,6 +116,11 @@
minimal_access = list(ACCESS_MEDICAL, ACCESS_MEDICAL_EQUIP, ACCESS_MORGUE, ACCESS_SURGERY, ACCESS_GENETICS, ACCESS_EVA)
outfit = /obj/outfit/job/doctor/surgeon
blacklisted_species = list(SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/surgery = SKILL_LEVEL_PROFESSIONAL,
+ /singleton/skill/medicine = SKILL_LEVEL_TRAINED,
+ /singleton/skill/anatomy = SKILL_LEVEL_TRAINED
+ )
/obj/outfit/job/doctor
name = "Physician"
@@ -172,6 +186,11 @@
minimal_access = list(ACCESS_MEDICAL, ACCESS_MEDICAL_EQUIP, ACCESS_PHARMACY, ACCESS_VIROLOGY)
outfit = /obj/outfit/job/pharmacist
blacklisted_species = list(SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/pharmacology = SKILL_LEVEL_PROFESSIONAL,
+ /singleton/skill/medicine = SKILL_LEVEL_TRAINED,
+ /singleton/skill/anatomy = SKILL_LEVEL_TRAINED
+ )
/obj/outfit/job/pharmacist
name = "Pharmacist"
@@ -282,6 +301,11 @@
outfit = /obj/outfit/job/med_tech
blacklisted_species = list(SPECIES_DIONA, SPECIES_DIONA_COEUS, SPECIES_IPC_G2, SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/surgery = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/medicine = SKILL_LEVEL_TRAINED,
+ /singleton/skill/anatomy = SKILL_LEVEL_TRAINED
+ )
/obj/outfit/job/med_tech
name = "Paramedic"
@@ -358,6 +382,11 @@
)
outfit = /obj/outfit/job/intern_med
blacklisted_species = list(SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/surgery = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/medicine = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/anatomy = SKILL_LEVEL_FAMILIAR
+ )
/obj/outfit/job/intern_med
name = "Medical Intern"
diff --git a/code/game/jobs/job/science.dm b/code/game/jobs/job/science.dm
index 8e7cab4944e..c1ab36f9bc5 100644
--- a/code/game/jobs/job/science.dm
+++ b/code/game/jobs/job/science.dm
@@ -139,6 +139,10 @@
outfit = /obj/outfit/job/scientist/xenoarchaeologist
blacklisted_species = list(SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/pilot_spacecraft = SKILL_LEVEL_FAMILIAR
+ )
+
/obj/outfit/job/scientist/xenoarchaeologist
name = "Xenoarchaeologist"
jobtype = /datum/job/xenoarchaeologist
diff --git a/code/game/jobs/job/security.dm b/code/game/jobs/job/security.dm
index 26ba0878753..6d70adde3fb 100644
--- a/code/game/jobs/job/security.dm
+++ b/code/game/jobs/job/security.dm
@@ -35,6 +35,11 @@
blacklisted_species = list(SPECIES_TAJARA_ZHAN, SPECIES_DIONA, SPECIES_DIONA_COEUS, SPECIES_IPC_G1, SPECIES_IPC_G2, SPECIES_IPC_XION, SPECIES_VAURCA_WORKER, SPECIES_VAURCA_WARRIOR, SPECIES_VAURCA_ATTENDANT, SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER)
+ // Make sure security at bare minimum won't footgun themselves...
+ skill_requirements = alist(
+ /singleton/skill/firearms = SKILL_LEVEL_TRAINED
+ )
+
/obj/outfit/job/hos
name = "Head of Security"
jobtype = /datum/job/hos
@@ -101,6 +106,11 @@
blacklisted_species = list(SPECIES_IPC_XION_REMOTE, SPECIES_VAURCA_BULWARK, SPECIES_DIONA_COEUS, SPECIES_VAURCA_BREEDER)
+ // Make sure security at bare minimum won't footgun themselves...
+ skill_requirements = alist(
+ /singleton/skill/firearms = SKILL_LEVEL_TRAINED
+ )
+
/obj/outfit/job/warden
name = "Warden"
jobtype = /datum/job/warden
@@ -164,6 +174,11 @@
minimal_player_age = 3
outfit = /obj/outfit/job/forensics
blacklisted_species = list(SPECIES_IPC_XION_REMOTE, SPECIES_VAURCA_BULWARK, SPECIES_VAURCA_BREEDER)
+ skill_requirements = alist(
+ /singleton/skill/surgery = SKILL_LEVEL_TRAINED,
+ /singleton/skill/anatomy = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/forensics = SKILL_LEVEL_PROFESSIONAL
+ )
/obj/outfit/job/forensics
name = "Investigator"
@@ -224,6 +239,11 @@
blacklisted_species = list(SPECIES_IPC_XION_REMOTE, SPECIES_VAURCA_BULWARK, SPECIES_DIONA_COEUS, SPECIES_VAURCA_BREEDER)
+ // Make sure security at bare minimum won't footgun themselves...
+ skill_requirements = alist(
+ /singleton/skill/firearms = SKILL_LEVEL_TRAINED
+ )
+
/obj/outfit/job/officer
name = "Security Officer"
jobtype = /datum/job/officer
@@ -291,6 +311,11 @@
blacklisted_species = list(SPECIES_IPC_XION_REMOTE, SPECIES_VAURCA_BULWARK, SPECIES_DIONA_COEUS, SPECIES_VAURCA_BREEDER)
+ // Make sure security at bare minimum won't footgun themselves...
+ skill_requirements = alist(
+ /singleton/skill/firearms = SKILL_LEVEL_TRAINED
+ )
+
/obj/outfit/job/intern_sec
name = "Security Cadet"
jobtype = /datum/job/intern_sec
diff --git a/code/game/machinery/mecha_fabricator.dm b/code/game/machinery/mecha_fabricator.dm
index 00ca8259f2d..82db73e8e23 100644
--- a/code/game/machinery/mecha_fabricator.dm
+++ b/code/game/machinery/mecha_fabricator.dm
@@ -125,6 +125,13 @@
to_chat(user, SPAN_WARNING("Access denied."))
return
do_hair_pull(user)
+
+ // Yes I'm doing this after do_hair_pull() intentionally, you can totally fail the skill check and still get your hair stuck in a blender.
+ var/cancelled = FALSE
+ SEND_SIGNAL(user, COMSIG_USE_MECH_FAB, &cancelled)
+ if (cancelled)
+ return
+
ui_interact(user)
/obj/machinery/mecha_part_fabricator/ui_interact(mob/user, datum/tgui/ui)
diff --git a/code/game/objects/items/stacks/medical.dm b/code/game/objects/items/stacks/medical.dm
index 7ff987f9c49..612896d9a1e 100644
--- a/code/game/objects/items/stacks/medical.dm
+++ b/code/game/objects/items/stacks/medical.dm
@@ -150,12 +150,13 @@ Contains:
user.visible_message(SPAN_NOTICE("\The [user] starts treating [target_mob]'s [affecting.name]."), \
SPAN_NOTICE("You start treating [target_mob]'s [affecting.name]."))
var/used = 0
+ var/medicine_skill = user.GetComponent(MEDICINE_SKILL_COMPONENT)?.skill_level
for (var/datum/wound/W in affecting.wounds)
if(W.bandaged)
continue
if(used == amount)
break
- if(!do_mob(user, target_mob, W.damage/5))
+ if(!do_mob(user, target_mob, W.damage/(5 + (medicine_skill ? medicine_skill - SKILL_LEVEL_TRAINED : 0))))
to_chat(user, SPAN_NOTICE("You must stand still to bandage wounds."))
break
if (W.current_stage <= W.max_bleeding_stage)
@@ -226,7 +227,8 @@ Contains:
user.visible_message(SPAN_NOTICE("\The [user] starts salving wounds on [target_mob]'s [affecting.name]."), \
SPAN_NOTICE("You start salving the wounds on [target_mob]'s [affecting.name]."))
playsound(src, pick(apply_sounds), 25)
- if(!do_mob(user, target_mob, 10))
+ var/medicine_skill = user.GetComponent(MEDICINE_SKILL_COMPONENT)?.skill_level
+ if(!do_mob(user, target_mob, 10 + (medicine_skill ? 5 * (SKILL_LEVEL_TRAINED - medicine_skill) : 0)))
to_chat(user, SPAN_NOTICE("You must stand still to salve wounds."))
return 1
user.visible_message(SPAN_NOTICE("[user] salved wounds on [target_mob]'s [affecting.name]."), \
@@ -282,7 +284,8 @@ Contains:
continue
if(used == amount)
break
- if(!do_mob(user, target_mob, W.damage/5))
+ var/medicine_skill = user.GetComponent(MEDICINE_SKILL_COMPONENT)?.skill_level
+ if(!do_mob(user, target_mob, W.damage/(5 + (medicine_skill ? medicine_skill - SKILL_LEVEL_TRAINED : 0))))
to_chat(user, SPAN_NOTICE("You must stand still to bandage wounds."))
break
if (W.current_stage <= W.max_bleeding_stage)
@@ -350,7 +353,8 @@ Contains:
user.visible_message(SPAN_NOTICE("\The [user] starts salving wounds on [target_mob]'s [affecting.name]."), \
SPAN_NOTICE("You start salving the wounds on [target_mob]'s [affecting.name]."))
playsound(src, pick(apply_sounds), 25)
- if(!do_mob(user, target_mob, 10))
+ var/medicine_skill = user.GetComponent(MEDICINE_SKILL_COMPONENT)?.skill_level
+ if(!do_mob(user, target_mob, 10 + (medicine_skill ? 5 * (SKILL_LEVEL_TRAINED - medicine_skill) : 0)))
to_chat(user, SPAN_NOTICE("You must stand still to salve wounds."))
return 1
user.visible_message(SPAN_NOTICE("[user] covers wounds on [target_mob]'s [affecting.name] with regenerative membrane."), \
@@ -405,7 +409,8 @@ Contains:
to_chat(user, SPAN_DANGER("You can't apply a splint to the arm you're using!"))
return
user.visible_message(SPAN_DANGER("[user] starts to apply \the [src] to their [limb]."), SPAN_DANGER("You start to apply \the [src] to your [limb]."), SPAN_DANGER("You hear something being wrapped."))
- if(do_after(user, 5 SECONDS, target_mob))
+ var/medicine_skill = user.GetComponent(MEDICINE_SKILL_COMPONENT)?.skill_level
+ if(do_after(user, 5 SECONDS + 25 * (medicine_skill ? SKILL_LEVEL_TRAINED - medicine_skill : 0), target_mob))
if (target_mob != user)
user.visible_message(SPAN_DANGER("[user] finishes applying \the [src] to [target_mob]'s [limb]."), SPAN_DANGER("You finish applying \the [src] to [target_mob]'s [limb]."), SPAN_DANGER("You hear something being wrapped."))
else
diff --git a/code/game/objects/items/stacks/stack.dm b/code/game/objects/items/stacks/stack.dm
index e2403acf342..ddb24daf839 100644
--- a/code/game/objects/items/stacks/stack.dm
+++ b/code/game/objects/items/stacks/stack.dm
@@ -99,11 +99,43 @@
if(locate(/datum/stack_recipe) in recipes_sublist)
var/sublist_title = sublist ? " ([capitalize_first_letters(sublist.title)])" : ""
t1 += "
Recipes[sublist_title]
"
+
+ // Cache "skill visits" to avoid having to constantly recheck for components.
+ var/alist/checked_skills = alist()
+ var/visited_skill = null // Trinary boolean as null/0/nonzero
+
for(var/datum/stack_recipe/R in recipes_sublist)
+ var/lacks_skill = FALSE
+ // If you put anything other than a skill component in the required skills for a recipe, I will destroy you.
+ for(var/skill_comp, skill_level_requirement in R.required_skills_hard)
+ visited_skill = checked_skills[skill_comp]
+ // ** Trinary Check for Null, 0, or Nonzero. **
+ // Case for skill hasn't been checked yet.
+ if (isnull(visited_skill))
+ // null if no component at all, otherwise equal to the skill level (which can be any real number).
+ var/visited_comp = astype(user.GetComponent(skill_comp), SKILL_COMPONENT)?.skill_level
+
+ // Case for "Non-Player Characters", which will never have the component at all.
+ if (isnull(visited_comp))
+ checked_skills[skill_comp] = SKILL_LEVEL_PROFESSIONAL
+ continue // Not a player character, just assume they can craft everything.
+
+ // Player characters will always have the component if relevant.
+ checked_skills[skill_comp] = visited_comp
+ if (visited_comp < skill_level_requirement)
+ lacks_skill = TRUE
+ break
+ // Case for skill has been checked AND the character has the component.
+ else if (visited_skill < skill_level_requirement)
+ lacks_skill = TRUE
+ break
+
var/max_multiplier = round(src.get_amount() / R.req_amount)
var/title = ""
var/can_build = TRUE
can_build = (max_multiplier > 0)
+ if(lacks_skill)
+ can_build = FALSE
if(R.res_amount > 1)
title += "[R.res_amount]x [R.title]\s"
@@ -116,7 +148,7 @@
var/sublist_var = sublist ? "[REF(sublist)]" : ""
t1 += "[title]"
else
- t1 += "[title]
"
+ t1 += "[lacks_skill ? "Missing Skill — " : ""][title]
[lacks_skill ? "" : ""]
"
continue
if(R.max_res_amount > 1 && max_multiplier > 1)
@@ -157,14 +189,22 @@
if (recipe.on_floor && !isfloor(user.loc))
to_chat(user, SPAN_WARNING("\The [recipe.title] must be constructed on the floor!"))
return
+ var/skill_diff = 0
+ for (var/skill_type, required_level in recipe.required_skills_soft)
+ skill_diff += required_level - astype(user.GetComponent(skill_type), SKILL_COMPONENT)?.skill_level
to_chat(user, SPAN_NOTICE("Building [recipe.title]..."))
- if (recipe.time)
- if (!do_after(user, recipe.time, do_flags = DO_REPAIR_CONSTRUCT))
+ var/doafter_time = recipe.time
+ if (doafter_time)
+
+ // Approximately no crafting time if you beat the skill req by 4, approximately twice as long to craft if you're under by 4.
+ // No need to do an expensive min because it is mathematically impossible for this to ever be negative.
+ doafter_time += doafter_time * ftanh(0.5 * skill_diff)
+ if (!do_after(user, doafter_time, do_flags = DO_REPAIR_CONSTRUCT))
return
if (use(required))
- recipe.Produce(produced, user.loc, user.dir, user)
+ recipe.Produce(produced, user.loc, user.dir, user, skill_diff)
/obj/item/stack/Topic(href, href_list)
..()
@@ -365,8 +405,21 @@
var/one_per_turf = 0
var/on_floor = 0
var/use_material
+ /**
+ * Assoc list of required skills to required skill levels. Requirements are taken as a "soft" requirement used to generate a "skill difference", which is applied as a modifier to a d20 roll.
+ * This roll currently just affects the name of the resulting item and is for fun, but should be later expanded to have more unique effects.
+ * THIS MAY ONLY HAVE /datum/component/skill/skill_name in it. I WILL DESTROY YOU IF YOU PUT ANYTHING ELSE IN HERE.
+ */
+ var/alist/required_skills_soft
-/datum/stack_recipe/New(title, result_type, req_amount = 1, res_amount = 1, max_res_amount = 1, time = 0, one_per_turf = 0, on_floor = 0, supplied_material = null)
+ /**
+ * Assoc list of required skills to required skill levels. This is the MINIMUM to craft the item.
+ * You must have ALL these skills at that level to see the item as craftable.
+ * THIS MAY ONLY HAVE /datum/component/skill/skill_name in it. I WILL DESTROY YOU IF YOU PUT ANYTHING ELSE IN HERE.
+ */
+ var/alist/required_skills_hard
+
+/datum/stack_recipe/New(title, result_type, req_amount = 1, res_amount = 1, max_res_amount = 1, time = 0, one_per_turf = 0, on_floor = 0, supplied_material = null, alist/required_skills_soft, alist/required_skills_hard)
src.title = title
src.result_type = result_type
if(ispath(result_type, /obj/structure))
@@ -380,8 +433,10 @@
src.one_per_turf = one_per_turf
src.on_floor = on_floor
src.use_material = supplied_material
+ src.required_skills_soft = required_skills_soft
+ src.required_skills_hard = required_skills_hard
-/datum/stack_recipe/proc/Produce(var/amount = 1, var/loc = null, var/dir = NORTH, var/user = null)
+/datum/stack_recipe/proc/Produce(var/amount = 1, var/loc = null, var/dir = NORTH, var/user = null, var/skill_diff = 0)
if(amount < 1)
return null
diff --git a/code/modules/background/education/_education.dm b/code/modules/background/education/_education.dm
new file mode 100644
index 00000000000..302684ed0fa
--- /dev/null
+++ b/code/modules/background/education/_education.dm
@@ -0,0 +1,18 @@
+/singleton/education
+ /// The name of this education type.
+ var/name
+ /// The description of this education type. It should ideally match what's on the Aurora wiki, but from an IC point of view.
+ var/description
+ /// Age requirement for this education. Should match the job this is intended for. This doesn't need to be here per se, but it helps to filter results.
+ var/list/minimum_character_age = list()
+ /// The jobs this education type allows you to access.
+ var/list/jobs = list()
+ /// The given skills for this education type. Linked list of skill type to level.
+ var/list/skills = list()
+ /// Species that CANNOT take this education, if necessary. This is a list of names, must match what's in the species pref variable.
+ /// Empty list means no restrictions.
+ var/list/species_restriction = list()
+
+/singleton/education/high_school
+ name = "High School Diploma"
+ description = "Your character has a high school diploma."
diff --git a/code/modules/background/education/engineering.dm b/code/modules/background/education/engineering.dm
new file mode 100644
index 00000000000..8774e669ec6
--- /dev/null
+++ b/code/modules/background/education/engineering.dm
@@ -0,0 +1,86 @@
+/singleton/education/mechanical_engineering
+ name = "Mechanical Engineering"
+ description = "You are at least 25 years of age, with either a Bachelor's degree in Mechanical Engineering, or otherwise have functionally equivalent working experience. \
+ You specialize in constructing structural systems, lathing, and the more manual pleasures of engineering, such as welding and wrenching."
+ jobs = list("Engineer")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 25,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/mechanical_engineering = SKILL_LEVEL_PROFESSIONAL,
+ /singleton/skill/electrical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/atmospherics_systems = SKILL_LEVEL_TRAINED,
+ /singleton/skill/reactor_systems = SKILL_LEVEL_TRAINED
+ )
+
+/singleton/education/electrical_engineering
+ name = "Electrical Engineering Background"
+ description = "You are at least 25 years of age, with a Bachelor's degree in Electrical Engineering, or otherwise have functionally equivalent working experience. \
+ You specialize in variable-voltage cabling, grid management, electronic hardware, and other electrical systems."
+ jobs = list("Engineer")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 25,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/mechanical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/electrical_engineering = SKILL_LEVEL_PROFESSIONAL,
+ /singleton/skill/atmospherics_systems = SKILL_LEVEL_TRAINED,
+ /singleton/skill/reactor_systems = SKILL_LEVEL_TRAINED
+ )
+
+/singleton/education/atmospherics_engineer
+ name = "Atmospherics Systems Background"
+ description = "You are at least 25 years of age, with a Bachelor's degree in Atmospherics Systems, or otherwise have functionally equivalent working experience. \
+ You specialize in everything to do with atmospherics systems, whether that's the delivery of gases, usage of atmospherics machines, or simply how to use a pipe wrench."
+ jobs = list("Atmospherics Technician")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 25,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/mechanical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/electrical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/atmospherics_systems = SKILL_LEVEL_PROFESSIONAL,
+ /singleton/skill/reactor_systems = SKILL_LEVEL_TRAINED
+ )
+
+
+/singleton/education/reactors_engineer
+ name = "Reactor Systems Background"
+ description = "You are at least 25 years of age, with a Bachelor's degree in Reactor Systems, or otherwise have functionally equivalent working experience. \
+ You specialize in everything to do with a reactor's systems, whether you are looking at a Supermatter crystal, a fusion reactor, or a combustion chamber."
+ jobs = list("Engineer")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 25,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/mechanical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/electrical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/atmospherics_systems = SKILL_LEVEL_TRAINED,
+ /singleton/skill/reactor_systems = SKILL_LEVEL_PROFESSIONAL
+ )
+
+/singleton/education/experienced_engineer
+ name = "Engineering Certification"
+ description = "You are at least 25 years of age. You may not have an Engineering degree or a specialized background, but you had enough fundamental experience for the Conglomerate to validate it instead \
+ of a degree. You do not have the same specialization as your fellow Engineers with a degree, making up for it by being a jack of all trades. \
+ You could probably fix a car, whereas they might not be able to."
+ jobs = list("Engineer")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 25,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/mechanical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/electrical_engineering = SKILL_LEVEL_TRAINED,
+ /singleton/skill/atmospherics_systems = SKILL_LEVEL_TRAINED,
+ /singleton/skill/reactor_systems = SKILL_LEVEL_TRAINED
+ )
diff --git a/code/modules/background/education/medical.dm b/code/modules/background/education/medical.dm
new file mode 100644
index 00000000000..35b7f1ec4aa
--- /dev/null
+++ b/code/modules/background/education/medical.dm
@@ -0,0 +1,102 @@
+/singleton/education/surgical_degree
+ name = "MD, Surgery Track"
+ description = "You are 30 years of age or older, with an applicable MD from accredited school and you have completed 2 years of residency at an \
+ accredited hospital or clinic."
+ jobs = list("Surgeon")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 30,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/surgery = SKILL_LEVEL_PROFESSIONAL,
+ /singleton/skill/medicine = SKILL_LEVEL_TRAINED,
+ /singleton/skill/anatomy = SKILL_LEVEL_TRAINED
+ )
+
+/singleton/education/physician_degree
+ name = "MD, Physician Track"
+ description = "You are at least 30 years of age, with an applicable MD from an accredited school and you have completed 2 years of residency at an \
+ accredited hospital or clinic."
+ jobs = list("Physician")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 30,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/surgery = SKILL_LEVEL_TRAINED,
+ /singleton/skill/medicine = SKILL_LEVEL_PROFESSIONAL,
+ /singleton/skill/anatomy = SKILL_LEVEL_TRAINED
+ )
+
+/singleton/education/pharmacist_degree
+ name = "Doctor of Pharmacy"
+ description = "You are at least 25 years of age, with an applicable Masters from an accredited school, along with 2 years of residency at an \
+ accredited hospital or clinic."
+ jobs = list("Pharmacist")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 30,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/pharmacology = SKILL_LEVEL_PROFESSIONAL,
+ /singleton/skill/medicine = SKILL_LEVEL_TRAINED,
+ /singleton/skill/anatomy = SKILL_LEVEL_TRAINED
+ )
+
+/singleton/education/psychologist_degree
+ name = "Doctor of Psychology"
+ description = "You are at least 30 years of age, with at least a doctorate from an accredited university in an applicable field. " \
+ + "This is more of a research degree that has medical applications, as opposed to a true medical degree. " \
+ + "As such, it is only tangentially involved with actual medicine. A character with only this education is not legally considered a licensed doctor." \
+ + "You are however qualified to perform psychological evaluations on behalf of the SCC, as well as perform psychotherapy."
+ jobs = list("Psychologist")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 30,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /* Uncomment this block after finishing the Leadership skill. Psychologists should be able to give people morale bonuses as a mechanic.
+ /singleton/skill/leadership = SKILL_LEVEL_TRAINED,
+ */
+ /singleton/skill/pharmacology = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/medicine = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/anatomy = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/research = SKILL_LEVEL_TRAINED
+ )
+
+/singleton/education/psychiatrist_degree
+ name = "MD, Psychiatry Track"
+ description = "You are at least 30 years of age, with an applicable MD from an accredited school and you have completed 2 years of residency at an \
+ accredited hospital or clinic. Unlike Psychology, this is an actual medical degree, and a character with this education is considered a licensed doctor."
+ jobs = list("Psychiatrist")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 30,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/surgery = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/medicine = SKILL_LEVEL_TRAINED,
+ /singleton/skill/anatomy = SKILL_LEVEL_TRAINED,
+ /singleton/skill/pharmacology = SKILL_LEVEL_TRAINED
+ )
+
+/singleton/education/paramedic
+ name = "Paramedic Certification"
+ description = "You are at least 18 years of age, with a Paramedic certification."
+ jobs = list("Paramedic")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 18,
+ SPECIES_SKRELL = 55,
+ SPECIES_SKRELL_AXIORI = 55
+ )
+ skills = list(
+ /* Can perform only the most basic surgeries up to arterial bleeds. */
+ /singleton/skill/surgery = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/medicine = SKILL_LEVEL_TRAINED,
+ /singleton/skill/anatomy = SKILL_LEVEL_TRAINED
+ )
diff --git a/code/modules/background/education/misc.dm b/code/modules/background/education/misc.dm
new file mode 100644
index 00000000000..e96252a051d
--- /dev/null
+++ b/code/modules/background/education/misc.dm
@@ -0,0 +1,19 @@
+/singleton/education/finance
+ name = "Finance Background"
+ description = "You are at least 21 years of age. You possibly graduated in a field related to finance, whether that is Business Management or something else. Otherwise, you likely found your knack in the ordinary course of life such that you have comparable expertise.\
+ You can count very well, and you have a statistician's eye - especially for credits."
+ minimum_character_age = list(
+ SPECIES_HUMAN = 21,
+ SPECIES_SKRELL = 55,
+ SPECIES_SKRELL_AXIORI = 55
+ )
+
+/singleton/education/arts
+ name = "Humanities Background"
+ description = "You are at least 21 years of age. You possibly graduated in a field related to the humanities, whether that is music, arts, linguistics, or a bachelor's in \
+ psychology. Otherwise, your life has provided ample opportunity for these pursuits such that you have comparable expertise. You are likely very well-read on foreign cultures and on the human mind."
+ minimum_character_age = list(
+ SPECIES_HUMAN = 21,
+ SPECIES_SKRELL = 55,
+ SPECIES_SKRELL_AXIORI = 55
+ )
diff --git a/code/modules/background/education/science.dm b/code/modules/background/education/science.dm
new file mode 100644
index 00000000000..aa6582040e6
--- /dev/null
+++ b/code/modules/background/education/science.dm
@@ -0,0 +1,79 @@
+/singleton/education/research_and_development
+ name = "Research & Development Degree"
+ description = "You are at least 30 years of age, with a PhD in an applicable field for Research and Development. This may range from a Firearms Engineering degree \
+ to a Bluespace Engineering degree or even Aerospace Engineering. Space is the limit for your research."
+ jobs = list("Scientist")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 30,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/research = SKILL_LEVEL_PROFESSIONAL
+ )
+
+/singleton/education/robotics_masters
+ name = "Robotics Master's"
+ description = "You are at least 25 years of age, with a Master's in Robotics. Your specialization is in building and repairing IPCs and other smaller robots, though \
+ you are also capable of building exoskeletons and mechs. You're also proficient with some fundamental engineering skills, though you prefer the \
+ theoretical aspect and robots in general."
+ jobs = list("Roboticist")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 25,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/research = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/robotics = SKILL_LEVEL_PROFESSIONAL,
+ /singleton/skill/electrical_engineering = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/mechanical_engineering = SKILL_LEVEL_FAMILIAR,
+ )
+
+/singleton/education/mechatronics_masters
+ name = "Mechatronics Master's"
+ description = "You are at least 25 years of age, with a Master's in Mechatronics. Your specialization is with building large human-sized exoskeletons and mechs, though \
+ you've also learnt how to repair IPCs and simpler robots as well. You're more proficient with the mechanical aspects of engineering as well."
+ jobs = list("Roboticist")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 25,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/research = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/robotics = SKILL_LEVEL_TRAINED,
+ /singleton/skill/electrical_engineering = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/mechanical_engineering = SKILL_LEVEL_PROFESSIONAL
+ )
+
+/singleton/education/xenobotany_degree
+ name = "Xenobotany Degree"
+ description = "You are at least 30 years of age, with a PhD in Xenobotany. Your specialization is with discovering, sequencing, and creating alien flora... though \
+ you can also grow some potatoes in your spare time."
+ jobs = list("Xenobotanist")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 30,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/research = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/gardening = SKILL_LEVEL_PROFESSIONAL,
+ /singleton/skill/xenobotany = SKILL_LEVEL_PROFESSIONAL
+ )
+
+
+/singleton/education/xenobiology_degree
+ name = "Xenobiology Degree"
+ description = "You are at least 30 years of age, with a PhD in Xenobiology. Your specialization is with discovering and cataloguing alien animals."
+ jobs = list("Xenobiologist")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 30,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/research = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/xenobiology = SKILL_LEVEL_PROFESSIONAL
+ )
diff --git a/code/modules/background/education/security.dm b/code/modules/background/education/security.dm
new file mode 100644
index 00000000000..8e57c51e0b2
--- /dev/null
+++ b/code/modules/background/education/security.dm
@@ -0,0 +1,67 @@
+/singleton/education/forensics_degree
+ name = "Forensics Science Degree"
+ description = "You are 25 years of age or older, with a degree in Forensics Science. You specialize in the medical procedures required to understand why someone died. " \
+ + "While not necessarily a medical degree, there's not much difference in suturing a body whether or not its approaching room temperature."
+ jobs = list("Investigator")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 25,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/surgery = SKILL_LEVEL_TRAINED,
+ /singleton/skill/anatomy = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/forensics = SKILL_LEVEL_PROFESSIONAL,
+ /singleton/skill/firearms = SKILL_LEVEL_TRAINED
+ )
+
+/singleton/education/military_basic
+ name = "Military Training"
+ description = "You have finished at least one full contract of military service. Alternatively, this could be equivalent experience from mercenary work (legal or otherwise), \
+ or or simply surviving for long in a grim environment where self-discipline matters as much as skill."
+ minimum_character_age = list(
+ SPECIES_HUMAN = 18,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/unarmed_combat = SKILL_LEVEL_TRAINED,
+ /singleton/skill/armed_combat = SKILL_LEVEL_TRAINED,
+ /singleton/skill/firearms = SKILL_LEVEL_TRAINED
+ )
+
+/singleton/education/military_corpsman
+ name = "Corpsman Training"
+ description = "After completing basic military training (or having lived a life that taught you comparable lessons), you received advanced individual training in battlefield medicine. " \
+ + "Your combat skills are not as sharp as others, but you made up for it by knowing how to keep your comrades in arms from bleeding out on the battlefield. " \
+ + "A character with this training is NOT legally considered a medical doctor. You're on the hook for manslaughter if you attempt and fail to save them yourself instead of taking them to a real doctor."
+ minimum_character_age = list(
+ SPECIES_HUMAN = 18,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /* Significantly worse combat skills than other security educations, though not so bad they'll footgun themselves.
+ Alternatively, this is a plausible though less specialized alternative for paramedic training, as is common in real life. */
+ /singleton/skill/armed_combat = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/firearms = SKILL_LEVEL_TRAINED,
+ /singleton/skill/surgery = SKILL_LEVEL_FAMILIAR, /* Only enough to repair an artery. */
+ /singleton/skill/medicine = SKILL_LEVEL_FAMILIAR
+ )
+
+/singleton/education/police_academy
+ name = "Police Academy Graduate"
+ description = "You are a police academy graduate, or else you have certified through (or survived) a local institution of comparable sort. \
+ Your combat skills are not as stringent as actual military service, though this is made up for with more generalized training suitable for a first-responder."
+ minimum_character_age = list(
+ SPECIES_HUMAN = 18,
+ SPECIES_SKRELL = 60,
+ SPECIES_SKRELL_AXIORI = 60
+ )
+ skills = list(
+ /singleton/skill/unarmed_combat = SKILL_LEVEL_FAMILIAR,
+ /singleton/skill/firearms = SKILL_LEVEL_TRAINED,
+ /singleton/skill/forensics = SKILL_LEVEL_FAMILIAR, /* Very basic crime investigation skills. */
+ /singleton/skill/medicine = SKILL_LEVEL_FAMILIAR /* Police are also trained in basic first aid. */
+ )
+
diff --git a/code/modules/background/education/service.dm b/code/modules/background/education/service.dm
new file mode 100644
index 00000000000..c05c64f093e
--- /dev/null
+++ b/code/modules/background/education/service.dm
@@ -0,0 +1,68 @@
+/singleton/education/mixing
+ name = "Mixologist"
+ description = "You know everyone's favorite and how to make it. Whether you successfully passed the test for an Idris mixing license or have tended enough bars to have seen it all, \
+ you are a specialist in mixing cocktails, mocktails, and whatever else. Time to mix drinks and save lives."
+ jobs = list("Bartender")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 18,
+ SPECIES_SKRELL = 50,
+ SPECIES_SKRELL_AXIORI = 50
+ )
+ skills = list(
+ /singleton/skill/bartending = SKILL_LEVEL_PROFESSIONAL,
+ )
+
+/singleton/education/cooking_degree
+ name = "Culinary Artist"
+ description = "You possibly obtained a degree in Culinary Arts or else you've survived enough kitchens to compete with the best. Pancakes, steaks, and cultural food - you've learnt about how to cook it all."
+ jobs = list("Chef")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 18,
+ SPECIES_SKRELL = 50,
+ SPECIES_SKRELL_AXIORI = 50
+ )
+ skills = list(
+ /singleton/skill/cooking = SKILL_LEVEL_PROFESSIONAL,
+ )
+
+/singleton/education/cooking_certification
+ name = "Culinary Background"
+ description = "You either obtained an Idris certification to work as a cook, or you've been on dinner duty for a long enough time to get pretty good at it. \
+ You won't be as good as a professional chef, but you can pour your soul out into a good breakfast."
+ jobs = list("Chef")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 18,
+ SPECIES_SKRELL = 50,
+ SPECIES_SKRELL_AXIORI = 50
+ )
+ skills = list(
+ /singleton/skill/cooking = SKILL_LEVEL_TRAINED,
+ )
+
+/singleton/education/hydroponics_degree
+ name = "Hydroponics Specialization"
+ description = "You either obtained a degree to work as a hydroponicist or gardener or you have enough of a gift and the experience that you could do just as well. \
+ Your background covered both manual and hydroponics gardening of just about every plant known to your species, alongside plants that are more typical to other cultures in the Spur."
+ jobs = list("Gardener")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 18,
+ SPECIES_SKRELL = 50,
+ SPECIES_SKRELL_AXIORI = 50
+ )
+ skills = list(
+ /singleton/skill/gardening = SKILL_LEVEL_PROFESSIONAL,
+ )
+
+/singleton/education/hydroponics_certification
+ name = "Hydroponics Background"
+ description = "You either obtained an Idris certification to work as a hydroponicist or gardener, or you have worked with enough plant life to hit the same mark. \
+ Although you might not be as much of an expert as someone with a Hydroponics degree, you can still plant just about everything if you give it your all."
+ jobs = list("Gardener")
+ minimum_character_age = list(
+ SPECIES_HUMAN = 18,
+ SPECIES_SKRELL = 50,
+ SPECIES_SKRELL_AXIORI = 50
+ )
+ skills = list(
+ /singleton/skill/gardening = SKILL_LEVEL_TRAINED,
+ )
diff --git a/code/modules/background/origins/_origins.dm b/code/modules/background/origins/_origins.dm
index 9962b019879..ceb0319d87a 100644
--- a/code/modules/background/origins/_origins.dm
+++ b/code/modules/background/origins/_origins.dm
@@ -1,11 +1,15 @@
/singleton/origin_item
var/name = "generic origin item"
var/desc = DESC_PARENT
- var/important_information //Big red text. Should only be used if not following it would incur a bwoink.
+ /// Big red text. Should only be used if not following it would incur a bwoink.
+ var/important_information
+ /// A list of the origin traits given by this culture item.
var/list/origin_traits = list()
/// Format for the following list: "Characters from this origin: [list entry], [list entry]."
/// One list item per trait.
var/list/origin_traits_descriptions = list()
+ /// A list of skills given by this origin. Assoc list of skill singleton type to level.
+ var/list/given_skills = list()
/singleton/origin_item/culture
name = "generic culture"
diff --git a/code/modules/client/preference_setup/occupation/occupation.dm b/code/modules/client/preference_setup/occupation/occupation.dm
index f9319acbe93..34df26727ad 100644
--- a/code/modules/client/preference_setup/occupation/occupation.dm
+++ b/code/modules/client/preference_setup/occupation/occupation.dm
@@ -219,6 +219,26 @@
if(C.name in job.blacklisted_citizenship)
dat += "[dispRank] \[BACKGROUND RESTRICTED] | "
continue
+
+ // Skill requirement handling.
+ var/skills_length = length(job.skill_requirements)
+ var/skill_index = 0
+ var/missing_skills = ""
+ for (var/key,value in job.skill_requirements)
+ skill_index++
+ if (!key || pref.skills[key] >= value)
+ continue
+
+ var/singleton/skill/skill = GET_SINGLETON(key)
+ missing_skills += "[skill.name] [value]"
+ if (skill_index != skills_length)
+ missing_skills += "\n"
+
+ if (missing_skills != "")
+ dat += "[dispRank] MISSING SKILLS: [missing_skills] | "
+ continue
+ // End of skill requirement handling.
+
if(job.alt_titles && (LAZYLEN(pref.GetValidTitles(job)) > 1))
dispRank = " \[[pref.GetPlayerAltTitle(job)]\]"
if((pref.job_civilian_low & ASSISTANT) && (rank != "Assistant"))
diff --git a/code/modules/client/preference_setup/origin/origin.dm b/code/modules/client/preference_setup/origin/origin.dm
index 2e31ccc9fe5..292fe98f2cf 100644
--- a/code/modules/client/preference_setup/origin/origin.dm
+++ b/code/modules/client/preference_setup/origin/origin.dm
@@ -70,27 +70,32 @@
var/datum/species/S = GLOB.all_species[pref.species]
if(!istext(pref.culture) || !ispath(text2path(pref.culture), /singleton/origin_item/culture))
var/singleton/origin_item/culture/CI = S.possible_cultures[1]
- pref.culture = "[CI]"
+ pref.culture = "[CI.type]"
+
var/singleton/origin_item/culture/our_culture = GET_SINGLETON(text2path(pref.culture))
if(!istext(pref.origin) || !ispath(text2path(pref.origin), /singleton/origin_item/origin))
var/singleton/origin_item/origin/OI = pick(our_culture.possible_origins)
- pref.origin = "[OI]"
+ pref.origin = "[OI.type]"
else
var/singleton/origin_item/origin/origin_check = text2path(pref.origin)
if(!(origin_check in our_culture.possible_origins))
to_client_chat(SPAN_WARNING("Your origin has been reset due to it being incompatible with your culture!"))
var/singleton/origin_item/origin/OI = pick(our_culture.possible_origins)
- pref.origin = "[OI]"
+ pref.origin = "[OI.type]"
+
var/singleton/origin_item/origin/our_origin = GET_SINGLETON(text2path(pref.origin))
if(!(pref.citizenship in our_origin.possible_citizenships))
to_client_chat(SPAN_WARNING("Your previous citizenship is invalid for this origin! Resetting."))
pref.citizenship = our_origin.possible_citizenships[1]
+
if(!(pref.religion in our_origin.possible_religions))
to_client_chat(SPAN_WARNING("Your previous religion is invalid for this origin! Resetting."))
pref.religion = our_origin.possible_religions[1]
+
if(!(pref.accent in our_origin.possible_accents))
to_client_chat(SPAN_WARNING("Your previous accent is invalid for this origin! Resetting."))
pref.accent = our_origin.possible_accents[1]
+
pref.economic_status = sanitize_inlist(pref.economic_status, ECONOMIC_POSITIONS, initial(pref.economic_status))
/datum/category_item/player_setup_item/origin/content(var/mob/user)
@@ -114,10 +119,11 @@
if(OR.important_information)
dat += "
- [OR.important_information]"
dat += "
"
- dat += "Economic Status: [pref.economic_status]
"
- dat += "Citizenship: [pref.citizenship]
"
- dat += "Religion: [pref.religion]
"
- dat += "Accent: [pref.accent]
"
+
+ dat += "Economic Status: [pref.economic_status]
"
+ dat += "Citizenship: [pref.citizenship]
"
+ dat += "Religion: [pref.religion]
"
+ dat += "Accent: [pref.accent]
"
. = dat.Join()
/datum/category_item/player_setup_item/origin/OnTopic(href, href_list, user)
@@ -131,7 +137,7 @@
var/result = tgui_input_list(user, "Choose your character's culture.", "Culture", options)
var/singleton/origin_item/culture/chosen_culture = options[result]
if(chosen_culture)
- show_window(chosen_culture, "set_culture_data", user)
+ show_origin_window(chosen_culture, "set_culture_data", user)
return TOPIC_HANDLED
if(href_list["open_origin_menu"])
@@ -144,7 +150,7 @@
var/result = tgui_input_list(user, "Choose your character's origin.", "Origins", options)
var/singleton/origin_item/origin/chosen_origin = options[result]
if(chosen_origin)
- show_window(chosen_origin, "set_origin_data", user)
+ show_origin_window(chosen_origin, "set_origin_data", user)
return TOPIC_HANDLED
if(href_list["set_culture_data"])
@@ -207,7 +213,7 @@
sanitize_character()
return TOPIC_REFRESH
-/datum/category_item/player_setup_item/origin/proc/show_window(var/singleton/origin_item/OI, var/topic_data, var/mob/user)
+/datum/category_item/player_setup_item/origin/proc/show_origin_window(var/singleton/origin_item/OI, var/topic_data, var/mob/user)
var/datum/browser/origin_win = new(user, topic_data, "Origins Selection")
var/dat = "[OI.name]"
dat += "
[OI.desc]
"
diff --git a/code/modules/client/preference_setup/preference_setup.dm b/code/modules/client/preference_setup/preference_setup.dm
index b01bfc0fdb6..46f4ef5d959 100644
--- a/code/modules/client/preference_setup/preference_setup.dm
+++ b/code/modules/client/preference_setup/preference_setup.dm
@@ -28,30 +28,35 @@
sort_order = 2
category_item_type = /datum/category_item/player_setup_item/origin
+/datum/category_group/player_setup_category/skill_preferences
+ name = "Skills"
+ sort_order = 3
+ category_item_type = /datum/category_item/player_setup_item/skills
+
/datum/category_group/player_setup_category/occupation_preferences
name = "Occupation"
- sort_order = 3
+ sort_order = 4
category_item_type = /datum/category_item/player_setup_item/occupation
/datum/category_group/player_setup_category/appearance_preferences
name = "Roles"
- sort_order = 4
+ sort_order = 5
category_item_type = /datum/category_item/player_setup_item/antagonism
/datum/category_group/player_setup_category/loadout_preferences
name = "Loadout"
- sort_order = 5
+ sort_order = 6
category_item_type = /datum/category_item/player_setup_item/loadout
/datum/category_group/player_setup_category/global_preferences
name = "Global"
- sort_order = 6
+ sort_order = 7
category_item_type = /datum/category_item/player_setup_item/player_global
sql_role = SQL_PREFERENCES
/datum/category_group/player_setup_category/other_preferences
name = "Other"
- sort_order = 7
+ sort_order = 8
category_item_type = /datum/category_item/player_setup_item/other
/****************************
diff --git a/code/modules/client/preference_setup/skills/skills.dm b/code/modules/client/preference_setup/skills/skills.dm
new file mode 100644
index 00000000000..4da48caba86
--- /dev/null
+++ b/code/modules/client/preference_setup/skills/skills.dm
@@ -0,0 +1,336 @@
+/datum/category_item/player_setup_item/skills
+ name = "Skills"
+ sort_order = 1
+
+/datum/category_item/player_setup_item/skills/load_character(var/savefile/S)
+ S["skills"] >> pref.skills
+ S["education"] >> pref.education
+
+/datum/category_item/player_setup_item/skills/save_character(var/savefile/S)
+ S["skills"] << pref.skills
+ S["education"] << pref.education
+
+/datum/category_item/player_setup_item/skills/gather_load_query()
+ return list(
+ "ss13_characters" = list(
+ "vars" = list(
+ "education",
+ "skills"
+ ),
+ "args" = list("id")
+ )
+ )
+
+/datum/category_item/player_setup_item/skills/gather_load_parameters()
+ return list("id" = pref.current_character)
+
+/datum/category_item/player_setup_item/skills/gather_save_query()
+ return list(
+ "ss13_characters" = list(
+ "education",
+ "skills",
+ "id" = 1,
+ "ckey" = 1
+ )
+ )
+
+/datum/category_item/player_setup_item/skills/gather_save_parameters()
+ var/list/sanitized_skills = list()
+ for(var/S in pref.skills)
+ var/singleton/skill/skill = GET_SINGLETON(S)
+ if(!istype(skill))
+ continue
+ var/skill_val = pref.skills[S]
+ if(skill_val <= SKILL_LEVEL_UNFAMILIAR)
+ continue
+ sanitized_skills["[skill.type]"] = skill_val
+
+ return list(
+ "education" = pref.education,
+ "skills" = json_encode(sanitized_skills),
+ "id" = pref.current_character,
+ "ckey" = PREF_CLIENT_CKEY
+ )
+
+/datum/category_item/player_setup_item/skills/load_character_special(savefile/S)
+ if(!pref.skills)
+ pref.skills = "{}"
+
+ var/before = pref.skills
+ var/loaded_skills
+ try
+ if(istext(pref.skills))
+ loaded_skills = json_decode(pref.skills)
+ else
+ loaded_skills = pref.skills
+ catch (var/exception/e)
+ log_debug("SKILLS: Caught [e]. Initial value: [before]")
+ loaded_skills = list()
+
+ pref.skills = list()
+ for(var/key in SSskills.required_skills)
+ var/singleton/skill/skill = GET_SINGLETON(key)
+ if (istype(skill))
+ pref.skills[skill.type] = SKILL_LEVEL_UNFAMILIAR
+
+ for(var/key,value in loaded_skills)
+ if (!key)
+ continue
+ var/path = istext(key) ? text2path(key) : key
+ var/singleton/skill/skill = GET_SINGLETON(path)
+ if(istype(skill))
+ pref.skills[skill.type] = value
+
+/datum/category_item/player_setup_item/skills/sanitize_character(var/sql_load = 0)
+ //todomatt
+ if(!istext(pref.education) || !ispath(text2path(pref.education), /singleton/education))
+ var/singleton/education/ED = find_suitable_education()
+ if(ED)
+ pref.education = "[ED.type]"
+ else
+ var/singleton/education/our_education = GET_SINGLETON(text2path(pref.education))
+ if(length(our_education.species_restriction))
+ if(pref.species in our_education.species_restriction)
+ var/singleton/education/ED = find_suitable_education()
+ if(ED)
+ pref.education = "[ED.type]"
+ if(length(our_education.minimum_character_age))
+ if(pref.species in our_education.minimum_character_age)
+ if(pref.age < our_education.minimum_character_age[pref.species])
+ var/singleton/education/ED = find_suitable_education()
+ if(ED)
+ pref.education = "[ED.type]"
+
+// Skills HTML UI, along with a lot of other components here, lifted from Baystation 12. Credit goes to Afterthought12. Thank you for saving me from HTML hell!
+/datum/category_item/player_setup_item/skills/content(var/mob/user)
+ if(!SSskills.initialized)
+ return "Skills not initialized yet. Please wait a bit and reload this section."
+
+ var/list/dat = list()
+
+ dat += ""
+ dat += ""
+ dat += ""
+ dat += ""
+ dat += ""
+ dat += ""
+ var/singleton/education/ED = GET_SINGLETON(text2path(pref.education))
+ dat += "Education: [ED.name]
"
+ dat += ""
+ var/singleton/education/education = GET_SINGLETON(text2path(pref.education))
+ for(var/category in SSskills.skill_tree)
+ var/singleton/skill_category/skill_category = category
+ dat += "| [skill_category.name] ([calculate_remaining_skill_points(skill_category)] points remaining)"
+ dat += " |
"
+ for(var/subcategory in SSskills.skill_tree[skill_category])
+ dat += "| [subcategory] |
"
+ for(var/singleton/skill/skill in SSskills.skill_tree[skill_category][subcategory])
+ dat += get_skill_row(skill, education)
+ dat += "
"
+
+ . = JOINTEXT(dat)
+
+/**
+ * Returns an HTML skill row.
+ */
+/datum/category_item/player_setup_item/skills/proc/get_skill_row(singleton/skill/skill, singleton/education/education)
+ var/list/dat = list()
+ dat += ""
+ dat += "| [skill.name] | "
+
+ var/level_from_pref = pref.skills[skill.type]
+ var/current_level = level_from_pref ? level_from_pref : SKILL_LEVEL_UNFAMILIAR
+ var/maximum_skill_level = get_maximum_skill_level(skill, education)
+
+ for(var/i = SKILL_LEVEL_UNFAMILIAR, i <= skill.maximum_level, i++)
+ dat += skill_to_button(skill, education, current_level, i, maximum_skill_level)
+
+ return JOINTEXT(dat)
+
+/datum/category_item/player_setup_item/skills/proc/get_maximum_skill_level(singleton/skill/skill, singleton/education/education)
+ var/base_maximum_level = skill.get_maximum_level(education)
+ var/remaining_skill_points = calculate_remaining_skill_points(GET_SINGLETON(skill.category))
+
+ var/current_level = SKILL_LEVEL_UNFAMILIAR
+ if(skill.type in pref.skills)
+ current_level = pref.skills[skill.type]
+
+ var/current_cost = 0
+ if(!(skill.type in education.skills))
+ current_cost = skill.get_cost(current_level)
+
+ var/available_points = remaining_skill_points + current_cost
+
+ for(var/skill_level = base_maximum_level; skill_level >= SKILL_LEVEL_UNFAMILIAR; skill_level--)
+ if(skill.get_cost(skill_level) <= available_points)
+ return skill_level
+
+ return SKILL_LEVEL_UNFAMILIAR
+
+/**
+ * Turns a skill into a dynamic button.
+ */
+/datum/category_item/player_setup_item/skills/proc/skill_to_button(singleton/skill/skill, singleton/education/education, current_level, selection_level, maximum_skill_level)
+ var/effective_level = selection_level
+ if(effective_level <= 0)
+ return " | "
+
+ var/level_name = skill.skill_level_map[effective_level]
+ var/cost = skill.get_cost(effective_level)
+ var/button_label = "[level_name] ([cost])"
+ var/given_skill = FALSE
+
+ // Prevent removal of skills given by education. These are meant to be minimum skills for jobs, after all.
+ if(skill.type in education.skills)
+ given_skill = TRUE
+
+ if((effective_level < current_level) && given_skill)
+ return "[span("Forced", "[button_label]")] | "
+ else if((effective_level < current_level) && !given_skill)
+ return "[add_link(skill, education, button_label, "'Current'", effective_level)] | "
+ else if(effective_level == current_level)
+ return "[span("Current", "[button_label]")] | "
+ else if(effective_level <= maximum_skill_level)
+ return "[add_link(skill, education, button_label, "'Selectable'", effective_level)] | "
+ else
+ return "[span("Toohigh", "[button_label]")] | "
+
+/**
+ * Returns a button to set a skill in the skill UI.
+ */
+/datum/category_item/player_setup_item/skills/proc/add_link(singleton/skill/skill, singleton/education/education, text, style, value)
+ if(skill.get_maximum_level(education) >= value)
+ return "[text]"
+ return text
+
+/**
+ * Returns the currently remaining skill points in a given category.
+ */
+/datum/category_item/player_setup_item/skills/proc/calculate_remaining_skill_points(singleton/skill_category/skill_category)
+ if(!istype(skill_category))
+ crash_with("Invalid skill category [skill_category] fed to calculate_remaining_skill_points!")
+
+ var/skill_points_remaining = skill_category.calculate_skill_points(GLOB.all_species[pref.species], pref.age, GET_SINGLETON(text2path(pref.culture)), GET_SINGLETON(text2path(pref.origin)))
+ var/current_points_used = get_used_skill_points_per_category(skill_category, GET_SINGLETON(text2path(pref.education)))
+ return skill_points_remaining - current_points_used
+
+/**
+ * Returns the amount of used skill points in a certain skill category, ignoring skills given by education.
+ */
+/datum/category_item/player_setup_item/skills/proc/get_used_skill_points_per_category(singleton/skill_category/skill_category, singleton/education/education)
+ if(!istype(skill_category))
+ crash_with("Invalid skill category [skill_category] fed to get_used_skill_points_per_category!")
+
+ if(!istype(education))
+ crash_with("Invalid education [education] fed to get_used_skill_points_per_category!")
+
+ . = 0
+ for(var/skill_type in pref.skills)
+ var/singleton/skill/skill = GET_SINGLETON(skill_type)
+ if(skill.category != skill_category.type)
+ continue
+
+ if(skill.type in education.skills)
+ continue
+
+ . += skill.get_cost(pref.skills[skill.type])
+
+/datum/category_item/player_setup_item/skills/OnTopic(href, href_list, user)
+ if(href_list["skillinfo"])
+ var/singleton/skill/skill_to_show = GET_SINGLETON(text2path(href_list["skillinfo"]))
+ if(!skill_to_show)
+ log_debug("SKILLS: Invalid skill selected for [user]: [skill_to_show]")
+ return
+ var/datum/browser/skill_window = new(user, "skill_info", "Skill Information")
+ var/dat = "[skill_to_show.name]"
+ dat += "
[skill_to_show.description]
"
+ if(skill_to_show.uneducated_skill_cap)
+ dat += "Without the relevant education, you may only reach the [skill_to_show.skill_level_map[skill_to_show.uneducated_skill_cap]] level.
"
+ dat += "
"
+ var/skill_level = (skill_to_show.type in pref.skills) ? pref.skills[skill_to_show.type] : SKILL_LEVEL_UNFAMILIAR
+ dat += "Your current level in this skill is [SPAN_BOLD(skill_to_show.skill_level_map[skill_level])].
"
+ dat += "[skill_to_show.skill_level_descriptions[skill_level]]"
+ dat += ""
+ skill_window.set_content(dat)
+ skill_window.open()
+
+ else if(href_list["setskill"])
+ var/singleton/skill/new_skill = GET_SINGLETON(text2path(href_list["setskill"]))
+ if(!new_skill)
+ log_debug("SKILLS: Invalid skill selected for [user]: [new_skill]")
+ return
+
+ var/new_skill_value = text2num(href_list["newvalue"])
+ pref.skills[new_skill.type] = text2num(new_skill_value)
+ return TOPIC_REFRESH
+
+ else if(href_list["open_education_menu"])
+ var/list/options = list()
+ var/list/singleton/education/education_list = GET_SINGLETON_SUBTYPE_MAP(/singleton/education)
+ for(var/singleton_type in education_list)
+ var/singleton/education/ED = education_list[singleton_type]
+ if(length(ED.species_restriction))
+ if(pref.species in ED.species_restriction)
+ continue
+ if(length(ED.minimum_character_age))
+ if(pref.species in ED.minimum_character_age)
+ if(pref.age < ED.minimum_character_age[pref.species])
+ continue
+ options[ED.name] = ED
+ var/result = tgui_input_list(user, "Choose your character's education.", "Education", options)
+ var/singleton/education/chosen_education = options[result]
+ if(chosen_education)
+ show_education_window(chosen_education, "set_education_data", user)
+
+ else if(href_list["set_education_data"])
+ user << browse(null, "window=set_education_data")
+ var/new_education = html_decode(href_list["set_education_data"])
+ pref.education = new_education
+
+ pref.skills = list() // reset skills because we have to give them new minimums
+ to_chat(user, SPAN_WARNING("Your skills have been reset as you changed your education."))
+ var/singleton/education/education = GET_SINGLETON(text2path(new_education))
+ if(istype(education))
+ for(var/skill in education.skills)
+ var/singleton/skill/new_skill = GET_SINGLETON(skill)
+ pref.skills[new_skill.type] = education.skills[new_skill.type]
+ to_chat(user, SPAN_NOTICE("Added the [new_skill.name] skill at level [new_skill.skill_level_map[education.skills[new_skill.type]]]."))
+
+ sanitize_character()
+ return TOPIC_REFRESH
+
+ return ..()
+
+/**
+ * Opens a window showing details of an education.
+ */
+/datum/category_item/player_setup_item/skills/proc/show_education_window(var/singleton/education/ED, var/topic_data, var/mob/user)
+ var/datum/browser/education_win = new(user, topic_data, "Education Selection")
+ var/dat = "[ED.name]"
+ dat += "
[ED.description]
"
+ dat += "This education gives you the following skills: "
+ var/list/skills_to_show = list()
+ for(var/skill in ED.skills)
+ var/singleton/skill/S = GET_SINGLETON(skill)
+ skills_to_show += "[S.name] ([SPAN_DANGER(S.skill_level_map[ED.skills[S.type]])])"
+ dat += "[english_list(skills_to_show)].
"
+ dat += "
\[Select\]"
+ dat += ""
+ education_win.set_content(dat)
+ education_win.open()
+
+/**
+ * Finds and returns the first suitable education for the pref datum.
+ */
+/datum/category_item/player_setup_item/skills/proc/find_suitable_education()
+ var/list/singleton/education/education_list = GET_SINGLETON_SUBTYPE_MAP(/singleton/education)
+ for(var/singleton_type in education_list)
+ var/singleton/education/ED = education_list[singleton_type]
+ if(length(ED.species_restriction))
+ if(pref.species in ED.species_restriction)
+ continue
+ if(length(ED.minimum_character_age))
+ if(pref.species in ED.minimum_character_age)
+ if(pref.age < ED.minimum_character_age[pref.species])
+ continue
+ return ED
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index 68f02e6c991..c449d82856a 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -97,16 +97,28 @@ GLOBAL_LIST_EMPTY_TYPED(preferences_datums, /datum/preferences)
var/machine_ownership_status = IPC_OWNERSHIP_COMPANY
var/hidden_shell_status = FALSE
- //Some faction information.
- var/home_system = "Unset" //System of birth.
- var/citizenship = "None" //Current home system.
- var/faction = "None" //Antag faction/general associated faction.
- var/religion = "None" //Religious association.
- var/accent = "None" //Character accent.
+ /// Character citizenship.
+ var/citizenship = "None"
+ /// Antag faction/general associated faction.
+ var/faction = "None"
+ /// Religious association.
+ var/religion = "None"
+ /// Character accent.
+ var/accent = "None"
+ /// The character's culture singleton.
var/culture
+ /// The character's origin singleton.
var/origin
+ /// The character's education singleton.
+ var/education
+ /// The character's skills list. JSON.
+ var/list/skills = list()
+ /// The character's current spent skill points. Assoc list of SKILL_CATEGORY define to number of remaining skill points.
+ var/list/skill_points_remaining
+
+ /// The character's psionics. JSON.
var/list/psionics = list()
var/list/char_render_holders //Should only be a key-value list of north/south/east/west = obj/screen.
@@ -476,7 +488,6 @@ GLOBAL_LIST_EMPTY_TYPED(preferences_datums, /datum/preferences)
character.set_culture(GET_SINGLETON(text2path(culture)))
character.set_origin(GET_SINGLETON(text2path(origin)))
-
// Destroy/cyborgize organs & setup body markings
character.sync_organ_prefs_to_mob(src)
@@ -517,6 +528,10 @@ GLOBAL_LIST_EMPTY_TYPED(preferences_datums, /datum/preferences)
if(istype(P) && (P.ability_flags & PSI_FLAG_CANON))
P.apply(character)
+ for(var/skill_type in skills)
+ var/singleton/skill/skill = GET_SINGLETON(skill_type)
+ skill.on_spawn(character, skills[skill.type])
+
if(icon_updates)
character.force_update_limbs()
character.update_mutations(0)
@@ -651,7 +666,6 @@ GLOBAL_LIST_EMPTY_TYPED(preferences_datums, /datum/preferences)
b_eyes = 0
species = SPECIES_HUMAN
- home_system = "Unset"
citizenship = "None"
faction = "None"
religion = "None"
diff --git a/code/modules/heavy_vehicle/mech_helpers.dm b/code/modules/heavy_vehicle/mech_helpers.dm
index 8757a4069be..2c3d5c796f8 100644
--- a/code/modules/heavy_vehicle/mech_helpers.dm
+++ b/code/modules/heavy_vehicle/mech_helpers.dm
@@ -1,4 +1,4 @@
-/mob/living/heavy_vehicle/proc/can_move(var/mob/user)
+/mob/living/heavy_vehicle/proc/can_move(var/mob/user, delay_modifier)
if(world.time < next_mecha_move)
return
@@ -28,10 +28,11 @@
to_chat(user, SPAN_WARNING("The power indicator flashes briefly."))
return
- next_mecha_move = world.time + (incorporeal_move ? legs.move_delay / 2 : legs.move_delay) + (legs.damaged_delay * (legs.total_damage / legs.max_damage) * legs.damaged_delay_slope)
+
+ next_mecha_move = delay_modifier + world.time + (incorporeal_move ? legs.move_delay / 2 : legs.move_delay) + (legs.damaged_delay * (legs.total_damage / legs.max_damage) * legs.damaged_delay_slope)
return TRUE
-/mob/living/heavy_vehicle/proc/can_turn(var/mob/user)
+/mob/living/heavy_vehicle/proc/can_turn(var/mob/user, delay_modifier)
if(world.time < next_mecha_turn)
return
@@ -61,10 +62,11 @@
to_chat(user, SPAN_WARNING("The power indicator flashes briefly."))
return
- next_mecha_turn = world.time + legs.turn_delay + (legs.damaged_delay * (legs.total_damage / legs.max_damage) * legs.damaged_delay_slope)
+
+ next_mecha_turn = delay_modifier + world.time + legs.turn_delay + (legs.damaged_delay * (legs.total_damage / legs.max_damage) * legs.damaged_delay_slope)
return TRUE
-/mob/living/heavy_vehicle/proc/can_strafe(var/mob/user)
+/mob/living/heavy_vehicle/proc/can_strafe(var/mob/user, delay_modifier)
if (world.time < next_mecha_move)
return
diff --git a/code/modules/heavy_vehicle/mech_interaction.dm b/code/modules/heavy_vehicle/mech_interaction.dm
index b8b993bebab..cb88ad103bb 100644
--- a/code/modules/heavy_vehicle/mech_interaction.dm
+++ b/code/modules/heavy_vehicle/mech_interaction.dm
@@ -299,22 +299,25 @@
/mob/living/heavy_vehicle/relaymove(mob/living/user, direction, var/turn_only = FALSE)
. = ..()
+ var/delay_modifier = 0
+ SEND_SIGNAL(user, COMSIG_MECH_MOVE_WASD, &direction, &delay_modifier)
+
if(hallucination >= EMP_MOVE_DISRUPT && prob(30))
direction = pick(GLOB.cardinals)
// Convert keyboard inputs to Battletech-style controls.
switch (direction)
if (NORTH) // "Throttle Forwards"
- throttle_move(user, dir, FALSE)
+ throttle_move(user, dir, FALSE, delay_modifier)
if (SOUTH) // "Throttle Reverse"
- throttle_move(user, angle2dir(dir2angle(dir) + 180), TRUE)
+ throttle_move(user, angle2dir(dir2angle(dir) + 180), TRUE, delay_modifier)
if (EAST) // "Turn Right"
- rotate_by_angle(user, angle2dir(dir2angle(dir) + 90))
+ rotate_by_angle(user, angle2dir(dir2angle(dir) + 90), delay_modifier)
if (WEST) // "Turn Left"
- rotate_by_angle(user, angle2dir(dir2angle(dir) + 270))
+ rotate_by_angle(user, angle2dir(dir2angle(dir) + 270), delay_modifier)
-/mob/living/heavy_vehicle/proc/throttle_move(mob/living/user, direction, reverse)
- if (!legs || !can_move(user))
+/mob/living/heavy_vehicle/proc/throttle_move(mob/living/user, direction, reverse, delay_modifier)
+ if (!legs || !can_move(user, delay_modifier))
return
// Get the tile in the direction.
@@ -335,7 +338,13 @@
Move(target_loc, direction, 0, FALSE)
/mob/living/heavy_vehicle/proc/strafe_move(mob/user, direction)
- if (!legs || !can_strafe(user))
+ if (!legs)
+ return
+
+ var/delay_modifier = 0
+ SEND_SIGNAL(user, COMSIG_MECH_MOVE_STRAFE, &direction, &delay_modifier)
+
+ if (!can_strafe(user, delay_modifier))
return
// Get the tile in the direction.
@@ -352,8 +361,8 @@
else
Move(target_loc, direction, 0, FALSE)
-/mob/living/heavy_vehicle/proc/rotate_by_angle(mob/living/user, direction)
- if (!legs || !can_turn(user))
+/mob/living/heavy_vehicle/proc/rotate_by_angle(mob/living/user, direction, delay_modifier)
+ if (!legs || !can_turn(user, delay_modifier))
return
use_cell_power(legs.power_use * CELLRATE)
diff --git a/code/modules/heavy_vehicle/mecha.dm b/code/modules/heavy_vehicle/mecha.dm
index 2fb44bb4330..c68c8420cbf 100644
--- a/code/modules/heavy_vehicle/mecha.dm
+++ b/code/modules/heavy_vehicle/mecha.dm
@@ -263,6 +263,13 @@
/// `var/remote` can be set to TRUE to have proc adjust where messages and hud elements are presented
/// If `remote` is TRUE, messages and other hud elements are called on the exosuit itself to prevent wierdness, and errors are handled in `handle_hear_say()`
/mob/living/heavy_vehicle/proc/toggle_power(var/mob/user, var/remote = FALSE)
+ var/cancelled = FALSE
+ var/delay = 0
+ SEND_SIGNAL(user, COMSIG_MECH_TOGGLE_POWER, &cancelled, &delay)
+ if (cancelled || delay && !do_after(user, delay))
+ to_chat(user, SPAN_WARNING("There's just too many buttons! You fail to find the power switch!"))
+ return
+
// if remotely called, send these messages to the exosuit, not the person calling this proc
var/reciever = user
if(remote)
diff --git a/code/modules/hydroponics/seed.dm b/code/modules/hydroponics/seed.dm
index 657892fb6f0..28bc309bbf4 100644
--- a/code/modules/hydroponics/seed.dm
+++ b/code/modules/hydroponics/seed.dm
@@ -59,6 +59,9 @@
var/force_layer
var/hydrotray_only
+ /// The amount of time it takes to harvest this plant.
+ var/harvest_time = 2 SECONDS
+
/datum/seed/proc/setup_traits()
/datum/seed/New()
@@ -773,13 +776,28 @@
/// Place the plant products at the feet of the user.
/datum/seed/proc/harvest(var/mob/user,var/yield_mod,var/harvest_sample,var/force_amount,var/stunted_status = FALSE)
- if(!user)
+ if(!istype(user))
+ return
+
+ var/total_yield = 0
+ var/cancelled = FALSE
+ var/doafter = harvest_time
+ // Check if any components on the user wish to modify the harvest.
+ SEND_SIGNAL(user, COMSIG_PLANT_HARVESTER, src, &total_yield, &cancelled, &doafter)
+ // And check if any components on the plant wish to modify the harvest.
+ SEND_SIGNAL(src, COMSIG_PLANT_HARVESTED, user, &total_yield, &cancelled, &doafter)
+ if (cancelled)
+ return
+
+ user.visible_message(SPAN_WARNING("[user] starts harvesting \the [display_name]"))
+ if (doafter > 0 && !do_after(user, doafter, src))
+ to_chat(user, SPAN_DANGER("You were interrupted while trying to harvest \the [display_name]"))
return
if(!force_amount && GET_SEED_TRAIT(src, TRAIT_YIELD) == 0 && !harvest_sample)
- if(istype(user)) to_chat(user, SPAN_DANGER("You fail to harvest anything useful."))
+ to_chat(user, SPAN_DANGER("You fail to harvest anything useful."))
else
- if(istype(user)) to_chat(user, "You [harvest_sample ? "take a sample" : "harvest"] from the [display_name].")
+ to_chat(user, "You [harvest_sample ? "take a sample" : "harvest"] from the [display_name].")
//This may be a new line. Update the global if it is.
if(name == "new line" || !(name in SSplants.seeds))
@@ -793,16 +811,15 @@
seeds.update_seed()
return
- var/total_yield = 0
if(!isnull(force_amount))
total_yield = force_amount
else
if(GET_SEED_TRAIT(src, TRAIT_YIELD) > -1)
if(isnull(yield_mod) || yield_mod < 1)
yield_mod = 0
- total_yield = GET_SEED_TRAIT(src, TRAIT_YIELD)
+ total_yield += GET_SEED_TRAIT(src, TRAIT_YIELD)
else
- total_yield = GET_SEED_TRAIT(src, TRAIT_YIELD) + rand(yield_mod)
+ total_yield += GET_SEED_TRAIT(src, TRAIT_YIELD) + rand(yield_mod)
total_yield = max(1,total_yield)
// If the plant is stunted, you get half the yield.
diff --git a/code/modules/materials/material_recipes.dm b/code/modules/materials/material_recipes.dm
index 8c0e8c2df40..8d421ea810b 100644
--- a/code/modules/materials/material_recipes.dm
+++ b/code/modules/materials/material_recipes.dm
@@ -9,37 +9,37 @@
// If is_brittle() returns true, these are only good for a single strike.
recipes += new /datum/stack_recipe_list("generic crafts",
list(
- new /datum/stack_recipe("[display_name] baseball bat", /obj/item/material/twohanded/baseballbat, 10, time = 20, one_per_turf = 0, on_floor = 1, supplied_material = "[name]"),
- new /datum/stack_recipe("[display_name] sword hilt", /obj/item/material/sword_hilt, 10, time = 100, one_per_turf = 0, on_floor = 1, supplied_material = "[name]"),
- new /datum/stack_recipe("[display_name] sword blade", /obj/item/material/sword_blade, 15, time = 100, one_per_turf = 0, on_floor = 1, supplied_material = "[name]"),
- new /datum/stack_recipe("[display_name] ring", /obj/item/clothing/ring/material, 1, on_floor = 1, supplied_material = "[name]"),
- new /datum/stack_recipe("[display_name] armor plating", /obj/item/material/armor_plating, 3, time = 20, on_floor = 1, supplied_material = "[name]")
+ new /datum/stack_recipe("[display_name] baseball bat", /obj/item/material/twohanded/baseballbat, 10, time = 20, one_per_turf = 0, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("[display_name] sword hilt", /obj/item/material/sword_hilt, 10, time = 100, one_per_turf = 0, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("[display_name] sword blade", /obj/item/material/sword_blade, 15, time = 100, one_per_turf = 0, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("[display_name] ring", /obj/item/clothing/ring/material, 1, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("[display_name] armor plating", /obj/item/material/armor_plating, 3, time = 20, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED))
))
if(integrity >= 50)
recipes += new /datum/stack_recipe_list("generic construction",
list(
- new /datum/stack_recipe("[display_name] door", /obj/structure/simple_door, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1, supplied_material = "[name]"),
- new /datum/stack_recipe("[display_name] blocker", /obj/structure/blocker, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1, supplied_material = "[name]"),
- new /datum/stack_recipe("[display_name] railing", /obj/structure/railing, BUILD_AMT, time = 25, one_per_turf = FALSE, on_floor = TRUE, supplied_material = "[name]"),
- new /datum/stack_recipe("[display_name] stool", /obj/structure/bed/stool, BUILD_AMT, one_per_turf = 1, on_floor = 1, supplied_material = "[name]"),
- new /datum/stack_recipe("[display_name] chair", /obj/structure/bed/stool/chair, BUILD_AMT, one_per_turf = 1, on_floor = 1, supplied_material = "[name]"),
- new /datum/stack_recipe("[display_name] bed", /obj/structure/bed, BUILD_AMT, one_per_turf = 1, on_floor = 1, supplied_material = "[name]"),
- new /datum/stack_recipe("[display_name] lock", /obj/item/material/lock_construct, 1, time = 20, one_per_turf = 0, on_floor = 1, supplied_material = "[name]"),
- new /datum/stack_recipe("[display_name] urn", /obj/item/material/urn, 10, time = 30, one_per_turf = FALSE, on_floor = 1, supplied_material = "[name]")
+ new /datum/stack_recipe("[display_name] door", /obj/structure/simple_door, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("[display_name] blocker", /obj/structure/blocker, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("[display_name] railing", /obj/structure/railing, BUILD_AMT, time = 25, one_per_turf = FALSE, on_floor = TRUE, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("[display_name] stool", /obj/structure/bed/stool, BUILD_AMT, one_per_turf = 1, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("[display_name] chair", /obj/structure/bed/stool/chair, BUILD_AMT, one_per_turf = 1, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("[display_name] bed", /obj/structure/bed, BUILD_AMT, one_per_turf = 1, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("[display_name] lock", /obj/item/material/lock_construct, 1, time = 20, one_per_turf = 0, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("[display_name] urn", /obj/item/material/urn, 10, time = 30, one_per_turf = FALSE, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR))
))
var/list/hardness_craftables = list()
if(hardness >= 10)
- hardness_craftables += new /datum/stack_recipe("[display_name] ashtray", /obj/item/material/ashtray, 2, one_per_turf = 1, on_floor = 1, supplied_material = "[name]")
+ hardness_craftables += new /datum/stack_recipe("[display_name] ashtray", /obj/item/material/ashtray, 2, one_per_turf = 1, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR))
if(hardness > 50)
- hardness_craftables += new /datum/stack_recipe("[display_name] fork", /obj/item/material/kitchen/utensil/fork/plastic, 1, on_floor = 1, supplied_material = "[name]")
- hardness_craftables += new /datum/stack_recipe("[display_name] spoon", /obj/item/material/kitchen/utensil/spoon/plastic, 1, on_floor = 1, supplied_material = "[name]")
- hardness_craftables += new /datum/stack_recipe("[display_name] knife", /obj/item/material/kitchen/utensil/knife/plastic, 1, on_floor = 1, supplied_material = "[name]")
- hardness_craftables += new /datum/stack_recipe("[display_name] blade", /obj/item/material/butterflyblade, 6, time = 20, one_per_turf = 0, on_floor = 1, supplied_material = "[name]")
- hardness_craftables += new /datum/stack_recipe("[display_name] spearhead", /obj/item/material/spearhead, 6, time = 20, one_per_turf = 0, on_floor = 1, supplied_material = "[name]")
- hardness_craftables += new /datum/stack_recipe("[display_name] drill head", /obj/item/material/drill_head, 6, time = 20, one_per_turf = 0, on_floor = 1, supplied_material = "[name]")
+ hardness_craftables += new /datum/stack_recipe("[display_name] fork", /obj/item/material/kitchen/utensil/fork/plastic, 1, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR))
+ hardness_craftables += new /datum/stack_recipe("[display_name] spoon", /obj/item/material/kitchen/utensil/spoon/plastic, 1, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR))
+ hardness_craftables += new /datum/stack_recipe("[display_name] knife", /obj/item/material/kitchen/utensil/knife/plastic, 1, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR))
+ hardness_craftables += new /datum/stack_recipe("[display_name] blade", /obj/item/material/butterflyblade, 6, time = 20, one_per_turf = 0, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED))
+ hardness_craftables += new /datum/stack_recipe("[display_name] spearhead", /obj/item/material/spearhead, 6, time = 20, one_per_turf = 0, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED))
+ hardness_craftables += new /datum/stack_recipe("[display_name] drill head", /obj/item/material/drill_head, 6, time = 20, one_per_turf = 0, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED))
if(length(hardness_craftables))
recipes += new /datum/stack_recipe_list("generic miscellaneous", hardness_craftables)
@@ -48,91 +48,91 @@
..()
recipes += new /datum/stack_recipe_list("construction recipes",
list(
- new /datum/stack_recipe("regular floor tile", /obj/item/stack/tile/floor, 1, 4, 20),
- new /datum/stack_recipe("full regular floor tile", /obj/item/stack/tile/mono, 1, 4, 20),
- new /datum/stack_recipe("metal rod", /obj/item/stack/rods, 1, 2, 60),
- new /datum/stack_recipe("steel barricade", /obj/structure/barricade/metal, BUILD_AMT, time = 10 SECONDS, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("barbed wire", /obj/item/stack/barbed_wire, 1, 2, 20),
- new /datum/stack_recipe("wall girders", /obj/structure/girder, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("steel window frame", /obj/structure/window_frame/unanchored, BUILD_AMT, time = 25, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("computer frame", /obj/structure/computerframe, BUILD_AMT, time = 25, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("machine blueprint", /obj/machinery/constructable_frame/machine_frame, 2, time = 25, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("light fixture frame", /obj/item/frame/light, 2),
- new /datum/stack_recipe("floor light fixture frame", /obj/item/floor_frame/light, 2),
- new /datum/stack_recipe("small light fixture frame", /obj/item/frame/light/small, 1),
- new /datum/stack_recipe("small floor light fixture frame", /obj/item/floor_frame/light/small, 1),
- new /datum/stack_recipe("spotlight fixture frame", /obj/item/frame/light/spot, 3),
- new /datum/stack_recipe("apc frame", /obj/item/frame/apc, 2),
- new /datum/stack_recipe("air alarm frame", /obj/item/frame/air_alarm, 2),
- new /datum/stack_recipe("fire alarm frame", /obj/item/frame/fire_alarm, 2),
- new /datum/stack_recipe("fuel port", /obj/item/fuel_port, 2, time = 2 SECONDS)
+ new /datum/stack_recipe("regular floor tile", /obj/item/stack/tile/floor, 1, 4, 20, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("full regular floor tile", /obj/item/stack/tile/mono, 1, 4, 20, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("metal rod", /obj/item/stack/rods, 1, 2, 60, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("steel barricade", /obj/structure/barricade/metal, BUILD_AMT, time = 10 SECONDS, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("barbed wire", /obj/item/stack/barbed_wire, 1, 2, 20, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("wall girders", /obj/structure/girder, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("steel window frame", /obj/structure/window_frame/unanchored, BUILD_AMT, time = 25, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("computer frame", /obj/structure/computerframe, BUILD_AMT, time = 10 SECONDS, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("machine blueprint", /obj/machinery/constructable_frame/machine_frame, 2, time = 10 SECONDS, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("light fixture frame", /obj/item/frame/light, 2, required_skills_hard = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("floor light fixture frame", /obj/item/floor_frame/light, 2, required_skills_hard = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("small light fixture frame", /obj/item/frame/light/small, 1, required_skills_hard = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("small floor light fixture frame", /obj/item/floor_frame/light/small, 1, required_skills_hard = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("spotlight fixture frame", /obj/item/frame/light/spot, 3, required_skills_hard = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("apc frame", /obj/item/frame/apc, 2, required_skills_hard = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("air alarm frame", /obj/item/frame/air_alarm, 2, required_skills_hard = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("fire alarm frame", /obj/item/frame/fire_alarm, 2, required_skills_hard = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("fuel port", /obj/item/fuel_port, 2, time = 2 SECONDS, required_skills_hard = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED))
))
recipes += new /datum/stack_recipe_list("miscellaneous construction",
list(
- new /datum/stack_recipe("key", /obj/item/key, 1, time = 10, one_per_turf = 0, on_floor = 1),
- new /datum/stack_recipe("custodial cart", /obj/structure/cart/storage/janitorialcart, BUILD_AMT, time = 120, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("engineering cart", /obj/structure/cart/storage/engineeringcart, BUILD_AMT, time = 120, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("parcel cart", /obj/structure/cart/storage/parcelcart, BUILD_AMT, time = 120, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("steel closet", /obj/structure/closet, BUILD_AMT, time = 15, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("canister", /obj/machinery/portable_atmospherics/canister, 10, time = 15, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("target stake", /obj/structure/target_stake, BUILD_AMT, time = 15, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("shooting target", /obj/item/target, 5, time = 10, one_per_turf = 0, on_floor = 1),
- new /datum/stack_recipe("dark office chair", /obj/structure/bed/stool/chair/office/dark, BUILD_AMT, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("light office chair", /obj/structure/bed/stool/chair/office/light, BUILD_AMT, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("steel chair (fancy)", /obj/structure/bed/stool/chair/fancy, BUILD_AMT, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("sofa (middle)", /obj/structure/bed/stool/chair/sofa, BUILD_AMT, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("sofa (left)", /obj/structure/bed/stool/chair/sofa/left, BUILD_AMT, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("sofa (right)", /obj/structure/bed/stool/chair/sofa/right, BUILD_AMT, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("sofa (corner, concave)", /obj/structure/bed/stool/chair/sofa/corner/concave, BUILD_AMT, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("sofa (corner, convex)", /obj/structure/bed/stool/chair/sofa/corner/convex, BUILD_AMT, one_per_turf = 1, on_floor = 1),
+ new /datum/stack_recipe("key", /obj/item/key, 1, time = 10, one_per_turf = 0, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("custodial cart", /obj/structure/cart/storage/janitorialcart, BUILD_AMT, time = 120, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("engineering cart", /obj/structure/cart/storage/engineeringcart, BUILD_AMT, time = 120, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("parcel cart", /obj/structure/cart/storage/parcelcart, BUILD_AMT, time = 120, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("steel closet", /obj/structure/closet, BUILD_AMT, time = 15, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("canister", /obj/machinery/portable_atmospherics/canister, 10, time = 15, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("target stake", /obj/structure/target_stake, BUILD_AMT, time = 15, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("shooting target", /obj/item/target, 5, time = 10, one_per_turf = 0, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("dark office chair", /obj/structure/bed/stool/chair/office/dark, BUILD_AMT, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("light office chair", /obj/structure/bed/stool/chair/office/light, BUILD_AMT, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("steel chair (fancy)", /obj/structure/bed/stool/chair/fancy, BUILD_AMT, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("sofa (middle)", /obj/structure/bed/stool/chair/sofa, BUILD_AMT, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("sofa (left)", /obj/structure/bed/stool/chair/sofa/left, BUILD_AMT, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("sofa (right)", /obj/structure/bed/stool/chair/sofa/right, BUILD_AMT, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("sofa (corner, concave)", /obj/structure/bed/stool/chair/sofa/corner/concave, BUILD_AMT, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("sofa (corner, convex)", /obj/structure/bed/stool/chair/sofa/corner/convex, BUILD_AMT, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
new /datum/stack_recipe("steel crate", /obj/structure/closet/crate, BUILD_AMT, time = 50, one_per_turf = 1)
))
recipes += new /datum/stack_recipe_list("airlock assemblies",
list(
- new /datum/stack_recipe("generic airlock assembly", /obj/structure/door_assembly/door_assembly_generic, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("maintenance airlock assembly", /obj/structure/door_assembly/door_assembly_mai, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("external airlock assembly", /obj/structure/door_assembly/door_assembly_ext, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("airtight hatch assembly", /obj/structure/door_assembly/door_assembly_hatch, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("maintenance hatch assembly", /obj/structure/door_assembly/door_assembly_mhatch, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("high security airlock assembly", /obj/structure/door_assembly/door_assembly_highsecurity, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("emergency shutter", /obj/structure/firedoor_assembly, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("multi-tile airlock assembly", /obj/structure/door_assembly/multi_tile, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1)
+ new /datum/stack_recipe("generic airlock assembly", /obj/structure/door_assembly/door_assembly_generic, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("maintenance airlock assembly", /obj/structure/door_assembly/door_assembly_mai, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("external airlock assembly", /obj/structure/door_assembly/door_assembly_ext, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("airtight hatch assembly", /obj/structure/door_assembly/door_assembly_hatch, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("maintenance hatch assembly", /obj/structure/door_assembly/door_assembly_mhatch, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("high security airlock assembly", /obj/structure/door_assembly/door_assembly_highsecurity, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1), required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED),
+ new /datum/stack_recipe("emergency shutter", /obj/structure/firedoor_assembly, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("multi-tile airlock assembly", /obj/structure/door_assembly/multi_tile, BUILD_AMT, time = 50, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED))
))
recipes += new /datum/stack_recipe_list("turret frames",
list(
- new /datum/stack_recipe("light turret frame", /obj/machinery/porta_turret_construct, 5, time = 25, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("dark turret frame", /obj/machinery/porta_turret_construct/dark, 5, time = 25, one_per_turf = 1, on_floor = 1)
+ new /datum/stack_recipe("light turret frame", /obj/machinery/porta_turret_construct, 5, time = 25, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("dark turret frame", /obj/machinery/porta_turret_construct/dark, 5, time = 25, one_per_turf = 1, on_floor = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED))
))
recipes += new /datum/stack_recipe_list("modular computers",
list(
- new /datum/stack_recipe("modular console frame", /obj/item/modular_computer/console, 20, time = 25, one_per_turf = TRUE),
- new /datum/stack_recipe("modular laptop frame", /obj/item/modular_computer/laptop, 10, time = 25),
- new /datum/stack_recipe("modular tablet frame", /obj/item/modular_computer/handheld, 5, time = 25)
+ new /datum/stack_recipe("modular console frame", /obj/item/modular_computer/console, 20, time = 25, one_per_turf = TRUE, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("modular laptop frame", /obj/item/modular_computer/laptop, 10, time = 25, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("modular tablet frame", /obj/item/modular_computer/handheld, 5, time = 25, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED))
))
recipes += new /datum/stack_recipe_list("[display_name] weaponry",
list(
- new /datum/stack_recipe("makeshift magazine (5.56mm)", /obj/item/ammo_magazine/a556/makeshift/empty, 5, time = 20),
- new /datum/stack_recipe("grenade casing", /obj/item/grenade/chem_grenade),
- new /datum/stack_recipe("firearm receiver", /obj/item/receivergun, 15, time = 25, one_per_turf = 0, on_floor = 0),
- new /datum/stack_recipe("shield fittings", /obj/item/material/shieldbits, 10, time = 25),
- new /datum/stack_recipe("cannon frame", /obj/item/cannonframe, 10, time = 15, one_per_turf = 0, on_floor = 0),
- new /datum/stack_recipe("large trap foundation", /obj/item/large_trap_foundation, 4, time = 40)
+ new /datum/stack_recipe("makeshift magazine (5.56mm)", /obj/item/ammo_magazine/a556/makeshift/empty, 5, time = 20, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("grenade casing", /obj/item/grenade/chem_grenade, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("firearm receiver", /obj/item/receivergun, 15, time = 25, one_per_turf = 0, on_floor = 0, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("shield fittings", /obj/item/material/shieldbits, 10, time = 25, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_UNFAMILIAR)),
+ new /datum/stack_recipe("cannon frame", /obj/item/cannonframe, 10, time = 15, one_per_turf = 0, on_floor = 0, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("large trap foundation", /obj/item/large_trap_foundation, 4, time = 40, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR))
))
/material/plasteel/generate_recipes()
..()
recipes += new /datum/stack_recipe_list("[display_name] recipes",
list(
- new /datum/stack_recipe("AI core", /obj/structure/AIcore, BUILD_AMT, time = 50, one_per_turf = 1),
- new /datum/stack_recipe("plateel barricade", /obj/structure/barricade/plasteel, BUILD_AMT, time = 12 SECONDS, one_per_turf = 1),
- new /datum/stack_recipe("knife grip", /obj/item/material/butterflyhandle, 4, time = 20, one_per_turf = 0, on_floor = 1, supplied_material = "[name]"),
- new /datum/stack_recipe("dark floor tile", /obj/item/stack/tile/floor_dark, 1, 4, 20),
- new /datum/stack_recipe("full dark floor tile", /obj/item/stack/tile/floor_dark/full, 1, 4, 20)
+ new /datum/stack_recipe("AI core", /obj/structure/AIcore, BUILD_AMT, time = 50, one_per_turf = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("plateel barricade", /obj/structure/barricade/plasteel, BUILD_AMT, time = 12 SECONDS, one_per_turf = 1, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)),
+ new /datum/stack_recipe("knife grip", /obj/item/material/butterflyhandle, 4, time = 20, one_per_turf = 0, on_floor = 1, supplied_material = "[name]", required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("dark floor tile", /obj/item/stack/tile/floor_dark, 1, 4, 20, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
+ new /datum/stack_recipe("full dark floor tile", /obj/item/stack/tile/floor_dark/full, 1, 4, 20, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR))
))
/material/plastic/generate_recipes()
@@ -163,9 +163,9 @@
new /datum/stack_recipe("wood circlet", /obj/item/woodcirclet, 1),
new /datum/stack_recipe("wood floor tile", /obj/item/stack/tile/wood, 1, 4, 20),
new /datum/stack_recipe("wooden chair", /obj/structure/bed/stool/chair/wood, BUILD_AMT, time = 10, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("crossbow frame", /obj/item/crossbowframe, 5, time = 25, one_per_turf = 0, on_floor = 0),
+ new /datum/stack_recipe("crossbow frame", /obj/item/crossbowframe, 5, time = 25, one_per_turf = 0, on_floor = 0, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
new /datum/stack_recipe("coffin", /obj/structure/closet/crate/coffin, BUILD_AMT, time = 15, one_per_turf = 1, on_floor = 1),
- new /datum/stack_recipe("rifle stock", /obj/item/stock, 10, time = 25, one_per_turf = 0, on_floor = 0),
+ new /datum/stack_recipe("rifle stock", /obj/item/stock, 10, time = 25, one_per_turf = 0, on_floor = 0, required_skills_soft = alist(MECHANICAL_ENGINEERING_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)),
new /datum/stack_recipe("beehive assembly", /obj/item/beehive_assembly, 4),
new /datum/stack_recipe("beehive frame", /obj/item/honey_frame, 1),
new /datum/stack_recipe("book shelf", /obj/structure/bookcase, BUILD_AMT, time = 15, one_per_turf = 1, on_floor = 1),
diff --git a/code/modules/mob/abstract/new_player/new_player.dm b/code/modules/mob/abstract/new_player/new_player.dm
index c1ea0c6b02f..26b32ec4802 100644
--- a/code/modules/mob/abstract/new_player/new_player.dm
+++ b/code/modules/mob/abstract/new_player/new_player.dm
@@ -242,37 +242,31 @@ INITIALIZE_IMMEDIATE(/mob/abstract/new_player)
/mob/abstract/new_player/proc/IsJobAvailable(rank)
var/datum/job/job = SSjobs.GetJob(rank)
- if (!job)
- return FALSE
- if (!job.is_position_available())
- return FALSE
- if (jobban_isbanned(src,rank))
+ // Check if the job is open in-round.
+ if (!job || !job.is_position_available() \
+ /* check for job bans */\
+ || jobban_isbanned(src,rank) \
+ /* check for species blacklists */\
+ || (job.blacklisted_species && (GLOB.all_species[client.prefs.species].name in job.blacklisted_species)) \
+ /* check for citizenship restrictions */\
+ || job.blacklisted_citizenship && (astype(SSrecords.citizenships[client.prefs.citizenship], /datum/citizenship).name in job.blacklisted_citizenship))
return FALSE
- if(job.blacklisted_species) // check for restricted species
- var/datum/species/S = GLOB.all_species[client.prefs.species]
- if(S.name in job.blacklisted_species)
- return FALSE
-
- if(job.blacklisted_citizenship)
- var/datum/citizenship/C = SSrecords.citizenships[client.prefs.citizenship]
- if(C.name in job.blacklisted_citizenship)
- return FALSE
-
+ // Checks for faction requirements.
var/datum/faction/faction = SSjobs.name_factions[client.prefs.faction] || SSjobs.default_faction
var/list/faction_allowed_roles = unpacklist(faction.allowed_role_types)
- if (!(job.type in faction_allowed_roles))
+ if (!(job.type in faction_allowed_roles) \
+ || !faction.can_select(client.prefs,src) \
+ || !(client.prefs.GetPlayerAltTitle(job) in client.prefs.GetValidTitles(job)))
return FALSE
- if(!faction.can_select(client.prefs,src))
- return FALSE
-
- if(!(client.prefs.GetPlayerAltTitle(job) in client.prefs.GetValidTitles(job))) // does age/species check for us!
- return FALSE
+ // Checks for skill requirements.
+ for (var/key,value in job.skill_requirements)
+ if (key && client.prefs.skills[key] < value)
+ return FALSE
return TRUE
-
/mob/abstract/new_player/proc/AttemptLateSpawn(rank,var/spawning_at)
if(src != usr)
return 0
diff --git a/code/modules/mob/living/carbon/human/human_attackhand.dm b/code/modules/mob/living/carbon/human/human_attackhand.dm
index be797e34515..4e5b0e99978 100644
--- a/code/modules/mob/living/carbon/human/human_attackhand.dm
+++ b/code/modules/mob/living/carbon/human/human_attackhand.dm
@@ -175,6 +175,16 @@
var/hit_zone = H.zone_sel.selecting
var/obj/item/organ/external/affecting = get_organ(hit_zone)
+ // Check both "attacker" and "defender" for component based responses to unarmed attacks.
+ var/attacker_skill_level = SKILL_LEVEL_UNFAMILIAR // Lacking any component responses, treat the attacker as 'rank 1'
+ var/defender_skill_level = SKILL_LEVEL_UNFAMILIAR // Lacking any component responses, treat the defender as 'rank 1'
+ var/miss_chance = 15 // Base chance for melee attacks targeting body parts other than the torso to fail.
+ var/block_chance = 20 // Chance to block incoming attacks when in Harm or Grab intent.
+ SEND_SIGNAL(H, COMSIG_UNARMED_HARM_ATTACKER, src, &attacker_skill_level, &miss_chance, &rand_damage, &block_chance)
+ SEND_SIGNAL(src, COMSIG_UNARMED_HARM_DEFENDER, H, &defender_skill_level, &miss_chance, &rand_damage, &block_chance)
+ if (miss_chance < 1)
+ accurate = TRUE
+
if(!affecting || affecting.is_stump())
to_chat(M, SPAN_DANGER("They are missing that limb!"))
return 1
@@ -186,7 +196,7 @@
accurate = 1
if(I_HURT, I_GRAB)
// We're in a fighting stance, there's a chance we block
- if(src.canmove && src!=H && prob(20))
+ if(src.canmove && src!=H && block_chance >= 1 && prob(block_chance))
block = 1
if (M.grabbed_by.len)
@@ -227,9 +237,10 @@
*/
if(prob(80))
hit_zone = ran_zone(hit_zone)
- if(prob(15) && hit_zone != BP_CHEST) // Missed!
+
+ if(prob(miss_chance) && hit_zone != BP_CHEST) // Missed!
if(!src.lying)
- attack_message = "[H] attempted to strike [src], but missed!"
+ attack_message = "[H] attempted to strike [src], [attacker_skill_level - defender_skill_level <= -1 ? "but [src] [pick("dodged", "ducked")] out of the way!" : "but missed!"]"
else
attack_message = "[H] attempted to strike [src], but [src.get_pronoun("he")] rolled out of the way!"
src.set_dir(pick(GLOB.cardinals))
@@ -308,17 +319,24 @@
to_chat(M, SPAN_NOTICE("You don't want to risk hurting [src]!"))
return FALSE
- var/disarm_cost
- var/obj/item/organ/internal/machine/power_core/cell = M.internal_organs_by_name[BP_CELL]
- var/obj/item/cell/potato
- if(cell)
- potato = cell.cell
+ var/attacker_skill_level = SKILL_LEVEL_UNFAMILIAR
+ var/defender_skill_level = SKILL_LEVEL_UNFAMILIAR
+ var/disarm_cost = 0
+ var/push_chance = 25
+ var/disarm_chance = 25
+ SEND_SIGNAL(H, COMSIG_UNARMED_DISARM_ATTACKER, src, &attacker_skill_level, &disarm_cost, &push_chance, &disarm_chance)
+ SEND_SIGNAL(src, COMSIG_UNARMED_DISARM_DEFENDER, H, &defender_skill_level, &disarm_cost, &push_chance, &disarm_chance)
+ var/skill_difference = attacker_skill_level - defender_skill_level
if(isipc(M))
+ var/obj/item/cell/potato = astype(M.internal_organs_by_name[BP_CELL], /obj/item/organ/internal/machine/power_core)?.cell
disarm_cost = potato.maxcharge / 24
if(potato.charge < disarm_cost)
to_chat(M, SPAN_DANGER("You don't have enough charge to disarm someone!"))
return FALSE
+ // Skill difference will be negative if the opponent is stronger than us.
+ if(skill_difference < 0)
+ disarm_cost += -(skill_difference * 50)
potato.use(disarm_cost)
else
if(M.max_stamina > 0)
@@ -330,6 +348,9 @@
if(M.stamina <= disarm_cost)
to_chat(M, SPAN_DANGER("You're too tired to disarm someone!"))
return FALSE
+ // Skill difference will be negative if the opponent is stronger than us.
+ if(skill_difference < 0)
+ disarm_cost = max(0, disarm_cost - (skill_difference * 5))
M.stamina = clamp(M.stamina - disarm_cost, 0, M.max_stamina) // attempting to knock something out of someone's hands, or pushing them over, is exhausting!
else if(M.max_stamina <= 0)
disarm_cost = M.max_nutrition / 6
@@ -353,14 +374,18 @@
//See if they have any weapons to retaliate with
if(src.a_intent != I_HELP)
for(var/obj/item/W in holding)
- if(W && prob(holding[W]))
+ if(!W)
+ continue
+
+ var/misfire_chance = holding[W] + -(skill_difference * 10)
+ if(W && prob(misfire_chance))
if(istype(W, /obj/item/grab))
var/obj/item/grab/G = W
if(G.affecting && G.affecting != M)
visible_message(SPAN_WARNING("[src] repositions \the [G.affecting] to block \the [M]'s disarm attempt!"), SPAN_NOTICE("You reposition \the [G.affecting] to block \the [M]'s disarm attempt!"))
G.attack_hand(M)
return
- if(istype(W,/obj/item/gun))
+ if(istype(W, /obj/item/gun))
var/list/turfs = list()
for(var/turf/T in view())
turfs += T
@@ -370,7 +395,7 @@
return W.afterattack(target,src)
else
if(M.Adjacent(src))
- visible_message(SPAN_DANGER("[src] retaliates against [M]'s disarm attempt with [W]!"))
+ visible_message(SPAN_DANGER("[src] retaliates against [M]'s [attacker_skill_level == SKILL_LEVEL_UNFAMILIAR ? "inexperienced shoving" : "disarm attempt"] with [W]!"))
return M.attackby(W,src)
var/randn = rand(1, 100)
@@ -402,7 +427,7 @@
forceMove(GET_TURF_ABOVE(current_turf)) //We use GET_TURF_ABOVE so people can't cheese it by turning their sprite.
return
- if(randn <= 25)
+ if(randn <= push_chance)
if(H.gloves && istype(H.gloves,/obj/item/clothing/gloves/force))
apply_effect(6, WEAKEN)
playsound(loc, 'sound/weapons/push_connect.ogg', 50, 1, -1)
@@ -423,8 +448,8 @@
playsound(loc, 'sound/weapons/push.ogg', 50, 1, -1)
return
- if(randn <= 60)
- if(H.gloves && istype(H.gloves,/obj/item/clothing/gloves/force))
+ if(randn <= disarm_chance)
+ if(H.gloves && istype(H.gloves, /obj/item/clothing/gloves/force))
playsound(loc, 'sound/weapons/push_connect.ogg', 50, 1, -1)
visible_message(SPAN_DANGER("[M] shoves, sending [src] flying!"))
step_away(src,M,15)
@@ -454,12 +479,14 @@
//No return here is intentional, as it will then try to disarm other items, and/or play a failed disarm message
playsound(loc, SFX_PUNCH_MISS, 25, 1, -1)
- visible_message(SPAN_DANGER("[M] attempted to disarm [src]!"))
+ visible_message(SPAN_DANGER("[M] [attacker_skill_level == SKILL_LEVEL_UNFAMILIAR ? "[pick("inexpertly", "clumsily")] disarm" : "disarm"] attempted to disarm [src]!"))
return
/mob/living/carbon/human/proc/cpr(mob/living/carbon/human/H, var/starting = FALSE, var/cpr_mode)
var/obj/item/main_hand = H.get_active_hand()
var/obj/item/off_hand = H.get_inactive_hand()
+ var/datum/component/skill/medicine/medicine_component = H.GetComponent(MEDICINE_SKILL_COMPONENT)
+ var/medicine_skill = medicine_component ? medicine_component.skill_level : SKILL_LEVEL_TRAINED
if(istype(main_hand) || istype(off_hand))
cpr = FALSE
to_chat(H, SPAN_NOTICE("You cannot perform CPR with anything in your hands."))
@@ -478,7 +505,12 @@
if(!cpr_mode)
cpr = FALSE
return
- to_chat(H, SPAN_NOTICE("You begin performing [cpr_mode] on \the [src]."))
+
+ var/cpr_attempt_message = SPAN_DANGER("You have no idea how to perform CPR on \the [src]... but you're going to try your best!")
+ if(medicine_skill > SKILL_LEVEL_UNFAMILIAR)
+ cpr_attempt_message = SPAN_NOTICE("You begin performing [cpr_mode] on \the [src].")
+
+ to_chat(H, cpr_attempt_message)
H.do_attack_animation(src, null, image('icons/mob/screen/generic.dmi', src, "cpr", src.layer + 1))
var/starting_pixel_y = pixel_y
@@ -490,23 +522,31 @@
cpr = FALSE //If it cancelled, cancel it. Simple.
if(cpr_mode == "Full CPR")
- cpr_compressions(H)
- cpr_ventilation(H)
+ cpr_compressions(H, medicine_skill)
+ cpr_ventilation(H, medicine_skill)
if(cpr_mode == "Compressions")
- cpr_compressions(H)
+ cpr_compressions(H, medicine_skill)
if(cpr_mode == "Mouth-to-Mouth")
- cpr_ventilation(H)
+ cpr_ventilation(H, medicine_skill)
cpr(H, FALSE, cpr_mode) //Again.
-/mob/living/carbon/human/proc/cpr_compressions(mob/living/carbon/human/H)
+/mob/living/carbon/human/proc/cpr_compressions(mob/living/carbon/human/H, medicine_skill)
if(is_asystole())
- if(prob(5 * rand(2, 3)))
+ var/break_probability = 5 * rand(2,3)
+ if(medicine_skill < SKILL_LEVEL_FAMILIAR)
+ break_probability *= 3
+ else if(medicine_skill < SKILL_LEVEL_TRAINED)
+ break_probability *= 1.5
+
+ if(prob(break_probability))
var/obj/item/organ/external/chest = get_organ(BP_CHEST)
if(chest)
chest.fracture()
+ if(medicine_skill < SKILL_LEVEL_FAMILIAR)
+ to_chat(H, FONT_HUGE(SPAN_DANGER("Something crunches under your hands...!")))
var/obj/item/organ/internal/heart/heart = internal_organs_by_name[BP_HEART]
if(heart)
@@ -515,7 +555,7 @@
if(stat != DEAD && prob(10 * rand(0.5, 1)))
resuscitate()
-/mob/living/carbon/human/proc/cpr_ventilation(mob/living/carbon/human/H)
+/mob/living/carbon/human/proc/cpr_ventilation(mob/living/carbon/human/H, medicine_skill)
if(!H.check_has_mouth())
to_chat(H, SPAN_WARNING("You don't have a mouth, you cannot do mouth-to-mouth resuscitation!"))
return
@@ -537,6 +577,12 @@
if(L)
var/datum/gas_mixture/breath = H.get_breath_from_environment()
var/fail = L.handle_breath(breath, 1)
+
+ if(medicine_skill == SKILL_LEVEL_UNFAMILIAR)
+ fail = prob(80)
+ if(fail)
+ to_chat(H, SPAN_DANGER("No... that wasn't how you do it!"))
+
if(!fail)
if(!L.is_bruised() || (L.is_bruised() && L.rescued))
losebreath = 0
diff --git a/code/modules/mob/living/carbon/human/species/species.dm b/code/modules/mob/living/carbon/human/species/species.dm
index e081b07cd2e..7c3d6f250a5 100644
--- a/code/modules/mob/living/carbon/human/species/species.dm
+++ b/code/modules/mob/living/carbon/human/species/species.dm
@@ -449,6 +449,13 @@
/// Which species-unique robolimb types can this species take?
var/list/valid_prosthetics
+ /// Modifiers for the available skill points for this species. Assoc list of SKILL_CATEGORY to number.
+ var/list/skill_points_modifiers = list(
+ SKILL_CATEGORY_EVERYDAY = 1,
+ SKILL_CATEGORY_OCCUPATIONAL = 1,
+ SKILL_CATEGORY_COMBAT = 1
+ )
+
//Sleeping stuff
/// Does this species sleep standing up?
var/sleeps_upright = FALSE
@@ -461,6 +468,9 @@
/// The default lighting alpha of this species. Override to set innate NVGs.
var/default_lighting_alpha = LIGHTING_PLANE_ALPHA_VISIBLE
+ /// Controls whether this species spawns with a Morale Component.
+ var/has_morale = TRUE
+
/datum/species/proc/get_eyes(var/mob/living/carbon/human/H)
return
@@ -591,6 +601,9 @@
if(natural_armor)
H.AddComponent(natural_armor_type, natural_armor)
+ if(has_morale)
+ H.LoadComponent(MORALE_COMPONENT)
+
/datum/species/proc/tap(var/mob/living/carbon/human/H,var/mob/living/target)
if(H.on_fire)
target.fire_stacks += 1
@@ -1080,5 +1093,18 @@
* This proc handles the species temperature regulation. By default, it just adds `passive_temp_gain` to the human's bodytemperature.
* Can be overridden for more complex calculations.
*/
+
/datum/species/proc/handle_temperature_regulation(mob/living/carbon/human/human, seconds_per_tick)
human.bodytemperature += passive_temp_gain * seconds_per_tick
+
+/**
+ * Gets a modifier for a skill category based on the character age or other species things.
+ * Must return a list with all three skill categories to a modifier (example: list(SKILL_CATEGORY_EVERYDAY = 1.5) )
+ */
+/datum/species/proc/modify_skill_points(singleton/skill_category/skill_category, age)
+ var/list/skill_age_modifiers = list(
+ SKILL_CATEGORY_EVERYDAY = 1,
+ SKILL_CATEGORY_OCCUPATIONAL = 1,
+ SKILL_CATEGORY_COMBAT = 1
+ )
+ return skill_age_modifiers
diff --git a/code/modules/overmap/ships/computers/ship.dm b/code/modules/overmap/ships/computers/ship.dm
index 7dd904f37d7..c14847be733 100644
--- a/code/modules/overmap/ships/computers/ship.dm
+++ b/code/modules/overmap/ships/computers/ship.dm
@@ -65,6 +65,13 @@ somewhere on that shuttle. Subtypes of these can be then used to perform ship ov
return ..()
/obj/machinery/computer/ship/attack_hand(mob/user)
+ // Snowflake case for checking player characters for a Pilot Spacecraft Skill.
+ // Only player characters will have the component. Which will both always be present on them, and will only enable its own return logic if it exists.
+ // NPCs, Ghostroles, and Offship Antags that don't generate skills are unaffected by this check by intentional design so that we don't have to account for them.
+ if (user.GetComponent(PILOT_SPACECRAFT_SKILL_COMPONENT)?.skill_level == SKILL_LEVEL_UNFAMILIAR)
+ to_chat(user, SPAN_WARNING("There's just so many buttons... You have no idea where to even begin."))
+ return
+
if(use_check_and_message(user))
return
if(!emagged && !allowed(user))
diff --git a/code/modules/power/fusion/consoles/_consoles.dm b/code/modules/power/fusion/consoles/_consoles.dm
index 6119a80b2d2..e0108dcffbd 100644
--- a/code/modules/power/fusion/consoles/_consoles.dm
+++ b/code/modules/power/fusion/consoles/_consoles.dm
@@ -43,6 +43,11 @@
. = data
/obj/machinery/computer/fusion/ui_interact(mob/user, datum/tgui/ui)
+ var/cancelled = FALSE
+ SEND_SIGNAL(user, COMSIG_USE_REACTOR_COMPUTER, &cancelled)
+ if (cancelled)
+ return
+
ui = SStgui.try_update_ui(user, src, ui)
if (!ui)
ui = new(user, src, ui_template, name)
diff --git a/code/modules/power/singularity/emitter.dm b/code/modules/power/singularity/emitter.dm
index 1c869ff8e54..51c95ff2f42 100644
--- a/code/modules/power/singularity/emitter.dm
+++ b/code/modules/power/singularity/emitter.dm
@@ -105,6 +105,15 @@
/obj/machinery/power/emitter/attack_hand(mob/user)
add_fingerprint(user)
+ var/max_engineering_skill = \
+ max(astype(user.GetComponent(REACTOR_SYSTEMS_SKILL_COMPONENT), SKILL_COMPONENT)?.skill_level, \
+ astype(user.GetComponent(ELECTRICAL_ENGINEERING_SKILL_COMPONENT), SKILL_COMPONENT)?.skill_level)
+
+ if(max_engineering_skill <= SKILL_LEVEL_TRAINED)
+ to_chat(user, SPAN_WARNING("You try to find the switch on \the [src]... How do you even turn this thing on?"))
+ if(!do_after(user, 3 SECONDS + 1.5 SECONDS * (SKILL_LEVEL_TRAINED - max_engineering_skill)))
+ return
+ to_chat(user, SPAN_NOTICE("You finally find the switch!"))
activate(user)
/obj/machinery/power/emitter/proc/activate(mob/user)
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index b56e374d53a..574bf574525 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -292,6 +292,11 @@ ABSTRACT_TYPE(/obj/item/gun)
return
/obj/item/gun/proc/toggle_firing_mode(var/mob/user, var/list/message_mobs)
+ var/cancelled = FALSE
+ SEND_SIGNAL(user, COMSIG_GUN_TOGGLE_FIRING_MODE, src, &cancelled)
+ if (cancelled)
+ return
+
var/datum/firemode/new_mode = switch_firemodes(user)
if(new_mode)
playsound(user, safetyoff_sound, 25)
@@ -433,7 +438,7 @@ ABSTRACT_TYPE(/obj/item/gun)
return TRUE
-/obj/item/gun/proc/Fire(atom/target, mob/living/user, clickparams, pointblank=0, reflex=0, accuracy_decrease=0, is_offhand=0)
+/obj/item/gun/proc/Fire(atom/target, mob/living/user, clickparams, pointblank = 0, reflex = 0, var/accuracy_decrease = 0, is_offhand = 0)
if(!fire_checks(target,user,clickparams,pointblank,reflex))
return FALSE
@@ -443,6 +448,10 @@ ABSTRACT_TYPE(/obj/item/gun)
var/decreased_accuracy = SG.w_class - SG.offhand_accuracy
addtimer(CALLBACK(SG, PROC_REF(Fire), target, user, clickparams, pointblank, reflex, decreased_accuracy, TRUE), 1)
+ /// The amount of extra degrees of firing arc the gun will have from the effects of a signal raised on the user.
+ var/dispersion_increase = 0
+ SEND_SIGNAL(user, COMSIG_BEFORE_GUN_FIRE, &accuracy_decrease, &dispersion_increase)
+
//actually attempt to shoot
var/turf/targloc = get_turf(target) //cache this in case target gets deleted during shooting, e.g. if it was a securitron that got destroyed.
for(var/i in 1 to burst)
@@ -452,7 +461,7 @@ ABSTRACT_TYPE(/obj/item/gun)
break
var/acc = burst_accuracy[min(i, burst_accuracy.len)] - accuracy_decrease
- var/disp = dispersion[min(i, dispersion.len)]
+ var/disp = dispersion[min(i, dispersion.len)] + dispersion_increase
process_accuracy(projectile, user, target, acc, disp)
if(pointblank)
diff --git a/code/modules/reagents/reagent_containers.dm b/code/modules/reagents/reagent_containers.dm
index 1d4e02d3563..7a993468667 100644
--- a/code/modules/reagents/reagent_containers.dm
+++ b/code/modules/reagents/reagent_containers.dm
@@ -248,6 +248,7 @@
user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN) //puts a limit on how fast people can eat/drink things
self_feed_message(user)
reagents.trans_to_mob(user, min(10,amount_per_transfer_from_this), CHEM_INGEST) //A sane limiter. So you don't go drinking 300u all at once.
+ SEND_SIGNAL(src, COMSIG_CONTAINER_DRANK, user)
feed_sound(user)
return TRUE
else
@@ -350,8 +351,8 @@
var/trans = reagents.trans_to(target, amount_per_transfer_from_this)
to_chat(user, SPAN_NOTICE("You transfer [trans] units of the solution to [target]."))
- on_pour()
+ on_pour(target)
return 1
-/obj/item/reagent_containers/proc/on_pour()
+/obj/item/reagent_containers/proc/on_pour(atom/target)
playsound(src, SFX_POUR, 25, 1)
diff --git a/code/modules/reagents/reagent_containers/food/drinks.dm b/code/modules/reagents/reagent_containers/food/drinks.dm
index 2504f2c9a5a..e44148b153c 100644
--- a/code/modules/reagents/reagent_containers/food/drinks.dm
+++ b/code/modules/reagents/reagent_containers/food/drinks.dm
@@ -368,6 +368,9 @@ If you add a drink with an empty icon sprite, ensure it is in the same folder, e
var/twisted = FALSE
var/obj/item/shaker_top/top
var/obj/item/reagent_containers/food/drinks/shaker_cup/cap
+ var/drink_quality = null
+ var/drink_moodlet_value = null
+ var/moodlet_value_per_bartending_rank = 5
/obj/item/reagent_containers/food/drinks/shaker/mechanics_hints(mob/user, distance, is_adjacent)
. += ..()
@@ -429,6 +432,16 @@ If you add a drink with an empty icon sprite, ensure it is in the same folder, e
if(last_shake <= world.time - 10) //Spam limiter.
last_shake = world.time
playsound(src.loc, SFX_SHAKER_SHAKING, 50, 1)
+
+ /**
+ ------------ Bartender Skill Handling ------------
+ */
+ var/datum/component/skill/bartending/bar_skill = user.GetComponent(BARTENDING_SKILL_COMPONENT)
+ if (bar_skill)
+ drink_moodlet_value = moodlet_value_per_bartending_rank * bar_skill.skill_level
+ if (isnull(drink_quality))
+ drink_quality = rand(1, 20) + 3 * bar_skill.skill_level
+
src.add_fingerprint(user)
return
@@ -464,7 +477,15 @@ If you add a drink with an empty icon sprite, ensure it is in the same folder, e
return TRUE
return ..()
-/obj/item/reagent_containers/food/drinks/shaker/on_pour()
+/obj/item/reagent_containers/food/drinks/shaker/on_pour(atom/target)
+ if (!isnull(drink_moodlet_value))
+ target.LoadComponent(/datum/component/drink_moodlet_provider, drink_moodlet_value, FALSE, drink_quality)
+
+ // Bartender has finished pouring a drink, prepare the shaker to make the next one.
+ if (!reagents.total_volume)
+ drink_quality = null
+ drink_moodlet_value = null
+
if(!top)
playsound(src, 'sound/effects/pour_big.ogg', 50, 1)
return
diff --git a/code/modules/shuttles/shuttle_console.dm b/code/modules/shuttles/shuttle_console.dm
index 75ada22dd0f..2a61c5203a3 100644
--- a/code/modules/shuttles/shuttle_console.dm
+++ b/code/modules/shuttles/shuttle_console.dm
@@ -15,6 +15,11 @@
/// For hotwiring, how many cycles are needed. This decreases by 1 each cycle and triggers at 0
var/hotwire_progress = 8
+ /// Skill needed to use this computer.
+ var/required_skill = PILOT_SPACECRAFT_SKILL_COMPONENT
+ /// Skill level needed to use this computer, see required_skill
+ var/required_skill_level = SKILL_LEVEL_FAMILIAR
+
/obj/machinery/computer/shuttle_control/feedback_hints(mob/user, distance, is_adjacent)
. += ..()
if(initial(hotwire_progress) != hotwire_progress)
@@ -89,6 +94,15 @@
return ..()
/obj/machinery/computer/shuttle_control/attack_hand(mob/user)
+ // Snowflake case for checking player characters for a Pilot Spacecraft Skill.
+ // Only player characters will have the component. Which will both always be present on them, and will only enable its own return logic if it exists.
+ // NPCs, Ghostroles, and Offship Antags that don't generate skills are unaffected by this check by intentional design so that we don't have to account for them.
+ if (required_skill)
+ var/datum/component/skill/skill_check = user.GetComponent(astype(required_skill, SKILL_COMPONENT))
+ if (skill_check && skill_check.skill_level < required_skill_level)
+ to_chat(user, SPAN_WARNING("There's just so many buttons... You have no idea where to even begin."))
+ return
+
if(use_check_and_message(user))
return
if(!emagged && !allowed(user))
diff --git a/code/modules/shuttles/shuttle_console_multi_lift.dm b/code/modules/shuttles/shuttle_console_multi_lift.dm
index 89ff63185fa..f030962a976 100644
--- a/code/modules/shuttles/shuttle_console_multi_lift.dm
+++ b/code/modules/shuttles/shuttle_console_multi_lift.dm
@@ -6,6 +6,7 @@
density = FALSE
req_access = null
ui_template = "ShuttleControlConsoleMultiLift"
+ required_skill = null
/obj/machinery/computer/shuttle_control/multi/lift/wall
icon_state = "lift_wall"
diff --git a/code/modules/surgery/bones.dm b/code/modules/surgery/bones.dm
index 8596f20ea93..3b13a0a5a5d 100644
--- a/code/modules/surgery/bones.dm
+++ b/code/modules/surgery/bones.dm
@@ -14,6 +14,7 @@
min_duration = 30
max_duration = 40
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/glue_bone/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -50,6 +51,7 @@
min_duration = 30
max_duration = 50
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/set_bone/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -90,6 +92,7 @@
min_duration = 40
max_duration = 50
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/mend_skull/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -127,6 +130,7 @@
min_duration = 30
max_duration = 40
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/finish_bone/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
diff --git a/code/modules/surgery/encased.dm b/code/modules/surgery/encased.dm
index a0f872e099b..73c69353a70 100644
--- a/code/modules/surgery/encased.dm
+++ b/code/modules/surgery/encased.dm
@@ -7,6 +7,7 @@
priority = 2
can_infect = TRUE
blood_level = 1
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/open_encased/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -73,6 +74,7 @@
min_duration = 20
max_duration = 30
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/open_encased/retract/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -127,6 +129,7 @@
min_duration = 20
max_duration = 30
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/open_encased/close/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -185,6 +188,7 @@
min_duration = 20
max_duration = 30
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/open_encased/mend/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
diff --git a/code/modules/surgery/face.dm b/code/modules/surgery/face.dm
index ad7815de60b..754c1ffc82e 100644
--- a/code/modules/surgery/face.dm
+++ b/code/modules/surgery/face.dm
@@ -12,6 +12,7 @@
min_duration = 70
max_duration = 90
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/generic/cut_face/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target_zone == BP_MOUTH && target.op_stage.face == FACE_NORMAL
@@ -42,6 +43,7 @@
min_duration = 70
max_duration = 90
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/robotics/face/synthskin/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target.op_stage.face == FACE_NORMAL && target.get_species() == SPECIES_IPC_SHELL
diff --git a/code/modules/surgery/facial_surgery.dm b/code/modules/surgery/facial_surgery.dm
index 6bb7f408ae1..430ea28999f 100644
--- a/code/modules/surgery/facial_surgery.dm
+++ b/code/modules/surgery/facial_surgery.dm
@@ -8,6 +8,7 @@
name = "Retract Facial Incisions"
priority = 2
can_infect = FALSE
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/face/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -25,6 +26,7 @@
min_duration = 70
max_duration = 90
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/generic/prepare_face/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target_zone == BP_MOUTH && target.op_stage.face == FACE_CUT_OPEN
@@ -57,6 +59,7 @@
min_duration = 30
max_duration = 70
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/generic/alter_face/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target_zone == BP_MOUTH && target.op_stage.face == FACE_RETRACTED
@@ -104,6 +107,7 @@
min_duration = 50
max_duration = 80
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/face/cauterize/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target.op_stage.face > FACE_NORMAL
@@ -147,6 +151,7 @@
min_duration = 70
max_duration = 90
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/robotics/face/synthskinopen/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target.op_stage.face == FACE_NORMAL && target.get_species() == SPECIES_IPC_SHELL
@@ -175,6 +180,7 @@
min_duration = 70
max_duration = 90
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/robotics/face/prepare_face/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target_zone == BP_MOUTH && target.op_stage.face == FACE_CUT_OPEN
@@ -204,6 +210,7 @@
min_duration = 30
max_duration = 70
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/robotics/face/alter_synthface/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target_zone == BP_MOUTH && target.op_stage.face == FACE_RETRACTED
@@ -249,6 +256,7 @@
min_duration = 50
max_duration = 80
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/robotics/face/seal_face/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
return ..() && target.op_stage.face > FACE_NORMAL
diff --git a/code/modules/surgery/generic.dm b/code/modules/surgery/generic.dm
index 47bad275177..c8f5a985670 100644
--- a/code/modules/surgery/generic.dm
+++ b/code/modules/surgery/generic.dm
@@ -30,6 +30,8 @@
priority = 2
min_duration = 50
max_duration = 70
+ // Basic procedure any novice can do
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/generic/cut_with_laser/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -73,6 +75,8 @@
priority = 2
min_duration = 60
max_duration = 80
+ // Automated tool that requires no skill
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_UNFAMILIAR)
/singleton/surgery_step/generic/incision_manager/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -117,6 +121,8 @@
min_duration = 60
max_duration = 80
+ // Basic surgery any novice surgeon can do
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/generic/cut_open/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -161,6 +167,8 @@
min_duration = 80
max_duration = 100
+ // Basic surgery any novice surgeon can do
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/generic/cut_open_vaurca/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -205,6 +213,8 @@
min_duration = 20
max_duration = 30
+ // Basic surgery any novice surgeon can do
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/generic/clamp_bleeders/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -244,6 +254,8 @@
min_duration = 20
max_duration = 30
+ // Basic surgery any novice surgeon can do
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/generic/retract_skin/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -307,6 +319,8 @@
min_duration = 40
max_duration = 70
+ // Basic surgery any novice surgeon can do
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/generic/cauterize/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
diff --git a/code/modules/surgery/implant.dm b/code/modules/surgery/implant.dm
index b39aea2bf51..a22c0d01d5e 100644
--- a/code/modules/surgery/implant.dm
+++ b/code/modules/surgery/implant.dm
@@ -6,6 +6,7 @@
/singleton/surgery_step/cavity
priority = 1
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/cavity/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -49,6 +50,7 @@
min_duration = 50
max_duration = 70
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/cavity/make_space/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -81,6 +83,7 @@
min_duration = 50
max_duration = 70
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/cavity/close_space/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -108,6 +111,7 @@
min_duration = 60
max_duration = 80
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/cavity/place_item/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -159,6 +163,7 @@
min_duration = 60
max_duration = 80
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/cavity/implant_removal/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
diff --git a/code/modules/surgery/limb_reattach.dm b/code/modules/surgery/limb_reattach.dm
index 686b6ae9825..152f25032ee 100644
--- a/code/modules/surgery/limb_reattach.dm
+++ b/code/modules/surgery/limb_reattach.dm
@@ -6,6 +6,7 @@
/singleton/surgery_step/limb
priority = 3 // Must be higher than /singleton/surgery_step/internal
can_infect = FALSE
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/limb/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -27,6 +28,7 @@
min_duration = 40
max_duration = 60
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/limb/attach/begin_step(mob/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
var/obj/item/organ/external/E = tool
@@ -60,6 +62,7 @@
min_duration = 80
max_duration = 100
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/limb/connect/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -98,6 +101,7 @@
min_duration = 60
max_duration = 80
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/limb/mechanize/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
diff --git a/code/modules/surgery/organs_internal.dm b/code/modules/surgery/organs_internal.dm
index 5ba79b6ad78..0fc05226843 100644
--- a/code/modules/surgery/organs_internal.dm
+++ b/code/modules/surgery/organs_internal.dm
@@ -3,6 +3,7 @@
priority = 2
can_infect = TRUE
blood_level = 1
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -28,6 +29,7 @@
min_duration = 50
max_duration = 70
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/fix_organ/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -116,6 +118,7 @@
min_duration = 50
max_duration = 70
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/fix_organ_robotic/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -187,6 +190,7 @@
min_duration = 70
max_duration = 90
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/detach_organ/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -253,6 +257,7 @@
min_duration = 40
max_duration = 60
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/remove_organ/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -312,6 +317,7 @@
min_duration = 40
max_duration = 60
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/replace_organ/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -417,6 +423,7 @@
min_duration = 80
max_duration = 100
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/attach_organ/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -471,6 +478,7 @@
min_duration = 80
max_duration = 100
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/prepare/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
diff --git a/code/modules/surgery/other.dm b/code/modules/surgery/other.dm
index 15e3cb29eb8..768c582b8a0 100644
--- a/code/modules/surgery/other.dm
+++ b/code/modules/surgery/other.dm
@@ -16,6 +16,7 @@
min_duration = 40
max_duration = 60
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/fix_vein/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -65,6 +66,7 @@
min_duration = 80
max_duration = 130
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_PROFESSIONAL)
/singleton/surgery_step/internal/fix_dead_tissue/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -130,6 +132,7 @@
min_duration = 80
max_duration = 90
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_PROFESSIONAL)
/singleton/surgery_step/treat_necrosis/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -196,6 +199,7 @@
min_duration = 50
max_duration = 70
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/fix_tendon/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -237,6 +241,7 @@
min_duration = 100
max_duration = 160
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/hardsuit/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -274,6 +279,7 @@
min_duration = 90
max_duration = 140
+ skill_requirements = alist(SURGERY_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/amputate/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
diff --git a/code/modules/surgery/robotics.dm b/code/modules/surgery/robotics.dm
index 2d83360d4ba..23a772295b5 100644
--- a/code/modules/surgery/robotics.dm
+++ b/code/modules/surgery/robotics.dm
@@ -5,6 +5,7 @@
/singleton/surgery_step/robotics
can_infect = FALSE
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/robotics/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -34,6 +35,8 @@
max_duration = 70
requires_surgery_compatibility = FALSE
+ // Basic surgery any novice roboticist can do
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/robotics/unscrew_hatch/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -76,6 +79,8 @@
max_duration = 70
requires_surgery_compatibility = FALSE
+ // Basic surgery any novice roboticist can do
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/robotics/screw_hatch/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -116,6 +121,8 @@
min_duration = 20
max_duration = 30
+ // Basic surgery any novice roboticist can do
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/robotics/open_hatch/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -153,6 +160,8 @@
min_duration = 50
max_duration = 80
+ // Basic surgery any novice roboticist can do
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/robotics/close_hatch/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -193,6 +202,8 @@
min_duration = 30
max_duration = 40
+ // Basic surgery any novice roboticist can do
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/robotics/repair_brute/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -237,6 +248,8 @@
min_duration = 30
max_duration = 40
+ // Basic surgery any novice roboticist can do
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/robotics/repair_burn/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -280,6 +293,7 @@
min_duration = 70
max_duration = 90
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/robotics/detach_organ_robotic/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -332,6 +346,7 @@
max_duration = 100
requires_surgery_compatibility = FALSE
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/robotics/attach_organ_robotic/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -381,6 +396,7 @@
min_duration = 40
max_duration = 60
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/robotics/install_mmi/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -456,6 +472,7 @@
min_duration = 50
max_duration = 70
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/fix_internal_wiring/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -505,6 +522,7 @@
min_duration = 100
max_duration = 200
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/fix_internal_electronics/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -545,6 +563,7 @@
min_duration = 75
max_duration = 120
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/fix_internal_plating/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -596,6 +615,7 @@
min_duration = 100
max_duration = 150
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/replace_internal_plating/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -641,6 +661,7 @@
max_duration = 200
var/fast_repair = FALSE
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/replace_external_plating/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -681,12 +702,14 @@
min_duration = 100
max_duration = 150
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_FAMILIAR)
/singleton/surgery_step/internal/replace_external_plating/g2
name = "Replace G2 External Armour Plating"
min_duration = 90
max_duration = 100
fast_repair = TRUE
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/internal/degunk/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
@@ -731,6 +754,7 @@
min_duration = 15
max_duration = 25
+ skill_requirements = alist(ROBOTICS_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/robotics/repair_endoskeleton/can_use(mob/living/user, mob/living/carbon/human/target, target_zone, obj/item/tool)
if(!..())
diff --git a/code/modules/surgery/slimes.dm b/code/modules/surgery/slimes.dm
index 3f448b9d81b..7fb196301e0 100644
--- a/code/modules/surgery/slimes.dm
+++ b/code/modules/surgery/slimes.dm
@@ -19,6 +19,7 @@
min_duration = 10
max_duration = 35
+ skill_requirements = alist(XENOBIOLOGY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/slime/cut_flesh/can_use(mob/living/user, mob/living/carbon/slime/target, target_zone, obj/item/tool)
return ..() && istype(target) && target.core_removal_stage == ORGAN_CLOSED
@@ -47,6 +48,7 @@
min_duration = 20
max_duration = 45
+ skill_requirements = alist(XENOBIOLOGY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/slime/cut_innards/can_use(mob/living/user, mob/living/carbon/slime/target, target_zone, obj/item/tool)
return ..() && istype(target) && target.core_removal_stage == ORGAN_OPEN_INCISION
@@ -74,6 +76,7 @@
min_duration = 35
max_duration = 55
+ skill_requirements = alist(XENOBIOLOGY_SKILL_COMPONENT = SKILL_LEVEL_TRAINED)
/singleton/surgery_step/slime/saw_core/can_use(mob/living/user, mob/living/carbon/slime/target, target_zone, obj/item/tool)
return ..() && (istype(target) && target.core_removal_stage == ORGAN_OPEN_RETRACTED && target.cores > 0) //This is being passed a human as target, unsure why.
diff --git a/code/modules/surgery/surgery.dm b/code/modules/surgery/surgery.dm
index e485b741ab3..83e3699e0ee 100644
--- a/code/modules/surgery/surgery.dm
+++ b/code/modules/surgery/surgery.dm
@@ -21,6 +21,9 @@
var/requires_surgery_compatibility = TRUE
+ /// The associative list of skills and their paired requirement levels to be able to perform a given surgery.
+ var/alist/skill_requirements
+
/// Returns how well tool is suited for this step.
/singleton/surgery_step/proc/tool_quality(obj/item/tool)
for(var/T in allowed_tools)
@@ -128,6 +131,14 @@
if(user.client) // In case of future autodocs.
S = tgui_input_list(user, "Which surgery would you like to perform?", "Surgery", possible_surgeries)
+ // Check via skill components if the user knows how to perform the surgery.
+ for (var/skill_comp, required_level in S.skill_requirements)
+ var/skill_level = GET_SKILL_LEVEL(user, skill_comp)
+ // Null condition handles NPCs and Antags that won't have the skill setup.
+ if (!isnull(skill_level) && skill_level < required_level)
+ to_chat(user, SPAN_WARNING("You lack the skills needed to perform this surgical procedure."))
+ return TRUE
+
// We didn't find a surgery, or decided not to perform one.
if(!istype(S))
if(tool.item_flags & ITEM_FLAG_SURGERY) //Is this supposed to be used for surgery?
diff --git a/html/changelogs/hellfirejag - Skills System.yml b/html/changelogs/hellfirejag - Skills System.yml
new file mode 100644
index 00000000000..e23b95d1fd3
--- /dev/null
+++ b/html/changelogs/hellfirejag - Skills System.yml
@@ -0,0 +1,4 @@
+author: Hellfirejag and Matt Atlas
+delete-after: True
+changes:
+ - rscadd: "Added a basic initial implementation of a Mechanical Skills System."