You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
262 lines
7.9 KiB
262 lines
7.9 KiB
import csv
|
|
import requests
|
|
|
|
from math import floor, ceil
|
|
from random import randint, choice, shuffle
|
|
|
|
from .models import (
|
|
CharacterClass,
|
|
EquipmentArmour,
|
|
EquipmentRangedWeapon,
|
|
EquipmentMeleeWeapon,
|
|
CharacterNPC,
|
|
Spell
|
|
)
|
|
|
|
|
|
def number_encountered():
|
|
return (randint(1, 4) + 2)
|
|
|
|
def npc_class(excluded_buckets):
|
|
if excluded_buckets:
|
|
classes = [c for cls in CharacterClass.query.filter(CharacterClass.bucket.notin_(excluded_buckets)).all() for c in ([cls] * cls.frequency_modifier)]
|
|
else:
|
|
classes = [c for cls in CharacterClass.query.all() for c in ([cls] * cls.frequency_modifier)]
|
|
return choice(classes)
|
|
|
|
def npc_alignment():
|
|
roll = randint(1, 6)
|
|
if roll == 1 or roll == 2:
|
|
return 'Lawful'
|
|
elif roll == 3 or roll == 4 or roll == 5:
|
|
return 'Neutral'
|
|
elif roll == 6:
|
|
return 'Chaotic'
|
|
|
|
def npc_baselevel(base_level):
|
|
roll = randint(1, 6)
|
|
mod = 0
|
|
if roll == 1:
|
|
mod = -2
|
|
elif roll == 2:
|
|
mod = -1
|
|
elif roll == 3 or roll == 4:
|
|
mod = 0
|
|
elif roll == 5:
|
|
mod = 1
|
|
elif roll == 6:
|
|
mod = 2
|
|
|
|
base_level += mod
|
|
if base_level < 1:
|
|
base_level = 1
|
|
|
|
return base_level
|
|
|
|
def npc_abilities():
|
|
ability_list = ['strength', 'intelligence', 'wisdom', 'dexterity', 'constitution', 'charisma']
|
|
abilities = [(randint(1, 6) + randint(1, 6) + randint(1, 6)) for x in range(0,6)]
|
|
return zip(ability_list, abilities)
|
|
|
|
def attribute_mod(atr):
|
|
mod = 0
|
|
|
|
if atr <= 3: mod = -3
|
|
if atr >= 4 and atr <= 5: mod = -2
|
|
if atr >= 6 and atr <= 8: mod = -1
|
|
if atr >= 9 and atr <= 12: mod = 0
|
|
if atr >= 13 and atr <= 15: mod = 1
|
|
if atr >= 16 and atr <= 17: mod = 2
|
|
if atr >= 18: mod = 3
|
|
|
|
return mod
|
|
|
|
def select_melee_weapon(guild, data):
|
|
weapons = []
|
|
for x in range(0, guild.melee_heavy):
|
|
weapons.extend(data['melee']['heavy'])
|
|
for x in range(0, guild.melee_medium):
|
|
weapons.extend(data['melee']['medium'])
|
|
for x in range(0, guild.melee_light):
|
|
weapons.extend(data['melee']['light'])
|
|
return choice(weapons)
|
|
|
|
def select_ranged_weapon(guild, data):
|
|
weapons = []
|
|
for x in range(0, guild.ranged_light):
|
|
weapons.extend(data['ranged']['light'])
|
|
for x in range(0, guild.ranged_heavy):
|
|
weapons.extend(data['ranged']['heavy'])
|
|
|
|
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)
|
|
|
|
return ','.join(str(sid) for sid in npc_spell_list)
|
|
|
|
def calc_hp(conmod, hit_die_size, level):
|
|
hp = 0
|
|
hitdice = [randint(1, hit_die_size) for x in range(0, level)]
|
|
for die in hitdice:
|
|
die += conmod
|
|
if die < 1:
|
|
die = 1
|
|
hp += die
|
|
|
|
return hp
|
|
|
|
def calc_armour(armour_mod, armours):
|
|
armourval = randint(0, len(armours) - 1) + armour_mod
|
|
if armourval < 0:
|
|
armourval = 0
|
|
if armourval > len(armours) - 1:
|
|
armourval = len(armours) - 1
|
|
return armours[armourval]
|
|
|
|
def generate_npc(base_level, data, guild_id=False):
|
|
npc = CharacterNPC()
|
|
|
|
if not guild_id:
|
|
npc.guild = npc_class(['Demi-Human'])
|
|
else:
|
|
npc.guild = CharacterClass.query.filter_by(id=guild_id).first()
|
|
|
|
npc.level = npc_baselevel(base_level)
|
|
if npc.level > npc.guild.maximum_level:
|
|
npc.level = npc.guild.maximum_level
|
|
npc.alignment = npc_alignment()
|
|
|
|
npc.update(npc_abilities())
|
|
npc.hit_points = calc_hp(attribute_mod(npc.constitution), npc.guild.hit_die_size, npc.level)
|
|
|
|
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_api(party):
|
|
male_names = requests.get(
|
|
'http://names.drycodes.com/{}'.format(ceil(len(party))),
|
|
params={'nameOptions': 'boy_names', 'separator': 'space'}
|
|
).json()
|
|
|
|
female_names = requests.get(
|
|
'http://names.drycodes.com/{}'.format(floor(len(party))),
|
|
params={'nameOptions': 'girl_names', 'separator': 'space'}
|
|
).json()
|
|
|
|
names = female_names + male_names
|
|
shuffle(names)
|
|
|
|
for i in range(0, len(party)):
|
|
party[i].name = names[i]
|
|
|
|
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 load_db_data():
|
|
data = {
|
|
'armours': EquipmentArmour.query.all(),
|
|
'ranged': {
|
|
'light': EquipmentRangedWeapon.query.filter_by(bucket='Light').all(),
|
|
'heavy': EquipmentRangedWeapon.query.filter_by(bucket='Heavy').all()
|
|
},
|
|
'melee': {
|
|
'light': EquipmentMeleeWeapon.query.filter_by(bucket='Light').all(),
|
|
'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 data
|
|
|
|
def create_npc(base_level, guild_id):
|
|
data = load_db_data()
|
|
if guild_id:
|
|
return name_party([generate_npc(base_level, data, guild_id=guild_id)])[0]
|
|
return name_party([generate_npc(base_level, data)])[0]
|
|
|
|
def create_party(base_level):
|
|
data = load_db_data()
|
|
return name_party([generate_npc(base_level, data) for x in range(0, number_encountered())])
|
|
|
|
def print_party(party):
|
|
def print_npc(npc):
|
|
print('Level {0} NPC, {1}, {2} HP'.format(npc.level, npc.guild, npc.hp))
|
|
print('{0} Str, {1} Int, {2} Wis, {3} Dex, {4} Con, {5} Chr'.format(
|
|
npc.str, npc.int, npc.wis,
|
|
npc.dex, npc.con, npc.chr
|
|
))
|
|
print('Armour Class: {0} - {1}, {2}gp'.format(npc.armour.name, npc.armour.ac_mod, npc.armour.gp_value))
|
|
print('{:^16} - {:^10} - {:^10} - {:^10}'.format('Weapon', 'Gold', 'Throw Mod', 'Damage'))
|
|
print('-------------------------------------------------------')
|
|
print('{:^16} | {:^10} | {:^10} | {:^10}'.format(npc.melee.name, 0, npc.melee.damage_die))
|
|
if npc['ranged']:
|
|
print('{:^16} | {:^10} | {:^10} | {:^10}'.format(npc.ranged.name, 0, npc.melee.damage_die))
|
|
print('\n')
|
|
|
|
for npc in party:
|
|
print_npc(npc)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
party = create_party(2)
|
|
print_party(party)
|