diff --git a/SQL/migrate-2023/V011__skills_education.sql b/SQL/migrate-2023/V011__skills_education.sql new file mode 100644 index 00000000000..4ba8a8996f1 --- /dev/null +++ b/SQL/migrate-2023/V011__skills_education.sql @@ -0,0 +1,8 @@ +-- +-- Implemented in PR #?????. +-- 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`; +ALTER TABLE `ss13_characters` DROP COLUMN `home_system`; diff --git a/aurorastation.dme b/aurorastation.dme index 759cb38da8d..8614dd5c5b5 100644 --- a/aurorastation.dme +++ b/aurorastation.dme @@ -129,6 +129,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" @@ -363,6 +364,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" @@ -537,6 +539,13 @@ #include "code\datums\repositories\singletons.dm" #include "code\datums\repositories\sound_channels.dm" #include "code\datums\repositories\unique.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\science.dm" #include "code\datums\state_machine\state.dm" #include "code\datums\state_machine\transition.dm" #include "code\datums\tips\tips.dm" @@ -1699,6 +1708,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" @@ -1864,6 +1880,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" @@ -2830,6 +2847,7 @@ #include "code\modules\mob\living\simple_animal\hostile\retaliate\retaliate.dm" #include "code\modules\mob\living\simple_animal\hostile\toy\mech.dm" #include "code\modules\mob\living\simple_animal\mechanical\mechanical.dm" +#include "code\modules\mob\skills\skills.dm" #include "code\modules\modular_computers\_description.dm" #include "code\modules\modular_computers\laptop_vendor.dm" #include "code\modules\modular_computers\computers\modular_computer\core.dm" diff --git a/code/__DEFINES/skills.dm b/code/__DEFINES/skills.dm new file mode 100644 index 00000000000..f9a6a4c7759 --- /dev/null +++ b/code/__DEFINES/skills.dm @@ -0,0 +1,32 @@ +/// 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" + +///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 BASE_SKILL_POINTS_COMBAT 4 +#define BASE_SKILL_POINTS_OCCUPATIONAL 8 +#define BASE_SKILL_POINTS_EVERYDAY 8 + +#define SKILL_DIFFICULTY_MODIFIER_EASY 1 +#define SKILL_DIFFICULTY_MODIFIER_MEDIUM 2 +#define SKILL_DIFFICULTY_MODIFIER_HARD 4 diff --git a/code/controllers/subsystems/skills.dm b/code/controllers/subsystems/skills.dm new file mode 100644 index 00000000000..b15d4e9936e --- /dev/null +++ b/code/controllers/subsystems/skills.dm @@ -0,0 +1,34 @@ +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() + /// A map of all the skill levels to their definition. + var/list/skill_level_map = list( + "Unfamiliar" = "You don't know anything about this subject.", + "Familiar" = "You're familiar with this subject, either by reading into it or by doing some courses.", + "Trained" = "You've been formally trained in this subject. Typically, this is the minimum level for a job.", + "Professional" = "You have a lot of training and a good amount of experience in this subject." + ) + +/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 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 in GET_SINGLETON_SUBTYPE_LIST(/singleton/skill)) + 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/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..1a9a558417a --- /dev/null +++ b/code/datums/skills/_skills.dm @@ -0,0 +1,49 @@ +/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/list/skill_level_descriptions = list( + 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." + ) + /// 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 + /// The modifier for how difficult the skill is. Each level costs this much * the level. + var/skill_difficulty_modifier = SKILL_DIFFICULTY_MODIFIER_MEDIUM + +/** + * Returns the maximum level a character can have in this skill depending on education. + */ +/singleton/skill/proc/get_maximum_level(var/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) + if(level == SKILL_LEVEL_UNFAMILIAR) //thanks byond for not supporting index 0 + return 0 + return skill_difficulty_modifier * level diff --git a/code/datums/skills/combat/combat.dm b/code/datums/skills/combat/combat.dm new file mode 100644 index 00000000000..4e36eaef643 --- /dev/null +++ b/code/datums/skills/combat/combat.dm @@ -0,0 +1,25 @@ +/singleton/skill/unarmed_combat + name = "Unarmed Combat" + description = "Unarmed combat represents your training in hand-to-hand combat, or without a weapon." + skill_level_descriptions = list( + SKILL_LEVEL_UNFAMILIAR = "You have rarely, if ever, fought someone in your life. You are likely to crack under pressure, not land punches, and are physically \ + a pushover in a real fight. Being shoved, grabbed, or moved is likely to be very dangerous for you.", + 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 not as likely to miss punches, and you are physically less likely to suffer being shoved, grabbed, or moved.", + 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.", + ) + category = /singleton/skill_category/combat + subcategory = SKILL_SUBCATEGORY_MELEE + +/singleton/skill/armed_combat + name = "Armed Combat" + description = "zomboid time" + category = /singleton/skill_category/combat + subcategory = SKILL_SUBCATEGORY_MELEE + +/singleton/skill/firearms + name = "Firearms" + description = "using guns. split this into close arms/longarms/special arms?" + category = /singleton/skill_category/combat + subcategory = SKILL_SUBCATEGORY_RANGED diff --git a/code/datums/skills/everyday/service.dm b/code/datums/skills/everyday/service.dm new file mode 100644 index 00000000000..47ac4cdc720 --- /dev/null +++ b/code/datums/skills/everyday/service.dm @@ -0,0 +1,20 @@ +/singleton/skill/mixing + name = "Mixing" + description = "valhalla skill" + maximum_level = SKILL_LEVEL_PROFESSIONAL + category = /singleton/skill_category/everyday + subcategory = SKILL_SUBCATEGORY_SERVICE + +/singleton/skill/cooking + name = "Cooking" + description = "Cooking Mama" + maximum_level = SKILL_LEVEL_PROFESSIONAL + category = /singleton/skill_category/everyday + subcategory = SKILL_SUBCATEGORY_SERVICE + +/singleton/skill/gardening + name = "Gardening" + description = "this is boring as shit" + maximum_level = SKILL_LEVEL_PROFESSIONAL + category = /singleton/skill_category/everyday + subcategory = SKILL_SUBCATEGORY_SERVICE diff --git a/code/datums/skills/occupational/engineering.dm b/code/datums/skills/occupational/engineering.dm new file mode 100644 index 00000000000..91da1afcf75 --- /dev/null +++ b/code/datums/skills/occupational/engineering.dm @@ -0,0 +1,35 @@ +/singleton/skill/electrical_engineering + name = "Electrical Engineering" + description = "Electrical engineering has to do with anything that involves wires and electricity. This includes things such as hacking doors, machines, but also \ + laying down wires, or repairing wiring damage in prosthetics." + maximum_level = SKILL_LEVEL_PROFESSIONAL + uneducated_skill_cap = SKILL_LEVEL_FAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_ENGINEERING + +/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." + maximum_level = SKILL_LEVEL_PROFESSIONAL + uneducated_skill_cap = SKILL_LEVEL_FAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_ENGINEERING + +/singleton/skill/atmospherics_systems + name = "Atmospherics Systems" + description = "Atmospherics systems involves the usage of atmospherics tooling and machinery, such as powered pumps, certain settings on air alarms, pipe wrenches, \ + pipe layers, and pipe construction." + maximum_level = SKILL_LEVEL_PROFESSIONAL + uneducated_skill_cap = SKILL_LEVEL_UNFAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_ENGINEERING + +/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_UNFAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_ENGINEERING diff --git a/code/datums/skills/occupational/medical.dm b/code/datums/skills/occupational/medical.dm new file mode 100644 index 00000000000..7acaa54cf10 --- /dev/null +++ b/code/datums/skills/occupational/medical.dm @@ -0,0 +1,37 @@ +/singleton/skill/medicine + name = "Medicine" + description ="how to use health analyzers, ATKs, syringes" + maximum_level = SKILL_LEVEL_PROFESSIONAL + uneducated_skill_cap = SKILL_LEVEL_FAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_MEDICAL + +/singleton/skill/surgery + name = "Surgery" + description = "the one everyone wants to do in medical to be the protag" + maximum_level = SKILL_LEVEL_PROFESSIONAL + uneducated_skill_cap = SKILL_LEVEL_UNFAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_MEDICAL + +/singleton/skill/pharmacology + name = "Pharmacology" + description = "why are you playing pharma LOL" + uneducated_skill_cap = SKILL_LEVEL_UNFAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_MEDICAL + +/singleton/skill/anatomy + name = "Anatomy" + description = "this one lets you know what's wrong with people" + maximum_level = SKILL_LEVEL_PROFESSIONAL + uneducated_skill_cap = SKILL_LEVEL_FAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_MEDICAL + +/singleton/skill/forensics + name = "Forensics" + description = "forensics shit i guess" + uneducated_skill_cap = SKILL_LEVEL_UNFAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_MEDICAL diff --git a/code/datums/skills/occupational/science.dm b/code/datums/skills/occupational/science.dm new file mode 100644 index 00000000000..e02f8a361fb --- /dev/null +++ b/code/datums/skills/occupational/science.dm @@ -0,0 +1,37 @@ +/singleton/skill/research + name = "Research" + description = "it's generic research shit, using the protolathe, destructive analyzers, etc" + uneducated_skill_cap = SKILL_LEVEL_UNFAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_SCIENCE + +/singleton/skill/robotics + name = "Robotics" + description = "Robotics is well you know what the fuck it is man" + maximum_level = SKILL_LEVEL_PROFESSIONAL + uneducated_skill_cap = SKILL_LEVEL_FAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_SCIENCE + +/singleton/skill/xenobotany + name = "Xenobotany" + description = "Xenobotany is the creation and study of new or alien genomes of plants. It is necessary to be able to properly splice and process them." + uneducated_skill_cap = SKILL_LEVEL_UNFAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_SCIENCE + +/singleton/skill/archaeology + name = "Xenoarchaeology" + description = "Xenoarchaeology is the study of alien civilizations, artifacts, architecture, and so on. It is necessary for the unearthing and cataloguing of \ + alien artifacts." + uneducated_skill_cap = SKILL_LEVEL_UNFAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_SCIENCE + +/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." + uneducated_skill_cap = SKILL_LEVEL_UNFAMILIAR + category = /singleton/skill_category/occupational + subcategory = SKILL_SUBCATEGORY_SCIENCE diff --git a/code/modules/background/education/_education.dm b/code/modules/background/education/_education.dm new file mode 100644 index 00000000000..b4fadedcf6e --- /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 + /// The jobs this education type allows you to access. + var/list/jobs + /// The given skills for this education type. Linked list of skill type to level. + var/list/skills + /// 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 + +/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..508d6355020 --- /dev/null +++ b/code/modules/background/education/engineering.dm @@ -0,0 +1,86 @@ +/singleton/education/mechanical_engineering_degree + name = "Mechanical Engineering Degree" + description = "You are at least 25 years of age, with a Bachelor's degree in Mechanical Engineering. 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 Degree" + description = "You are at least 25 years of age, with a Bachelor's degree in Electrical Engineering. You specialize in cutting wires, electronic circuits, 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 Degree" + description = "You are at least 25 years of age, with a Bachelor's degree in Atmospherics Systems. 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 Degree" + description = "You are at least 25 years of age, with a Bachelor's degree in Reactor Systems. You specialize in everything to do with reactors systems, \ + whether you are looking at a Supermatter, an INDRA 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, but you had enough 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..58db4c6991d --- /dev/null +++ b/code/modules/background/education/medical.dm @@ -0,0 +1,75 @@ +/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/medicine = SKILL_LEVEL_TRAINED, + /singleton/skill/anatomy = SKILL_LEVEL_TRAINED + ) + +/singleton/education/psychologist_degree + name = "Psychology PhD" + description = "You are at least 30 years of age, with a PhD from an accredited university in an applicable field." + jobs = list("Psychologist") + minimum_character_age = list( + SPECIES_HUMAN = 25, + 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/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( + /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..d2986659294 --- /dev/null +++ b/code/modules/background/education/misc.dm @@ -0,0 +1,19 @@ +/singleton/education/finance + name = "Finance Degree" + description = "You are at least 21 years of age. You graduated in a field related to finance, whether that is Business Management or something else. \ + 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 Degree" + description = "You are at least 21 years of age. You graduated in a field related to the humanities, whether that is music, arts, linguistics, or a bachelor's in \ + psychology. 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..218ff30e23e --- /dev/null +++ b/code/modules/background/education/science.dm @@ -0,0 +1,81 @@ +/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 more basic 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/surgery = SKILL_LEVEL_FAMILIAR, + /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/surgery = SKILL_LEVEL_FAMILIAR, + /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..ed51254aaba --- /dev/null +++ b/code/modules/background/education/security.dm @@ -0,0 +1,24 @@ +/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." + jobs = list("Investigator") + minimum_character_age = list( + SPECIES_HUMAN = 25, + SPECIES_SKRELL = 60, + SPECIES_SKRELL_AXIORI = 60 + ) + skills = list( + /singleton/skill/surgery = SKILL_LEVEL_FAMILIAR, + /singleton/skill/medicine = SKILL_LEVEL_FAMILIAR, + /singleton/skill/anatomy = SKILL_LEVEL_FAMILIAR, + /singleton/skill/forensics = SKILL_LEVEL_PROFESSIONAL + ) + +/singleton/education/protagonist + name = "Protagonist Degree" + description = "you are the protagonist of aurora" + skills = list( + /singleton/skill/unarmed_combat = SKILL_LEVEL_TRAINED, + /singleton/skill/armed_combat = SKILL_LEVEL_TRAINED, + /singleton/skill/firearms = SKILL_LEVEL_TRAINED + ) diff --git a/code/modules/background/education/service.dm b/code/modules/background/education/service.dm new file mode 100644 index 00000000000..33c2327f559 --- /dev/null +++ b/code/modules/background/education/service.dm @@ -0,0 +1,67 @@ +/singleton/education/mixing + name = "Mixing License" + description = "You paid for and successfully attained an Idris mixing license, making you officially 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/mixing = SKILL_LEVEL_PROFESSIONAL, + ) + +/singleton/education/cooking_degree + name = "Culinary Arts Degree" + description = "You obtained a degree in Culinary Arts, making you an artist at cooking. 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 Certification" + description = "You obtained an Idris certification to work as a cook. 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 Degree" + description = "You obtained a degree to work as a hydroponicist or gardener. Your degree 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 Certification" + description = "You obtained an Idris certification to work as a hydroponicist or gardener. 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 4004d45da5a..d54a029677a 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/origin/origin.dm b/code/modules/client/preference_setup/origin/origin.dm index 378230ebbfd..02ab8293337 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,6 +119,7 @@ if(OR.important_information) dat += "
- [OR.important_information]" dat += "
" + dat += "Economic Status: [pref.economic_status]
" dat += "Citizenship: [pref.citizenship]
" dat += "Religion: [pref.religion]
" @@ -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 9ee8f2aa9e2..f05c198e2bd 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..1eff73a7bb2 --- /dev/null +++ b/code/modules/client/preference_setup/skills/skills.dm @@ -0,0 +1,318 @@ +/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 + sanitized_skills[skill.type] = pref.skills[S] + + 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 + loaded_skills = json_decode(pref.skills) + catch (var/exception/e) + log_debug("SKILLS: Caught [e]. Initial value: [before]") + pref.skills = list() + + pref.skills = list() + for(var/new_skill in loaded_skills) + var/singleton/skill/skill = GET_SINGLETON(text2path(new_skill)) + if(istype(skill)) + pref.skills[skill.type] = loaded_skills[new_skill] + +/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 += "" + for(var/subcategory in SSskills.skill_tree[skill_category]) + dat += "" + for(var/singleton/skill/skill in SSskills.skill_tree[skill_category][subcategory]) + dat += get_skill_row(skill, education) + dat += "
[skill_category.name] ([calculate_remaining_skill_points(skill_category)] points remaining)" + dat += "
[subcategory]
" + + . = 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/current_level = pref.skills[skill.type] + 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)) + + for(var/skill_level = 0 to base_maximum_level) + . = skill_level + + var/skill_cost = skill.get_cost(skill_level + 1) + if(skill_cost > remaining_skill_points) + break + + skill_level++ + remaining_skill_points -= skill_cost + +/** + * 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 = SSskills.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 [SSskills.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(SSskills.skill_level_map[skill_level])].
" + dat += SPAN_NOTICE("[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"]) + if(new_skill_value == SKILL_LEVEL_UNFAMILIAR) + pref.skills -= new_skill.type + else + 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 [SSskills.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(SSskills.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 c05b2a37a85..f0b9f4214ab 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -93,16 +93,28 @@ var/list/preferences_datums = list() var/machine_serial_number var/machine_ownership_status = IPC_OWNERSHIP_COMPANY - //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. @@ -473,7 +485,6 @@ var/list/preferences_datums = list() 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) @@ -514,6 +525,8 @@ var/list/preferences_datums = list() if(istype(P) && (P.ability_flags & PSI_FLAG_CANON)) P.apply(character) + character.skills.set_skills_from_pref(src) + if(icon_updates) character.force_update_limbs() character.update_mutations(0) @@ -648,7 +661,6 @@ var/list/preferences_datums = list() b_eyes = 0 species = SPECIES_HUMAN - home_system = "Unset" citizenship = "None" faction = "None" religion = "None" diff --git a/code/modules/mob/living/carbon/human/human_attackhand.dm b/code/modules/mob/living/carbon/human/human_attackhand.dm index 75a2fa656b5..d6b25523b7d 100644 --- a/code/modules/mob/living/carbon/human/human_attackhand.dm +++ b/code/modules/mob/living/carbon/human/human_attackhand.dm @@ -224,9 +224,19 @@ */ if(prob(80)) hit_zone = ran_zone(hit_zone) - if(prob(15) && hit_zone != BP_CHEST) // Missed! + + var/melee_skill_level = H.get_skill_level(/singleton/skill/unarmed_combat) + var/miss_chance = 15 + switch(melee_skill_level) + if(SKILL_LEVEL_UNFAMILIAR) + miss_chance = 25 + if(SKILL_LEVEL_FAMILIAR) + miss_chance = 20 + + var/skill_difference = H.get_skill_difference(src, /singleton/skill/unarmed_combat) + 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], [skill_difference < -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)) @@ -305,6 +315,8 @@ to_chat(M, SPAN_NOTICE("You don't want to risk hurting [src]!")) return FALSE + var/melee_skill_level = H.get_skill_level(/singleton/skill/unarmed_combat) + var/skill_difference = H.get_skill_difference(src, /singleton/skill/unarmed_combat) var/disarm_cost var/obj/item/organ/internal/cell/cell = M.internal_organs_by_name[BP_CELL] var/obj/item/cell/potato @@ -316,6 +328,9 @@ 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 * 100) potato.use(disarm_cost) else if(M.max_stamina > 0) @@ -327,6 +342,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 += -(skill_difference * 10) 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 @@ -350,14 +368,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 @@ -367,7 +389,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 [melee_skill_level == SKILL_LEVEL_UNFAMILIAR ? "inexperienced shoving" : "disarm attempt"] with [W]!")) return M.attackby(W,src) var/randn = rand(1, 100) @@ -399,7 +421,9 @@ 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) + var/push_chance = 25 + push_chance += -(skill_difference * 5) + 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) @@ -420,8 +444,10 @@ playsound(loc, 'sound/weapons/push.ogg', 50, 1, -1) return - if(randn <= 60) - if(H.gloves && istype(H.gloves,/obj/item/clothing/gloves/force)) + var/disarm_chance = 25 + disarm_chance += -(skill_difference * 5) + 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) @@ -451,12 +477,13 @@ //No return here is intentional, as it will then try to disarm other items, and/or play a failed disarm message playsound(loc, /singleton/sound_category/punchmiss_sound, 25, 1, -1) - visible_message(SPAN_DANGER("[M] attempted to disarm [src]!")) + visible_message(SPAN_DANGER("[M] [melee_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/medicine_skill = H.get_skill_level(/singleton/skill/medicine) if(istype(main_hand) || istype(off_hand)) cpr = FALSE to_chat(H, SPAN_NOTICE("You cannot perform CPR with anything in your hands.")) @@ -475,7 +502,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 @@ -487,23 +519,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) @@ -512,7 +552,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 @@ -534,6 +574,17 @@ 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!")) + + else if(medicine_skill == SKILL_LEVEL_UNFAMILIAR) + fail = prob(90) + if(fail) + to_chat(H, SPAN_NOTICE("You were just slightly off!")) + 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 3c36b009ebb..ef821c6f054 100644 --- a/code/modules/mob/living/carbon/human/species/species.dm +++ b/code/modules/mob/living/carbon/human/species/species.dm @@ -346,6 +346,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? @@ -950,3 +957,15 @@ */ /datum/species/proc/sleep_examine_msg(var/mob/M) return SPAN_NOTICE("[M.get_pronoun("He")] appears to be fast asleep.\n") + +/** + * 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/mob/mob.dm b/code/modules/mob/mob.dm index a1bc4fc7a73..adab58db08b 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -54,6 +54,8 @@ click_handlers.QdelClear() QDEL_NULL(click_handlers) + QDEL_NULL(skills) + return ..() /mob/New() @@ -104,6 +106,8 @@ become_hearing_sensitive() + skills = new skills(src) + /** * Generate the tag for this mob * diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 7c49b1fbf25..cf4f2c7e09b 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -274,3 +274,5 @@ /// A assoc lazylist of to_chat notifications, key = string message, value = world time integer var/list/message_notifications + /// The holder for mob skills. + var/datum/skills/skills = /datum/skills diff --git a/code/modules/mob/skills/skills.dm b/code/modules/mob/skills/skills.dm new file mode 100644 index 00000000000..ef3b281471d --- /dev/null +++ b/code/modules/mob/skills/skills.dm @@ -0,0 +1,63 @@ +/datum/skills + /// The mob that owns this skill datum. + var/mob/owner + /// The skills that this skill datum holds. Assoc list of skill type to level. + var/list/skills = list() + /// The TGUI module for changing skills. + var/tgui_module + +/datum/skills/New(mob/M) + if(!istype(M)) + crash_with("Invalid mob [M] supplied to skill datum!") + owner = M + ..() + +/** + * Returns the proficiency with a certain skill. + */ +/datum/skills/proc/get_skill_level(skill_type) + if(skill_type in skills) + return skills[skill_type] + return SKILL_LEVEL_UNFAMILIAR + +/** + * Sets skills starting from a preferences datum. + */ +/datum/skills/proc/set_skills_from_pref(datum/preferences/pref) + for(var/S in pref.skills) + var/singleton/skill/skill = GET_SINGLETON(S) + var/skill_level = pref.skills[skill.type] + skills[skill.type] = skill_level + +/** + * Returns the mob's proficiency with a certain skill. + */ +/mob/proc/get_skill_level(skill_type) + return skills.get_skill_level(skill_type) + +/** + * Takes a skill type and the level of skill needed. + * Returns TRUE if the mob's skill level exceeds or equals the skill level needed. + * Returns FALSE otherwise. + */ +/mob/proc/skill_check(skill_type, skill_level_needed) + return get_skill_level(skill_type) >= skill_level_needed + +/** + * Gets the skill difference in a given skill between two mobs. + */ +/mob/proc/get_skill_difference(mob/opponent, skill_type) + return get_skill_level(skill_type) - opponent.get_skill_level(skill_type) + +/** + * Returns a multiplier based on the mob's skill. Takes a skill type and a minimum skill floor at least. + * Bonus and malus are the modifiers added or removed for each skill level of difference from the required skill floor. + */ +/mob/proc/get_skill_multiplier(skill_type, skill_floor = SKILL_LEVEL_TRAINED, bonus = 0.2, malus = 0.2) + var/modifier = 1 + var/skill_level = get_skill_level(skill_type) + if(skill_level >= SKILL_LEVEL_TRAINED) + modifier -= bonus * max(1, skill_level - skill_floor) + else + modifier += malus * max(1, skill_floor - skill_level) + return modifier diff --git a/code/modules/power/singularity/emitter.dm b/code/modules/power/singularity/emitter.dm index 4cdc8cce05a..7e88c4a88da 100644 --- a/code/modules/power/singularity/emitter.dm +++ b/code/modules/power/singularity/emitter.dm @@ -73,6 +73,9 @@ /obj/machinery/power/emitter/attack_hand(mob/user) add_fingerprint(user) + if(user.get_skill_level(/singleton/skill/reactor_systems) <= SKILL_LEVEL_FAMILIAR) + to_chat(user, SPAN_WARNING("You have no idea where the switch even is on this thing...")) + return activate(user) /obj/machinery/power/emitter/proc/activate(mob/user)