- mostly working, but there's now way yet to enter/edit damage levels in the add/edit modal - changed some of the icons
464 lines
15 KiB
JavaScript
464 lines
15 KiB
JavaScript
/*
|
||
* helper functions
|
||
*/
|
||
|
||
const penalty = [0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4];
|
||
|
||
const damageMonitorHTML = ['<table class="damage-monitor text-center">\n',
|
||
'<thead>\n',
|
||
'<tr><th title="Stun damage"><img src="img/zzz.png" /></th><th title="Physical Damage"><img src="img/skull.png" /></th></tr>\n',
|
||
'</thead>\n',
|
||
'<tbody>\n',
|
||
'<tr><td class="damage-stun selected" title="0">0</td><td class="damage-physical selected" title="0">0</td></tr>\n',
|
||
'<tr><td class="damage-stun" title="-1">L</td><td class="damage-physical" title="-1">L</td></tr>\n',
|
||
'<tr><td class="damage-stun" title="-1"></td><td class="damage-physical" title="-1"></td></tr>\n',
|
||
'<tr><td class="damage-stun" title="-2">M</td><td class="damage-physical" title="-2">M</td></tr>\n',
|
||
'<tr><td class="damage-stun" title="-2"></td><td class="damage-physical" title="-2"></td></tr>\n',
|
||
'<tr><td class="damage-stun" title="-2"></td><td class="damage-physical" title="-2"></td></tr>\n',
|
||
'<tr><td class="damage-stun" title="-3">S</td><td class="damage-physical" title="-3">S</td></tr>\n',
|
||
'<tr><td class="damage-stun" title="-3"></td><td class="damage-physical" title="-3"></td></tr>\n',
|
||
'<tr><td class="damage-stun" title="-3"></td><td class="damage-physical" title="-3"></td></tr>\n',
|
||
'<tr><td class="damage-stun" title="-3"></td><td class="damage-physical" title="-3"></td></tr>\n',
|
||
'<tr><td class="damage-stun" title="knocked out">D</td><td class="damage-physical" title="dead">D</td></tr>\n',
|
||
'</tbody>\n',
|
||
'</table>'].join("");
|
||
|
||
|
||
// roll for initiative with the given reaction and number of ini dice
|
||
function rollForInitiative(dice, rea) {
|
||
let ini = 0;
|
||
for ( let i = 0; i < parseInt(dice); i++ ) {
|
||
roll = Math.floor(Math.random() * 6) + 1;
|
||
ini += roll;
|
||
}
|
||
return ini + parseInt(rea);
|
||
}
|
||
|
||
|
||
// figure out whose action comes first out of two combatants a and b
|
||
function whoGoesFirst(a, b) {
|
||
let comparer = parseInt($(b).find(".combatantIni").text()) - parseInt($(a).find(".combatantIni").text());
|
||
if (comparer != 0) {
|
||
return comparer;
|
||
} else {
|
||
let reaA = parseInt($(a).find(".combatantRea").text());
|
||
let reaB = parseInt($(b).find(".combatantRea").text());
|
||
reaA = isNaN(reaA) ? 0 : reaA;
|
||
if (isNaN(reaB)) { reaB = 0; }
|
||
console.log(reaA, reaB);
|
||
return reaB - reaA;
|
||
}
|
||
}
|
||
|
||
|
||
// sorts the combatants by ini value
|
||
function sortTable() {
|
||
// sort rows and append them in new order
|
||
let $rows = $(".combatantRow").toArray().sort(whoGoesFirst);
|
||
for ( var i = 0; i < $rows.length; i++ ) {
|
||
$("#combatantsTable").append($rows[i]);
|
||
}
|
||
|
||
// add contectual classes to rows – currently for highest ini and ini = 0
|
||
// compute highest ini
|
||
let iniValues = $.map( $(".combatantIni"), function(td, i) {
|
||
return parseInt($(td).text());
|
||
});
|
||
let iniMax = Math.max.apply(null, iniValues);
|
||
|
||
// add contextual classes to rows
|
||
$(".combatantRow").each( function() {
|
||
// always remove previous classes, disable act button
|
||
$(this).removeClass("table-primary table-secondary").find(".act-button").prop("disabled", true).attr("aria-disabled", "true");
|
||
// add class if ini is zero
|
||
if ( parseInt($(this).find(".combatantIni").text()) == 0 ) {
|
||
$(this).addClass("table-secondary");
|
||
}
|
||
// add class, enable act button if ini is max and non-zero
|
||
else if ( parseInt($(this).find(".combatantIni").text()) == iniMax && iniMax > 0 ) {
|
||
$(this).addClass("table-primary").find(".act-button").prop("disabled", false).removeAttr("aria-disabled");
|
||
}
|
||
})
|
||
|
||
return;
|
||
}
|
||
|
||
|
||
// returns a combatant's effective ini value (modified by wound penalties)
|
||
function getEffectiveIni(value, dmgLvl1, dmgLvl2) {
|
||
let effectiveIni;
|
||
|
||
// was function called with 1 argument (tr jQuery object)?
|
||
if ( arguments.length == 1 && $(value).is("tr.combatantRow") ) {
|
||
let trueIni = parseInt($(value).attr("data-true-ini"));
|
||
let dmgStun = parseInt($(value).attr("data-damage-stun")) || 0;
|
||
let dmgPhysical = parseInt($(value).attr("data-damage-physical")) || 0;
|
||
effectiveIni = trueIni - penalty[dmgStun] - penalty[dmgPhysical];
|
||
}
|
||
// or with 3 arguments (ini and dmg levels)?
|
||
else if ( arguments.length == 3 ) {
|
||
effectiveIni = parseInt(value) - penalty[parseInt(dmgLvl1)] - penalty[parseInt(dmgLvl2)];
|
||
}
|
||
//
|
||
else { return NaN; }
|
||
console.log("effectiveIni is ", effectiveIni);
|
||
return effectiveIni < 0 ? 0 : effectiveIni;
|
||
}
|
||
|
||
|
||
/*
|
||
* Event handler functions
|
||
*/
|
||
|
||
// click handler for act buttons
|
||
function handleActButtonClick (e) {
|
||
// find current table row
|
||
let $tr = $(e.target).parents(".combatantRow");
|
||
let ini = $tr.attr("data-true-ini");
|
||
|
||
// reduce ini by 10 but not lower than 0
|
||
ini = Math.max(parseInt(ini) - 10, 0);
|
||
|
||
// set new ini value
|
||
$tr.attr("data-true-ini", ini);
|
||
$tr.find(".combatantIni").text(getEffectiveIni($tr));
|
||
|
||
// resort table
|
||
sortTable();
|
||
}
|
||
|
||
|
||
// click handler for add buttons
|
||
function handleAddButtonClick (e) {
|
||
|
||
// restyle modal
|
||
$("#combatantModal .modal-title").text("Add Combatant");
|
||
$("#combatantModalAddOkButton").show();
|
||
$("#combatantModalEditOkButton").hide();
|
||
|
||
// add handler for enter key
|
||
$("#combatantModal input[id*='combatantModal']").off("keydown");
|
||
$("#combatantModal input[id*='combatantModal']").on("keydown", function (e) {
|
||
if ( e.which == 13 || e.which == 10 ) {
|
||
addCombatant(e);
|
||
}
|
||
});
|
||
|
||
// show modal
|
||
$("#combatantModal").modal("show");
|
||
}
|
||
|
||
// click handler for damage buttons
|
||
function handleDamageButtonClick (e) {
|
||
let display = $(e.target).parents(".damage-dropdown").find(".damage-monitor").css("display");
|
||
$(e.target).parents(".damage-dropdown").find(".damage-monitor").css("display", display == "block" ? "none" : "block");
|
||
return false;
|
||
}
|
||
|
||
|
||
// click handler for damage monitor fields
|
||
function handleDamageMonitorClick (e) {
|
||
let $td = $(e.target);
|
||
let $tr = $td.parents("tr.combatantRow");
|
||
let damageType;
|
||
let otherDamageLevel
|
||
|
||
// calculate new damage level and type
|
||
let damageLevel = $td.parent().index();
|
||
if ( $td.hasClass("damage-stun") ) {
|
||
damageType = "stun";
|
||
otherDamageLevel = $tr.attr("data-damage-physical") ? parseInt($tr.attr("data-damage-physical")) : 0;
|
||
} else if ( $td.hasClass("damage-physical") ) {
|
||
damageType = "physical";
|
||
otherDamageLevel = $tr.attr("data-damage-stun") ? parseInt($tr.attr("data-damage-stun")) : 0;
|
||
} else {
|
||
return false;
|
||
}
|
||
|
||
// add damage level to table row as as data attribute
|
||
$tr.attr("data-damage-" + damageType, damageLevel);
|
||
|
||
// select/unselect damage boxes
|
||
$td.addClass("selected");
|
||
$td.parent().nextAll().children("td.damage-" + damageType).removeClass("selected");
|
||
$td.parent().prevAll().children("td.damage-" + damageType).addClass("selected");
|
||
|
||
// recalculate effective ini and resort
|
||
$tr.find(".combatantIni").text(getEffectiveIni($tr));
|
||
sortTable();
|
||
|
||
|
||
return false;
|
||
}
|
||
|
||
// click handler for edit buttons
|
||
function handleEditButtonClick (e) {
|
||
// find current table row
|
||
let $tr = $(e.target).parents(".combatantRow");
|
||
|
||
// restyle modal
|
||
$("#combatantModal .modal-title").text("Edit Combatant");
|
||
$("#combatantModalAddOkButton").hide();
|
||
$("#combatantModalEditOkButton").show();
|
||
|
||
// populate modal with values from row
|
||
$("#combatantModalName").val($tr.find(".combatantName").text());
|
||
$("#combatantModalDice").val($tr.find(".combatantDice").text());
|
||
$("#combatantModalRea").val($tr.find(".combatantRea").text());
|
||
$("#combatantModalIni").val($tr.attr("data-true-ini"));
|
||
//TODO: show effective ini in modal
|
||
|
||
// mark which row is being edited
|
||
$("#combatantModal").attr("data-row", $(".combatantRow").index($tr));
|
||
|
||
// add handler for enter key
|
||
$("#combatantModal input[id*='combatantModal']").off("keydown");
|
||
$("#combatantModal input[id*='combatantModal']").on("keydown", function (e) {
|
||
if ( e.which == 13 || e.which == 10 ) {
|
||
editCombatant(e);
|
||
}
|
||
});
|
||
|
||
// show modal
|
||
$("#combatantModal").modal("show");
|
||
}
|
||
|
||
|
||
// click handler for remove buttons
|
||
function handleRemoveButtonClick (e) {
|
||
// remove table row
|
||
$(e.target).parents(".combatantRow").remove();
|
||
}
|
||
|
||
|
||
/*
|
||
* Validation functions
|
||
*/
|
||
|
||
// validate a combatant row form by checking for all conditions, including regular HTML5 validation
|
||
function validateCombatant() {
|
||
|
||
// get input elements
|
||
let inputElements = {
|
||
name: $("#combatantModalName").get(0),
|
||
ini: $("#combatantModalIni").get(0),
|
||
dice: $("#combatantModalDice").get(0),
|
||
rea: $("#combatantModalRea").get(0)
|
||
};
|
||
|
||
// do standard HTML5 form validation first
|
||
// (makes sure that name is not empty and that all other values are numbers within their individual ranges)
|
||
let valid = true;
|
||
Object.values(inputElements).forEach(function(input) {
|
||
if ( ! input.reportValidity() ) {
|
||
valid = false;
|
||
}
|
||
})
|
||
if ( ! valid ) {
|
||
return false;
|
||
}
|
||
|
||
// now for some custom validation; first we need to get the input values
|
||
let ini = inputElements["ini"].value.trim();
|
||
let dice = inputElements["dice"].value.trim();
|
||
let rea = inputElements["rea"].value.trim();
|
||
|
||
// invalidate if ini, dice and rea are all empty
|
||
if ( ini == "" && ( dice == "" || rea == "" ) ) {
|
||
inputElements["ini"].setCustomValidity("Requiring values for ini dice and reaction, or initiative, or all three");
|
||
inputElements["ini"].reportValidity();
|
||
inputElements["ini"].setCustomValidity("");
|
||
return false;
|
||
}
|
||
|
||
// invalidate if dice or rea is empty but not both
|
||
if ( ( dice == "" ) != ( rea == "" ) ) {
|
||
inputElements["dice"].setCustomValidity("Values required for both dice and reaction, or none (in which case ini is required)");
|
||
inputElements["dice"].reportValidity();
|
||
inputElements["dice"].setCustomValidity("");
|
||
return false;
|
||
}
|
||
|
||
// ok then
|
||
return true;
|
||
}
|
||
|
||
|
||
/*
|
||
* Main functions
|
||
*/
|
||
|
||
// add new combatant
|
||
function addCombatant (e) {
|
||
|
||
e.preventDefault();
|
||
|
||
// validate form
|
||
if ( ! validateCombatant() ) {
|
||
return false;
|
||
}
|
||
|
||
// hide modal
|
||
$("#combatantModal").modal("hide");
|
||
|
||
// get values
|
||
let name = $("#combatantModalName").val().trim();
|
||
let ini = $("#combatantModalIni").val().trim();
|
||
let dice = $("#combatantModalDice").val().trim();
|
||
let rea = $("#combatantModalRea").val().trim();
|
||
//TODO: retrieve initial damage levels
|
||
|
||
// roll for initiative if necessary
|
||
ini = (ini != "") ? ini : rollForInitiative(dice, rea);
|
||
|
||
// TODO: actually calculate effective ini
|
||
let effectiveIni = getEffectiveIni(ini, 0, 0);
|
||
console.log("effective ini = ", effectiveIni);
|
||
|
||
// construct jQuery object for table row
|
||
let $tr = $($.parseHTML( [
|
||
'<tr class="combatantRow align-middle" data-true-ini="', ini, '">\n', //TODO: add data-damage-* attributes with initial damage levels
|
||
'<td class="combatantName" title="Combatant\'s name">', name, '</td>\n',
|
||
'<td class="combatantIni text-center" title="Initiative">', effectiveIni, '</td>\n',
|
||
'<td class="text-center combatantDiceAndRea" title="Iniative dice and reaction"><span class="combatantDice">', dice, '</span>D+<span class="combatantRea">', rea, '</span></td>\n',
|
||
'<td class="text-end">\n',
|
||
'<div class="btn-group">\n',
|
||
'<button type="button" class="btn btn-light btn-rounded mx-1 p-1 edit-button" title="Edit combatant\'s values"><img src="img/edit.png" /></button>\n',
|
||
'<button type="button" class="btn btn-light btn-rounded mx-1 p-1 act-button" title="Act and reduce ini by 10"><img src="img/check.png" /></button>\n',
|
||
'<div class="damage-dropdown">\n',
|
||
'<button type="button" class="btn btn-light btn-rounded mx-1 p-1 damage-button" title="Take damage"><img src="img/explosion.png" /></button>\n',
|
||
damageMonitorHTML + "\n",
|
||
'</div>\n',
|
||
'</div>\n',
|
||
'</td>\n',
|
||
'</tr>'].join("")
|
||
));
|
||
|
||
//TODO: mark initial damage levels with .selected class
|
||
|
||
// add handlers to table row buttons
|
||
$tr.find("button.edit-button").on("click", handleEditButtonClick);
|
||
$tr.find("button.act-button").on("click", handleActButtonClick);
|
||
$tr.find("button.remove-button").on("click", handleRemoveButtonClick);
|
||
$tr.find("button.damage-button").on("click", handleDamageButtonClick);
|
||
|
||
// add handler to table cells (click to edit)
|
||
$tr.find(".combatantName, .combatantIni, .combatantDiceAndRea").on("click", handleEditButtonClick);
|
||
|
||
// add handler to damage monitor
|
||
$tr.find(".damage-stun, .damage-physical").on("click", handleDamageMonitorClick);
|
||
|
||
// add row to table and sort
|
||
$("#combatantsTable").append($tr);
|
||
sortTable();
|
||
}
|
||
|
||
|
||
// edit combatant values
|
||
function editCombatant (e) {
|
||
e.preventDefault();
|
||
|
||
// validate form
|
||
if ( ! validateCombatant() ) {
|
||
return false;
|
||
}
|
||
|
||
// hide modal
|
||
$("#combatantModal").modal("hide");
|
||
|
||
// get values
|
||
let name = $("#combatantModalName").val().trim();
|
||
let ini = $("#combatantModalIni").val().trim();
|
||
let dice = $("#combatantModalDice").val().trim();
|
||
let rea = $("#combatantModalRea").val().trim();
|
||
|
||
// roll for initiative if ini is empty
|
||
ini = (ini != "") ? ini : rollForInitiative(dice, rea);
|
||
|
||
// get correct row
|
||
let index = parseInt($("#combatantModal").attr("data-row"));
|
||
$tr = $("tr.combatantRow").eq(index);
|
||
|
||
// set new values
|
||
$tr.find(".combatantName").text(name);
|
||
$tr.find(".combatantDice").text(dice);
|
||
$tr.find(".combatantRea").text(rea);
|
||
$tr.attr("data-true-ini", ini);
|
||
$tr.find(".combatantIni").text(getEffectiveIni($tr));
|
||
|
||
// sort table
|
||
sortTable();
|
||
|
||
// clean up
|
||
$("#combatantModal").removeAttr("data-row");
|
||
}
|
||
|
||
|
||
// start a new combat round
|
||
function newRound() {
|
||
// are there rows at all?
|
||
if ( $(".combatantRow").length == 0 ) {
|
||
return;
|
||
}
|
||
|
||
// reset ini values
|
||
$(".combatantRow").each( function() {
|
||
let effectiveIni = $(this).find(".combatantIni").text();
|
||
let dice = $(this).find(".combatantDice").text();
|
||
if ( dice == "" ) {
|
||
$(this).attr("data-true-ini", "1");
|
||
} else {
|
||
$(this).attr("data-true-ini", rollForInitiative(dice, $(this).find(".combatantRea").text()));
|
||
}
|
||
$(this).find(".combatantIni").text(getEffectiveIni($(this)));
|
||
});
|
||
|
||
// resort table
|
||
sortTable();
|
||
}
|
||
|
||
|
||
// add test combatant for testing purposes (duh)
|
||
function addTestCombatant() {
|
||
$("#addCombatantButton").click();
|
||
$("#combatantModalName").val("Goon1");
|
||
$("#combatantModalDice").val(2);
|
||
$("#combatantModalRea").val(6);
|
||
$("#combatantModalIni").val(12);
|
||
setTimeout(function(){
|
||
$("#combatantModalAddOkButton").click();
|
||
},500);
|
||
}
|
||
|
||
|
||
/*
|
||
* Initialize document
|
||
*/
|
||
|
||
$(document).ready(function(){
|
||
// add event handlers to navbar buttons
|
||
$("#addCombatantButton").on("click", handleAddButtonClick);
|
||
$("#newroundModalOkButton").on("click", newRound);
|
||
|
||
// add event handlers to modal buttons
|
||
$("#combatantModalAddOkButton").on("click", addCombatant);
|
||
$("#combatantModalEditOkButton").on("click", editCombatant);
|
||
|
||
// always focus name input field when combatant modal appears
|
||
$('#combatantModal').on('shown.bs.modal', function() {
|
||
$('#combatantModalName').focus();
|
||
});
|
||
// always empty input fields when combatant modal disappears
|
||
$("#combatantModal").on('hidden.bs.modal', function (e) {
|
||
$("#combatantModal input[id*='combatantModal']").val("");
|
||
});
|
||
|
||
// Hide damage monitors if mouse is clicked outside
|
||
$("html").on("click", function(e) {
|
||
$(".damage-monitor:visible").css("display", "none");
|
||
});
|
||
|
||
addTestCombatant();
|
||
|
||
});
|