default melee profile, abilties, prepare for "launch"

This commit is contained in:
NilsUeter
2023-04-13 21:56:32 +02:00
parent 10434057b3
commit aefd7244b6
9 changed files with 491 additions and 343 deletions
+58 -7
View File
@@ -2,6 +2,8 @@ import JSZip from "jszip";
import { Create40kRoster } from "./roster40k"; import { Create40kRoster } from "./roster40k";
import { useState } from "react"; import { useState } from "react";
import { Roster } from "./Roster"; import { Roster } from "./Roster";
import Demo0 from "./assets/Demo0.png";
import Demo1 from "./assets/Demo1.png";
function App() { function App() {
const [error, setError] = useState(); const [error, setError] = useState();
@@ -55,7 +57,7 @@ function App() {
console.log(doc); console.log(doc);
const roster = Create40kRoster(doc); const roster = Create40kRoster(doc);
console.log(roster); console.log(roster);
if (roster && roster._forces.length > 0) { if (roster && roster.forces.length > 0) {
setRoster(roster); setRoster(roster);
/* const renderer = new Renderer40k(roster); /* const renderer = new Renderer40k(roster);
renderer.render(rosterTitle, rosterList, forceUnits); */ renderer.render(rosterTitle, rosterList, forceUnits); */
@@ -67,13 +69,62 @@ function App() {
return ( return (
<div className="App"> <div className="App">
<label> <div className="header print-display-none">
<input type="file" accept=".ros, .rosz" onChange={handleFileSelect} /> FancyScribe
Select roster file (.ros, .rosz) <div className="subheader">
</label> A fancy way to view your Warhammer 40k BattleScribe rosters.
<div style={{ color: "red" }}>{error}</div> </div>
</div>
<Roster roster={roster} /> <div className="body">
<input
type="file"
name="rosterUpload"
id="rosterUpload"
onChange={handleFileSelect}
style={{ display: "none" }}
/>
<label
for="rosterUpload"
className={
"print-display-none rosterUpload" +
" " +
(roster ? "rosterUploaded" : "")
}
id="rosterUploadContainer"
>
<div id="preloadContainer">
<span>Upload roster file (.ros, .rosz)</span>
</div>
</label>
<div style={{ color: "red" }}>{error}</div>
<Roster roster={roster} />
<div
className="print-display-none"
style={{ display: roster ? "none" : "" }}
>
<div style={{ padding: "8px 0", fontSize: "1.7rem" }}>About</div>
<div style={{ fontSize: "1.2rem" }}>
PrettyScribe is a website that renders{" "}
<a href="https://www.battlescribe.net/" target="_blank">
BattleScribe
</a>{" "}
roster files in an opinionated format inspired by the new 10th
edition datacards. Inspiration and large parts of the parsing logic
come from the{" "}
<a href="https://rweyrauch.github.io/PrettyScribe" target="_blank">
PrettyScribe
</a>{" "}
application.
</div>
<div style={{ padding: "8px 0", fontSize: "1.7rem" }}>
Output Examples
</div>
<img src={Demo0} />
<img src={Demo1} />
</div>
</div>
</div> </div>
); );
} }
+78 -39
View File
@@ -1,14 +1,14 @@
import factionBackground from "./assets/factionBackground.png"; import factionBackground from "./assets/factionBackground.png";
import adeptusAstartesIcon from "./assets/adeptusAstartesIcon.png"; import adeptusAstartesIcon from "./assets/adeptusAstartesIcon.png";
import rangedIcon from "./assets/rangedIcon.png"; import rangedIcon from "./assets/rangedIcon.png";
import { Arrow } from "./assets/icons"; import { Arrow, wavyLine } from "./assets/icons";
import { Weapons } from "./Weapons"; import { Weapons } from "./Weapons";
export const Roster = ({ roster }) => { export const Roster = ({ roster }) => {
if (!roster) { if (!roster) {
return null; return null;
} }
const { _name, _cost, _forces } = roster; const { name, cost, forces } = roster;
return ( return (
<div <div
style={{ style={{
@@ -19,17 +19,18 @@ export const Roster = ({ roster }) => {
> >
<div <div
style={{ style={{
backgroundColor: "var(--primary-color)", backgroundColor: "#536766",
color: "#fff", color: "#fff",
padding: "4px 16px", padding: "4px 16px",
fontSize: " 1.7em", fontSize: " 1.7em",
fontWeight: "800", fontWeight: "800",
textTransform: "uppercase", textTransform: "uppercase",
marginBottom: -16,
}} }}
> >
{_name} [{_cost._commandPoints} CP] {name} [{cost.commandPoints} CP]
</div> </div>
{_forces.map((force, index) => ( {forces.map((force, index) => (
<Force key={index} force={force} /> <Force key={index} force={force} />
))} ))}
</div> </div>
@@ -84,9 +85,9 @@ const getPrimaryColor = (catalog) => {
}; };
const Force = ({ force }) => { const Force = ({ force }) => {
const { _units, rules, _catalog } = force; const { units, rules, catalog } = force;
const primaryColor = getPrimaryColor(_catalog); const primaryColor = getPrimaryColor(catalog);
return ( return (
<div <div
style={{ style={{
@@ -95,7 +96,7 @@ const Force = ({ force }) => {
"--primary-color-transparent": "#53676660", "--primary-color-transparent": "#53676660",
}} }}
> >
{_units.map((unit, index) => ( {units.map((unit, index) => (
<Unit key={index} unit={unit} /> <Unit key={index} unit={unit} />
))} ))}
<ForceRules rules={rules} /> <ForceRules rules={rules} />
@@ -105,35 +106,47 @@ const Force = ({ force }) => {
const Unit = ({ unit }) => { const Unit = ({ unit }) => {
let { let {
_name, name,
weapons, weapons,
abilities, abilities,
keywords, keywords,
factions, factions,
rules, rules,
modelStats, modelStats,
_modelList, modelList,
_psykers, psykers,
} = unit; } = unit;
const hasDifferentProfiles = weapons.some( const hasDifferentProfiles = weapons.some(
(weapon) => (weapon) =>
weapon._selectionName !== weapon._name && weapon.selectionName !== weapon.name &&
!weapon._name.includes("(Shooting)") && !weapon.name.includes("(Shooting)") &&
!weapon._name.includes("(Melee)") !weapon.name.includes("(Melee)")
); );
const meleeWeapons = weapons const meleeWeapons = weapons
.filter((weapon) => weapon._range === "Melee" && weapon._range !== "-") .filter((weapon) => weapon.range === "Melee" && weapon.range !== "-")
.sort((a, b) => a._selectionName.localeCompare(b._selectionName)); .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 const rangedWeapons = weapons
.filter((weapon) => weapon._range !== "Melee" && weapon._range !== "-") .filter((weapon) => weapon.range !== "Melee" && weapon.range !== "-")
.sort((a, b) => a._selectionName.localeCompare(b._selectionName)); .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" 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 ( return (
<div <div
className="avoid-page-break" className="avoid-page-break"
style={{ style={{
fontSize: 13,
fontWeight: 500, fontWeight: 500,
border: "2px solid var(--primary-color)", border: "2px solid var(--primary-color)",
backgroundColor: "#DFE0E2", backgroundColor: "#DFE0E2",
@@ -141,8 +154,7 @@ const Unit = ({ unit }) => {
> >
<div <div
style={{ style={{
padding: "24px 16px", padding: "24px 0",
paddingRight: 0,
paddingBottom: 24, paddingBottom: 24,
background: background:
"linear-gradient(90deg, rgba(20,21,25,1) 0%, rgba(48,57,62,1) 45%, rgba(73,74,79,1) 100%)", "linear-gradient(90deg, rgba(20,21,25,1) 0%, rgba(48,57,62,1) 45%, rgba(73,74,79,1) 100%)",
@@ -153,7 +165,6 @@ const Unit = ({ unit }) => {
style={{ style={{
padding: "4px 16px", padding: "4px 16px",
color: "#fff", color: "#fff",
minHeight: 85,
position: "relative", position: "relative",
}} }}
> >
@@ -162,11 +173,40 @@ const Unit = ({ unit }) => {
position: "absolute", position: "absolute",
left: 0, left: 0,
top: 0, top: 0,
backgroundColor: "var(--primary-color-transparent)",
minHeight: 85,
width: "100%", width: "100%",
display: "flex",
}} }}
></div> >
<div
style={{
backgroundColor: "var(--primary-color-transparent)",
height: 80,
minWidth: 340,
}}
></div>
<div
style={{
height: 80,
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
backgroundColor: "var(--primary-color-transparent)",
height: 40,
}}
></div>
{wavyLine}
</div>
<div
style={{
flex: "10",
backgroundColor: "var(--primary-color-transparent)",
height: 40,
}}
></div>
</div>
<div <div
style={{ style={{
fontSize: "1.7em", fontSize: "1.7em",
@@ -176,7 +216,7 @@ const Unit = ({ unit }) => {
position: "relative", position: "relative",
}} }}
> >
{_name} {name}
</div> </div>
<div <div
style={{ style={{
@@ -192,7 +232,7 @@ const Unit = ({ unit }) => {
<ModelStats <ModelStats
key={index} key={index}
modelStats={model} modelStats={model}
modelList={_modelList} modelList={modelList}
index={index} index={index}
showName={modelStats.length > 1} showName={modelStats.length > 1}
/> />
@@ -225,7 +265,7 @@ const Unit = ({ unit }) => {
weapons={meleeWeapons} weapons={meleeWeapons}
modelStats={modelStats} modelStats={modelStats}
/> />
<Psykers title="PSYKER" psykers={_psykers} /> <Psykers title="PSYKER" psykers={psykers} />
</table> </table>
<div style={{ flex: "1" }}></div> <div style={{ flex: "1" }}></div>
{hasDifferentProfiles && ( {hasDifferentProfiles && (
@@ -282,12 +322,12 @@ const Unit = ({ unit }) => {
}; };
const ModelStats = ({ modelStats, index, showName, modelList }) => { const ModelStats = ({ modelStats, index, showName, modelList }) => {
let { move, toughness, save, wounds, leadership, _name } = modelStats; let { move, toughness, save, wounds, leadership, name } = modelStats;
if (!wounds) { if (!wounds) {
wounds = "/"; wounds = "/";
} }
const modelListMatches = modelList const modelListMatches = modelList
.filter((model) => model.includes(_name)) .filter((model) => model.includes(name))
.map((model) => "(" + model.split("(")[1]); .map((model) => "(" + model.split("(")[1]);
return ( return (
<div style={{ display: "flex", gap: 16, alignItems: "center" }}> <div style={{ display: "flex", gap: 16, alignItems: "center" }}>
@@ -305,7 +345,7 @@ const ModelStats = ({ modelStats, index, showName, modelList }) => {
whiteSpace: "nowrap", whiteSpace: "nowrap",
}} }}
> >
{_name} {name}
</div> </div>
<div <div
style={{ style={{
@@ -529,7 +569,7 @@ const Psykers = ({ title, psykers }) => {
)} )}
<tbody> <tbody>
{psykers.map((psyker, index) => ( {psykers.map((psyker, index) => (
<Psyker key={psyker._name} psyker={psyker} index={index} /> <Psyker key={psyker.name} psyker={psyker} index={index} />
))} ))}
{psykers.length > 0 && ( {psykers.length > 0 && (
<tr className="emptyRow"> <tr className="emptyRow">
@@ -543,7 +583,7 @@ const Psykers = ({ title, psykers }) => {
}; };
const Psyker = ({ psyker, index }) => { const Psyker = ({ psyker, index }) => {
let { _name, _cast, _deny, _powers } = psyker; let { name, cast, deny, powers } = psyker;
return ( return (
<tr className={index % 2 ? "rowOtherColor" : ""}> <tr className={index % 2 ? "rowOtherColor" : ""}>
@@ -557,13 +597,13 @@ const Psyker = ({ psyker, index }) => {
gap: "0 4px", gap: "0 4px",
}} }}
> >
{_name} {name}
</div> </div>
</td> </td>
<td>{_cast}</td> <td>{cast}</td>
<td>{_deny}</td> <td>{deny}</td>
<td style={{ textAlign: "left" }} colSpan="4"> <td style={{ textAlign: "left" }} colSpan="4">
{_powers} {powers}
</td> </td>
</tr> </tr>
); );
@@ -588,7 +628,6 @@ const ForceRules = ({ rules }) => {
<div <div
key={rule} key={rule}
style={{ style={{
fontSize: ".8em",
lineHeight: 1.4, lineHeight: 1.4,
}} }}
> >
+83 -59
View File
@@ -32,12 +32,11 @@ export const Weapons = ({ title, weapons, modelStats }) => {
<tbody> <tbody>
{weapons.map((weapon, index) => ( {weapons.map((weapon, index) => (
<Weapon <Weapon
key={weapon._name} key={weapon.name}
weapon={weapon} weapon={weapon}
modelStats={modelStats} modelStats={modelStats}
isMelee={isMelee} isMelee={isMelee}
className={getWeaponClassNames(weapons, index)} className={getWeaponClassNames(weapons, index)}
index={index}
/> />
))} ))}
{weapons.length > 0 && ( {weapons.length > 0 && (
@@ -51,77 +50,102 @@ export const Weapons = ({ title, weapons, modelStats }) => {
); );
}; };
const Weapon = ({ weapon, modelStats, isMelee, className, index }) => { const Weapon = ({ weapon, modelStats, isMelee, className }) => {
let { _name, _selectionName, _range, _type, str, _ap, _damage } = weapon; let { name, selectionName, range, type, str, ap, damage, abilities } = weapon;
var lastWhiteSpace = _type.lastIndexOf(" "); var lastWhiteSpace = type.lastIndexOf(" ");
const type = _type.substring(0, lastWhiteSpace); type = type.substring(0, lastWhiteSpace);
const attacks = _type.substring(lastWhiteSpace + 1); const attacks = type.substring(lastWhiteSpace + 1);
const bs = modelStats[0]._bs; const bs = modelStats[0].bs;
const ws = modelStats[0]._ws; const ws = modelStats[0].ws;
const strModel = modelStats[0].str; const strModel = modelStats[0].str;
const meleeAttacks = modelStats[0]._attacks; const meleeAttacks = modelStats[0].attacks;
if (_name === "Krak grenades") { if (name === "Krak grenades") {
_name = "Krak grenade"; name = "Krak grenade";
} }
const differentProfiles = const differentProfiles =
_selectionName !== _name && selectionName !== name &&
!_name.includes("(Shooting)") && !name.includes("(Shooting)") &&
!_name.includes("(Melee)"); !name.includes("(Melee)");
const interestingType = type && type !== "Melee"; const interestingType = type && type !== "Melee";
if (differentProfiles && _name.endsWith(" grenades")) { if (differentProfiles && name.endsWith(" grenades")) {
_name = _name.replace(" grenades", ""); name = name.replace(" grenades", "");
} }
if (differentProfiles && _name.endsWith(" grenade")) { if (differentProfiles && name.endsWith(" grenade")) {
_name = _name.replace(" grenade", ""); name = name.replace(" grenade", "");
} }
if (differentProfiles && _name.includes(" - ")) { if (differentProfiles && name.includes(" - ")) {
_name = _name.split(" - ")[1]; name = name.split(" - ")[1];
} }
if (abilities === "Blast") {
abilities = abilities.replaceAll("Blast", "");
type += ", Blast";
}
return ( return (
<tr className={className}> <>
<td style={{ borderTop: "none", backgroundColor: "#dfe0e2" }}> <tr className={className}>
{differentProfiles && Arrow} <td style={{ borderTop: "none", backgroundColor: "#dfe0e2" }}>
</td> {differentProfiles && Arrow}
<td style={{ textAlign: "left" }}> </td>
<div <td style={{ textAlign: "left" }}>
style={{ <div
display: "flex", style={{
alignItems: "center", display: "flex",
flexWrap: "wrap", alignItems: "center",
gap: "0 4px", flexWrap: "wrap",
}} gap: "0 4px",
> }}
{differentProfiles && _selectionName + " - "} >
{_name} {differentProfiles && selectionName + " - "}
{interestingType && ( {name}
<span {interestingType && (
style={{ <span
fontSize: ".8em", style={{
fontWeight: 700, fontSize: ".8em",
color: "var(--primary-color)", fontWeight: 700,
}} color: "var(--primary-color)",
> }}
[{type}] >
</span> [{type}]
)} </span>
</div> )}
</td> </div>
<td>{_range}</td> </td>
<td>{isMelee ? meleeAttacks : attacks}</td> <td>{range}</td>
<td>{isMelee ? ws : bs}</td> <td>{isMelee ? meleeAttacks : attacks}</td>
<td>{isMelee ? calculateWeaponStrength(strModel, str) : str}</td> <td>{isMelee ? ws : bs}</td>
<td>{_ap}</td> <td>{isMelee ? calculateWeaponStrength(strModel, str) : str}</td>
<td>{_damage}</td> <td>{ap}</td>
</tr> <td>{damage}</td>
</tr>
{abilities && abilities !== "-" && (
<tr className={className + " " + "noBorderTop"}>
<td style={{ backgroundColor: "#dfe0e2" }}></td>
<td
colSpan="7"
style={{
textAlign: "left",
fontSize: "0.9em",
paddingTop: 0,
paddingBottom: 1,
lineHeight: 1.4,
}}
>
{abilities}
</td>
</tr>
)}
</>
); );
}; };
const getWeaponClassNames = (weapons, index) => { const getWeaponClassNames = (weapons, index) => {
let differentColor = false; let differentColor = false;
for (let i = 1; i <= index; i++) { for (let i = 1; i <= index; i++) {
let { _selectionName } = weapons[i]; let { selectionName } = weapons[i];
if (_selectionName !== weapons[i - 1]._selectionName) { if (selectionName !== weapons[i - 1].selectionName) {
differentColor = !differentColor; differentColor = !differentColor;
} }
} }
@@ -130,7 +154,7 @@ const getWeaponClassNames = (weapons, index) => {
if (index === 0) classes.push("noBorderTop"); if (index === 0) classes.push("noBorderTop");
if ( if (
index > 0 && index > 0 &&
weapons[index]._selectionName === weapons[index - 1]._selectionName weapons[index].selectionName === weapons[index - 1].selectionName
) )
classes.push("noBorderTop"); classes.push("noBorderTop");
return classes.join(" "); return classes.join(" ");
Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

+10
View File
@@ -3,3 +3,13 @@ export const Arrow = (
<path d="m0 0h10l6 4-6 4h-10z" /> <path d="m0 0h10l6 4-6 4h-10z" />
</svg> </svg>
); );
export const wavyLine = (
<svg
height={40}
width={100}
viewBox="0 0 100 40"
style={{ fill: "var(--primary-color-transparent)" }}
>
<path d="m0 0h100c-32 0-68 40-100 40z" />
</svg>
);
+3
View File
@@ -0,0 +1,3 @@
<svg version="1.1" viewBox="0 0 80 40" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="m0 0h80c-16 0-64 40-80 40z"/>
</svg>

After

Width:  |  Height:  |  Size: 186 B

+53 -6
View File
@@ -3,6 +3,7 @@
Arial, sans-serif; Arial, sans-serif;
line-height: 1.5; line-height: 1.5;
font-weight: 400; font-weight: 400;
font-size: 13px;
color-scheme: light dark; color-scheme: light dark;
color: rgba(255, 255, 255, 0.87); color: rgba(255, 255, 255, 0.87);
@@ -32,8 +33,6 @@ a:hover {
body { body {
margin: 0; margin: 0;
display: flex;
place-items: center;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
} }
@@ -76,19 +75,67 @@ button:focus-visible {
} }
#root { #root {
max-width: 1100px;
margin-left: auto;
margin-right: auto;
color: #111113; color: #111113;
} }
.App { .body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
padding: 24px 32px; 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 { .weapons-table {
+206 -232
View File
@@ -1,48 +1,39 @@
export class BaseNotes { export class BaseNotes {
_name = ""; name = "";
_customNotes = "";
name() {
return this._name;
}
notes() {
return this._customNotes;
}
equal(other) { equal(other) {
if (other == null) return false; if (other == null) return false;
// Weapons in 40k have unique names // Weapons in 40k have unique names
return this._name === other._name; return this.name === other.name;
} }
} }
/** A `selection` attached to a unit or model. */ /** A `selection` attached to a unit or model. */
export class Upgrade extends BaseNotes { export class Upgrade extends BaseNotes {
_cost = new Costs(); cost = new Costs();
_count = 1; count = 1;
selectionName() { getSelectionName() {
return this.name(); return this.name;
} }
toString() { toString() {
let string = this.selectionName(); let string = this.getSelectionName();
if (this._count > 1) string = `${this._count}x ${string}`; if (this.count > 1) string = `${this.count}x ${string}`;
if (this._cost.hasValues()) string += ` ${this._cost.toString()}`; if (this.cost.hasValues()) string += ` ${this.cost.toString()}`;
return string; return string;
} }
} }
/** A weapon `profile` that is under a `selection`. */ /** A weapon `profile` that is under a `selection`. */
export class Weapon extends Upgrade { export class Weapon extends Upgrade {
_selectionName = ""; selectionName = "";
_range = "Melee"; range = "Melee";
_type = "Melee"; type = "Melee";
str = "user"; str = "user";
_ap = ""; ap = "";
_damage = ""; damage = "";
abilities = ""; abilities = "";
@@ -50,35 +41,35 @@ export class Weapon extends Upgrade {
* Name of this weapon's `selection`. This is different from name() because * Name of this weapon's `selection`. This is different from name() because
* name() is used for sorting and deduping weapon profiles. * name() is used for sorting and deduping weapon profiles.
*/ */
selectionName() { getSelectionName() {
return this._selectionName || this.name(); return this.selectionName || this.name;
} }
} }
export class WoundTracker extends BaseNotes { export class WoundTracker extends BaseNotes {
_name = ""; name = "";
_table = new Map(); table = new Map();
} }
export class Explosion extends BaseNotes { export class Explosion extends BaseNotes {
_name = ""; name = "";
_diceRoll = ""; diceRoll = "";
_distance = ""; distance = "";
_mortalWounds = ""; mortalWounds = "";
} }
export class Psyker extends BaseNotes { export class Psyker extends BaseNotes {
_cast = ""; cast = "";
_deny = ""; deny = "";
_powers = ""; powers = "";
_other = ""; other = "";
} }
export class PsychicPower extends BaseNotes { export class PsychicPower extends BaseNotes {
_name = ""; name = "";
_manifest = 0; manifest = 0;
_range = ""; range = "";
_details = ""; details = "";
} }
export const UnitRole = { export const UnitRole = {
@@ -118,48 +109,48 @@ export const UnitRoleToString = [
]; ];
export class Model extends BaseNotes { export class Model extends BaseNotes {
_count = 0; count = 0;
// Characteristics // Characteristics
move = '0"'; move = '0"';
_ws = ""; ws = "";
_bs = ""; bs = "";
str = 4; str = 4;
toughness = 4; toughness = 4;
wounds = 1; wounds = 1;
_attacks = ""; attacks = "";
leadership = 7; leadership = 7;
save = ""; save = "";
weapons = []; weapons = [];
_upgrades = []; upgrades = [];
// TODO model upgrades (i.e. tau support systems) // TODO model upgrades (i.e. tau support systems)
_psyker = null; psyker = null;
_psychicPowers = []; psychicPowers = [];
_explosions = []; explosions = [];
equal(model) { equal(model) {
if (model == null) return false; if (model == null) return false;
if ( if (
this._name === model._name && this.name === model.name &&
this._count === model._count && this.count === model.count &&
this.weapons.length === model.weapons.length && 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++) { for (let wi = 0; wi < this.weapons.length; wi++) {
if (!this.weapons[wi].equal(model.weapons[wi])) { if (!this.weapons[wi].equal(model.weapons[wi])) {
return false; return false;
} }
} }
for (let wi = 0; wi < this._upgrades.length; wi++) { for (let wi = 0; wi < this.upgrades.length; wi++) {
if (!this._upgrades[wi].equal(model._upgrades[wi])) { if (!this.upgrades[wi].equal(model.upgrades[wi])) {
return false; return false;
} }
} }
// TODO: check for the same psychic powers // 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; return true;
} }
@@ -167,9 +158,9 @@ export class Model extends BaseNotes {
} }
nameAndGear() { 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(); const gear = this.getDedupedWeaponsAndUpgrades();
name += ` (${gear.map((u) => u.toString()).join(", ")})`; name += ` (${gear.map((u) => u.toString()).join(", ")})`;
} }
@@ -178,8 +169,12 @@ export class Model extends BaseNotes {
getDedupedWeaponsAndUpgrades() { getDedupedWeaponsAndUpgrades() {
const deduped = []; const deduped = [];
for (const upgrade of [...this.weapons, ...this._upgrades]) { for (const upgrade of [...this.weapons, ...this.upgrades]) {
if (!deduped.some((e) => upgrade.selectionName() === e.selectionName())) { if (
!deduped.some(
(e) => upgrade.getSelectionName() === e.getSelectionName()
)
) {
deduped.push(upgrade); deduped.push(upgrade);
} }
} }
@@ -188,75 +183,75 @@ export class Model extends BaseNotes {
normalize() { normalize() {
this.weapons.sort(CompareWeapon); this.weapons.sort(CompareWeapon);
this._upgrades.sort(CompareObj); this.upgrades.sort(CompareObj);
this.normalizeUpgrades(this.weapons); this.normalizeUpgrades(this.weapons);
this.normalizeUpgrades(this._upgrades); this.normalizeUpgrades(this.upgrades);
} }
normalizeUpgrades(upgrades) { normalizeUpgrades(upgrades) {
for (let i = 0; i < upgrades.length - 1; i++) { for (let i = 0; i < upgrades.length - 1; i++) {
const upgrade = upgrades[i]; const upgrade = upgrades[i];
if (upgrade._name === upgrades[i + 1]._name) { if (upgrade.name === upgrades[i + 1].name) {
upgrade._count += upgrades[i + 1]._count; upgrade.count += upgrades[i + 1].count;
upgrade._cost.add(upgrades[i + 1]._cost); upgrade.cost.add(upgrades[i + 1].cost);
upgrades.splice(i + 1, 1); upgrades.splice(i + 1, 1);
i--; i--;
} }
} }
for (let upgrade of upgrades) { for (let upgrade of upgrades) {
if (upgrade._count % this._count == 0) { if (upgrade.count % this.count == 0) {
upgrade._count /= this._count; upgrade.count /= this.count;
upgrade._cost._points /= this._count; upgrade.cost.points /= this.count;
} }
} }
} }
} }
export class Unit extends BaseNotes { export class Unit extends BaseNotes {
_roleRole = UnitRole.NONE; roleRole = UnitRole.NONE;
factions = new Set(); factions = new Set();
keywords = new Set(); keywords = new Set();
abilities = {}; abilities = {};
rules = new Map(); rules = new Map();
_models = []; models = [];
modelStats = []; modelStats = [];
_modelList = []; modelList = [];
weapons = []; weapons = [];
_spells = []; spells = [];
_psykers = []; psykers = [];
_explosions = []; explosions = [];
_cost = new Costs(); cost = new Costs();
_woundTracker = []; woundTracker = [];
nameWithExtraCosts() { nameWithExtraCosts() {
const extraCosts = []; // Track extra costs like cabal points. const extraCosts = []; // Track extra costs like cabal points.
for (const freeformCostType in this._cost._freeformValues) { for (const freeformCostType in this.cost.freeformValues) {
if (this._cost._freeformValues[freeformCostType] === 0) continue; if (this.cost.freeformValues[freeformCostType] === 0) continue;
extraCosts.push( extraCosts.push(
`${this._cost._freeformValues[freeformCostType]}${freeformCostType}` `${this.cost.freeformValues[freeformCostType]}${freeformCostType}`
); );
} }
return extraCosts.length return extraCosts.length
? `${this.name()} [${extraCosts.join(", ")}]` ? `${this.name} [${extraCosts.join(", ")}]`
: this.name(); : this.name;
} }
equal(unit) { equal(unit) {
if (unit == null) return false; if (unit == null) return false;
if ( if (
unit._name === this._name && unit.name === this.name &&
unit._role === this._role && unit.role === this.role &&
unit._models.length === this._models.length && unit.models.length === this.models.length &&
unit.modelStats.length === this.modelStats.length unit.modelStats.length === this.modelStats.length
) { ) {
for (let mi = 0; mi < this._models.length; mi++) { for (let mi = 0; mi < this.models.length; mi++) {
if (!this._models[mi].equal(unit._models[mi])) { if (!this.models[mi].equal(unit.models[mi])) {
return false; return false;
} }
} }
@@ -268,9 +263,9 @@ export class Unit extends BaseNotes {
} }
// Check how to replace without lodash // Check how to replace without lodash
/* if (!_.isEqual(this.abilities, unit.abilities)) { /* if (!.isEqual(this.abilities, unit.abilities)) {
return false; return false;
} else if (!_.isEqual(this.rules, unit.rules)) { } else if (!.isEqual(this.rules, unit.rules)) {
return false; return false;
} */ } */
@@ -281,19 +276,19 @@ export class Unit extends BaseNotes {
normalize() { normalize() {
// Sort force units by role and name // Sort force units by role and name
this._models.sort(CompareModel); this.models.sort(CompareModel);
this.modelStats.sort(CompareObj); this.modelStats.sort(CompareObj);
for (let model of this._models) { for (let model of this.models) {
model.normalize(); model.normalize();
} }
for (let i = 0; i < this._models.length - 1; i++) { for (let i = 0; i < this.models.length - 1; i++) {
const model = this._models[i]; const model = this.models[i];
if (model.nameAndGear() === this._models[i + 1].nameAndGear()) { if (model.nameAndGear() === this.models[i + 1].nameAndGear()) {
model._count++; model.count++;
this._models.splice(i + 1, 1); this.models.splice(i + 1, 1);
i--; i--;
} }
} }
@@ -307,77 +302,72 @@ export class Unit extends BaseNotes {
} }
} }
this._modelList = this._models.map( this.modelList = this.models.map(
(model) => (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) .map((m) => m.weapons)
.reduce((acc, val) => acc.concat(val), []) .reduce((acc, val) => acc.concat(val), [])
.sort(CompareWeapon) .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.spells.push(
...this._models ...this.models
.map((m) => m._psychicPowers) .map((m) => m.psychicPowers)
.reduce((acc, val) => acc.concat(val), []) .reduce((acc, val) => acc.concat(val), [])
); );
this._psykers.push(...this._models.map((m) => m._psyker).filter((p) => p)); this.psykers.push(...this.models.map((m) => m.psyker).filter((p) => p));
this._explosions.push( this.explosions.push(
...this._models ...this.models
.map((m) => m._explosions) .map((m) => m.explosions)
.reduce((acc, val) => acc.concat(val), []) .reduce((acc, val) => acc.concat(val), [])
); );
} }
} }
export class Force extends BaseNotes { export class Force extends BaseNotes {
_catalog = ""; catalog = "";
_faction = "Unknown"; faction = "Unknown";
_factionRules = new Map(); factionRules = new Map();
_configurations = []; configurations = [];
rules = new Map(); rules = new Map();
_units = []; units = [];
} }
export class Roster40k extends BaseNotes { export class Roster40k extends BaseNotes {
_cost = new Costs(); cost = new Costs();
_forces = []; forces = [];
} }
export class Costs { export class Costs {
_powerLevel = 0; commandPoints = 0;
_commandPoints = 0; points = 0;
_points = 0; freeformValues;
_freeformValues;
hasValues() { hasValues() {
return ( return this.commandPoints !== 0 || this.points !== 0;
this._powerLevel !== 0 || this._commandPoints !== 0 || this._points !== 0
);
} }
toString() { toString() {
const values = []; const values = [];
if (this._points !== 0) values.push(`${this._points} pts`); 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._commandPoints !== 0) values.push(`${this._commandPoints} CP`);
return `[${values.join(" / ")}]`; return `[${values.join(" / ")}]`;
} }
add(other) { add(other) {
this._powerLevel += other._powerLevel; this.commandPoints += other.commandPoints;
this._commandPoints += other._commandPoints; this.points += other.points;
this._points += other._points; for (const name in other.freeformValues) {
for (const name in other._freeformValues) { this.addFreeformValue(name, other.freeformValues[name]);
this.addFreeformValue(name, other._freeformValues[name]);
} }
} }
addFreeformValue(name, value) { addFreeformValue(name, value) {
if (!this._freeformValues) this._freeformValues = {}; if (!this.freeformValues) this.freeformValues = {};
const oldValue = this._freeformValues[name] || 0; const oldValue = this.freeformValues[name] || 0;
this._freeformValues[name] = oldValue + value; this.freeformValues[name] = oldValue + value;
} }
} }
@@ -389,9 +379,9 @@ export function Create40kRoster(doc) {
const name = info.getAttributeNode("name")?.nodeValue; const name = info.getAttributeNode("name")?.nodeValue;
if (name) { if (name) {
roster._name = name; roster.name = name;
} else { } else {
roster._name = "40k Army Roster"; roster.name = "40k Army Roster";
} }
ParseRosterPoints(doc, roster); ParseRosterPoints(doc, roster);
@@ -404,7 +394,7 @@ export function Create40kRoster(doc) {
function ParseRosterPoints(doc, roster) { function ParseRosterPoints(doc, roster) {
let costs = doc.querySelectorAll("roster>costs>cost"); let costs = doc.querySelectorAll("roster>costs>cost");
for (let cost of costs) { 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; let value = root.getAttributeNode("catalogueName")?.nodeValue;
if (which) { if (which) {
f._name = which; f.name = which;
} }
if (value) { if (value) {
f._catalog = value; f.catalog = value;
} }
// TODO: Determine force faction and faction specific rules. // TODO: Determine force faction and faction specific rules.
@@ -436,7 +426,7 @@ function ParseForces(doc, roster) {
ParseSelections(root, f); ParseSelections(root, f);
roster._forces.push(f); roster.forces.push(f);
} }
} }
} }
@@ -458,7 +448,7 @@ function ParseSelections(root, force) {
ParseConfiguration(selection, force); ParseConfiguration(selection, force);
} else if (selection.querySelector('profile[typeName="Unit"]')) { } else if (selection.querySelector('profile[typeName="Unit"]')) {
const unit = ParseUnit(selection); const unit = ParseUnit(selection);
force._units.push(unit); force.units.push(unit);
for (const entry of unit.rules.entries()) { for (const entry of unit.rules.entries()) {
force.rules.set(entry[0], entry[1]); force.rules.set(entry[0], entry[1]);
} }
@@ -470,11 +460,11 @@ function ParseSelections(root, force) {
// sub-faction // sub-faction
const name = prop.getAttribute("name"); const name = prop.getAttribute("name");
if (name && prop.getAttribute("type") === "upgrade") { if (name && prop.getAttribute("type") === "upgrade") {
if (force._faction === "Unknown") { if (force.faction === "Unknown") {
// pick the first upgrade we see // pick the first upgrade we see
force._faction = name; force.faction = name;
} }
ExtractRuleFromSelection(prop, force._factionRules); ExtractRuleFromSelection(prop, force.factionRules);
} }
} }
} else { } 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); force.rules.delete(key);
} }
// Sort force units by role and name // Sort force units by role and name
force._units.sort((a, b) => { force.units.sort((a, b) => {
if (a._role > b._role) return 1; if (a.role > b.role) return 1;
else if (a._role == b._role) { else if (a.role == b.role) {
if (a._name > b._name) return 1; if (a.name > b.name) return 1;
else if (a._name == b._name) return 0; else if (a.name == b.name) return 0;
return -1; return -1;
} }
return -1; return -1;
@@ -517,14 +507,14 @@ function ParseConfiguration(selection, force) {
if (details.length > 0) configuration += `: ${details.join(", ")}`; if (details.length > 0) configuration += `: ${details.join(", ")}`;
if (costs.hasValues()) configuration += ` ${costs.toString()}`; if (costs.hasValues()) configuration += ` ${costs.toString()}`;
force._configurations.push(configuration); force.configurations.push(configuration);
} }
function DuplicateForce(force, roster) { function DuplicateForce(force, roster) {
if (!roster || !force) return false; if (!roster || !force) return false;
for (let f of roster._forces) { for (let f of roster.forces) {
if (f._catalog === force._catalog) return true; if (f.catalog === force.catalog) return true;
} }
return false; return false;
} }
@@ -591,22 +581,8 @@ function LookupRole(roleText) {
} }
function ExpandBaseNotes(root, obj) { function ExpandBaseNotes(root, obj) {
obj._name = root.getAttributeNode("name")?.nodeValue; obj.name = root.getAttributeNode("name")?.nodeValue;
return obj.name;
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;
} }
function ExtractNumberFromParent(root) { function ExtractNumberFromParent(root) {
@@ -676,12 +652,10 @@ function ParseCost(cost) {
const which = cost.getAttribute("name"); const which = cost.getAttribute("name");
const value = cost.getAttribute("value"); const value = cost.getAttribute("value");
if (which && value) { if (which && value) {
if (which === " PL") { if (which === "pts") {
costs._powerLevel += +value; costs.points += +value;
} else if (which === "pts") {
costs._points += +value;
} else if (which === "CP") { } else if (which === "CP") {
costs._commandPoints += +value; costs.commandPoints += +value;
} else { } else {
costs.addFreeformValue(which, +value); costs.addFreeformValue(which, +value);
} }
@@ -706,7 +680,7 @@ function ParseUnit(root) {
const roleText = catName.trim(); const roleText = catName.trim();
let unitRole = LookupRole(roleText); let unitRole = LookupRole(roleText);
if (unitRole != UnitRole.NONE) { if (unitRole != UnitRole.NONE) {
unit._role = unitRole; unit.role = unitRole;
} else { } else {
// Keyword // Keyword
unit.keywords.add(catName); unit.keywords.add(catName);
@@ -763,9 +737,9 @@ function ParseUnit(root) {
seenProfiles.push(...unseenProfiles); seenProfiles.push(...unseenProfiles);
const model = new Model(); const model = new Model();
model._name = modelSelection.getAttribute("name") || "Unknown Model"; model.name = modelSelection.getAttribute("name") || "Unknown Model";
model._count = Number(modelSelection.getAttribute("number") || 1); model.count = Number(modelSelection.getAttribute("number") || 1);
unit._models.push(model); unit.models.push(model);
// Find stats for all profiles (weapons, powers, abilities, etc). // Find stats for all profiles (weapons, powers, abilities, etc).
ParseModelProfiles(profiles, model, unit); ParseModelProfiles(profiles, model, unit);
@@ -788,10 +762,10 @@ function ParseUnit(root) {
let upgradeName = upgradeSelection.getAttribute("name"); let upgradeName = upgradeSelection.getAttribute("name");
if (upgradeName) { if (upgradeName) {
const upgrade = new Upgrade(); const upgrade = new Upgrade();
upgrade._name = upgradeName; upgrade.name = upgradeName;
upgrade._cost = GetSelectionCosts(upgradeSelection); upgrade.cost = GetSelectionCosts(upgradeSelection);
upgrade._count = Number(upgradeSelection.getAttribute("number")); upgrade.count = Number(upgradeSelection.getAttribute("number"));
model._upgrades.push(upgrade); model.upgrades.push(upgrade);
} }
} }
} }
@@ -805,28 +779,28 @@ function ParseUnit(root) {
seenProfiles.push(...unseenProfiles); seenProfiles.push(...unseenProfiles);
if (unseenProfiles.length > 0) { if (unseenProfiles.length > 0) {
const unitUpgradesModel = new Model(); const unitUpgradesModel = new Model();
unitUpgradesModel._name = "Unit Upgrades"; unitUpgradesModel.name = "Unit Upgrades";
ParseModelProfiles(unseenProfiles, unitUpgradesModel, unit); 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. // 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); model.weapons.push(...unitUpgradesModel.weapons);
} }
unitUpgradesModel.weapons.length = 0; // Clear the array. 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 // Add spells to the unit's spell list. However, we'll still need
// to add spell upgrade selections to the upgrade list, below. // to add spell upgrade selections to the upgrade list, below.
unit._spells.push(...unitUpgradesModel._psychicPowers); unit.spells.push(...unitUpgradesModel.psychicPowers);
unitUpgradesModel._psychicPowers.length = 0; unitUpgradesModel.psychicPowers.length = 0;
} }
if (unitUpgradesModel._psyker) { if (unitUpgradesModel.psyker) {
unit._psykers.push(unitUpgradesModel._psyker); unit.psykers.push(unitUpgradesModel.psyker);
unitUpgradesModel._psyker = null; unitUpgradesModel.psyker = null;
} }
if (unitUpgradesModel._explosions.length > 0) { if (unitUpgradesModel.explosions.length > 0) {
unit._explosions.push(...unitUpgradesModel._explosions); unit.explosions.push(...unitUpgradesModel.explosions);
unitUpgradesModel._explosions.length = 0; unitUpgradesModel.explosions.length = 0;
} }
// Look for any unit-level upgrade selections that we didn't catch // Look for any unit-level upgrade selections that we didn't catch
@@ -843,24 +817,24 @@ function ParseUnit(root) {
if (!name) continue; if (!name) continue;
const upgrade = new Upgrade(); const upgrade = new Upgrade();
upgrade._name = name; upgrade.name = name;
upgrade._cost = GetSelectionCosts(selection); upgrade.cost = GetSelectionCosts(selection);
upgrade._count = Number(selection.getAttribute("number")); upgrade.count = Number(selection.getAttribute("number"));
unitUpgradesModel._upgrades.push(upgrade); unitUpgradesModel.upgrades.push(upgrade);
} }
if ( if (
unitUpgradesModel.weapons.length > 0 || 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. // Only match costs->costs associated with the unit and not its children (model and weapon) costs.
let costs = root.querySelectorAll("costs>cost"); let costs = root.querySelectorAll("costs>cost");
for (let cost of costs) { for (let cost of costs) {
unit._cost.add(ParseCost(cost)); unit.cost.add(ParseCost(cost));
} }
let rules = root.querySelectorAll("rules > rule"); let rules = root.querySelectorAll("rules > rule");
@@ -879,7 +853,7 @@ function ParseModelStatsProfiles(profiles, unit, unitName) {
if (!profileName || !profileType) return; if (!profileName || !profileType) return;
const model = new Model(); const model = new Model();
model._name = profileName; model.name = profileName;
unit.modelStats.push(model); unit.modelStats.push(model);
ExpandBaseNotes(profile, model); ExpandBaseNotes(profile, model);
@@ -895,10 +869,10 @@ function ParseModelStatsProfiles(profiles, unit, unitName) {
model.move = char.textContent; model.move = char.textContent;
break; break;
case "WS": case "WS":
model._ws = char.textContent; model.ws = char.textContent;
break; break;
case "BS": case "BS":
model._bs = char.textContent; model.bs = char.textContent;
break; break;
case "S": case "S":
model.str = +char.textContent; model.str = +char.textContent;
@@ -910,7 +884,7 @@ function ParseModelStatsProfiles(profiles, unit, unitName) {
model.wounds = +char.textContent; model.wounds = +char.textContent;
break; break;
case "A": case "A":
model._attacks = char.textContent; model.attacks = char.textContent;
break; break;
case "Ld": case "Ld":
model.leadership = +char.textContent; model.leadership = +char.textContent;
@@ -945,15 +919,15 @@ function ParseModelProfiles(profiles, model, unit) {
typeName.includes(" Wounds") typeName.includes(" Wounds")
) { ) {
const tracker = ParseWoundTrackerProfile(profile); const tracker = ParseWoundTrackerProfile(profile);
unit._woundTracker.push(tracker); unit.woundTracker.push(tracker);
} else if (typeName == "Psychic Power") { } else if (typeName == "Psychic Power") {
const power = ParsePsychicPowerProfile(profile); const power = ParsePsychicPowerProfile(profile);
model._psychicPowers.push(power); model.psychicPowers.push(power);
} else if (typeName.includes("Explosion")) { } else if (typeName.includes("Explosion")) {
const explosion = ParseExplosionProfile(profile); const explosion = ParseExplosionProfile(profile);
model._explosions.push(explosion); model.explosions.push(explosion);
} else if (typeName == "Psyker") { } else if (typeName == "Psyker") {
model._psyker = ParsePsykerProfile(profile); model.psyker = ParsePsykerProfile(profile);
} else { } else {
// Everything else, like Prayers and Warlord Traits. // Everything else, like Prayers and Warlord Traits.
if (!unit.abilities[typeName]) unit.abilities[typeName] = new Map(); if (!unit.abilities[typeName]) unit.abilities[typeName] = new Map();
@@ -986,7 +960,7 @@ function ParseProfileCharacteristics(profile, profileName, typeName, map) {
function ParseWeaponProfile(profile) { function ParseWeaponProfile(profile) {
const weapon = new Weapon(); const weapon = new Weapon();
ExpandBaseNotes(profile, weapon); ExpandBaseNotes(profile, weapon);
weapon._count = ExtractNumberFromParent(profile); weapon.count = ExtractNumberFromParent(profile);
let chars = profile.querySelectorAll("characteristics>characteristic"); let chars = profile.querySelectorAll("characteristics>characteristic");
for (let char of chars) { for (let char of chars) {
@@ -995,19 +969,19 @@ function ParseWeaponProfile(profile) {
if (char.textContent) { if (char.textContent) {
switch (charName) { switch (charName) {
case "Range": case "Range":
weapon._range = char.textContent; weapon.range = char.textContent;
break; break;
case "Type": case "Type":
weapon._type = char.textContent; weapon.type = char.textContent;
break; break;
case "S": case "S":
weapon.str = char.textContent; weapon.str = char.textContent;
break; break;
case "AP": case "AP":
weapon._ap = char.textContent; weapon.ap = char.textContent;
break; break;
case "D": case "D":
weapon._damage = char.textContent; weapon.damage = char.textContent;
break; break;
case "Abilities": case "Abilities":
weapon.abilities = char.textContent; weapon.abilities = char.textContent;
@@ -1021,8 +995,8 @@ function ParseWeaponProfile(profile) {
const selection = profile.parentElement?.parentElement; const selection = profile.parentElement?.parentElement;
const selectionName = selection?.getAttribute("name"); const selectionName = selection?.getAttribute("name");
if (selection?.getAttribute("type") === "upgrade" && selectionName) { if (selection?.getAttribute("type") === "upgrade" && selectionName) {
weapon._selectionName = selectionName; weapon.selectionName = selectionName;
weapon._cost = GetSelectionCosts(selection); weapon.cost = GetSelectionCosts(selection);
} }
return weapon; return weapon;
} }
@@ -1035,9 +1009,9 @@ function ParseWoundTrackerProfile(profile) {
const charName = char.getAttribute("name"); const charName = char.getAttribute("name");
if (charName) { if (charName) {
if (char.textContent) { if (char.textContent) {
tracker._table.set(charName, char.textContent); tracker.table.set(charName, char.textContent);
} else { } else {
tracker._table.set(charName, "-"); tracker.table.set(charName, "-");
} }
} }
} }
@@ -1054,13 +1028,13 @@ function ParsePsychicPowerProfile(profile) {
if (charName && char.textContent) { if (charName && char.textContent) {
switch (charName) { switch (charName) {
case "Range": case "Range":
power._range = char.textContent; power.range = char.textContent;
break; break;
case "Warp Charge": case "Warp Charge":
power._manifest = +char.textContent; power.manifest = +char.textContent;
break; break;
case "Details": case "Details":
power._details = char.textContent; power.details = char.textContent;
break; break;
} }
} }
@@ -1078,13 +1052,13 @@ function ParseExplosionProfile(profile) {
if (charName && char.textContent) { if (charName && char.textContent) {
switch (charName) { switch (charName) {
case "Dice Roll": case "Dice Roll":
explosion._diceRoll = char.textContent; explosion.diceRoll = char.textContent;
break; break;
case "Distance": case "Distance":
explosion._distance = char.textContent; explosion.distance = char.textContent;
break; break;
case "Mortal Wounds": case "Mortal Wounds":
explosion._mortalWounds = char.textContent; explosion.mortalWounds = char.textContent;
break; break;
} }
} }
@@ -1102,16 +1076,16 @@ function ParsePsykerProfile(profile) {
if (charName && char.textContent) { if (charName && char.textContent) {
switch (charName) { switch (charName) {
case "Cast": case "Cast":
psyker._cast = char.textContent; psyker.cast = char.textContent;
break; break;
case "Deny": case "Deny":
psyker._deny = char.textContent; psyker.deny = char.textContent;
break; break;
case "Powers Known": case "Powers Known":
psyker._powers = char.textContent; psyker.powers = char.textContent;
break; break;
case "Other": case "Other":
psyker._other = char.textContent; psyker.other = char.textContent;
break; break;
} }
} }
@@ -1120,35 +1094,35 @@ function ParsePsykerProfile(profile) {
} }
function CompareObj(a, b) { function CompareObj(a, b) {
return Compare(a._name, b._name); return Compare(a.name, b.name);
} }
function CompareModel(a, b) { function CompareModel(a, b) {
if (a._name === b._name) { if (a.name === b.name) {
return Compare(a.nameAndGear(), b.nameAndGear()); 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. // "Unit Upgrades", a special model name, is always sorted last.
return 1; return 1;
} else if (b._name === "Unit Upgrades") { } else if (b.name === "Unit Upgrades") {
// "Unit Upgrades", a special model name, is always sorted last. // "Unit Upgrades", a special model name, is always sorted last.
return -1; return -1;
} else { } else {
return Compare(a._name, b._name); return Compare(a.name, b.name);
} }
} }
export function CompareWeapon(a, b) { export function CompareWeapon(a, b) {
const aType = a._type.startsWith("Grenade") const aType = a.type.startsWith("Grenade")
? 2 ? 2
: a._type.startsWith("Melee") : a.type.startsWith("Melee")
? 1 ? 1
: 0; : 0;
const bType = b._type.startsWith("Grenade") const bType = b.type.startsWith("Grenade")
? 2 ? 2
: b._type.startsWith("Melee") : b.type.startsWith("Melee")
? 1 ? 1
: 0; : 0;
return aType - bType || a.name().localeCompare(b.name()); return aType - bType || a.name.localeCompare(b.name);
} }
export function Compare(a, b) { export function Compare(a, b) {