sr2ini/js/sr2ini.js
Tobias 7d5455fdfd - added damage monitor functionality to track stun/phys damage levels and automatically apply ini penalties accordingly
- mostly working, but there's now way yet to enter/edit damage levels in the add/edit modal
- changed some of the icons
2023-02-07 11:29:29 +01:00

464 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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