diff --git a/code/__DEFINES/skills/skills.dm b/code/__DEFINES/skills/skills.dm new file mode 100644 index 0000000000..b97b921550 --- /dev/null +++ b/code/__DEFINES/skills/skills.dm @@ -0,0 +1,28 @@ +/// true/false +#define SKILL_PROGRESSION_BINARY 1 +/// numerical +#define SKILL_PROGRESSION_NUMERICAL 2 +/// Enum +#define SKILL_PROGRESSION_ENUM 3 + +/// Max value of skill for numerical skills +#define SKILL_NUMERICAL_MAX 100 +/// Min value of skill for numerical skills +#define SKILL_NUMERICAL_MIN 0 + +// Standard values for job starting skills + +#define STARTING_SKILL_SURGERY_MEDICAL 35 //out of SKILL_NUMERICAL_MAX + +// Standard values for job starting skill affinities + +#define STARTING_SKILL_AFFINITY_SURGERY_MEDICAL 1.2 + +// Standard values for skill gain (this is multiplied by affinity) + +#define SKILL_GAIN_SURGERY_PER_STEP 0.25 + +// Misc + +/// 40% speedup at 100 skill +#define SURGERY_SKILL_SPEEDUP_NUMERICAL_SCALE(number) clamp(number / 250, 1, 2) diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index a7be36abc2..8042631b7b 100755 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -387,6 +387,9 @@ SUBSYSTEM_DEF(ticker) for(var/mob/dead/new_player/N in GLOB.player_list) var/mob/living/carbon/human/player = N.new_character if(istype(player) && player.mind && player.mind.assigned_role) + var/datum/job/J = SSjob.GetJob(player.mind.assigned_role) + if(J) + J.standard_assign_skills(player.mind) if(player.mind.assigned_role == "Captain") captainless=0 if(player.mind.assigned_role != player.mind.special_role) diff --git a/code/datums/mind.dm b/code/datums/mind.dm index 31704d5451..728c4fb51b 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -63,7 +63,11 @@ var/force_escaped = FALSE // Set by Into The Sunset command of the shuttle manipulator var/list/learned_recipes //List of learned recipe TYPES. + /// Our skill holder. + var/datum/skill_holder/skill_holder + /datum/mind/New(var/key) + skill_holder = new src.key = key soulOwner = src martial_art = default_martial_art diff --git a/code/datums/skills/_check_skills.dm b/code/datums/skills/_check_skills.dm new file mode 100644 index 0000000000..d9dc6dad3d --- /dev/null +++ b/code/datums/skills/_check_skills.dm @@ -0,0 +1,16 @@ +// yeah yeah verbs suck whatever I suck at this fix this someone please - kevinz000 + +/mob/verb/check_skills() + set name = "Check Skills" + set category = "IC" + set desc = "Check your skills (if you have any..)" + + if(!mind) + to_chat(usr, "How do you check the skills of [(usr == src)? "yourself when you are" : "something"] without a mind?") + return + if(!mind.skill_holder) + to_chat(usr, "How do you check the skills of [(usr == src)? "yourself when you are" : "something"] without the capability for skills? (PROBABLY A BUG, PRESS F1.)") + return + var/datum/browser/B = new(usr, "skilldisplay_[REF(src)]", "Skills of [src]") + B.set_content(mind.skill_holder.html_readout()) + B.open() diff --git a/code/datums/skills/_skill.dm b/code/datums/skills/_skill.dm new file mode 100644 index 0000000000..2dd321c4c6 --- /dev/null +++ b/code/datums/skills/_skill.dm @@ -0,0 +1,95 @@ +GLOBAL_LIST_INIT(skill_datums, init_skill_datums()) + +/proc/init_skill_datums() + . = list() + for(var/path in subtypesof(/datum/skill)) + var/datum/skill/S = path + if(initial(S.abstract_type) == path) + continue + S = new path + .[S.type] = S + +/proc/get_skill_datum(path) + return GLOB.skill_datums[path] + +/proc/sanitize_skill_value(path, value) + var/datum/skill/S = get_skill_datum(path) + // don't check, if we runtime let it happen. + return S.sanitize_value(value) + +/proc/is_skill_value_greater(path, existing, new_value) + var/datum/skill/S = get_skill_datum(path) + // don't check, if we runtime let it happen. + return S.is_value_greater(existing, new_value) + +/** + * Skill datums + */ +/datum/skill + /// Our name + var/name + /// Our description + var/desc + /// Our progression type + var/progression_type + /// Abstract type + var/abstract_type = /datum/skill + +/** + * Ensures what someone's setting as a value for this skill is valid. + */ +/datum/skill/proc/sanitize_value(new_value) + return new_value + +/** + * Checks if a value is greater + */ +/datum/skill/proc/is_value_greater(existing, new_value) + if(!existing) + return TRUE + return new_value > existing + +/** + * Standard value "render" + */ +/datum/skill/proc/standard_render_value(value) + return value + +// Just saying, the choice to use different sub-parent-types is to force coders to resolve issues as I won't be implementing custom procs to grab skill levels in a certain context. +// Aka: So people don't forget to change checks if they change a skill's progression type. + +/datum/skill/binary + abstract_type = /datum/skill/binary + progression_type = SKILL_PROGRESSION_BINARY + +/datum/skill/binary/sanitize_value(new_value) + return new_value? TRUE : FALSE + +/datum/skill/binary/standard_render_value(value) + return value? "Yes" : "No" + +/datum/skill/numerical + abstract_type = /datum/skill/numerical + progression_type = SKILL_PROGRESSION_NUMERICAL + /// Max value of this skill + var/max_value = 100 + /// Min value of this skill + var/min_value = 0 + /// Display as a percent in standard_render_value? + var/display_as_percent = FALSE + +/datum/skill/numerical/sanitize_value(new_value) + return clamp(new_value, min_value, max_value) + +/datum/skill/numerical/standard_render_value(value) + return display_as_percent? "[round(value/max_value/100, 0.01)]%" : "[value] / [max_value]" + +/datum/skill/enum + abstract_type = /datum/skill/enum + progression_type = SKILL_PROGRESSION_ENUM + /// Valid values for the skill + var/list/valid_values = list() + +/datum/skill/enum/sanitize_value(new_value) + if(new_value in valid_values) + return new_value diff --git a/code/datums/skills/_skill_holder.dm b/code/datums/skills/_skill_holder.dm new file mode 100644 index 0000000000..352adc46ff --- /dev/null +++ b/code/datums/skills/_skill_holder.dm @@ -0,0 +1,78 @@ +/** + * Skill holder datums + */ +/datum/skill_holder + /// Our list of skills and values. Lazylist. Associative. Keys are datum typepaths to the skill. + var/list/skills + /// Same as [skills] but affinities, which are multiplied to increase amount when gaining skills. + var/list/skill_affinities + +/** + * Grabs the value of a skill. + */ +/datum/skill_holder/proc/get_skill_value(skill) + if(!ispath(skill)) + CRASH("Invalid get_skill_value call. Use typepaths.") //until a time when we somehow need text ids for dynamic skills, I'm enforcing this. + if(!skills) + return null + return skills[skill] + +/** + * Grabs our affinity for a skill. !!This is a multiplier!! + */ +/datum/skill_holder/proc/get_skill_affinity(skill) + if(!ispath(skill)) + CRASH("Invalid get_skill_affinity call. Use typepaths.") //until a time when we somehow need text ids for dynamic skills, I'm enforcing this. + if(!skills) + return 1 + var/affinity = skill_affinities[skill] + if(isnull(affinity)) + return 1 + return affinity + +/** + * Sets the value of a skill. + */ +/datum/skill_holder/proc/set_skill_value(skill, value) + if(!ispath(skill)) + CRASH("Invalid set_skill_value call. Use typepaths.") //until a time when we somehow need text ids for dynamic skills, I'm enforcing this. + LAZYINITLIST(skills) + value = sanitize_skill_value(skill, value) + if(!isnull(value)) + skills[skill] = value + return TRUE + return FALSE + +/** + * Boosts a skill to a value if not aobve + */ +/datum/skill_holder/proc/boost_skill_value_to(skill, value) + var/current = get_skill_value(skill) + if(!is_skill_value_greater(skill, current, value)) + return FALSE + set_skill_value(skill, value) + return TRUE + +/** + * Automatic skill increase, multiplied by skill affinity if existing. + * Only works if skill is numerical. + */ +/datum/skill_holder/proc/auto_gain_experience(skill, value) + if(!ispath(skill, /datum/skill/numerical)) + CRASH("You cannot auto increment a non numerical skill!") + var/current = get_skill_value(skill) + var/affinity = get_skill_affinity(skill) + boost_skill_value_to(skill, current + (value * affinity)) + +/** + * Generates a HTML readout of our skills. + * Port to tgui-next when? + */ +/datum/skill_holder/proc/html_readout() + var/list/out = list("

Skills


") + out += "" + for(var/path in skills) + var/datum/skill/S = GLOB.skill_datums[path] + out += "" + out += "
SkillValue
[S.name][S.standard_render_value(skills[path])]
" + return out.Join("") diff --git a/code/datums/skills/medical.dm b/code/datums/skills/medical.dm new file mode 100644 index 0000000000..3b67cacb04 --- /dev/null +++ b/code/datums/skills/medical.dm @@ -0,0 +1,3 @@ +/datum/skill/numerical/surgery + name = "Surgery" + desc = "How proficient you are at doing surgery." diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index ee441c21f7..e40c278e92 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -66,6 +66,11 @@ // How much threat this job is worth in dynamic. Is subtracted if the player's not an antag, added if they are. var/threat = 0 + /// Starting skill levels. + var/list/starting_skills + /// Skill affinities to set + var/list/skill_affinities + //Only override this proc //H is usually a human unless an /equip override transformed it /datum/job/proc/after_spawn(mob/living/H, mob/M, latejoin = FALSE) @@ -142,7 +147,6 @@ return TRUE //Available in 0 days = available right now = player is old enough to play. return FALSE - /datum/job/proc/available_in_days(client/C) if(!C) return 0 @@ -166,6 +170,17 @@ /datum/job/proc/radio_help_message(mob/M) to_chat(M, "Prefix your message with :h to speak on your department's radio. To see other prefixes, look closely at your headset.") +/datum/job/proc/standard_assign_skills(datum/mind/M) + if(!starting_skills) + return + for(var/skill in starting_skills) + M.skill_holder.boost_skill_value_to(skill, starting_skills[skill]) + // do wipe affinities though + M.skill_holder.skill_affinities = list() + for(var/skill in skill_affinities) + M.skill_holder.skill_affinities[skill] = skill_affinities[skill] + UNSETEMPTY(M.skill_holder.skill_affinities) //if we didn't set any. + /datum/outfit/job name = "Standard Gear" diff --git a/code/modules/jobs/job_types/chemist.dm b/code/modules/jobs/job_types/chemist.dm index 44b0e90ec8..571785526b 100644 --- a/code/modules/jobs/job_types/chemist.dm +++ b/code/modules/jobs/job_types/chemist.dm @@ -19,6 +19,9 @@ display_order = JOB_DISPLAY_ORDER_CHEMIST threat = 1.5 + starting_skills = list(/datum/skill/numerical/surgery = STARTING_SKILL_SURGERY_MEDICAL) + skill_affinities = list(/datum/skill/numerical/surgery = STARTING_SKILL_AFFINITY_SURGERY_MEDICAL) + /datum/outfit/job/chemist name = "Chemist" jobtype = /datum/job/chemist diff --git a/code/modules/jobs/job_types/chief_medical_officer.dm b/code/modules/jobs/job_types/chief_medical_officer.dm index 8dd7fafab3..478c33551d 100644 --- a/code/modules/jobs/job_types/chief_medical_officer.dm +++ b/code/modules/jobs/job_types/chief_medical_officer.dm @@ -29,6 +29,9 @@ blacklisted_quirks = list(/datum/quirk/mute, /datum/quirk/brainproblems, /datum/quirk/insanity) threat = 2 + starting_skills = list(/datum/skill/numerical/surgery = STARTING_SKILL_SURGERY_MEDICAL) + skill_affinities = list(/datum/skill/numerical/surgery = STARTING_SKILL_AFFINITY_SURGERY_MEDICAL) + /datum/outfit/job/cmo name = "Chief Medical Officer" jobtype = /datum/job/cmo diff --git a/code/modules/jobs/job_types/geneticist.dm b/code/modules/jobs/job_types/geneticist.dm index 2e8b18d772..4fb989f6e8 100644 --- a/code/modules/jobs/job_types/geneticist.dm +++ b/code/modules/jobs/job_types/geneticist.dm @@ -19,6 +19,8 @@ display_order = JOB_DISPLAY_ORDER_GENETICIST threat = 1.5 + starting_skills = list(/datum/skill/numerical/surgery = STARTING_SKILL_SURGERY_MEDICAL) + /datum/outfit/job/geneticist name = "Geneticist" jobtype = /datum/job/geneticist diff --git a/code/modules/jobs/job_types/medical_doctor.dm b/code/modules/jobs/job_types/medical_doctor.dm index 3eaa2ae10a..0e8b2e645b 100644 --- a/code/modules/jobs/job_types/medical_doctor.dm +++ b/code/modules/jobs/job_types/medical_doctor.dm @@ -17,6 +17,9 @@ display_order = JOB_DISPLAY_ORDER_MEDICAL_DOCTOR threat = 0.5 + starting_skills = list(/datum/skill/numerical/surgery = STARTING_SKILL_SURGERY_MEDICAL) + skill_affinities = list(/datum/skill/numerical/surgery = STARTING_SKILL_AFFINITY_SURGERY_MEDICAL) + /datum/outfit/job/doctor name = "Medical Doctor" jobtype = /datum/job/doctor diff --git a/code/modules/jobs/job_types/paramedic.dm b/code/modules/jobs/job_types/paramedic.dm index df6e1431ae..e8098daddc 100644 --- a/code/modules/jobs/job_types/paramedic.dm +++ b/code/modules/jobs/job_types/paramedic.dm @@ -19,6 +19,9 @@ threat = 0.5 + starting_skills = list(/datum/skill/numerical/surgery = STARTING_SKILL_SURGERY_MEDICAL) + skill_affinities = list(/datum/skill/numerical/surgery = STARTING_SKILL_AFFINITY_SURGERY_MEDICAL) + /datum/outfit/job/paramedic name = "Paramedic" jobtype = /datum/job/paramedic diff --git a/code/modules/jobs/job_types/virologist.dm b/code/modules/jobs/job_types/virologist.dm index 4ba65dff59..78f0ba4272 100644 --- a/code/modules/jobs/job_types/virologist.dm +++ b/code/modules/jobs/job_types/virologist.dm @@ -20,6 +20,9 @@ threat = 1.5 + starting_skills = list(/datum/skill/numerical/surgery = STARTING_SKILL_SURGERY_MEDICAL) + skill_affinities = list(/datum/skill/numerical/surgery = STARTING_SKILL_AFFINITY_SURGERY_MEDICAL) + /datum/outfit/job/virologist name = "Virologist" jobtype = /datum/job/virologist diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index 35b1a69761..f7e1f24b63 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -392,6 +392,8 @@ character.update_parallax_teleport() + job.standard_assign_skills(character.mind) + SSticker.minds += character.mind var/mob/living/carbon/human/humanc diff --git a/code/modules/surgery/surgery_step.dm b/code/modules/surgery/surgery_step.dm index 1857f8db48..7127b41ee3 100644 --- a/code/modules/surgery/surgery_step.dm +++ b/code/modules/surgery/surgery_step.dm @@ -58,7 +58,10 @@ return FALSE if(tool) speed_mod = tool.toolspeed - if(do_after(user, time * speed_mod, target = target)) + var/skill_mod = 1 + if(user?.mind?.skill_holder) + skill_mod = SURGERY_SKILL_SPEEDUP_NUMERICAL_SCALE(user.mind.skill_holder.get_skill_value(/datum/skill/numerical/surgery)) + if(do_after(user, time * speed_mod * skill_mod, target = target)) var/prob_chance = 100 if(implement_type) //this means it isn't a require hand or any item step. prob_chance = implements[implement_type] @@ -66,6 +69,7 @@ if((prob(prob_chance) || (iscyborg(user) && !silicons_obey_prob)) && chem_check(target) && !try_to_fail) if(success(user, target, target_zone, tool, surgery)) + user?.mind?.skill_holder?.auto_gain_experience(/datum/skill/numerical/surgery, SKILL_GAIN_SURGERY_PER_STEP) advance = TRUE else if(failure(user, target, target_zone, tool, surgery)) diff --git a/tgstation.dme b/tgstation.dme index 848d149ae0..e1ac9673ff 100755 --- a/tgstation.dme +++ b/tgstation.dme @@ -125,6 +125,7 @@ #include "code\__DEFINES\dcs\signals.dm" #include "code\__DEFINES\flags\shields.dm" #include "code\__DEFINES\misc\return_values.dm" +#include "code\__DEFINES\skills\skills.dm" #include "code\__HELPERS\_cit_helpers.dm" #include "code\__HELPERS\_lists.dm" #include "code\__HELPERS\_logging.dm" @@ -570,6 +571,10 @@ #include "code\datums\ruins\lavaland.dm" #include "code\datums\ruins\space.dm" #include "code\datums\ruins\station.dm" +#include "code\datums\skills\_check_skills.dm" +#include "code\datums\skills\_skill.dm" +#include "code\datums\skills\_skill_holder.dm" +#include "code\datums\skills\medical.dm" #include "code\datums\status_effects\buffs.dm" #include "code\datums\status_effects\debuffs.dm" #include "code\datums\status_effects\gas.dm"