Skip to content

zeth-bit/mob

Repository files navigation

πŸ‰ Monster Archive: TTRPG Elemental Resistance Tracker

A personal tool built for an online tabletop RPG. It tracks every encountered monster's elemental resistances, computes global averages to guide combat strategy, enables side-by-side monster comparison, and provides a multi-formula damage calculator for weapons and spells.

https://zeth.altervista.org/mob/


Overview

While playing an online RPG, one recurring tactical question was: "When I don't know a monster's weakness, which element should I default to?", and there was no convenient way to answer it across the full roster of encountered enemies.

This project solves that. It is a zero-dependency, single-file web application that:

  • Stores all encountered monsters and their elemental resistances in a structured JSON file
  • Displays them in a searchable, sortable table
  • Computes a global average resistance per element across all monsters, revealing statistically optimal attack types
  • Allows multi-monster comparison via inline checkboxes
  • Provides a damage calculator with distinct formulas for melee weapons, arcane spells (Mage), and divine spells (Cleric)

The entire frontend is a self-contained index.html backed by a remote mostri.json, no framework, no build step, no server-side logic.


Architecture

β”œβ”€β”€ index.html        # Entire application (HTML + CSS + JS in one file)
└── mostri.json       # Monster data: name + 8 elemental resistance values (%)

The architecture is deliberately minimal. All logic lives in a single HTML file, split into three logical layers:

1. Data Layer (mostri.json) Each entry follows a fixed schema:

{
  "nome": "Troll",
  "resistenze": {
    "Fuoco": -30,
    "Freddo": 20,
    "Energia": 0,
    "Veleno": 20,
    "Psionico": 0,
    "Sacro": 0,
    "Malefico": 0,
    "Magia": 30
  }
}

Resistance values range from negative (vulnerability) to 100 (full immunity). Negative values are intentional and meaningful, they denote elemental weaknesses that amplify incoming damage.

2. Presentation Layer (CSS) A dark-themed UI with a gold (#ffb300) accent palette suitable for a fantasy setting. All interactive elements use CSS transitions and animations without JavaScript. Element types have dedicated color classes (colore-Fuoco, colore-Freddo, etc.) that are applied uniformly across all tables and result panels.

3. Logic Layer (vanilla JavaScript) All JS is inline and organized into distinct functional sections:

  • Data fetching and initialization
  • Table rendering and live search filtering
  • Average resistance computation
  • Multi-select comparison table
  • Three separate damage calculators

Key Design Decisions

Single-file deployment

The application is hosted on a shared hosting provider (AltaVista). A single-file approach eliminates module bundling complexity, avoids CORS issues with local scripts, and makes the tool trivially deployable. The trade-off is that the file grows large, but for a personal tool of this scope, maintainability outweighs separation of concerns.

External JSON data source

Monster data is fetched via fetch() from a remote URL rather than hardcoded inline. This decouples content from code, adding a new monster requires only editing mostri.json, not touching the application logic. It also means the data file can be updated independently and cached by the browser.

Resistance as a percentage, including negatives

Modeling resistance as a signed percentage is the correct representation for this game system. A value of -50 (e.g., "Albero Demoniaco" vs. Fire) means the monster takes 50% extra damage, not just "no resistance". The formattaResistenza() function applies distinct CSS classes for negative, zero, and maximum (100) values, making vulnerabilities immediately visible without any additional logic.

Set-based selection state

The comparison feature uses a JavaScript Set (selezionati) to track which monsters are checked. This is a deliberate choice over a plain array: Set provides O(1) add/delete/has operations and prevents duplicate entries, which is exactly the right data structure for a checkbox-driven selection model.

Proportional damage clamping in the weapon calculator

When a weapon has both a runic enchantment and an elemental material, the combined elemental conversion could theoretically exceed 100%. The calcolaDannoArma() function handles this explicitly:

if (sommaConv > 100) {
  const scale = 100 / sommaConv;
  percRunica *= scale;
  mats.forEach(m => m.percent *= scale);
}

Rather than clamping one source or rejecting the input, the function scales both proportionally, preserving the ratio of the two elemental contributions while ensuring physical damage never goes negative.

Three separate damage models

The calculator exposes three distinct formulas:

Calculator Formula
Weapon Physical (reduced by flat AR) + Elemental material % + Runic element %
Mage 70% elemental (vs. element resistance) + 30% magic (vs. magic resistance)
Cleric 100% pure elemental

These are not parameterized variations of one function, they are independent implementations. This is intentional: each has a fundamentally different internal logic, and conflating them into a single configurable function would make the code harder to reason about and modify per-class.


Code Walkthrough

The script block at the bottom of index.html is the best entry point. Read it in this order:

  1. Global variables (mostri, selezionati, tipiResistenza), understand the shared state before anything else.
  2. fetch() bootstrap, this is the single initialization trigger. Everything else is called from here.
  3. inizializza(), wires up event listeners and triggers the first table render.
  4. aggiornaTabella(), the core render loop. It filters mostri[] by the search input, rebuilds the table DOM, and re-attaches checkbox listeners. Note that listeners are re-attached on every render, which is simple but means they cannot accumulate on stale elements.
  5. calcolaMediaResistenze(), a straightforward reduce+average over all 8 resistance types.
  6. aggiornaConfronto(), reads from the selezionati Set and rebuilds the comparison table dynamically with variable-width columns.
  7. calcolaDannoArma() / calcolaMago() / calcolaClerico(), the three damage models. Each takes a params object and returns { comps, total } where comps is an array of damage components (type, element, raw value, resistance applied, final value).
  8. mostraRisultato(), a pure display function that takes any { comps, total } result and renders it to a target div. Shared across all three calculators.

Modification Guide

Adding a new monster

Edit mostri.json and add an entry following the existing schema. All 8 resistance keys must be present. The table will automatically reflect the change on next page load.

Adding a new resistance type

This requires coordinated changes in three places:

  1. Add the new type to the tipiResistenza array in the JS
  2. Add a <th> column in the HTML table header
  3. Add a colore-<Type> CSS class
  4. Add the new key to every entry in mostri.json

Extending the weapon calculator

calcolaDannoArma() takes a params object with well-defined keys. New weapon types can be added by extending the <select id="tipoArma"> element and adding a new conditional branch in the function. The scaling logic for combined sources already handles arbitrary numbers of elemental contributors.

Adding a new spell class

Implement a new function following the calcolaMago / calcolaClerico signature, (params) => { comps, total }, then wire it up in setupCalcolatori() and add the corresponding HTML section. The mostraRisultato() renderer is already generic.


Debugging & Problem Solving

Resistance lookup returning 0 for unknown types

getResistenze() returns m?.resistenze || {} and individual resistance values default to || 0 at the call site. This means a missing key silently falls back to 0% resistance. This is safe for display but could mask data entry errors in mostri.json. A validation step on load (checking that all 8 keys are present) would catch these issues early.

Column sort breaks after search filtering

abilitaOrdinamentoTabella() operates on #tabellaMostri tbody row elements. Since aggiornaTabella() rebuilds the tbody innerHTML on every search input event, the sort state (direzione object) is preserved but the sorted DOM is discarded on the next keypress. This is acceptable for a personal tool, but a persistent sort state would require tracking the last-sorted column and re-sorting after each filter update.

Comparison checkboxes losing state after table re-render

aggiornaTabella() preserves checkbox state by reading from the selezionati Set when building each row: ${selezionati.has(m.nome) ? 'checked' : ''}. This correctly restores visual state after a search filter is applied, as long as the monster name in the Set matches exactly the m.nome key, case-sensitive.

calcolaDannoArma() with mixed multi-element materials

When 2 or 3 material elements are selected, each is hardcoded to 15%. This is a game-system rule, not a simplification, but it means the percentage input for single-element materials (materialPercentArma) is shown/hidden conditionally via JS. If this rule changes, both the JS display logic and the material array construction in setupCalcolatori() need updating.


Limitations & Future Improvements

Data entry is fully manual. Every monster entry must be hand-written in JSON. There is no admin UI, no import from external sources. For a campaign tool this is acceptable, but it does not scale if the monster roster grows significantly.

No persistence of user state. Selected monsters, calculator inputs, and search state are all lost on page reload. For a tool used regularly mid-session, adding localStorage persistence for at least the calculator values would meaningfully improve usability.

No validation on mostri.json. A malformed entry (missing key, wrong type) will either render as 0% silently or break the row rendering entirely. A schema validation step during the fetch response processing would make errors visible immediately.

Single-file architecture limits long-term maintainability. As the tool grows, splitting HTML, CSS, and JS into separate files, even without a build system, would improve readability and enable browser-level caching of the script independently from the markup.

The damage model is game-specific. The 70/30 split for Mage spells and the flat AR subtraction model are hardcoded to the specific RPG system this was built for. Making these configurable would generalize the tool significantly.


Built April 2025 as a personal companion tool for an online tabletop RPG.

About

A personal tool built for an online tabletop RPG. It tracks every encountered monster's elemental resistances, computes global averages to guide combat strategy, enables side-by-side monster comparison, and provides a multi-formula damage calculator for weapons and spells.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages