5 Commits

  1. 1
      acks/npc/commands.py
  2. 15
      acks/npc/models.py
  3. 22
      acks/npc/npc_party.py
  4. 13
      acks/npc/templates/generate_npc_party.html
  5. 355
      acks/npc/templates/generate_single_npc.html
  6. 92
      acks/npc/templates/spell_list.html
  7. 24
      acks/npc/views.py
  8. BIN
      acks/static/WorldMap.png
  9. 50
      acks/templates/base.html
  10. 8
      acks/templates/handbook.html
  11. 8
      acks/templates/index.html
  12. 68
      acks/templates/treasure.html
  13. 23
      acks/templates/worldmap.html
  14. 20
      acks/views.py
  15. 144
      roll20/base64.js
  16. 13
      roll20/char_sheet.css
  17. 23
      roll20/char_sheet.html
  18. 117
      roll20/hit_dice.js
  19. 19
      roll20/npc_party_import.js

1
acks/npc/commands.py

@ -55,6 +55,7 @@ def populate_npc_database():
spell.arcane = 0
if spell.divine == '':
spell.divine = 0
spell.description = spell.description.strip()
db.session.bulk_save_objects(spells)
db.session.commit()

15
acks/npc/models.py

@ -1,3 +1,5 @@
import base64
from flask_sqlalchemy import SQLAlchemy
from ..models import db, BaseModel
@ -109,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 = {
@ -120,7 +126,7 @@ class Spell(BaseModel):
'is_divine': self.is_divine,
'arcane': self.arcane,
'is_arcane': self.is_arcane,
'description': self.description #.replace('"', '\\"').replace("'", "\\'")
'description': base64.b64encode(self.description.encode('ascii')).decode('ascii')
}
return spell_dict
@ -283,6 +289,13 @@ class CharacterNPC(BaseModel):
}
}
if self.guild.is_divine_spellcaster or self.guild.is_arcane_spellcaster:
npc_dict['spells'] = []
spell_list = self.spell_list()
for level in spell_list:
for spell in spell_list[level]:
npc_dict['spells'].append(spell.roll20_format)
if self.ranged:
npc_dict['ranged'] = {
'name': self.ranged.name,

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

13
acks/npc/templates/generate_npc_party.html

@ -4,7 +4,7 @@
{% block title %}NPC Party Generation{% endblock %}
{% block content %}
<div class="uk-flex uk-flex-center uk-margin-bottom uk-margin-top">
<h1>Adventurer Conqueror King NPC Party Generator</h1>
<h1 class="uk-text-center"><strong>Adventurer Conqueror King</strong>NPC Party Generator</h1>
</div>
<div class="uk-flex uk-flex-bottom uk-flex-center uk-margin-large-bottom">
<div>
@ -238,6 +238,11 @@ div.save-block > div > div:last-child {
div.acks-npc-card {
width: 400px;
}
h1 strong {
display: block;
font-size: 50%;
opacity: 0.65;
}
</style>
<script type="text/javascript">
var party = [];
@ -270,7 +275,7 @@ div.acks-npc-card {
// Fill in spell details
document.querySelector('#spell-modal-title').innerText = modal_spell.name;
document.querySelector('#spell-modal-desc').innerText = modal_spell.description;
document.querySelector('#spell-modal-desc').innerText = atob(modal_spell.description);
if(spell.range) {
document.querySelector('#spell-modal-range').innerText = modal_spell.range;
@ -294,9 +299,9 @@ div.acks-npc-card {
function showExportModal() {
{% if party %}
{% for npc in party %}
{% for npc in party %}
party.push(JSON.parse('{{ npc.roll20_format | tojson }}'));
{% endfor %}
{% endfor %}
{% endif %}
for(let cb of document.querySelectorAll('#pe_selects > label > input')) {

355
acks/npc/templates/generate_single_npc.html

@ -0,0 +1,355 @@
{% 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 class="uk-text-center"><strong>Adventurer Conqueror King</strong>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;
}
h1 strong {
display: block;
font-size: 50%;
opacity: 0.65;
}
</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 %}

92
acks/npc/templates/spell_list.html

@ -4,15 +4,31 @@
{% 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>
<h1 class="uk-text-center"><strong>Adventurer Conqueror King</strong>Spell Reference</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;
@ -69,5 +146,10 @@ div.save-block > div > div:last-child {
div.acks-npc-card {
width: 400px;
}
h1 strong {
display: block;
font-size: 50%;
opacity: 0.65;
}
</style>
{% endblock %}

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

50
acks/templates/base.html

@ -1,18 +1,22 @@
{% set navigation_bar = [
('/', 'index', 'Home'),
('/handbook', 'handbook', 'Handbook'),
('/npc/party', 'npcparty', 'NPC Party'),
('/worldmap', 'worldmap', 'World Map'),
('', 'generate', 'Generate'),
('/npc/spells', 'spells', 'Spells'),
('/api/schema', 'api', 'API'),
('#', 'treasure', 'Treasure'),
('#', 'henchmen', 'Henchmen'),
] %}
{% set generation_bar = [
('/npc/party', 'npcparty', 'NPC Party'),
('/npc/single', 'npcsingle', 'Single NPC'),
('/treasure', 'treasure', 'Treasure'),
] %}
{% set active_page = active_page|default('index') %}
<!doctype html>
<html>
<head>
<title>{% block title %}{% endblock %} - Atr0phy ACKS</title>
<title>{% block title %}{% endblock %} - Palisma ACKS</title>
<meta charset="utf-8">
<meta name="viewport" contents="width=device-width, initial-scale=1">
@ -31,18 +35,52 @@
<body>
<nav class="uk-navbar-container" uk-navbar>
<div class="uk-navbar-left">
<a href="" class="uk-navbar-item uk-logo">Atr0phy ACKS</a>
<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">Palisma 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 %}>
{% if id == 'generate' %}
<a href="">{{ label |e }}</a>
<div class="uk-navbar-dropdown">
<ul class="uk-nav uk-navbar-dropdown-nav">
{% for ghref, gid, glabel in generation_bar %}
<li><a href="{{ ghref | e }}">{{ glabel|e }}</a></li>
{% endfor %}
</ul>
</div>
{% else %}
<a href="{{ href|e }}">{{ label|e }}</a>
{% endif %}
</li>
{% endfor %}
</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 %}>
{% if id == 'generate' %}
<a href="">{{ label |e }}</a>
<div class="uk-navbar-dropdown">
<ul class="uk-nav uk-navbar-dropdown-nav">
{% for ghref, gid, glabel in generation_bar %}
<li><a href="{{ ghref | e }}">{{ glabel|e }}</a></li>
{% endfor %}
</ul>
</div>
{% else %}
<a href="{{ href|e }}">{{ label|e }}</a>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="uk-container">
{% block content %}{% endblock %}
</div>

8
acks/templates/handbook.html

@ -3,6 +3,9 @@
{% block title %}ACKS Handbook{% endblock %}
{% block content %}
<div class="uk-flex uk-flex-center uk-margin-bottom uk-margin-top">
<h1 class="uk-text-center"><strong>Adventurer Conqueror King</strong>Handbook</h1>
</div>
<div id="frame-container">
<iframe id="handbook-frame" src="https://atr0phy.net/acks/handbook"/>
</div>
@ -22,5 +25,10 @@
margin: 0;
padding: 0;
}
h1 strong {
display: block;
font-size: 50%;
opacity: 0.65;
}
</style>
{% endblock %}

8
acks/templates/index.html

@ -3,11 +3,17 @@
{% block title %}ACKS Toolset Home{% endblock %}
{% block content %}
<div>
<div class="uk-flex uk-flex-center uk-margin-bottom uk-margin-top">
<h1 class="uk-text-center"><strong>Adventurer Conqueror King</strong>Home</h1>
</div>
{% endblock %}
{% block head %}
<style>
h1 strong {
display: block;
font-size: 50%;
opacity: 0.65;
}
</style>
{% endblock %}

68
acks/templates/treasure.html

@ -0,0 +1,68 @@
{% extends "base.html" %}
{% set active_page = "handbook" %}
{% set treasure_letters = [
("A", "Incidental 275gp"),
("B", "Hoarder 500gp"),
("C", "Incidental 700gp"),
("D", "Hoarder 1,000gp"),
("E", "Raider 1,250gp"),
("F", "Incidental 1,500gp"),
("G", "Raider 2,000gp"),
("H", "Hoarder 2,500gp"),
("I", "Incidental 3,250gp"),
("J", "Raider 4,000gp"),
("K", "Incidental 5,000gp"),
("L", "Raider 6,000gp"),
("M", "Incidental 8,000gp"),
("N", "Hoarder 9,000gp"),
("O", "Raider 12,000gp"),
("P", "Incidental 17,000gp"),
("Q", "Hoarder 22,000gp"),
("R", "Hoarder 45,000gp"),
] %}
{% block title %}ACKS Treasure Generator{% endblock %}
{% block content %}
<div class="uk-flex uk-flex-center uk-margin-bottom uk-margin-top">
<h1 class="uk-text-center"><strong>Adventurer Conqueror King</strong>Treasure Generator</h1>
</div>
<div class="uk-flex uk-flex-bottom uk-flex-center uk-margin-large-bottom">
<div>
<label for="treasure-type" class="uk-form-label">Select Treasure Type</label>
<select id="treasure-type" class="uk-select">
<option value="">Please select one</option>
{% for v, l in treasure_letters %}
<option value="{{ v }}" {% if treasure_type == v %} selected="true" {% endif %}>{{ v }} ({{ l }})</option>
{% endfor %}
</select>
</div>
<div class="uk-margin-left">
<button class="uk-button uk-button-primary" onclick="generateTreasure();">Generate</button>
</div>
</div>
{% if generated_treasure %}
<div class="uk-flex uk-flex-bottom uk-flex-center uk-margin-large-bottom">
<div id="treasure-content">
{{ generated_treasure|safe }}
</div>
</div>
{% endif %}
{% endblock %}
{% block head %}
<style>
h1 strong {
display: block;
font-size: 50%;
opacity: 0.65;
}
</style>
<script>
function generateTreasure() {
var treasure_type = document.querySelector("select#treasure-type").value;
window.location = "/treasure/" + treasure_type.toString();
}
</script>
{% endblock %}

23
acks/templates/worldmap.html

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% set active_page = "worldmap" %}
{% block title %}World Map{% endblock %}
{% block content %}
<div class="uk-flex uk-flex-center uk-margin-bottom uk-margin-top">
<h1 class="uk-text-center"><strong>Adventurer Conqueror King</strong>World Map</h1>
</div>
<br/>
<div>
<img id="worldmap-img" src="https://acks.atr0phy.net/static/WorldMap.png"/>
</div>
{% endblock %}
{% block head %}
<style>
h1 strong {
display: block;
font-size: 50%;
opacity: 0.65;
}
</style>
{% endblock %}

20
acks/views.py

@ -1,3 +1,4 @@
import requests
from flask import current_app, Blueprint, render_template, url_for, redirect
@ -11,3 +12,22 @@ def index():
@default_views.route('/handbook')
def handbook():
return render_template('handbook.html')
@default_views.route('/worldmap')
def worldmap():
return render_template('worldmap.html')
@default_views.route('/treasure')
@default_views.route('/treasure/<string:treasure_type>')
def treasure(treasure_type=None):
if treasure_type is not None:
headers = {'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'}
payload = {
"treasure_type": treasure_type,
"form_id": "acks_treasure_form",
"form_build_id": "form-PqO-1VglfW4Q3x1fm7HMewhLFjst2oxY5AR_m6WOGBg"
}
response = requests.request("POST", "http://autarch.co/system/ajax", data=payload, headers=headers)
generated_treasure = response.json()[1]["data"].replace('class="form-textarea"', 'class="uk-textarea"')
return render_template('treasure.html', generated_treasure=generated_treasure, treasure_type=treasure_type)
return render_template('treasure.html')

144
roll20/base64.js

@ -0,0 +1,144 @@
// Github: https://github.com/shdwjk/Roll20API/blob/master/Base64/Base64.js
// By: The Aaron, Arcane Scriptomancer
// Contact: https://app.roll20.net/users/104025/the-aaron
// modified from: http://www.webtoolkit.info/
const Base64 = (() => { // eslint-disable-line no-unused-vars
const version = '0.3.2';
const lastUpdate = 1576507905;
const keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
const checkInstall = () => {
log('-=> Base64 v'+version+' <=- ['+(new Date(lastUpdate*1000))+']');
};
// private method for UTF-8 encoding
const utf8_encode = (string) => {
let utftext = '';
for (let n = 0; n < string.length; n++) {
let c1 = string.charCodeAt(n);
if (c1 < 128) {
utftext += String.fromCharCode(c1);
}
else if((c1 > 127) && (c1 < 2048)) {
utftext += String.fromCharCode((c1 >> 6) | 192);
utftext += String.fromCharCode((c1 & 63) | 128);
}
else {
utftext += String.fromCharCode((c1 >> 12) | 224);
utftext += String.fromCharCode(((c1 >> 6) & 63) | 128);
utftext += String.fromCharCode((c1 & 63) | 128);
}
}
return utftext;
};
// private method for UTF-8 decoding
const utf8_decode = (utftext) => {
let string = '';
let i = 0;
while ( i < utftext.length ) {
let c1 = utftext.charCodeAt(i);
if (c1 < 128) {
string += String.fromCharCode(c1);
i++;
}
else if((c1 > 191) && (c1 < 224)) {
let c2 = utftext.charCodeAt(i+1);
string += String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));
i += 2;
}
else {
let c2 = utftext.charCodeAt(i+1);
let c3 = utftext.charCodeAt(i+2);
string += String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
};
const encode = (input) => {
let output = '';
let i = 0;
input = utf8_encode(input);
while (i < input.length) {
let chr1 = input.charCodeAt(i++);
let chr2 = input.charCodeAt(i++);
let chr3 = input.charCodeAt(i++);
let enc1 = chr1 >> 2;
let enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
let enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
let enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
keyStr.charAt(enc1) + keyStr.charAt(enc2) +
keyStr.charAt(enc3) + keyStr.charAt(enc4);
}
return output;
};
// public method for decoding
const decode = (input) => {
let output = '';
let i = 0;
input = input.replace(/[^A-Za-z0-9+/=]/g, "");
while (i < input.length) {
let enc1 = keyStr.indexOf(input.charAt(i++));
let enc2 = keyStr.indexOf(input.charAt(i++));
let enc3 = keyStr.indexOf(input.charAt(i++));
let enc4 = keyStr.indexOf(input.charAt(i++));
let chr1 = (enc1 << 2) | (enc2 >> 4);
let chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
let chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 !== 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 !== 64) {
output = output + String.fromCharCode(chr3);
}
}
output = utf8_decode(output);
return output;
};
on("ready",()=>{
checkInstall();
});
return {
encode: encode,
decode: decode
};
})();

13
roll20/char_sheet.css

@ -120,7 +120,7 @@ div.sheet-currency-grid {
}
div.sheet-spells-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
row-gap: 5px;
column-gap: 10px;
margin-bottom: 5px;
@ -207,7 +207,7 @@ input.sheet-spells-input {
grid-column: 1 / span 2;
}
.sheet-grid-span-4-2 {
grid-column: 4 / span 2;
grid-column: 4 / span 3;
}
input.sheet-input-big {
width: 55px!important;
@ -250,7 +250,7 @@ input.sheet-input-big {
}
.sheet-spells-title {
text-align: center;
grid-column: 1 / span 7;
grid-column: 1 / span 9;
}
.sheet-exp-title {
text-align: center;
@ -357,3 +357,10 @@ div.sheet-npc { display: none; }
/*input.sheet-block-switch { display: none; };*/
input.sheet-block-switch:checked ~ div.sheet-main { display: none; }
input.sheet-block-switch:checked ~ div.sheet-npc { display: grid; }
textarea.sheet-txtarea {
height: 24px;
margin: 0;
padding: 0;
text-align: left;
}

23
roll20/char_sheet.html

@ -284,7 +284,7 @@
<div class="sheet-spells-title">
<h3>Spells Per Day</h3>
</div>
<span></span><span>One</span><span>Two</span><span>Three</span><span>Four</span><span>Five</span><span>Six</span>
<span></span><span>One</span><span>Two</span><span>Three</span><span>Four</span><span>Five</span><span>Six</span><span></span><span></span>
<span>Used</span>
<input class="spells-input" type="text" name="attr_spells_l1">
<input class="spells-input" type="text" name="attr_spells_l2">
@ -292,6 +292,7 @@
<input class="spells-input" type="text" name="attr_spells_l4">
<input class="spells-input" type="text" name="attr_spells_l5">
<input class="spells-input" type="text" name="attr_spells_l6">
<span></span><span></span>
<span>Total</span>
<input class="spells-input" type="text" name="attr_spells_l1_max">
@ -300,20 +301,23 @@
<input class="spells-input" type="text" name="attr_spells_l4_max">
<input class="spells-input" type="text" name="attr_spells_l5_max">
<input class="spells-input" type="text" name="attr_spells_l6_max">
<span></span><span></span>
</div>
<div class="spells-grid">
<div class="sheet-spells-title sheet-margin-top">
<h3>Spellbook</h3>
</div>
<span class="grid-span-1-2">Spell Name</span><span>Level</span><span class="grid-span-4-2">Effect</span><span>Damage</span><span></span><span></span><span></span>
<span class="grid-span-1-2">Spell Name</span><span>Level</span><span class="grid-span-4-2">Effect</span><span>Range</span><span>Roll</span><span></span><span></span><span></span><span></span>
</div>
<fieldset class="repeating_spells">
<div class="spells-grid">
<input class="sheet-name-input grid-span-1-2" type="text" name="attr_spell_name">
<div><input class="sheet-stat-input" type="text" name="attr_spell_level"></div>
<div class="grid-span-4-2"><input class="sheet-stat-input" type="text" name="attr_spell_effect"></div>
<div><input class="sheet-stat-input" type="text" name="attr_spell_damage"></div>
<div><button class="sheet-roll-button" type="roll" value="&{template:acks} {{name=@{spell_name}}} {{subheader=Level @{spell_level} Arcane Spell}} {{desc=@{spell_effect}}} {{damage=[[@{spell_damage}]]}}" name="roll_Spell"></button></div>
<div class="grid-span-4-2"><textarea class="sheet-stat-input sheet-txtarea" name="attr_spell_effect" ></textarea></div>
<div><input class="sheet-stat-input" type="text" name="attr_spell_range"></div>
<div><input class="sheet-stat-input" type="text" name="attr_spell_roll"></div>
<div><button class="sheet-roll-button" type="roll" value="&{template:acks} {{name=@{spell_name}}} {{subheader=Level @{spell_level} @{spell_school} Spell}} {{desc=@{spell_effect}}} {{roll=[[@{spell_roll}]]}} {{range=@{spell_range}}}" name="roll_Spell"></button></div>
<div class="sheet-hidden"><input type="text" name="attr_spell_school"></div>
</div>
</fieldset>
</div>
@ -440,13 +444,16 @@
<div class="sheet-acks-subheader sheet-acks-row">{{subheader}}</div>
{{/subheader}}
{{#target}}
<div class="sheet-acks-row">Target <span class="sheet-acks-target-value">{{target}}+</span></div>
<div class="sheet-acks-row"><strong>Target</strong> <span class="sheet-acks-target-value">{{target}}+</span></div>
{{/target}}
{{#range}}
<div class="sheet-acks-row"><strong>Range</strong> <span class="sheet-acks-target-value">{{range}}</span></div>
{{/range}}
{{#roll}}
<div class="sheet-acks-row">Roll {{roll}}</div>
<div class="sheet-acks-row"><strong>Roll</strong> {{roll}}</div>
{{/roll}}
{{#damage}}
<div class="sheet-acks-row">Damage {{damage}}</div>
<div class="sheet-acks-row"><strong>Damage</strong> {{damage}}</div>
{{/damage}}
{{#desc}}
<div class="sheet-acks-row sheet-acks-desc">{{desc}}</div>

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();
});

19
roll20/npc_party_import.js

@ -107,6 +107,25 @@ on("chat:message", function(msg) {
addAttr(sheet.id, melee_name + "_attack_name", c.melee.name, null);
addAttr(sheet.id, melee_name + "_attack_throw_mod", c.melee.throw_mod, null);
addAttr(sheet.id, melee_name + "_attack_dmg", c.melee.damage, null);
if(c.spells) {
for(spell of c.spells) {
let spell_name = "repeating_spells_" + generateRowID();
addAttr(sheet.id, spell_name + "_spell_name", spell.name, null);
addAttr(sheet.id, spell_name + "_spell_effect", Base64.decode(spell.description), null);
addAttr(sheet.id, spell_name + "_spell_range", spell.range, null);
addAttr(sheet.id, spell_name + "_spell_roll", spell.roll || 0, null);
if(spell.is_arcane) {
addAttr(sheet.id, spell_name + "_spell_school", "Arcane", null);
addAttr(sheet.id, spell_name + "_spell_level", spell.arcane, null);
}
if(spell.is_divine) {
addAttr(sheet.id, spell_name + "_spell_school", "Divine", null);
addAttr(sheet.id, spell_name + "_spell_level", spell.divine, null);
}
}
}
}
}

Loading…
Cancel
Save