diff --git a/app/app.py b/app/app.py index 0735a52..15994d4 100644 --- a/app/app.py +++ b/app/app.py @@ -4,6 +4,7 @@ from flask import Flask from flask_cors import CORS from flask_restx import Api, Resource +from app.functions.role_physical_attack import roll_physical_attack from .functions.build_character_sheet import build_character_sheet from .functions.roll_ability_scores import roll_ability_scores from .functions.roll_ability_check import roll_ability_check @@ -12,10 +13,9 @@ from .functions.roll_encounter import roll_encounter from .functions.roll_mental_attack import roll_mental_attack from .functions.roll_mutations import roll_mutations from .models.models import dice_model, ability_model, hp_model, character_model, encounter_model, ma_model, \ - mutation_model, \ - check_model + mutation_model, check_model, pa_model from .schemas.schemas import DiceSchema, CharacterSchema, EncounterSchema, MentalAttackSchema, AbilitySchema, \ - HPSchema, MutationSchema, CheckSchema + HPSchema, MutationSchema, CheckSchema, PhysicalAttackSchema app = Flask(__name__) CORS(app) @@ -28,6 +28,7 @@ dice = api.namespace('dice', description='Dice operations') ability = api.namespace('ability', description='Ability operations') hp = api.namespace('hp', description='HP operations') ma = api.namespace('ma', description='Mental Attack operations') +pa = api.namespace('pa', description='Physical Attack operations') mut = api.namespace('mut', description='Mutation operations') character = api.namespace('character', description='Character operations') encounter = api.namespace('encounter', description='Encounter operations') @@ -51,6 +52,9 @@ dice_schema = DiceSchema() ma_model = ma.model('MA', ma_model) ma_schema = MentalAttackSchema() +pa_model = pa.model('PA', pa_model) +pa_schema = PhysicalAttackSchema() + character_model = character.model('Character', character_model) character_schema = CharacterSchema() @@ -58,6 +62,20 @@ encounter_model = encounter.model('Encounter', encounter_model) encounter_schema = EncounterSchema() +@api.route('/coinflip', methods=['GET']) +class RollCoinflip(Resource): + @staticmethod + def get(): + return random.choice(['Heads', 'Tails']), 200 + + +@api.route('/roll/chance', methods=['GET']) +class RollChance(Resource): + @staticmethod + def get(): + return roll_dices(1, 100, False), 200 + + @api.route('/roll/dice', methods=['POST']) class RollDice(Resource): @dice.expect(dice_model) @@ -137,6 +155,24 @@ class RollMentalAttack(Resource): return roll_mental_attack(ams, dms, modifier), 200 +@api.route('/roll/attack/physical', methods=['POST']) +class RollPhysicalAttack(Resource): + @pa.expect(pa_model) + def post(self): + data = api.payload + errors = pa_schema.validate(data) + if errors: + return errors, 400 + + weapon_attack = data.get('weapon_attack') # to pick the attack table + dac = data.get('dac') # needed for both attacks + awc = data.get('awc') # only needed for weapon attacks + ahd = data.get('ahd') # only needed for non-weapon attacks + modifier = data.get('modifier') + + return roll_physical_attack(weapon_attack, dac, modifier, awc, ahd), 200 + + @api.route('/roll/check', methods=['POST']) class RollCheck(Resource): @check.expect(check_model) @@ -152,27 +188,6 @@ class RollCheck(Resource): return roll_ability_check(ability_score, multiplier), 200 -@api.route('/roll/tohit', methods=['GET']) -class RollToHit(Resource): - @staticmethod - def get(): - return roll_dices(1, 20, False), 200 - - -@api.route('/roll/chance', methods=['GET']) -class RollChance(Resource): - @staticmethod - def get(): - return roll_dices(1, 100, False), 200 - - -@api.route('/coinflip', methods=['GET']) -class RollCoinflip(Resource): - @staticmethod - def get(): - return random.choice(['Heads', 'Tails']), 200 - - @api.route('/roll/mutations', methods=['POST']) class RollMutations(Resource): @mut.expect(mutation_model) diff --git a/app/functions/get_attack_roll_outcome.py b/app/functions/get_attack_roll_outcome.py new file mode 100644 index 0000000..454517d --- /dev/null +++ b/app/functions/get_attack_roll_outcome.py @@ -0,0 +1,26 @@ +from app.functions.roll_dices import roll_dices + + +def get_attack_roll_outcome(result, modifier=0): + outcome = 'Unknown' + raw_roll = roll_dices(1, 20, False).get('result') + needed = result['needed'] + + if modifier != 0: + result["original-roll"] = raw_roll + result["modifier"] = modifier + rolled = raw_roll + modifier # negative modifiers will subtract themselves naturally + result["adjusted-roll"] = rolled + else: + rolled = raw_roll + result["original-roll"] = rolled + + if (rolled >= 20) and (needed <= 16): + outcome = "Devastating Success!" + elif needed <= rolled: + outcome = "Attack Successful!" + elif needed > rolled: + outcome = "Attack Failed!" + + result['outcome'] = outcome + return result diff --git a/app/functions/get_attack_values.py b/app/functions/get_attack_values.py new file mode 100644 index 0000000..a780f9b --- /dev/null +++ b/app/functions/get_attack_values.py @@ -0,0 +1,12 @@ +from app.tables.physattack import AttackerWeaponClassMatrix, AttackerHitDiceMatrix + + +def get_weapon_class_threshold(awc, dac): + awc_table = AttackerWeaponClassMatrix() + return awc_table.get_attack_score(awc, dac) + + +def get_hit_dice_threshold(ahd, dac): + ahd_table = AttackerHitDiceMatrix() + return ahd_table.get_attack_score(ahd, dac) + diff --git a/app/functions/role_physical_attack.py b/app/functions/role_physical_attack.py new file mode 100644 index 0000000..72f3887 --- /dev/null +++ b/app/functions/role_physical_attack.py @@ -0,0 +1,32 @@ +from app.functions.get_attack_roll_outcome import get_attack_roll_outcome +from app.tables.physattack import AttackerHitDiceMatrix, AttackerWeaponClassMatrix + + +def roll_physical_attack(weapon_attack, dac, modifier, awc=0, ahd=0): + """ + :param weapon_attack: boolean. required. Determines which attack matrix to use. + :param dac: integer. required. defender armour class. used in both matrices. + :param modifier: integer. required. any pluses or minuses to be applied to roll + :param awc: integer. optional(*). Attacker weapon class. This is required, if weapon attack == True. + :param ahd: integer. optional(*). Attacker hit dice. This is required, if weapon attack == False. + :return: + """ + result = {} + if weapon_attack: + if awc == 0: + print("Attacker Weapon Class is required for Weapon Attacks!") + result["outcome"] = "Attacker Weapon Class is required for Weapon Attacks!" + return result + else: + hit_table = AttackerWeaponClassMatrix() + result["needed"] = hit_table.get_attack_score(awc, dac) + else: + if ahd == 0: + print("Attacker Hit Dice is required for Non-Weapon Attacks!") + result["outcome"] = "Attacker Hit Dice is required for Non-Weapon Attacks!" + return result + else: + hit_table = AttackerHitDiceMatrix() + result["needed"] = hit_table.get_attack_score(ahd, dac) + + return get_attack_roll_outcome(result, modifier) diff --git a/app/functions/roll_mental_attack.py b/app/functions/roll_mental_attack.py index 39c35f2..79924f2 100644 --- a/app/functions/roll_mental_attack.py +++ b/app/functions/roll_mental_attack.py @@ -1,4 +1,4 @@ -from app.functions.roll_dices import roll_dices +from app.functions.get_attack_roll_outcome import get_attack_roll_outcome from app.tables.mentattack import MentalAttackMatrix @@ -8,28 +8,5 @@ def roll_mental_attack(ams, dms, modifier): mam = MentalAttackMatrix() needed = mam.get_attack_score(ams, dms) result["needed"] = needed - outcome = None - if needed < 2: - outcome = "Automatic Success!" - elif needed > 20: - outcome = "Absolutely No Chance!" - else: - raw_roll = roll_dices(1, 20, False).get('result') - if modifier != 0: - result["original-roll"] = raw_roll - result["modifier"] = modifier - rolled = raw_roll + modifier # negative modifiers will subtract themselves naturally - result["adjusted-roll"] = rolled - else: - rolled = raw_roll - result["original-roll"] = rolled + return get_attack_roll_outcome(result, modifier) - if (rolled >= 20) and (needed <= 16): - outcome = "Devastating Success!" - elif needed <= rolled: - outcome = "Attack Successful!" - elif needed > rolled: - outcome = "Attack Failed!" - - result["outcome"] = outcome - return result diff --git a/app/models/models.py b/app/models/models.py index 427384e..1292166 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -66,6 +66,44 @@ hp_model = { 'conscore': conscore_field } + +pa_model = { + 'weapon_attack': fields.Boolean( + required=True, + default=True, + description='Is the attacker using a weapon? If so, To-Hit is based on weapon class.' + ), + 'dac': fields.Integer( + required=True, + min=1, + max=10, + default=1, + description='The defenders armour class. This is needed for both weapon attacks and non-weapon attacks' + ), + 'awc': fields.Integer( + required=False, + min=1, + max=16, + default=1, + description='The attackers weapon class. This is needed for weapon attacks only.' + ), + 'ahd': fields.Integer( + required=False, + min=1, + max=16, + default=1, + description='The attackers hit dice count. This is needed for non-weapon attacks only.' + ), + 'modifier': fields.Integer( + required=False, + min=-100, + max=100, + default=0, + description='The roll modifier to be applied to the hit roll.' + ) +} + + ma_model = { 'ams': fields.Integer( required=True, diff --git a/app/schemas/schemas.py b/app/schemas/schemas.py index be44dfa..a07eef2 100644 --- a/app/schemas/schemas.py +++ b/app/schemas/schemas.py @@ -101,3 +101,39 @@ class MentalAttackSchema(Schema): validate=validate.Range(min=-100, max=100), description='Roll modifier for mental attack' ) + + +class PhysicalAttackSchema(Schema): + weapon_attack = fields.Boolean( + required=True, + default=True, + description="Is the attacker using a weapon? If so, To-Hit is based on weapon class." + ) + dac = fields.Integer( + required=True, + min=1, + max=10, + default=1, + description='The defenders armour class. This is needed for both weapon attacks and non-weapon attacks' + ) + awc = fields.Integer( + required=False, + min=1, + max=16, + default=1, + description='The attackers weapon class. This is needed for weapon attacks only.' + ) + ahd = fields.Integer( + required=False, + min=1, + max=16, + default=1, + description='The attackers hit dice count. This is needed for non-weapon attacks only.' + ) + modifier = fields.Integer( + required=False, + min=-100, + max=100, + default=0, + description='The roll modifier to be applied to the hit roll.' + ) diff --git a/app/tables/physattack.py b/app/tables/physattack.py index c91a633..2ef950e 100644 --- a/app/tables/physattack.py +++ b/app/tables/physattack.py @@ -1,12 +1,77 @@ import pandas as pd -import numpy as np +from math import floor -class WeaponClassAttackMatrix: +class AttackerWeaponClassMatrix: def __init__(self): - data = {} + # Define the range for X-axis (column) and Y-axis (row). + x_range = list(range(1, 17)) # attacker weapon class + y_range = list(range(1, 11)) # defender armour class + + # Provide your static values here as per the intersection + values = [ + # Attacker Weapon class + # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 + [19, 19, 18, 15, 13, 16, 14, 18, 18, 16, 16, 16, 12, 14, 14, 12], # DAC 1 + [17, 18, 17, 14, 12, 15, 13, 17, 16, 15, 15, 15, 11, 13, 13, 11], # DAC 2 + [16, 16, 16, 12, 10, 15, 12, 16, 15, 14, 15, 15, 8, 12, 13, 11], # DAC 3 + [15, 14, 15, 12, 10, 15, 11, 15, 14, 13, 15, 15, 8, 11, 13, 18], # DAC 4 + [14, 13, 14, 12, 10, 15, 10, 14, 13, 12, 14, 15, 8, 11, 13, 11], # DAC 5 + [13, 12, 13, 12, 10, 15, 9, 13, 12, 11, 11, 15, 8, 10, 13, 11], # DAC 6 + [12, 11, 12, 12, 10, 13, 8, 12, 11, 10, 10, 11, 8, 10, 13, 11], # DAC 7 + [11, 10, 11, 12, 10, 13, 7, 11, 10, 9, 9, 9, 8, 9, 13, 11], # DAC 8 + [10, 9, 10, 12, 10, 7, 6, 10, 9, 8, 7, 6, 8, 8, 8, 11], # DAC 9 + [9, 8, 9, 11, 9, 6, 5, 9, 8, 7, 6, 5, 8, 8, 8, 10] # DAC 10 + ] + + # Create the DataFrame. + self.table = pd.DataFrame(values, columns=x_range, index=y_range) + + def get_attack_score(self, awc, dac): + # pandas uses a 'column-major' order + # So, (X,Y) method arguments become (Y,X) + # for pandas locators. + return int(self.table.loc[dac, awc]) + + def get_matrix(self): + return self.table + + def dump_matrix(self): + print(self.table) -class HitDiceAttackMatrix: +class AttackerHitDiceMatrix: def __init__(self): - data = {} + # Define the range for X-axis (column) and Y-axis (row). + x_range = list(range(1, 17)) # attacker weapon class + y_range = list(range(1, 11)) # defender armour class + + values = [ + # Attacker Hit Dice + # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 + [20, 19, 19, 18, 18, 17, 17, 17, 16, 16, 15, 15, 15, 15, 14, 14], # DAC 1 + [19, 18, 18, 17, 17, 16, 16, 16, 15, 15, 14, 14, 14, 14, 13, 13], # DAC 2 + [18, 17, 17, 16, 16, 15, 15, 15, 14, 14, 13, 13, 13, 13, 12, 12], # DAC 3 + [17, 16, 16, 15, 15, 14, 14, 14, 13, 13, 12, 12, 12, 12, 11, 11], # DAC 4 + [16, 15, 15, 14, 14, 13, 13, 13, 12, 12, 11, 11, 11, 11, 10, 10], # DAC 5 + [14, 13, 13, 12, 12, 11, 11, 11, 10, 10, 9, 9, 9, 9, 8, 8], # DAC 6 + [13, 12, 12, 11, 11, 10, 10, 10, 9, 9, 8, 8, 8, 8, 7, 7], # DAC 7 + [12, 11, 11, 10, 10, 9, 9, 9, 8, 8, 7, 7, 7, 7, 6, 6], # DAC 8 + [11, 10, 10, 9, 9, 8, 8, 8, 7, 7, 6, 6, 6, 6, 5, 5], # DAC 9 + [10, 9, 9, 8, 8, 7, 7, 7, 6, 6, 5, 5, 5, 5, 4, 4] # DAC 10 + ] + + # Create the DataFrame. + self.table = pd.DataFrame(values, columns=x_range, index=y_range) + + def get_attack_score(self, ahd, dac): + # pandas uses a 'column-major' order + # So, (X,Y) method arguments become (Y,X) + # for pandas locators. + return int(self.table.loc[dac, ahd]) + + def get_matrix(self): + return self.table + + def dump_matrix(self): + print(self.table)