Compare commits

...

17 Commits

Author SHA1 Message Date
22a53eca62 refactored much of the code to call functions with parameters whenever possible (instead of the functions pulling their input data from global vars or the DOM); added an index to each row; #combatant-form now encapsulates all input elements 2023-10-09 23:35:48 +02:00
2d64915cb0 cleaned up code a little: replaced tabs with spaces, clarified some code 2023-10-07 10:32:17 +02:00
26955b841d updated and enhanced 2023-10-03 22:44:33 +02:00
9073106f61 Fixed two bugs regarding focussing elements 2023-10-03 22:43:04 +02:00
3aa06b6d7f fixed a bug with dead icon links in site.webmanifest, and slightly expanded manifest 2023-09-23 23:23:02 +02:00
aa3a6cf155 removed 2023-09-23 22:43:11 +02:00
4b57ebb5ea updated 2023-09-23 22:41:19 +02:00
465569c109 changed links to icons from absolute to relative 2023-09-23 22:40:42 +02:00
e70f25b8ef added expicit value for --dist-dir 2023-09-23 22:40:03 +02:00
675a6b7c07 removed 2023-09-23 22:38:21 +02:00
0bb40881a3 renamed from sr2ini.iconmanifest b/c parcel wasn't parsing file 2023-09-23 22:37:46 +02:00
dff4b1620b clean now also removes .parcel-cache/; upped version number; correcte author name 2023-09-21 09:12:53 +02:00
d89227e7cd updated 2023-09-21 09:10:59 +02:00
1e496b5807 added description 2023-09-21 08:55:56 +02:00
00740bcf3d corrected author name in footer 2023-09-20 22:35:33 +02:00
17ad920477 updated 2023-09-20 22:17:38 +02:00
665d25ca44 fixed a bug where a more actions dropdown would not center under its parent divs anymore; also the cursor now changes to pointer when hovering over a table cell which opens the edit modal 2023-09-20 22:13:39 +02:00
11 changed files with 1282 additions and 1244 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ hint-report/
.csslintrc .csslintrc
coverage/ coverage/
tmp/ tmp/
*.geany

View File

@ -1,3 +1,3 @@
# sr2ini # sr2ini a simple initiative tracker for Shadowrun 2e
Simple Initiative tracker for Shadowrun 2e. sr2ini is a lightweight, single-page initiative tracker for the TTRPG Shadowrun in its 2nd edition. The app helps DMs and players to manage fights by tracking each combatant's initiative, order of action, damage (stun and physical), and wound modifiers. It was written specifically for mobile use and can be installed as a web app.

80
TODO.md
View File

@ -109,7 +109,6 @@
- x custom validation not triggering error message nor visible styles - x custom validation not triggering error message nor visible styles
- x außerdem bleiben nichtvalid. Werte stehen, wenn man das modal mit ESC schließt und dann mit edit wieder öffnet - x außerdem bleiben nichtvalid. Werte stehen, wenn man das modal mit ESC schließt und dann mit edit wieder öffnet
- x warum sind im dist/-Folder immer zwei Versionen der gleichen Datei? -> lag an parcel-reporter-static-file-copy - x warum sind im dist/-Folder immer zwei Versionen der gleichen Datei? -> lag an parcel-reporter-static-file-copy
- x revamp Act-icon - x revamp Act-icon
- alternative to "-10"? - alternative to "-10"?
- at least recreate Electrolize characters in SVG (with fontforge) - at least recreate Electrolize characters in SVG (with fontforge)
@ -119,33 +118,74 @@
- x color scheme beim Favicon anpassen - x color scheme beim Favicon anpassen
- https://realfavicongenerator.net/ - https://realfavicongenerator.net/
- see also here: https://github.com/audreyfeldroy/favicon-cheat-sheet - see also here: https://github.com/audreyfeldroy/favicon-cheat-sheet
- x Mauszeiger soll Finger werden, wenn er über combatant-name/ini/dice-and-rea hovert
- x actions-menu erscheint nicht direkt unter dem Button
- beide dropdowns sind gerade garbled
- x im FP3T Tor Browser kann ich rauszoomen, bis ich die ganzen damage monitors und action menus sehe -> verhindern!
- action-menu und damage-monitor sliden jetzt nicht mehr rein, sondern bleiben an Ort und Stelle
- das sollte die Sache verhindern
- x warum fkt. das Ganze nicht als Webapp?
- Firefox (android) sieht die Seite nicht als installable an
- Webmanifest ist aber da und scheint auch in Ordnung zu sein (sagt Firefox on Linux)
- x favicon checken: https://realfavicongenerator.net/favicon_checker
- jetzt fkt. immer noch nicht die URLs im Icon manifest
- x Links zu den versch. favicons fkt. nicht: sie lauten /… statt /sr2ini/…
- x Lösung: parcel build braucht als --public-url "./" (statt "/"), dann werden die Links korrekt erzeugt
- neues Problem: für parcel serve fkt. das nicht; statt CSS/JS/favicon-Files wird jedes Mal index.html serviert
- edit: jetzt geht's wieder (nachdem ich sr2ini.iconmanifest zurückbenannt hatte nach site.webmanifest)
- bekanntes Problem: https://github.com/parcel-bundler/parcel/issues/857
- Workaround: weiter --public-url / verwenden
- WICHTIG: fürs Deployment muss ich dann immer die mit parcel build erzeugten Dateien verwenden
- was auch geht, ist aber umständlich: parcel serve durch anderen HTTP-Server ersetzen
- parcel build
- im Dist-Verz.: python3 -m http.server
- CORS-Fehler im Firefox vermeiden: about:config -> content.cors.disable = true
- letztes Problem (hoffentlich): die Links zu den android-chrome-XYZxXYZ Icons im site.webmanifest stimmen nicht
- hab site.manifest nach src/ verschoben und die Links angepasst -> jetzt scheint's zu gehen
- x focus-related stuff
- x enter key doesn't work right away after clicking add button
- x wenn ein damage monitor offen ist und ich auf add combatant clicke, springt der Fokus nicht zuverlässig ins erste input feld
- x after pressing damage level button, focus moves to first table row act button
- it's probably b/c I resort the table
- x Geheimfunktion, um Eclipse, Solitaire, Pi und Q zu adden: hold navbar title
- x refactor (in neuem Branch):
- jede combatant tablerow kriegt eine unique #id
- Funktionen holen sich ihre Infos Infos nicht mehr aus dem DOM, sondern kriegen sie als Parameter übergeben
- tablerow id
- table
- modal
- …
### open ### open
- favicon checken: https://realfavicongenerator.net/favicon_checker - focus trapping im modal fkt. nur rückwärts (shift-tab), aber nicht vorwärts
- Mauszeiger soll Finger werden, wenn er über combatant-name/ini/dice-and-rea hovert - wenn ich in einem modal mit Tab durchgehe und zu den Buttons ganz unten komme, bewegt sich der untere Rand des Modals ab und auf
- actions-menu erscheint nicht direkt unter dem Button
- Deployment: use minified libraries (aug-ui, bs, jq)
- focus-related stuff
- after pressing damage button, focus moves to first table row act button
- it's probably b/c I resort the table
- wenn ein damage monitor offen ist und ich auf add combatant clicke, springt der Fokus nicht zuverlässig ins erste input feld
- im FP3T Tor Browser kann ich rauszoomen, bis ich die ganzen damage monitors und action menus sehe -> verhindern!
- action-menu und damage-monitor sliden jetzt nicht mehr rein, sondern bleiben an Ort und Stelle
- das sollte die Sache verhindern
- warum fkt. das Ganze nicht als Webapp?
- Firefox (android) sieht die Seite nicht als installable an
- Webmanifest ist aber da und scheint auch in Ordnung zu sein (sagt Firefox on Linux)
- comments with general info in source files? - comments with general info in source files?
- nice to have: focus trapping im modal fkt. nur rückwärts (shift-tab), aber nicht vorwärts - unter die GPL stellen
- zumindest im Chrome; FF ungetestet - https://www.gnu.org/licenses/gpl-howto.html
- sr2ini_ynh auf neue Version bringen
- manifest: Version, tarball URL und sha256 ersetzen
- README.md: Version ersetzen
- ggf. doc/screenshots/sr2ini-screenshot.jpg ersetzen (960x640)
- Deployment:
- CI/CD: es gibt Jenkins für Yunohost
- hier ist ein Tutorial, um Jenkins und Gitea miteinander bekannt zu machen: https://mike42.me/blog/2019-05-how-to-integrate-gitea-and-jenkins
- Jenkins Doc: https://www.jenkins.io/doc/book/using/best-practices/
- ist aber wohl mit Kanonen auf Spatzen
- use minified libraries (aug-ui, bs, jq)
## Feature Requests ## Feature Requests
- Seite als Web App auf FF4And installable machen - author name soll durchgehend der gleiche sein
- mal sehen … - aber welcher? tobias oder eclipse?
- index.html: meta, footer
- sr2ini.js: test combatant
- git: user.name, user.email
- git commits, ggf. tags
- Animationen? Transitions? - Animationen? Transitions?
- deployment: dist/* soll direkt auf hermes hochgeladen werden - deployment: dist/* soll direkt auf hermes hochgeladen werden
- HTML soll nicht in eine Zeile umgedingst werden, das sieht doch nicht aus - HTML soll nicht in eine Zeile umgedingst werden, das sieht doch nicht aus
@ -156,7 +196,6 @@
- falls ja: .htmlnanorc anlegen, s. https://parceljs.org/languages/html/#minification und https://htmlnano.netlify.app/modules#collapsewhitespace - falls ja: .htmlnanorc anlegen, s. https://parceljs.org/languages/html/#minification und https://htmlnano.netlify.app/modules#collapsewhitespace
- nicetohave: Wenn ich rea editiere, könnte sich die ini automatisch anpassen -> da müsste ich aber die Würfelergebnisse für speichern - nicetohave: Wenn ich rea editiere, könnte sich die ini automatisch anpassen -> da müsste ich aber die Würfelergebnisse für speichern
- nicetohave: Anzeige, wieviele Aktionen einer hat u.d wieviele davon schon verbraucht sind - nicetohave: Anzeige, wieviele Aktionen einer hat u.d wieviele davon schon verbraucht sind
- Sache mit dem ServiceWorker mal richtig angehen
- x progressive web app - x progressive web app
- x Service Worker einrichten, um die Dateien lokal zu cachen - x Service Worker einrichten, um die Dateien lokal zu cachen
@ -197,4 +236,3 @@
- x dafür sorgen, dass die Seite erst dann aufgebaut wird, wenn die CSS-Files geladen sind, damit man nicht den ungestylten Krams sieht -> passt schon - x dafür sorgen, dass die Seite erst dann aufgebaut wird, wenn die CSS-Files geladen sind, damit man nicht den ungestylten Krams sieht -> passt schon
- x CSS aufräumen - x CSS aufräumen
- Variablen für Farben, Filter etc. - Variablen für Farben, Filter etc.

View File

@ -1,21 +0,0 @@
# Your Favicon Package
This package was generated with [RealFaviconGenerator](https://realfavicongenerator.net/) [v0.16](https://realfavicongenerator.net/change_log#v0.16)
## Install instructions
To install this package:
Extract this package in the root of your web site. If your site is <code>http://www.example.com</code>, you should be able to access a file named <code>http://www.example.com/favicon.ico</code>.
Insert the following code in the `head` section of your pages:
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#004aa5">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="theme-color" content="#ffffff">
*Optional* - Check your favicon with the [favicon checker](https://realfavicongenerator.net/favicon_checker)

View File

@ -1,7 +0,0 @@
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#004aa5">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="theme-color" content="#ffffff">

View File

@ -1,19 +0,0 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "gold",
"background_color": "#004aa5",
"display": "standalone"
}

View File

@ -1,12 +1,12 @@
{ {
"name": "sr2ini", "name": "sr2ini",
"version": "0.9", "version": "0.9.5",
"description": "Simple Initiative tracker for Shadowrun 2e", "description": "Simple Initiative tracker for Shadowrun 2e",
"private": true, "private": true,
"author": { "author": {
"name": "Eclipse2049", "name": "Eclipse",
"email": "eclipse@unterdemradar.de", "email": "eclipse@unterdemradar.de",
"url": "https://git.unterdemradar.de" "url": "https://git.unterdemradar.de/eclipse"
}, },
"homepage": "https://unterdemradar.de/sr2ini/", "homepage": "https://unterdemradar.de/sr2ini/",
"license": "ISC", "license": "ISC",
@ -25,14 +25,14 @@
}, },
"scripts": { "scripts": {
"start": "npx parcel serve src/index.html --public-url / --dist-dir dist", "start": "npx parcel serve src/index.html --public-url / --dist-dir dist",
"prebuild": "rm -rf dist/", "clean": "rm -rf dist/ && rm -rf .parcel-cache/",
"build": "npx parcel build --no-optimize --public-url ./", "build": "npx parcel build --no-optimize --public-url ./ --dist-dir dist",
"test": "jest --coverage --env=jsdom", "test": "jest --coverage --env=jsdom",
"webhint": "hint http://localhost:1234" "webhint": "hint http://localhost:1234"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@git.unterdemradar.de:tobias/sr2ini.git" "url": "git@git.unterdemradar.de:eclipse/sr2ini.git"
}, },
"keywords": [ "keywords": [
"Shadowrun", "Shadowrun",
@ -52,10 +52,5 @@
"augmented-ui": "^2.0.0", "augmented-ui": "^2.0.0",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"jquery": "^3.6.3" "jquery": "^3.6.3"
},
"comments": {
"dependencies": {
"@parcel/service-worker": "^2.8.3"
}
} }
} }

View File

@ -167,8 +167,6 @@ body {
margin: .2rem; margin: .2rem;
} }
//button { font-family: Electrolize; }
.container { .container {
padding: .2rem; padding: .2rem;
position: relative; position: relative;
@ -262,6 +260,8 @@ header.navbar {
--aug-border-bottom: 0px; --aug-border-bottom: 0px;
--aug-border-right: 0px; --aug-border-right: 0px;
position: relative;
} }
tr:last-of-type td, tr:last-of-type td,
@ -315,6 +315,10 @@ header.navbar {
} }
} }
.combatant-name:hover,
.combatant-ini:hover,
.combatant-dice-and-rea:hover { cursor: pointer; }
.combatant-name { .combatant-name {
padding-left: .6rem !important; padding-left: .6rem !important;
text-align: left; text-align: left;
@ -378,6 +382,11 @@ header.navbar {
top: .3rem; top: .3rem;
} }
.damage-dropdown,
.actions-dropdown {
position: relative; // required for the dropdowns' absolute position to relate to
}
.damage-monitor, .damage-monitor,
.actions-menu { .actions-menu {
@include aug; @include aug;
@ -388,6 +397,7 @@ header.navbar {
padding-top: .7rem; padding-top: .7rem;
padding-bottom: .7rem; padding-bottom: .7rem;
position: absolute; position: absolute;
top: calc(100% + .12rem);
z-index: 200; z-index: 200;
// center element horizontally w/ respect to parent // center element horizontally w/ respect to parent

View File

@ -14,13 +14,14 @@
<script type="module" src="js/sr2ini.js"></script> <script type="module" src="js/sr2ini.js"></script>
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="../icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="../icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="../icons/favicon-16x16.png">
<link rel="manifest" href="/icons/sr2ini.iconmanifest"> <link rel="manifest" href="site.webmanifest">
<link rel="shortcut icon" href="/icons/favicon.ico"> <link rel="mask-icon" href="../icons/safari-pinned-tab.svg" color="#004aa5">
<link rel="shortcut icon" href="../icons/favicon.ico">
<meta name="msapplication-TileColor" content="#004aa5"> <meta name="msapplication-TileColor" content="#004aa5">
<meta name="msapplication-config" content="/icons/browserconfig.xml"> <meta name="msapplication-config" content="../icons/browserconfig.xml">
<meta name="theme-color" content="gold"> <meta name="theme-color" content="gold">
</head> </head>
@ -28,7 +29,7 @@
<!-- navbar --> <!-- navbar -->
<div class="container"> <div class="container">
<header class="navbar navbar-expand" data-augmented-ui="tl-2-clip-x tr-clip-y bl-clip-y br-2-clip-x b-scoop-x both"> <header class="navbar navbar-expand" data-augmented-ui="tl-2-clip-x tr-clip-y bl-clip-y br-2-clip-x b-scoop-x both">
<span class="navbar-brand ps-4">SR2 Initiative Tracker</span> <span id="navbar-title" class="navbar-brand ps-4">SR2 Initiative Tracker</span>
<nav class="container-fluid justify-content-end" aria-label="Main navigation"> <nav class="container-fluid justify-content-end" aria-label="Main navigation">
<button type="submit" class="sr2-button" id="add-combatant-button" title="Add combatant" data-bs-toggle="modal" data-bs-target="#combatant-modal"><svg viewbox="0 0 512 512"><use href="#add" /></svg> <button type="submit" class="sr2-button" id="add-combatant-button" title="Add combatant" data-bs-toggle="modal" data-bs-target="#combatant-modal"><svg viewbox="0 0 512 512"><use href="#add" /></svg>
</button> </button>
@ -69,8 +70,8 @@
</div> </div>
<div class="modal-footer" data-augmented-ui="inlay"> <div class="modal-footer" data-augmented-ui="inlay">
<button type="button" class="sr2-button" data-bs-dismiss="modal">Cancel</button> <button type="button" class="sr2-button" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="sr2-button" id="confirm-modal-new-round-ok-button" data-bs-dismiss="modal">OK</button> <button type="button" class="sr2-button" id="confirm-modal-new-round-ok-button" data-bs-dismiss="modal">OK</button>
<button type="submit" class="sr2-button display-none" id="confirm-modal-remove-combatant-ok-button" data-bs-dismiss="modal">OK</button> <button type="button" class="sr2-button display-none" id="confirm-modal-remove-combatant-ok-button" data-bs-dismiss="modal">OK</button>
</div> </div>
</div> </div>
</div> </div>
@ -84,8 +85,8 @@
<h2 class="modal-title">Add New Combatant</h2> <h2 class="modal-title">Add New Combatant</h2>
<button type="button" class="sr2-button" data-bs-dismiss="modal" aria-label="Close">&#10006;</button> <button type="button" class="sr2-button" data-bs-dismiss="modal" aria-label="Close">&#10006;</button>
</div> </div>
<div class="modal-body" data-augmented-ui="inlay">
<form id="combatant-form" name="combatant-modal-form" class="was-validated" onsubmit="return false;"> <form id="combatant-form" name="combatant-modal-form" class="was-validated" onsubmit="return false;">
<div class="modal-body" data-augmented-ui="inlay">
<div> <div>
<input type="text" maxlength="40" class="form-control form-control-sm" id="combatant-modal-name" form="combatant-form" placeholder="Name" required> <input type="text" maxlength="40" class="form-control form-control-sm" id="combatant-modal-name" form="combatant-form" placeholder="Name" required>
</div> </div>
@ -107,14 +108,14 @@
<input type="range" class="form-range" min="0" max="10" value="0" id="combatant-modal-physical" list="damage-level"> <input type="range" class="form-range" min="0" max="10" value="0" id="combatant-modal-physical" list="damage-level">
</div> </div>
</div> </div>
</form>
</div> </div>
<div class="modal-footer" data-augmented-ui="inlay"> <div class="modal-footer" data-augmented-ui="inlay">
<button type="button" class="sr2-button" id="combatant-modal-cancel-button" data-bs-dismiss="modal">Cancel</button> <button type="button" class="sr2-button" id="combatant-modal-cancel-button" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="sr2-button" id="combatant-modal-add-apply-button">Apply</button> <button type="button" class="sr2-button" id="combatant-modal-add-apply-button">Apply</button>
<button type="submit" class="sr2-button" id="combatant-modal-add-ok-button" >OK</button> <button type="button" class="sr2-button" id="combatant-modal-add-ok-button" data-bs-dismiss="modal">OK</button>
<button type="submit" class="sr2-button display-none" id="combatant-modal-edit-ok-button" >OK</button> <button type="button" class="sr2-button display-none" id="combatant-modal-edit-ok-button" data-bs-dismiss="modal">OK</button>
</div> </div>
</form>
</div> </div>
</div> </div>
</div> </div>
@ -189,7 +190,7 @@
<!-- footer --> <!-- footer -->
<div class="footer-container container"> <div class="footer-container container">
<footer data-augmented-ui="tl-clip br-clip both"> <footer data-augmented-ui="tl-clip br-clip both">
<p><a href="https://git.unterdemradar.de/tobias/sr2ini" tabindex="-1" title="sr2ini">sr2ini</a> | Copyright (C) 2023 by Eclipse | background by <a href="https://www.deviantart.com/xxaries1970xx" tabindex="-1" title="xxAries1970xx on DeviantArt">xxAries1970xx</a></p> <p><a href="https://git.unterdemradar.de/eclipse/sr2ini" tabindex="-1" title="sr2ini">sr2ini</a> | Copyright (C) 2022-23 by <a href="https://git.unterdemradar.de/eclipse">Eclipse</a> | background by <a href="https://www.deviantart.com/xxaries1970xx" tabindex="-1" title="xxAries1970xx on DeviantArt">xxAries1970xx</a></p>
</footer> </footer>
</div> </div>
</body> </body>

View File

@ -1,26 +1,9 @@
/* *****************
// Register Service Worker
if (navigator && navigator.serviceWorker) {
navigator.serviceWorker.register(
new URL("service-worker.js", import.meta.url),
{type: "module"}
).then( (registration) => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, (err) => {
console.log('ServiceWorker registration failed: ', err);
});
} else {
console.error("Service workers are not supported.");
}
***************** */
/* /*
* import libraries * import libraries
*/ */
const bs = require("../../node_modules/bootstrap/js/dist/modal.js");
const $ = require("../../node_modules/jquery/dist/jquery.js"); const $ = require("../../node_modules/jquery/dist/jquery.js");
const bs = require("../../node_modules/bootstrap/js/dist/modal.js");
/* /*
@ -30,7 +13,7 @@ const $ = require("../../node_modules/jquery/dist/jquery.js");
const DAMAGE_PENALTY = [0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4]; const DAMAGE_PENALTY = [0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4];
const DAMAGE_NIVEAU = ["", "L", "M", "S", "D"]; const DAMAGE_NIVEAU = ["", "L", "M", "S", "D"];
const COMBATANT_TABLE_ROW = [ const $COMBATANT_TABLE_ROW = $($.parseHTML([
'<tr class="combatant-row" data-true-ini="">\n', '<tr class="combatant-row" data-true-ini="">\n',
'<td class="combatant-name" title="Combatant\'s name" data-bs-toggle="modal" data-bs-target="#combatant-modal" data-augmented-ui="both"></td>\n', '<td class="combatant-name" title="Combatant\'s name" data-bs-toggle="modal" data-bs-target="#combatant-modal" data-augmented-ui="both"></td>\n',
'<td class="combatant-ini" title="Effective initiative (w/ wound penalties)" data-bs-toggle="modal" data-bs-target="#combatant-modal" data-augmented-ui="both"></td>\n', '<td class="combatant-ini" title="Effective initiative (w/ wound penalties)" data-bs-toggle="modal" data-bs-target="#combatant-modal" data-augmented-ui="both"></td>\n',
@ -41,20 +24,7 @@ const COMBATANT_TABLE_ROW = [
'</div>\n', '</div>\n',
'<div class="damage-dropdown">\n', '<div class="damage-dropdown">\n',
'<button type="button" class="sr2-button damage-button" title="Take damage"><svg viewbox="0 0 512 512"><use href="#take-damage" /></svg></button>\n', '<button type="button" class="sr2-button damage-button" title="Take damage"><svg viewbox="0 0 512 512"><use href="#take-damage" /></svg></button>\n',
'</div>\n', '<div class="damage-monitor" data-augmented-ui="tl-scoop bl-clip-y tr-clip-y br-scoop both">\n',
'<div class="actions-dropdown">\n',
'<button type="button" class="sr2-button actions-button" title="More actions"><svg viewbox="0 0 512 512"><use href="#more-actions" /</svg></button>\n',
'<div class="actions-menu" data-augmented-ui="tl-scoop bl-clip-y tr-clip-y br-scoop both">\n',
'<button type="button" class="sr2-button edit-button" title="Edit combatant" data-bs-toggle="modal" data-bs-target="#combatant-modal" tabindex="-1"><svg viewbox="0 0 512 512"><use href="#edit" /></svg></button>\n',
'<button type="button" class="sr2-button clone-button" title="Clone combatant" data-bs-toggle="modal" data-bs-target="#combatant-modal" tabindex="-1"><svg viewbox="0 0 512 512"><use href="#clone" /></svg></button>\n',
'<button type="button" class="sr2-button remove-button" title="Remove combatant" data-bs-toggle="modal" data-bs-target="#confirm-modal" tabindex="-1"><svg viewbox="0 0 512 512"><use href="#delete" /></svg></button>\n',
'</div>\n',
'</div>\n',
'</td>\n',
'</tr>'].join("");
const DAMAGE_MONITOR_HTML = [
'<div class="damage-monitor" data-augmented-ui="tl-scoop bl-clip-y tr-clip-y br-scoop both">\n',
'<table>\n', '<table>\n',
'<tr><td><button type="button" class="damage-stun active" title="Light stun damage" tabindex="-1">L</button></td><td><button type="button" class="damage-physical active" title="Light physical damage" tabindex="-1">L</button></td></tr>\n', '<tr><td><button type="button" class="damage-stun active" title="Light stun damage" tabindex="-1">L</button></td><td><button type="button" class="damage-physical active" title="Light physical damage" tabindex="-1">L</button></td></tr>\n',
'<tr><td><button type="button" class="damage-stun active" tabindex="-1">&nbsp;</button></td><td><button type="button" class="damage-physical active" tabindex="-1">&nbsp;</button></td></tr>\n', '<tr><td><button type="button" class="damage-stun active" tabindex="-1">&nbsp;</button></td><td><button type="button" class="damage-physical active" tabindex="-1">&nbsp;</button></td></tr>\n',
@ -67,16 +37,39 @@ const DAMAGE_MONITOR_HTML = [
'<tr><td><button type="button" class="damage-stun active" tabindex="-1">&nbsp;</button></td><td><button type="button" class="damage-physical active" tabindex="-1">&nbsp;</button></td></tr>\n', '<tr><td><button type="button" class="damage-stun active" tabindex="-1">&nbsp;</button></td><td><button type="button" class="damage-physical active" tabindex="-1">&nbsp;</button></td></tr>\n',
'<tr><td><button type="button" class="damage-stun active" title="K.O." tabindex="-1"><svg viewbox="0 0 512 512"><use href="#ko" /></svg></button></td><td><button type="button" class="damage-physical active" title="Dead" tabindex="-1"><svg viewbox="0 0 512 512"><use href="#dead" /></svg></button></td></tr>\n', '<tr><td><button type="button" class="damage-stun active" title="K.O." tabindex="-1"><svg viewbox="0 0 512 512"><use href="#ko" /></svg></button></td><td><button type="button" class="damage-physical active" title="Dead" tabindex="-1"><svg viewbox="0 0 512 512"><use href="#dead" /></svg></button></td></tr>\n',
'</table>\n', '</table>\n',
'</div>'].join(""); '</div>\n',
'</div>\n',
'<div class="actions-dropdown">\n',
'<button type="button" class="sr2-button more-actions-button" title="More actions"><svg viewbox="0 0 512 512"><use href="#more-actions" /</svg></button>\n',
'<div class="actions-menu" data-augmented-ui="tl-scoop bl-clip-y tr-clip-y br-scoop both">\n',
'<button type="button" class="sr2-button edit-button" title="Edit combatant" data-bs-toggle="modal" data-bs-target="#combatant-modal" tabindex="-1"><svg viewbox="0 0 512 512"><use href="#edit" /></svg></button>\n',
'<button type="button" class="sr2-button clone-button" title="Clone combatant" data-bs-toggle="modal" data-bs-target="#combatant-modal" tabindex="-1"><svg viewbox="0 0 512 512"><use href="#clone" /></svg></button>\n',
'<button type="button" class="sr2-button remove-button" title="Remove combatant" data-bs-toggle="modal" data-bs-target="#confirm-modal" tabindex="-1"><svg viewbox="0 0 512 512"><use href="#delete" /></svg></button>\n',
'</div>\n',
'</div>\n',
'</td>\n',
'</tr>'].join("")));
const STUN_BADGE_HTML = '<sup><span class="badge bg-warning translate-middle stun-badge" title="Stun damage niveau"></span></sup>'; const $STUN_BADGE_HTML = $($.parseHTML('<sup><span class="badge bg-warning translate-middle stun-badge" title="Stun damage niveau"></span></sup>'));
const PHYSICAL_BADGE_HTML = '<sub><span class="badge bg-danger translate-middle physical-badge" title="Physical damage niveau"></span></sub>'; const $PHYSICAL_BADGE_HTML = $($.parseHTML('<sub><span class="badge bg-danger translate-middle physical-badge" title="Physical damage niveau"></span></sub>'));
/* /*
* helper functions * helper class and functions
*/ */
// this class generates unique IDs
// thx to stackoverflow user user1005939 (https://stackoverflow.com/questions/26203453/jquery-generate-unique-ids#comment128736848_33226136)
class IDGenerator {
#id = 0;
get next() { return this.#id++; }
}
// IDGenerator instance that will be used to generate combatant IDs
var idGen = new IDGenerator();
// roll for initiative with the given reaction and number of ini dice // roll for initiative with the given reaction and number of ini dice
function rollForInitiative(dice, rea) { function rollForInitiative(dice, rea) {
if (isNaN(parseInt(dice)) || isNaN(parseInt(rea))) { if (isNaN(parseInt(dice)) || isNaN(parseInt(rea))) {
@ -110,28 +103,61 @@ function whoGoesFirst(a, b) {
// compute a combatant's effective ini value (modified by wound penalties) // compute a combatant's effective ini value (modified by wound penalties)
function getEffectiveIni(tr) { function getEffectiveIni(tr) {
let $tr = $(tr);
// return 0 if combatant is K.O. or dead // return 0 if combatant is K.O. or dead
if ($(tr).hasClass("ko-or-dead")) { if ($tr.hasClass("ko-or-dead")) {
return 0; return -1;
} }
// otherwise compute effective ini (true ini minus wound penalties) // otherwise compute effective ini (true ini minus wound penalties)
let effectiveIni = parseInt($(tr).attr("data-true-ini")) - DAMAGE_PENALTY[parseInt($(tr).attr("data-damage-stun")) || 0] - DAMAGE_PENALTY[parseInt($(tr).attr("data-damage-physical")) || 0]; let effectiveIni = parseInt($tr.attr("data-true-ini")) - DAMAGE_PENALTY[parseInt($tr.attr("data-damage-stun")) || 0] - DAMAGE_PENALTY[parseInt($tr.attr("data-damage-physical")) || 0];
return Math.max(effectiveIni, 0); return Math.max(effectiveIni, 0);
} }
// add test combatant for testing purposes (duh) function getStatsFromCombatantModal() {
function addTestCombatant() { return {
name: $("#combatant-modal-name").val().trim(),
ini: $("#combatant-modal-ini").val().trim(),
dice: $("#combatant-modal-dice").val().trim(),
rea: $("#combatant-modal-rea").val().trim(),
stun: $("#combatant-modal-stun").val() || "0",
physical: $("#combatant-modal-physical").val() || "0"
}
};
function addTestCombatants() {
// Eclipse // Eclipse
// $("#add-combatant-button").click();
$("#combatant-modal-name").val("Eclipse"); $("#combatant-modal-name").val("Eclipse");
$("#combatant-modal-dice").val(3); $("#combatant-modal-dice").val("3");
$("#combatant-modal-rea").val(6); $("#combatant-modal-rea").val("6");
// $("#combatant-modal-ini").val(12); $("#combatant-modal-stun").val("0");
addCombatant(); $("#combatant-modal-physical").val("0");
// setTimeout( () => $("#combatant-modal-add-ok-button").click(), 500); handleCombatantModalAddApplyButtonClick();
// Solitaire
$("#combatant-modal-name").val("Solitaire");
$("#combatant-modal-dice").val("3");
$("#combatant-modal-rea").val("17");
$("#combatant-modal-stun").val("0");
$("#combatant-modal-physical").val("0");
handleCombatantModalAddApplyButtonClick();
// Q
$("#combatant-modal-name").val("Q");
$("#combatant-modal-dice").val("2");
$("#combatant-modal-rea").val("9");
$("#combatant-modal-stun").val("0");
$("#combatant-modal-physical").val("0");
handleCombatantModalAddApplyButtonClick();
// Pie
$("#combatant-modal-name").val("Pie");
$("#combatant-modal-dice").val("3");
$("#combatant-modal-rea").val("19");
$("#combatant-modal-stun").val("0");
$("#combatant-modal-physical").val("0");
handleCombatantModalAddApplyButtonClick();
} }
/* /*
* Event handler functions * Event handler functions
*/ */
@ -140,10 +166,9 @@ function addTestCombatant() {
function handleActButtonClick(event) { function handleActButtonClick(event) {
// reduce ini by 10 but not lower than 0 // reduce ini by 10 but not lower than 0
let ini = Math.max(parseInt($(event.target).parents(".combatant-row").attr("data-true-ini")) - 10, 0); let ini = Math.max(parseInt($(event.target).parents(".combatant-row").attr("data-true-ini")) - 10, 0);
// set new ini value
$(event.target).parents(".combatant-row").attr("data-true-ini", ini); $(event.target).parents(".combatant-row").attr("data-true-ini", ini);
// resort table // resort table
sortTable(); updateTable($("#combatants-table"));
} }
// click handler for add button // click handler for add button
@ -152,15 +177,16 @@ function handleAddButtonClick(event) {
$("#combatant-modal .modal-title").text("Add Combatant"); $("#combatant-modal .modal-title").text("Add Combatant");
$("#combatant-modal-add-ok-button, #combatant-modal-add-apply-button").removeClass("display-none"); $("#combatant-modal-add-ok-button, #combatant-modal-add-apply-button").removeClass("display-none");
$("#combatant-modal-edit-ok-button").addClass("display-none"); $("#combatant-modal-edit-ok-button").addClass("display-none");
// set default values // clear values
$("#combatant-modal-stun, #combatant-modal-physical").val("0");
// $("#combatant-modal-name #combatant-modal-ini #combatant-modal-dice #combatant-modal-rea").val("");
// preset values
$("#combatant-modal-name").val("Goon 1"); $("#combatant-modal-name").val("Goon 1");
$("#combatant-modal-dice").val("2"); $("#combatant-modal-dice").val("2");
$("#combatant-modal-rea").val("7"); $("#combatant-modal-rea").val("7");
// set damage sliders to zero
$("#combatant-modal-stun, #combatant-modal-physical").val("0");
// add handler for enter key // add handler for enter key
$("#combatant-modal input[id*='combatant-modal']").off("keydown"); $("#combatant-modal input[id*='combatant-modal']").off("keydown");
$("#combatant-modal input[id*='combatant-modal']").on("keydown", (e) => { $("#combatant-modal input[id*='combatant-modal']").on("keydown", e => {
if (e.which == 13 || e.which == 10) { if (e.which == 13 || e.which == 10) {
handleCombatantModalAddOkButtonClick(e); handleCombatantModalAddOkButtonClick(e);
} }
@ -195,25 +221,55 @@ function handleCloneButtonClick(event) {
// click handler for combatant modal OK button (add mode) // click handler for combatant modal OK button (add mode)
function handleCombatantModalAddOkButtonClick(event) { function handleCombatantModalAddOkButtonClick(event) {
if (validateCombatant()) { // validate
bs.getInstance($("#combatant-modal")).hide(); if ( ! validateCombatant($("#combatant-form").get(0))) {
addCombatant(); return false;
} }
// hide modal
bs.getInstance($("#combatant-modal")).hide();
// everything else can be handled by the apply button handler
handleCombatantModalAddApplyButtonClick(event);
} }
// click handler for combatant modal Apply button (add mode) // click handler for combatant modal Apply button (add mode)
function handleCombatantModalAddApplyButtonClick(event) { function handleCombatantModalAddApplyButtonClick(event) {
if (validateCombatant()) { // validate
addCombatant(); if ( ! validateCombatant($("#combatant-form").get(0))) {
return false;
} }
// prepare new table row
let $tr = $COMBATANT_TABLE_ROW.clone();
$tr.attr("id", idGen.next);
$tr.find(".act-button").on("click", handleActButtonClick);
$tr.find(".damage-button").on("click", handleDamageButtonClick);
$tr.find(".damage-stun, .damage-physical").on("click", handleDamageLevelClick);
$tr.find(".more-actions-button").on("click", handleMoreActionsButtonClick);
$tr.find(".edit-button, .combatant-name, .combatant-ini, .combatant-dice-and-rea").on("click", handleEditButtonClick);
$tr.find(".clone-button").on("click", handleCloneButtonClick);
$tr.find(".remove-button").on("click", handleRemoveButtonClick);
// update table row with stats from modal
$tr = updateCombatantTablerow(getStatsFromCombatantModal(), $tr);
// append row to table
$("#combatants-table").append($tr);
// update (and sort) table
updateTable($("#combatants-table"));
} }
// click handler for combatant modal OK button (edit mode) // click handler for combatant modal OK button (edit mode)
function handleCombatantModalEditOkButtonClick(event) { function handleCombatantModalEditOkButtonClick(event) {
if (validateCombatant()) { // validate
bs.getInstance($("#combatant-modal")).hide(); if ( ! validateCombatant($("#combatant-form").get(0)) ) {
editCombatant(); return false;
} }
// hide modal
bs.getInstance($("#combatant-modal")).hide();
// update table row
let id = $("#combatant-modal").data("row-id"),
$tr = updateCombatantTablerow(getStatsFromCombatantModal(), $(".combatant-row#" + id));
// update table
updateTable($("#combatants-table"));
// clean up
$("#combatant-modal").data("row-id", "");
} }
// click handler for damage buttons; basically toggles visibility of .damage-monitor // click handler for damage buttons; basically toggles visibility of .damage-monitor
@ -222,7 +278,7 @@ function handleDamageButtonClick(event) {
let seenAtClick = $(event.target).parents(".damage-dropdown").find(".damage-monitor").hasClass("seen"); let seenAtClick = $(event.target).parents(".damage-dropdown").find(".damage-monitor").hasClass("seen");
// hide all damage monitors and actions menus // hide all damage monitors and actions menus
$(".damage-monitor.seen, .actions-menu.seen").removeClass("seen").find("button").attr("tabindex", "-1"); $(".damage-monitor.seen, .actions-menu.seen").removeClass("seen").find("button").attr("tabindex", "-1");
// if targeted dm was hidden before, show it now // if targeted damage monitor was hidden before, show it now
if (! seenAtClick) { if (! seenAtClick) {
$(event.target).parents(".damage-dropdown").find(".damage-monitor").addClass("seen").find("button").attr("tabindex", "0"); $(event.target).parents(".damage-dropdown").find(".damage-monitor").addClass("seen").find("button").attr("tabindex", "0");
} }
@ -231,6 +287,7 @@ function handleDamageButtonClick(event) {
// handle click on damage level button in damage monitor and apply damage to combatant // handle click on damage level button in damage monitor and apply damage to combatant
function handleDamageLevelClick(event) { function handleDamageLevelClick(event) {
// find button
let $btn = $(event.target).is("button") ? $(event.target) : $(event.target).closest("button"); let $btn = $(event.target).is("button") ? $(event.target) : $(event.target).closest("button");
// retrieve new damage level and type from button position and "damage-[type]" class // retrieve new damage level and type from button position and "damage-[type]" class
let damageLevel = $btn.parent().parent().index(); let damageLevel = $btn.parent().parent().index();
@ -239,18 +296,21 @@ function handleDamageLevelClick(event) {
} }
let damageType = $btn.attr("class").split(" ").filter(cls => cls.substr(0, 7) == "damage-" ? cls : false).toString().substr(7); let damageType = $btn.attr("class").split(" ").filter(cls => cls.substr(0, 7) == "damage-" ? cls : false).toString().substr(7);
// add damage level to table row as as data attribute // add damage level to table row as as data attribute
$btn.parents("tr.combatant-row").attr("data-damage-" + damageType, damageLevel); $btn.parents(".combatant-row").attr("data-damage-" + damageType, damageLevel);
// select/unselect damage buttons above/below // select/unselect damage buttons above/below
$btn.toggleClass("active"); $btn.toggleClass("active");
$btn.parent().parent().prevAll().find("button.damage-" + damageType).removeClass("active"); $btn.parent().parent().prevAll().find(".damage-" + damageType).removeClass("active");
$btn.parent().parent().nextAll().find("button.damage-" + damageType + ":not(.active)").addClass("active"); $btn.parent().parent().nextAll().find(".damage-" + damageType).addClass("active");
sortTable(); updateTable($("#combatants-table"));
$btn.focus();
} }
// click handler for edit buttons // click handler for edit buttons
function handleEditButtonClick(event) { function handleEditButtonClick(event) {
// find current table row // save ID of row being edited
// here it's okay to use the jQuery data() function (which is not the same as using a data attribute) b/c this value is only used with JS, not with HTML or CSS
let $tr = $(event.target).parents(".combatant-row"); let $tr = $(event.target).parents(".combatant-row");
$("#combatant-modal").data("row-id", $tr.attr("id"));
// restyle modal // restyle modal
$("#combatant-modal .modal-title").text("Edit Combatant"); $("#combatant-modal .modal-title").text("Edit Combatant");
$("#combatant-modal-edit-ok-button").removeClass("display-none"); $("#combatant-modal-edit-ok-button").removeClass("display-none");
@ -262,8 +322,6 @@ function handleEditButtonClick(event) {
$("#combatant-modal-ini").val($tr.attr("data-true-ini")); $("#combatant-modal-ini").val($tr.attr("data-true-ini"));
$("#combatant-modal-stun").val($tr.attr("data-damage-stun") || "0"); $("#combatant-modal-stun").val($tr.attr("data-damage-stun") || "0");
$("#combatant-modal-physical").val($tr.attr("data-damage-physical") || "0"); $("#combatant-modal-physical").val($tr.attr("data-damage-physical") || "0");
// mark which row is being edited
$("#combatant-modal").data("row", $(".combatant-row").index($tr)); // here it's okay to use the jQuery data() function (which is not the same as using a data attribute) b/c this value is used only in this script and not via HTML or CSS
// add handler for enter key // add handler for enter key
$("#combatant-modal input[id*='combatant-modal']").off("keydown"); $("#combatant-modal input[id*='combatant-modal']").off("keydown");
$("#combatant-modal input[id*='combatant-modal']").on("keydown", (e) => { $("#combatant-modal input[id*='combatant-modal']").on("keydown", (e) => {
@ -277,14 +335,12 @@ function handleEditButtonClick(event) {
function handleMoreActionsButtonClick(event) { function handleMoreActionsButtonClick(event) {
// get visibility status at click time // get visibility status at click time
let seenAtClick = $(event.target).parents(".actions-dropdown").find(".actions-menu").hasClass("seen"); let seenAtClick = $(event.target).parents(".actions-dropdown").find(".actions-menu").hasClass("seen");
// hide all damage monitors // hide all damage monitors
$(".actions-menu.seen, .damage-monitor.seen").removeClass("seen").find("button").attr("tabindex", "-1"); $(".actions-menu.seen, .damage-monitor.seen").removeClass("seen").find("button").attr("tabindex", "-1");
// if targeted dm was seen before, show it now // if targeted actions menu was seen before, show it now
if (! seenAtClick) { if (! seenAtClick) {
$(event.target).parents(".actions-dropdown").find(".actions-menu").addClass("seen").find("button").attr("tabindex", "0"); $(event.target).parents(".actions-dropdown").find(".actions-menu").addClass("seen").find("button").attr("tabindex", "0");
} }
return false; return false;
} }
@ -306,12 +362,13 @@ function handleNewRoundButtonClick(event) {
// click handler for remove buttons // click handler for remove buttons
function handleRemoveButtonClick(event) { function handleRemoveButtonClick(event) {
// mark which row is being removed
let id = $(event.target).parents(".combatant-row").attr("id");
$("#confirm-modal").data("row-id", id); // again, here it's okay to use jQuery .data() method (see handleEditButtonClick())
// restyle modal // restyle modal
$("#confirm-modal .modal-title").text("Remove Combatant"); $("#confirm-modal .modal-title").text("Remove Combatant");
$("#confirm-modal-remove-combatant-ok-button").removeClass("display-none"); $("#confirm-modal-remove-combatant-ok-button").removeClass("display-none");
$("#confirm-modal-new-round-ok-button").addClass("display-none"); $("#confirm-modal-new-round-ok-button").addClass("display-none");
// mark which row is being removed
$("#confirm-modal").data("row", $(".combatant-row").index($(event.target).parents(".combatant-row"))); // here it's okay to use .data() b/c HTML/CSS does not care about this value
// add handler for enter key // add handler for enter key
$("#confirm-modal").off("keydown"); $("#confirm-modal").off("keydown");
$("#confirm-modal").on("keydown", (e) => { $("#confirm-modal").on("keydown", (e) => {
@ -327,70 +384,31 @@ function handleRemoveButtonClick(event) {
* Main functions * Main functions
*/ */
// add new combatant
function addCombatant() {
// roll for initiative if necessary
let ini = $("#combatant-modal-ini").val().trim();
ini = (ini != "") ? ini : rollForInitiative($("#combatant-modal-dice").val(), $("#combatant-modal-rea").val());
// construct jQuery object for table row
let $tr = $($.parseHTML(COMBATANT_TABLE_ROW));
$tr.find(".damage-dropdown").append($.parseHTML(DAMAGE_MONITOR_HTML));
// populate table row with values from modal
$tr.attr("data-true-ini", ini);
$tr.find(".combatant-name").text($("#combatant-modal-name").val().trim());
$tr.find(".combatant-dice").attr("data-combatant-dice", $("#combatant-modal-dice").val().trim());
$tr.find(".combatant-rea").attr("data-combatant-rea", $("#combatant-modal-rea").val().trim());
// retrieve initial damage levels
$tr.attr("data-damage-stun", $("#combatant-modal-stun").val() || "0");
$tr.find(".damage-stun").addClass("active").slice(0, parseInt($tr.attr("data-damage-stun")) || 0).removeClass("active");
$tr.attr("data-damage-physical", $("#combatant-modal-physical").val() || "0");
$tr.find(".damage-physical").addClass("active").slice(0, parseInt($tr.attr("data-damage-physical")) || 0).removeClass("active");
// add event handlers
$tr.find("button.act-button").on("click", handleActButtonClick);
$tr.find("button.damage-button").on("click", handleDamageButtonClick);
$tr.find("button.actions-button").on("click", handleMoreActionsButtonClick);
$tr.find("button.edit-button, .combatant-name, .combatant-ini, .combatant-dice-and-rea").on("click", handleEditButtonClick);
$tr.find("button.clone-button").on("click", handleCloneButtonClick);
$tr.find("button.remove-button").on("click", handleRemoveButtonClick);
$tr.find(".damage-stun, .damage-physical").on("click", handleDamageLevelClick);
// append row to table and sort
$("#combatants-table").append($tr);
sortTable();
}
// edit combatant function updateCombatantTablerow(stats, $tr) {
function editCombatant() { // roll for initiative if necessary
// get values let ini = (stats["ini"] == "") ? rollForInitiative(stats["dice"], stats["rea"]) : stats["ini"];
let name = $("#combatant-modal-name").val().trim(); // populate table row with combatant stats
let ini = $("#combatant-modal-ini").val().trim(); $tr.find(".combatant-name").text(stats["name"]);
let dice = $("#combatant-modal-dice").val().trim();
let rea = $("#combatant-modal-rea").val().trim();
// roll for initiative if ini is empty
ini = (ini != "") ? ini : rollForInitiative(dice, rea);
// get correct row
let index = parseInt($("#combatant-modal").data("row"));
let $tr = $("tr.combatant-row").eq(index);
// set new values
$tr.attr("data-true-ini", ini); $tr.attr("data-true-ini", ini);
$tr.find(".combatant-name").text(name); $tr.find(".combatant-dice").attr("data-combatant-dice", stats["dice"]);
$tr.find(".combatant-dice").attr("data-combatant-dice", dice); $tr.find(".combatant-rea").attr("data-combatant-rea", stats["rea"]);
$tr.find(".combatant-rea").attr("data-combatant-rea", rea); // set initial damage levels
$tr.attr("data-damage-stun", $("#combatant-modal-stun").val() || "0"); $tr.attr("data-damage-stun", stats["stun"]);
$tr.find(".damage-stun").addClass("active").slice(0, parseInt($tr.attr("data-damage-stun")) || 0).removeClass("active"); $tr.find(".damage-stun").addClass("active").slice(0, parseInt(stats["stun"])).removeClass("active");
$tr.attr("data-damage-physical", $("#combatant-modal-physical").val() || "0"); $tr.attr("data-damage-physical", stats["physical"]);
$tr.find(".damage-physical").addClass("active").slice(0, parseInt($tr.attr("data-damage-physical")) || 0).removeClass("active"); $tr.find(".damage-physical").addClass("active").slice(0, parseInt(stats["physical"])).removeClass("active");
// sort table // done
sortTable(); return $tr;
// clean up
$("#combatant-modal").data("row", "");
} }
// remove combatant // remove combatant
function removeCombatant() { function removeCombatant() {
// remove correct row // remove correct row
let index = parseInt($("#confirm-modal").data("row")); let id = $("#confirm-modal").data("row-id");
$(".combatant-row").eq(index).remove(); $(".combatant-row#" + id).remove();
sortTable(); // update table
updateTable($("#combatants-table"));
// clean up // clean up
$("#confirm-modal").data("row", ""); $("#confirm-modal").data("row", "");
} }
@ -403,98 +421,94 @@ function startNewRound() {
} }
// reset ini values // reset ini values
$(".combatant-row").each(function () { $(".combatant-row").each(function () {
if ($(this).find(".combatant-dice").attr("data-combatant-dice") == "") { let $this = $(this);
$(this).attr("data-true-ini", 1); if ($this.find(".combatant-dice").attr("data-combatant-dice") == "") {
$this.attr("data-true-ini", 1);
} else { } else {
$(this).attr("data-true-ini", rollForInitiative(parseInt($(this).find(".combatant-dice").attr("data-combatant-dice")), parseInt($(this).find(".combatant-rea").attr("data-combatant-rea")))); $this.attr("data-true-ini", rollForInitiative(parseInt($this.find(".combatant-dice").attr("data-combatant-dice")), parseInt($this.find(".combatant-rea").attr("data-combatant-rea"))));
} }
}); });
// resort table // resort table
sortTable(); updateTable($("#combatants-table"));
} }
// sort combatants by ini value and add contextual classes // update combatants' table's effective inis, contextual classes, and order
function sortTable() { function updateTable(table) {
// do some clean up: remove previous classes from rows, remove effective ini and damage badges let $table = $(table),
$(".combatant-row").removeClass("ko-or-dead max-ini zero-ini"); //REGULAR_INI $rows = $table.find(".combatant-row");
$(".combatant-ini").empty(); // do some clean up: remove contextual classes, remove effective ini and damage badges
$rows.removeClass("ko-or-dead max-ini zero-ini");
$table.find(".combatant-ini").empty();
$table.find(".stun-badge .physical-badge").remove();
// disable all act buttons // disable all act buttons
$(".combatant-row").find(".act-button").prop("disabled", true).attr("aria-disabled", "true"); $table.find(".act-button").prop("disabled", true).attr("aria-disabled", "true");
// mark KO or death with class // mark KO/death with class
$(".combatant-row").each(function() { $rows.each(function(i) {
if (parseInt($(this).attr("data-damage-stun")) == 10 || parseInt($(this).attr("data-damage-physical")) == 10) { let $this = $(this);
$(this).addClass("ko-or-dead"); if (parseInt($this.attr("data-damage-stun")) == 10 || parseInt($this.attr("data-damage-physical")) == 10) {
$this.addClass("ko-or-dead");
} }
}); });
// compute highest effective ini // compute highest effective ini while writing effective ini to each row
let iniMax = Math.max.apply(null, $.map($(".combatant-row"), function (tr, i) { let iniMax = Math.max.apply(null, $.map($rows, (tr, i) => {
// write current effective ini to table row let $tr = $(tr);
$(tr).find(".combatant-ini").text($(tr).hasClass("ko-or-dead") ? 0 : getEffectiveIni($(tr))); $tr.find(".combatant-ini").text($tr.hasClass("ko-or-dead") ? 0 : getEffectiveIni($tr));
return $(tr).find(".combatant-ini").text(); return parseInt($tr.find(".combatant-ini").text());
})); }));
// add damage badges and contextual classes // iterate over rows to add damage badges and contextual classes
$(".combatant-row").each(function () { $rows.each(function(i) {
let $this = $(this);
// damage badges // damage badges
if ($(this).attr("data-damage-stun") && $(this).attr("data-damage-stun") != "0") { if ($this.attr("data-damage-stun") && $this.attr("data-damage-stun") != "0") {
$(this).find(".combatant-ini").append($.parseHTML(STUN_BADGE_HTML)); $this.find(".combatant-ini").append($STUN_BADGE_HTML.clone());
$(this).find(".stun-badge").append(DAMAGE_NIVEAU[DAMAGE_PENALTY[$(this).attr("data-damage-stun")]]); $this.find(".stun-badge").append(DAMAGE_NIVEAU[DAMAGE_PENALTY[$this.attr("data-damage-stun")]]);
} }
if ($(this).attr("data-damage-physical") && $(this).attr("data-damage-physical") != "0") { if ($this.attr("data-damage-physical") && $this.attr("data-damage-physical") != "0") {
$(this).find(".combatant-ini").append($.parseHTML(PHYSICAL_BADGE_HTML)); $this.find(".combatant-ini").append($PHYSICAL_BADGE_HTML.clone());
$(this).find(".physical-badge").append(DAMAGE_NIVEAU[DAMAGE_PENALTY[$(this).attr("data-damage-physical")]]); $this.find(".physical-badge").append(DAMAGE_NIVEAU[DAMAGE_PENALTY[$this.attr("data-damage-physical")]]);
} }
// K.O./dead -> do nothing // K.O./dead -> done
if ($(this).hasClass("ko-or-dead")) { if ($this.hasClass("ko-or-dead")) {
return true; return true;
} }
// ini = zero -> set contextual class // ini = zero -> set class and done
if (parseInt($(this).find(".combatant-ini").text()) == 0) { if (parseInt($this.find(".combatant-ini").text()) == 0) {
$(this).addClass("zero-ini"); $this.addClass("zero-ini");
return true; return true;
} }
// ini = max and non-zero -> enable act-button // ini = max and non-zero -> set class, enable act-button
if (parseInt($(this).find(".combatant-ini").text()) == iniMax && iniMax > 0) { if (parseInt($this.find(".combatant-ini").text()) == iniMax && iniMax > 0) {
$(this).addClass("max-ini").find(".act-button").prop("disabled", false).removeAttr("aria-disabled"); $this.addClass("max-ini").find(".act-button").prop("disabled", false).removeAttr("aria-disabled");
return true; return true;
} }
}) })
// sort rows and append them in new order // sort rows and append them in new order
let $rows = $(".combatant-row").toArray().sort(whoGoesFirst); rows = $rows.toArray().sort(whoGoesFirst);
for (let i = 0; i < $rows.length; i++) { for (let i = 0; i < rows.length; i++) {
$("#combatants-table").append($rows[i]); $table.append($(rows[i]).css("z-index", 50-i));
$($rows[i]).css("z-index", 50-i).css("position", "relative");
} }
return; return $table;
} }
// validate a combatant row form by checking for all conditions, including regular HTML5 validation // validate a combatant row form by checking for all conditions, including regular HTML5 validation
function validateCombatant() { function validateCombatant(form) {
// do standard HTML5 form validation first // do standard HTML5 form validation first
// (makes sure that name is not empty and that all other values are numbers within their individual ranges) // (makes sure that name is not empty and that all other values are numbers within their individual ranges)
if ( ! $("#combatant-form").get(0).reportValidity() ) { if ( ! form.reportValidity() ) {
return false; return false;
} }
// get input elements // get input values
let inputElements = { let ini = $(form).find("#combatant-modal-ini").get(0).value.trim(),
name: $("#combatant-modal-name").get(0), dice = $(form).find("#combatant-modal-dice").get(0).value.trim(),
ini: $("#combatant-modal-ini").get(0), rea = $(form).find("#combatant-modal-rea").get(0).value.trim();
dice: $("#combatant-modal-dice").get(0),
rea: $("#combatant-modal-rea").get(0)
};
// 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 // invalidate if ini, dice and rea are all empty
if (ini == "" && (dice == "" || rea == "")) { if (ini == "" && (dice == "" || rea == "")) {
inputElements["ini"].setCustomValidity("Values required for either initiative, or dice and reaction, or all three"); $(form).find("#combatant-modal-ini").get(0).setCustomValidity("Values required for either initiative, or dice and reaction, or all three");
inputElements["ini"].reportValidity();
return false; return false;
} }
// invalidate if dice or rea is empty but not both // invalidate if dice or rea is empty but not both
if ((dice == "") != (rea == "")) { if ((dice == "") != (rea == "")) {
inputElements["dice"].setCustomValidity("Values required for both dice and reaction, or none (in which case ini is required)"); $(form).find("#combatant-modal-ini").get(0).setCustomValidity("Values required for both dice and reaction, or none (in which case ini is required)");
inputElements["dice"].reportValidity();
return false; return false;
} }
// ok then // ok then
@ -514,16 +528,10 @@ $(document).ready(function () {
$("#combatant-modal-add-apply-button").on("click", handleCombatantModalAddApplyButtonClick); $("#combatant-modal-add-apply-button").on("click", handleCombatantModalAddApplyButtonClick);
$("#combatant-modal-add-ok-button").on("click", handleCombatantModalAddOkButtonClick); $("#combatant-modal-add-ok-button").on("click", handleCombatantModalAddOkButtonClick);
$("#combatant-modal-edit-ok-button").on("click", handleCombatantModalEditOkButtonClick); $("#combatant-modal-edit-ok-button").on("click", handleCombatantModalEditOkButtonClick);
$("#confirm-modal-new-round-ok-button").on("click", () => { $("#confirm-modal-new-round-ok-button").on("click", startNewRound);
startNewRound(); $("#confirm-modal-remove-combatant-ok-button").on("click", removeCombatant);
}); // add event handler removing custom validity messages in combatant modal
$("#confirm-modal-remove-combatant-ok-button").on("click", () => { $("#combatant-modal-dice #combatant-modal-rea #combatant-modal-ini").on("input", (event) => event.target.setCustomValidity(""));
removeCombatant();
});
// add event handler to certain input elements that removes any custom validity message
$("#combatant-modal-dice #combatant-modal-rea #combatant-modal-ini").on("input", (event) => {
event.target.setCustomValidity("");
});
// add event listeners to damage sliders in combatant modal // add event listeners to damage sliders in combatant modal
$("#combatant-modal-stun").on("input change", () => { $("#combatant-modal-stun").on("input change", () => {
if ($("#combatant-modal-stun").val() == "10") { if ($("#combatant-modal-stun").val() == "10") {
@ -544,9 +552,10 @@ $(document).ready(function () {
} }
}); });
// always focus name input field when combatant modal appears // always focus name input field when combatant modal appears
$('#combatant-modal').on('shown.bs.modal', () => $('#combatant-modal-name').focus()); // (need to use vanilla JS b/c jQuery can't seem to attach event handler correctly)
document.getElementById('combatant-modal').addEventListener('shown.bs.modal', () => $('#combatant-modal-name').focus());
// always empty input fields when combatant modal disappears // always empty input fields when combatant modal disappears
$("#combatant-modal").on('hidden.bs.modal', () => $("input[id*='combatant-modal']").val("")); document.getElementById('combatant-modal').addEventListener('hidden.bs.modal', () => $("input[id*='combatant-modal']").val(""));
// Hide damage monitors and actions menus after click somewhere else // Hide damage monitors and actions menus after click somewhere else
$("html").on("click", (e) => { $("html").on("click", (e) => {
if ($(e.target).parents(".damage-monitor").length == 0) { if ($(e.target).parents(".damage-monitor").length == 0) {
@ -556,7 +565,16 @@ $(document).ready(function () {
$(".actions-menu.seen").removeClass("seen"); $(".actions-menu.seen").removeClass("seen");
} }
}); });
addTestCombatant(); // add test combatants when title is held for one second
// thx to stackoverflow user Šime Vidas (https://stackoverflow.com/a/6091129)
$("#navbar-title").mousedown(function(e) {
clearTimeout(this.downTimer);
this.downTimer = setTimeout(function() {
addTestCombatants();
}, 1000);
}).mouseup(function(e) {
clearTimeout(this.downTimer);
});
}); });
module.exports = { rollForInitiative, validateCombatant, whoGoesFirst, getEffectiveIni }; module.exports = { rollForInitiative, validateCombatant, whoGoesFirst, getEffectiveIni };

22
src/site.webmanifest Normal file
View File

@ -0,0 +1,22 @@
{
"name": "sr2ini",
"short_name": "sr2ini",
"description": "A simple Initiative tracker for Shadowrun 2e",
"start_url": ".",
"icons": [
{
"src": "../icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "../icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "gold",
"background_color": "#004aa5",
"display": "standalone",
"orientstion": "portrait"
}