Initial commit

This commit is contained in:
Joey Kimsey
2024-08-04 21:15:59 -04:00
parent c9d1fb983f
commit 0d469501ee
695 changed files with 70184 additions and 71 deletions

View File

@ -0,0 +1,43 @@
<?php
/**
* app/plugins/initiativetracker/controllers/initiative.php
*
* This is the home controller for the initiativetracker plugin.
*
* @package TP InitiativeTracker
* @version 3.0
* @author Joey Kimsey <Joey@thetempusproject.com>
* @link https://TheTempusProject.com
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
*/
namespace TheTempusProject\Controllers;
use TheTempusProject\Houdini\Classes\Views;
use TheTempusProject\Classes\Controller;
use TheTempusProject\Models\Inithistory;
use TheTempusProject\Houdini\Classes\Issues;
use TheTempusProject\Houdini\Classes\Components;
use TheTempusProject\TheTempusProject as App;
class Initiative extends Controller {
protected static $initiativeHistory;
public function __construct() {
self::$title = 'Initiative Tracker - {SITENAME}';
self::$pageDescription = 'This is for tracking player and creature turns and initiatives.';
self::$initiativeHistory = new Inithistory;
if ( ! App::$isLoggedIn ) {
Issues::add( 'info', 'If you register an account and log in, you can save your initiatives.' );
Components::set( 'initiativeHistory', '' );
} else {
$history = self::$initiativeHistory->userHistory();
$historyView = Views::simpleView( 'initiativetracker.historyElement', $history );
Components::set( 'initiativeHistory', $historyView );
}
parent::__construct();
}
public function index() {
Views::view( 'initiativetracker.index' );
}
}

View File

@ -0,0 +1,35 @@
.initiative-container {
margin-bottom: 50px;
}
.character-form {
margin-bottom: 50px;
}
.list-controls {
margin-bottom: 30px;
}
.round-footer {
padding-right: 15px;
padding-bottom: 20px;
padding-left: 15px;
}
.list-controls-center,.list-controls,.character-form {
display: flex;
justify-content: center;
align-items: center;
}
.list-controls-last {
display: flex;
justify-content: right;
align-items: right;
}
.list-controls-first {
display: flex;
justify-content: left;
align-items: left;
}

View File

@ -0,0 +1,414 @@
$(document).ready(function() {
// Top Checkbox Controls
$('input[type="checkbox"]').change(function() {
var name = $(this).attr('name');
if ( 'trackAC' == name ) {
var eles = document.getElementsByClassName('ac-tracker');
}
if ( 'trackHP' == name ) {
var eles = document.getElementsByClassName('hp-tracker');
}
if ( 'trackRounds' == name ) {
var eles = document.getElementsByClassName('rounds-tracker');
}
if ($(this).is(':checked')) {
Array.prototype.forEach.call(eles, function(ele) {
ele.style.display = '';
});
} else {
Array.prototype.forEach.call(eles, function(ele) {
ele.style.display = 'none';
});
}
});
$(document).on('click', '.character-remove', function() {
var characterId = $(this).closest('tr').data('character-id');
var allTrsWithCharacterId = document.querySelectorAll(`tr[data-round-character-id="${characterId}"]`);
allTrsWithCharacterId.forEach(function(tr) {
tr.remove();
});
var allTrsWithCharacterId = document.querySelectorAll(`tr[data-character-id="${characterId}"]`);
allTrsWithCharacterId.forEach(function(tr) {
tr.remove();
});
findByIdAndRemove( characterId, characterInitiativeList );
});
$(document).on('click', '.hp-minus', function() {
var characterId = $(this).closest('tr').data('character-id');
var dataValue = parseInt($(this).data('value'), 10);
var allTrsWithCharacterId = document.querySelectorAll(`tr[data-character-id="${characterId}"]`);
allTrsWithCharacterId.forEach(function(tr) {
var $input = $(tr).find('input[name="hp"]');
var newValue = parseInt($input.val(), 10) - dataValue;
$input.val(newValue);
});
var hpIndex = findById( characterId );
hpIndex.hp = parseInt(hpIndex.hp) - dataValue;
});
$(document).on('click', '.hp-plus', function() {
var characterId = $(this).closest('tr').data('character-id');
var dataValue = parseInt($(this).data('value'), 10);
var allTrsWithCharacterId = document.querySelectorAll(`tr[data-character-id="${characterId}"]`);
allTrsWithCharacterId.forEach(function(tr) {
var $input = $(tr).find('input[name="hp"]');
var newValue = parseInt($input.val(), 10) + dataValue;
$input.val(newValue);
});
var hpIndex = findById( characterId );
hpIndex.hp = parseInt(hpIndex.hp) + dataValue;
});
});
let currentCharacter;
let roundCount = 0;
let roundHistory = [];
let characterInitiativeList = [];
function findById(id) {
return characterInitiativeList.find(item => item.id === id);
}
function findByIdAndRemove(id, array) {
const index = array.findIndex(item => item.id === id);
if (index !== -1) {
array.splice(index, 1);
}
}
const resetCharBtn = document.getElementById('character-reset');
const resetCharacter = async function (event) {
formReset();
}
const resetListBtn = document.getElementById('list-reset');
const resetListEvent = async function (event) {
resetList();
}
const nextCharBtn = document.getElementById('list-next');
const nextCharacter = async function (event) {
cycleCharacter();
}
function cycleCharacter() {
if ( !currentCharacter ) {
currentCharacter = 0;
}
if ( currentCharacter == characterInitiativeList.length ) {
currentCharacter = 0;
roundCount++;
addRoundHistory();
updateRoundCount();
}
if ( !characterInitiativeList[currentCharacter] ) {
currentCharacter = 0;
}
var allTrs = document.querySelectorAll(`tr`);
allTrs.forEach(function(tr) {
tr.classList.remove("success");
});
var allTrsWithCharacterId = document.querySelectorAll(`tr[data-character-id="${characterInitiativeList[currentCharacter].id}"]`);
allTrsWithCharacterId.forEach(function(tr) {
tr.classList.add("success");
});
currentCharacter++;
}
const clearListBtn = document.getElementById('list-clear');
const clearListEvent = async function (event) {
clearList();
hideList();
}
const sortCharBtn = document.getElementById('character-sort');
const sortCharacters = async function (event) {
event.preventDefault();
sortthem();
}
function sortthem() {
var fieldName = document.getElementById('sortBy').value;
clearListHTML();
resetList();
var newList = sortCharacterListByField( fieldName, characterInitiativeList );
for (let i = 0; i < newList.length; i++) {
addCharacterRow( newList[i] );
}
}
function sortCharacterListByField( fieldName, list ) {
if ( 'id' == fieldName ) {
return list;
}
let sortedList = [...list];
sortedList.sort( ( a, b ) => {
let fieldA = a[fieldName];
let fieldB = b[fieldName];
if ( 'type' == fieldName || 'name' == fieldName ) {
return fieldA.localeCompare(fieldB);
}
if (fieldA > fieldB) {
return -1;
}
if (fieldA < fieldB) {
return 1;
}
return 0;
});
return sortedList;
}
const addCharBtn = document.getElementById('character-add');
const addCharacter = function (event) {
createCharacterFromForm();
}
function createCharacterFromForm() {
showList();
var id = Date.now().toString(36) + Math.random().toString(36).substring(2);;
var name = document.getElementById('name').value;
if ( !name ) {
name = '4816';
}
var initiative = document.getElementById('initiative').value;
if ( !initiative ) {
initiative = getRandomInt( 1, 21 );
}
initiative = parseInt( initiative );
var ac = document.getElementById('ac').value;
if ( !ac ) {
ac = 0;
}
ac = parseInt( ac );
var hp = document.getElementById('hp').value;
if ( !hp ) {
hp = 0;
}
hp = parseInt( hp );
var characterType = document.querySelector('input[name="characterType"]:checked').value;
var character = {
id: id,
name: name,
initiative: initiative,
ac: ac,
hp: hp,
type: characterType,
};
characterInitiativeList.push( character );
// re-order the list by initiative to keep the next character working properly
characterInitiativeList = sortCharacterListByField( 'initiative', characterInitiativeList );
addCharacterRow( character );
addRoundRow( character );
}
const d20Btn = document.getElementById('d20-roll');
const rollD20 = async function (event) {
event.preventDefault();
var resultDiv = document.getElementById('initiative');
resultDiv.value = getRandomInt( 1, 21 );
}
const resetRoundsBtn = document.getElementById('rounds-reset');
const resetRoundsEvent = async function (event) {
resetRoundCount();
}
const clearRoundsBtn = document.getElementById('rounds-clear');
const clearRoundsEvent = async function (event) {
clearRounds();
}
window.addEventListener('DOMContentLoaded', (event) => {
d20Btn.addEventListener('click', rollD20);
addCharBtn.addEventListener('click', addCharacter);
resetCharBtn.addEventListener('click', resetCharacter);
resetListBtn.addEventListener('click', resetListEvent);
clearListBtn.addEventListener('click', clearListEvent);
nextCharBtn.addEventListener('click', nextCharacter);
resetRoundsBtn.addEventListener('click', resetRoundsEvent);
clearRoundsBtn.addEventListener('click', clearRoundsEvent);
sortCharBtn.addEventListener('click', sortCharacters);
updateRoundCount();
});
function formReset() {
document.getElementById('name').value = null;
document.getElementById('initiative').value = null;
document.getElementById('ac').value = null;
document.getElementById('hp').value = null;
}
function showList() {
var tbody = document.getElementById('character-container');
tbody.style.display = '';
}
function addCharacterRow( character ) {
var characterList = document.getElementById('character-list');
var pcList = document.getElementById('character-list-pc');
var npcList = document.getElementById('character-list-npc');
var row = document.createElement('tr');
row.setAttribute('data-character-id', character.id);
var hpStyle = '';
var acStyle = '';
var checkbox = document.getElementById('trackHP');
if ( !checkbox.checked ) {
var hpStyle = 'style="display: none;"';
}
var checkbox = document.getElementById('trackAC');
if ( !checkbox.checked ) {
var acStyle = 'style="display: none;"';
}
row.innerHTML = `
<td>${character.name}</td>
<td class="text-center">${character.initiative}</td>
<td class="ac-tracker text-center"${acStyle}>${character.ac}</td>
<td class="hp-tracker text-center"${hpStyle}>
<div class="form-group hp-tracker list-controls-center">
<div class="input-group col-lg-6">
<div class="input-group-btn">
<button class="hp-minus btn btn-danger" data-value="1" aria-label="Remove 1 hp">-1</button>
<button class="hp-minus btn btn-danger" data-value="5" aria-label="Remove 5 hp">-5</button>
<button class="hp-minus btn btn-danger" data-value="10" aria-label="Remove 10 hp">-10</button>
</div>
<input type="number" class="form-control" name="hp" value="${character.hp}">
<div class="input-group-btn">
<button class="hp-plus btn btn-success" data-value="10" aria-label="Add 10 hp">+10</button>
<button class="hp-plus btn btn-success" data-value="5" aria-label="Add 5 hp">+5</button>
<button class="hp-plus btn btn-success" data-value="1" aria-label="Add 1 hp">+1</button>
</div>
</div>
</div>
</td>
<td class="text-center">${character.type}</td>
<td class="text-right"><button class="btn btn-danger btn-sm character-remove" aria-label="Add 10 hp"><span class="glyphicon glyphicon-trash"></span></button></td>
`;
// Append the new row to the table body
characterList.appendChild(row);
if ( 'pc' == character.type ) {
var pc_row = row.cloneNode(true);
pcList.appendChild(pc_row);
}
if ( 'npc' == character.type ) {
var npc_row = row.cloneNode(true);
npcList.appendChild(npc_row);
}
}
function hideList() {
var tbody = document.getElementById('character-container');
tbody.style.display = 'none';
}
function clearList() {
characterInitiativeList = [];
clearListHTML();
}
function clearListHTML() {
var tbody = document.getElementById('character-list');
tbody.innerHTML = '';
var tbody = document.getElementById('character-list-pc');
tbody.innerHTML = '';
var tbody = document.getElementById('character-list-npc');
tbody.innerHTML = '';
}
function resetList() {
currentCharacter = 0;
var allTrsWithCharacterId = document.querySelectorAll(`tr`);
allTrsWithCharacterId.forEach(function(tr) {
tr.classList.remove("success");
});
}
function clearRounds() {
var headers = document.getElementById('rounds-history-header-row');
var thElements = headers.getElementsByTagName('th');
// Loop through the th elements in reverse order to safely remove them without affecting the iteration
for (var i = thElements.length - 1; i > 0; i--) {
thElements[i].parentNode.removeChild(thElements[i]);
}
var history = document.getElementById('rounds-history');
var trElements = history.getElementsByTagName('tr');
for (var i = 0; i < trElements.length; i++) {
var elements = trElements[i].getElementsByTagName('td');
for (var x = elements.length - 1; x > 0; x--) {
elements[x].parentNode.removeChild(elements[x]);
}
}
roundCount = 0;
updateRoundCount();
}
function clearCharacters() {
var history = document.getElementById('rounds-history');
var trElements = history.getElementsByTagName('tr');
for (var i = 0; i < trElements.length; i++) {
trElements[i].remove();
}
}
function resetRoundCount() {
roundCount = 0;
updateRoundCount();
}
function updateRoundCount() {
var round = document.getElementById('round-count');
round.innerText = roundCount;
}
function addRoundRow( character ) {
var characterList = document.getElementById('rounds-history');
var row = document.createElement('tr');
row.setAttribute('data-round-character-id', character.id);
row.innerHTML = `<td class="char-name">${character.name}</td>`;
characterList.appendChild(row);
}
function addRoundHistory() {
var headers = document.getElementById('rounds-history-header');
var newHeader = document.createElement('th');
newHeader.innerText = "Round " + roundCount;
newHeader.classList.add("text-center");
headers.after( newHeader );
for (let i = 0; i < characterInitiativeList.length; i++) {
var char = characterInitiativeList[i];
var newCol = document.createElement('td');
newCol.innerText = char.hp;
newCol.classList.add("text-center");
var allTrsWithCharacterId = document.querySelector(`tr[data-round-character-id="${char.id}"]`);
var indexd = allTrsWithCharacterId.getElementsByClassName('char-name');
indexd[0].after( newCol );
}
}

View File

@ -0,0 +1,105 @@
<?php
/**
* app/plugins/feedback/models/feedback.php
*
* This class is used for the manipulation of the feedback database table.
*
* @todo make this send a confirmation email
*
* @package TP Feedback
* @version 3.0
* @author Joey Kimsey <Joey@thetempusproject.com>
* @link https://TheTempusProject.com
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
*/
namespace TheTempusProject\Models;
use TheTempusProject\Bedrock\Classes\Config;
use TheTempusProject\Bedrock\Functions\Check;
use TheTempusProject\Canary\Canary as Debug;
use TheTempusProject\Classes\DatabaseModel;
use TheTempusProject\Plugins\Feedback as Plugin;
use TheTempusProject\TheTempusProject as App;
class Inithistory extends DatabaseModel {
public $tableName = 'feedback';
public $databaseMatrix = [
[ 'name', 'varchar', '128' ],
[ 'time', 'int', '10' ],
[ 'email', 'varchar', '128' ],
[ 'ip', 'varchar', '64' ],
[ 'feedback', 'text', '' ],
];
public $plugin;
/**
* The model constructor.
*/
public function __construct() {
parent::__construct();
$this->plugin = new Plugin;
}
/**
* Saves a feedback form to the db.
*
* @param string $name -the name on the form
* @param string $email -the email provided
* @param string $feedback -contents of the feedback form.
* @return bool
*/
public function create( $name, $email, $feedback ) {
if ( !$this->plugin->checkEnabled() ) {
Debug::info( 'Feedback is disabled in the config.' );
return false;
}
$fields = [
'name' => $name,
'email' => $email,
'feedback' => $feedback,
'time' => time(),
'ip' => $_SERVER['REMOTE_ADDR'],
];
if ( !self::$db->insert( $this->tableName, $fields ) ) {
Debug::info( 'Feedback::create - failed to insert to db' );
return false;
}
return self::$db->lastId();
}
/**
* Function to clear feedback from the DB.
*
* @todo is there a way i could check for success here I'm pretty sure this is just a bad idea?
* @return bool
*/
public function clear() {
if ( empty( self::$log ) ) {
self::$log = new Log;
}
self::$db->delete( $this->tableName, ['ID', '>=', '0'] );
self::$log->admin( 'Cleared Feedback' );
Debug::info( 'Feedback Cleared' );
return true;
}
public function userHistory( $limit = 0 ) {
$whereClause = [
'userID', '=', App::$activeUser->ID,
'AND',
'deletedAt', '=', '0',
'AND',
'expiresAt', '<', time(),
];
if ( empty( $limit ) ) {
$notifications = self::$db->get( $this->tableName, $whereClause );
} else {
$notifications = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] );
}
if ( !$notifications->count() ) {
Debug::info( 'No Notifications found.' );
return false;
}
return $this->filter( $notifications->results() );
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* app/plugins/initiativetracker/plugin.php
*
* This houses all of the main plugin info and functionality.
*
* @package TP InitiativeTracker
* @version 3.0
* @author Joey Kimsey <Joey@thetempusproject.com>
* @link https://TheTempusProject.com
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
*/
namespace TheTempusProject\Plugins;
use TheTempusProject\Classes\Plugin;
class Initiativetracker extends Plugin {
public $pluginName = 'TP InitiativeTracker';
public $pluginAuthor = 'JoeyK';
public $pluginWebsite = 'https://TheTempusProject.com';
public $modelVersion = '1.0';
public $pluginVersion = '3.0';
public $pluginDescription = 'A simple plugin which adds a simple initiative tracker.';
public $configName = 'initiativeTracker';
public $configMatrix = [
'enabled' => [
'type' => 'radio',
'pretty' => 'Enable tracker usage.',
'default' => true,
],
];
public $permissionMatrix = [
'useInitiativeTracker' => [
'pretty' => 'Can use the tracker',
'default' => true,
],
];
public $main_links = [
[
'text' => 'Initiative',
'url' => '{ROOT_URL}initiative/index',
],
];
}
// need to add a way to save the initiative history ( both personal, and for the table )
// need a way to save characters for the autofill
// need to add a "clear" button for initiative roller
// if you have tables, and are a DM, your group can access the initiative history

View File

@ -0,0 +1,210 @@
<link rel="stylesheet" href="{ROOT_URL}app/plugins/initiativetracker/css/initiative.css" crossorigin="anonymous">
<script src="{ROOT_URL}app/plugins/initiativetracker/js/initiative.js" type="module" defer></script>
<div id="initiative-container" class="initiative-container">
<div class="row">
<h2>Initiative Tracker</h2>
</div>
<!-- Tracker Settings -->
<div class="row well well-sm">
<div class="checkbox">
<label><input type="checkbox" name="trackHP" id="trackHP"> Track HP</label>
</div>
<div class="checkbox">
<label><input type="checkbox" name="trackAC" id="trackAC"> Track AC</label>
</div>
<div class="checkbox">
<label><input type="checkbox" name="trackRounds" id="trackRounds"> Track Rounds</label>
</div>
</div>
<!-- Character Form -->
<div id="character-form" class="character-form row well well-sm">
<div class="form-group col-lg-3">
<label for="name">Name</label>
<input type="text" class="form-control" name="name" id="name" autocomplete="on">
</div>
<div class="form-group col-lg-2">
<label for="initiative">Initiative</label>
<div class="input-group">
<div class="input-group-btn">
<button id="d20-roll" class="btn btn-default" aria-label="Roll a d20 for initiative.">Roll</button>
</div>
<input type="number" class="form-control" name="initiative" id="initiative">
</div>
</div>
<div class="form-group col-lg-1 ac-tracker" style="display: none;">
<label for="ac">AC</label>
<input type="number" class="form-control" name="ac" id="ac">
</div>
<div class="form-group col-lg-1 hp-tracker" style="display: none;">
<label for="hp">HP</label>
<input type="number" class="form-control" name="hp" id="hp">
</div>
<div class="form-group col-lg-2">
<label for="pc">Type</label><br>
<label class="radio-inline">
<input type="radio" name="characterType" id="pc" value="pc" checked> PC
</label>
<label class="radio-inline">
<input type="radio" name="characterType" id="npc" value="npc"> NPC
</label>
</div>
<div class="form-group col-lg-3">
<button id="character-add" class="btn btn-primary">
Add
</button>
<button id="character-reset" class="btn btn-danger">
Reset
</button>
</div>
</div>
<!-- Initiative List -->
<div id="character-container" style="display: none;">
<div id="initiative-list" class="well well-sm">
<!-- Tab Header -->
<ul class="nav nav-tabs" role="tablist">
<li class="active">
<a data-target="#cmb-list" role="tab" data-toggle="tab">
Combined
</a>
</li>
<li>
<a data-target="#pc-list" role="tab" data-toggle="tab">
PC
</a>
</li>
<li>
<a data-target="#npc-list" role="tab" data-toggle="tab">
NPC
</a>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<!-- Combined List -->
<div role="tabpanel" class="tab-pane active" id="cmb-list">
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th class="text-center">Initiative</th>
<th class="ac-tracker text-center" style="display: none;">AC</th>
<th class="hp-tracker text-center" style="display: none;">HP</th>
<th class="text-center">Type</th>
<th class="text-right"></th>
</tr>
</thead>
<tbody id="character-list">
</tbody>
</table>
</div>
<!-- PC List -->
<div role="tabpanel" class="tab-pane" id="pc-list">
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th class="text-center">Initiative</th>
<th class="ac-tracker text-center" style="display: none;">AC</th>
<th class="hp-tracker text-center" style="display: none;">HP</th>
<th class="text-center">Type</th>
<th class="text-right"></th>
</tr>
</thead>
<tbody id="character-list-pc">
</tbody>
</table>
</div>
<!-- NPC List -->
<div role="tabpanel" class="tab-pane" id="npc-list">
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th class="text-center">Initiative</th>
<th class="ac-tracker text-center" style="display: none;">AC</th>
<th class="hp-tracker text-center" style="display: none;">HP</th>
<th class="text-center">Type</th>
<th class="text-right"></th>
</tr>
</thead>
<tbody id="character-list-npc">
</tbody>
</table>
</div>
</div>
<!-- List Controls -->
<div class="row list-controls row">
<div class="form-group col-lg-3">
<button id="list-reset" class="d4-die die-btn btn btn-warning">
Reset Round
</button>
</div>
<div class="form-group col-lg-3">
<div class="input-group">
<div class="input-group-btn">
<button id="character-sort" class="btn btn-primary" aria-label="Sort the List.">Sort</button>
</div>
<select class="form-control" id="sortBy">
<option value="initiative">Initiative</option>
<option value="name">Name</option>
<option value="ac">AC</option>
<option value="hp">HP</option>
<option value="type">Type</option>
</select>
</div>
</div>
<div class="form-group col-lg-3">
<button id="list-next" class="btn btn-success">
Next Character
</button>
</div>
<div class="form-group col-lg-3 list-controls-last">
<button id="list-clear" class="btn btn-danger">
Clear List
</button>
</div>
</div>
</div>
<!-- Round Controls -->
<div class="row rounds-tracker" style="display: none;">
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title">Round History</h3>
</div>
<div class="panel-body">
<table class="table table-striped">
<thead>
<tr id="rounds-history-header-row">
<th id="rounds-history-header">Character</th>
</tr>
</thead>
<tbody id="rounds-history">
</tbody>
</table>
<div class="form-group col-lg-2">
<button id="rounds-clear" class="d4-die die-btn btn btn-danger">
Clear History
</button>
</div>
</div>
<div class="panel-footer round-footer">
<b>Round Count</b>: <span id="round-count"></span>
<span class="pull-right round-reset">
<button id="rounds-reset" class="btn btn-warning">
Reset Round Count
</button>
</span>
</div>
</div>
</div>
</div>
</div>