From 3cc5bb079dc43675482e19b195742203dc7b78aa Mon Sep 17 00:00:00 2001 From: Brandon Cornejo Date: Sat, 9 May 2020 21:36:30 -0500 Subject: [PATCH] Added spells data, names spreadsheet --- .gitignore | 1 + acks/__init__.py | 2 +- acks/npc/api.py | 10 +- acks/npc/commands.py | 12 +- acks/npc/models.py | 135 +++++++++++++++++++-- acks/npc/npc_party.py | 74 +++++++++-- acks/npc/templates/generate_npc_party.html | 104 ++++++++++++---- acks/npc/templates/spell_list.html | 73 +++++++++++ acks/npc/views.py | 6 + acks/templates/base.html | 6 +- acks/views.py | 5 +- uwsgi.ini | 12 +- wsgi.py | 4 + 13 files changed, 393 insertions(+), 51 deletions(-) create mode 100644 acks/npc/templates/spell_list.html create mode 100644 wsgi.py diff --git a/.gitignore b/.gitignore index 8c3bc09..13800db 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ pyvenv.cfg # App specific data config +logs diff --git a/acks/__init__.py b/acks/__init__.py index 142cbc3..3e85d55 100644 --- a/acks/__init__.py +++ b/acks/__init__.py @@ -38,7 +38,7 @@ def create_app(): # Initialize API and load resources from flask_potion import Api, ModelResource - api = Api(app) + api = Api(app, prefix='/api') # from acks.api import api # api.init_app(app) diff --git a/acks/npc/api.py b/acks/npc/api.py index 90de548..4afb7e2 100644 --- a/acks/npc/api.py +++ b/acks/npc/api.py @@ -4,7 +4,8 @@ from .models import ( EquipmentArmour, EquipmentMeleeWeapon, EquipmentRangedWeapon, - CharacterNPC + CharacterNPC, + Spell ) class CharacterClassResource(BaseModelResource): @@ -28,11 +29,16 @@ class CharacterNPCResource(BaseModelResource): class Meta(BaseModelResource.Meta): model = CharacterNPC +class SpellResource(BaseModelResource): + class Meta(BaseModelResource.Meta): + model = Spell + resources = [ CharacterClassResource, EquipmentArmourResource, EquipmentMeleeWeaponResource, EquipmentRangedWeaponResource, - CharacterNPCResource + CharacterNPCResource, + SpellResource ] diff --git a/acks/npc/commands.py b/acks/npc/commands.py index fb86b1d..1a7e310 100644 --- a/acks/npc/commands.py +++ b/acks/npc/commands.py @@ -14,7 +14,8 @@ def populate_npc_database(): ClassLevelProgression, EquipmentArmour, EquipmentRangedWeapon, - EquipmentMeleeWeapon + EquipmentMeleeWeapon, + Spell ) def load_csv_data(file_name, cls): @@ -47,4 +48,13 @@ def populate_npc_database(): prog.guild_id = classes[prog.guild_id] db.session.bulk_save_objects(progressions) + # Spells + spells = load_csv_data('default_spells.csv', Spell) + for spell in spells: + if spell.arcane == '': + spell.arcane = 0 + if spell.divine == '': + spell.divine = 0 + db.session.bulk_save_objects(spells) + db.session.commit() diff --git a/acks/npc/models.py b/acks/npc/models.py index 6d4bf64..1da0201 100644 --- a/acks/npc/models.py +++ b/acks/npc/models.py @@ -6,6 +6,7 @@ class CharacterClass(BaseModel): __tablename__ = 'character_class' name = db.Column(db.String(50), unique=True, nullable=False) + spellcaster = db.Column(db.String(10)) bucket = db.Column(db.String(50)) frequency_modifier = db.Column(db.Integer, default=1) @@ -21,7 +22,15 @@ class CharacterClass(BaseModel): ranged_heavy = db.Column(db.Integer) def __repr__(self): - return ''.format(self.name) + return ''.format(self.name) + + @property + def is_divine_spellcaster(self): + return self.spellcaster == 'Divine' + + @property + def is_arcane_spellcaster(self): + return self.spellcaster == 'Arcane' class ClassLevelProgression(BaseModel): __tablename__ = 'class_progression' @@ -35,12 +44,85 @@ class ClassLevelProgression(BaseModel): save_staffs_wands = db.Column(db.Integer) save_spells = db.Column(db.Integer) + spellslots1 = db.Column(db.Integer) + spellslots2 = db.Column(db.Integer) + spellslots3 = db.Column(db.Integer) + spellslots4 = db.Column(db.Integer) + spellslots5 = db.Column(db.Integer) + spellslots6 = db.Column(db.Integer) + guild_id = db.Column(db.Integer, db.ForeignKey('character_class.id'), nullable=False) guild = db.relationship('CharacterClass', backref=db.backref('progressions', lazy=True)) + def spell_slots(self, level=None): + if level > self.guild.maximum_level: + level = self.guild.maximum_level + + spellslots = [ + self.spellslots1, + self.spellslots2, + self.spellslots3, + self.spellslots4, + self.spellslots5, + self.spellslots6 + ] + + if level: + return spellslots[level - 1] + + return spellslots + def __repr__(self): - return ''.format(self.level, self.guild.name) + return ''.format(self.level, self.guild.name) + +class Spell(BaseModel): + __tablename__ = 'spells' + + name = db.Column(db.String(50), unique=True, nullable=False) + range = db.Column(db.String(50)) + duration = db.Column(db.String(50)) + arcane = db.Column(db.Integer, nullable=False) + divine = db.Column(db.Integer, nullable=False) + description = db.Column(db.Text(1000), nullable=False) + + def __repr__(self): + return ''.format(self.name, self.school) + + def level_for(self, npc): + return self.arcane if npc.is_arcane_spellcaster else self.divine + + @property + def school(self): + if self.is_arcane and self.is_divine: + return 'Multi' + if self.is_arcane: + return 'Arcane' + if self.is_divine: + return 'Divine' + + @property + def is_divine(self): + return bool(self.divine and self.divine > 0) + + @property + def is_arcane(self): + return bool(self.arcane and self.arcane > 0) + + @property + def roll20_format(self): + spell_dict = { + 'id': self.id, + 'name': self.name, + 'range': self.range, + 'duration': self.duration, + 'divine': self.divine, + 'is_divine': self.is_divine, + 'arcane': self.arcane, + 'is_arcane': self.is_arcane, + 'description': self.description #.replace('"', '\\"').replace("'", "\\'") + } + return spell_dict class EquipmentArmour(BaseModel): __tablename__ = 'eq_armour' @@ -50,7 +132,7 @@ class EquipmentArmour(BaseModel): ac_mod = db.Column(db.Integer, nullable=False) def __repr__(self): - return ''.format(self.name) + return ''.format(self.name) class EquipmentRangedWeapon(BaseModel): __tablename__ = 'eq_ranged_wep' @@ -61,7 +143,7 @@ class EquipmentRangedWeapon(BaseModel): damage_die = db.Column(db.String(10), nullable=False) def __repr__(self): - return ''.format(self.name) + return ''.format(self.name) class EquipmentMeleeWeapon(BaseModel): __tablename__ = 'eq_melee_wep' @@ -73,7 +155,7 @@ class EquipmentMeleeWeapon(BaseModel): two_handed = db.Column(db.Boolean, nullable=False) def __repr__(self): - return ''.format(self.name) + return ''.format(self.name) class CharacterNPC(BaseModel): __tablename__ = 'npcs' @@ -90,6 +172,8 @@ class CharacterNPC(BaseModel): constitution = db.Column(db.Integer, nullable=False) charisma = db.Column(db.Integer, nullable=False) + spells = db.Column(db.String(200)) + guild_id = db.Column(db.Integer, db.ForeignKey('character_class.id'), nullable=False) guild = db.relationship('CharacterClass', backref=db.backref('npcs', lazy=True)) @@ -103,7 +187,7 @@ class CharacterNPC(BaseModel): armour = db.relationship('EquipmentArmour') def __repr__(self): - return ''.format(self.name) + return ''.format(self.name) @staticmethod def calculate_attr_mod(attr): @@ -124,6 +208,35 @@ class CharacterNPC(BaseModel): mod = 3 return mod + def spell_slots(self, level=None): + if level > self.guild.maximum_level: + level = self.guild.maximum_level + return self.current_progression.spell_slots(level) + + def spells_known(self, level=None): + if level: + if level > self.guild.maximum_level: + level = self.guild.maximum_level + # Return for a specific level + if self.is_arcane_spellcaster: + return (self.spell_slots(level) + self.wis_mod) + return self.spell_slots(level) + + # Return for all levels + if self.is_arcane_spellcaster: + return [self.spell_slots(x) + self.wis_mod for x in range(1,6)] + return [self.spell_slots(x) for x in range(1,6)] + + def spell_list(self, level=None): + spell_ids = self.spells.split(',') + spells = Spell.query.filter(Spell.id.in_(spell_ids)).all() + + spells_by_level = {} + for spell in spells: + spells_by_level.setdefault(spell.level_for(self), []).append(spell) + + return spells_by_level + @property def roll20_format(self): npc_dict = { @@ -260,8 +373,16 @@ class CharacterNPC(BaseModel): def hp(self): return self.hit_points + @property + def is_divine_spellcaster(self): + return self.guild.is_divine_spellcaster + + @property + def is_arcane_spellcaster(self): + return self.guild.is_arcane_spellcaster + def update(self, kwargs): # Allows us to update like a dict, used for inserting attributes zip self.__dict__.update(kwargs) -admin_models = [CharacterClass, ClassLevelProgression, EquipmentArmour, EquipmentRangedWeapon, EquipmentMeleeWeapon, CharacterNPC] +admin_models = [CharacterClass, ClassLevelProgression, EquipmentArmour, EquipmentRangedWeapon, EquipmentMeleeWeapon, CharacterNPC, Spell] diff --git a/acks/npc/npc_party.py b/acks/npc/npc_party.py index a4563e8..8367747 100644 --- a/acks/npc/npc_party.py +++ b/acks/npc/npc_party.py @@ -1,3 +1,4 @@ +import csv import requests from math import floor, ceil @@ -8,7 +9,8 @@ from .models import ( EquipmentArmour, EquipmentRangedWeapon, EquipmentMeleeWeapon, - CharacterNPC + CharacterNPC, + Spell ) @@ -77,9 +79,7 @@ def select_melee_weapon(guild, data): weapons.extend(data['melee']['medium']) for x in range(0, guild.melee_light): weapons.extend(data['melee']['light']) - - selection = randint(0, len(weapons) - 1) - return weapons[selection] + return choice(weapons) def select_ranged_weapon(guild, data): weapons = [] @@ -90,9 +90,23 @@ def select_ranged_weapon(guild, data): if not weapons: return None + return choice(weapons) + +def select_spell_list(npc, data): + npc_spell_list = [] + overall_spell_list = [] + if npc.is_arcane_spellcaster: + overall_spell_list = data['spells']['arcane'] + if npc.is_divine_spellcaster: + overall_spell_list = data['spells']['divine'] + + for level, known in enumerate(npc.spells_known(), start=1): + level_set = set() + while len(level_set) < known: + level_set.add(choice(overall_spell_list[level]).id) + npc_spell_list.extend(level_set) - selection = randint(0, len(weapons) - 1) - return weapons[selection] + return ','.join(str(sid) for sid in npc_spell_list) def calc_hp(conmod, hit_die_size, level): hp = 0 @@ -118,6 +132,8 @@ def generate_npc(base_level, data): npc.guild = npc_class(['Demi-Human']) npc.level = npc_baselevel(base_level) + if npc.level > npc.guild.maximum_level: + npc.level = npc.guild.maximum_level npc.alignment = npc_alignment() abilities = npc_abilities() @@ -128,10 +144,11 @@ def generate_npc(base_level, data): npc.armour = calc_armour(npc.guild.armour_modifier, data['armours']) npc.melee = select_melee_weapon(npc.guild, data) npc.ranged = select_ranged_weapon(npc.guild, data) + npc.spells = select_spell_list(npc, data) return npc -def name_party(party): +def name_party_api(party): male_names = requests.get( 'http://names.drycodes.com/{}'.format(ceil(len(party))), params={'nameOptions': 'boy_names', 'separator': 'space'} @@ -150,6 +167,33 @@ def name_party(party): return party +def load_name_data(): + names = [] + surnames = [] + with open('acks/npc/data/fantasy_names.csv', newline='') as data: + reader = csv.DictReader(data) + for row in reader: + for k,v in row.items(): + if v == '1': + row[k] = True + elif v == '': + row[k] = False + if row['Family Name']: + surnames.append(row['Name']) + else: + names.append(row['Name']) + return (names, surnames) + +def name_party(party): + names, surnames = load_name_data() + shuffle(names) + shuffle(surnames) + + for i in range(0, len(party)): + party[i].name = '{} {}'.format(choice(names), choice(surnames)) + + return party + def create_party(base_level): data = { 'armours': EquipmentArmour.query.all(), @@ -162,8 +206,24 @@ def create_party(base_level): 'medium': EquipmentMeleeWeapon.query.filter_by(bucket='Medium').all(), 'heavy': EquipmentMeleeWeapon.query.filter_by(bucket='Heavy').all() }, + 'spells': { + 'divine': {}, + 'arcane': {}, + } } + for spell in Spell.query.all(): + if spell.is_arcane: + spells = [spell] + if data['spells']['arcane'] and spell.arcane in data['spells']['arcane']: + spells.extend(data['spells']['arcane'][spell.arcane]) + data['spells']['arcane'][spell.arcane] = spells + if spell.is_divine: + spells = [spell] + if data['spells']['divine'] and spell.divine in data['spells']['divine']: + spells.extend(data['spells']['divine'][spell.divine]) + data['spells']['divine'][spell.divine] = spells + return name_party([generate_npc(base_level, data) for x in range(0, number_encountered())]) def print_party(party): diff --git a/acks/npc/templates/generate_npc_party.html b/acks/npc/templates/generate_npc_party.html index b581f10..e038125 100644 --- a/acks/npc/templates/generate_npc_party.html +++ b/acks/npc/templates/generate_npc_party.html @@ -9,7 +9,7 @@
- +
@@ -97,7 +97,7 @@
  • @@ -130,31 +130,21 @@
  • - +
    - + - - - - - - - {% if npc.ranged %} - - - - - - - {% endif %} - - - - - - + {% for level in npc.spell_list() %} + {% for spell in npc.spell_list()[level] %} + + + + + + + {% endfor %} + {% endfor %}
    NameWorthThrDmg
    NameRangeDurationLevel
    {{ npc.melee.name }}{{ npc.melee.gp_value }}gp{{ npc.attack_throw }}{{ npc.melee.damage_die }}
    {{ npc.ranged.name }}{{ npc.ranged.gp_value }}gp{{ npc.attack_throw }}{{ npc.ranged.damage_die }}
    {{ npc.armour.name }}{{ npc.armour.gp_value }}gp
    {{ spell.name }}{{ spell.range }}{{ spell.duration }}{{ spell.level_for(npc) }}
  • @@ -166,6 +156,23 @@ {% endif %} +
    +
    + +
    +

    {Spell Title}

    +
    +
    +
      +
    • Range: {Spell Range}
    • +
    • Duration: {Spell Duration}
    • +
    • {Spell School}
    • +
    +

    {Spell Description}

    +
    +
    +
    +
    @@ -202,7 +209,7 @@
    +{% endblock %} diff --git a/acks/npc/views.py b/acks/npc/views.py index 399e9df..ff172a6 100644 --- a/acks/npc/views.py +++ b/acks/npc/views.py @@ -7,6 +7,7 @@ from flask import ( ) from .npc_party import create_party +from .models import Spell npc_views = Blueprint( @@ -27,3 +28,8 @@ def generate_npc_party(base_level=None): if request.args.get('format', 'html') == 'json': return jsonify([npc.roll20_format for npc in party]) return render_template('generate_npc_party.html', party=party, base_level=base_level) + +@npc_views.route('/spells') +def spell_list(): + spells = Spell.query.all() + return render_template('spell_list.html', spells=spells) diff --git a/acks/templates/base.html b/acks/templates/base.html index cd72862..b41eb96 100644 --- a/acks/templates/base.html +++ b/acks/templates/base.html @@ -2,8 +2,10 @@ ('/', 'index', 'Home'), ('/handbook', 'handbook', 'Handbook'), ('/npc/party', 'npcparty', 'NPC Party'), - ('#', 'treasure', 'Treasure Generator'), - ('#', 'henchmen', 'Henchmen') + ('/npc/spells', 'spells', 'Spells'), + ('/api/schema', 'api', 'API'), + ('#', 'treasure', 'Treasure'), + ('#', 'henchmen', 'Henchmen'), ] %} {% set active_page = active_page|default('index') %} diff --git a/acks/views.py b/acks/views.py index d312249..e3efbc4 100644 --- a/acks/views.py +++ b/acks/views.py @@ -1,11 +1,12 @@ -from flask import current_app, Blueprint, render_template +from flask import current_app, Blueprint, render_template, url_for, redirect default_views = Blueprint('default_views', __name__, url_prefix='/') @default_views.route('/') def index(): - return render_template('index.html') + return redirect(url_for('npc_views.generate_npc_party')) + # return render_template('index.html') @default_views.route('/handbook') def handbook(): diff --git a/uwsgi.ini b/uwsgi.ini index 8e9d40a..990b61b 100644 --- a/uwsgi.ini +++ b/uwsgi.ini @@ -4,8 +4,14 @@ module = wsgi:application master = true processes = 4 -socket = ackstools.sock -chmod-scoket = 660 -vacuum = true +socket = acks.sock +chmod-socket = 777 +uid = www-data +gid = www-data +vacuum = true die-on-term = true + +logger = file:/home/br4n/code/acks-tools/logs/uwsgi.log + +env = FLASK_SETTINGS_FILE=/home/br4n/code/acks-tools/config/prod.cfg diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..678af13 --- /dev/null +++ b/wsgi.py @@ -0,0 +1,4 @@ +from start import app as application + +if __name__ == "__main__": + application.run()