Finish up basic perktree view

This commit is contained in:
Pavle Portic 2019-03-23 04:25:03 +01:00
parent 3415608f95
commit 91f6d77135
Signed by: TheEdgeOfRage
GPG Key ID: 6758ACE46AA2A849
37 changed files with 595 additions and 206 deletions

2
.gitignore vendored
View File

@ -1 +1 @@
perks
static/perks

1
backend/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
db.sqlite3

Binary file not shown.

View File

3
backend/perks/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
backend/perks/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class PerksConfig(AppConfig):
name = 'perks'

View File

6
backend/perks/models.py Normal file
View File

@ -0,0 +1,6 @@
from django.db import models
class Perk(models.Model):
pass

3
backend/perks/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

8
backend/perks/urls.py Normal file
View File

@ -0,0 +1,8 @@
from django.urls import path
from perks import views
urlpatterns = [
path('trees', views.ListTrees.as_view()),
path('trees/<int:tree>', views.ListPerks.as_view()),
]

40
backend/perks/views.py Normal file
View File

@ -0,0 +1,40 @@
import json
from os import listdir
from os.path import isfile, join
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions
PERKS_DIR = '../static/perks'
def get_tree_list():
return sorted([file for file in listdir(PERKS_DIR) if isfile(join(PERKS_DIR, file))])
class ListTrees(APIView):
"""
View to list all perk trees
"""
authentication_classes = (authentication.TokenAuthentication,)
# permission_classes = (permissions.IsAuthenticated,)
def get(self, request, format=None):
return Response([tree[:-5] for tree in get_tree_list()])
class ListPerks(APIView):
"""
View to list all perks in a tree
"""
authentication_classes = (authentication.TokenAuthentication,)
# permission_classes = (permissions.IsAuthenticated,)
def get(self, request, tree, format=None):
print(request.user)
filename = get_tree_list()[tree]
with open(f'{PERKS_DIR}/{filename}', 'r') as f:
return Response(json.load(f))

View File

@ -120,7 +120,7 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/
STATIC_URL = '/static/'
STATIC_URL = '/django-static/'
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',

View File

@ -22,14 +22,13 @@ from rest_framework_simplejwt.views import (
TokenVerifyView,
)
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
path('api/', include([
path('admin/', admin.site.urls),
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
path('', include('perks.urls'))
]))
]

View File

@ -15,9 +15,11 @@
"roboto-fontface": "*",
"vue": "^2.6.6",
"vue-router": "^3.0.2",
"vuetify": "^1.5.5"
"vuetify": "^1.5.5",
"vuex": "^3.1.0"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^5.8.1",
"@vue/cli-plugin-eslint": "^3.5.0",
"@vue/cli-service": "^3.5.0",
"@vue/eslint-config-standard": "^4.0.0",

View File

@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>perktree</title>
<title>Perktree</title>
</head>
<body>
<noscript>

View File

@ -1,47 +0,0 @@
<template>
<v-app dark>
<!-- <v-toolbar class="teal darken-2" dark dense fixed clipped-left app> -->
<!-- <v-toolbar-title> -->
<!-- <v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon> -->
<!-- </v-toolbar-title> -->
<!-- <v-spacer></v-spacer> -->
<!-- <v-toolbar-items class="hidden-xs-only"> -->
<!-- <template v-if="admin"> -->
<!-- <v-btn -->
<!-- v-for="item in toolbarItems['admin']" -->
<!-- :key="item.text" -->
<!-- :to="item.path" -->
<!-- flat -->
<!-- > -->
<!-- <v-icon left>{{ item.icon }}</v-icon> -->
<!-- {{ item.text }} -->
<!-- </v-btn> -->
<!-- </template> -->
<!-- <v-btn -->
<!-- flat -->
<!-- v-for="item in toolbarItems[authStatus]" -->
<!-- :key="item.text" -->
<!-- :to="item.path" -->
<!-- > -->
<!-- <v-icon left>{{ item.icon }}</v-icon> -->
<!-- {{ item.text }} -->
<!-- </v-btn> -->
<!-- </v-toolbar-items> -->
<!-- </v-toolbar> -->
<v-content>
<router-view></router-view>
</v-content>
</v-app>
</template>
<script>
export default {
name: 'App',
};
</script>
<style lang=stylus>
</style>

View File

@ -1,27 +0,0 @@
import Axios from 'axios';
const ENDPOINTS = {
LOGIN: '/token/',
REFRESH: '/token/refresh/',
USER: '/user/',
};
const AUTH_HEADER = 'Authorization';
export default class AuthApiService {
static setAuthHeader() {
Axios.defaults.headers.common[AUTH_HEADER] = `Bearer ${localStorage.getItem('access_token')}`;
}
static login(data) {
return Axios.post(ENDPOINTS.LOGIN, data);
}
static signup(data) {
return Axios.post(ENDPOINTS.USER, data);
}
static changePassword(data) {
return Axios.patch(ENDPOINTS.USER, data);
}
}

View File

@ -0,0 +1,47 @@
/*
* auth.api.js
* Copyright (C) 2019 pavle <pavle.portic@tilda.center>
*
* Distributed under terms of the BSD-3-Clause license.
*/
import Axios from 'axios';
const ENDPOINTS = {
LOGIN: '/token/',
VERIFY: '/token/verify/',
REFRESH: '/token/refresh/',
USER: '/user/',
};
const AUTH_HEADER = 'Authorization';
export default class AuthApi {
static setAuthHeader(token) {
Axios.defaults.headers.common[AUTH_HEADER] = `Bearer ${token}`;
}
static login(data) {
return Axios.post(ENDPOINTS.LOGIN, data);
}
static getUser() {
return Axios.get(ENDPOINTS.USER);
}
static signup(data) {
return Axios.post(ENDPOINTS.USER, data);
}
static changePassword(data) {
return Axios.patch(ENDPOINTS.USER, data);
}
static verifyToken(token) {
return Axios.post(ENDPOINTS.VERIFY, token);
}
static refreshToken(refresh) {
return Axios.post(ENDPOINTS.REFRESH, refresh);
}
}

View File

@ -0,0 +1,23 @@
/*
* perk.api.js
* Copyright (C) 2019 pavle <pavle.portic@tilda.center>
*
* Distributed under terms of the BSD-3-Clause license.
*/
import Axios from 'axios';
const ENDPOINTS = {
TREES: '/trees',
};
export default class AuthApi {
static getTrees() {
return Axios.get(ENDPOINTS.TREES);
}
static getPerks(tree) {
return Axios.get(ENDPOINTS.TREES + `/${tree}`);
}
}

View File

@ -0,0 +1,62 @@
<template>
<v-app dark>
<v-toolbar color="primary" dense fixed app>
<v-toolbar-title>
<router-link :to="{ name: 'index' }">Perktree</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items>
<v-btn
flat
v-for="item in toolbarItems"
:key="item.text"
:to="item.path"
>
<v-icon left>{{ item.icon }}</v-icon>
{{ item.text }}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<v-content>
<router-view></router-view>
</v-content>
</v-app>
</template>
<script>
import { mapGetters } from 'vuex';
import AuthController from '../controllers/auth.controller.js';
export default {
name: 'App',
data() {
return {
// toolbarItems: {
// 'loggedIn': [
// { icon: 'fas fa-code-branch ', text: 'Perk trees', path: '/trees' },
// { icon: 'account_circle', text: 'Admin panel', path: '/admin' },
// { icon: 'exit_to_app', text: 'Logout', path: '/logout' },
// ],
// 'loggedOut': [{ icon: 'lock_open', text: 'Login', path: '/login' }],
// },
toolbarItems: [{ icon: 'fas fa-code-branch ', text: 'Perk trees', path: '/trees' }],
};
},
computed: {
...mapGetters(['token']),
authStatus() {
return this.token ? 'loggedIn' : 'loggedOut';
},
},
mounted() {
AuthController.refreshToken();
},
};
</script>
<style lang="stylus">
@import '../stylus/app.styl'
</style>

View File

@ -51,11 +51,8 @@ export default {
AuthController.login(data).then(() => {
this.$router.push({ name: 'index' });
this.$store.dispatch('fetchFormData');
}).catch((error) => {
if (error.response.status === 401) {
this.loginErrors.push(error.response.data);
}
this.loginErrors.push(error.response.data);
});
},
},

View File

@ -1,7 +1,7 @@
<template>
<v-content>
arst
</v-content>
<v-container>
<h1>Welcome to Perktree</h1>
</v-container>
</template>
<script>

View File

@ -1,11 +1,42 @@
<template>
<v-content>
<div id="stuff"></div>
</v-content>
<v-container class="perktree">
<v-layout row wrap>
<v-flex md8 offset-md2 sm12>
<v-card>
<div id="perktree"></div>
</v-card>
</v-flex>
</v-layout>
<v-dialog
v-model="dialog"
width="500"
>
<v-card>
<v-card-title
class="headline grey darken-2"
primary-title
>{{ perk }}</v-card-title>
<v-card-text>{{ effect }}</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="primary"
flat
@click="dialog = false"
>Colse</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import Graph from '../helpers/perk-tree.helper.js';
import * as d3 from 'd3';
import Sankey from 'd3.chart.sankey';
import * as _ from 'lodash';
import PerksController from '../controllers/perks.controller';
export default {
name: 'Perks',
@ -13,14 +44,52 @@ export default {
},
data () {
return {
dialog: false,
perk: '',
effect: '',
colorScheme: [
'#458588',
'#d79921',
'#98971a',
'#cc241d',
'#d5c4a1',
],
};
},
methods: {
renderGraph(graphData) {
const svg = d3.select('#perktree').append('svg');
const chart = new Sankey(svg);
const nodes = graphData.nodes;
chart.nodeWidth(24)
.nodePadding(6)
.spread(false)
.colorNodes((name, node) => {
return this.colorScheme[node.colour];
})
.on('node:click', (node) => {
const clicked_node = _.find(nodes, (n) => {
return n.name === node.name;
});
if (clicked_node.effect) {
this.perk = clicked_node.name;
this.effect = clicked_node.effect;
this.dialog = true;
}
})
.draw(graphData);
},
},
mounted() {
const g = new Graph();
console.log(g);
PerksController.getPerks(this.$route.params.tree).then((response) => {
this.renderGraph(response.data);
});
},
};
</script>
<style lang="stylus">
@import '../stylus/perks.styl'
</style>

View File

@ -0,0 +1,50 @@
<template>
<v-container grid-list-md text-xs-center class="tree-list">
<v-layout row wrap>
<v-flex
v-for="(tree, index) in trees"
:key="index"
sm2
xs6
>
<v-btn
block
color="secondary"
@click="openPerks(index)"
>{{ tree }}</v-btn>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
// import * as _ from 'lodash';
import PerksController from '../controllers/perks.controller';
export default {
name: 'Trees',
components: {
},
data () {
return {
trees: null,
};
},
methods: {
openPerks(index) {
this.$router.push({ name: 'perks', params: { tree: index } });
},
},
mounted() {
PerksController.getTrees().then((response) => {
this.trees = response.data;
});
},
};
</script>
<style lang="stylus">
@import '../stylus/trees.styl'
</style>

View File

@ -1,55 +1,94 @@
import * as _ from 'lodash';
/*
* perks.controller.js
* Copyright (C) 2019 pavle <pavle.portic@tilda.center>
*
* Distributed under terms of the BSD-3-Clause license.
*/
import AuthApiService from '../api-services/auth-api-services';
import AuthApi from '../apis/auth.api';
import router from '../router';
import store from '../store';
export default class AuthController {
static setLocalStorageAuthData(data) {
console.log(data);
localStorage.setItem('refresh', data.refresh);
localStorage.setItem('access', data.access);
}
static checkLocalStorage() {
const userData = JSON.parse(localStorage.getItem('user'));
if (userData) {
AuthApiService.setAuthHeader();
static setLocalStorageToken(data) {
if (data.refresh) {
localStorage.setItem('refresh', data.refresh);
}
if (data.access) {
localStorage.setItem('access', data.access);
}
return userData;
}
static updateUserInLocalStorage(newUserData) {
const userData = JSON.parse(localStorage.getItem('user'));
_.assign(userData, newUserData);
localStorage.setItem('user', JSON.stringify(userData));
static getLocalStorageToken() {
const tokens = {
access: localStorage.getItem('access'),
refresh: localStorage.getItem('refresh'),
};
return tokens;
}
static getLocalStorageRefresh() {
return localStorage.getItem('access');
}
static clearLocalStorageToken() {
localStorage.removeItem('refresh');
localStorage.removeItem('access');
}
static setupToken() {
const access = this.getLocalStorageToken().access;
AuthApi.setAuthHeader(access);
store.commit('setToken', access);
}
static login(data) {
return AuthApiService.login(data).then((response) => {
this.setLocalStorageAuthData(response.data);
AuthApiService.setAuthHeader();
return AuthApi.login(data).then((response) => {
this.setLocalStorageToken(response.data);
this.setupToken(response.data);
});
}
static register(data) {
return AuthApiService.register(data);
static signup(data) {
return AuthApi.signup(data);
}
static logout() {
this.setLocalStorageAuthData({
token: null,
user: null,
});
AuthApiService.setAuthHeader();
this.clearLocalStorageToken();
store.commit('clearToken');
AuthApi.setAuthHeader('');
}
static changePassword(data) {
return AuthApiService.changePassword(data);
return AuthApi.changePassword(data);
}
static checkAuthStatus() {
return Boolean(this.checkLocalStorage());
static verifyToken() {
const token = this.getLocalStorageToken().access;
if (token === null) {
router.push('login');
}
return AuthApi.verifyToken({ token });
}
static refreshToken() {
if (!this.getLocalStorageRefresh()) {
return Promise.reject(new Error('No token'));
}
const refresh = {
refresh: this.getLocalStorageToken().refresh,
};
return AuthApi.refreshToken(refresh).then((response) => {
this.setLocalStorageToken(response.data);
this.setupToken();
});
}
static getAuthStatus() {
const token = this.getLocalStorageToken().access;
return Boolean(token);
}
}

View File

@ -0,0 +1,19 @@
/*
* perks.controller.js
* Copyright (C) 2019 pavle <pavle.portic@tilda.center>
*
* Distributed under terms of the BSD-3-Clause license.
*/
import PerkApi from '../apis/perk.api';
// import router from '../router';
export default class PerksController {
static getTrees() {
return PerkApi.getTrees();
}
static getPerks(tree) {
return PerkApi.getPerks(tree);
}
}

View File

@ -1,31 +0,0 @@
/*
* perk-tree.helper.js
* Copyright (C) 2019 pavle <pavle.portic@tilda.center>
*
* Distributed under terms of the BSD-3-Clause license.
*/
import * as d3 from 'd3';
import * as _ from 'lodash';
const loadGraph = () => {
d3.json('/static/perks/Dexterity.json', (error, json) => {
const chart = d3.select('#chart').append('svg').chart('Sankey');
const color = d3.scale.category10();
const nodes = json.nodes;
chart.nodeWidth(20)
.nodePadding(5)
.colorNodes((name, node) => {
return color(node.colour);
}).on('node:click', (node) => {
const clicked_node = _.find(nodes, (n) => {
return n.name === node.name;
});
console.log(clicked_node);
}).draw(json);
});
};
export default loadGraph;

View File

@ -2,9 +2,11 @@ import Axios from 'axios';
import Vue from 'vue';
import './plugins/vuetify';
import App from './App.vue';
import App from './components/app.vue';
import { config } from './config';
import router from './router';
import store from './store';
import AuthController from './controllers/auth.controller';
import 'roboto-fontface/css/roboto/roboto-fontface.css';
import 'material-design-icons-iconfont/dist/material-design-icons.css';
@ -12,31 +14,38 @@ import 'material-design-icons-iconfont/dist/material-design-icons.css';
Vue.config.productionTip = false;
const configureHttp = () => {
Axios.defaults.baseURL = config.getApiUrl();
Axios.defaults.headers.Accept = 'application/json';
// Axios.defaults.headers['Access-Control-Allow-Origin'] = '*';
Axios.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response && error.response.status === 401) {
router.push({
name: 'logout',
});
}
Axios.defaults.baseURL = config.getApiUrl();
Axios.defaults.headers.Accept = 'application/json';
// Axios.defaults.headers['Access-Control-Allow-Origin'] = '*';
Axios.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response && error.response.status === 401) {
// AuthController.refreshToken().catch(() => {
// router.push({
// name: 'logout',
// });
// });
router.push({
name: 'logout',
});
}
return Promise.reject(error);
}
);
return Promise.reject(error);
}
);
};
configureHttp();
AuthController.setupToken();
new Vue({
router,
render (h) {
return h(App);
},
router,
store,
render (h) {
return h(App);
},
}).$mount('#app');

View File

@ -1,18 +1,19 @@
import Vue from 'vue';
import Vuetify from 'vuetify';
import colors from 'vuetify/es5/util/colors';
// import colors from 'vuetify/es5/util/colors';
import 'vuetify/dist/vuetify.min.css';
import '@fortawesome/fontawesome-free/css/all.css';
Vue.use(Vuetify, {
theme: {
primary: colors.teal,
secondary: '#424242',
accent: '#82B1FF',
error: '#FF5252',
info: '#2196F3',
success: '#4CAF50',
warning: '#FFC107',
primary: '#458588',
secondary: '#282828',
accent: '#689d6a',
error: '#cc241d',
info: '#458588',
success: '#98971a',
warning: '#d79921',
},
iconfont: 'md',
iconfont: 'fa',
});

View File

@ -53,9 +53,20 @@ const router = new Router({
component: () => import(/* webpackChunkName: "admin" */ './components/auth/admin-panel.component'),
},
{
path: '/perks',
path: '/trees',
name: 'trees',
component: () => import(/* webpackChunkName: "perks" */ './components/trees.component'),
meta: {
guest: true,
},
},
{
path: '/perks/:tree',
name: 'perks',
component: () => import(/* webpackChunkName: "perks" */ './components/perks.component'),
meta: {
guest: true,
},
},
],
});
@ -65,16 +76,12 @@ router.isCurrentRoute = (routeName) => {
};
router.beforeEach((to, from, next) => {
if (to.meta.guest && AuthController.checkAuthStatus()) {
return next(to.meta.guest_redirect || '/');
}
if (!to.meta.guest && !AuthController.checkAuthStatus()) {
if (to.name === 'logout') {
AuthController.logout();
return next({ name: 'login' });
}
if (to.name === 'logout' && AuthController.checkAuthStatus()) {
AuthController.logout();
if (!to.meta.guest && !AuthController.getAuthStatus()) {
return next({ name: 'login' });
}

35
frontend/src/store.js Normal file
View File

@ -0,0 +1,35 @@
/*
* store.js
* Copyright (C) 2019 pavle <pavle.portic@tilda.center>
*
* Distributed under terms of the BSD-3-Clause license.
*/
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const state = {
token: null,
};
const getters = {
token: (state) => state.token,
};
const mutations = {
setToken(token) {
state.token = token;
},
clearToken() {
state.token = '';
},
};
export default new Vuex.Store({
state,
getters,
mutations,
});

View File

@ -0,0 +1,11 @@
h1, h2, h3, h4, h5, h6, p, span
color #ebdbb2
.v-toolbar__title
a
color #ebdbb2
text-decoration none
.v-btn
color #ebdbb2 !important

View File

@ -0,0 +1,30 @@
.perktree
.v-card
padding 1rem
height 42rem
#perktree
height 100%
text
fill #ebdbb2
.node rect
fill-opacity .9
shape-rendering crispEdges
cursor pointer
.node text
pointer-events none
text-shadow 0 1px 0 #fff
fill white
.link
fill none
stroke #000
stroke-opacity .5
&:hover
stroke-opacity .4
stroke #fbf1c7

View File

@ -0,0 +1,11 @@
.tree-list
.layout
padding-top 3rem
.v-btn
height 6rem
margin 0.3rem
display flex
justify-content center
align-items center

View File

@ -90,6 +90,11 @@
lodash "^4.17.11"
to-fast-properties "^2.0.0"
"@fortawesome/fontawesome-free@^5.8.1":
version "5.8.1"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.8.1.tgz#cbafbfe8894c4e3e3c3a9da6774e249ac1f2da8b"
integrity sha512-GJtx6e55qLEOy2gPOsok2lohjpdWNGrYGtQx0FFT/++K4SYx+Z8LlPHdQBaFzKEwH5IbBB4fNgb//uyZjgYXoA==
"@intervolga/optimize-cssnano-plugin@^1.0.5":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@intervolga/optimize-cssnano-plugin/-/optimize-cssnano-plugin-1.0.6.tgz#be7c7846128b88f6a9b1d1261a0ad06eb5c0fdf8"
@ -7074,6 +7079,11 @@ vuetify@^1.5.5:
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-1.5.7.tgz#92e7558f590ccb5696bbd1156381e8537c86c2f9"
integrity sha512-e7Vvj9gh41Pth7pXJxH1hrm8wfnvWt3nvxPCnwBWTgr4kiUXTG4CaUBqWVVgyfLjJGJSyR1y1EoRNEP5tJv0HQ==
vuex@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.1.0.tgz#634b81515cf0cfe976bd1ffe9601755e51f843b9"
integrity sha512-mdHeHT/7u4BncpUZMlxNaIdcN/HIt1GsGG5LKByArvYG/v6DvHcOxvDCts+7SRdCoIRGllK8IMZvQtQXLppDYg==
watchpack@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"

View File

@ -10,12 +10,16 @@
import argparse
import csv
import json
import os
import re
CLASS_LIST = ['Artificer', 'Barbarian', 'Bard', 'Cleric', 'Druid', 'Fighter', 'Monk', 'Mystic', 'Paladin', 'Ranger', 'Rogue', 'Sorcerer', 'Warlock', 'Wizard', 'Mage', 'Alchemist']
RACE_LIST = ['Dwarf', 'Elf', 'Halfling', 'Human', 'Dragonborn', 'Gnome', 'Half-elf', 'Half-orc', 'Tiefling']
ability_pattern = re.compile(r'Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma \d+\+')
OUTPUT_DIR = f'{os.path.abspath(os.path.dirname(__file__))}/../static/perks'
INPUT_FILE = f'{os.path.abspath(os.path.dirname(__file__))}/perks_all.tsv'
ability_pattern = re.compile(r'Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma \d+\+', re.IGNORECASE)
class_pattern = re.compile(r'((Artificer|Barbarian|Bard|Cleric|Druid|Fighter|Monk|Mystic|Paladin|Ranger|Rogue|Sorcerer|Warlock|Wizard|Mage|Alchemist|Magus)(, )?)+', re.IGNORECASE)
race_pattern = re.compile(r'((Dwarf|Elf|Halfling|Human|Dragonborn|Gnome|Half-Elf|Half-Orc|Tiefling)(, )?)+', re.IGNORECASE)
def load_csv(filename):
@ -35,6 +39,8 @@ def input_transform(perks):
perk.pop('tree')
perk['level'] = int(perk['level'])
perk['colour'] = 0
perk['name'] = perk['name'].strip()
perk['effect'] = perk['effect'].strip()
if tree not in trees:
trees[tree] = {
'nodes': [],
@ -96,9 +102,9 @@ def create_links(trees):
if found:
break
else:
if requirement in CLASS_LIST:
if re.match(class_pattern, requirement):
new_perk = create_perk(requirement, '', level=0, colour=1)
elif requirement in RACE_LIST:
elif re.match(race_pattern, requirement):
new_perk = create_perk(requirement, '', level=0, colour=2)
elif re.match(ability_pattern, requirement):
new_perk = create_perk(requirement, '', level=0, colour=3)
@ -122,14 +128,15 @@ def output_transform(trees):
def write_json(trees, split):
os.makedirs(OUTPUT_DIR, exist_ok=True)
for tree, data in trees.items():
with open(f'perks/{tree}.json', 'w') as jsonfile:
with open(f'{OUTPUT_DIR}/{tree}.json', 'w') as jsonfile:
json.dump(data, jsonfile)
def main():
parser = argparse.ArgumentParser(description='Parse tsv perk tree')
parser.add_argument('input_file', nargs='?', default='perks_all.tsv', help='path to the input file')
parser.add_argument('input_file', nargs='?', default=INPUT_FILE, help='path to the input file')
parser.add_argument('-s', '--split', action='store_true', help='split output into multiple json files by tree')
args = parser.parse_args()

View File

@ -53,7 +53,7 @@ Bewildering koan As a bonus action you can make charisma check vs wisdom save ag
Criminal reputation Add profficiency bonus on any skill checks when interacting with criminals. If already profficient with skill, double profficience bonus. Charisma 3 Cosmopolitan
Free Spirit You can gain advantage on any roll to resist mind-affecting effects or to escape grapple and any bonds. *short rest Charisma 3 Steadfast Personality
Guided Hand You can add your charisma modifier to an attack and damage roll. *short rest Charisma 3 Greater Ability focus (Charisma)
Inspiring Talent If copying someones actions that require skill checks, if that creature has profficiency in that skill and you don't, your skill check counts as being profficient. Charisma 3 Greater Ability focus (Charisma) Half elf
Inspiring Talent If copying someones actions that require skill checks, if that creature has profficiency in that skill and you don't, your skill check counts as being profficient. Charisma 3 Greater Ability focus (Charisma) Half-Elf
Intoxicating Flattery Take 1 minute to flatter someone, after that roll charisma vs insight. If succesful, target gets disadvantage on all wisdom based rolls for 10 x your cha mod minutes. Charisma 3 Greater Ability focus (Charisma)
Monstrous Mask When using intimidate against a humanoid, if your intimidate skill check beats the opposing skill check by 10 or more, target creature is frightened by you. Charisma 3 Greater Ability focus (Charisma) Tiefling
Strong Comeback Whenever you are allowed to reroll an ability check, a skill check, or a saving throw, you gain a +2 bonus on the reroll. Charisma 3 Greater Ability focus (Charisma)
@ -74,7 +74,7 @@ Drunkards Recovery If you are dying and a creature gives you at least a sip o
Fast Healer When you regain hit points by resting or through magical healing, you recover additional hit points equal to half your Constitution modifier (minimum +1). Constitution 4 Endurance
Heroic Recovery After concetrating for 10 minute you can remove half points of exaustion rounded up. When you use Heroic Defiance, you can spend the use of this ability to instead of ignoring one harmful condition for one round, you can remove it totally. *long rest Constitution 4 Heroic Defiance
Improved Stalwart If you haven't moved this turn, you get the effect of dodge action. You can not use this ability if you already took a dodge action. Constitution 4 Stalwart
Ironhide Gain +1 AC. Constitution 4 Greater Ability focus (Constitution) Half-ork, dwarf
Ironhide Gain +1 AC. Constitution 4 Greater Ability focus (Constitution) Half-Orc, Dwarf
Raging Brutality When using two handed weapons while raging, you can add your constitution modifier in addition to strength modifier to damage rolls. Constitution 4 Greater Ability focus (Constitution) Barbarian
Survivor You gain advantage bonus on all Constitution checks made to stabilize while dying. Once per day, if you are struck by a critical hit or sneak attack, you can spend a reaction to negate the critical or the sneak attack damage, making the attack a normal hit. Replenishes after long rest. Constitution 4 Fight On
Uncanny Concentration Gain advantages on concentration rolls in any kind of situation. Constitution 4 Combat Casting
@ -87,7 +87,7 @@ Second skin When false-faceing gain passive deception, 8+deception bonus. If som
Willing Accomplice Ally that is attempting Deception checks to disguise themself as another creature have advantage if in 15 ft from you. Can't use this ablity if deafened or blind. Deception 3 Greater Skill focus (Deception)
Improved conceal spell Creatures no longer gain bonus to their passive perception equal to spell level. If the creature is 30ft or more from you, gain advantage on deception or sleight of hand. Deception 4 Conceal spell
Kinslayer Deal 1d6 bonus damage per attack when attacking creatures that have the same creature type (and subtype, if applicable) as you. Deception 4 Greater Skill focus (Deception)
Pass For Human Gain advantages when making a deception skill check to disguise yourself as a human. Deception 4 Second skin Half elf, half orc
Pass For Human Gain advantages when making a deception skill check to disguise yourself as a human. Deception 4 Second skin Half-Elf, Half-Orc
Sycophant As a standard action, you can attempt to force an enemy to show you mercy by succeeding at a Deception check (DC = 12 + the targets Wisdom saving throw bonus). If you succeed, your targets next successful attack against you that brings you to 0 hp will knock you out instead of killing you. Deception 4 Helpless Prisoner
Fool Magic When you are in disguise as a member of a particular race or a person of a particular alignment, you can use your Deception bonus instead of your Arcana bonus to emulate that race and alignment for the purpose of attempting to activate a magic item. Deception - Arcana 4 Second skin Skill focus (Arcana)
Taunt You can demoralize opponents using Deception rather than Intimidate and take no skill check penalty for being smaller than your target. Deception - Intimidation 3 Skill focus (Deception) Demoralize
@ -163,7 +163,7 @@ Animal call You can use your deception skill to mimic the calls of animals t
Eagle-eyed You can see to a considerable distance without penalty. Ignore disadvantage to perception due to distance. Perception 2 Skill focus (Perception)
Alertness Gain advantages on saving throws against sleep and charm effects for a number of times equal to your. Wisdom modifier. *long rest Perception 2 Skill focus (Perception)
Eyes of Judgment You may spend 3 rounds studying a creature within 60 feet. You cannot take any other actions while doing this. Effect: you learn the alignment of the creature. Perception 3 Eagle-eyed
Keen Scent You can scent creatures by their smell. You don't need to see someone in 30ft to sense them with perception check. Perception 3 Greater Skill focus (Perception) Half orc
Keen Scent You can scent creatures by their smell. You don't need to see someone in 30ft to sense them with perception check. Perception 3 Greater Skill focus (Perception) Half-Orc
Sentry You can roll a perception check and count it as your passive perception for minutes equal to your level. *short rest Perception 3 Greater Skill focus (Perception)
Uncanny Alertness In addition to your wisdom modifier, add your Dexterity modifier to your perception. Perception 3 Alertness
Judgement of weakness When using eyes of judgement, in addition to knowing creatures allignment, you know it's weaknesses and resistances. Perception 4 Eyes of Judgment

Can't render this file because it has a wrong number of fields in line 3.