From f485db2b2d441633b10b24b1eff681fe6a236667 Mon Sep 17 00:00:00 2001 From: Shoes01 <0jason.gilbert@gmail.com> Date: Wed, 9 Oct 2019 14:29:19 -0300 Subject: [PATCH] Player needs to meet job requirements before switching The player needs a certain amount of stat points in order to change. The player needs to be the correct race. The player needs to have mastered X number of skills from job Y in order to change. Added a Rogue class. Player starts as Soldier. Improve readability of SkillDirectoryProcessor. Added message feedback when trying to swtich jobs. Reduce duplicate code in WearableProcessor. Change structure of SkillDirectoryComponent, and updated codebase to reflect this. --- _jobs.py | 25 ++++++--- components/actor/skill_directory.py | 15 ++++-- engine.py | 3 +- game.py | 2 +- processors/job.py | 84 +++++++++++++++++++++-------- processors/skill_directory.py | 66 +++++++++++++---------- processors/sub/character_sheet.py | 8 +-- processors/sub/message_log.py | 17 ++++++ processors/wearable.py | 24 +++++---- 9 files changed, 165 insertions(+), 79 deletions(-) diff --git a/_jobs.py b/_jobs.py index 19156da..fbdd857 100644 --- a/_jobs.py +++ b/_jobs.py @@ -1,8 +1,9 @@ class Job: - def __init__(self, description, name, races, upkeep): + def __init__(self, description, name, races, skills, upkeep): self.description = description # Type: str self.name = name # Type: str self.races = races # Type: list + self.skills = skills # Type: dict {'job': number} self.upkeep = upkeep # Type: dict {'stat': penalty value} @@ -10,19 +11,29 @@ def __init__(self, description, name, races, upkeep): 'soldier': Job( description="Baby's first job.", name='soldier', - upkeep={}, - races=('human',) + races=('human',), + skills={}, + upkeep={} ), 'warrior': Job( description='Has access to more devastating skills.', # Not rly. name='warrior', - upkeep={'magic': 1, 'speed': 2}, - races=('human',) + races=('human',), + skills={}, + upkeep={'magic': 1, 'speed': 2} ), 'beserker': Job( description='Classic orc.', name='beserker', - upkeep={'speed': 1, 'hp': 1}, - races=('orc',) + races=('orc',), + skills={}, + upkeep={'speed': 1, 'hp': 1} + ), + 'rogue': Job( + description='A job for seasoned fighters.', + name='rogue', + races=('human', 'goblin'), + skills={'soldier': 1}, + upkeep={'speed': 1, 'hp': 15} ) } \ No newline at end of file diff --git a/components/actor/skill_directory.py b/components/actor/skill_directory.py index 5d55107..7d9abd7 100644 --- a/components/actor/skill_directory.py +++ b/components/actor/skill_directory.py @@ -1,14 +1,19 @@ class SkillDirectoryComponent: - def __init__(self): - self.skill_directory = {} + def __init__(self, job='unemployed'): + self.skill_directory = {job: {}} """ Usage Example: self.skill_directory = { - 'bash': (85, 100), - 'slash': (100, 100), - 'skill_name': (current_ap, max_ap) + 'soldier': { + 'bash': (85, 100), + 'slash': (100, 100), + 'skill_name': (current_ap, max_ap) + }, + 'job_name': { + + } } """ \ No newline at end of file diff --git a/engine.py b/engine.py index 8aa7b80..7933454 100644 --- a/engine.py +++ b/engine.py @@ -14,4 +14,5 @@ def main(): if __name__ == '__main__': # cProfile.run('main()') # This runs the profiler - main() \ No newline at end of file + main() + \ No newline at end of file diff --git a/game.py b/game.py index b05ba64..6f0c376 100644 --- a/game.py +++ b/game.py @@ -245,7 +245,7 @@ def create_entity(self, entity): RaceComponent(race='human'), PositionComponent(), RenderComponent(color_bg=None, char='@', codepoint=SPRITES['player'], color_fg=ENTITY_COLORS['player'], color_explored=None), - SkillDirectoryComponent(), + SkillDirectoryComponent(job='soldier'), SoulComponent(eccentricity=5, max_rarity=10), StatsComponent(hp=500, attack=10) ) diff --git a/processors/job.py b/processors/job.py index 8dfb41b..d3cfaf7 100644 --- a/processors/job.py +++ b/processors/job.py @@ -4,6 +4,8 @@ from _jobs import JOBS, Job from components.actor.job import JobComponent from components.actor.race import RaceComponent +from components.actor.skill_directory import SkillDirectoryComponent +from processors.skill_directory import SkillDirectoryProcessor from processors.state import StateProcessor from menu import PopupMenu, PopupChoice from queue import Queue @@ -30,30 +32,68 @@ def process(self): _result = {'ent': ent, 'job': job} _processor = JobProcessor _upkeep = job.upkeep - - # /// Check the validity of the job change. /// - _validity = True - - # Race validity. - if self.world.component_for_entity(ent, RaceComponent).race not in job.races: - _validity = False - - # Upkeep validity. - ### The player may not switch to a job if it can't pay the upkeep. - ### However, other stats unrelated to the job may be negative. - ### Note: _upkeep does not include the * -10. - bare_stats = generate_stats(ent, self.world, include_upkeep=False) - for key, value in _upkeep.items(): - if bare_stats[key] - value * 10 < 0: - _validity = False - - # ///////////////////////////////////////////// + _validity, _ = check_validity(ent, job, self.world) - menu.contents.append(PopupChoice(name=_name, key=_key, result=_result, processor=_processor, valid=_validity, description=_description, upkeep=_upkeep)) + menu.contents.append( + PopupChoice( + name=_name, + key=_key, + result=_result, + processor=_processor, + valid=_validity, + description=_description, + upkeep=_upkeep + ) + ) self.world.get_processor(StateProcessor).queue.put({'popup': menu}) else: - ent_job = self.world.component_for_entity(ent, JobComponent) - ent_job.update_upkeep(job.upkeep) - ent_job.job = job.name \ No newline at end of file + valid, message_data = check_validity(ent, job, self.world) + + if valid: + # Switch jobs! + ent_job = self.world.component_for_entity(ent, JobComponent) + ent_job.update_upkeep(job.upkeep) + ent_job.job = job.name + self.world.get_processor(SkillDirectoryProcessor).queue.put({'new_job': job.name, 'ent': ent}) + + self.world.messages.append({'job_switch': message_data}) + +def check_validity(ent, job, world): + validity = True + message_data = {} + + # Race validity. + if world.component_for_entity(ent, RaceComponent).race not in job.races: + validity = False + message_data['wrong_race'] = True + + # Upkeep validity. + ### The player may not switch to a job if it can't pay the upkeep. + ### However, other stats unrelated to the job may be negative. + ### Note: _upkeep does not include the * -10. + bare_stats = generate_stats(ent, world, include_upkeep=False) + for key, value in job.upkeep.items(): + if bare_stats[key] - value * 10 < 0: + validity = False + message_data['not_enough_stats'] = True + + # Skill validity. + sd_comp = world.component_for_entity(ent, SkillDirectoryComponent) + for required_job, required_number in job.skills.items(): + # Determine how many skills from the given job have been mastered. + mastery_number = 0 + if required_job in sd_comp.skill_directory: + for name, ap in sd_comp.skill_directory[required_job].items(): + if ap[0] == ap[1]: + mastery_number += 1 + + if mastery_number < required_number: + validity = False + message_data['not_enough_skills'] = True + + if validity: + message_data['switch_class'] = job.name + + return validity, message_data \ No newline at end of file diff --git a/processors/skill_directory.py b/processors/skill_directory.py index 19ba189..81bbe2a 100644 --- a/processors/skill_directory.py +++ b/processors/skill_directory.py @@ -1,6 +1,7 @@ import esper from components.actor.equipment import EquipmentComponent +from components.actor.job import JobComponent from components.actor.skill_directory import SkillDirectoryComponent from components.item.skill import ItemSkillComponent from queue import Queue @@ -15,42 +16,49 @@ def process(self): event = self.queue.get() ap_gain = event.get('ap_gain') + ent = event['ent'] item = event.get('item') + new_job = event.get('new_job') new_skill = event.get('new_skill') - ent = event['ent'] - skill = event.get('skill') - sd_comp = self.world.component_for_entity(ent, SkillDirectoryComponent) + job = self.world.component_for_entity(ent, JobComponent).job + sd_comp = self.world.component_for_entity(ent, SkillDirectoryComponent) - if new_skill: - skill = None if not self.world.has_component(item, ItemSkillComponent) else self.world.component_for_entity(item, ItemSkillComponent) + # Add an entry to the ent's skill directory. + if new_job: + if new_job not in sd_comp.skill_directory.keys(): + sd_comp.skill_directory[job] = {} + + elif item and new_skill: + # Add this item's skill to the directory, if it is not already present. + skill = self.world.component_for_entity(item, ItemSkillComponent) - if skill and skill.name not in sd_comp.skill_directory.keys(): - sd_comp.skill_directory[skill.name] = (0, skill.ap_max) + if job in sd_comp.skill_directory.keys(): + if skill not in sd_comp.skill_directory[job].keys(): + sd_comp.skill_directory[job][skill.name] = (0, skill.ap_max) + else: + print("ERROR: This item is trying to add a skill to a job that doesn't exist!") elif ap_gain: # Go through each item that is equipped, and add AP to its skill. eqp_comp = self.world.component_for_entity(ent, EquipmentComponent) - for item in eqp_comp.equipment: - skill = None if not self.world.has_component(item, ItemSkillComponent) else self.world.component_for_entity(item, ItemSkillComponent) + for other_item in eqp_comp.equipment: + newly_maxed = False + + other_skill = None if not self.world.component_for_entity(other_item, ItemSkillComponent) else self.world.component_for_entity(other_item, ItemSkillComponent).name + + ap_current, ap_max = sd_comp.skill_directory[job][other_skill] + + already_maxed = True if ap_current == ap_max else False - if skill is not None: - newly_maxed = False - already_maxed = False - - ap_current, ap_max = sd_comp.skill_directory[skill.name] - - if ap_current == ap_max: - already_maxed = True - - ap_current += ap_gain - - if ap_current >= ap_max: - ap_current = ap_max - newly_maxed = True - - sd_comp.skill_directory[skill.name] = (ap_current, ap_max) - - if newly_maxed and not already_maxed: - message_data = {'name': skill.name} - self.world.messages.append({'skill_mastered': message_data}) + ap_current += ap_gain + + if ap_current >= ap_max: + ap_current = ap_max + newly_maxed = True if not already_maxed else False + + sd_comp.skill_directory[job][other_skill] = (ap_current, ap_max) + + if newly_maxed: + message_data = {'name': other_skill} + self.world.messages.append({'skill_mastered': message_data}) diff --git a/processors/sub/character_sheet.py b/processors/sub/character_sheet.py index 8599c84..ed6eb59 100644 --- a/processors/sub/character_sheet.py +++ b/processors/sub/character_sheet.py @@ -105,9 +105,11 @@ def render_character_sheet(console_object, world): x, y = 77, 5 console.print(x - 1, y - 1, 'Skill Directory:', color_fg) i = 0 - for skill, ap in skill_directory.items(): - console.print(x, y + i, f"{skill.capitalize()}: {ap[0]}/{ap[1]}") - i += 1 + for job, skill_info in skill_directory.items(): + if skill_info: + for skill_name, ap in skill_info.items(): + console.print(x, y + i, f"{skill_name.capitalize()}: {ap[0]}/{ap[1]}") + i += 1 def generate_equipped_items(titles, world): equipment = world.component_for_entity(1, EquipmentComponent).equipment diff --git a/processors/sub/message_log.py b/processors/sub/message_log.py index 1f805ce..df10ae5 100644 --- a/processors/sub/message_log.py +++ b/processors/sub/message_log.py @@ -44,6 +44,7 @@ def render_message_log(console_object, new_turn, world): _game_loaded = message.get('game_loaded') _game_saved = message.get('game_saved') _heal = message.get('heal') + _job_switch = message.get('job_switch') _max_hp = message.get('max_hp') _move_items = message.get('move_items') _pickup = message.get('pickup') @@ -113,6 +114,22 @@ def render_message_log(console_object, new_turn, world): console.print(0, 0 + dy, '(Turn %s) You heal for %s point(s).' % (turn, value), LOG_COLORS['success']) + elif _job_switch: + wrong_race = _job_switch.get('wrong_race') + not_enough_stats = _job_switch.get('not_enough_stats') + not_enough_skills = _job_switch.get('not_enough_skills') + switch_class = _job_switch.get('switch_class') + + if wrong_race: + console.print(0, 0 + dy, f'You fail to switch classes, due to race.', LOG_COLORS['failure']) + elif not_enough_stats: + console.print(0, 0 + dy, f'You fail to switch classes, due to stats.', LOG_COLORS['failure']) + elif not_enough_skills: + console.print(0, 0 + dy, f'You fail to switch classes, due to skills.', LOG_COLORS['failure']) + elif switch_class: + console.print(0, 0 + dy, f'You are now a {switch_class}. Way to go.', LOG_COLORS['success']) + + elif _max_hp: value, turn = _max_hp diff --git a/processors/wearable.py b/processors/wearable.py index 0f60c79..07f9a31 100644 --- a/processors/wearable.py +++ b/processors/wearable.py @@ -67,29 +67,31 @@ def process(self): if item in eqp.equipment: # Already worn, so remove it. + message_data = {} # Clear the message data, as the RemovableProcessor will do its own thing. self.world.get_processor(RemovableProcessor).queue.put({'ent': ent, 'item': item}) elif slot_filled: # An item is already in the slot we want; swap the two items. + # Wear the item! + wear_item(ent, eqp, item, name_component, self.world) message_data['success'] = 'slot_filled' - self.world.messages.append({'wear': message_data}) - eqp.equipment.append(item) - name_component.name += ' (worn)' - message_data['job'] = self.world.component_for_entity(item, JobReqComponent).job_req.capitalize() + # Remove the other item. self.world.get_processor(RemovableProcessor).queue.put({'ent': ent, 'item': slot_filled_item}) elif self.world.component_for_entity(item, JobReqComponent).job_req != self.world.component_for_entity(ent, JobComponent).job: # Not the correct job to wear the item. message_data['success'] = 'wrong_job' message_data['job'] = self.world.component_for_entity(item, JobReqComponent).job_req.capitalize() - self.world.messages.append({'wear': message_data}) elif self.world.has_component(item, WearableComponent): # Wear the item! + wear_item(ent, eqp, item, name_component, self.world) message_data['success'] = True - self.world.messages.append({'wear': message_data}) - eqp.equipment.append(item) - name_component.name += ' (worn)' - self.world.get_processor(EnergyProcessor).queue.put({'ent': ent, 'item': True}) - self.world.get_processor(SkillDirectoryProcessor).queue.put({'ent': ent, 'item': item, 'new_skill': True}) else: # This is not a wearable item. message_data['success'] = False - self.world.messages.append({'wear': message_data}) \ No newline at end of file + + self.world.messages.append({'wear': message_data}) + +def wear_item(ent, eqp, item, name_component, world): + name_component.name += ' (worn)' + eqp.equipment.append(item) + world.get_processor(EnergyProcessor).queue.put({'ent': ent, 'item': True}) + world.get_processor(SkillDirectoryProcessor).queue.put({'ent': ent, 'item': item, 'new_skill': True})