1598 lines
79 KiB
HTML
1598 lines
79 KiB
HTML
<!doctype html>
|
|
<!-- Site website is underl GPL-v3 license, please refer to https://git.sceptique.eu/Sceptique/rpg-sheet/src/branch/master/LICENSE -->
|
|
<html lang="fr" data-bs-theme="dark">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
<title>Rpg Sheet</title>
|
|
</head>
|
|
<body>
|
|
<div class="container-fluid">
|
|
<div class="row px-1">
|
|
<div class="col-12 gx-4">
|
|
<div class="alert alert-success slideSource fade" id="characterUpdateSuccess">
|
|
Sauvegarde locale du personnage réussie !
|
|
</div>
|
|
<!-- <div class="alert alert-danger slideSource fade" id="characterUpdateFailure">
|
|
Sauvegarde locale du personnage échouée !
|
|
</div> -->
|
|
|
|
<!-- Rolling Dice Modal -->
|
|
<div class="modal fade" id="roll_dice" tabindex="-1" aria-labelledby="roll_dice_label" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h1 class="modal-title fs-5" id="roll_dice_label">Lancer des dés</h1>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
...
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-primary" onclick=RollDice.execute(event)>Jeter les dés !</button>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="characters-list-part">
|
|
<div class="row">
|
|
<div class="accordion" id="characters_list">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<form onsubmit="return htmlNewCharacter(event)">
|
|
<div class="row">
|
|
<div class="col">
|
|
<div class="mb-3">
|
|
<label for="characters_list_new" class="form-text">Nom</label>
|
|
<input type="text" class="form-control" id="characters_list_new" />
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<button type="submit" class="btn btn-primary">Ajouter un nouveau personnage</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div><!-- col -->
|
|
|
|
<div class="col-12">
|
|
<div id="logs-part">
|
|
<h1>Journal des dés</h1>
|
|
<button onclick="reloadLogs(event, 20)">Reload last 20</button>
|
|
<div id="logs-entries"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<div id="discord-part">
|
|
<h1>Discord</h1>
|
|
<label for="discord_host">Nom de domaine</label>
|
|
<input id="discord_host" class="form-control" value="" />
|
|
<label for="discord_password">Mot de passe partagé</label>
|
|
<input id="discord_password" class="form-control" value="" type="password" />
|
|
</div>
|
|
</div>
|
|
|
|
</div> <!-- row -->
|
|
|
|
<footer class="row">
|
|
<a href="https://git.sceptique.eu/Sceptique/rpg-sheet">source code repository</a>
|
|
</footer>
|
|
</div><!-- container -->
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
|
<style>
|
|
.add-speciality:hover, .add-inventory:hover, .add-spell:hover { color: green; }
|
|
.add-speciality:active, .add-inventory:active, .add-spell:hover { color: red; }
|
|
textarea.character-speciality { font-size: 11px; }
|
|
.form-floating > label { font-size: 11px; }
|
|
.form-floating > .form-control {
|
|
/* padding: 0.5rem !important; */
|
|
height: calc(3.0rem + calc(var(--bs-border-width) * 2));
|
|
min-height: calc(3.0rem + calc(var(--bs-border-width) * 2));
|
|
}
|
|
.form-floating > .form-control {
|
|
padding-top: 1.2rem !important;
|
|
padding-bottom: 0.625rem !important;
|
|
}
|
|
.slideSource {
|
|
opacity:1;
|
|
-webkit-transition: opacity 3s;
|
|
-moz-transition: opacity 3s;
|
|
transition: opacity 3s;
|
|
}
|
|
.slideSource.fade {
|
|
opacity:0;
|
|
}
|
|
#characterUpdateSuccess {
|
|
position: absolute;
|
|
top: 2rem;
|
|
z-index: 10;
|
|
font-size: x-large;
|
|
}
|
|
</style>
|
|
<script type="text/javascript">
|
|
|
|
function blankCharacter(store, options = {}) {
|
|
if (options.force || store.stats == undefined) store.stats = {};
|
|
if (options.force || store.spells == undefined) store.spells = [];
|
|
if (options.force || store.inventory == undefined) store.inventory = [];
|
|
if (options.force || store.equipments == undefined) store.equipments = {};
|
|
if (options.force || store.personnals == undefined) store.personnals = {};
|
|
if (options.force || store.states == undefined) store.states = {};
|
|
if (options.force || store.specialities == undefined) store.specialities = {};
|
|
if (options.force || store.experience == undefined) store.experience = {};
|
|
return store;
|
|
}
|
|
|
|
function createHtmlElem(parent, type_ = "div", attributes = {}, text = "") {
|
|
if (!parent) { throw `Invalid parent to create ${type_} ${JSON.stringify(attributes)} ${text}` }
|
|
const elem = document.createElement(type_);
|
|
elem.textContent = text;
|
|
Object.keys(attributes).forEach((attr) => {
|
|
elem.setAttribute(attr, attributes[attr]);
|
|
});
|
|
parent.append(elem);
|
|
return elem;
|
|
}
|
|
function sluggify(text) {
|
|
return text
|
|
.replaceAll("à", "a")
|
|
.replaceAll("â", "a")
|
|
.replaceAll("é", "e")
|
|
.replaceAll("è", "e")
|
|
.replaceAll("ê", "e")
|
|
.replaceAll("î", "i")
|
|
.replaceAll("ï", "i")
|
|
.replaceAll("À", "A")
|
|
.replaceAll("Â", "A")
|
|
.replaceAll("É", "E")
|
|
.replaceAll("È", "E")
|
|
.replaceAll("Ê", "E")
|
|
.replaceAll("Î", "I")
|
|
.replaceAll("Ï", "I")
|
|
.replaceAll(/[^\w]/g, "_")
|
|
.toLowerCase();
|
|
}
|
|
function capitalize(text) {
|
|
return text[0].toUpperCase() + text.slice(1).toLowerCase();
|
|
}
|
|
function getElementByXpath(path) {
|
|
return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
}
|
|
function stackTrace() {
|
|
const err = new Error();
|
|
return err.stack;
|
|
}
|
|
function checkHtmlErrors(html) {
|
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
const errorNode = doc.querySelector("parsererror");
|
|
if (errorNode) {
|
|
const error = `Error renderHtml: ${errorNode.textContent}\n${html}\n\n${stackTrace()}`;
|
|
throw error;
|
|
}
|
|
}
|
|
function renderHtml(root, html, options = {}) {
|
|
if (options.version == 1) {
|
|
root.innerHTML += html;
|
|
return null;
|
|
}
|
|
else if (options.version == 2) {
|
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
const errorNode = doc.querySelector("parsererror");
|
|
if (errorNode) {
|
|
const error = `Error renderHtml: ${errorNode.textContent}\n${html}\n\n${stackTrace()}`;
|
|
throw error;
|
|
}
|
|
root.append(doc.firstChild);
|
|
return doc;
|
|
}
|
|
else if (options.version == 3 || options.version == undefined) {
|
|
checkHtmlErrors(html);
|
|
const template = document.createElement("template"); // in fact it can be anything
|
|
template.innerHTML = html;
|
|
root.append(template.content);
|
|
return template.content;
|
|
}
|
|
}
|
|
function download(filename, text) {
|
|
var element = document.createElement('a');
|
|
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
|
element.setAttribute('download', filename);
|
|
element.style.display = 'none';
|
|
document.body.appendChild(element);
|
|
element.click();
|
|
document.body.removeChild(element);
|
|
}
|
|
|
|
/*
|
|
in the inputs:
|
|
- mandatory: name
|
|
- optional: id (by default slug of name)
|
|
- optional: type (by default "text")
|
|
- optional: row (each uniq row number will be a new layout line) (default 1)
|
|
- optional: colspan relative size to 12 (default 12)
|
|
- optional: sticky (if true, no new column) (default false)
|
|
- optional: class (to add)
|
|
- optionals: min, max, select
|
|
in the options
|
|
{ roll: [...] },
|
|
*/
|
|
function addHtmlFloatForm(parent, parent_id, inputs, options = {}) {
|
|
const current_char = new CurrentCharacter().data();
|
|
if (!inputs || inputs.length == 0) {
|
|
throw `Invalid float form for ${parent} with ${parent_id} and no inputs`;
|
|
}
|
|
inputs.forEach((input) => {
|
|
input.id ||= sluggify(input.name);
|
|
input.type ||= "text";
|
|
if (input.row == undefined) input.row = 1;
|
|
if (input.colspan == undefined) input.colspan = 12;
|
|
if (input.sticky == undefined) input.sticky = true;
|
|
input.class ||= "";
|
|
if (input.value == undefined) input.value = "";
|
|
if (input.html == undefined) input.html = "";
|
|
});
|
|
const sorted_inputs = inputs.sort((a, b) => a.row - b.row) // sort by row so we can split them
|
|
const inputs_by_row = Sort.groupBy(sorted_inputs, "row");
|
|
|
|
let current_row = 0;
|
|
Object.keys(inputs_by_row).forEach((row) => {
|
|
const inputs = inputs_by_row[row];
|
|
let current_col = 0;
|
|
current_row += 1;
|
|
const rows_html = inputs.map((input) => {
|
|
const group_size = `input-group-${input.size || "sm"}`;
|
|
current_col += 1;
|
|
|
|
let input_html = "";
|
|
if (input.type == "textarea") input_html = makeFloatFormV2Textarea(parent_id, input);
|
|
else if (input.type == "checkbox") input_html = makeFloatFormV2Checkbox(parent_id, input);
|
|
else if (input.type == "select") input_html = makeFloatFormV2Select(parent_id, input);
|
|
/* else if (input.type == "radio") input_html = makeFloatFormV2Radio(parent_id, input); */
|
|
else if (input.type == "button") input_html = makeFloatFormV2Button(parent_id, input);
|
|
else input_html = makeFloatFormV2Input(parent_id, input);
|
|
|
|
if (input.name != undefined) {
|
|
input_html = `<div class="form-floating">${input_html}</div>`;
|
|
}
|
|
|
|
if (current_col == 1 && current_row == 1 && options.roll) {
|
|
const roll_paths = (options.roll.paths || []).map(c => `"${c}"`).join(", ")
|
|
input_html = `
|
|
<span
|
|
class="input-group-text ${options.roll.class || ''}"
|
|
data-bs-toggle="modal" data-bs-target="#roll_dice"
|
|
onclick='RollDice.openForm(event, "${current_char.name}", [${roll_paths}], "${options.roll.reason || ''}")'
|
|
>
|
|
<i class="bi bi-dice-3"></i>
|
|
</span>
|
|
${input_html}
|
|
`;
|
|
}
|
|
if (current_col == 1 && options.refresh) {
|
|
input_html = `
|
|
<span
|
|
class="input-group-text ${options.refresh.class || ''}"
|
|
onclick='${options.refresh.onclick || ''}'
|
|
value-id='${input.id}'
|
|
>
|
|
<i class="bi bi-arrow-clockwise"></i>
|
|
</span>
|
|
${input_html}
|
|
`;
|
|
}
|
|
|
|
if (!input.sticky || options.roll || options.refresh) {
|
|
input_html = `<div class="input-group ${group_size} ${options.groups_class}">${input_html}</div>`;
|
|
}
|
|
input_html = `<div class="col-${input.colspan} gx-${input.sticky ? 0 : 3}">${input_html}</div>`;
|
|
return input_html;
|
|
});
|
|
|
|
const html = `<span><div class="row ${options.row_class || ""}">${rows_html.join("")}</div></span>`;
|
|
renderHtml(parent, html, { version: 3 })
|
|
});
|
|
}
|
|
|
|
function makeFloatFormV2Button(parent_id, input) {
|
|
const full_id = `form_field_${parent_id}_${input.id}`;
|
|
const size = `btn-${input.size || "sm"}`;
|
|
const input_html = `
|
|
<button id="${full_id}"
|
|
type="${input.type}"
|
|
class="floatformfield ${input.class} ${size}"
|
|
value-id="${input.id}"
|
|
value="${input.value}" ${input.html}
|
|
>${input.name || input.id}</button>
|
|
`;
|
|
return input_html;
|
|
}
|
|
function makeFloatFormV2Input(parent_id, input) {
|
|
const full_id = `form_field_${parent_id}_${input.id}`;
|
|
const size = `form-control-${input.size || "sm"}`;
|
|
const label_html = input.name ? `<label for="${full_id}">${input.name}</label>` : "";
|
|
let optional_html = "";
|
|
if (input.min) optional_html += ` min="${input.min}"`
|
|
if (input.max) optional_html += ` max="${input.max}"`
|
|
const input_html = `
|
|
<input id="${full_id}"
|
|
type="${input.type}"
|
|
class="floatformfield form-control ${input.class} ${size}"
|
|
value-id="${input.id}"
|
|
value="${input.value}" ${optional_html} ${input.html}
|
|
/>${label_html}
|
|
`;
|
|
return input_html;
|
|
}
|
|
function makeFloatFormV2Textarea(parent_id, input) {
|
|
const full_id = `form_field_${parent_id}_${input.id}`;
|
|
const size = `form-control-${input.size || "sm"}`;
|
|
const label_html = input.name ? `<label for="${full_id}">${input.name}</label>` : "";
|
|
return `
|
|
<textarea id="${full_id}"
|
|
class="floatformfield form-control ${input.class} ${size}"
|
|
value-id="${input.id}" ${input.html}
|
|
>${input.value}</textarea>${label_html}
|
|
`;
|
|
}
|
|
function makeFloatFormV2Checkbox(parent_id, input) {
|
|
const full_id = `form_field_${parent_id}_${input.id}`;
|
|
const label_html = input.name ? `<label for="${full_id}">${input.name}</label>` : "";
|
|
const input_html = `
|
|
<div class="form-check">
|
|
<input type="checkbox"
|
|
id="${full_id}"
|
|
class="floatformfield form-check-input ${input.class}"
|
|
value-id="${input.id}" ${input.html}
|
|
${input.value ? 'checked' : ''}
|
|
/>
|
|
${label_html}
|
|
</div>
|
|
`;
|
|
return input_html;
|
|
}
|
|
function makeFloatFormV2Select(parent_id, input) {
|
|
const full_id = `form_field_${parent_id}_${input.id}`;
|
|
const size = `form-select-${input.size || "sm"}`;
|
|
const label_html = input.name ? `<label for="${full_id}">${input.name}</label>` : "";
|
|
const options_html = Object.keys(input.options).map(option_key => `<option value="${input.options[option_key]}" ${input.options[option_key] == input.value ? 'selected' : ''}>${option_key}</option>`).join("");
|
|
const input_html = `
|
|
<select id="${full_id}"
|
|
class="floatformfield form-select ${input.class} ${size}"
|
|
value-id="${input.id}"
|
|
aria-label="${input.name}"
|
|
>
|
|
${options_html}
|
|
</select>${label_html}
|
|
`;
|
|
return input_html;
|
|
}
|
|
/* function makeFloatFormV2Radio(parent_id, input) {
|
|
* const full_id = `form_field_${parent_id}_${input.id}`;
|
|
* const size = `form-select-${input.size || "sm"}`;
|
|
* const label_html = input.name ? `<label for="${full_id}">${input.name}</label>` : "";
|
|
* const options_html = Object.keys(input.options).map((option_key) => {
|
|
* const option_html = `
|
|
* <input class="floatformfield form-check-input ${input.class}" type="radio" id="${full_id}_${option_key}" value="${option_key}" ${input.value == option_key ? "selected" : ""} /><label class="form-check-label" for="${full_id}_${option_key}">${input.options[option_key]}</label>
|
|
* `;
|
|
* return option_html;
|
|
* });
|
|
* const full_options_html = options_html.map(option_html => `<div class="form-check form-check-inline">${option_html}</div>`).join("");
|
|
* const input_html = `
|
|
<div class="row"><div class="col-4"><span>${input.name}</span></div><div class="col-4">${full_options_html}</div><div class="col-4"><button class="btn">Upgrade</button></div></div>
|
|
* `;
|
|
* return input_html
|
|
* } */
|
|
|
|
class Sort {
|
|
static groupBy(list, callback) {
|
|
return list.reduce((base, item) => {
|
|
if (callback.__proto__.isPrototypeOf(Function)) {
|
|
const key = callback(item);
|
|
base[key] ||= [];
|
|
base[key].push(item);
|
|
} else {
|
|
const key = item[callback];
|
|
base[key] ||= [];
|
|
base[key].push(item);
|
|
}
|
|
return base;
|
|
}, {});
|
|
}
|
|
|
|
static sortBy(list, callback) {
|
|
return list.sort((a, b) => {
|
|
const ca = callback(a);
|
|
const cb = callback(b);
|
|
if (ca == cb) return 0;
|
|
if (ca > cb) return 1;
|
|
return -1;
|
|
});
|
|
}
|
|
|
|
static toggle(event, parent_selector, lines_selector, cell_selector, value) {
|
|
const parent = document.querySelector(parent_selector);
|
|
const lines = [];
|
|
parent.querySelectorAll(lines_selector).forEach((line) => lines.push(line));
|
|
const sorted_lines = Sort.sortBy(lines, (line) => {
|
|
const input = line.querySelector(cell_selector);
|
|
if (input == undefined) return null;
|
|
if (input.type == "checkbox") return input.checked;
|
|
return input.value;
|
|
});
|
|
const sort_order = lines.length > 0 ? lines[0].getAttribute("sort-order") : "desc";
|
|
if (sort_order == "asc") sorted_lines.reverse();
|
|
sorted_lines.forEach((line) => {
|
|
line.setAttribute("sort-order", sort_order == "asc" ? "desc" : "asc");
|
|
parent.removeChild(line);
|
|
parent.appendChild(line);
|
|
});
|
|
return sorted_lines;
|
|
}
|
|
}
|
|
|
|
/* access to local storage data with some error/default values management and serialization */
|
|
class DataAPI {
|
|
constructor(storage) { this.storage = storage; }
|
|
addDiscordInfos(host, password) {
|
|
this.storage.setItem("discord.host", host);
|
|
this.storage.setItem("discord.password", password);
|
|
}
|
|
getDiscordInfos() {
|
|
return {
|
|
host: this.storage.getItem("discord.host") || "",
|
|
password: this.storage.getItem("discord.password") || "",
|
|
};
|
|
}
|
|
addPortrait(name, b64) {
|
|
this.storage.setItem(`portraits.${name}`, b64);
|
|
}
|
|
getPortrait(name) {
|
|
return this.storage.getItem(`portraits.${name}`);
|
|
}
|
|
addLog(log) {
|
|
this.storage.setItem(log.key, JSON.stringify(log));
|
|
const keys = JSON.parse(this.storage.getItem("logs.keys") || "[]");
|
|
keys.push(log.key);
|
|
this.storage.setItem("logs.keys", JSON.stringify(keys));
|
|
}
|
|
getLastLogs(last = 20) {
|
|
const keys = JSON.parse(this.storage.getItem("logs.keys") || "[]");
|
|
const last_keys = keys.slice(0, last - 1);
|
|
return last_keys.map((key) => JSON.parse(this.storage.getItem(key)));
|
|
}
|
|
getCharacter(name) {
|
|
const list = this.getCharactersList();
|
|
const character = list[name];
|
|
if (character == undefined) {
|
|
alert("Erreur: personnage non-trouvé. Rafraichir la page ou contacter Sceptique.");
|
|
return null;
|
|
}
|
|
blankCharacter(character, { force: false });
|
|
return character;
|
|
}
|
|
addCharacter(name) {
|
|
const list = this.getCharactersList();
|
|
if (!list[name]) {
|
|
list[name] = { name: name };
|
|
this.setCharactersList(list);
|
|
return list[name];
|
|
} else {
|
|
throw `Invalid character already exists ${name}`;
|
|
}
|
|
}
|
|
updateCharacter(name, data) {
|
|
const list = this.getCharactersList();
|
|
if (list[name]) {
|
|
list[name] = data;
|
|
this.setCharactersList(list);
|
|
return list[name];
|
|
} else {
|
|
throw `Invalid character "${name}" do not exists`;
|
|
}
|
|
}
|
|
getCharactersList() {
|
|
try {
|
|
const data = JSON.parse(this.storage.getItem("characters_list") || "{}");
|
|
return data;
|
|
}
|
|
catch (error) {
|
|
console.log("invalid storage, reset")
|
|
return {};
|
|
}
|
|
}
|
|
setCharactersList(data) {
|
|
return this.storage.setItem("characters_list", JSON.stringify(data));
|
|
}
|
|
removeCharacter(name) {
|
|
const data = this.getCharactersList();
|
|
delete data[name];
|
|
return this.storage.setItem("characters_list", JSON.stringify(data));
|
|
}
|
|
}
|
|
|
|
/* reads where the character is in html and the local storage data link */
|
|
class CurrentCharacter {
|
|
constructor() {
|
|
}
|
|
isSelected() {
|
|
if (this._is_selected == undefined)
|
|
this._is_selected = document.getElementsByClassName("accordion-collapse show")[0] != undefined;
|
|
return this._is_selected;
|
|
}
|
|
htmlRoot() {
|
|
if (!this.isSelected()) return console.log("No character selected, no root selection");
|
|
this._html_root ||= document.getElementsByClassName("accordion-collapse show")[0];
|
|
return this._html_root;
|
|
}
|
|
name() {
|
|
if (!this.isSelected()) return console.log("No character selected, no name");
|
|
this._name ||= this.htmlRoot().attributes["character-name"].value;
|
|
return this._name;
|
|
}
|
|
data() {
|
|
if (!this.isSelected()) return console.log("No character selected, no data");
|
|
this._data ||= api.getCharacter(this.name());
|
|
return this._data;
|
|
}
|
|
recordUpdate(event) {
|
|
if (!this.isSelected()) return console.log("No character selected, no update");
|
|
const character = this.data();
|
|
console.log("update record for", character.name);
|
|
blankCharacter(character, { force: true });
|
|
updateStoreWithFields(
|
|
character, // TODO: use character.stats
|
|
this.htmlRoot().querySelectorAll(".character-value"),
|
|
);
|
|
// V2 done
|
|
updateStoreWithFields(
|
|
character.stats,
|
|
this.htmlRoot().querySelectorAll(".character-stats .character-stat"),
|
|
);
|
|
// V2 done
|
|
updateStoreWithFields(
|
|
character.states,
|
|
this.htmlRoot().querySelectorAll(".character-states .character-state"),
|
|
);
|
|
// V2 done
|
|
updateStoreWithFields(
|
|
character.personnals,
|
|
this.htmlRoot().querySelectorAll(".character-personnals .character-personnal"),
|
|
);
|
|
// V2 done
|
|
this.htmlRoot().querySelectorAll(".character-speciality-line").forEach((current_elem) => {
|
|
const inputs = current_elem.querySelectorAll(".character-speciality");
|
|
const speciality = parseMultiFields(inputs);
|
|
character.specialities[speciality.name] = speciality;
|
|
});
|
|
// V2 done
|
|
updateStoreWithFields(
|
|
character.equipments,
|
|
this.genericGetInputs(".character-equipments input"),
|
|
);
|
|
// V2 done
|
|
document.querySelectorAll(".accordion-collapse.show .character-inventory .inventory-line").forEach((inventory_line) => {
|
|
const inventory_item = parseMultiFields(
|
|
inventory_line.querySelectorAll("input"),
|
|
inventory_line.querySelectorAll("textarea"),
|
|
inventory_line.querySelectorAll("select"),
|
|
);
|
|
character.inventory.push(inventory_item);
|
|
});
|
|
// V2 done
|
|
document.querySelectorAll(".accordion-collapse.show .character-spells .character-spell").forEach((line) => {
|
|
const spell = parseMultiFields(line.querySelectorAll("input, textarea"));
|
|
character.spells.push(spell);
|
|
});
|
|
// V2 done
|
|
updateStoreWithFields(
|
|
character.experience,
|
|
document.querySelectorAll(".accordion-collapse.show .character-experience .character-xp"),
|
|
);
|
|
api.updateCharacter(character.name, character);
|
|
|
|
const alert = document.getElementById("characterUpdateSuccess");
|
|
alert.classList.toggle('fade');
|
|
setTimeout(() => {
|
|
alert.classList.toggle('fade');
|
|
}, 2000);
|
|
return character;
|
|
}
|
|
|
|
increaseStat(key, value) {
|
|
const input = this.genericGetInput(".character-stats .character-stat", key);
|
|
input.value = input.valueAsNumber + value;
|
|
return input;
|
|
}
|
|
increaseSpe(key, value) {
|
|
const input = this.htmlRoot().querySelector(`.character-speciality[value-id=name][value="${key}"]`).parentElement.parentElement.parentElement.querySelector(".character-speciality[value-id=level]")
|
|
input.value = input.valueAsNumber + value;
|
|
return input;
|
|
}
|
|
increaseExperience(key, value) {
|
|
const input = this.genericGetInput(".character-experience .character-xp", key);
|
|
if (input.valueAsNumber < 3) {
|
|
input.value = input.valueAsNumber + value;
|
|
}
|
|
return input;
|
|
}
|
|
setExperience(key, value) {
|
|
const input = this.genericGetInput(".character-experience .character-xp", key);
|
|
input.value = value;
|
|
return input;
|
|
}
|
|
getExperience(key, value) {
|
|
const inputs = Array.fromDom(this.htmlRoot().querySelectorAll(".character-experience .character-xp"));
|
|
return inputs.reduce((base, input) => {
|
|
base[input.getAttribute("value-id")] = input.valueAsNumber || 0;
|
|
return base;
|
|
}, {});
|
|
}
|
|
genericGetInputs(inputs_selector) {
|
|
return Array.fromDom(this.htmlRoot().querySelectorAll(inputs_selector));
|
|
}
|
|
genericGetInput(inputs_selector, key) {
|
|
return this.genericGetInputs(inputs_selector).find(e => e.getAttribute("value-id") == key);
|
|
}
|
|
}
|
|
|
|
Array.fromDom = function (dom) {
|
|
const arr = [];
|
|
dom.forEach(e => arr.push(e));
|
|
return arr;
|
|
};
|
|
|
|
class CharactersList {
|
|
constructor(api) { this.api = api; }
|
|
|
|
addHtml(characters_list_elem, display_pointer = undefined) {
|
|
let first_shown = false;
|
|
const names = Object.keys(this.api.getCharactersList());
|
|
characters_list_elem.innerHTML = "";
|
|
names.forEach((name) => {
|
|
const slug = sluggify(name);
|
|
/* <div class="accordion-item">
|
|
* <h2 class="accordion-header">
|
|
* <button class="accordion-button" type="button" data-bs-toggle="collapse" "data-bs-target"=#character_${slug} "aria-controls"=${slug}>${name}</button>
|
|
* </h2>
|
|
* <div id="character_${slug}" character-name=${name} class="accordion-collapse collapse" data-bs-parent="#characters_list">
|
|
* <div class="accordion-body">
|
|
* </div>
|
|
* </div>
|
|
* </div>
|
|
*/
|
|
let show = "";
|
|
let collapsed = "collapsed";
|
|
if (!first_shown && display_pointer == undefined) {
|
|
first_shown = true;
|
|
show = "show";
|
|
collapsed = "";
|
|
} else if (display_pointer == name) {
|
|
first_shown = true;
|
|
show = "show";
|
|
collapsed = "";
|
|
}
|
|
// build html dom item by item because we need to return one of the leaf
|
|
const character_elem = createHtmlElem(
|
|
characters_list_elem,
|
|
"div",
|
|
{ class: "accordion-item" },
|
|
);
|
|
const character_header = createHtmlElem(
|
|
character_elem,
|
|
"h2",
|
|
{ class: "accordion-header" },
|
|
);
|
|
const character_header_button = createHtmlElem(
|
|
character_header,
|
|
"button",
|
|
{ class: `accordion-button ${collapsed}`, "character-name": name, type: "button", "data-bs-toggle": "collapse", "data-bs-target": `#character_${slug}`, "aria-controls": slug },
|
|
name,
|
|
);
|
|
const character_body = createHtmlElem(
|
|
character_elem,
|
|
"div",
|
|
{ id: `character_${slug}`, "character-name": name, class: `accordion-collapse collapse ${show}`, "data-bs-parent": "#characters_list" },
|
|
);
|
|
const character_body_content = createHtmlElem(
|
|
character_body,
|
|
"div",
|
|
{ class: "accordion-body" },
|
|
);
|
|
const sheet = new CharacterSheet(this.api, name)
|
|
sheet.addHtml(character_body_content, name)
|
|
return character_elem;
|
|
});
|
|
}
|
|
|
|
addNew(name) {
|
|
if (!name) {
|
|
alert("Nom vide illégal, ajouter un nom pour continuer.")
|
|
}
|
|
else if (this.api.addCharacter(name)) {
|
|
this.addHtml(document.getElementById("characters_list"), name);
|
|
} else {
|
|
alert(`${name} est déjà un nom de personnage utilisé. Action annulée.`)
|
|
}
|
|
}
|
|
}
|
|
|
|
class Inventory {
|
|
addHtml(col) {
|
|
const inventory_row = createHtmlElem(col, "div", { class: "row character-inventory" });
|
|
const inventory_h2 = createHtmlElem(inventory_row, "h2", {}, "Inventaire & sacs");
|
|
const new_entry_button = ` <i class="bi bi-plus-square add-inventory" onclick="Inventory.addRow()"></i>`;
|
|
renderHtml(inventory_h2, new_entry_button, { version: 3 });
|
|
/* const inventory_table = createHtmlElem(inventory_row, "table", { class: "table table-striped table-hover table-sm" });
|
|
* const inventory_table_thead = createHtmlElem(inventory_table, "thead");
|
|
* const thead_html = "<tr><th>Infos</th><th>Usure</th><th>Utilisé ?</th></tr>";
|
|
* renderHtml(inventory_table_thead, thead_html, { version: 3 });
|
|
* const inventory_table_tbody = createHtmlElem(inventory_table, "tbody", { class: "table-group-divider"}); */
|
|
const current_char = new CurrentCharacter().data();
|
|
|
|
const headers_html = `
|
|
<div class="row">
|
|
<div onclick="Sort.toggle(event, '.accordion-collapse.show .character-inventory', '.inventory-line', '.floatformfield[value-id=objet]')" class="col-6" data-bs-toggle="tooltip" data-bs-title="Nom, description, nombre">Objet</div>
|
|
<div onclick="Sort.toggle(event, '.accordion-collapse.show .character-inventory', '.inventory-line', '.floatformfield[value-id=usure]')" class="col-2" data-bs-toggle="tooltip" data-bs-title="Le niveau d'usure. A fragilisé, l'usage devient dangereux">Usure</div>
|
|
<div onclick="Sort.toggle(event, '.accordion-collapse.show .character-inventory', '.inventory-line', '.floatformfield[value-id=used]')" class="col-1" data-bs-toggle="tooltip" data-bs-title="Est-ce que l'objet à été utilisé depuis le dernier jet de SUR ?">Uti.</div>
|
|
<div onclick="Sort.toggle(event, '.accordion-collapse.show .character-inventory', '.inventory-line', '.floatformfield[value-id=hold]')" class="col-1" data-bs-toggle="tooltip" data-bs-title="A porté de main ?">Port.</div>
|
|
<div onclick="Sort.toggle(event, '.accordion-collapse.show .character-inventory', '.inventory-line', '.floatformfield[value-id=categorie]')" class="col-2" data-bs-toggle="tooltip" data-bs-title="Catégorie">Cat.</div>
|
|
</div>
|
|
`;
|
|
renderHtml(inventory_row, headers_html, { version: 3 });
|
|
let index = 1;
|
|
current_char.inventory.forEach((inventory_data) => {
|
|
this.addHtmlInventoryLine(inventory_row, index, inventory_data);
|
|
index += 1;
|
|
});
|
|
Sort.toggle({}, '.accordion-collapse.show .character-inventory', '.inventory-line', '.floatformfield[value-id=categorie]');
|
|
|
|
}
|
|
addHtmlInventoryLine(table, index, data = {}) {
|
|
const inventory_line = createHtmlElem(table, "div", { class: "inventory-line" });
|
|
addHtmlFloatForm(
|
|
inventory_line,
|
|
`inventory_${index}`, [
|
|
{ id: "objet", colspan: 6, type: "text", value: data.objet || data.infos },
|
|
{ id: "usure", colspan: 2, type: "select", value: data.usure, options: {"parfait":1, "neuf":2, "bon":3, "usé":4, "endommagé":5, "fragilisé":6} },
|
|
{ id: "used", colspan: 1, type: "checkbox", value: data.used },
|
|
{ id: "hold", colspan: 1, type: "checkbox", value: data.hold },
|
|
{ id: "categorie", colspan: 2, type: "text", value: data.categorie },
|
|
],
|
|
{ },
|
|
)
|
|
}
|
|
static addRow(event) {
|
|
const current_character = new CurrentCharacter();
|
|
const inventory_elem = current_character.htmlRoot().querySelector(".character-inventory");
|
|
const new_inventory_line = new Inventory().addHtmlInventoryLine(inventory_elem);
|
|
}
|
|
}
|
|
|
|
class Spell {
|
|
addHtml(col) {
|
|
const character = new CurrentCharacter().data();
|
|
const spells_h2 = createHtmlElem(col, "h2", { }, "Magie & Sorts");
|
|
const add_entry_button = ` <i class="bi bi-plus-square add-inventory" onclick="Spell.addRow()"></i>`;
|
|
renderHtml(spells_h2, add_entry_button, { version: 3 });
|
|
const spells_row = createHtmlElem(col, "div", { class: "character-spells" }, "");
|
|
const spells = character.spells || [];
|
|
spells.forEach(spell => this.addHtmlSpellLine(spells_row, spell));
|
|
}
|
|
addHtmlSpellLine(parent, spell_data = {}) {
|
|
this.spell_count ||= 0;
|
|
this.spell_count += 1;
|
|
const spell_name = spell_data.nom || `Nouveau sort n°${this.spell_count}`;
|
|
const spell_id = sluggify(spell_name);
|
|
const spell_line_elem = createHtmlElem(parent, "div", { class: "row g-0 character-spell" });
|
|
addHtmlFloatForm(
|
|
spell_line_elem,
|
|
spell_id,
|
|
[
|
|
{ name: "Nom", row: 1, colspan: 8, value: spell_name, id: "nom" },
|
|
{ name: "Coût", row: 1, colspan: 1, value: spell_data.cost || 0, id: "cost", type: "number", min: 0, max: 100, },
|
|
{ name: "Bonus", row: 1, colspan: 1, value: spell_data.bonus || 0, id: "bonus", type: "number", min: -100, max: 100, },
|
|
{ name: "Rune", row: 1, colspan: 2, value: spell_data.rune || "", id: "run", },
|
|
{ name: "Description", row: 2, colspan: 6, value: spell_data.desc || "", id: "desc", type: "textarea" },
|
|
{ name: "Incantation", row: 2, colspan: 6, value: spell_data.incantation || "", id: "incantation", type: "textarea", },
|
|
],
|
|
{ roll: { paths: ["stats.MAG"], reason: spell_name } },
|
|
)
|
|
}
|
|
static addRow(event) {
|
|
const current_character = new CurrentCharacter();
|
|
const spells_elem = current_character.htmlRoot().querySelector(".character-spells");
|
|
const new_spell_line = new Spell().addHtmlSpellLine(spells_elem);
|
|
}
|
|
}
|
|
|
|
class Speciality {
|
|
addHtml(col) {
|
|
const current_char = new CurrentCharacter().data();
|
|
const specialities_h2 = createHtmlElem(col, "h2", {}, "Traits et spécialités");
|
|
const new_entry_button = ` <i class="bi bi-plus-square add-speciality" onclick="Speciality.addRow()"></i>`;
|
|
renderHtml(specialities_h2, new_entry_button, { version: 3 });
|
|
const specialities_group = createHtmlElem(col, "div", { class: "specialities" });
|
|
Object.keys(current_char.specialities || {}).forEach((speciality_name) => {
|
|
this.addHtmlSpecialityLine(specialities_group, speciality_name);
|
|
});
|
|
}
|
|
/*
|
|
* Add a new speciality line of html
|
|
* requires the root html to add the line in
|
|
*/
|
|
addHtmlSpecialityLine(parent, speciality_name_to_load = null) {
|
|
this.specialities_count ||= 0;
|
|
this.specialities_count += 1;
|
|
const current_char = new CurrentCharacter().data();
|
|
const speciality_id = `speciality_${speciality_name_to_load || this.specialities_count}`;
|
|
const speciality = (current_char.specialities || {})[speciality_name_to_load] || {};
|
|
// complex id so i don't rewrite them twice
|
|
const name_id = `character_${this.name}_s_${this.specialities_count}_name`;
|
|
const level_id = `character_${this.name}_s_${this.specialities_count}_level`;
|
|
const desc_id = `character_${this.name}_s_${this.specialities_count}_desc`;
|
|
// the main element where subfields are defined
|
|
const main = createHtmlElem(parent, "div", { class: "character-speciality-line" })
|
|
addHtmlFloatForm(main, "specialities", [
|
|
{ name: "Nom", colspan: 4, class: "character-speciality", value: speciality.name || "", id: "name" },
|
|
{ name: "Niveau", colspan: 2, class: "character-speciality", value: speciality.level || "", id: "level", type: "number", min: 1, max: 20 },
|
|
{ name: "Description", colspan: 6, class: "character-speciality", value: speciality.desc || "", id: "desc", type: "textarea" },
|
|
]);
|
|
return main;
|
|
}
|
|
static addRow(event) {
|
|
const current_character = new CurrentCharacter();
|
|
const specialities_elem = current_character.htmlRoot().querySelectorAll(".specialities")[0];
|
|
const new_spe = new Speciality().addHtmlSpecialityLine(specialities_elem);
|
|
}
|
|
}
|
|
|
|
class Personnal {
|
|
addHtml(col) {
|
|
const current_char = new CurrentCharacter().data();
|
|
const personnals = current_char.personnals;
|
|
createHtmlElem(col, "h2", {}, "Infos personnelles");
|
|
const personnals_row = createHtmlElem(col, "div", { class: "row character-personnals" });
|
|
addHtmlFloatForm(personnals_row, "personnals", [
|
|
{ name: "Prénoms", class: "character-personnal", value: personnals.prenoms || "" },
|
|
{ name: "Nom", class: "character-personnal", value: personnals.nom || "" },
|
|
{ name: "Carrière", class: "character-personnal", value: personnals.carriere || "" },
|
|
{ name: "Origine", class: "character-personnal", value: personnals.origine || "" },
|
|
{ name: "Âge", class: "character-personnal", value: personnals.age || "" },
|
|
{ name: "Taille", class: "character-personnal", value: personnals.taille || "" },
|
|
{ name: "Masse", class: "character-personnal", value: personnals.masse || "" },
|
|
{ name: "Silouette", class: "character-personnal", value: personnals.silouette || "" },
|
|
{ name: "Dextrie", class: "character-personnal", value: personnals.dextrie || "" },
|
|
{ name: "Cheveux", class: "character-personnal", value: personnals.cheveux || "" },
|
|
{ name: "Yeux", class: "character-personnal", value: personnals.yeux || "" },
|
|
{ name: "Traits", class: "character-personnal", value: personnals.traits || "" },
|
|
], { rows_class: "", groups_class: "" });
|
|
}
|
|
}
|
|
|
|
class Portrait {
|
|
addHtml(col) {
|
|
const current_char = new CurrentCharacter().data();
|
|
const figure = createHtmlElem(col, "figure", { class: "figure" });
|
|
const portraitb64 = api.getPortrait(current_char.name);
|
|
const figure_html = `
|
|
<figure class="figure">
|
|
<img class="figure-img img-fluid rounded" src="${portraitb64}" alt="Portrait de ${current_char.name} ici" />
|
|
<div class="mb-3">
|
|
<label for="portraitFile" class="form-label">Nouveau portrait</label>
|
|
<input class="form-control form-control-sm portraitFile" id="portraitFile" type="file" accept="image/*" />
|
|
</div>
|
|
</figure>
|
|
`;
|
|
renderHtml(figure, figure_html, { version: 3 });
|
|
}
|
|
}
|
|
|
|
class Notes {
|
|
addHtml(col) {
|
|
const current_char = new CurrentCharacter().data();
|
|
createHtmlElem(col, "h2", {}, "Notes");
|
|
createHtmlElem(col, "textarea", { class: "form-control character-value", "value-id": "custom_notes" }, current_char.custom_notes || "");
|
|
}
|
|
}
|
|
|
|
class CharacterHelper {
|
|
addHtml(col, name) {
|
|
const slug = sluggify(name);
|
|
const character_buttons = createHtmlElem(
|
|
col,
|
|
"div",
|
|
{ class: "btn-group", role: "group" },
|
|
);
|
|
const character_remove_button = createHtmlElem(
|
|
character_buttons,
|
|
"button",
|
|
{ class: "btn btn-danger", id: `character_${slug}_delete`, "onclick": "CharacterButtons.characterDelete()" },
|
|
"effacer",
|
|
);
|
|
const character_export_button = createHtmlElem(
|
|
character_buttons,
|
|
"button",
|
|
{ class: "btn btn-primary", id: `character_${slug}_export`, "onclick": "CharacterButtons.characterExport()" },
|
|
"export",
|
|
);
|
|
const character_import_button = createHtmlElem(
|
|
character_buttons,
|
|
"input",
|
|
{ class: "btn btn-warning", type: "file", id: `character_${slug}_import`, "onchange": "CharacterButtons.characterImport(event)" },
|
|
"import",
|
|
);
|
|
}
|
|
}
|
|
|
|
class CharacterState {
|
|
addHtml(col) {
|
|
const states_row = createHtmlElem(col, "div", { class: "character-states row" });
|
|
createHtmlElem(states_row, "h2", {}, "Bonus, Malus");
|
|
const states = new CurrentCharacter().data().states;
|
|
addHtmlFloatForm(states_row, "states", [{ name: "Inspiration", value: states.inspiration, class: "character-state", color: "green" }]);
|
|
addHtmlFloatForm(states_row, "states", [{ name: "État 1", id: "etat1", value: states.etat1, class: "character-state", colorizable: true }]);
|
|
addHtmlFloatForm(states_row, "states", [{ name: "État 2", id: "etat2", value: states.etat2, class: "character-state", colorizable: true }]);
|
|
addHtmlFloatForm(states_row, "states", [{ name: "État 3", id: "etat3", value: states.etat3, class: "character-state", colorizable: true }]);
|
|
addHtmlFloatForm(states_row, "states", [{ name: "État 4", id: "etat4", value: states.etat4, class: "character-state", colorizable: true }]);
|
|
addHtmlFloatForm(states_row, "states", [{ name: "État 5", id: "etat5", value: states.etat5, class: "character-state", colorizable: true }]);
|
|
}
|
|
}
|
|
|
|
class Characteristic {
|
|
static STATS = [
|
|
{ name: "Combat", code: "CMB" },
|
|
{ name: "Connaissance", code: "CNS" },
|
|
{ name: "Discretion", code: "DIS" },
|
|
{ name: "Endurance", code: "END" },
|
|
{ name: "Force", code: "FOR" },
|
|
{ name: "Habileté", code: "HAB" },
|
|
{ name: "Mouvement", code: "MVT" },
|
|
{ name: "Perception", code: "PER" },
|
|
{ name: "Sociabilité", code: "SOC" },
|
|
{ name: "Survie", code: "SUR" },
|
|
{ name: "Tir", code: "TIR" },
|
|
{ name: "Magie", code: "MAG" },
|
|
{ name: "Volonté", code: "VOL" },
|
|
];
|
|
static STATS_BY_CODE = Characteristic.STATS.reduce((base, s) => {
|
|
base[s.code] = s.name;
|
|
return base;
|
|
}, {})
|
|
addHtml(col) {
|
|
const current_char = new CurrentCharacter().data();
|
|
createHtmlElem(col, "h2", {}, "Caractéristiques");
|
|
const stats_row = createHtmlElem(col, "div", { class: "row character-stats" });
|
|
Characteristic.STATS.forEach((stat) => {
|
|
const { code, name } = stat;
|
|
const tmp_code = `${code}_tmp`;
|
|
addHtmlFloatForm(stats_row, "stats", [
|
|
{ name: `${stat.name} (${code})`, class: "character-stat", colspan: 6, id: code, size: "sm", value: current_char.stats[code] || 0, type: "number", min: -100, max: 200, sticky: true },
|
|
{ name: `Tmp (${code})`, class: "character-stat", colspan: 6, id: tmp_code, size: "sm", value: current_char.stats[tmp_code] || 0, type: "number", min: -100, max: 200, sticky: true },
|
|
], { roll: { paths: [`stats.${code}`] } });
|
|
});
|
|
}
|
|
}
|
|
|
|
class SecondaryCharacteristic {
|
|
addHtml(col) {
|
|
const current_char = new CurrentCharacter().data();
|
|
const stats_row = createHtmlElem(col, "div", { class: "character-stats" });
|
|
createHtmlElem(stats_row, "h2", {}, "Stats");
|
|
addHtmlFloatForm(stats_row, "stats", [
|
|
{ name: `Niveau`, class: "character-stat", colspan: 6, id: "niveau", size: "sm", type: "number", min: 0, max: 100, value: current_char.stats.niveau || 0, sticky: true },
|
|
{ name: `XP utilisé`, class: "character-stat", colspan: 6, id: "xp_used", size: "sm", type: "number", min: 0, max: 100, value: current_char.stats.xp_used || 0, sticky: true },
|
|
]);
|
|
addHtmlFloatForm(stats_row, "stats", [
|
|
{ name: `Points de Vie (PV)`, class: "character-stat", colspan: 6, id: "pv", size: "sm", type: "number", min: 0, max: 100, value: current_char.stats.pv || 0, sticky: true },
|
|
{ name: `PV restants`, class: "character-stat", colspan: 6, id: "pv_restants", size: "sm", type: "number", min: 0, max: 100, value: current_char.stats.pv_restants || 0, sticky: true },
|
|
], { refresh: { onclick: "SecondaryCharacteristic.refresh(event)", class: "" } });
|
|
addHtmlFloatForm(stats_row, "stats", [
|
|
{ name: `Points de Fortune (PF)`, class: "character-stat", colspan: 6, id: "pf", size: "sm", type: "number", min: 0, max: 100, value: current_char.stats.pf || 0, sticky: true },
|
|
{ name: `PF restants`, class: "character-stat", colspan: 6, id: "pf_restants", size: "sm", type: "number", min: 0, max: 100, value: current_char.stats.pf_restants || 0, sticky: true },
|
|
], { refresh: { onclick: "SecondaryCharacteristic.refresh(event)", class: "" } });
|
|
addHtmlFloatForm(stats_row, "stats", [
|
|
{ name: `Points de Mana (PM)`, class: "character-stat", colspan: 6, id: "mana", size: "sm", type: "number", min: 0, max: 100, value: current_char.stats.mana || 0, sticky: true },
|
|
{ name: `PM restants`, class: "character-stat", colspan: 6, id: "mana_restants", size: "sm", type: "number", min: 0, max: 100, value: current_char.stats.mana_restants || 0, sticky: true },
|
|
]);
|
|
addHtmlFloatForm(stats_row, "stats", [
|
|
{ name: `Bonus de Force (BF)`, class: "character-stat", colspan: 6, id: "bf", size: "sm", type: "number", min: 0, max: 100, value: current_char.stats.bf || 0, sticky: true },
|
|
], { refresh: { onclick: "SecondaryCharacteristic.refresh(event)", class: "" } });
|
|
}
|
|
|
|
static refresh(event) {
|
|
const stat = event.target.parentElement.getAttribute("value-id");
|
|
if (stat == "pv") { this.refreshPV(event) }
|
|
else if (stat == "pf") { this.refreshPF(event) }
|
|
else if (stat == "bf") { this.refreshBF(event) }
|
|
}
|
|
|
|
static refreshPV(event) {
|
|
const current_char = new CurrentCharacter();
|
|
const pv = current_char.genericGetInput(".character-stat", "pv");
|
|
const stat_end = current_char.genericGetInput(".character-stat", "END").valueAsNumber || 0;
|
|
const stat_for = current_char.genericGetInput(".character-stat", "FOR").valueAsNumber || 0;
|
|
const stat_sur = current_char.genericGetInput(".character-stat", "SUR").valueAsNumber || 0;
|
|
const stat_vol = current_char.genericGetInput(".character-stat", "VOL").valueAsNumber || 0;
|
|
pv.value = Math.floor(stat_end / 5) + Math.floor(stat_for / 10) + Math.floor(stat_sur / 10) + Math.floor(stat_vol / 10);
|
|
}
|
|
|
|
static refreshPF(event) {
|
|
const current_char = new CurrentCharacter();
|
|
const pf = current_char.genericGetInput(".character-stat", "pf");
|
|
const stat_mvt = current_char.genericGetInput(".character-stat", "MVT").valueAsNumber || 0;
|
|
const stat_per = current_char.genericGetInput(".character-stat", "PER").valueAsNumber || 0;
|
|
pf.value = Math.floor(stat_mvt / 10) + Math.floor(stat_per / 10);
|
|
}
|
|
|
|
static refreshBF(event) {
|
|
const current_char = new CurrentCharacter();
|
|
const bf = current_char.genericGetInput(".character-stat", "bf");
|
|
const stat_for = current_char.genericGetInput(".character-stat", "FOR").valueAsNumber || 0;
|
|
bf.value = Math.floor(stat_for / 10);
|
|
}
|
|
}
|
|
|
|
class Equipment {
|
|
addHtml(col) {
|
|
const col_row = createHtmlElem(col, "div", { class: "row character-equipments" });
|
|
const equipement_h2 = createHtmlElem(col_row, "h2", {}, "Équipement porté");
|
|
const current_char = new CurrentCharacter().data();
|
|
const equipments = current_char.equipments;
|
|
addHtmlFloatForm(col_row, "equipments", [
|
|
{ name: "Mêlée 1", value: equipments.melee_1, row: 1, colspan: 4 },
|
|
{ name: "Dégats", value: equipments.degats_melee_1, row: 1, colspan: 2, id: "degats_melee_1" },
|
|
{ name: "Bonus", value: equipments.bonus_melee_1, row: 1, colspan: 2, id: "bonus_melee_1" },
|
|
{ name: "Bouclier", value: equipments.bouclier, row: 1, colspan: 4, sticky: false },
|
|
], { roll: { paths: ["stats.CMB", "equipments.melee_1"], reason: equipments.melee_1 } });
|
|
addHtmlFloatForm(col_row, "equipments", [
|
|
{ name: "Mêlée 2", value: equipments.melee_2, row: 2, colspan: 4 },
|
|
{ name: "Dégats", value: equipments.degats_melee_2, row: 2, colspan: 2, id: "degats_melee_2" },
|
|
{ name: "Bonus", value: equipments.bonus_melee_2, row: 2, colspan: 2, id: "bonus_melee_2" },
|
|
{ name: "Armure", value: equipments.armure, row: 2, colspan: 4, sticky: false },
|
|
], { roll: { paths: ["stats.CMB", "equipments.melee_2"], reason: equipments.melee_2 } });
|
|
addHtmlFloatForm(col_row, "equipments", [
|
|
{ name: "Distance", value: equipments.distance, row: 3, colspan: 4 },
|
|
{ name: "Dégats", value: equipments.degats_distance, row: 3, colspan: 2, id: "degats_distance" },
|
|
{ name: "Bonus", value: equipments.bonus_distance, row: 3, colspan: 2, id: "bonus_distance" },
|
|
{ name: "Vêtements", value: equipments.vetements, row: 3, colspan: 4, sticky: false },
|
|
], { roll: { paths: ["stats.TIR", "equipments.distance"], reason: equipments.distance } });
|
|
}
|
|
}
|
|
|
|
class Experience {
|
|
addHtml(col) {
|
|
const col_row = createHtmlElem(col, "div", { class: "row character-experience" });
|
|
const col_stats = createHtmlElem(col_row, "div", { class: "col-md-6" });
|
|
const h2 = createHtmlElem(col_stats, "h2", {}, "Expérience");
|
|
const col_spe = createHtmlElem(col_row, "div", { class: "col-md-6" });
|
|
createHtmlElem(col_spe, "h2", {}, "Spécialitées");
|
|
this.addHtmlStats(col_stats);
|
|
this.addHtmlSpecialities(col_spe);
|
|
Experience.resetXpColors();
|
|
}
|
|
addHtmlStats(col_stats) {
|
|
const current_char = new CurrentCharacter().data();
|
|
const xp = current_char.experience;
|
|
Characteristic.STATS.forEach((stat) => {
|
|
addHtmlFloatForm(col_stats, "xp_stats", [
|
|
{ name: `${stat.name} (${stat.code})`, id: stat.code, value: xp[stat.code] || 0, type: "number", class: "character-xp", min: 0, max: 3, colspan: 3, html: 'onchange="Experience.resetXpColors()"' },
|
|
{ name: "Améliorer", id: stat.code, type: "button", class: "btn btn-primary character-xp-upgrade", html: `onclick=\"Experience.increaseStat(event, 'stats.${stat.code}')\"`, colspan: 3, size: "lg" },
|
|
]);
|
|
});
|
|
}
|
|
addHtmlSpecialities(col_spe) {
|
|
const current_char = new CurrentCharacter().data();
|
|
const xp = current_char.experience;
|
|
Object.values(current_char.specialities).forEach((spe) => {
|
|
addHtmlFloatForm(col_spe, "xp_specialities", [
|
|
{ name: spe.name, id: spe.name, value: xp[spe.level] || 0, type: "number", class: "character-xp", min: 0, max: 20, colspan: 3, html: 'onchange="Experience.resetXpColors()"' },
|
|
{ name: "Améliorer", id: spe.name, type: "button", class: "btn btn-primary character-xp-upgrade", html: `onclick=\"Experience.increaseStat(event, 'specialities.${spe.name}')\"`, colspan: 3, size: "lg" },
|
|
]);
|
|
});
|
|
}
|
|
static increaseStat(event, path) {
|
|
const current_char = new CurrentCharacter();
|
|
const character = current_char.data();
|
|
const [store, key] = path.split(".");
|
|
if (store == "stats") {
|
|
const current_value = character.stats[key];
|
|
const increase = current_value % 5 == 0 ? 3 : 2;
|
|
current_char.increaseStat(key, increase);
|
|
current_char.setExperience(key, 0);
|
|
}
|
|
else if (store == "specialities") {
|
|
const current_value = character.specialities[key];
|
|
current_char.increaseSpe(key, 1);
|
|
current_char.setExperience(key, 0);
|
|
}
|
|
const xp_used = current_char.genericGetInput(".character-stat", "xp_used");
|
|
xp_used.valueAsNumber++;
|
|
current_char.recordUpdate(null);
|
|
refreshPage();
|
|
}
|
|
static increaseExperience(event, path) {
|
|
console.log("Is this DEAD code?")
|
|
const [store, key] = stat_path.split(".");
|
|
current_char.increaseExperience(key, increase);
|
|
Experience.resetXpColors();
|
|
}
|
|
static resetXpColors() {
|
|
const current_char = new CurrentCharacter()
|
|
const character = current_char.data();
|
|
const inputs = Array.fromDom(current_char.htmlRoot().querySelectorAll(".character-experience .character-xp"));
|
|
const xp = current_char.getExperience();
|
|
xp.template = 0; // ensure we always have a 0
|
|
const high_xp = Object.values(xp).sort((a, b) => b - a)[0];
|
|
|
|
inputs.forEach((input) => {
|
|
const carac = input.getAttribute("value-id");
|
|
if (carac) {
|
|
const btn = current_char.htmlRoot().querySelector(`.character-experience .character-xp-upgrade[value-id="${carac}"]`);
|
|
if (!btn) {
|
|
console.log(input)
|
|
}
|
|
if (input.valueAsNumber == high_xp) {
|
|
btn.disabled = false;
|
|
btn.classList.remove("btn-danger");
|
|
btn.classList.remove("btn-primary");
|
|
btn.classList.add("btn-success");
|
|
} else {
|
|
btn.disabled = true;
|
|
btn.classList.remove("btn-success");
|
|
btn.classList.remove("btn-primary");
|
|
btn.classList.add("btn-danger");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
class CharacterSheet {
|
|
constructor(api, name) {
|
|
this.api = api;
|
|
this.name = name;
|
|
}
|
|
currentCharacter() {
|
|
this.character ||= this.api.getCharacter(this.name);
|
|
return this.character;
|
|
}
|
|
/*
|
|
* Update roll modale parameters
|
|
*/
|
|
rollForm(options = {carac: "", specialities: [], equipments: [], reason: "?" }) {
|
|
const { carac, specialities, equipments, reason } = options;
|
|
const STATS = {
|
|
CMB: "Combat",
|
|
CNS: "Connaissance",
|
|
DIS: "Discretion",
|
|
END: "Endurance",
|
|
FOR: "Force",
|
|
HAB: "Habileté",
|
|
MVT: "Mouvement",
|
|
PER: "Perception",
|
|
SOC: "Sociabilité",
|
|
SUR: "Survie",
|
|
TIR: "Tir",
|
|
MAG: "Magie",
|
|
VOL: "Volonté",
|
|
};
|
|
const current_char = new CurrentCharacter().data();
|
|
const real_name = current_char.personnals.prenoms || current_char.name;
|
|
const modal = document.querySelectorAll("#roll_dice .modal-body")[0];
|
|
const specialities_list_html = Object.values(current_char.specialities).map((spe) => {
|
|
const spe_id = `roll_dice_speciality_${spe.name}`;
|
|
const spe_html = `
|
|
<div class="row">
|
|
<div class="form-check">
|
|
<input class="form-control form-check-input speciality_value" speciality_level="${spe.level}" speciality_name="${spe.name}" type="checkbox" value="" id="${spe_id}">
|
|
<label class="form-check-label" for="${spe_id}">
|
|
${spe.name} pour un bonus de ${Number(spe.level) * 5}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
`;
|
|
return spe_html;
|
|
}).join("");
|
|
const equipment_html = equipments.map(equipment => `
|
|
<div class="input-group mb-3">
|
|
<div class="form-floating">
|
|
<input id="roll_dice_equipment" class="form-control equipment_value" type="number" value="${current_char.equipments["bonus_" + equipment]}" min="-200" max="200" disabled />
|
|
<label for="roll_dice_base">Arme utilisée: ${current_char.equipments[equipment]}</label>
|
|
<input id="roll_dice_equipment_degats" class="form-control equipment_degats" type="number" value="${current_char.equipments["degats_" + equipment]}" min="-200" max="200" disabled hidden />
|
|
<input id="roll_dice_equipment_name" class="form-control equipment_name" type="text" value="${current_char.equipments[equipment]}" disabled hidden />
|
|
</div>
|
|
</div>
|
|
`).join("");
|
|
|
|
const modal_body_html = `
|
|
<div class="row g-3">
|
|
|
|
<p>Personnage: <strong id="roll_dice_character_name">${real_name}</strong></p>
|
|
<p>Jet: <strong id="roll_dice_carac">${STATS[carac]} (<span id="roll_dice_carac_code">${carac}</span>)</strong>: ${reason}</p>
|
|
<p hidden id="roll_dice_reason">${reason}</p>
|
|
|
|
<div class="input-group mb-3">
|
|
<div class="form-floating">
|
|
<input id="roll_dice_base" class="form-control base_value" type="number" value="${current_char.stats[carac]}" min="-200" max="200" disabled />
|
|
<label for="roll_dice_base">Valeur de base</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="input-group mb-3">
|
|
<div class="form-floating">
|
|
<input id="roll_dice_base" class="form-control base_value" type="number" value="${current_char.stats[carac + "_tmp"]}" min="-200" max="200" disabled />
|
|
<label for="roll_dice_base">Modificateur temporaire</label>
|
|
</div>
|
|
</div>
|
|
|
|
${equipment_html}
|
|
|
|
<div class="input-group mb-3">
|
|
<div class="form-floating">
|
|
<input id="roll_dice_base" class="form-control base_value" type="number" value="0" min="-200" max="200" />
|
|
<label for="roll_dice_base">Circonstances</label>
|
|
</div>
|
|
</div>
|
|
|
|
${specialities_list_html}
|
|
</div>
|
|
`;
|
|
|
|
modal.innerHTML = "";
|
|
renderHtml(modal, modal_body_html, { version: 3 });
|
|
}
|
|
|
|
/*
|
|
* Apply current values in the roll modal
|
|
* and run the dice.
|
|
*/
|
|
rollResult(char_name, carac_code, reason, roll_values) {
|
|
// base values
|
|
const current_char = new CurrentCharacter();
|
|
const character = current_char.data();
|
|
const carac_name = `${Characteristic.STATS_BY_CODE[carac_code]} (${carac_code})`
|
|
const target = roll_values.reduce((base, spe) => base + spe.value, 0);
|
|
const modal = document.querySelectorAll("#roll_dice .modal-body")[0];
|
|
const real_name = character.personnals.prenoms || char_name;
|
|
const equipment_name = document.getElementById("roll_dice_equipment_name")?.value;
|
|
const reasons = [reason].concat(roll_values.filter(e => e.show == true).map(e => e.text)).filter(e => e);
|
|
// roll related
|
|
const roll = Math.round(Math.random() * 100) % 100 + 1;
|
|
const is_success = roll <= target;
|
|
const result_color = !is_success ? "danger" : "success";
|
|
const result_word = !is_success ? "d'échec" : "de succès";
|
|
const grade = Math.floor(Math.abs(target - roll) / 10);
|
|
const is_fight = carac_code == "CMB" || carac_code == "TIR"
|
|
const is_doublon = roll % 11 == 0;
|
|
const is_critic = roll % 10 == 0;
|
|
const critical_bonus = is_critic && is_success ? Math.round((Math.random() * 10) % 10 + 1) : 0;
|
|
const bf_value = current_char.genericGetInput(".character-stat", "bf").valueAsNumber || 0;
|
|
const equipment_degats = document.getElementById("roll_dice_equipment_degats")?.valueAsNumber || 0;
|
|
const degats = is_fight ? roll % 10 + bf_value + equipment_degats + critical_bonus : null;
|
|
const RU = roll % 10;
|
|
// html
|
|
const degats_critical_html = is_success && is_critic ? ` + ${critical_bonus} (CRITIQUE)` : ""
|
|
const degats_html = is_fight && is_success && degats != null ? `${degats} dégâts [ ${RU} + ${bf_value} (BF) + ${equipment_degats} (${equipment_name})${degats_critical_html} ]` : "";
|
|
const doublon_html = is_fight && is_doublon ? `DOUBLON !` : "";
|
|
const reason_html = reason;
|
|
const niveau_html = `Niveau ${result_word}: ${grade}`;
|
|
const alert_html = `
|
|
<div class="row alert alert-${result_color}">
|
|
Jet de dé ${reason}: ${roll} / ${target}. ${niveau_html}<br />${degats_html} ${doublon_html}
|
|
</div>`;
|
|
renderHtml(modal, alert_html, { version: 3 });
|
|
// discord
|
|
const degats_critical_discord = is_success && is_critic ? ` + ${critical_bonus} (CRITIQUE)` : ""
|
|
const degats_discord = is_fight && is_success && degats != null ? `\n**${degats} dégâts** \`${RU} + ${bf_value} (BF) + ${equipment_degats} (${equipment_name})${degats_critical_discord}\`` : "";
|
|
const doublon_discord = is_fight && is_doublon ? `**DOUBLON** !` : "";
|
|
const reason_discord = reason;
|
|
const niveau_discord = `Niveau ${result_word}: ${grade}`;
|
|
const alert_discord = `${real_name} lance un dé ! \`${carac_name} ${reasons.join(", ")}\` : **${roll} / ${target}** : ${niveau_discord}${degats_discord}`;
|
|
new DiscordMessage(this.api).forwardMessage(alert_discord);
|
|
// more things to do
|
|
new CharacterLogs(this.api).add(char_name, carac_name, target, roll, roll_values);
|
|
if (roll < target) current_char.increaseExperience(carac_code, 1);
|
|
const specialities = roll_values.filter(i => i.speciality).map(i => i.speciality);
|
|
specialities.forEach(spe_name => current_char.increaseExperience(spe_name, 1))
|
|
Experience.resetXpColors();
|
|
}
|
|
|
|
/*
|
|
* Squeleton html of the character
|
|
*/
|
|
addHtml(character_body_content, name) {
|
|
if (!new CurrentCharacter().isSelected()) return null;
|
|
const main_row = createHtmlElem(character_body_content, "div", { class: "row gy-0 py-2" });
|
|
const main_col_1 = createHtmlElem(main_row, "div", { class: "col-lg-2 gx-5" });
|
|
const main_col_2 = createHtmlElem(main_row, "div", { class: "col-lg-2 gx-5" });
|
|
const main_col_3 = createHtmlElem(main_row, "div", { class: "col-lg-4 gx-5" });
|
|
const main_col_4 = createHtmlElem(main_row, "div", { class: "col-lg-4 gx-5" });
|
|
const secondary_row = createHtmlElem(character_body_content, "div", { class: "row gy-0 py-2 border-top" });
|
|
const secondary_col_1 = createHtmlElem(secondary_row, "div", { class: "col-lg-4 gx-5" });
|
|
const secondary_col_2 = createHtmlElem(secondary_row, "div", { class: "col-lg-4 gx-5" });
|
|
const secondary_col_3 = createHtmlElem(secondary_row, "div", { class: "col-lg-4 gx-5" });
|
|
const third_row = createHtmlElem(character_body_content, "div", { class: "row gy-0 py-2" });
|
|
const third_col_1 = createHtmlElem(third_row, "div", { class: "col-lg-6 gx-5" });
|
|
|
|
new Characteristic().addHtml(main_col_1);
|
|
new SecondaryCharacteristic().addHtml(main_col_2);
|
|
new CharacterState().addHtml(main_col_2);
|
|
new Equipment().addHtml(main_col_3);
|
|
new Speciality().addHtml(main_col_3);
|
|
new Inventory().addHtml(main_col_4);
|
|
|
|
new Personnal().addHtml(secondary_col_1);
|
|
new Portrait().addHtml(secondary_col_2);
|
|
new Notes().addHtml(secondary_col_2);
|
|
new Spell().addHtml(secondary_col_3);
|
|
new CharacterHelper().addHtml(character_body_content, name);
|
|
|
|
new Experience().addHtml(third_col_1);
|
|
return character_body_content;
|
|
}
|
|
}
|
|
|
|
class CharacterLogs {
|
|
constructor(api) { this.api = api; }
|
|
add(character_name, carac, target, roll, roll_details = {}) {
|
|
const timestamp = new Date().toISOString();
|
|
const log = {
|
|
key: `logs.${character_name}.${timestamp}`,
|
|
character_name: character_name,
|
|
carac: carac,
|
|
target: target,
|
|
roll: roll,
|
|
roll_details: roll_details,
|
|
timestamp: timestamp,
|
|
success: target <= roll,
|
|
}
|
|
this.api.addLog(log);
|
|
}
|
|
}
|
|
|
|
class CharacterButtons {
|
|
static characterDelete(event) {
|
|
if (confirm("Supprimer le personnage ?")) {
|
|
const current_character = new CurrentCharacter();
|
|
const char_list = new CharactersList(api);
|
|
const name = current_character.name();
|
|
api.removeCharacter(name);
|
|
alert(`${name} a été supprimé !`);
|
|
new CharactersList(api).addHtml(characters_list_elem);
|
|
}
|
|
}
|
|
static characterExport(event) {
|
|
const current_character = new CurrentCharacter();
|
|
download(
|
|
`${current_character.name()}.json`,
|
|
JSON.stringify(current_character.data()),
|
|
);
|
|
}
|
|
static characterImport(event) {
|
|
if (confirm("Remplacer totalement le personnage ?")) {
|
|
const current_character = new CurrentCharacter();
|
|
const file = event.target.files[0];
|
|
const reader = new FileReader();
|
|
reader.onloadend = function() {
|
|
api.updateCharacter(current_character.name(), JSON.parse(reader.result));
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
function htmlNewCharacter(event) {
|
|
event.preventDefault();
|
|
char_list.addNew(document.getElementById("characters_list_new").value);
|
|
return null;
|
|
}
|
|
|
|
function inputValueParsed(input) {
|
|
if (input.type == "number") return Number(input.value);
|
|
else if (input.type == "checkbox") return input.checked;
|
|
else if (input.tagName == "TEXTAREA") return input.value;
|
|
else return input.value;
|
|
}
|
|
|
|
function parseField(element) {
|
|
const name = (element.attributes["value-id"]).value;
|
|
const value = inputValueParsed(element);
|
|
return { name, value };
|
|
}
|
|
|
|
function parseMultiFields(...args_elements) {
|
|
const base = {};
|
|
args_elements.forEach((elements) => {
|
|
elements.forEach((element) => {
|
|
const parsed = parseField(element);
|
|
base[parsed.name] = parsed.value;
|
|
});
|
|
});
|
|
return base;
|
|
}
|
|
|
|
function updateStoreWithFields(store, elements, field_value_modifier = undefined) {
|
|
elements.forEach((current_elem) => {
|
|
const parsed = parseField(current_elem);
|
|
store[parsed.name] = field_value_modifier ? field_value_modifier(parsed.value) : parsed.value;
|
|
});
|
|
}
|
|
|
|
class RollDice {
|
|
constructor() {}
|
|
static openForm(event, char_name, paths, reason = "?") {
|
|
const paths_by_store = Sort.groupBy(
|
|
paths.map(e => e.split(".")),
|
|
path => path[0],
|
|
);
|
|
paths_by_store.specialities ||= []
|
|
/* const carac = paths[0].split(".").slice(-1)[0]; */
|
|
const carac = paths_by_store.stats[0].slice(-1);
|
|
const specialities = paths_by_store?.specialities?.map(item => item[1]) || [];
|
|
const equipments = paths_by_store?.equipments?.map(item => item[1]) || [];
|
|
new CharacterSheet(api, char_name).rollForm({
|
|
carac: carac, specialities: specialities, equipments: equipments, reason: reason,
|
|
});
|
|
}
|
|
static execute(event) {
|
|
const current_character = new CurrentCharacter();
|
|
const carac_name = document.getElementById("roll_dice_carac").textContent;
|
|
const carac_code = document.getElementById("roll_dice_carac_code").textContent;
|
|
const roll_values = [];
|
|
document.querySelectorAll("#roll_dice .modal-body input.base_value").forEach((input) => {
|
|
roll_values.push({
|
|
value: Number(input.value),
|
|
text: input.parentNode.children[1].textContent,
|
|
show: false,
|
|
});
|
|
})
|
|
document.querySelectorAll("#roll_dice .modal-body input.speciality_value").forEach((spe) => {
|
|
if (spe.checked) {
|
|
const spe_name = spe.attributes["speciality_name"].value;
|
|
const spe_level = Number(spe.attributes["speciality_level"].value);
|
|
roll_values.push({
|
|
value: spe_level * 5,
|
|
text: `${spe_name} niveau ${spe_level}`,
|
|
show: true,
|
|
speciality: spe_name,
|
|
});
|
|
}
|
|
});
|
|
const reason = document.querySelector("#roll_dice .modal-body #roll_dice_reason").textContent || "";
|
|
const name = current_character.name();
|
|
new CharacterSheet(api, name).rollResult(name, carac_code, reason, roll_values);
|
|
|
|
}
|
|
}
|
|
|
|
function reloadLogs(event, logs_amount) {
|
|
const logs = api.getLastLogs(logs_amount);
|
|
let log_entries_html = "";
|
|
logs.reverse().forEach((log) => {
|
|
const color = log.success ? "success" : "danger";
|
|
const log_entry_html = `<div class="alert alert-${color}">${log.timestamp}: ${log.character_name} - ${log.carac}: ${log.roll} / ${log.target}</div>`;
|
|
log_entries_html += log_entry_html;
|
|
});
|
|
document.getElementById("logs-entries").innerHTML = log_entries_html;
|
|
}
|
|
|
|
class DiscordMessage {
|
|
constructor(api) { this.api = api; }
|
|
forwardMessage(message) {
|
|
const { host, password } = this.api.getDiscordInfos();
|
|
const xhttp = new XMLHttpRequest();
|
|
xhttp.onload = function() { console.log(`Discord sent: ${message}`); }
|
|
xhttp.open("POST", host, true);
|
|
xhttp.setRequestHeader("AUTH", password);
|
|
xhttp.setRequestHeader("Access-Control-Allow-Origin", "*");
|
|
xhttp.send(message);
|
|
}
|
|
recordUpdate(_event) {
|
|
const host = document.querySelector("input#discord_host").value
|
|
const password = document.querySelector("input#discord_password").value
|
|
api.addDiscordInfos(host, password);
|
|
}
|
|
refreshFormValues() {
|
|
const discord_infos = api.getDiscordInfos();
|
|
document.querySelector("input#discord_host").value = discord_infos.host;
|
|
document.querySelector("input#discord_password").value = discord_infos.password;
|
|
}
|
|
}
|
|
|
|
var api = new DataAPI(localStorage);
|
|
const char_list = new CharactersList(api);
|
|
const characters_list_elem = document.getElementById("characters_list");
|
|
|
|
function refreshPage(target_name = undefined) {
|
|
char_list.addHtml(characters_list_elem, target_name);
|
|
}
|
|
refreshPage();
|
|
|
|
document.addEventListener('click', (event) => {
|
|
if (event.target.classList.contains("accordion-button") && !event.target.classList.contains("collapsed")) {
|
|
let target_name = event.target.attributes["character-name"].value;
|
|
refreshPage(target_name);
|
|
}
|
|
});
|
|
|
|
function colorizeUsure(target) {
|
|
if (target.tagName == "SELECT" && target.attributes["value-id"].value == "usure") {
|
|
const value = target.value;
|
|
const colors = {
|
|
"1": "green",
|
|
"2": "green",
|
|
"4": "yellow",
|
|
"5": "orange",
|
|
"6": "red",
|
|
}
|
|
const color = colors[value];
|
|
if (color) target.style.color = color;
|
|
}
|
|
}
|
|
|
|
function portraitUploader(target) {
|
|
if (target.classList.contains('portraitFile')) {
|
|
const current_character = new CurrentCharacter();
|
|
const file = target.files[0];
|
|
const reader = new FileReader();
|
|
reader.onloadend = function() {
|
|
const base64 = reader.result;
|
|
try {
|
|
api.addPortrait(current_character.name(), base64);
|
|
current_character.htmlRoot().querySelector("img").src = base64;
|
|
} catch (error) {
|
|
alert("Impossible de charger le portrait, fichier trop gros ? (essayer une image de <2MB)");
|
|
}
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
}
|
|
|
|
document.onchange = (event) => {
|
|
colorizeUsure(event.target);
|
|
portraitUploader(event.target);
|
|
};
|
|
|
|
document.querySelectorAll("select[value-id=usure]").forEach((target) => colorizeUsure(target));
|
|
|
|
new DiscordMessage(api).refreshFormValues();
|
|
|
|
document.addEventListener("keypress", (event) => {
|
|
if (event.key != "Enter") return null;
|
|
const current_char = new CurrentCharacter();
|
|
if (current_char.isSelected()) {
|
|
current_char.recordUpdate(event);
|
|
}
|
|
});
|
|
|
|
document.onkeydown = (event) => {
|
|
if (event.ctrlKey && event.key === 's') {
|
|
event.preventDefault();
|
|
const current_char = new CurrentCharacter();
|
|
if (current_char.isSelected()) {
|
|
current_char.recordUpdate(event);
|
|
new DiscordMessage(api).recordUpdate(event);
|
|
}
|
|
}
|
|
};
|
|
|
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
|
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|