Compare commits

...

11 Commits
0.9.3 ... main

11 changed files with 1276 additions and 1271 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ hint-report/
.csslintrc
coverage/
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.

75
TODO.md
View File

@ -118,47 +118,64 @@
- x color scheme beim Favicon anpassen
- https://realfavicongenerator.net/
- 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
- favicon checken: https://realfavicongenerator.net/favicon_checker
- Links zu den versch. favicons fkt. nicht: sie lauten /… statt /sr2ini/…
- Lösung: parcel serve / parcel build braucht als --public-url "./" (statt "/"), dann werden die Links korrekt erzeugt
- neues Problem: dann fkt. parcel serve aber nicht mehr; statt CSS/JS/favicon-Files wird jedes Mal index.html serviert
- bekanntes Problem: https://github.com/parcel-bundler/parcel/issues/857
- kein Workaround fkt für mich
- was fkt: 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
- focus trapping im modal fkt. nur rückwärts (shift-tab), aber nicht vorwärts
- 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
- comments with general info in source files?
- unter die GPL stellen
- https://www.gnu.org/licenses/gpl-howto.html
- sr2ini_ynh auf neue Version bringen
- Screenshot ersetzen
- tarball ersetzen
- 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)
- focus-related stuff
- enter key doesn't work right away after clicking add button
- 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
- focus trapping im modal fkt. nur rückwärts (shift-tab), aber nicht vorwärts
- zumindest im Chrome; FF ungetestet
- 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?
## Feature Requests
@ -169,8 +186,6 @@
- sr2ini.js: test combatant
- git: user.name, user.email
- git commits, ggf. tags
- Seite als Web App auf FF4And installable machen
- mal sehen …
- Animationen? Transitions?
- deployment: dist/* soll direkt auf hermes hochgeladen werden
- HTML soll nicht in eine Zeile umgedingst werden, das sieht doch nicht aus
@ -181,7 +196,6 @@
- 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: Anzeige, wieviele Aktionen einer hat u.d wieviele davon schon verbraucht sind
- Sache mit dem ServiceWorker mal richtig angehen
- x progressive web app
- x Service Worker einrichten, um die Dateien lokal zu cachen
@ -222,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 CSS aufräumen
- 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": "A simple Initiative tracker for Shadowrun 2e",
"short_name": "sr2ini",
"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",
"version": "0.9.3",
"version": "0.9.5",
"description": "Simple Initiative tracker for Shadowrun 2e",
"private": true,
"author": {
"name": "Eclipse729",
"name": "Eclipse",
"email": "eclipse@unterdemradar.de",
"url": "https://git.unterdemradar.de"
"url": "https://git.unterdemradar.de/eclipse"
},
"homepage": "https://unterdemradar.de/sr2ini/",
"license": "ISC",
@ -26,13 +26,13 @@
"scripts": {
"start": "npx parcel serve src/index.html --public-url / --dist-dir 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",
"webhint": "hint http://localhost:1234"
},
"repository": {
"type": "git",
"url": "git@git.unterdemradar.de:tobias/sr2ini.git"
"url": "git@git.unterdemradar.de:eclipse/sr2ini.git"
},
"keywords": [
"Shadowrun",
@ -52,10 +52,5 @@
"augmented-ui": "^2.0.0",
"bootstrap": "^5.2.3",
"jquery": "^3.6.3"
},
"comments": {
"dependencies": {
"@parcel/service-worker": "^2.8.3"
}
}
}

View File

@ -260,6 +260,8 @@ header.navbar {
--aug-border-bottom: 0px;
--aug-border-right: 0px;
position: relative;
}
tr:last-of-type td,

View File

@ -14,13 +14,14 @@
<script type="module" src="js/sr2ini.js"></script>
<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="16x16" href="/icons/favicon-16x16.png">
<link rel="manifest" href="/icons/sr2ini.iconmanifest">
<link rel="shortcut icon" href="/icons/favicon.ico">
<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="16x16" href="../icons/favicon-16x16.png">
<link rel="manifest" href="site.webmanifest">
<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-config" content="/icons/browserconfig.xml">
<meta name="msapplication-config" content="../icons/browserconfig.xml">
<meta name="theme-color" content="gold">
</head>
@ -28,7 +29,7 @@
<!-- navbar -->
<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">
<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">
<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>
@ -69,8 +70,8 @@
</div>
<div class="modal-footer" data-augmented-ui="inlay">
<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="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" id="confirm-modal-new-round-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>
@ -84,8 +85,8 @@
<h2 class="modal-title">Add New Combatant</h2>
<button type="button" class="sr2-button" data-bs-dismiss="modal" aria-label="Close">&#10006;</button>
</div>
<div class="modal-body" data-augmented-ui="inlay">
<form id="combatant-form" name="combatant-modal-form" class="was-validated" onsubmit="return false;">
<div class="modal-body" data-augmented-ui="inlay">
<div>
<input type="text" maxlength="40" class="form-control form-control-sm" id="combatant-modal-name" form="combatant-form" placeholder="Name" required>
</div>
@ -107,14 +108,14 @@
<input type="range" class="form-range" min="0" max="10" value="0" id="combatant-modal-physical" list="damage-level">
</div>
</div>
</form>
</div>
<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="submit" 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="submit" class="sr2-button display-none" id="combatant-modal-edit-ok-button" >OK</button>
<button type="button" class="sr2-button" id="combatant-modal-add-apply-button">Apply</button>
<button type="button" class="sr2-button" id="combatant-modal-add-ok-button" data-bs-dismiss="modal">OK</button>
<button type="button" class="sr2-button display-none" id="combatant-modal-edit-ok-button" data-bs-dismiss="modal">OK</button>
</div>
</form>
</div>
</div>
</div>
@ -189,7 +190,7 @@
<!-- footer -->
<div class="footer-container container">
<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 Eclipse729 | 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>
</div>
</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
*/
const bs = require("../../node_modules/bootstrap/js/dist/modal.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_NIVEAU = ["", "L", "M", "S", "D"];
const COMBATANT_TABLE_ROW = [
const $COMBATANT_TABLE_ROW = $($.parseHTML([
'<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-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 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',
'</div>\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',
'<div class="damage-monitor" data-augmented-ui="tl-scoop bl-clip-y tr-clip-y br-scoop both">\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" 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" 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',
'</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 PHYSICAL_BADGE_HTML = '<sub><span class="badge bg-danger translate-middle physical-badge" title="Physical damage niveau"></span></sub>';
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 = $($.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
function rollForInitiative(dice, 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)
function getEffectiveIni(tr) {
let $tr = $(tr);
// return 0 if combatant is K.O. or dead
if ($(tr).hasClass("ko-or-dead")) {
return 0;
if ($tr.hasClass("ko-or-dead")) {
return -1;
}
// 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);
}
// add test combatant for testing purposes (duh)
function addTestCombatant() {
function getStatsFromCombatantModal() {
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
// $("#add-combatant-button").click();
$("#combatant-modal-name").val("Eclipse");
$("#combatant-modal-dice").val(3);
$("#combatant-modal-rea").val(6);
// $("#combatant-modal-ini").val(12);
addCombatant();
// setTimeout( () => $("#combatant-modal-add-ok-button").click(), 500);
$("#combatant-modal-dice").val("3");
$("#combatant-modal-rea").val("6");
$("#combatant-modal-stun").val("0");
$("#combatant-modal-physical").val("0");
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
*/
@ -140,10 +166,9 @@ function addTestCombatant() {
function handleActButtonClick(event) {
// 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);
// set new ini value
$(event.target).parents(".combatant-row").attr("data-true-ini", ini);
// resort table
sortTable();
updateTable($("#combatants-table"));
}
// click handler for add button
@ -152,15 +177,16 @@ function handleAddButtonClick(event) {
$("#combatant-modal .modal-title").text("Add Combatant");
$("#combatant-modal-add-ok-button, #combatant-modal-add-apply-button").removeClass("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-dice").val("2");
$("#combatant-modal-rea").val("7");
// set damage sliders to zero
$("#combatant-modal-stun, #combatant-modal-physical").val("0");
// add handler for enter key
$("#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) {
handleCombatantModalAddOkButtonClick(e);
}
@ -195,25 +221,55 @@ function handleCloneButtonClick(event) {
// click handler for combatant modal OK button (add mode)
function handleCombatantModalAddOkButtonClick(event) {
if (validateCombatant()) {
bs.getInstance($("#combatant-modal")).hide();
addCombatant();
// validate
if ( ! validateCombatant($("#combatant-form").get(0))) {
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)
function handleCombatantModalAddApplyButtonClick(event) {
if (validateCombatant()) {
addCombatant();
// validate
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)
function handleCombatantModalEditOkButtonClick(event) {
if (validateCombatant()) {
bs.getInstance($("#combatant-modal")).hide();
editCombatant();
// validate
if ( ! validateCombatant($("#combatant-form").get(0)) ) {
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
@ -222,7 +278,7 @@ function handleDamageButtonClick(event) {
let seenAtClick = $(event.target).parents(".damage-dropdown").find(".damage-monitor").hasClass("seen");
// hide all damage monitors and actions menus
$(".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) {
$(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
function handleDamageLevelClick(event) {
// find 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
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);
// 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
$btn.toggleClass("active");
$btn.parent().parent().prevAll().find("button.damage-" + damageType).removeClass("active");
$btn.parent().parent().nextAll().find("button.damage-" + damageType + ":not(.active)").addClass("active");
sortTable();
$btn.parent().parent().prevAll().find(".damage-" + damageType).removeClass("active");
$btn.parent().parent().nextAll().find(".damage-" + damageType).addClass("active");
updateTable($("#combatants-table"));
$btn.focus();
}
// click handler for edit buttons
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");
$("#combatant-modal").data("row-id", $tr.attr("id"));
// restyle modal
$("#combatant-modal .modal-title").text("Edit Combatant");
$("#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-stun").val($tr.attr("data-damage-stun") || "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
$("#combatant-modal input[id*='combatant-modal']").off("keydown");
$("#combatant-modal input[id*='combatant-modal']").on("keydown", (e) => {
@ -277,14 +335,12 @@ function handleEditButtonClick(event) {
function handleMoreActionsButtonClick(event) {
// get visibility status at click time
let seenAtClick = $(event.target).parents(".actions-dropdown").find(".actions-menu").hasClass("seen");
// hide all damage monitors
$(".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) {
$(event.target).parents(".actions-dropdown").find(".actions-menu").addClass("seen").find("button").attr("tabindex", "0");
}
return false;
}
@ -306,12 +362,13 @@ function handleNewRoundButtonClick(event) {
// click handler for remove buttons
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
$("#confirm-modal .modal-title").text("Remove Combatant");
$("#confirm-modal-remove-combatant-ok-button").removeClass("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
$("#confirm-modal").off("keydown");
$("#confirm-modal").on("keydown", (e) => {
@ -327,70 +384,31 @@ function handleRemoveButtonClick(event) {
* 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 editCombatant() {
// get values
let name = $("#combatant-modal-name").val().trim();
let ini = $("#combatant-modal-ini").val().trim();
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
function updateCombatantTablerow(stats, $tr) {
// roll for initiative if necessary
let ini = (stats["ini"] == "") ? rollForInitiative(stats["dice"], stats["rea"]) : stats["ini"];
// populate table row with combatant stats
$tr.find(".combatant-name").text(stats["name"]);
$tr.attr("data-true-ini", ini);
$tr.find(".combatant-name").text(name);
$tr.find(".combatant-dice").attr("data-combatant-dice", dice);
$tr.find(".combatant-rea").attr("data-combatant-rea", rea);
$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");
// sort table
sortTable();
// clean up
$("#combatant-modal").data("row", "");
$tr.find(".combatant-dice").attr("data-combatant-dice", stats["dice"]);
$tr.find(".combatant-rea").attr("data-combatant-rea", stats["rea"]);
// set initial damage levels
$tr.attr("data-damage-stun", stats["stun"]);
$tr.find(".damage-stun").addClass("active").slice(0, parseInt(stats["stun"])).removeClass("active");
$tr.attr("data-damage-physical", stats["physical"]);
$tr.find(".damage-physical").addClass("active").slice(0, parseInt(stats["physical"])).removeClass("active");
// done
return $tr;
}
// remove combatant
function removeCombatant() {
// remove correct row
let index = parseInt($("#confirm-modal").data("row"));
$(".combatant-row").eq(index).remove();
sortTable();
let id = $("#confirm-modal").data("row-id");
$(".combatant-row#" + id).remove();
// update table
updateTable($("#combatants-table"));
// clean up
$("#confirm-modal").data("row", "");
}
@ -403,98 +421,94 @@ function startNewRound() {
}
// reset ini values
$(".combatant-row").each(function () {
if ($(this).find(".combatant-dice").attr("data-combatant-dice") == "") {
$(this).attr("data-true-ini", 1);
let $this = $(this);
if ($this.find(".combatant-dice").attr("data-combatant-dice") == "") {
$this.attr("data-true-ini", 1);
} 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
sortTable();
updateTable($("#combatants-table"));
}
// sort combatants by ini value and add contextual classes
function sortTable() {
// do some clean up: remove previous classes from rows, remove effective ini and damage badges
$(".combatant-row").removeClass("ko-or-dead max-ini zero-ini"); //REGULAR_INI
$(".combatant-ini").empty();
// update combatants' table's effective inis, contextual classes, and order
function updateTable(table) {
let $table = $(table),
$rows = $table.find(".combatant-row");
// 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
$(".combatant-row").find(".act-button").prop("disabled", true).attr("aria-disabled", "true");
// mark KO or death with class
$(".combatant-row").each(function() {
if (parseInt($(this).attr("data-damage-stun")) == 10 || parseInt($(this).attr("data-damage-physical")) == 10) {
$(this).addClass("ko-or-dead");
$table.find(".act-button").prop("disabled", true).attr("aria-disabled", "true");
// mark KO/death with class
$rows.each(function(i) {
let $this = $(this);
if (parseInt($this.attr("data-damage-stun")) == 10 || parseInt($this.attr("data-damage-physical")) == 10) {
$this.addClass("ko-or-dead");
}
});
// compute highest effective ini
let iniMax = Math.max.apply(null, $.map($(".combatant-row"), function (tr, i) {
// write current effective ini to table row
$(tr).find(".combatant-ini").text($(tr).hasClass("ko-or-dead") ? 0 : getEffectiveIni($(tr)));
return $(tr).find(".combatant-ini").text();
// compute highest effective ini while writing effective ini to each row
let iniMax = Math.max.apply(null, $.map($rows, (tr, i) => {
let $tr = $(tr);
$tr.find(".combatant-ini").text($tr.hasClass("ko-or-dead") ? 0 : getEffectiveIni($tr));
return parseInt($tr.find(".combatant-ini").text());
}));
// add damage badges and contextual classes
$(".combatant-row").each(function () {
// iterate over rows to add damage badges and contextual classes
$rows.each(function(i) {
let $this = $(this);
// damage badges
if ($(this).attr("data-damage-stun") && $(this).attr("data-damage-stun") != "0") {
$(this).find(".combatant-ini").append($.parseHTML(STUN_BADGE_HTML));
$(this).find(".stun-badge").append(DAMAGE_NIVEAU[DAMAGE_PENALTY[$(this).attr("data-damage-stun")]]);
if ($this.attr("data-damage-stun") && $this.attr("data-damage-stun") != "0") {
$this.find(".combatant-ini").append($STUN_BADGE_HTML.clone());
$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") {
$(this).find(".combatant-ini").append($.parseHTML(PHYSICAL_BADGE_HTML));
$(this).find(".physical-badge").append(DAMAGE_NIVEAU[DAMAGE_PENALTY[$(this).attr("data-damage-physical")]]);
if ($this.attr("data-damage-physical") && $this.attr("data-damage-physical") != "0") {
$this.find(".combatant-ini").append($PHYSICAL_BADGE_HTML.clone());
$this.find(".physical-badge").append(DAMAGE_NIVEAU[DAMAGE_PENALTY[$this.attr("data-damage-physical")]]);
}
// K.O./dead -> do nothing
if ($(this).hasClass("ko-or-dead")) {
// K.O./dead -> done
if ($this.hasClass("ko-or-dead")) {
return true;
}
// ini = zero -> set contextual class
if (parseInt($(this).find(".combatant-ini").text()) == 0) {
$(this).addClass("zero-ini");
// ini = zero -> set class and done
if (parseInt($this.find(".combatant-ini").text()) == 0) {
$this.addClass("zero-ini");
return true;
}
// ini = max and non-zero -> enable act-button
if (parseInt($(this).find(".combatant-ini").text()) == iniMax && iniMax > 0) {
$(this).addClass("max-ini").find(".act-button").prop("disabled", false).removeAttr("aria-disabled");
// ini = max and non-zero -> set class, enable act-button
if (parseInt($this.find(".combatant-ini").text()) == iniMax && iniMax > 0) {
$this.addClass("max-ini").find(".act-button").prop("disabled", false).removeAttr("aria-disabled");
return true;
}
})
// sort rows and append them in new order
let $rows = $(".combatant-row").toArray().sort(whoGoesFirst);
for (let i = 0; i < $rows.length; i++) {
$("#combatants-table").append($rows[i]);
$($rows[i]).css("z-index", 50-i).css("position", "relative");
rows = $rows.toArray().sort(whoGoesFirst);
for (let i = 0; i < rows.length; i++) {
$table.append($(rows[i]).css("z-index", 50-i));
}
return;
return $table;
}
// 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
// (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;
}
// get input elements
let inputElements = {
name: $("#combatant-modal-name").get(0),
ini: $("#combatant-modal-ini").get(0),
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();
// get input values
let ini = $(form).find("#combatant-modal-ini").get(0).value.trim(),
dice = $(form).find("#combatant-modal-dice").get(0).value.trim(),
rea = $(form).find("#combatant-modal-rea").get(0).value.trim();
// invalidate if ini, dice and rea are all empty
if (ini == "" && (dice == "" || rea == "")) {
inputElements["ini"].setCustomValidity("Values required for either initiative, or dice and reaction, or all three");
inputElements["ini"].reportValidity();
$(form).find("#combatant-modal-ini").get(0).setCustomValidity("Values required for either initiative, or dice and reaction, or all three");
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();
$(form).find("#combatant-modal-ini").get(0).setCustomValidity("Values required for both dice and reaction, or none (in which case ini is required)");
return false;
}
// ok then
@ -514,16 +528,10 @@ $(document).ready(function () {
$("#combatant-modal-add-apply-button").on("click", handleCombatantModalAddApplyButtonClick);
$("#combatant-modal-add-ok-button").on("click", handleCombatantModalAddOkButtonClick);
$("#combatant-modal-edit-ok-button").on("click", handleCombatantModalEditOkButtonClick);
$("#confirm-modal-new-round-ok-button").on("click", () => {
startNewRound();
});
$("#confirm-modal-remove-combatant-ok-button").on("click", () => {
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("");
});
$("#confirm-modal-new-round-ok-button").on("click", startNewRound);
$("#confirm-modal-remove-combatant-ok-button").on("click", removeCombatant);
// add event handler removing custom validity messages in combatant modal
$("#combatant-modal-dice #combatant-modal-rea #combatant-modal-ini").on("input", (event) => event.target.setCustomValidity(""));
// add event listeners to damage sliders in combatant modal
$("#combatant-modal-stun").on("input change", () => {
if ($("#combatant-modal-stun").val() == "10") {
@ -544,9 +552,10 @@ $(document).ready(function () {
}
});
// 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
$("#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
$("html").on("click", (e) => {
if ($(e.target).parents(".damage-monitor").length == 0) {
@ -556,7 +565,16 @@ $(document).ready(function () {
$(".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 };

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"
}