diff --git a/src/App.jsx b/src/App.jsx index 022860e..1f91235 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { Roster } from "./Roster"; function App() { + const [error, setError] = useState(); const [roster, setRoster] = useState(); function handleFileSelect(event) { const files = event?.target?.files; @@ -12,7 +13,7 @@ function App() { const reader = new FileReader(); reader.onerror = () => { reader.abort(); - console.log("Failed to read roster file."); + setError("Failed to read roster file."); }; reader.onloadend = async () => { const content = reader.result; @@ -60,7 +61,7 @@ function App() { renderer.render(rosterTitle, rosterList, forceUnits); */ } } else { - console.log("No support for game type '" + gameType + "'."); + setError("No support for game type '" + gameType + "'."); } } @@ -70,9 +71,9 @@ function App() { Select roster file (.ros, .rosz) -
- -
+
{error}
+ + ); } diff --git a/src/Roster.jsx b/src/Roster.jsx index 2531349..bf2418a 100644 --- a/src/Roster.jsx +++ b/src/Roster.jsx @@ -8,12 +8,18 @@ export const Roster = ({ roster }) => { if (!roster) { return null; } - const { _name, _forces } = roster; + const { _name, _cost, _forces } = roster; return ( - <> +
{ textTransform: "uppercase", }} > - {_name} + {_name} [{_cost._commandPoints} CP]
{_forces.map((force, index) => ( ))} - +
); }; +const getPrimaryColor = (catalog) => { + switch (catalog) { + case "Adeptus Astartes": + return "#536766"; + case "Adeptus Custodes": + return "#536766"; + case "Adeptus Mechanicus": + return "#536766"; + case "Astra Militarum": + return "#536766"; + case "Chaos Daemons": + return "#536766"; + case "Chaos Space Marines": + return "#536766"; + case "Death Guard": + return "#536766"; + case "Drukhari": + return "#536766"; + case "Genestealer Cults": + return "#536766"; + case "Grey Knights": + return "#536766"; + case "Harlequins": + return "#536766"; + case "Imperial Knights": + return "#536766"; + case "Necrons": + return "#536766"; + case "Orks": + return "#536766"; + case "Sisters of Battle": + return "#536766"; + case "Space Marines": + return "#536766"; + case "Tau Empire": + return "#536766"; + case "Thousand Sons": + return "#536766"; + case "Tyranids": + return "#44264C"; + case "Ynnari": + return "#536766"; + default: + return "#536766"; + } +}; + const Force = ({ force }) => { - const { _units, rules } = force; + const { _units, rules, _catalog } = force; + + const primaryColor = getPrimaryColor(_catalog); return ( - <> +
{_units.map((unit, index) => ( ))} - +
); }; const Unit = ({ unit }) => { - const { _name, weapons, abilities, keywords, factions, rules, modelStats } = - unit; + let { + _name, + weapons, + abilities, + keywords, + factions, + rules, + modelStats, + _modelList, + _psykers, + } = unit; + const hasDifferentProfiles = weapons.some( + (weapon) => + weapon._selectionName !== weapon._name && + !weapon._name.includes("(Shooting)") && + !weapon._name.includes("(Melee)") + ); const meleeWeapons = weapons - .filter((weapon) => weapon._range === "Melee") + .filter((weapon) => weapon._range === "Melee" && weapon._range !== "-") .sort((a, b) => a._selectionName.localeCompare(b._selectionName)); const rangedWeapons = weapons - .filter((weapon) => weapon._range !== "Melee") + .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" return (
@@ -65,40 +143,56 @@ const Unit = ({ unit }) => { style={{ padding: "24px 16px", paddingRight: 0, - paddingBottom: 24 + 50 * (modelStats.length - 1), - backgroundColor: "#2F2F31", + paddingBottom: 24, + background: + "linear-gradient(90deg, rgba(20,21,25,1) 0%, rgba(48,57,62,1) 45%, rgba(73,74,79,1) 100%)", color: "#fff", }} >
+
{_name}
{modelStats.map((model, index) => ( 1} /> @@ -110,9 +204,11 @@ const Unit = ({ unit }) => {
{ weapons={meleeWeapons} modelStats={modelStats} /> +
- { + {hasDifferentProfiles && ( @@ -148,15 +245,15 @@ const Unit = ({ unit }) => {
{Arrow}
- } + )}
@@ -164,7 +261,7 @@ const Unit = ({ unit }) => { style={{ fontSize: "1.1em", padding: "1px 8px", - backgroundColor: "#536766", + backgroundColor: "var(--primary-color)", color: "#fff", fontWeight: 600, minHeight: 27, @@ -184,11 +281,14 @@ const Unit = ({ unit }) => { ); }; -const ModelStats = ({ modelStats, index, showName }) => { +const ModelStats = ({ modelStats, index, showName, modelList }) => { let { move, toughness, save, wounds, leadership, _name } = modelStats; if (!wounds) { wounds = "/"; } + const modelListMatches = modelList + .filter((model) => model.includes(_name)) + .map((model) => "(" + model.split("(")[1]); return (
@@ -197,9 +297,28 @@ const ModelStats = ({ modelStats, index, showName }) => { {showName && ( -
- {_name} -
+ <> +
+ {_name} +
+
+ {modelListMatches.map((model, index) => ( +
{model}
+ ))} +
+ )}
); @@ -215,7 +334,7 @@ const FactionIcon = () => { width: 54, height: 54, transform: "rotate(-45deg)", - border: "2px solid #536766", + border: "2px solid var(--primary-color)", overflow: "hidden", display: "flex", alignItems: "center", @@ -236,6 +355,9 @@ const FactionIcon = () => { }; const Keywords = ({ keywords }) => { + keywords = Array.from(keywords).filter( + (keyword) => keyword !== "Configuration" + ); return (
{ display: "flex", alignItems: "center", backgroundColor: "#BCBCBE", - border: "2px solid #536766", + border: "2px solid var(--primary-color)", width: "calc(100% - 18px)", backgroundSize: "contain", minHeight: 54, gap: 3, - color: "#101010", }} > KEYWORDS: @@ -277,7 +398,7 @@ const Factions = ({ factions }) => { flexDirection: "column", background: `url(${factionBackground})`, color: "#fff", - border: "2px solid #536766", + border: "2px solid var(--primary-color)", borderLeft: "none", width: "calc(100% - 18px)", backgroundSize: "contain", @@ -295,14 +416,14 @@ const Factions = ({ factions }) => { }; const Rules = ({ rules }) => { + if (!rules || rules.size === 0) return null; return (
RULES: @@ -314,29 +435,30 @@ const Rules = ({ rules }) => { }; const Abilities = ({ abilities }) => { + if (!abilities) return null; return (
- {[...abilities.keys()].map((ability) => ( -
- {ability}:{" "} - {abilities.get(ability)} -
- ))} + {abilities && + [...abilities.keys()].map((ability) => ( +
+ {ability}:{" "} + {abilities.get(ability)} +
+ ))}
); }; @@ -358,9 +480,10 @@ const FancyBox = ({ children }) => { return (
{ @@ -396,7 +519,7 @@ const Weapons = ({ title, weapons, modelStats }) => {
- {title} + {title} RANGE A {isMelee ? "WS" : "BS"} @@ -428,17 +551,31 @@ const Weapons = ({ title, weapons, modelStats }) => { const Weapon = ({ weapon, modelStats, isMelee, index }) => { let { _name, _selectionName, _range, _type, str, _ap, _damage } = weapon; - const [type, attacks] = _type.split(" "); + 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 strModel = modelStats[0].str; const meleeAttacks = modelStats[0]._attacks; - const differentProfiles = _selectionName !== _name; - const interestingType = type !== "Melee"; + if (_name === "Krak grenades") { + _name = "Krak grenade"; + } + const differentProfiles = + _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(" grenade")) { + _name = _name.replace(" grenade", ""); + } + if (differentProfiles && _name.includes(" - ")) { + _name = _name.split(" - ")[1]; + } return ( { {_name} {interestingType && ( [{type}] @@ -481,22 +622,89 @@ const Weapon = ({ weapon, modelStats, isMelee, index }) => { }; const calculateWeaponStrength = (strModel, strWeapon) => { + if (strWeapon.startsWith("User")) return strModel; if (strWeapon.startsWith("x")) return strModel * parseInt(strWeapon.replace("x", "")); return strModel + parseInt(strWeapon, 10); }; +const Psykers = ({ title, psykers }) => { + return ( + <> + {psykers.length > 0 && ( + + + +
+ +
+ + {title} + Cast + Deny + + Powers Known + + + + )} + + {psykers.map((psyker, index) => ( + + ))} + {psykers.length > 0 && ( + + + + )} + + + ); +}; + +const Psyker = ({ psyker, index }) => { + let { _name, _cast, _deny, _powers } = psyker; + + return ( + + + +
+ {_name} +
+ + {_cast} + {_deny} + + {_powers} + + + ); +}; + const ForceRules = ({ rules }) => { return (
{[...rules.keys()] diff --git a/src/index.css b/src/index.css index 4fa821a..4d3d478 100644 --- a/src/index.css +++ b/src/index.css @@ -17,6 +17,8 @@ * { box-sizing: border-box; + print-color-adjust: exact; + -webkit-print-color-adjust: exact; } a { @@ -74,9 +76,10 @@ button:focus-visible { } #root { - max-width: 1000px; + max-width: 1100px; margin-left: auto; margin-right: auto; + color: #111113; } .App { @@ -103,6 +106,7 @@ button:focus-visible { padding: 1px 8px; border-bottom: 1px dotted #9e9fa1; text-align: center; + vertical-align: baseline; } .emptyRow { @@ -120,3 +124,17 @@ button:focus-visible { .differentProfiles + .differentProfiles { border-top: 1px solid #dfe0e2; } + +@media print { + .print-display-none { + display: none; + } + + .avoid-page-break { + break-inside: avoid; + } + + #root { + min-width: 1000px; + } +} diff --git a/src/roster40k.js b/src/roster40k.js index 72945c1..6fc13a8 100644 --- a/src/roster40k.js +++ b/src/roster40k.js @@ -1,10 +1,8 @@ export class BaseNotes { _name = ""; - _customName = ""; _customNotes = ""; name() { - if (this._customName) return this._customName; return this._name; } @@ -604,7 +602,6 @@ function ExpandBaseNotes(root, obj) { element = root.parentElement.parentElement; } - obj._customName = element.getAttributeNode("customName")?.nodeValue; let child = element.firstElementChild; if (child && child.tagName === "customNotes") { obj._customNotes = child.textContent;