Browse Source

Single NPC gen, spell list, world map, roll20-hitdice

master
Brandon Cornejo 2 years ago
parent
commit
9017792b3c
  1. 4
      acks/npc/models.py
  2. 22
      acks/npc/npc_party.py
  3. 350
      acks/npc/templates/generate_single_npc.html
  4. 85
      acks/npc/templates/spell_list.html
  5. 24
      acks/npc/views.py
  6. BIN
      acks/static/WorldMap.png
  7. 18
      acks/templates/base.html
  8. 15
      acks/templates/worldmap.html
  9. 4
      acks/views.py
  10. 117
      roll20/hit_dice.js

4
acks/npc/models.py

@ -111,6 +111,10 @@ class Spell(BaseModel):
def is_arcane(self):
return bool(self.arcane and self.arcane > 0)
@property
def dom_id(self):
return self.name.lower().replace("*", "").replace("'", "").replace(",", "").replace(" ", "_")
@property
def roll20_format(self):
spell_dict = {

22
acks/npc/npc_party.py

@ -127,18 +127,20 @@ def calc_armour(armour_mod, armours):
armourval = len(armours) - 1
return armours[armourval]
def generate_npc(base_level, data):
def generate_npc(base_level, data, guild_id=False):
npc = CharacterNPC()
npc.guild = npc_class(['Demi-Human'])
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()
abilities = npc_abilities()
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'])
@ -194,7 +196,7 @@ def name_party(party):
return party
def create_party(base_level):
def load_db_data():
data = {
'armours': EquipmentArmour.query.all(),
'ranged': {
@ -224,6 +226,16 @@ def create_party(base_level):
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):

350
acks/npc/templates/generate_single_npc.html

@ -0,0 +1,350 @@
{% extends "base.html" %}
{% set active_page = "npcsingle" %}
{% block title %}Single NPC Generation{% endblock %}
{% block content %}
<div class="uk-flex uk-flex-center uk-margin-bottom uk-margin-top">
<h1>Adventurer Conqueror King NPC Generator</h1>
</div>
<div class="uk-flex uk-flex-bottom uk-flex-center uk-margin-large-bottom">
<div>
<label for="guild_select">Class: </label>
<select name="guild_select" id="guild_select" class="uk-select">
<option value="0">Random</option>
{% for guild in guilds %}
<option value="{{ guild.id }}"{% if guild.id == guild_id %} selected{% endif%}>{{ guild.name }}</option>
{% endfor %}
</select>
</div>
<div class="uk-margin-left">
<label for="base_level">Base level of npc to generate: </label>
<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="generateNPC();">Generate</button>
</div>
</div>
<hr class="uk-divider-icon">
{% if npc %}
<div class="uk-flex uk-flex-between uk-margin-large-top">
<h3 class="uk-display-inline-block">Generated NPC</h3>
<div class="uk-text-right uk-display-inline-block">
<a class="uk-button uk-button-small uk-button-secondary uk-border-rounded" onclick="showExportModal();">Roll20 Export</a>
</div>
</div>
<div class="uk-flex-center" uk-grid>
<div class="uk-card uk-card-body uk-card-default uk-box-shadow-hover-large">
<h4 class="uk-card-title uk-margin-small-top">{{ npc.name }}</h4>
<div class="uk-card-badge uk-label uk-label-primary">{{ npc.guild.name }}</div>
<div class="uk-flex uk-flex-around uk-text-center uk-margin-bottom">
<div>
<div class="uk-text-bold">Level</div>
<div>{{ npc.level }}</div>
</div>
<div>
<div class="uk-text-bold">HP</div>
<div>{{ npc.hp }}</div>
</div>
<div>
<div class="uk-text-bold">AC</div>
<div>{{ npc.armour.ac_mod }}</div>
</div>
</div>
<div class="uk-flex uk-flex-around stat-block">
<div>
<div title="{{ npc.str_mod }}">{{ npc.str }}</div>
<div title="{{ npc.str_mod }}">Str</div>
</div>
<div>
<div title="{{ npc.int_mod }}">{{ npc.int }}</div>
<div title="{{ npc.int_mod }}">Int</div>
</div>
<div>
<div title="{{ npc.wis_mod }}">{{ npc.wis }}</div>
<div title="{{ npc.wis_mod }}">Wis</div>
</div>
<div>
<div title="{{ npc.dex_mod }}">{{ npc.dex }}</div>
<div title="{{ npc.dex_mod }}">Dex</div>
</div>
<div>
<div title="{{ npc.con_mod }}">{{ npc.con }}</div>
<div title="{{ npc.con_mod }}">Con</div>
</div>
<div>
<div title="{{ npc.chr_mod }}">{{ npc.chr }}</div>
<div title="{{ npc.chr_mod }}">Chr</div>
</div>
</div>
<hr class="uk-divider-small uk-text-center">
<div class="uk-flex uk-flex-around uk-text-center uk-margin-bottom save-block uk-margin-top">
<div>
<div title="Petrification & Paralysis">{{ npc.save_pp }}</div>
<div title="Petrification & Paralysis">P &amp; P</div>
</div>
<div>
<div title="Poison & Death">{{ npc.save_pd }}</div>
<div title="Poison & Death">P &amp; D</div>
</div>
<div>
<div title="Blast & Breath">{{ npc.save_bb }}</div>
<div title="Blast & Breath">B &amp; B</div>
</div>
<div>
<div title="Staffs & Wands">{{ npc.save_sw }}</div>
<div title="Staffs & Wands">S &amp; W</div>
</div>
<div>
<div title="Spells">{{ npc.save_sp }}</div>
<div title="Spells">Spells</div>
</div>
</div>
<ul uk-tab>
<li class="uk-active"><a href="">Equipment</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>
<table class="uk-table uk-table-hover uk-table-small item-table">
<thead>
<tr> <th>Name</th><th>Worth</th><th>Thr</th><th>Dmg</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>
</tbody>
</table>
</li>
<li>
<table class="uk-table uk-table-hover uk-table-small spell-table">
<thead>
<tr> <th>Name</th><th>Range</th><th>Duration</th><th>Level</th> </tr>
</thead>
<tbody>
{% 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>
</ul>
</div>
</div>
{% 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>
<div class="uk-modal-header">
<h2 class="uk-modal-title">Roll20 Export</h2>
</div>
<div class="uk-modal-body">
<div class="uk-margin">
<h5>Characters to Export:</h5>
<div id="pe_selects" class="uk-grid-small uk-grid uk-child-width-auto">
{% if npc %}
<label><input class="uk-checkbox" type="checkbox" name="ch[[ loop.index ]]" onchange="prepareSelectiveExport();" checked> {{ npc.name }}</label>
{% endif %}
</div>
<div class="uk-margin uk-align-right">
<div class="uk-button-group">
<button id="pe_none" class="uk-button uk-button-small uk-button-default" onclick="partyExportToggle();">None</button>
<button id="pe_all" class="uk-button uk-button-small uk-button-secondary" onclick="partyExportToggle();">All</button>
</div>
</div>
</div>
<textarea id="party-export-show" class="uk-textarea uk-text-small" rows="10"></textarea>
<textarea id="party-export-data" class="uk-textarea uk-hidden"></textarea>
</div>
<div class="uk-modal-footer uk-text-right">
<button class="uk-button uk-button-default uk-modal-close" type="button">Cancel</button>
<button class="uk-button uk-button-primary" type="button" onclick="exportParty();">Copy</button>
</div>
</div>
</div>
<br>
<style>
table.item-table, table.item-table th, table.spell-table, table.spell-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>
<script type="text/javascript">
var party = [];
function generateNPC() {
// Base level
let bl = document.querySelector('#base_level').value;
if(bl < 0) { bl = 0; }
if(bl > 14) { bl = 14; }
let gl = document.querySelector('#guild_select').value;
window.location = "/npc/single/" + bl.toString() + "/" + gl.toString();
}
function showSpellModal() {
spells = new Set();
{% if npc %}
{% for level in npc.spell_list() %}
{% for spell in npc.spell_list()[level] %}
spells.add({{ spell.roll20_format | tojson }});
{% 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 = atob(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 npc %}
party.push(JSON.parse('{{ npc.roll20_format | tojson }}'));
{% endif %}
for(let cb of document.querySelectorAll('#pe_selects > label > input')) {
cb.checked = true;
}
prepareSelectiveExport();
UIkit.modal(document.querySelector('#party-export-modal')).show();
}
function prepareSelectiveExport() {
var selected_party = [];
var checkboxes = document.querySelectorAll('#pe_selects > label > input');
for(let [i, cb] of checkboxes.entries()) {
if(cb.checked) {
selected_party.push(party[i]);
}
}
document.querySelector('#party-export-data').value = "!acksimport " + JSON.stringify(selected_party);
document.querySelector('#party-export-show').value = JSON.stringify(selected_party, undefined, 2);
}
function exportParty() {
let party_data = document.querySelector('#party-export-data')
party_data.classList.remove('uk-hidden');
party_data.select();
document.execCommand("copy");
party_data.classList.add('uk-hidden');
UIkit.modal(document.querySelector('#party-export-modal')).hide();
}
function partyExportToggle() {
var checkboxes = document.querySelectorAll('#pe_selects > label > input');
for(let cb of checkboxes) {
if(event.target.id === "pe_none") {
cb.checked = false;
}
if(event.target.id === "pe_all") {
cb.checked = true;
}
}
prepareSelectiveExport();
}
</script>
{% endblock %}

85
acks/npc/templates/spell_list.html

@ -6,13 +6,29 @@
<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-small-bottom">
<div>
<label for="spell_level">Spell Level</label>
<input type="number" name="spell_level" id="spell_level" class="uk-input" value="0">
</div>
<div class="uk-margin-left">
<div>
<label for="divine_toggle">Divine</label>
<input type="checkbox" name="divine_toggle" id="divine_toggle" class="uk-checkbox" checked>
</div>
<div>
<label for="arcane_toggle">Arcane</label>
<input type="checkbox" name="arcane_toggle" id="arcane_toggle" class="uk-checkbox" checked>
</div>
</div>
</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 }}" >
<label for="spell_query">Name Search</label>
<input type="text" name="spell_query" id="spell_query" class="uk-input"/>
</div>
<div class="uk-margin-left">
<button class="uk-button uk-button-primary" onclick="generateParty();">Generate</button>
<button class="uk-button uk-button-primary" onclick="applyFilters();">Filter</button>
</div>
</div>
@ -21,7 +37,7 @@
{% 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="acks-npc-card" id="{{ spell.dom_id }}">
<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>
@ -39,6 +55,67 @@
</div>
{% endif %}
<script type="text/javascript">
var spells = [{% for spell in spells %} {{ spell.roll20_format | tojson }}, {% endfor %}];
function filterSpell(filters, spell) {
function name2id(name) {
return "#" + name.toLowerCase().replace(/[\*',]/g, "").replace(/\s/g, "_");
}
let hide = false;
// Handle search query filtering
if(filters.spell_query) {
if(spell.name.toLowerCase().indexOf(filters.spell_query.toLowerCase()) < 0) {
hide = true;
}
}
// Handle school and level filtering
if(!filters.arcane_included && spell.is_arcane) {
hide = true;
}
if(!filters.divine_included && spell.is_divine) {
hide = true;
}
if(filters.spell_level > 0) {
if(spell.is_arcane && spell.arcane != filters.spell_level) {
hide = true;
}
if(spell.is_divine && spell.divine != filters.spell_level) {
hide = true;
}
}
// Hide this spell-card if appropriate
if(hide) {
document.querySelector(name2id(spell.name)).classList.add('uk-hidden');
}
}
function resetFiltering() {
var hidden_spells = document.querySelectorAll("div.acks-npc-card.uk-hidden");
for(let hd of hidden_spells) {
hd.classList.remove('uk-hidden');
}
}
function applyFilters() {
resetFiltering();
filters = {
spell_level: document.querySelector('#spell_level').value,
spell_query: document.querySelector('#spell_query').value,
arcane_included: document.querySelector('#arcane_toggle').checked,
divine_included: document.querySelector('#divine_toggle').checked,
};
spells.forEach((spell, index) => { filterSpell(filters, spell); });
}
</script>
<style>
table.item-table, table.item-table th {
font-size: 12px;

24
acks/npc/views.py

@ -6,8 +6,14 @@ from flask import (
Blueprint
)
from .npc_party import create_party
from .models import Spell
from .npc_party import (
create_party,
create_npc
)
from .models import (
Spell,
CharacterClass
)
npc_views = Blueprint(
@ -29,6 +35,20 @@ def generate_npc_party(base_level=None):
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('/single')
@npc_views.route('/single/<int:base_level>/<int:guild_id>')
def generate_single_npc(base_level=None, guild_id=0):
guilds = CharacterClass.query.filter(CharacterClass.bucket.notin_(['Demi-Human'])).all()
npc = None
if base_level:
npc = create_npc(base_level, guild_id)
# If asked for JSON, return the npc, otherwise render HTML template
if request.args.get('format', 'html') == 'json':
return jsonify(npc)
return render_template('generate_single_npc.html', npc=npc, base_level=base_level, guilds=guilds, guild_id=guild_id)
@npc_views.route('/spells')
def spell_list():
spells = Spell.query.all()

BIN
acks/static/WorldMap.png

After

Width: 2560  |  Height: 1440  |  Size: 4.6 MiB

18
acks/templates/base.html

@ -2,10 +2,10 @@
('/', 'index', 'Home'),
('/handbook', 'handbook', 'Handbook'),
('/npc/party', 'npcparty', 'NPC Party'),
('/npc/single', 'npcsingle', 'Single NPC'),
('/npc/spells', 'spells', 'Spells'),
('/api/schema', 'api', 'API'),
('#', 'treasure', 'Treasure'),
('#', 'henchmen', 'Henchmen'),
('/worldmap', 'worldmap', 'World Map'),
] %}
{% set active_page = active_page|default('index') %}
@ -31,9 +31,10 @@
<body>
<nav class="uk-navbar-container" uk-navbar>
<div class="uk-navbar-left">
<a href="" class="uk-hidden@m uk-button-default uk-margin-small-left uk-margin-small-right" uk-toggle="target: #offcanvas-nav" uk-icon="menu"></a>
<a href="" class="uk-navbar-item uk-logo">Atr0phy ACKS</a>
</div>
<div class="uk-navbar-center">
<div class="uk-navbar-center uk-visible@m">
<ul class="uk-navbar-nav">
{% for href, id, label in navigation_bar %}
<li {% if id == active_page %} class="uk-active" {% endif %}>
@ -43,6 +44,17 @@
</ul>
</div>
</nav>
<div id="offcanvas-nav" uk-offcanvas="overlay: true">
<div class="uk-offcanvas-bar uk-flex uk-flex-column">
<ul class="uk-nav uk-nav-primary uk-nav-center uk-margin-auto-vertical">
{% for href, id, label in navigation_bar %}
<li {% if id == active_page %} class="uk-active" {% endif %}>
<a href="{{ href|e }}">{{ label|e }}</a>
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="uk-container">
{% block content %}{% endblock %}
</div>

15
acks/templates/worldmap.html

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% set active_page = "worldmap" %}
{% block title %}World Map{% endblock %}
{% block content %}
<br/>
<div>
<img id="worldmap-img" src="https://acks.atr0phy.net/static/WorldMap.png"/>
</div>
{% endblock %}
{% block head %}
<style>
</style>
{% endblock %}

4
acks/views.py

@ -11,3 +11,7 @@ def index():
@default_views.route('/handbook')
def handbook():
return render_template('handbook.html')
@default_views.route('/worldmap')
def worldmap():
return render_template('worldmap.html')

117
roll20/hit_dice.js

@ -0,0 +1,117 @@
var HitDice = HitDice || (function() {
'use strict';
var tokenIds = [],
configure = function() {
if(!state.HitDice) {
state.HitDice = {
version: 0.1,
config: {
bar: 3,
hitDiceAttribute: 'npc_hitdice',
}
};
}
},
handleInput = function(msg) {
if (msg.type === "api" && /^!mhd(\b|$)/i.test(msg.content) && playerIsGM(msg.playerid) ) {
let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname');
let count = 0;
// WUSSALLTHISTHEN
(msg.selected || [])
.map(o=>getObj('graphic',o._id))
.filter(g=>undefined !== g)
.forEach( o => {
++count;
tokenIds.push(o.id);
rollHitDice(o);
})
;
sendChat('',`/w "${who}" Rolling hit dice for ${count} token(s).`);
}
},
findRoll = function(txt){
return txt.match(/\d+d\d+(\+\d+)?/)[0] || 0;
},
rollHitDice = function(obj) {
var sets = {},
bar = 'bar'+state.HitDice.config.bar,
hdAttrib,
hdExpression = 0,
bonus = 0
;
if(_.contains(tokenIds,obj.id)){
tokenIds=_.without(tokenIds,obj.id);
if('graphic' === obj.get('type') &&
'token' === obj.get('subtype') &&
'' !== obj.get('represents')
) {
if( obj && '' === obj.get(bar+'_link') ) {
hdAttrib = findObjs({
type: 'attribute',
characterid: obj.get('represents'),
name: state.HitDice.config.hitDiceAttribute
})[0];
if( hdAttrib ) {
//sendChat('', 'HERE WE ARE');
//log(hdAttrib);
hdExpression = findRoll(hdAttrib.get('current'));
sendChat('','/r '+hdExpression+'+'+bonus,function(r){
var hp=0;
_.each(r,function(subr){
var val=JSON.parse(subr.content);
if(_.has(val,'total'))
{
hp+=val.total;
}
});
sets[bar+"_value"] = hp||1;
sets[bar+"_max"] = hp||1;
obj.set(sets);
});
}
}
}
}
},
saveTokenId = function(obj){
tokenIds.push(obj.id);
setTimeout((function(id){
return function(){
var token=getObj('graphic',id);
if(token){
rollHitDice(token);
}
};
}(obj.id)),100);
},
registerEventHandlers = function() {
on('chat:message', handleInput);
on('add:graphic', saveTokenId);
on('change:graphic', rollHitDice);
};
return {
configure: configure,
RegisterEventHandlers: registerEventHandlers
};
}());
on('ready',function() {
'use strict';
HitDice.configure();
HitDice.RegisterEventHandlers();
});
Loading…
Cancel
Save