diff --git a/src/App.jsx b/src/App.jsx index 1f91235..a219cd1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,6 +2,8 @@ import JSZip from "jszip"; import { Create40kRoster } from "./roster40k"; import { useState } from "react"; import { Roster } from "./Roster"; +import Demo0 from "./assets/Demo0.png"; +import Demo1 from "./assets/Demo1.png"; function App() { const [error, setError] = useState(); @@ -55,7 +57,7 @@ function App() { console.log(doc); const roster = Create40kRoster(doc); console.log(roster); - if (roster && roster._forces.length > 0) { + if (roster && roster.forces.length > 0) { setRoster(roster); /* const renderer = new Renderer40k(roster); renderer.render(rosterTitle, rosterList, forceUnits); */ @@ -67,13 +69,62 @@ function App() { return (
- -
{error}
+
+ FancyScribe +
+ A fancy way to view your Warhammer 40k BattleScribe rosters. +
+
- +
+ + +
{error}
+ + +
+
About
+
+ PrettyScribe is a website that renders{" "} + + BattleScribe + {" "} + roster files in an opinionated format inspired by the new 10th + edition datacards. Inspiration and large parts of the parsing logic + come from the{" "} + + PrettyScribe + {" "} + application. +
+
+ Output Examples +
+ + +
+
); } diff --git a/src/Roster.jsx b/src/Roster.jsx index ee118df..217bf1b 100644 --- a/src/Roster.jsx +++ b/src/Roster.jsx @@ -1,14 +1,14 @@ import factionBackground from "./assets/factionBackground.png"; import adeptusAstartesIcon from "./assets/adeptusAstartesIcon.png"; import rangedIcon from "./assets/rangedIcon.png"; -import { Arrow } from "./assets/icons"; +import { Arrow, wavyLine } from "./assets/icons"; import { Weapons } from "./Weapons"; export const Roster = ({ roster }) => { if (!roster) { return null; } - const { _name, _cost, _forces } = roster; + const { name, cost, forces } = roster; return (
{ >
- {_name} [{_cost._commandPoints} CP] + {name} [{cost.commandPoints} CP]
- {_forces.map((force, index) => ( + {forces.map((force, index) => ( ))}
@@ -84,9 +85,9 @@ const getPrimaryColor = (catalog) => { }; const Force = ({ force }) => { - const { _units, rules, _catalog } = force; + const { units, rules, catalog } = force; - const primaryColor = getPrimaryColor(_catalog); + const primaryColor = getPrimaryColor(catalog); return (
{ "--primary-color-transparent": "#53676660", }} > - {_units.map((unit, index) => ( + {units.map((unit, index) => ( ))} @@ -105,35 +106,47 @@ const Force = ({ force }) => { const Unit = ({ unit }) => { let { - _name, + name, weapons, abilities, keywords, factions, rules, modelStats, - _modelList, - _psykers, + modelList, + psykers, } = unit; const hasDifferentProfiles = weapons.some( (weapon) => - weapon._selectionName !== weapon._name && - !weapon._name.includes("(Shooting)") && - !weapon._name.includes("(Melee)") + weapon.selectionName !== weapon.name && + !weapon.name.includes("(Shooting)") && + !weapon.name.includes("(Melee)") ); const meleeWeapons = weapons - .filter((weapon) => weapon._range === "Melee" && weapon._range !== "-") - .sort((a, b) => a._selectionName.localeCompare(b._selectionName)); + .filter((weapon) => weapon.range === "Melee" && weapon.range !== "-") + .sort((a, b) => a.selectionName.localeCompare(b.selectionName)); + if (meleeWeapons.length === 0) { + meleeWeapons.push({ + name: "Close combat weapon", + selectionName: "Close combat weapon", + range: "Melee", + damage: "1", + type: "Melee", + str: "0", + ap: "0", + }); + } const rangedWeapons = weapons - .filter((weapon) => weapon._range !== "Melee" && weapon._range !== "-") - .sort((a, b) => a._selectionName.localeCompare(b._selectionName)); - modelStats = modelStats.sort((a, b) => b._name.length - a._name.length); // Sort by length of name, so that for example "Primaris Intercessor Sergeant" is before "Primaris Intercessor" + .filter((weapon) => weapon.range !== "Melee" && weapon.range !== "-") + .sort((a, b) => a.selectionName.localeCompare(b.selectionName)); + if (!modelStats.some((model) => model.name.includes("wounds remaining)"))) { + modelStats = modelStats.sort((a, b) => b.name.length - a.name.length); // Sort by length of name, so that for example "Primaris Intercessor Sergeant" is before "Primaris Intercessor" + } return (
{ >
{ style={{ padding: "4px 16px", color: "#fff", - minHeight: 85, position: "relative", }} > @@ -162,11 +173,40 @@ const Unit = ({ unit }) => { position: "absolute", left: 0, top: 0, - backgroundColor: "var(--primary-color-transparent)", - minHeight: 85, width: "100%", + display: "flex", }} - >
+ > +
+
+
+ {wavyLine} +
+
+
{ position: "relative", }} > - {_name} + {name}
{ 1} /> @@ -225,7 +265,7 @@ const Unit = ({ unit }) => { weapons={meleeWeapons} modelStats={modelStats} /> - +
{hasDifferentProfiles && ( @@ -282,12 +322,12 @@ const Unit = ({ unit }) => { }; const ModelStats = ({ modelStats, index, showName, modelList }) => { - let { move, toughness, save, wounds, leadership, _name } = modelStats; + let { move, toughness, save, wounds, leadership, name } = modelStats; if (!wounds) { wounds = "/"; } const modelListMatches = modelList - .filter((model) => model.includes(_name)) + .filter((model) => model.includes(name)) .map((model) => "(" + model.split("(")[1]); return (
@@ -305,7 +345,7 @@ const ModelStats = ({ modelStats, index, showName, modelList }) => { whiteSpace: "nowrap", }} > - {_name} + {name}
{ )} {psykers.map((psyker, index) => ( - + ))} {psykers.length > 0 && ( @@ -543,7 +583,7 @@ const Psykers = ({ title, psykers }) => { }; const Psyker = ({ psyker, index }) => { - let { _name, _cast, _deny, _powers } = psyker; + let { name, cast, deny, powers } = psyker; return ( @@ -557,13 +597,13 @@ const Psyker = ({ psyker, index }) => { gap: "0 4px", }} > - {_name} + {name}
- {_cast} - {_deny} + {cast} + {deny} - {_powers} + {powers} ); @@ -588,7 +628,6 @@ const ForceRules = ({ rules }) => {
diff --git a/src/Weapons.jsx b/src/Weapons.jsx index 0cdd0cf..1856b16 100644 --- a/src/Weapons.jsx +++ b/src/Weapons.jsx @@ -32,12 +32,11 @@ export const Weapons = ({ title, weapons, modelStats }) => { {weapons.map((weapon, index) => ( ))} {weapons.length > 0 && ( @@ -51,77 +50,102 @@ export const Weapons = ({ title, weapons, modelStats }) => { ); }; -const Weapon = ({ weapon, modelStats, isMelee, className, index }) => { - let { _name, _selectionName, _range, _type, str, _ap, _damage } = weapon; - var lastWhiteSpace = _type.lastIndexOf(" "); - const type = _type.substring(0, lastWhiteSpace); - const attacks = _type.substring(lastWhiteSpace + 1); - const bs = modelStats[0]._bs; - const ws = modelStats[0]._ws; +const Weapon = ({ weapon, modelStats, isMelee, className }) => { + let { name, selectionName, range, type, str, ap, damage, abilities } = weapon; + var lastWhiteSpace = type.lastIndexOf(" "); + type = type.substring(0, lastWhiteSpace); + const attacks = type.substring(lastWhiteSpace + 1); + const bs = modelStats[0].bs; + const ws = modelStats[0].ws; const strModel = modelStats[0].str; - const meleeAttacks = modelStats[0]._attacks; + const meleeAttacks = modelStats[0].attacks; - if (_name === "Krak grenades") { - _name = "Krak grenade"; + if (name === "Krak grenades") { + name = "Krak grenade"; } const differentProfiles = - _selectionName !== _name && - !_name.includes("(Shooting)") && - !_name.includes("(Melee)"); + selectionName !== name && + !name.includes("(Shooting)") && + !name.includes("(Melee)"); const interestingType = type && type !== "Melee"; - if (differentProfiles && _name.endsWith(" grenades")) { - _name = _name.replace(" grenades", ""); + if (differentProfiles && name.endsWith(" grenades")) { + name = name.replace(" grenades", ""); } - if (differentProfiles && _name.endsWith(" grenade")) { - _name = _name.replace(" grenade", ""); + if (differentProfiles && name.endsWith(" grenade")) { + name = name.replace(" grenade", ""); } - if (differentProfiles && _name.includes(" - ")) { - _name = _name.split(" - ")[1]; + if (differentProfiles && name.includes(" - ")) { + name = name.split(" - ")[1]; } + + if (abilities === "Blast") { + abilities = abilities.replaceAll("Blast", ""); + type += ", Blast"; + } + return ( - - - {differentProfiles && Arrow} - - -
- {differentProfiles && _selectionName + " - "} - {_name} - {interestingType && ( - - [{type}] - - )} -
- - {_range} - {isMelee ? meleeAttacks : attacks} - {isMelee ? ws : bs} - {isMelee ? calculateWeaponStrength(strModel, str) : str} - {_ap} - {_damage} - + <> + + + {differentProfiles && Arrow} + + +
+ {differentProfiles && selectionName + " - "} + {name} + {interestingType && ( + + [{type}] + + )} +
+ + {range} + {isMelee ? meleeAttacks : attacks} + {isMelee ? ws : bs} + {isMelee ? calculateWeaponStrength(strModel, str) : str} + {ap} + {damage} + + {abilities && abilities !== "-" && ( + + + + {abilities} + + + )} + ); }; const getWeaponClassNames = (weapons, index) => { let differentColor = false; for (let i = 1; i <= index; i++) { - let { _selectionName } = weapons[i]; - if (_selectionName !== weapons[i - 1]._selectionName) { + let { selectionName } = weapons[i]; + if (selectionName !== weapons[i - 1].selectionName) { differentColor = !differentColor; } } @@ -130,7 +154,7 @@ const getWeaponClassNames = (weapons, index) => { if (index === 0) classes.push("noBorderTop"); if ( index > 0 && - weapons[index]._selectionName === weapons[index - 1]._selectionName + weapons[index].selectionName === weapons[index - 1].selectionName ) classes.push("noBorderTop"); return classes.join(" "); diff --git a/src/assets/Demo0.png b/src/assets/Demo0.png new file mode 100644 index 0000000..61fd40f Binary files /dev/null and b/src/assets/Demo0.png differ diff --git a/src/assets/Demo1.png b/src/assets/Demo1.png new file mode 100644 index 0000000..ef785a3 Binary files /dev/null and b/src/assets/Demo1.png differ diff --git a/src/assets/icons.jsx b/src/assets/icons.jsx index 4894311..5c614f8 100644 --- a/src/assets/icons.jsx +++ b/src/assets/icons.jsx @@ -3,3 +3,13 @@ export const Arrow = ( ); +export const wavyLine = ( + + + +); diff --git a/src/assets/wavyLine.svg b/src/assets/wavyLine.svg new file mode 100644 index 0000000..1812a67 --- /dev/null +++ b/src/assets/wavyLine.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/index.css b/src/index.css index 7c40fbc..c2b31cf 100644 --- a/src/index.css +++ b/src/index.css @@ -3,6 +3,7 @@ Arial, sans-serif; line-height: 1.5; font-weight: 400; + font-size: 13px; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); @@ -32,8 +33,6 @@ a:hover { body { margin: 0; - display: flex; - place-items: center; min-width: 320px; min-height: 100vh; } @@ -76,19 +75,67 @@ button:focus-visible { } #root { - max-width: 1100px; - margin-left: auto; - margin-right: auto; color: #111113; } -.App { +.body { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 24px 32px; + max-width: 1100px; + margin-left: auto; + margin-right: auto; +} + +.header { + display: flex; + flex-direction: column; + padding: 8px 16px; + background-color: #536766; + width: 100%; + color: #fff; + font-size: 1.7em; + font-weight: 800; +} + +.subheader { + font-size: 1rem; + font-weight: 600; +} + +.rosterUpload { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 8px 24px; + width: 100%; + height: 450px; + border: 10px solid #999; + border-radius: 16px; + font-weight: bold; + font-size: 1.5rem; + text-transform: uppercase; + cursor: pointer; + transition: color 0.25s, border 0.25s, font-size 0.25s, background-color 0.25s; +} + +.rosterUpload:hover { + border-color: #536766; + background-color: #53676660; + font-size: 2rem; +} + +.rosterUploaded, +.rosterUploaded:hover { + height: auto; + border: 2px solid #999; + font-size: 1rem; + border-radius: 8px; } .weapons-table { diff --git a/src/roster40k.js b/src/roster40k.js index 6fc13a8..e365cb6 100644 --- a/src/roster40k.js +++ b/src/roster40k.js @@ -1,48 +1,39 @@ export class BaseNotes { - _name = ""; - _customNotes = ""; - - name() { - return this._name; - } - - notes() { - return this._customNotes; - } + name = ""; equal(other) { if (other == null) return false; // Weapons in 40k have unique names - return this._name === other._name; + return this.name === other.name; } } /** A `selection` attached to a unit or model. */ export class Upgrade extends BaseNotes { - _cost = new Costs(); - _count = 1; + cost = new Costs(); + count = 1; - selectionName() { - return this.name(); + getSelectionName() { + return this.name; } toString() { - let string = this.selectionName(); - if (this._count > 1) string = `${this._count}x ${string}`; - if (this._cost.hasValues()) string += ` ${this._cost.toString()}`; + let string = this.getSelectionName(); + if (this.count > 1) string = `${this.count}x ${string}`; + if (this.cost.hasValues()) string += ` ${this.cost.toString()}`; return string; } } /** A weapon `profile` that is under a `selection`. */ export class Weapon extends Upgrade { - _selectionName = ""; + selectionName = ""; - _range = "Melee"; - _type = "Melee"; + range = "Melee"; + type = "Melee"; str = "user"; - _ap = ""; - _damage = ""; + ap = ""; + damage = ""; abilities = ""; @@ -50,35 +41,35 @@ export class Weapon extends Upgrade { * Name of this weapon's `selection`. This is different from name() because * name() is used for sorting and deduping weapon profiles. */ - selectionName() { - return this._selectionName || this.name(); + getSelectionName() { + return this.selectionName || this.name; } } export class WoundTracker extends BaseNotes { - _name = ""; - _table = new Map(); + name = ""; + table = new Map(); } export class Explosion extends BaseNotes { - _name = ""; - _diceRoll = ""; - _distance = ""; - _mortalWounds = ""; + name = ""; + diceRoll = ""; + distance = ""; + mortalWounds = ""; } export class Psyker extends BaseNotes { - _cast = ""; - _deny = ""; - _powers = ""; - _other = ""; + cast = ""; + deny = ""; + powers = ""; + other = ""; } export class PsychicPower extends BaseNotes { - _name = ""; - _manifest = 0; - _range = ""; - _details = ""; + name = ""; + manifest = 0; + range = ""; + details = ""; } export const UnitRole = { @@ -118,48 +109,48 @@ export const UnitRoleToString = [ ]; export class Model extends BaseNotes { - _count = 0; + count = 0; // Characteristics move = '0"'; - _ws = ""; - _bs = ""; + ws = ""; + bs = ""; str = 4; toughness = 4; wounds = 1; - _attacks = ""; + attacks = ""; leadership = 7; save = ""; weapons = []; - _upgrades = []; + upgrades = []; // TODO model upgrades (i.e. tau support systems) - _psyker = null; - _psychicPowers = []; - _explosions = []; + psyker = null; + psychicPowers = []; + explosions = []; equal(model) { if (model == null) return false; if ( - this._name === model._name && - this._count === model._count && + this.name === model.name && + this.count === model.count && this.weapons.length === model.weapons.length && - this._upgrades.length === model._upgrades.length + this.upgrades.length === model.upgrades.length ) { for (let wi = 0; wi < this.weapons.length; wi++) { if (!this.weapons[wi].equal(model.weapons[wi])) { return false; } } - for (let wi = 0; wi < this._upgrades.length; wi++) { - if (!this._upgrades[wi].equal(model._upgrades[wi])) { + for (let wi = 0; wi < this.upgrades.length; wi++) { + if (!this.upgrades[wi].equal(model.upgrades[wi])) { return false; } } // TODO: check for the same psychic powers - if (this._psyker != null || model._psyker != null) return false; + if (this.psyker != null || model.psyker != null) return false; return true; } @@ -167,9 +158,9 @@ export class Model extends BaseNotes { } nameAndGear() { - let name = super.name(); + let name = super.name; - if (this.weapons.length > 0 || this._upgrades.length > 0) { + if (this.weapons.length > 0 || this.upgrades.length > 0) { const gear = this.getDedupedWeaponsAndUpgrades(); name += ` (${gear.map((u) => u.toString()).join(", ")})`; } @@ -178,8 +169,12 @@ export class Model extends BaseNotes { getDedupedWeaponsAndUpgrades() { const deduped = []; - for (const upgrade of [...this.weapons, ...this._upgrades]) { - if (!deduped.some((e) => upgrade.selectionName() === e.selectionName())) { + for (const upgrade of [...this.weapons, ...this.upgrades]) { + if ( + !deduped.some( + (e) => upgrade.getSelectionName() === e.getSelectionName() + ) + ) { deduped.push(upgrade); } } @@ -188,75 +183,75 @@ export class Model extends BaseNotes { normalize() { this.weapons.sort(CompareWeapon); - this._upgrades.sort(CompareObj); + this.upgrades.sort(CompareObj); this.normalizeUpgrades(this.weapons); - this.normalizeUpgrades(this._upgrades); + this.normalizeUpgrades(this.upgrades); } normalizeUpgrades(upgrades) { for (let i = 0; i < upgrades.length - 1; i++) { const upgrade = upgrades[i]; - if (upgrade._name === upgrades[i + 1]._name) { - upgrade._count += upgrades[i + 1]._count; - upgrade._cost.add(upgrades[i + 1]._cost); + if (upgrade.name === upgrades[i + 1].name) { + upgrade.count += upgrades[i + 1].count; + upgrade.cost.add(upgrades[i + 1].cost); upgrades.splice(i + 1, 1); i--; } } for (let upgrade of upgrades) { - if (upgrade._count % this._count == 0) { - upgrade._count /= this._count; - upgrade._cost._points /= this._count; + if (upgrade.count % this.count == 0) { + upgrade.count /= this.count; + upgrade.cost.points /= this.count; } } } } export class Unit extends BaseNotes { - _roleRole = UnitRole.NONE; + roleRole = UnitRole.NONE; factions = new Set(); keywords = new Set(); abilities = {}; rules = new Map(); - _models = []; + models = []; modelStats = []; - _modelList = []; + modelList = []; weapons = []; - _spells = []; - _psykers = []; - _explosions = []; + spells = []; + psykers = []; + explosions = []; - _cost = new Costs(); + cost = new Costs(); - _woundTracker = []; + woundTracker = []; nameWithExtraCosts() { const extraCosts = []; // Track extra costs like cabal points. - for (const freeformCostType in this._cost._freeformValues) { - if (this._cost._freeformValues[freeformCostType] === 0) continue; + for (const freeformCostType in this.cost.freeformValues) { + if (this.cost.freeformValues[freeformCostType] === 0) continue; extraCosts.push( - `${this._cost._freeformValues[freeformCostType]}${freeformCostType}` + `${this.cost.freeformValues[freeformCostType]}${freeformCostType}` ); } return extraCosts.length - ? `${this.name()} [${extraCosts.join(", ")}]` - : this.name(); + ? `${this.name} [${extraCosts.join(", ")}]` + : this.name; } equal(unit) { if (unit == null) return false; if ( - unit._name === this._name && - unit._role === this._role && - unit._models.length === this._models.length && + unit.name === this.name && + unit.role === this.role && + unit.models.length === this.models.length && unit.modelStats.length === this.modelStats.length ) { - for (let mi = 0; mi < this._models.length; mi++) { - if (!this._models[mi].equal(unit._models[mi])) { + for (let mi = 0; mi < this.models.length; mi++) { + if (!this.models[mi].equal(unit.models[mi])) { return false; } } @@ -268,9 +263,9 @@ export class Unit extends BaseNotes { } // Check how to replace without lodash - /* if (!_.isEqual(this.abilities, unit.abilities)) { + /* if (!.isEqual(this.abilities, unit.abilities)) { return false; - } else if (!_.isEqual(this.rules, unit.rules)) { + } else if (!.isEqual(this.rules, unit.rules)) { return false; } */ @@ -281,19 +276,19 @@ export class Unit extends BaseNotes { normalize() { // Sort force units by role and name - this._models.sort(CompareModel); + this.models.sort(CompareModel); this.modelStats.sort(CompareObj); - for (let model of this._models) { + for (let model of this.models) { model.normalize(); } - for (let i = 0; i < this._models.length - 1; i++) { - const model = this._models[i]; + for (let i = 0; i < this.models.length - 1; i++) { + const model = this.models[i]; - if (model.nameAndGear() === this._models[i + 1].nameAndGear()) { - model._count++; - this._models.splice(i + 1, 1); + if (model.nameAndGear() === this.models[i + 1].nameAndGear()) { + model.count++; + this.models.splice(i + 1, 1); i--; } } @@ -307,77 +302,72 @@ export class Unit extends BaseNotes { } } - this._modelList = this._models.map( + this.modelList = this.models.map( (model) => - (model._count > 1 ? `${model._count}x ` : "") + model.nameAndGear() + (model.count > 1 ? `${model.count}x ` : "") + model.nameAndGear() ); - this.weapons = this._models + this.weapons = this.models .map((m) => m.weapons) .reduce((acc, val) => acc.concat(val), []) .sort(CompareWeapon) - .filter((weap, i, array) => weap.name() !== array[i - 1]?.name()); + .filter((weap, i, array) => weap.name !== array[i - 1]?.name); - this._spells.push( - ...this._models - .map((m) => m._psychicPowers) + this.spells.push( + ...this.models + .map((m) => m.psychicPowers) .reduce((acc, val) => acc.concat(val), []) ); - this._psykers.push(...this._models.map((m) => m._psyker).filter((p) => p)); - this._explosions.push( - ...this._models - .map((m) => m._explosions) + this.psykers.push(...this.models.map((m) => m.psyker).filter((p) => p)); + this.explosions.push( + ...this.models + .map((m) => m.explosions) .reduce((acc, val) => acc.concat(val), []) ); } } export class Force extends BaseNotes { - _catalog = ""; - _faction = "Unknown"; - _factionRules = new Map(); - _configurations = []; + catalog = ""; + faction = "Unknown"; + factionRules = new Map(); + configurations = []; rules = new Map(); - _units = []; + units = []; } export class Roster40k extends BaseNotes { - _cost = new Costs(); - _forces = []; + cost = new Costs(); + forces = []; } export class Costs { - _powerLevel = 0; - _commandPoints = 0; - _points = 0; - _freeformValues; + commandPoints = 0; + points = 0; + freeformValues; hasValues() { - return ( - this._powerLevel !== 0 || this._commandPoints !== 0 || this._points !== 0 - ); + return this.commandPoints !== 0 || this.points !== 0; } toString() { const values = []; - if (this._points !== 0) values.push(`${this._points} pts`); - if (this._powerLevel !== 0) values.push(`${this._powerLevel} PL`); - if (this._commandPoints !== 0) values.push(`${this._commandPoints} CP`); + if (this.points !== 0) values.push(`${this.points} pts`); + if (this.commandPoints !== 0) values.push(`${this.commandPoints} CP`); return `[${values.join(" / ")}]`; } add(other) { - this._powerLevel += other._powerLevel; - this._commandPoints += other._commandPoints; - this._points += other._points; - for (const name in other._freeformValues) { - this.addFreeformValue(name, other._freeformValues[name]); + this.commandPoints += other.commandPoints; + this.points += other.points; + for (const name in other.freeformValues) { + this.addFreeformValue(name, other.freeformValues[name]); } } addFreeformValue(name, value) { - if (!this._freeformValues) this._freeformValues = {}; - const oldValue = this._freeformValues[name] || 0; - this._freeformValues[name] = oldValue + value; + if (!this.freeformValues) this.freeformValues = {}; + const oldValue = this.freeformValues[name] || 0; + this.freeformValues[name] = oldValue + value; } } @@ -389,9 +379,9 @@ export function Create40kRoster(doc) { const name = info.getAttributeNode("name")?.nodeValue; if (name) { - roster._name = name; + roster.name = name; } else { - roster._name = "40k Army Roster"; + roster.name = "40k Army Roster"; } ParseRosterPoints(doc, roster); @@ -404,7 +394,7 @@ export function Create40kRoster(doc) { function ParseRosterPoints(doc, roster) { let costs = doc.querySelectorAll("roster>costs>cost"); for (let cost of costs) { - roster._cost.add(ParseCost(cost)); + roster.cost.add(ParseCost(cost)); } } @@ -418,10 +408,10 @@ function ParseForces(doc, roster) { let value = root.getAttributeNode("catalogueName")?.nodeValue; if (which) { - f._name = which; + f.name = which; } if (value) { - f._catalog = value; + f.catalog = value; } // TODO: Determine force faction and faction specific rules. @@ -436,7 +426,7 @@ function ParseForces(doc, roster) { ParseSelections(root, f); - roster._forces.push(f); + roster.forces.push(f); } } } @@ -458,7 +448,7 @@ function ParseSelections(root, force) { ParseConfiguration(selection, force); } else if (selection.querySelector('profile[typeName="Unit"]')) { const unit = ParseUnit(selection); - force._units.push(unit); + force.units.push(unit); for (const entry of unit.rules.entries()) { force.rules.set(entry[0], entry[1]); } @@ -470,11 +460,11 @@ function ParseSelections(root, force) { // sub-faction const name = prop.getAttribute("name"); if (name && prop.getAttribute("type") === "upgrade") { - if (force._faction === "Unknown") { + if (force.faction === "Unknown") { // pick the first upgrade we see - force._faction = name; + force.faction = name; } - ExtractRuleFromSelection(prop, force._factionRules); + ExtractRuleFromSelection(prop, force.factionRules); } } } else { @@ -482,16 +472,16 @@ function ParseSelections(root, force) { } } - for (const key of force._factionRules.keys()) { + for (const key of force.factionRules.keys()) { force.rules.delete(key); } // Sort force units by role and name - force._units.sort((a, b) => { - if (a._role > b._role) return 1; - else if (a._role == b._role) { - if (a._name > b._name) return 1; - else if (a._name == b._name) return 0; + force.units.sort((a, b) => { + if (a.role > b.role) return 1; + else if (a.role == b.role) { + if (a.name > b.name) return 1; + else if (a.name == b.name) return 0; return -1; } return -1; @@ -517,14 +507,14 @@ function ParseConfiguration(selection, force) { if (details.length > 0) configuration += `: ${details.join(", ")}`; if (costs.hasValues()) configuration += ` ${costs.toString()}`; - force._configurations.push(configuration); + force.configurations.push(configuration); } function DuplicateForce(force, roster) { if (!roster || !force) return false; - for (let f of roster._forces) { - if (f._catalog === force._catalog) return true; + for (let f of roster.forces) { + if (f.catalog === force.catalog) return true; } return false; } @@ -591,22 +581,8 @@ function LookupRole(roleText) { } function ExpandBaseNotes(root, obj) { - obj._name = root.getAttributeNode("name")?.nodeValue; - - let element = root; - if ( - root.tagName === "profile" && - root.parentElement && - root.parentElement.parentElement - ) { - element = root.parentElement.parentElement; - } - - let child = element.firstElementChild; - if (child && child.tagName === "customNotes") { - obj._customNotes = child.textContent; - } - return obj._name; + obj.name = root.getAttributeNode("name")?.nodeValue; + return obj.name; } function ExtractNumberFromParent(root) { @@ -676,12 +652,10 @@ function ParseCost(cost) { const which = cost.getAttribute("name"); const value = cost.getAttribute("value"); if (which && value) { - if (which === " PL") { - costs._powerLevel += +value; - } else if (which === "pts") { - costs._points += +value; + if (which === "pts") { + costs.points += +value; } else if (which === "CP") { - costs._commandPoints += +value; + costs.commandPoints += +value; } else { costs.addFreeformValue(which, +value); } @@ -706,7 +680,7 @@ function ParseUnit(root) { const roleText = catName.trim(); let unitRole = LookupRole(roleText); if (unitRole != UnitRole.NONE) { - unit._role = unitRole; + unit.role = unitRole; } else { // Keyword unit.keywords.add(catName); @@ -763,9 +737,9 @@ function ParseUnit(root) { seenProfiles.push(...unseenProfiles); const model = new Model(); - model._name = modelSelection.getAttribute("name") || "Unknown Model"; - model._count = Number(modelSelection.getAttribute("number") || 1); - unit._models.push(model); + model.name = modelSelection.getAttribute("name") || "Unknown Model"; + model.count = Number(modelSelection.getAttribute("number") || 1); + unit.models.push(model); // Find stats for all profiles (weapons, powers, abilities, etc). ParseModelProfiles(profiles, model, unit); @@ -788,10 +762,10 @@ function ParseUnit(root) { let upgradeName = upgradeSelection.getAttribute("name"); if (upgradeName) { const upgrade = new Upgrade(); - upgrade._name = upgradeName; - upgrade._cost = GetSelectionCosts(upgradeSelection); - upgrade._count = Number(upgradeSelection.getAttribute("number")); - model._upgrades.push(upgrade); + upgrade.name = upgradeName; + upgrade.cost = GetSelectionCosts(upgradeSelection); + upgrade.count = Number(upgradeSelection.getAttribute("number")); + model.upgrades.push(upgrade); } } } @@ -805,28 +779,28 @@ function ParseUnit(root) { seenProfiles.push(...unseenProfiles); if (unseenProfiles.length > 0) { const unitUpgradesModel = new Model(); - unitUpgradesModel._name = "Unit Upgrades"; + unitUpgradesModel.name = "Unit Upgrades"; ParseModelProfiles(unseenProfiles, unitUpgradesModel, unit); - if (unitUpgradesModel.weapons.length > 0 && unit._models.length > 0) { + if (unitUpgradesModel.weapons.length > 0 && unit.models.length > 0) { // Apply weapons at the unit level to all models in the unit. - for (const model of unit._models) { + for (const model of unit.models) { model.weapons.push(...unitUpgradesModel.weapons); } unitUpgradesModel.weapons.length = 0; // Clear the array. } - if (unitUpgradesModel._psychicPowers.length > 0) { + if (unitUpgradesModel.psychicPowers.length > 0) { // Add spells to the unit's spell list. However, we'll still need // to add spell upgrade selections to the upgrade list, below. - unit._spells.push(...unitUpgradesModel._psychicPowers); - unitUpgradesModel._psychicPowers.length = 0; + unit.spells.push(...unitUpgradesModel.psychicPowers); + unitUpgradesModel.psychicPowers.length = 0; } - if (unitUpgradesModel._psyker) { - unit._psykers.push(unitUpgradesModel._psyker); - unitUpgradesModel._psyker = null; + if (unitUpgradesModel.psyker) { + unit.psykers.push(unitUpgradesModel.psyker); + unitUpgradesModel.psyker = null; } - if (unitUpgradesModel._explosions.length > 0) { - unit._explosions.push(...unitUpgradesModel._explosions); - unitUpgradesModel._explosions.length = 0; + if (unitUpgradesModel.explosions.length > 0) { + unit.explosions.push(...unitUpgradesModel.explosions); + unitUpgradesModel.explosions.length = 0; } // Look for any unit-level upgrade selections that we didn't catch @@ -843,24 +817,24 @@ function ParseUnit(root) { if (!name) continue; const upgrade = new Upgrade(); - upgrade._name = name; - upgrade._cost = GetSelectionCosts(selection); - upgrade._count = Number(selection.getAttribute("number")); - unitUpgradesModel._upgrades.push(upgrade); + upgrade.name = name; + upgrade.cost = GetSelectionCosts(selection); + upgrade.count = Number(selection.getAttribute("number")); + unitUpgradesModel.upgrades.push(upgrade); } if ( unitUpgradesModel.weapons.length > 0 || - unitUpgradesModel._upgrades.length > 0 + unitUpgradesModel.upgrades.length > 0 ) { - unit._models.push(unitUpgradesModel); + unit.models.push(unitUpgradesModel); } } // Only match costs->costs associated with the unit and not its children (model and weapon) costs. let costs = root.querySelectorAll("costs>cost"); for (let cost of costs) { - unit._cost.add(ParseCost(cost)); + unit.cost.add(ParseCost(cost)); } let rules = root.querySelectorAll("rules > rule"); @@ -879,7 +853,7 @@ function ParseModelStatsProfiles(profiles, unit, unitName) { if (!profileName || !profileType) return; const model = new Model(); - model._name = profileName; + model.name = profileName; unit.modelStats.push(model); ExpandBaseNotes(profile, model); @@ -895,10 +869,10 @@ function ParseModelStatsProfiles(profiles, unit, unitName) { model.move = char.textContent; break; case "WS": - model._ws = char.textContent; + model.ws = char.textContent; break; case "BS": - model._bs = char.textContent; + model.bs = char.textContent; break; case "S": model.str = +char.textContent; @@ -910,7 +884,7 @@ function ParseModelStatsProfiles(profiles, unit, unitName) { model.wounds = +char.textContent; break; case "A": - model._attacks = char.textContent; + model.attacks = char.textContent; break; case "Ld": model.leadership = +char.textContent; @@ -945,15 +919,15 @@ function ParseModelProfiles(profiles, model, unit) { typeName.includes(" Wounds") ) { const tracker = ParseWoundTrackerProfile(profile); - unit._woundTracker.push(tracker); + unit.woundTracker.push(tracker); } else if (typeName == "Psychic Power") { const power = ParsePsychicPowerProfile(profile); - model._psychicPowers.push(power); + model.psychicPowers.push(power); } else if (typeName.includes("Explosion")) { const explosion = ParseExplosionProfile(profile); - model._explosions.push(explosion); + model.explosions.push(explosion); } else if (typeName == "Psyker") { - model._psyker = ParsePsykerProfile(profile); + model.psyker = ParsePsykerProfile(profile); } else { // Everything else, like Prayers and Warlord Traits. if (!unit.abilities[typeName]) unit.abilities[typeName] = new Map(); @@ -986,7 +960,7 @@ function ParseProfileCharacteristics(profile, profileName, typeName, map) { function ParseWeaponProfile(profile) { const weapon = new Weapon(); ExpandBaseNotes(profile, weapon); - weapon._count = ExtractNumberFromParent(profile); + weapon.count = ExtractNumberFromParent(profile); let chars = profile.querySelectorAll("characteristics>characteristic"); for (let char of chars) { @@ -995,19 +969,19 @@ function ParseWeaponProfile(profile) { if (char.textContent) { switch (charName) { case "Range": - weapon._range = char.textContent; + weapon.range = char.textContent; break; case "Type": - weapon._type = char.textContent; + weapon.type = char.textContent; break; case "S": weapon.str = char.textContent; break; case "AP": - weapon._ap = char.textContent; + weapon.ap = char.textContent; break; case "D": - weapon._damage = char.textContent; + weapon.damage = char.textContent; break; case "Abilities": weapon.abilities = char.textContent; @@ -1021,8 +995,8 @@ function ParseWeaponProfile(profile) { const selection = profile.parentElement?.parentElement; const selectionName = selection?.getAttribute("name"); if (selection?.getAttribute("type") === "upgrade" && selectionName) { - weapon._selectionName = selectionName; - weapon._cost = GetSelectionCosts(selection); + weapon.selectionName = selectionName; + weapon.cost = GetSelectionCosts(selection); } return weapon; } @@ -1035,9 +1009,9 @@ function ParseWoundTrackerProfile(profile) { const charName = char.getAttribute("name"); if (charName) { if (char.textContent) { - tracker._table.set(charName, char.textContent); + tracker.table.set(charName, char.textContent); } else { - tracker._table.set(charName, "-"); + tracker.table.set(charName, "-"); } } } @@ -1054,13 +1028,13 @@ function ParsePsychicPowerProfile(profile) { if (charName && char.textContent) { switch (charName) { case "Range": - power._range = char.textContent; + power.range = char.textContent; break; case "Warp Charge": - power._manifest = +char.textContent; + power.manifest = +char.textContent; break; case "Details": - power._details = char.textContent; + power.details = char.textContent; break; } } @@ -1078,13 +1052,13 @@ function ParseExplosionProfile(profile) { if (charName && char.textContent) { switch (charName) { case "Dice Roll": - explosion._diceRoll = char.textContent; + explosion.diceRoll = char.textContent; break; case "Distance": - explosion._distance = char.textContent; + explosion.distance = char.textContent; break; case "Mortal Wounds": - explosion._mortalWounds = char.textContent; + explosion.mortalWounds = char.textContent; break; } } @@ -1102,16 +1076,16 @@ function ParsePsykerProfile(profile) { if (charName && char.textContent) { switch (charName) { case "Cast": - psyker._cast = char.textContent; + psyker.cast = char.textContent; break; case "Deny": - psyker._deny = char.textContent; + psyker.deny = char.textContent; break; case "Powers Known": - psyker._powers = char.textContent; + psyker.powers = char.textContent; break; case "Other": - psyker._other = char.textContent; + psyker.other = char.textContent; break; } } @@ -1120,35 +1094,35 @@ function ParsePsykerProfile(profile) { } function CompareObj(a, b) { - return Compare(a._name, b._name); + return Compare(a.name, b.name); } function CompareModel(a, b) { - if (a._name === b._name) { + if (a.name === b.name) { return Compare(a.nameAndGear(), b.nameAndGear()); - } else if (a._name === "Unit Upgrades") { + } else if (a.name === "Unit Upgrades") { // "Unit Upgrades", a special model name, is always sorted last. return 1; - } else if (b._name === "Unit Upgrades") { + } else if (b.name === "Unit Upgrades") { // "Unit Upgrades", a special model name, is always sorted last. return -1; } else { - return Compare(a._name, b._name); + return Compare(a.name, b.name); } } export function CompareWeapon(a, b) { - const aType = a._type.startsWith("Grenade") + const aType = a.type.startsWith("Grenade") ? 2 - : a._type.startsWith("Melee") + : a.type.startsWith("Melee") ? 1 : 0; - const bType = b._type.startsWith("Grenade") + const bType = b.type.startsWith("Grenade") ? 2 - : b._type.startsWith("Melee") + : b.type.startsWith("Melee") ? 1 : 0; - return aType - bType || a.name().localeCompare(b.name()); + return aType - bType || a.name.localeCompare(b.name); } export function Compare(a, b) {