diff --git a/app/__init__.py b/app/__init__.py index e69de29..0408b19 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -0,0 +1,38 @@ +from flask import Flask +from flask_cors import CORS +from flask_restx import Api + +from config import BASE_DIR, DATA_DIR, MONSTERS_JSON_PATH + +app = Flask(__name__) +CORS(app) +restx_api = Api(app, version='1.0', title='Gamma World Dice', description='Rolled Dice As A Service') + +app.config['DEBUG'] = True +app.config.SWAGGER_UI_JSONEDITOR = True +app.config['SWAGGER_UI_JSONEDITOR'] = True +app.config['BASE_DIR'] = BASE_DIR +app.config['DATA_DIR'] = DATA_DIR +app.config['MONSTERS_JSON_PATH'] = MONSTERS_JSON_PATH + + +def init(): + from app.routes import coinflip, roll_chance, roll_dice, table_views, encounter, character, \ + ability_check, mental_attack, physical_attack, get_monster + # root namespace + restx_api.add_namespace(coinflip.namespace) + # dice namespace + restx_api.add_namespace(roll_dice.namespace) + restx_api.add_namespace(roll_chance.namespace) + # rules namespace + restx_api.add_namespace(table_views.namespace) + restx_api.add_namespace(character.namespace) + restx_api.add_namespace(get_monster.namespace) + # gameplay namespace + restx_api.add_namespace(encounter.namespace) + restx_api.add_namespace(ability_check.namespace) + restx_api.add_namespace(mental_attack.namespace) + restx_api.add_namespace(physical_attack.namespace) + + +init() diff --git a/app/app.py b/app/app.py deleted file mode 100644 index 210156a..0000000 --- a/app/app.py +++ /dev/null @@ -1,237 +0,0 @@ -import random - -from flask import Flask -from flask_cors import CORS -from flask_restx import Api, Resource - -from .functions.roll_dices import roll_dices -from .functions.role_physical_attack import roll_physical_attack -from .functions.roll_mental_attack import roll_mental_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 -from .functions.roll_encounter import roll_encounter -from .functions.roll_mutations import roll_mutations - -from .tables.physattack import AttackerWeaponClassMatrix, AttackerHitDiceMatrix -from .tables.mentattack import MentalAttackMatrix - -from .models.models import dice_model, ability_model, hp_model, character_model, encounter_model, ma_model, \ - mutation_model, check_model, pa_model -from .schemas.schemas import DiceSchema, CharacterSchema, EncounterSchema, MentalAttackSchema, AbilitySchema, \ - HPSchema, MutationSchema, CheckSchema, PhysicalAttackSchema - -app = Flask(__name__) -CORS(app) - -app.config.SWAGGER_UI_JSONEDITOR = True -app.config['SWAGGER_UI_JSONEDITOR'] = True -api = Api(app, version='1.0', title='Gamma World Dice', description='Rolled Dice As A Service') - -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') -check = api.namespace('check', description='Check operations') - -check_model = check.model('Check', check_model) -check_schema = CheckSchema() - -ability_model = ability.model('Ability', ability_model) -ability_schema = AbilitySchema() - -mutation_model = mut.model('Mutation', mutation_model) -mutation_schema = MutationSchema() - -hp_model = hp.model('HP', hp_model) -hp_schema = HPSchema() - -dice_model = dice.model('Dice', dice_model) -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() - -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) - def post(self): - data = api.payload - errors = dice_schema.validate(data) - if errors: - return errors, 400 - quantity = data.get('quantity') - geometry = data.get('geometry') - discard_lowest = data.get('discard_lowest') - if quantity is None or geometry is None: - return {"message": "Required dice data not provided"}, 400 - return roll_dices(quantity, geometry, discard_lowest), 200 - - -@api.route('/roll/ability', methods=['POST']) -class RollAbility(Resource): - @ability.expect(ability_model) - def post(self): - data = api.payload - errors = ability_schema.validate(data) - if errors: - return errors, 400 - - chartype = data.get('chartype') - attribute = data.get('ability') - - return roll_ability_scores(chartype, attribute), 200 - - -@api.route('/roll/hp', methods=['POST']) -class RollHP(Resource): - @hp.expect(hp_model) - def post(self): - data = api.payload - errors = hp_schema.validate(data) - if errors: - return errors, 400 - chartype = data.get('chartype') - conscore = data.get('conscore') - if conscore is None: - return {"message": "A constitution score is required"}, 400 - if chartype == 'human': - geometry = 8 - else: - geometry = 6 - return roll_dices(conscore, geometry, False), 200 - - -@api.route('/roll/encounter', methods=['POST']) -class RollEncounter(Resource): - @encounter.expect(encounter_model) - def post(self): - data = api.payload - errors = encounter_schema.validate(data) - if errors: - return errors, 400 - - terrain = data.get('terrain').lower() - return roll_encounter(terrain), 200 - - -@api.route('/roll/attack/mental', methods=['POST']) -class RollMentalAttack(Resource): - @ma.expect(ma_model) - def post(self): - data = api.payload - errors = ma_schema.validate(data) - if errors: - return errors, 400 - - ams = data.get('ams') - dms = data.get('dms') - modifier = data.get('modifier') - - 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) - def post(self): - data = api.payload - errors = check_schema.validate(data) - if errors: - return errors, 400 - - ability_score = data.get('ability_score') - multiplier = data.get('multiplier') - - return roll_ability_check(ability_score, multiplier), 200 - - -@api.route('/roll/mutations', methods=['POST']) -class RollMutations(Resource): - @mut.expect(mutation_model) - def post(self): - data = api.payload - errors = mutation_schema.validate(data) - if errors: - return errors, 400 - - conscore = data.get('conscore') - intscore = data.get('intscore') - - return roll_mutations(conscore, intscore), 200 - - -@api.route('/character/generate', methods=['POST']) -class GenerateCharacter(Resource): - @character.expect(character_model) - def post(self): - data = api.payload - errors = character_schema.validate(data) - if errors: - return errors, 400 - chartype = data.get('chartype') - return build_character_sheet(chartype), 200 - - -@api.route('/matrices/dump', methods=['GET']) -class DumpMatrices(Resource): - def get(self): - awc_table = AttackerWeaponClassMatrix().get_matrix().to_json(orient='index') - ahd_table = AttackerHitDiceMatrix().get_matrix().to_json(orient='index') - mat_table = MentalAttackMatrix().get_matrix().to_json(orient='index') - - return { - "Weapon Attack Table": awc_table, - "Non-Weapon Attack Table": ahd_table, - "Mental Attack Table": mat_table - }, 200 - - -if __name__ == '__main__': - app.run() diff --git a/app/functions/role_physical_attack.py b/app/functions/role_physical_attack.py deleted file mode 100644 index 72f3887..0000000 --- a/app/functions/role_physical_attack.py +++ /dev/null @@ -1,32 +0,0 @@ -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_physical_attack.py b/app/functions/roll_physical_attack.py new file mode 100644 index 0000000..4d6366d --- /dev/null +++ b/app/functions/roll_physical_attack.py @@ -0,0 +1,27 @@ +from app.functions.get_attack_roll_outcome import get_attack_roll_outcome +from app.tables.physattack import AttackerHitDiceMatrix, AttackerWeaponClassMatrix + + +def roll_physical_attack(dac, modifier=0, awc=None, ahd=None): + """ + :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 isinstance(awc, int): + hit_table = AttackerWeaponClassMatrix() + result["needed"] = hit_table.get_attack_score(awc, dac) + elif isinstance(ahd, int): + hit_table = AttackerHitDiceMatrix() + result["needed"] = hit_table.get_attack_score(ahd, dac) + # use non-weapon attack lookup table + else: + # handle error state where neither awc nor ahd are integers (None or other non-integer value) + result["outcome"] = "Attacker Hit Dice is required for Non-Weapon Attacks!" + return result + + return get_attack_roll_outcome(result, modifier) diff --git a/app/models/models.py b/app/models/models.py index 1292166..7ba8562 100644 --- a/app/models/models.py +++ b/app/models/models.py @@ -131,11 +131,3 @@ character_model = { 'chartype': chartype_field, } -encounter_model = { - 'terrain': fields.String( - required=True, - default="clear", - description='The terrain being traversed by the party when the encounter roll is made. Valid values are: ' - '"clear", "mountains", "forest", "desert", "watery", "ruins", "deathlands"' - ) -} diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/ability_check.py b/app/routes/ability_check.py new file mode 100644 index 0000000..710506c --- /dev/null +++ b/app/routes/ability_check.py @@ -0,0 +1,20 @@ +from flask import request +from flask_restx import Resource, Namespace + +from app.functions.roll_ability_check import roll_ability_check + +namespace = Namespace('gameplay', description='Gamma World Rules') + + +@namespace.route('/ability/check') # resolves to: /gameplay/ability/check +class AbilityCheck(Resource): + @namespace.doc(params={'score': 'The ability score', 'multiplier': 'Score multiplier for the attempt.'}) + def get(self): + score = request.args.get('score', type=int) + multiplier = request.args.get('multiplier', type=float) + + if not score or not multiplier: + return {'error': 'Both score and multiplier parameters are required.'}, 400 + + outcome = roll_ability_check(score, multiplier) + return outcome, 200 diff --git a/app/routes/character.py b/app/routes/character.py new file mode 100644 index 0000000..0981887 --- /dev/null +++ b/app/routes/character.py @@ -0,0 +1,25 @@ +from flask import request +from flask_restx import Resource, Namespace + +from app.functions.build_character_sheet import build_character_sheet + +namespace = Namespace('rules', description='Gamma World Rules') + +VALID_CHARTYPES = ["human", "humanoid", "mutant", "cyborg"] + + +@namespace.route('/character') # resolves to: /rules/character +class GenerateCharacter(Resource): + @namespace.doc(params={'chartype': 'The Character Type for the new character'}) + def get(self): + chartype = request.args.get('chartype', default='Human', type=str) + if chartype: + if chartype.lower() in VALID_CHARTYPES: + return build_character_sheet(chartype.lower()), 200 + else: + return { + 'error': 'Invalid character type provided.', + 'valid_chartypes': VALID_CHARTYPES + }, 400 + else: + return {'error': 'No character type provided', 'valid_chartypes': VALID_CHARTYPES}, 400 diff --git a/app/routes/coinflip.py b/app/routes/coinflip.py new file mode 100644 index 0000000..b3a1848 --- /dev/null +++ b/app/routes/coinflip.py @@ -0,0 +1,11 @@ +from flask_restx import Resource, Namespace +import random + +namespace = Namespace('coinflip', description='Coinflip related operations') + + +@namespace.route('/', methods=['GET']) # resolves to /coinflip +class RollCoinflip(Resource): + @staticmethod + def get(): + return random.choice(['Heads', 'Tails']), 200 diff --git a/app/routes/encounter.py b/app/routes/encounter.py new file mode 100644 index 0000000..56204d6 --- /dev/null +++ b/app/routes/encounter.py @@ -0,0 +1,26 @@ +from flask import request +from flask_restx import Resource, Namespace + +from app.functions.roll_encounter import roll_encounter + +namespace = Namespace('gameplay', description='Gamma World Game Play') + +VALID_TERRAINS = ["clear", "mountains", "forest", "desert", "watery", "ruins", "deathlands"] + + +@namespace.route('/encounter') # resolves to: /gameplay/encounter +class RollEncounter(Resource): + @namespace.doc(params={'terrain': 'The terrain type for the encounter'}) + def get(self): + terrain = request.args.get('terrain', default=None, type=str) + + if terrain: + if terrain.lower() in VALID_TERRAINS: + return roll_encounter(terrain.lower()), 200 + else: + return { + 'error': 'Invalid terrain type provided.', + 'valid_terrains': VALID_TERRAINS + }, 400 + else: + return {'error': 'No terrain type provided', 'valid_terrains': VALID_TERRAINS}, 400 diff --git a/app/routes/get_monster.py b/app/routes/get_monster.py new file mode 100644 index 0000000..ae55e67 --- /dev/null +++ b/app/routes/get_monster.py @@ -0,0 +1,19 @@ +from flask import request +from flask_restx import Resource, Namespace + +from app.tables.monsters import Monsters + +namespace = Namespace('rules', description='Gamma World Rules') + + +@namespace.route('/creature') # resolves to: /gameplay/encounter +class RollEncounter(Resource): + @namespace.doc(params={'creature': 'The terrain type for the encounter'}) + def get(self): + creature = request.args.get('creature', default=None, type=str) + + if creature is None: + return {'error': 'Provide the name of a Gamma World creature to search for'}, 400 + + monsters = Monsters() + return monsters.get_monster(creature), 200 diff --git a/app/routes/mental_attack.py b/app/routes/mental_attack.py new file mode 100644 index 0000000..7fe089f --- /dev/null +++ b/app/routes/mental_attack.py @@ -0,0 +1,19 @@ +from flask import request +from flask_restx import Resource, Namespace + +from app.functions.roll_mental_attack import roll_mental_attack + +namespace = Namespace('gameplay', description='Gamma World Rules') + + +@namespace.route('/attack/mental') # resolves to: /gameplay/attack/mental +class MentalAttack(Resource): + @namespace.doc(params={'ams': 'Attacker Mental Strength', + 'dms': 'Defender Mental Strength', + 'modifier': 'Roll Modifier'}) + def get(self): + ams = request.args.get('ams', type=int) + dms = request.args.get('dms', type=int) + modifier = request.args.get('modifier', type=int) + # Validate params here + return roll_mental_attack(ams, dms, modifier), 200 diff --git a/app/routes/physical_attack.py b/app/routes/physical_attack.py new file mode 100644 index 0000000..0429204 --- /dev/null +++ b/app/routes/physical_attack.py @@ -0,0 +1,30 @@ +from flask import request +from flask_restx import Resource, Namespace + +from app.functions.roll_physical_attack import roll_physical_attack + +namespace = Namespace('gameplay', description='Gamma World Rules') + + +@namespace.route('/attack/physical') # resolves to: /gameplay/attack/physical +class PhysicalAttack(Resource): + @namespace.doc( + params={'dac': 'REQUIRED: Needed for both attacks', + 'modifier': 'OPTIONAL: Roll Modifier', + 'awc': 'OPTIONAL(*): Attacker Weapon Class. Only needed for weapon attacks', + 'ahd': 'OPTIONAL(*): Attacker Hit Dice. Only needed for non-weapon attacks'}) + def get(self): + dac = request.args.get('dac', type=int) + awc = request.args.get('awc', type=int) + ahd = request.args.get('ahd', type=int) + modifier = request.args.get('modifier', type=int) + + # Request validation + if dac is None: + return {"error": "'dac' parameter is needed"}, 400 + if (awc is None and ahd is None) or (awc is not None and ahd is not None): + return {"error": "Exactly one of 'awc' or 'ahd' parameters must be provided"}, 400 + + # Call to business logic after validation, could be placed in try-except block for handling exceptions if any + result = roll_physical_attack(dac, modifier, awc, ahd) + return result, 200 diff --git a/app/routes/roll_chance.py b/app/routes/roll_chance.py new file mode 100644 index 0000000..79a6406 --- /dev/null +++ b/app/routes/roll_chance.py @@ -0,0 +1,11 @@ +from flask_restx import Resource, Namespace +from app.functions.roll_dices import roll_dices + +namespace = Namespace('dice', description='Roll Chance Operations') + + +@namespace.route('/probability', methods=['GET']) # resolves to: /dice/probability +class RollChance(Resource): + @staticmethod + def get(): + return roll_dices(1, 100, False), 200 diff --git a/app/routes/roll_dice.py b/app/routes/roll_dice.py new file mode 100644 index 0000000..e8ae163 --- /dev/null +++ b/app/routes/roll_dice.py @@ -0,0 +1,24 @@ +from flask_restx import Resource, Namespace +from app.functions.roll_dices import roll_dices +from app.models.models import dice_model +from app.schemas.schemas import DiceSchema + +namespace = Namespace('dice', description='Dice Operations') +dice_model = namespace.model('Dice', dice_model) +dice_schema = DiceSchema() + + +@namespace.route('/') # resolves to: /dice +class RollDice(Resource): + @namespace.expect(dice_model) + def post(self): + data = namespace.payload + errors = dice_schema.validate(data) + if errors: + return errors, 400 + quantity = data.get('quantity') + geometry = data.get('geometry') + discard_lowest = data.get('discard_lowest') + if quantity is None or geometry is None: + return {"message": "Required dice data not provided"}, 400 + return roll_dices(quantity, geometry, discard_lowest), 200 diff --git a/app/routes/table_views.py b/app/routes/table_views.py new file mode 100644 index 0000000..f035c73 --- /dev/null +++ b/app/routes/table_views.py @@ -0,0 +1,19 @@ +from flask_restx import Resource, Namespace +from app.tables.mentattack import MentalAttackMatrix +from app.tables.physattack import AttackerWeaponClassMatrix, AttackerHitDiceMatrix + +namespace = Namespace('rules', description='Gamma World Rules') + + +@namespace.route('/tables') # resolves to /rules/tables +class DumpMatrices(Resource): + def get(self): + awc_table = AttackerWeaponClassMatrix().get_matrix().to_json(orient='index') + ahd_table = AttackerHitDiceMatrix().get_matrix().to_json(orient='index') + mat_table = MentalAttackMatrix().get_matrix().to_json(orient='index') + + return { + "Weapon Attack Table": awc_table, + "Non-Weapon Attack Table": ahd_table, + "Mental Attack Table": mat_table + }, 200 diff --git a/app/schemas/schemas.py b/app/schemas/schemas.py index a07eef2..163aa9c 100644 --- a/app/schemas/schemas.py +++ b/app/schemas/schemas.py @@ -75,15 +75,6 @@ class CheckSchema(Schema): ) -class EncounterSchema(Schema): - terrain = fields.String( - required=True, - default="clear", - validate=validate.OneOf(["clear", "mountains", "forest", "desert", "watery", "ruins", "deathlands"]), - description='The terrain traversed at the time of the encounter roll' - ) - - class MentalAttackSchema(Schema): ams = fields.Integer( required=True, diff --git a/app/tables/monsters.json b/app/tables/monsters.json new file mode 100644 index 0000000..9d86e7e --- /dev/null +++ b/app/tables/monsters.json @@ -0,0 +1,371 @@ +{ + "android": { + "thinker": { + "number": [1, 4, 0], + "morale": [1, 4, 6], + "armour": 9, + "hit dice": [8, 10, 0], + "environ": ["land"], + "land speed": [12, 900, 18], + "ms": [1, 101, 11], + "in": [1, 101, 11], + "dx": [5, 4, 0], + "ch": [1, 10, 11], + "cn": [1, 10, 11], + "ps": [1, 10, 8], + "attacks": null, + "mutations": null, + "description": "Androids are man-made beings who look exactly like Pure Strain Humans and are often mistaken for them. They usually consider Pure Strain Humans to be enemies. All Androids wear Tech Level III armour. Warriors and Thinkers carry Tech Level III weapons." + }, + "worker": { + "number": [1, 6, 2], + "morale": [1, 4, 0], + "armour": 8, + "hit dice": [7, 10, 0], + "environ": ["land"], + "land speed": [12, 900, 18], + "ms": [5, 4, 0], + "in": [1, 10, 2], + "dx": [5, 4, 0], + "ch": [1, 10, 2], + "cn": [1, 10, 11], + "ps": [1, 10, 11], + "attacks": null, + "mutations": null, + "description": "Androids are man-made beings who look exactly like Pure Strain Humans and are often mistaken for them. They usually consider Pure Strain Humans to be enemies. All Androids wear Tech Level III armour. Warriors and Thinkers carry Tech Level III weapons." + }, + "warrior": { + "number": [1, 6, 0], + "morale": [1, 6, 4], + "armour": 7, + "hit dice": [10, 10, 0], + "environ": ["land"], + "land speed": [12, 900, 36], + "ms": [1, 10, 11], + "in": [1, 10, 8], + "dx": [1, 10, 11], + "ch": [1, 10, 11], + "cn": [1, 10, 11], + "ps": [1, 10, 11], + "attacks": null, + "mutations": null, + "description": "Androids are man-made beings who look exactly like Pure Strain Humans and are often mistaken for them. They usually consider Pure Strain Humans to be enemies. All Androids wear Tech Level III armour. Warriors and Thinkers carry Tech Level III weapons." + } + }, + "badder": { + "number": [3, 6, 0], + "morale": [2, 4, 0], + "hit dice": [6, 6, 0], + "armour": 5, + "environ": ["land"], + "land speed": [12, 900, 18], + "ms": [1, 10, 8], + "in": [3, 6, 0], + "dx": [1, 10, 11], + "ch": [1, 10, 2], + "cn": [1, 6, 8], + "ps": [1, 10, 5], + "attacks": {"bite": [1, 6, 0]}, + "mutations": ["Empathy"], + "description": "1.5 meter tall bipedal mutated badgers. They inhabit temperate areas. Organized into Tech Level II societies run by their 'nobility'. 10% chance of each badder in a party having 1 Tech Level III weapon. 10d10 males of fighting age live in tunnels under their villages." + + }, + "ark": { + "number": [1, 4, 0], + "morale": [1, 8, 2], + "hit dice": [8, 6, 0], + "armour": 5, + "environ": ["land"], + "land speed": [12, 900, 36], + "ms": [5, 4, 0], + "in": [1, 10, 2], + "dx": [1, 12, 4], + "ch": [1, 10, 2], + "cn": [5, 4, 0], + "ps": [1, 10, 11], + "attacks": {"bite": [1, 6, 0]}, + "mutations": ["Telekinesis", "Weather Manipulation", "Life Leach"], + "description": "[Hound Folk] These intelligent man-dogs grow up to 3 meters high [standing on their hind legs]. They are ferocious enemies, but have a deathly fear of large winged creatures. Arks carry Tech Level I weapons and most wear leather or studded leather armour and carry shields. They consider human [or humanoid] hands to be a particular delicacy." + }, + "barl nep": { + "number": [1, 4, -2], + "morale": [1, 4, 0], + "hit dice": [20, 4, 0], + "armour": 4, + "environ": ["water"], + "water speed": [24, 1800, 36], + "ms": [1, 4, 0], + "in": [1, 4, 0], + "dx": [1, 10, 2], + "ch": [1, 4, 0], + "cn": [5, 4, 0], + "ps": [1, 12, 4], + "attacks": {"bite": [1, 4, 0]}, + "mutations": null, + "description": "This black predator fish often grows to a length of 1.5 meters. Once per day it can secrete intensity level 18 radioactive oil over an area of 9 meters square. This slick lasts 10 minutes. If killed before it uses the days allotment of oil, the Barl Nep's oil may be extracted and used as a weapon [but will have to be carried in a lead-lined container]. Oil extracted from the Barl Nep will be only intensity level 12." + }, + "arn": { + "number": [1, 6, 0], + "morale": [1, 4, 3], + "hit dice": [8, 4, 0], + "armour": 9, + "environ": ["land", "air"], + "land speed": [12, 180, 3], + "water speed": [24, 900, 18], + "ms": [1, 8, 1], + "in": [1, 4, 0], + "dx": [1, 10, 2], + "ch": [1, 4, 0], + "cn": [1, 12, 4], + "ps": [1, 8, 2], + "attacks": {"bite": [2, 6, 0]}, + "mutations": null, + "description": "[Dragon Bugs] These 1 to 2-meter long mutated dragonflies can carry loads weighing up to twice their PS in kilograms while airborne, but not while crawling along the ground. They are often captured and domesticated as flying steeds by small Humanoids and Mutated Animals. However, they must be caught young to be trainable." + }, + "ber lep": { + "number": [1, 8, 0], + "morale": [1, 4, 6], + "hit dice": [15, 4, 0], + "armour": 6, + "environ": ["water"], + "water speed": null, + "ms": [1, 4, 0], + "in": [1, 4, 0], + "dx": [1, 4, 0], + "ch": [1, 4, 0], + "cn": [1, 10, 11], + "ps": [1, 12, 4], + "attacks": {"acid": [2, 8, 0]}, + "mutations": null, + "description": "[Sweetpads] This 2-meter diameter, free-floating aquatic plant rests on the surface of the water much like a lily pad [to which it is related]. It will support the weight of a normal human, but pressure on the center of the pad causes it to snap shut around the trespasser. The plant secretes a sweet-smelling acid which attracts and gradually dissolves its prey. [HOUSE NOTE: PLAYERS SHOULD MAKE A DEX CHECK WHEN SNARED, TO SEE IF THEY ESCAPE, AND TAKE DAMAGE FOR EVERY ROUND THE CHECK FAILS - GMG]" + }, + "blaash": { + "number": [1, 10, 0], + "morale": [1, 4, 6], + "hit dice": [15, 4, 0], + "armour": 8, + "environ": ["land", "air"], + "land speed": [0, 60, 6], + "air speed": [6, 900, 18], + "ms": [1, 4, 0], + "in": [1, 4, 0], + "dx": [1, 6, 4], + "ch": [1, 4, 0], + "cn": [1, 10, 5], + "ps": [1, 6, 4], + "attacks": {"radiation": [0, 0, 0]}, + "mutations": null, + "description": "[Gamma Moths] This mutated gypsy moth often grows to 1 meter in length [with a 2 meter wingspan]. It is fearless and quite carnivorous. When attacking, the intensity 18 radiation it emits from its abdomen causes it to glow brightly. All parties within 6 meters must make a Radiation Check." + }, + "blood bird": { + "number": [1, 4, -2], + "morale": [1, 4, 3], + "hit dice": [3, 6, 0], + "armour": 4, + "environ": ["air"], + "air speed": [6, 900, 18], + "ms": [1, 10, 11], + "in": [1, 4, 0], + "dx": [1, 10, 2], + "ch": [1, 4, 0], + "cn": [1, 10, 8], + "ps": [1, 10, 5], + "attacks": {"bite": [1, 4, 0]}, + "mutations": null, + "description": "[Red Deaths] Mutated Scarlet Tanager. Emits intensity level 10 radiation. Parties within 6 meters must make a Radiation Check each Action Turn. 100% resistant to mental attacks. 1m tall." + }, + "blackun": { + "number": [1, 4, -1], + "morale": [1, 4, 5], + "hit dice": [5, 8, 0], + "armour": 3, + "environ": ["land"], + "land speed": [3, 900, 18], + "ms": [1, 10, 11], + "in": [1, 4, 0], + "dx": [1, 10, 5], + "ch": [1, 4, 0], + "cn": [1, 10, 5], + "ps": [1, 10, 11], + "attacks": {"bite": [1, 4, 0]}, + "mutations": ["Electrical Generation"], + "description": "[Attercops] This mutated garden spider stands 1.5 meters at the shoulder. It is unaffected by mental attacks EXCEPT ILLUSIONS. It uses an electrical jolt to stun prey which it then trusses in a sticky web. Blackun webs have been known to reach a diameter of 60 meters." + }, + "brutorz": { + "number": [2, 6, 0], + "morale": [1, 6, 4], + "hit dice": [14, 6, 0], + "armour": 7, + "environ": ["land"], + "land speed": [6, 900, 18], + "ms": [1, 10, 5], + "in": [1, 10, 8], + "dx": [1, 12, 4], + "ch": [1, 10, 2], + "cn": [1, 10, 8], + "ps": [2, 20, 60], + "attacks": {"bite": [3, 6, 0], "kick1": [2, 6, 0], "kick2": [2, 6, 0]}, + "mutations": ["Electrical Generation"], + "description": "[Big Walkers] Standing 2 meters high at the shoulder, this mutated Percheron is heavily-muscled and can carry 5 times its PS for long distances without tiring. It is surprisingly agile considering it's 1,000 kilograms of bulk. Brutorz willingly serve as riding, pack, and dray animals if well-treated, but will turn on a cruel master." + }, + "blight": { + "number": [1, 4, 0], + "morale": [1, 4, 6], + "hit dice": [12, 6, 0], + "armour": 9, + "environ": ["land", "air"], + "land speed": [0, 300, 6], + "air speed": [2, 900, 18], + "ms": [1, 10, 5], + "in": [1, 10, 2], + "dx": [1, 10, 8], + "ch": [1, 4, 0], + "cn": [1, 10, 11], + "ps": [1, 8, 12], + "attacks": {"bite": [3, 6, 0], "squeeze": [5, 6, 0]}, + "mutations": ["Light Generation"], + "description": "[Cloud Worms] These 3 meter long, carnivorous, winged worms have a 9 meter wingspan. Blights secrete a substance which bends light, causing them to be invisible wherever their skin is coated with the substance. Rain will wash the oil away and takes a full day to replenish the protective coating in such cases. Blights are completely resistant to weapons involving radiation, heat, or sonic effects. Their preferred method of attack is to wrap themselves around a victim, constricting and biting [counts as 1 action, but is resolved as two separate attacks]." + }, + "cal then": { + "number": [1, 4, -2], + "morale": [1, 6, 4], + "hit dice": [6, 8, 0], + "armour": 9, + "environ": ["land", "air"], + "land speed": [0, 300, 6], + "air speed": [6, 600, 12], + "ms": [1, 10, 11], + "in": [1, 10, 8], + "dx": [1, 10, 5], + "ch": [1, 4, 0], + "cn": [1, 10, 5], + "ps": [1, 10, 8], + "attacks": {"bite": [10, 6, 0]}, + "mutations": null, + "description": "[Flying Rippers] This intelligent mutated insect often reaches a length of 2.5 meters. It is immune to weapons using heat or cold. The Cal Then feeds on bone marrow and will rip through anything [even duralloy, given time] to get at fresh bone." + }, + "carrin": { + "number": [1, 4, -2], + "morale": [1, 4, 6], + "hit dice": [15, 8, 0], + "armour": 7, + "environ": ["land", "air"], + "land speed": [2, 300, 6], + "air speed": [12, 900, 18], + "ms": [1, 10, 8], + "in": [1, 10, 11], + "dx": [3, 6, 0], + "ch": [1, 10, 11], + "cn": [1, 10, 11], + "ps": [2, 20, 20], + "attacks": {"poison": [0, 0, 0]}, + "mutations": ["Heightened Intelligence", "Telepathy", "Mental Shield", "Genius Capability", "Quills"], + "description": "[Dark Emperors] Carrins are 3 meter tall mutated vultures weighing about 50 kilograms. Each Carrin has 1d4 Blood Bird followers. They are highly intelligent. Their quills are coated with an intensity level 12 contact poison to which they are immune. [HOUSE NOTE: Poison Check On Contact - GMG]" + }, + "centisteed": { + "number": [1, 4, 0], + "morale": [1, 4, 0], + "hit dice": [7, 10, 0], + "armour": 9, + "environ": ["land"], + "land speed": [12, 1800, 36], + "ms": [1, 10, 11], + "in": [1, 4, 0], + "dx": [1, 8, 1], + "ch": [1, 4, 0], + "cn": [1, 10, 5], + "ps": [2, 20, 30], + "attacks": null, + "mutations": ["Increased Metabolism", "Force Field Generation"], + "description": "[Fast Trotters] Centisteeds are mutated horses of insectoid appearance. Each has between 12 and 18 legs and can carry 2 human sized characters. One rider must concentrate at all times on controlling the mount or it will try to throw [and then trample] the riders. [HOUSE NOTE: Mental Strength Ability Check every round. - GMG]" + }, + "dabber": { + "number": [1, 8, 0], + "morale": [1, 4, 6], + "hit dice": [4, 6, 0], + "armour": 5, + "environ": ["land"], + "land speed": [6, 600, 12], + "ms": [1, 10, 8], + "in": [1, 10, 5], + "dx": [1, 10, 5], + "ch": [1, 8, 1], + "cn": [1, 10, 2], + "ps": [1, 10, 5], + "attacks": null, + "mutations": ["Empathy", "Illusion Generation", "Light Generation", "Repulsion Field", + "Telekinesis", "Telepathy"], + "description": "[Brown Beggars] These highly intelligent 1 meter tall mutated racoons walk upright and have manipulative paws. They are usually found in small family groups and will often have Tech evel III equipment including some weapons but no armour." + }, + "cren tosh": { + "number": [1, 4, 2], + "morale": [1, 4, 0], + "hit dice": [16, 6, 0], + "armour": 3, + "environ": ["land", "water"], + "land speed": [12, 900, 18], + "water speed": [6, 1800, 36], + "ms": [1, 10, 2], + "in": [1, 4, 0], + "dx": [1, 8, 1], + "ch": [1, 4, 0], + "cn": [1, 10, 2], + "ps": [1, 10, 8], + "attacks": {"bite": [1,4,0]}, + "mutations": null, + "description": "[Lizard Fish] This mutated lizard-fish prefers to live in water as a 2-meter long fish, but can transform itself into any lizard of about the same size (complete with all lizard characteristics) for up to 24 hours at a time. This power may only be used once per day. In fish form, it lives under overhanging banks and lines its nests with shiny objects. It is a vegetarian." + }, + "ert": { + "number": [1, 4, -2], + "morale": [1, 4, 0], + "hit dice": [3, 8, 0], + "armour": 9, + "environ": ["water"], + "water speed": [6, 1800, 36], + "ms": [1, 4, 0], + "in": [1, 4, 0], + "dx": [1, 10, 2], + "ch": [1, 4, 0], + "cn": [1, 10, 2], + "ps": [1, 10, 2], + "attacks": {"poison": [0,0,0]}, + "mutations": null, + "description": "[Stone Fish] This 1-meter long fish injects a chemical into those it bites, causing them to petrify and turn to stone within 60 seconds. For purposes of deciding if a character suffers this result, treat the chemical as intensity level 12 poison. Characters turn to stone on a 'D' result. Other results are ignored. [HOUSE NOTE: Intensity Level 10. I don't want my players constantly dying - GMG]" + }, + "ert telden": { + "number": [1, 6, 0], + "morale": [1, 4, 0], + "hit dice": [12, 4, 0], + "armour": 9, + "environ": ["water"], + "water speed": [6, 900, 18], + "ms": [1, 4, 0], + "in": [1, 4, 0], + "dx": [1, 8, 1], + "ch": [1, 4, 0], + "cn": [1, 10, 2], + "ps": [1, 12, 4], + "attacks": {"fireball": [10,6,0]}, + "mutations": null, + "description": "[Fire Fish] This 1-meter long fish lives in backwaters and marshes. It secretes a substance which makes it burst into flame 5d6 seconds after being removed from water and exposed to air. The super-heated fish does 10d6 damage to those within 30 meters when it bursts into flames" + }, + "crep plant": { + "number": [1, 4, 0], + "morale": [1, 4, 6], + "hit dice": [15, 4, 0], + "armour": 3, + "environ": ["land","water"], + "land speed": [0, 120, 3], + "water speed": [0, 120, 3], + "ms": [1, 4, 0], + "in": [1, 4, 0], + "dx": [1, 8, 1], + "ch": [1, 4, 0], + "cn": [1, 10, 2], + "ps": [1, 12, 4], + "attacks": {"Life Leach": [0,0,0]}, + "mutations": ["Death Field Generation", "Life Leach", "Manipulative Vines", "Mobility", + "Molecular Disruption", "Symbiotic Attachment"], + "description": "[Red Crep | Pink Crep] Creps come in 2 varieties: the Water Crep (also called the Pink Crep) and the Land Crep (also called the Red Crep). Water Creps live totally submerged and Land Creps grow under a mat of other foliage. Both are carnivorous, using their broad flat leaves to feed by Life Leaching those with whom they come into contact. Leaves that have been used to feed drop off once the victim escapes or dies, eventually sprouting new plants. [HOUSE NOTE: Players snared by a Crep Plant must make a Dex Check to escape. Life Leach attacks continue for each round the Dex Check fails. - GMG]" + } +} \ No newline at end of file diff --git a/app/tables/monsters.py b/app/tables/monsters.py new file mode 100644 index 0000000..7e12163 --- /dev/null +++ b/app/tables/monsters.py @@ -0,0 +1,28 @@ +import json + + +class Monsters: + monsters = None + + @staticmethod + def load_monsters_data(): + """Loads the monsters data from a JSON file""" + with open('app/tables/monsters.json') as f: + Monsters.monsters = json.load(f) + + def __init__(self): + if not Monsters.monsters: + self.load_monsters_data() + + def get_monster(self, monster_name): + """Returns the dictionary of the specified monster.""" + return self.monsters.get(monster_name) + + def add_monster(self, monster_name, attributes): + """Adds a new monster to the monsters dictionary.""" + self.monsters[monster_name] = attributes + + def remove_monster(self, monster_name): + """Removes a monster from the monsters dictionary.""" + if monster_name in self.monsters: + del self.monsters[monster_name] diff --git a/config.py b/config.py new file mode 100644 index 0000000..fb5bb62 --- /dev/null +++ b/config.py @@ -0,0 +1,6 @@ +# config.py +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +DATA_DIR = os.path.join(BASE_DIR, 'app/tables') +MONSTERS_JSON_PATH = os.path.join(DATA_DIR, 'monsters.json') diff --git a/run.py b/run.py new file mode 100644 index 0000000..c40aff5 --- /dev/null +++ b/run.py @@ -0,0 +1,5 @@ +# run.py +from app import app + +if __name__ == "__main__": + app.run(debug=True)