yet another GARGANTUAN refactor. Completely harmonized with REST conventions now.

This commit is contained in:
Greg Gauthier 2024-07-01 01:56:11 +01:00
parent 2767a438f3
commit f3ec123d6b
21 changed files with 679 additions and 286 deletions

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -131,11 +131,3 @@ character_model = {
'chartype': chartype_field, '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"'
)
}

0
app/routes/__init__.py Normal file
View File

View File

@ -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

25
app/routes/character.py Normal file
View File

@ -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

11
app/routes/coinflip.py Normal file
View File

@ -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

26
app/routes/encounter.py Normal file
View File

@ -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

19
app/routes/get_monster.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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

11
app/routes/roll_chance.py Normal file
View File

@ -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

24
app/routes/roll_dice.py Normal file
View File

@ -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

19
app/routes/table_views.py Normal file
View File

@ -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

View File

@ -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): class MentalAttackSchema(Schema):
ams = fields.Integer( ams = fields.Integer(
required=True, required=True,

371
app/tables/monsters.json Normal file
View File

@ -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]"
}
}

28
app/tables/monsters.py Normal file
View File

@ -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]

6
config.py Normal file
View File

@ -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')

5
run.py Normal file
View File

@ -0,0 +1,5 @@
# run.py
from app import app
if __name__ == "__main__":
app.run(debug=True)