Merge branch 'master' of github.com:NikolajDanger/Gwendolyn

This commit is contained in:
NikolajDanger
2020-03-29 15:45:34 +02:00
25 changed files with 309 additions and 187 deletions

5
.gitignore vendored
View File

@ -1,6 +1,10 @@
# Byte-compiled / optimized / DLL files
__pycache__/
funcs/__pycache__/
funcs/lookup/__pycache__/
funcs/other/__pycache__/
funcs/roll/__pycache__/
funcs/swfuncs/__pycache__/
*.py[cod]
*$py.class
@ -143,3 +147,4 @@ cython_debug/
media
static
.vscode/
token.txt

View File

@ -9,7 +9,11 @@ import codecs
import funcs
logging.basicConfig(filename="logfilename.log", level=logging.INFO)
logging.basicConfig(filename="gwendolyn.log", level=logging.INFO)
with open("token.txt","r") as f:
token = f.read().replace("\n","")
client = discord.Client()
@client.event
@ -30,13 +34,16 @@ async def on_message(message):
localtime = time.asctime( time.localtime(time.time()) )
print("\n"+localtime+"\n"+message.author.name+" ran !help")
logging.info("\n"+localtime+"\n"+message.author.name+" ran !help")
with codecs.open("help.txt",encoding="utf-8") as f:
with codecs.open("resources/help.txt",encoding="utf-8") as f:
text = f.read()
print(text)
em = discord.Embed(title = "Help", description = text,colour = 0x59f442)
await message.channel.send(embed = em)
if message.content.lower().startswith("!hello"):
elif message.content.lower().startswith("!stop"):
await client.logout()
elif message.content.lower().startswith("!hello"):
localtime = time.asctime( time.localtime(time.time()) )
print("\n"+localtime+"\n"+message.author.name+" ran !hello")
logging.info("\n"+localtime+"\n"+message.author.name+" ran !hello")
@ -63,6 +70,9 @@ async def on_message(message):
logging.info("\n"+localtime+"\n"+message.author.name+" ran !monster")
title, text1, text2, text3, text4, text5 = funcs.monsterFunc(message.content)
em1 = discord.Embed(title = title, description = text1, colour=0xDEADBF)
# Sends the received information. Seperates into seperate messages if
# there is too much text
await message.channel.send(embed = em1)
if text2 != "":
if len(text2) < 2048:
@ -167,5 +177,5 @@ async def on_message(message):
await message.channel.send(desc)
client.run("MzgwNzI4OTkwMTEwODQyODgx.DO81GQ.rctkEQtieciETXnmsYbwZvvOkaA")
client.run(token)

View File

@ -1,3 +1,7 @@
# Gwendolyn
Gwendolyn is a discord bot that I made. It does a bunch of stuff.
## Setup
Create a file named "token.txt" that contains your bot token.

View File

@ -1 +1,7 @@
from .gwendolynFuncs import helloFunc, roll_dice, cap, imageFunc, movieFunc, parseChar, parseRoll, spellFunc, monsterFunc, nameGen, tavernGen
from .gwendolynFuncs import helloFunc, roll_dice, cap, imageFunc
from .swfuncs import parseChar, parseRoll
from .lookup import spellFunc, monsterFunc
from .other import nameGen, tavernGen, movieFunc

View File

@ -7,11 +7,8 @@ import urllib #used by imageFunc
import imdb #used by movieFunc
from .roll import dice
from .lookup import lookupFuncs
from .other import movie, generators
from .swfuncs import swchar, swroll
def roll_dice(author, rollStr: str = "1d20"):
def roll_dice(author : str, rollStr : str = "1d20"):
print("Rolling "+str(rollStr))
if rollStr == '0/0': # easter eggs
return("What do you expect me to do, destroy the universe?")
@ -100,50 +97,3 @@ def imageFunc():
print("Successfully returned an image")
print("")
return(image)
def movieFunc():
try:
print("Creating IMDb object")
ia = imdb.IMDb()
print("Picking a movie")
movs = open("funcs/other/movies.txt", "r")
movlist = movs.read().split("\n")
mov = random.choice(movlist)
movs.close()
print("Searching for "+mov)
s_result = ia.search_movie(mov)
print("Getting the data")
movie = s_result[0]
ia.update(movie)
cast = movie['cast']
pcast = ""
for x in range(3):
if cast[x]:
pcast += cast[x]['name']+", "
print("Successfully ran !movie")
print("")
return(movie['title'], movie['plot'][0].split("::")[0], movie['cover url'].replace("150","600").replace("101","404"), pcast[:-2])
except:
print("Something bad happened...")
return("error","","","")
def parseChar(user : str, cmd : str = ""):
return swchar.parseChar(user,cmd)
def parseRoll(user : str, cmd : str = ""):
return swroll.parseRoll(user,cmd)
def spellFunc(content):
return lookupFuncs.spellFunc(content)
def monsterFunc(content):
return lookupFuncs.monsterFunc(content)
def nameGen():
return generators.nameGen()
def tavernGen():
return generators.tavernGen()

1
funcs/lookup/__init__.py Normal file
View File

@ -0,0 +1 @@
from .lookupFuncs import spellFunc, monsterFunc

View File

@ -1,10 +1,11 @@
import math
import discord
import json
import logging
from funcs import gwendolynFuncs as gf
logging.basicConfig(filename="gwendolyn.log", level=logging.INFO)
def modifier(statistic):
mods = math.floor((statistic-10)/2)
@ -18,15 +19,21 @@ abilities = ["acrobatics","animal_handling","arcana","athletics","deception","hi
def monsterFunc(content):
command = gf.cap(content.lower().replace("!monster ",""))
print("Looking up "+command)
logging.info("Looking up "+command)
if len(content.lower().split()) < 2:
print("Monster doesn't exist in database")
print("")
print("Monster doesn't exist in database\n")
logging.info("Monster doesn't exist in database\n")
return("I don't know that monster...","","","","","")
else:
data = json.load(open('funcs/lookup/monsters.json', encoding = "utf8"))
data = json.load(open('resources/monsters.json', encoding = "utf8"))
for monster in data:
print("Found it!")
if str(command) == monster["name"]:
print("Found it!")
logging.info("Found it!")
# Looks at the information about the monster and returns that information
# in seperate variables, allowing Gwendolyn to know where to seperate
# the messages
if monster["subtype"] != "":
typs = (monster["type"]+" ("+monster["subtype"]+")")
else:
@ -96,29 +103,40 @@ def monsterFunc(content):
hit_dice += (" - "+str(con_mod * int(monster["hit_dice"].replace("d"," ").split()[0])*(-1)))
if con_mod > 0:
hit_dice += (" + "+str(con_mod * int(monster["hit_dice"].replace("d"," ").split()[0])))
new_part = "\n--------------------"
monster_type = monster["size"]+" "+typs+", "+monster["alignment"]+"*"
basic_info = "\n**Armor Class** "+str(monster["armor_class"])+"\n**Hit Points** "+str(monster["hit_points"])+" ("+hit_dice+")\n**Speed **"+monster["speed"]+new_part+"\n"
text1 = (monster_type+new_part+basic_info+stats+new_part+saving_throws+skills+vulnerabilities+resistances+immunities+c_immunities+"\n**Senses** "+monster["senses"]+"\n**Languages** "+monster["languages"]+"\n**Challenge** "+monster["challenge_rating"])
text2 = (spec_ab)
text3 = (act)
text4 = (react)
text5 = (leg_act)
print("Returning monster information")
print("")
logging.info("Returning monster information")
return(str(command),text1,text2,text3,text4,text5)
print("")
print("Couldn't find monster")
logging.info("Couldn't find monster")
return("I don't know that monster...","","","","","")
def spellFunc(content):
command = gf.cap(content.lower().replace("!spell ",""))
print("Looking up "+command)
data = json.load(open('funcs/lookup/spells.json', encoding = "utf8"))
logging.info("Looking up "+command)
data = json.load(open('resources/spells.json', encoding = "utf8"))
if str(command) in data:
print("Returning spell information")
logging.info("Returning spell information")
spell_output = ("***"+str(command)+"***\n*"+str(data[str(command)]["level"])+" level "+str(data[str(command)]["school"])+"\nCasting Time: "+str(data[str(command)]["casting_time"])+"\nRange: "+str(data[str(command)]["range"])+"\nComponents: "+str(data[str(command)]["components"])+"\nDuration: "+str(data[str(command)]["duration"])+"*\n \n"+str(data[str(command)]["description"]))
else:
print("I don't know that spell")
logging.info("I don't know that spell")
spell_output = "I don't think that's a spell"
print("Successfully ran !spell")
logging.info("Successfully ran !spell")
return(spell_output)

2
funcs/other/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from .generators import nameGen, tavernGen
from .movie import movieFunc

View File

@ -1,14 +1,15 @@
import numpy as np
import random
import logging
logging.basicConfig(filename="gwendolyn.log", level=logging.INFO)
def make_pairs(corpus):
for i in range(len(corpus)-1):
yield (corpus[i], corpus[i+1])
def nameGen():
names = open('funcs/other/names.txt', encoding='utf8').read()
names = open('resources/names.txt', encoding='utf8').read()
corpus = list(names)
pairs = make_pairs(corpus)
@ -36,13 +37,15 @@ def nameGen():
if new_letter == "\n":
done = True
genName = "".join(chain)
print("Generated "+genName+"\n")
print("Generated "+genName)
logging.info("Generated "+genName)
return(genName)
def tavernGen():
fp = ["The Silver","The Golden","The Staggering","The Laughing","The Prancing","The Gilded","The Running","The Howling","The Slaughtered","The Leering","The Drunken","The Leaping","The Roaring","The Frowning","The Lonely","The Wandering","The Mysterious","The Barking","The Black","The Gleaming","The Tap-Dancing","The Sad","The Sexy","The Artificial","The Groovy","The Merciful","The Confused","The Pouting","The Horny","The Okay","The Friendly","The Hungry","The Handicapped","The Fire-breathing","The One-Eyed","The Psychotic","The Mad","The Evil","The Idiotic"]
sp = ["Eel","Dolphin","Dwarf","Pegasus","Pony","Rose","Stag","Wolf","Lamb","Demon","Goat","Spirit","Horde","Jester","Mountain","Eagle","Satyr","Dog","Spider","Star","Dad","Rat","Jeremy","Mouse","Unicorn","Pearl","Ant","Crab","Penguin","Octopus","Lawyer","Ghost","Toad","Handjob","Immigrant","SJW","Dragon","Bard","Sphinx","Soldier","Salmon","Owlbear","Kite","Frost Giant","'̶̧̗̣̰̞̜̤̦̖͗̈́̏͊͒͜+̴͎̰͓̱̻̝̬̼͕̥͍̪͕̮͙͂͝*̶̲̓̊̏'̷̥̺͈̞͒̆̏͋̀̐̇͆̓͊͠'̷͖̱̟̟͉̝̪̮͕̃͑́̍͆̓̌͒̄͛̇͘̚ͅ!̷̡̻̈́#̸̳̰̿̿̏͐̏̓̌̚̚͠¨̷̟͙̱͎̟̱̅̀͋̇͗͂͋͋̕͘´̴̡̡͎͔̦̜̟̼̠̰̤͋́̀̓́̄́̏͂̀͜.̸̛̭͍̮̜͑̋̀̋̈́̇̆̆̌_̸̡̥̜̞̝̮̑͑̓̓̇͜͜^̴̡̢͕̠̖̤̺̭̮̙͕̼̳̺̼͋̿̏̎̑͑̊̀̅͐̚͝͝","Arsonist"]
fp = ["The Silver","The Golden","The Staggering","The Laughing","The Prancing","The Gilded","The Running","The Howling","The Slaughtered","The Leering","The Drunken","The Leaping","The Roaring","The Frowning","The Lonely","The Wandering","The Mysterious","The Barking","The Black","The Gleaming","The Tap-Dancing","The Sad","The Sexy","The Artificial","The Groovy","The Merciful","The Confused","The Pouting","The Horny","The Okay","The Friendly","The Hungry","The Handicapped","The Fire-breathing","The One-Eyed","The Psychotic","The Mad","The Evil","The Idiotic","The Trusty","The Busty"]
sp = ["Eel","Dolphin","Dwarf","Pegasus","Pony","Rose","Stag","Wolf","Lamb","Demon","Goat","Spirit","Horde","Jester","Mountain","Eagle","Satyr","Dog","Spider","Star","Dad","Rat","Jeremy","Mouse","Unicorn","Pearl","Ant","Crab","Penguin","Octopus","Lawyer","Ghost","Toad","Handjob","Immigrant","SJW","Dragon","Bard","Sphinx","Soldier","Salmon","Owlbear","Kite","Frost Giant","Arsonist"]
tp = [" Tavern"," Inn","","","","","","","","",""]
genTav = random.choice(fp)+" "+random.choice(sp)+random.choice(tp)
print("Generated "+genTav+"\n")
print("Generated "+genTav)
logging.info("Generated "+genTav)
return(genTav)

View File

@ -1,28 +1,39 @@
import imdb
import random
import logging
logging.basicConfig(filename="gwendolyn.log", level=logging.INFO)
def movieFunc():
print("Running !movie")
print("Creating IMDb object")
ia = imdb.IMDb()
try:
print("Creating IMDb object")
logging.info("Creating IMDb object")
ia = imdb.IMDb()
print("Picking a movie")
movs = open("movies.txt", "r")
movlist = movs.read().split("\n")
mov = random.choice(movlist)
movs.close()
print("Picking a movie")
logging.info("Picking a movie")
movs = open("resources/movies.txt", "r")
movlist = movs.read().split("\n")
mov = random.choice(movlist)
movs.close()
print("Searching")
s_result = ia.search_movie(mov)
print("Searching for "+mov)
logging.info("Searching for "+mov)
s_result = ia.search_movie(mov)
print("Picking the movie")
movie = s_result[0]
ia.update(movie)
cast = movie['cast']
pcast = ""
for x in range(3):
if cast[x]:
pcast += cast[x]['name']+", "
print("Successfully ran !movie")
print("")
return(movie['title'], movie['plot'][0].split("::")[0], movie['cover url'].replace("150","600").replace("101","404"), pcast[:-2])
print("Getting the data")
logging.info("Getting the data")
movie = s_result[0]
ia.update(movie)
cast = movie['cast']
pcast = ""
for x in range(3):
if cast[x]:
pcast += cast[x]['name']+", "
print("Successfully ran !movie")
logging.info("Successfully ran !movie")
return(movie['title'], movie['plot'][0].split("::")[0], movie['cover url'].replace("150","600").replace("101","404"), pcast[:-2])
except:
print("Something bad happened...")
logging.info("Something bad happened...")
return("error","","","")

View File

@ -10,7 +10,7 @@ import numexpr
from . import errors
log = logging.getLogger(__name__)
logging.basicConfig(filename="gwendolyn.log", level=logging.INFO)
VALID_OPERATORS = 'k|rr|ro|mi|ma|ra|e|p'
VALID_OPERATORS_ARRAY = VALID_OPERATORS.split('|')
@ -41,10 +41,10 @@ def get_roll_comment(rollStr):
no_comment = ''
dice_set = re.split('([-+*/().=])', rollStr)
dice_set = [d for d in dice_set if not d in (None, '')]
log.debug("Found dice set: " + str(dice_set))
logging.debug("Found dice set: " + str(dice_set))
for index, dice in enumerate(dice_set):
match = DICE_PATTERN.match(dice)
log.debug("Found dice group: " + str(match.groups()))
logging.debug("Found dice group: " + str(match.groups()))
no_comment += dice.replace(match.group(5), '')
if match.group(5):
comment = match.group(5) + ''.join(dice_set[index + 1:])
@ -85,10 +85,10 @@ class Roll(object):
# parse each, returning a SingleDiceResult
dice_set = re.split('([-+*/().=])', rollStr)
dice_set = [d for d in dice_set if not d in (None, '')]
log.debug("Found dice set: " + str(dice_set))
logging.debug("Found dice set: " + str(dice_set))
for index, dice in enumerate(dice_set):
match = DICE_PATTERN.match(dice)
log.debug("Found dice group: " + str(match.groups()))
logging.debug("Found dice group: " + str(match.groups()))
# check if it's dice
if match.group(1):
roll = self.roll_one(dice.replace(match.group(5), ''), adv)
@ -157,7 +157,7 @@ class Roll(object):
skeleton=skeletonReply, raw_dice=self)
except Exception as ex:
if not isinstance(ex, (SyntaxError, KeyError, errors.AvraeException)):
log.error('Error in roll() caused by roll {}:'.format(rollStr))
logging.error('Error in roll() caused by roll {}:'.format(rollStr))
traceback.print_exc()
return DiceResult(verbose_result="Invalid input: {}".format(ex))

View File

@ -0,0 +1,2 @@
from .swchar import parseChar
from .swroll import parseRoll

View File

@ -1,73 +0,0 @@
{
"Nikolaj": {
"Name": "Jared",
"Species": "",
"Career": "",
"Specialization Trees": [],
"Soak": 0,
"Wound Threshold": 0,
"Wounds": 0,
"Strain Threshold": 0,
"Strain": 0,
"Defense, Ranged": 0,
"Defense, Melee": 0,
"Force Rating": 0,
"Characteristics": {
"Brawn": 5,
"Agility": 0,
"Intellect": 0,
"Cunning": 0,
"Willpower": 0,
"Presence": 0
},
"Skills": {
"Astrogation": 0,
"Athletics": 0,
"Brawl": 1,
"Charm": 0,
"Coercion": 0,
"Computers": 0,
"Cool": 0,
"Coordination": 0,
"Core Worlds": 0,
"Discipline": 0,
"Gunnery": 0,
"Leadership": 0,
"Lightsaber": 0,
"Lore": 0,
"Mechanics": 0,
"Medicine": 0,
"Melee": 0,
"Negotiation": 0,
"Outer Rim": 0,
"Perception": 0,
"Piloting - Planetary": 0,
"Piloting - Space": 0,
"Ranged - Heavy": 0,
"Ranged - Light": 0,
"Resilience": 0,
"Skullduggery": 0,
"Stealth": 0,
"Streetwise": 0,
"Survival": 0,
"Underworld": 0,
"Vigilance": 0,
"Xenology": 0
},
"Lightsaber Characteristic": "Brawn",
"Obligations": {},
"Morality": {
"Emotional Weakness": "",
"Emotional Strength": "",
"Conflict": "",
"Morality": ""
},
"Credits": 0,
"Equipment": [],
"Armor": "",
"Critical Injuries": {},
"Weapons": {},
"Talents": {},
"Force Powers": {}
}
}

View File

@ -1,8 +1,11 @@
import json
import string
import logging
logging.basicConfig(filename="gwendolyn.log", level=logging.INFO)
def getName(user : str):
with open("funcs/swfuncs/characters.json", "r") as f:
with open("resources/swcharacters.json", "r") as f:
data = json.load(f)
if user in data:
@ -81,7 +84,7 @@ def characterSheet(character : dict):
return name, text1+"\n\n"+text2+divider+text3
def charData(user : str,cmd : str):
with open("funcs/swfuncs/characters.json", "r") as f:
with open("resources/swcharacters.json", "r") as f:
data = json.load(f)
key = string.capwords(cmd.split(" ")[0])
@ -107,7 +110,7 @@ def charData(user : str,cmd : str):
if type(lookUpResult) is dict:
data[user][key] = lookUpResult
with open("funcs/swfuncs/characters.json", "w") as f:
with open("resources/swcharacters.json", "w") as f:
json.dump(data,f,indent = 4)
return "Changed " + data[user]["Name"] + "'s " + key
else:
@ -133,7 +136,7 @@ def charData(user : str,cmd : str):
return "Can't add that"
else:
data[user][key] = cmd
with open("funcs/swfuncs/characters.json", "w") as f:
with open("resources/swcharacters.json", "w") as f:
json.dump(data,f,indent = 4)
return "Changed " + data[user]["Name"] + "'s " + key +" to " + cmd
else:
@ -142,7 +145,7 @@ def charData(user : str,cmd : str):
return "You don't have a character. You can make one with !swchar"
def parseChar(user : str, cmd : str):
with open("funcs/swfuncs/characters.json", "r") as f:
with open("resources/swcharacters.json", "r") as f:
data = json.load(f)
if cmd == " ":
@ -157,25 +160,30 @@ def parseChar(user : str, cmd : str):
if user in data:
return characterSheet(data[user])
else:
with open("funcs/swfuncs/templates.json", "r") as f:
with open("resources/swtemplates.json", "r") as f:
templates = json.load(f)
newChar = templates["Character"]
data[user] = newChar
with open("funcs/swfuncs/characters.json", "w") as f:
with open("resources/swcharacters.json", "w") as f:
json.dump(data,f,indent = 4)
return "", "Character for " + user + " created"
else:
if cmd == "purge":
del data[user]
with open("resources/swcharacters.json", "w") as f:
json.dump(data,f,indent = 4)
return "", "Character for " + user + " deleted"
return "", charData(user,cmd)
def lightsaberChar(user : str):
with open("funcs/swfuncs/characters.json", "r") as f:
with open("resources/swcharacters.json", "r") as f:
data = json.load(f)
if user in data:
return data[user]["Lightsaber Characteristic"]
def userHasChar(user : str):
with open("funcs/swfuncs/characters.json", "r") as f:
with open("resources/swcharacters.json", "r") as f:
data = json.load(f)
return user in data

View File

@ -2,10 +2,11 @@ import random
import re
import string
import json
import os
from . import swchar
with open("funcs/swfuncs/skills.json", "r") as f:
with open("resources/swskills.json", "r") as f:
skillData = json.load(f)
def roll(abi : int = 1, prof : int = 0, dif : int = 3, cha : int = 0, boo : int = 0, setb : int = 0, force : int = 0):

30
gwendolynTest.py Normal file
View File

@ -0,0 +1,30 @@
import unittest
import funcs
class testSwChar(unittest.TestCase):
def testNewChar(self):
text1, result = funcs.parseChar("TestUser","")
self.assertEqual(result,"Character for TestUser created")
def testChangeName(self):
text1, result = funcs.parseChar("TestUser","name TestChar")
self.assertEqual(result,"Changed TestChar's Name to TestChar")
def testChangeChar(self):
text1, result = funcs.parseChar("TestUser","characteristics brawn 1")
self.assertEqual(result,"Changed New Character's Characteristics")
def testDelChar(self):
text1, result = funcs.parseChar("TestUser","purge")
self.assertEqual(result,"Character for TestUser deleted")
class testGwendolynFuncs(unittest.TestCase):
def testCap(self):
self.assertEqual(funcs.cap("planet of the apes"),"Planet of the Apes")
self.assertEqual(funcs.cap("the greatest showman"),"The Greatest Showman")
self.assertEqual(funcs.cap("i'm the best person!"),"I'm the Best Person!")
if __name__ == "__main__":
unittest.main()

View File

@ -303,13 +303,13 @@ Fantastic Four
Fantastic Mr Fox
Fargo
The Fast and the Furious
The Fast and the Furious 2 (2 Fast 2 Furious)
The Fast and the Furious 3 (Tokyo Drift)
The Fast and the Furious 4 (Fast & Furious)
The Fast and the Furious 5 (Fast Five)
2 Fast 2 Furious
The Fast and the Furious Tokyo Drift
Fast & Furious
Fast Five
The Fast and the Furious 6
The Fast and the Furious 7 (Furious 7)
The Fast and the Furious 8 (The Fate of the Furious)
Furious 7
The Fate of the Furious
Fast and Furious Presents: Hobbs and Shaw
Fast Times of Ridgemont High
The Fault in Our Stars

144
resources/swcharacters.json Normal file
View File

@ -0,0 +1,144 @@
{
"Nikolaj": {
"Name": "Jared",
"Species": "",
"Career": "",
"Specialization Trees": [],
"Soak": 0,
"Wound Threshold": 0,
"Wounds": 0,
"Strain Threshold": 0,
"Strain": 0,
"Defense, Ranged": 0,
"Defense, Melee": 0,
"Force Rating": 0,
"Characteristics": {
"Brawn": 5,
"Agility": 0,
"Intellect": 0,
"Cunning": 0,
"Willpower": 0,
"Presence": 0
},
"Skills": {
"Astrogation": 0,
"Athletics": 0,
"Brawl": 1,
"Charm": 0,
"Coercion": 0,
"Computers": 0,
"Cool": 0,
"Coordination": 0,
"Core Worlds": 0,
"Discipline": 0,
"Gunnery": 0,
"Leadership": 0,
"Lightsaber": 0,
"Lore": 0,
"Mechanics": 0,
"Medicine": 0,
"Melee": 0,
"Negotiation": 0,
"Outer Rim": 0,
"Perception": 0,
"Piloting - Planetary": 0,
"Piloting - Space": 0,
"Ranged - Heavy": 0,
"Ranged - Light": 0,
"Resilience": 0,
"Skullduggery": 0,
"Stealth": 0,
"Streetwise": 0,
"Survival": 0,
"Underworld": 0,
"Vigilance": 0,
"Xenology": 0
},
"Lightsaber Characteristic": "Brawn",
"Obligations": {},
"Morality": {
"Emotional Weakness": "",
"Emotional Strength": "",
"Conflict": "",
"Morality": ""
},
"Credits": 0,
"Equipment": [],
"Armor": "",
"Critical Injuries": {},
"Weapons": {},
"Talents": {},
"Force Powers": {}
},
"TestUser": {
"Name": "New Character",
"Species": "",
"Career": "",
"Specialization Trees": [],
"Soak": 0,
"Wound Threshold": 0,
"Wounds": 0,
"Strain Threshold": 0,
"Strain": 0,
"Defense, Ranged": 0,
"Defense, Melee": 0,
"Force Rating": 0,
"Characteristics": {
"Brawn": 0,
"Agility": 0,
"Intellect": 0,
"Cunning": 0,
"Willpower": 0,
"Presence": 0
},
"Skills": {
"Astrogation": 0,
"Athletics": 0,
"Brawl": 0,
"Charm": 0,
"Coercion": 0,
"Computers": 0,
"Cool": 0,
"Coordination": 0,
"Core Worlds": 0,
"Discipline": 0,
"Gunnery": 0,
"Leadership": 0,
"Lightsaber": 0,
"Lore": 0,
"Mechanics": 0,
"Medicine": 0,
"Melee": 0,
"Negotiation": 0,
"Outer Rim": 0,
"Perception": 0,
"Piloting - Planetary": 0,
"Piloting - Space": 0,
"Ranged - Heavy": 0,
"Ranged - Light": 0,
"Resilience": 0,
"Skullduggery": 0,
"Stealth": 0,
"Streetwise": 0,
"Survival": 0,
"Underworld": 0,
"Vigilance": 0,
"Xenology": 0
},
"Lightsaber Characteristic": "Brawn",
"Obligations": {},
"Morality": {
"Emotional Weakness": "",
"Emotional Strength": "",
"Conflict": "",
"Morality": ""
},
"Credits": 0,
"Equipment": [],
"Armor": "",
"Critical Injuries": {},
"Weapons": {},
"Talents": {},
"Force Powers": {}
}
}