Browse Source

Added spells data, names spreadsheet

master
Brandon Cornejo 2 years ago
parent
commit
3cc5bb079d
  1. 1
      .gitignore
  2. 2
      acks/__init__.py
  3. 10
      acks/npc/api.py
  4. 12
      acks/npc/commands.py
  5. 135
      acks/npc/models.py
  6. 74
      acks/npc/npc_party.py
  7. 104
      acks/npc/templates/generate_npc_party.html
  8. 73
      acks/npc/templates/spell_list.html
  9. 6
      acks/npc/views.py
  10. 6
      acks/templates/base.html
  11. 5
      acks/views.py
  12. 12
      uwsgi.ini
  13. 4
      wsgi.py

1
.gitignore

@ -16,3 +16,4 @@ pyvenv.cfg
# App specific
data
config
logs

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

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

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

135
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 '<CharacterClass {0}>'.format(self.name)
return '<CharacterClass: {0}>'.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 '<LevelProgression {0} {1}>'.format(self.level, self.guild.name)
return '<LevelProgression: {0} {1}>'.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 '<Spell: {0} ({1})>'.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 '<EquipmentArmour {0}>'.format(self.name)
return '<EquipmentArmour: {0}>'.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 '<EquipmentRangedWeapon {0}>'.format(self.name)
return '<EquipmentRangedWeapon: {0}>'.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 '<EquipmentMeleeWeapon {0}>'.format(self.name)
return '<EquipmentMeleeWeapon: {0}>'.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 '<CharacterNPC {0}>'.format(self.name)
return '<CharacterNPC: {0}>'.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]

74
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):

104
acks/npc/templates/generate_npc_party.html

@ -9,7 +9,7 @@
<div class="uk-flex uk-flex-bottom uk-flex-center uk-margin-large-bottom">
<div>
<label for="base_level">Base level of party to generate: </label>
<input type="number" name="base_level" id="base_level" class="uk-input" value="{{ base_level if base_level else 1 }}" >
<input type="number" name="base_level" id="base_level" class="uk-input" value="{{ base_level if base_level else 1 }}" min="0" max="14">
</div>
<div class="uk-margin-left">
<button class="uk-button uk-button-primary" onclick="generateParty();">Generate</button>
@ -97,7 +97,7 @@
</div>
<ul uk-tab>
<li class="uk-active"><a href="">Equipment</a></li>
<li {% if not npc.spellcaster %}class="uk-disabled"{% endif %}><a href="">Spells</a></li>
<li {% if not npc.is_divine_spellcaster and not npc.is_arcane_spellcaster %}class="uk-disabled"{% endif %}><a href="">Spells</a></li>
</ul>
<ul class="uk-switcher uk-margin">
<li>
@ -130,31 +130,21 @@
</table>
</li>
<li>
<table class="uk-table uk-table-hover uk-table-small item-table">
<table class="uk-table uk-table-hover uk-table-small spell-table">
<thead>
<tr> <th>Name</th><th>Worth</th><th>Thr</th><th>Dmg</th> </tr>
<tr> <th>Name</th><th>Range</th><th>Duration</th><th>Level</th> </tr>
</thead>
<tbody>
<tr>
<td>{{ npc.melee.name }}</td>
<td>{{ npc.melee.gp_value }}gp</td>
<td>{{ npc.attack_throw }}</td>
<td>{{ npc.melee.damage_die }}</td>
</tr>
{% if npc.ranged %}
<tr>
<td>{{ npc.ranged.name }}</td>
<td>{{ npc.ranged.gp_value }}gp</td>
<td>{{ npc.attack_throw }}</td>
<td>{{ npc.ranged.damage_die }}</td>
</tr>
{% endif %}
<tr>
<td>{{ npc.armour.name }}</td>
<td>{{ npc.armour.gp_value }}gp</td>
<td></td>
<td></td>
</tr>
{% for level in npc.spell_list() %}
{% for spell in npc.spell_list()[level] %}
<tr>
<td data-spell-id="{{ spell.id }}" onclick="showSpellModal();">{{ spell.name }}</td>
<td>{{ spell.range }}</td>
<td>{{ spell.duration }}</td>
<td>{{ spell.level_for(npc) }}</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</li>
@ -166,6 +156,23 @@
{% endif %}
<div id="spell-modal" uk-modal>
<div class="uk-modal-dialog uk-margin-auto-vertical">
<button class="uk-modal-close-default" type="button" uk-close></button>
<div class="uk-modal-header">
<h2 class="uk-modal-title"><span id="spell-modal-title">{Spell Title}</h2>
</div>
<div class="uk-modal-body">
<ul>
<li>Range: <span id="spell-modal-range">{Spell Range}</span></li>
<li>Duration: <span id="spell-modal-duration">{Spell Duration}</span></li>
<li><span id="spell-modal-school">{Spell School}</span></li>
</ul>
<p id="spell-modal-desc">{Spell Description}</p>
</div>
</div>
</div>
<div id="party-export-modal" uk-modal>
<div class="uk-modal-dialog uk-margin-auto-vertical">
<button class="uk-modal-close-default" type="button" uk-close></button>
@ -202,7 +209,7 @@
<br>
<style>
table.item-table, table.item-table th {
table.item-table, table.item-table th, table.spell-table, table.spell-table th {
font-size: 12px;
}
div.stat-block > div {
@ -237,13 +244,58 @@ div.acks-npc-card {
function generateParty() {
let bl = document.querySelector('#base_level').value;
if(bl < 0) { bl = 0; }
if(bl > 14) { bl = 14; }
window.location = "/npc/party/" + bl.toString();
}
function showSpellModal() {
spells = new Set();
{% if party %}
{% for npc in party %}
{% for level in npc.spell_list() %}
{% for spell in npc.spell_list()[level] %}
spells.add({{ spell.roll20_format | tojson }});
{% endfor %}
{% endfor %}
{% endfor %}
{% endif %}
var modal_spell, spell_id = event.target.dataset.spellId;
for(var spell of spells) {
if(spell.id == spell_id) {
modal_spell = spell
}
}
// Fill in spell details
document.querySelector('#spell-modal-title').innerText = modal_spell.name;
document.querySelector('#spell-modal-desc').innerText = modal_spell.description;
if(spell.range) {
document.querySelector('#spell-modal-range').innerText = modal_spell.range;
}
if(spell.duration) {
document.querySelector('#spell-modal-duration').innerText = modal_spell.duration;
}
var school = '';
if(modal_spell.is_arcane) {
school += 'Arcane ' + modal_spell.arcane + ' ';
}
if(modal_spell.is_divine) {
school += 'Divine ' + modal_spell.divine + ' ';
}
document.querySelector('#spell-modal-school').innerText = school;
UIkit.modal(document.querySelector('#spell-modal')).show();
}
function showExportModal() {
{% if party %}
{% for npc in party %}
party.push(JSON.parse('{{ npc.roll20_format | tojson }}'));
party.push(JSON.parse('{{ npc.roll20_format | tojson }}'));
{% endfor %}
{% endif %}

73
acks/npc/templates/spell_list.html

@ -0,0 +1,73 @@
{% extends "base.html" %}
{% set active_page = "spells" %}
{% block title %}Spell List{% endblock %}
{% block content %}
<div class="uk-flex uk-flex-center uk-margin-bottom uk-margin-top">
<h1>Adventurer Conqueror King Spell List</h1>
</div>
<div class="uk-flex uk-flex-bottom uk-flex-center uk-margin-large-bottom">
<div>
<label for="base_level">TURN INTO FILTER AREA</label>
<input type="number" name="base_level" id="base_level" class="uk-input" value="{{ base_level if base_level else 1 }}" >
</div>
<div class="uk-margin-left">
<button class="uk-button uk-button-primary" onclick="generateParty();">Generate</button>
</div>
</div>
<hr class="uk-divider-icon">
{% if spells %}
<div class="uk-grid-medium uk-grid-match uk-flex-center" uk-grid>
{% for spell in spells %}
<div class="acks-npc-card">
<div class="uk-card uk-card-body uk-card-default uk-box-shadow-hover-large">
<h4 class="uk-card-title uk-margin-small-top">{{ spell.name }}</h4>
<ul>
<li>Arcane: {{ spell.arcane }}</li>
<li>Divine: {{ spell.divine }}</li>
<li>Duration: {{ spell.duration }}</li>
<li>Range: {{ spell.range }}</li>
</ul>
<div>
{{ spell.description }}
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<style>
table.item-table, table.item-table th {
font-size: 12px;
}
div.stat-block > div {
text-align: center;
padding: 3px;
width: 26px;
}
div.stat-block > div > div:first-child {
font-weight: bold;
color: green;
}
div.stat-block > div > div:last-child {
font-weight: bold;
font-size: 12px;
color: #888;
}
div.save-block > div > div:first-child {
font-weight: bold;
color: purple;
}
div.save-block > div > div:last-child {
font-weight: bold;
font-size: 12px;
color: #888;
}
div.acks-npc-card {
width: 400px;
}
</style>
{% endblock %}

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

6
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') %}

5
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():

12
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

4
wsgi.py

@ -0,0 +1,4 @@
from start import app as application
if __name__ == "__main__":
application.run()
Loading…
Cancel
Save