save rosters in localStorage, better mobile support for 10th edition, add tailwind
This commit is contained in:
Generated
+3915
-1663
File diff suppressed because it is too large
Load Diff
+31
-20
@@ -1,22 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "fancyscribe",
|
"name": "fancyscribe",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.2.31",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.2.14",
|
||||||
"@vitejs/plugin-react": "^3.1.0",
|
"@vitejs/plugin-react": "^4.1.0",
|
||||||
"vite": "^4.2.0"
|
"autoprefixer": "^10.4.16",
|
||||||
}
|
"postcss": "^8.4.31",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"vite": "^4.5.0"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"useTabs": true,
|
||||||
|
"plugins": [
|
||||||
|
"prettier-plugin-tailwindcss"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
+74
-83
@@ -1,7 +1,6 @@
|
|||||||
import { useRef, useState, useEffect } from "react";
|
import { useRef, useState, useEffect, Fragment } from "react";
|
||||||
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 { Arrow, wavyLine } from "../assets/icons";
|
import { Arrow, wavyLine } from "../assets/icons";
|
||||||
import { Weapons, hasDifferentProfiles } from "./Weapons";
|
import { Weapons, hasDifferentProfiles } from "./Weapons";
|
||||||
import { useLocalStorage } from "../helpers/useLocalStorage";
|
import { useLocalStorage } from "../helpers/useLocalStorage";
|
||||||
@@ -85,7 +84,7 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
const weaponDescriptions = weapons
|
const weaponDescriptions = weapons
|
||||||
.filter(
|
.filter(
|
||||||
(weapon) =>
|
(weapon) =>
|
||||||
(weapon.range === "-" || weapon.range === "") && weapon.abilities
|
(weapon.range === "-" || weapon.range === "") && weapon.abilities,
|
||||||
)
|
)
|
||||||
.sort((a, b) => a.selectionName.localeCompare(b.selectionName));
|
.sort((a, b) => a.selectionName.localeCompare(b.selectionName));
|
||||||
const modelsWithDifferentProfiles = weapons.filter((weapon, index) => {
|
const modelsWithDifferentProfiles = weapons.filter((weapon, index) => {
|
||||||
@@ -97,7 +96,7 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
((previousWeapon &&
|
((previousWeapon &&
|
||||||
hasDifferentProfiles(
|
hasDifferentProfiles(
|
||||||
previousWeapon.selectionName,
|
previousWeapon.selectionName,
|
||||||
previousWeapon.name
|
previousWeapon.name,
|
||||||
) &&
|
) &&
|
||||||
selectionName === previousWeapon.selectionName) ||
|
selectionName === previousWeapon.selectionName) ||
|
||||||
(nextWeapon &&
|
(nextWeapon &&
|
||||||
@@ -135,7 +134,7 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
model.oc === firstModel.oc &&
|
model.oc === firstModel.oc &&
|
||||||
model.bs === firstModel.bs &&
|
model.bs === firstModel.bs &&
|
||||||
model.ws === firstModel.ws &&
|
model.ws === firstModel.ws &&
|
||||||
model.attacks === firstModel.attacks
|
model.attacks === firstModel.attacks,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
if (areAllModelsTheSame(modelStats)) {
|
if (areAllModelsTheSame(modelStats)) {
|
||||||
@@ -164,6 +163,7 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input type="checkbox" onChange={() => setHide(!hide)} />
|
<input type="checkbox" onChange={() => setHide(!hide)} />
|
||||||
@@ -200,7 +200,7 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor: "var(--primary-color-transparent)",
|
backgroundColor: "var(--primary-color-transparent)",
|
||||||
height: 80,
|
height: 80,
|
||||||
minWidth: 340,
|
minWidth: "33%",
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
@@ -286,57 +286,53 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
style={{ width: "100%", height: "100%", objectFit: "contain" }}
|
style={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<label
|
<div className="absolute right-[1px] top-[2px] flex gap-1">
|
||||||
className="button print-display-none"
|
<label
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
border: "1px solid #999",
|
|
||||||
top: 2,
|
|
||||||
right: 1,
|
|
||||||
padding: "1px 4px",
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
backgroundColor: "#f0f0f0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
ref={uploadRef}
|
|
||||||
type="file"
|
|
||||||
className="print-display-none"
|
|
||||||
accept=".jpg,.png,.jpeg,.gif,.bmp,.tif,.tiff,.webp,.svg,.jfif,.pjpeg,.pjp,.avif,.apng,.ico,.cur,.ani"
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.files && e.target.files[0]) {
|
|
||||||
let reader = new FileReader();
|
|
||||||
reader.onload = function (ev) {
|
|
||||||
setImage(ev.target.result);
|
|
||||||
}.bind(this);
|
|
||||||
reader.readAsDataURL(e.target.files[0]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
display: "none",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{hasImage ? "Change image" : "Upload image "}
|
|
||||||
</label>
|
|
||||||
{hasImage && (
|
|
||||||
<button
|
|
||||||
className="button print-display-none"
|
className="button print-display-none"
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
|
||||||
border: "1px solid #999",
|
border: "1px solid #999",
|
||||||
top: 2,
|
padding: "1px 4px",
|
||||||
right: 84,
|
|
||||||
padding: "2px 4px",
|
|
||||||
fontSize: "0.8rem",
|
fontSize: "0.8rem",
|
||||||
backgroundColor: "#f0f0f0",
|
backgroundColor: "#f0f0f0",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
|
||||||
setImage(undefined);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Clear image
|
<input
|
||||||
</button>
|
ref={uploadRef}
|
||||||
)}
|
type="file"
|
||||||
|
className="print-display-none"
|
||||||
|
accept=".jpg,.png,.jpeg,.gif,.bmp,.tif,.tiff,.webp,.svg,.jfif,.pjpeg,.pjp,.avif,.apng,.ico,.cur,.ani"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.files && e.target.files[0]) {
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.onload = function (ev) {
|
||||||
|
setImage(ev.target.result);
|
||||||
|
}.bind(this);
|
||||||
|
reader.readAsDataURL(e.target.files[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
display: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{hasImage ? "Change image" : "Upload image "}
|
||||||
|
</label>
|
||||||
|
{hasImage && (
|
||||||
|
<button
|
||||||
|
className="button print-display-none"
|
||||||
|
style={{
|
||||||
|
border: "1px solid #999",
|
||||||
|
padding: "1px 4px",
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
backgroundColor: "#f0f0f0",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setImage(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear image
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -383,30 +379,24 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
>
|
>
|
||||||
<tbody>
|
<tbody>
|
||||||
{weaponDescriptions.map((weapon, index) => (
|
|
||||||
<tr key={index} className="emptyRow noBorderTop">
|
|
||||||
<td style={{ width: 37, borderTop: "none" }}>{Arrow}</td>
|
|
||||||
<td
|
|
||||||
style={{
|
|
||||||
textAlign: "left",
|
|
||||||
fontSize: ".8em",
|
|
||||||
paddingLeft: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{weapon.name} - {weapon.abilities}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{modelsWithDifferentProfiles.length > 0 &&
|
{modelsWithDifferentProfiles.length > 0 &&
|
||||||
!weaponDescriptions.length && (
|
!weaponDescriptions.length && (
|
||||||
<tr className="emptyRow noBorderTop">
|
<tr className="emptyRow noBorderTop">
|
||||||
<td style={{ width: 37, borderTop: "none" }}>{Arrow}</td>
|
<td
|
||||||
|
style={{
|
||||||
|
borderTop: "none",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex justify-center">{Arrow}</div>
|
||||||
|
</td>
|
||||||
<td
|
<td
|
||||||
colSpan={7}
|
colSpan={7}
|
||||||
style={{
|
style={{
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
fontSize: ".8em",
|
fontSize: ".8em",
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
|
verticalAlign: "middle",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Before selecting targets for this weapon, select one of
|
Before selecting targets for this weapon, select one of
|
||||||
@@ -419,14 +409,7 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
|
|
||||||
<Keywords keywords={keywords} />
|
<Keywords keywords={keywords} />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className=" relative max-w-[400px] flex-1 p-1 pb-[50px] print:p-[20px] sm:p-2 sm:pb-[50px] sm:print:p-[20px] sm:print:pb-[50px] md:p-[20px] md:pb-[50px] md:print:p-[20px] md:print:pb-[50px]">
|
||||||
style={{
|
|
||||||
padding: "var(--size-20) var(--size-20) 50px var(--size-20)",
|
|
||||||
flex: "1",
|
|
||||||
maxWidth: 400,
|
|
||||||
position: "relative",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "1.1em",
|
fontSize: "1.1em",
|
||||||
@@ -461,14 +444,14 @@ const ModelStats = ({ modelStat, index, showName, modelList }) => {
|
|||||||
|
|
||||||
const showWeapons = index === 0;
|
const showWeapons = index === 0;
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", gap: 16 }}>
|
<div style={{ display: "flex", gap: "1.2rem", flexWrap: "wrap" }}>
|
||||||
<Characteristic title="M" characteristic={move} index={index} />
|
<Characteristic title="M" characteristic={move} index={index} />
|
||||||
<Characteristic title="T" characteristic={toughness} index={index} />
|
<Characteristic title="T" characteristic={toughness} index={index} />
|
||||||
<Characteristic title="SV" characteristic={save} index={index} />
|
<Characteristic title="SV" characteristic={save} index={index} />
|
||||||
<Characteristic title="W" characteristic={wounds} index={index} />
|
<Characteristic title="W" characteristic={wounds} index={index} />
|
||||||
<Characteristic title="LD" characteristic={leadership} index={index} />
|
<Characteristic title="LD" characteristic={leadership} index={index} />
|
||||||
<Characteristic title="OC" characteristic={oc} index={index} />
|
<Characteristic title="OC" characteristic={oc} index={index} />
|
||||||
<div style={{ display: "flex", alignItems: "center", marginLeft: -10 }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
{showName && (
|
{showName && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -530,6 +513,7 @@ const FactionIcon = ({ catalog }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Keywords = ({ keywords }) => {
|
const Keywords = ({ keywords }) => {
|
||||||
|
const joinedKeywords = [...keywords].join(", ");
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -542,15 +526,20 @@ const Keywords = ({ keywords }) => {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
backgroundColor: "#BCBCBE",
|
backgroundColor: "#BCBCBE",
|
||||||
border: "2px solid var(--primary-color)",
|
border: "2px solid var(--primary-color)",
|
||||||
width: "calc(100% - 18px)",
|
width: "calc(100% - 0.9rem)",
|
||||||
backgroundSize: "contain",
|
backgroundSize: "contain",
|
||||||
minHeight: 54,
|
minHeight: 54,
|
||||||
gap: 3,
|
gap: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: "1.1em" }}>KEYWORDS:</span>
|
<span style={{ fontSize: "1.1em" }}>KEYWORDS:</span>
|
||||||
<span style={{ fontSize: "1em", fontWeight: 800 }}>
|
<span
|
||||||
{[...keywords].join(", ")}
|
style={{
|
||||||
|
fontSize: joinedKeywords.length > 80 ? ".8em" : "1em",
|
||||||
|
fontWeight: 800,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{joinedKeywords}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -572,7 +561,7 @@ const Factions = ({ factions }) => {
|
|||||||
color: "#fff",
|
color: "#fff",
|
||||||
border: "2px solid var(--primary-color)",
|
border: "2px solid var(--primary-color)",
|
||||||
borderLeft: "none",
|
borderLeft: "none",
|
||||||
width: "calc(100% - 18px)",
|
width: "calc(100% - 0.9rem)",
|
||||||
backgroundSize: "contain",
|
backgroundSize: "contain",
|
||||||
minHeight: 54,
|
minHeight: 54,
|
||||||
}}
|
}}
|
||||||
@@ -670,8 +659,8 @@ const FancyBox = ({ children }) => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
minWidth: 40,
|
minWidth: "3rem",
|
||||||
minHeight: 40,
|
minHeight: "3rem",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
@@ -691,7 +680,7 @@ const OtherAbilities = ({ abilities }) => {
|
|||||||
let keys = [...Object.keys(abilities)?.filter((key) => key !== "Abilities")];
|
let keys = [...Object.keys(abilities)?.filter((key) => key !== "Abilities")];
|
||||||
return keys.map((key) => {
|
return keys.map((key) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment key={key}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr
|
<tr
|
||||||
style={{
|
style={{
|
||||||
@@ -725,7 +714,7 @@ const OtherAbilities = ({ abilities }) => {
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -746,6 +735,8 @@ const ForceRules = ({ rules, onePerPage }) => {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
|
gap: 4,
|
||||||
|
paddingBottom: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input type="checkbox" onChange={() => setHide(!hide)} />
|
<input type="checkbox" onChange={() => setHide(!hide)} />
|
||||||
|
|||||||
+24
-16
@@ -14,18 +14,24 @@ export const Weapons = ({ title, weapons, modelStats, forceRules }) => {
|
|||||||
color: "#fff",
|
color: "#fff",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<th style={{ width: 37 }}>
|
<th className="w-[2.2em]">
|
||||||
<div style={{ display: "flex" }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
minWidth: ".7em",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<img src={isMelee ? meleeIcon : rangedIcon} />
|
<img src={isMelee ? meleeIcon : rangedIcon} />
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th style={{ textAlign: "left" }}>{title}</th>
|
<th style={{ textAlign: "left" }}>{title}</th>
|
||||||
<th>RANGE</th>
|
<th className="w-[5%]">RANGE</th>
|
||||||
<th>A</th>
|
<th className="w-[5%]">A</th>
|
||||||
<th>{isMelee ? "WS" : "BS"}</th>
|
<th className="w-[5%]">{isMelee ? "WS" : "BS"}</th>
|
||||||
<th>S</th>
|
<th className="w-[5%]">S</th>
|
||||||
<th>AP</th>
|
<th className="w-[5%]">AP</th>
|
||||||
<th>D</th>
|
<th className="w-[5%]">D</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
)}
|
)}
|
||||||
@@ -44,7 +50,7 @@ export const Weapons = ({ title, weapons, modelStats, forceRules }) => {
|
|||||||
))}
|
))}
|
||||||
{weapons.length > 0 && (
|
{weapons.length > 0 && (
|
||||||
<tr className="emptyRow">
|
<tr className="emptyRow">
|
||||||
<td style={{ width: 37, borderTop: "none" }}></td>
|
<td style={{ borderTop: "none" }}></td>
|
||||||
<td colSpan={7}></td>
|
<td colSpan={7}></td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
@@ -102,7 +108,9 @@ const Weapon = ({ weapon, previousWeapon, nextWeapon, isMelee, className }) => {
|
|||||||
<>
|
<>
|
||||||
<tr className={className}>
|
<tr className={className}>
|
||||||
<td style={{ borderTop: "none", backgroundColor: "#dfe0e2" }}>
|
<td style={{ borderTop: "none", backgroundColor: "#dfe0e2" }}>
|
||||||
{differentProfiles && Arrow}
|
{differentProfiles && (
|
||||||
|
<div className="flex justify-center">{Arrow}</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ textAlign: "left" }}>
|
<td style={{ textAlign: "left" }}>
|
||||||
<div
|
<div
|
||||||
@@ -131,9 +139,9 @@ const Weapon = ({ weapon, previousWeapon, nextWeapon, isMelee, className }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{range}</td>
|
<td className="align-middle">{range}</td>
|
||||||
<td>{attacks}</td>
|
<td className="align-middle">{attacks}</td>
|
||||||
<td>
|
<td className="align-middle">
|
||||||
{isMelee
|
{isMelee
|
||||||
? ws.join
|
? ws.join
|
||||||
? ws.join("|")
|
? ws.join("|")
|
||||||
@@ -142,9 +150,9 @@ const Weapon = ({ weapon, previousWeapon, nextWeapon, isMelee, className }) => {
|
|||||||
? bs.join("|")
|
? bs.join("|")
|
||||||
: bs}
|
: bs}
|
||||||
</td>
|
</td>
|
||||||
<td>{str}</td>
|
<td className="align-middle">{str}</td>
|
||||||
<td>{ap}</td>
|
<td className="align-middle">{ap}</td>
|
||||||
<td>{damage}</td>
|
<td className="align-middle">{damage}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{abilities && abilities !== "-" && (
|
{abilities && abilities !== "-" && (
|
||||||
<tr className={className + " noBorderTop"}>
|
<tr className={className + " noBorderTop"}>
|
||||||
|
|||||||
+20
-9
@@ -84,7 +84,7 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
.filter(
|
.filter(
|
||||||
(weapon) =>
|
(weapon) =>
|
||||||
(weapon.range === "Melee" || weapon.type === "Melee") &&
|
(weapon.range === "Melee" || weapon.type === "Melee") &&
|
||||||
weapon.range !== ""
|
weapon.range !== "",
|
||||||
)
|
)
|
||||||
.sort((a, b) => a.selectionName.localeCompare(b.selectionName));
|
.sort((a, b) => a.selectionName.localeCompare(b.selectionName));
|
||||||
if (meleeWeapons.length === 0) {
|
if (meleeWeapons.length === 0) {
|
||||||
@@ -101,14 +101,14 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
const rangedWeapons = weapons
|
const rangedWeapons = weapons
|
||||||
.filter(
|
.filter(
|
||||||
(weapon) =>
|
(weapon) =>
|
||||||
weapon.range !== "Melee" && weapon.range !== "-" && weapon.range !== ""
|
weapon.range !== "Melee" && weapon.range !== "-" && weapon.range !== "",
|
||||||
)
|
)
|
||||||
.sort((a, b) => a.selectionName.localeCompare(b.selectionName));
|
.sort((a, b) => a.selectionName.localeCompare(b.selectionName));
|
||||||
|
|
||||||
const weaponDescriptions = weapons
|
const weaponDescriptions = weapons
|
||||||
.filter(
|
.filter(
|
||||||
(weapon) =>
|
(weapon) =>
|
||||||
(weapon.range === "-" || weapon.range === "") && weapon.abilities
|
(weapon.range === "-" || weapon.range === "") && weapon.abilities,
|
||||||
)
|
)
|
||||||
.sort((a, b) => a.selectionName.localeCompare(b.selectionName));
|
.sort((a, b) => a.selectionName.localeCompare(b.selectionName));
|
||||||
const modelsWithDifferentProfiles = weapons.filter((weapon, index) => {
|
const modelsWithDifferentProfiles = weapons.filter((weapon, index) => {
|
||||||
@@ -120,7 +120,7 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
((previousWeapon &&
|
((previousWeapon &&
|
||||||
hasDifferentProfiles(
|
hasDifferentProfiles(
|
||||||
previousWeapon.selectionName,
|
previousWeapon.selectionName,
|
||||||
previousWeapon.name
|
previousWeapon.name,
|
||||||
) &&
|
) &&
|
||||||
selectionName === previousWeapon.selectionName) ||
|
selectionName === previousWeapon.selectionName) ||
|
||||||
(nextWeapon &&
|
(nextWeapon &&
|
||||||
@@ -168,6 +168,7 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label className="print-display-none">
|
<label className="print-display-none">
|
||||||
@@ -377,12 +378,21 @@ const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{weaponDescriptions.map((weapon, index) => (
|
{weaponDescriptions.map((weapon, index) => (
|
||||||
<tr key={index} className="emptyRow noBorderTop">
|
<tr key={index} className="emptyRow noBorderTop">
|
||||||
<td style={{ width: 37, borderTop: "none" }}>{Arrow}</td>
|
<td
|
||||||
|
style={{
|
||||||
|
width: 37,
|
||||||
|
borderTop: "none",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Arrow}
|
||||||
|
</td>
|
||||||
<td
|
<td
|
||||||
style={{
|
style={{
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
fontSize: ".8em",
|
fontSize: ".8em",
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
|
verticalAlign: "middle",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{weapon.name} - {weapon.abilities}
|
{weapon.name} - {weapon.abilities}
|
||||||
@@ -457,13 +467,13 @@ const ModelStats = ({ modelStat, index, showName, showWeapons, modelList }) => {
|
|||||||
modelList = modelList.filter((model) => !model.includes("Sergeant"));
|
modelList = modelList.filter((model) => !model.includes("Sergeant"));
|
||||||
}
|
}
|
||||||
let modelListMatches = modelList.filter((model) =>
|
let modelListMatches = modelList.filter((model) =>
|
||||||
model.includes(name + " w")
|
model.includes(name + " w"),
|
||||||
);
|
);
|
||||||
if (modelListMatches.length === 0) {
|
if (modelListMatches.length === 0) {
|
||||||
modelListMatches = modelList.filter((model) => model.includes(name));
|
modelListMatches = modelList.filter((model) => model.includes(name));
|
||||||
}
|
}
|
||||||
modelListMatches = modelListMatches.map((model) =>
|
modelListMatches = modelListMatches.map((model) =>
|
||||||
model.replaceAll(name, "")
|
model.replaceAll(name, ""),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -616,7 +626,7 @@ const Abilities = ({ abilities }) => {
|
|||||||
if (!abilities) return null;
|
if (!abilities) return null;
|
||||||
let keys = [...abilities.keys()];
|
let keys = [...abilities.keys()];
|
||||||
keys = keys.filter(
|
keys = keys.filter(
|
||||||
(key) => key !== "Stratagem: Warlord Trait" && key !== "Stratagem: Relic"
|
(key) => key !== "Stratagem: Warlord Trait" && key !== "Stratagem: Relic",
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -747,7 +757,7 @@ const WoundTracker = ({ woundTracker }) => {
|
|||||||
if (!woundTracker || woundTracker.length === 0) return null;
|
if (!woundTracker || woundTracker.length === 0) return null;
|
||||||
const uniqueWoundTracker = woundTracker.filter((woundTrack, index) => {
|
const uniqueWoundTracker = woundTracker.filter((woundTrack, index) => {
|
||||||
const firstIndex = woundTracker.findIndex(
|
const firstIndex = woundTracker.findIndex(
|
||||||
(woundTrack2) => woundTrack2.name === woundTrack.name
|
(woundTrack2) => woundTrack2.name === woundTrack.name,
|
||||||
);
|
);
|
||||||
return firstIndex === index;
|
return firstIndex === index;
|
||||||
});
|
});
|
||||||
@@ -913,6 +923,7 @@ const ForceRules = ({ rules, onePerPage }) => {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input type="checkbox" onChange={() => setHide(!hide)} />
|
<input type="checkbox" onChange={() => setHide(!hide)} />
|
||||||
|
|||||||
+95
-11
@@ -7,11 +7,15 @@ import Demo0 from "./assets/Demo0.png";
|
|||||||
import Demo1 from "./assets/Demo1.png";
|
import Demo1 from "./assets/Demo1.png";
|
||||||
import { Roster } from "./9th/Roster";
|
import { Roster } from "./9th/Roster";
|
||||||
import { Roster as Roster10th } from "./10th/Roster";
|
import { Roster as Roster10th } from "./10th/Roster";
|
||||||
|
import { useLocalStorage } from "./helpers/useLocalStorage";
|
||||||
|
import { parseJSON, stringifyJSON } from "./helpers/json";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [rosters, setRosters] = useLocalStorage("rosters", "[]");
|
||||||
|
const rostersJSON = parseJSON(rosters ?? "[]");
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
const [roster, setRoster] = useState();
|
const [roster, setRoster] = useState();
|
||||||
const [edition, setEdition] = useState(9); // [9, 10]
|
const [edition, setEdition] = useState(10); // [9, 10]
|
||||||
const [onePerPage, setOnePerPage] = useState(false);
|
const [onePerPage, setOnePerPage] = useState(false);
|
||||||
const [primaryColor, setPrimaryColor] = useState("#536766");
|
const [primaryColor, setPrimaryColor] = useState("#536766");
|
||||||
const uploadRef = useRef();
|
const uploadRef = useRef();
|
||||||
@@ -27,7 +31,7 @@ function App() {
|
|||||||
reader.onloadend = async () => {
|
reader.onloadend = async () => {
|
||||||
const content = reader.result;
|
const content = reader.result;
|
||||||
const xmldata = await unzip(content);
|
const xmldata = await unzip(content);
|
||||||
parseXML(xmldata);
|
parseXML(xmldata, true);
|
||||||
};
|
};
|
||||||
reader.readAsBinaryString(files[0]);
|
reader.readAsBinaryString(files[0]);
|
||||||
} else {
|
} else {
|
||||||
@@ -54,9 +58,20 @@ function App() {
|
|||||||
// Create a new Blob object from the zip file contents
|
// Create a new Blob object from the zip file contents
|
||||||
const zipBlob = new Blob([arrayBuffer], { type: "application/zip" });
|
const zipBlob = new Blob([arrayBuffer], { type: "application/zip" });
|
||||||
const xmldata = await unzip(zipBlob);
|
const xmldata = await unzip(zipBlob);
|
||||||
parseXML(xmldata);
|
parseXML(xmldata, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const loadFromLocalStorage = (roster) => {
|
||||||
|
if (roster.gameType == "Warhammer 40,000 9th Edition") {
|
||||||
|
setRoster(roster);
|
||||||
|
setEdition(9);
|
||||||
|
setError("");
|
||||||
|
} else if (roster.gameType == "Warhammer 40,000 10th Edition") {
|
||||||
|
setRoster(roster);
|
||||||
|
setEdition(10);
|
||||||
|
setError("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const unzip = async (file) => {
|
const unzip = async (file) => {
|
||||||
if (file?.charAt && file.charAt(0) !== "P") {
|
if (file?.charAt && file.charAt(0) !== "P") {
|
||||||
@@ -68,7 +83,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseXML(xmldata) {
|
function parseXML(xmldata, addToLocalStorage) {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(xmldata, "text/xml");
|
const doc = parser.parseFromString(xmldata, "text/xml");
|
||||||
if (!doc) return;
|
if (!doc) return;
|
||||||
@@ -85,17 +100,18 @@ function App() {
|
|||||||
document.title = `FancyScribe ${rosterName}`;
|
document.title = `FancyScribe ${rosterName}`;
|
||||||
}
|
}
|
||||||
console.log(doc);
|
console.log(doc);
|
||||||
|
|
||||||
|
let roster;
|
||||||
if (gameType == "Warhammer 40,000 9th Edition") {
|
if (gameType == "Warhammer 40,000 9th Edition") {
|
||||||
const roster = Create40kRoster(doc);
|
roster = Create40kRoster(doc, gameType);
|
||||||
console.log(roster);
|
|
||||||
if (roster && roster.forces.length > 0) {
|
if (roster && roster.forces.length > 0) {
|
||||||
setRoster(roster);
|
setRoster(roster);
|
||||||
setEdition(9);
|
setEdition(9);
|
||||||
setError("");
|
setError("");
|
||||||
}
|
}
|
||||||
} else if (gameType == "Warhammer 40,000 10th Edition") {
|
} else if (gameType == "Warhammer 40,000 10th Edition") {
|
||||||
const roster = Create40kRoster10th(doc);
|
roster = Create40kRoster10th(doc, gameType);
|
||||||
console.log(roster);
|
|
||||||
if (roster && roster.forces.length > 0) {
|
if (roster && roster.forces.length > 0) {
|
||||||
setRoster(roster);
|
setRoster(roster);
|
||||||
setEdition(10);
|
setEdition(10);
|
||||||
@@ -104,6 +120,10 @@ function App() {
|
|||||||
} else {
|
} else {
|
||||||
setError("No support for game type '" + gameType + "'.");
|
setError("No support for game type '" + gameType + "'.");
|
||||||
}
|
}
|
||||||
|
if (roster && addToLocalStorage) {
|
||||||
|
console.log(roster);
|
||||||
|
setRosters(stringifyJSON([roster, ...rostersJSON]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -130,6 +150,14 @@ function App() {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (edition === 9) {
|
||||||
|
document.body.style.minWidth = "600px";
|
||||||
|
} else {
|
||||||
|
document.body.style.minWidth = "";
|
||||||
|
}
|
||||||
|
}, [edition]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="App"
|
className="App"
|
||||||
@@ -145,8 +173,7 @@ function App() {
|
|||||||
color: "#fff",
|
color: "#fff",
|
||||||
fontWeight: 800,
|
fontWeight: 800,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
flexDirection: "column",
|
||||||
gap: 6,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
FancyScribe{" "}
|
FancyScribe{" "}
|
||||||
@@ -165,6 +192,60 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="body">
|
<div className="body">
|
||||||
|
<div
|
||||||
|
className="print-display-none"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: roster ? "row" : "column",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="print-display-none"
|
||||||
|
style={{
|
||||||
|
fontSize: "1.7rem",
|
||||||
|
display: rostersJSON.length > 0 ? "" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Your rosters
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="print-display-none"
|
||||||
|
style={{
|
||||||
|
gap: 8,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rostersJSON.map((r, index) => (
|
||||||
|
<div key={index} style={{ position: "relative" }}>
|
||||||
|
<button
|
||||||
|
className="print-display-none"
|
||||||
|
style={{ fontSize: roster ? "" : "1.2rem" }}
|
||||||
|
onClick={() => loadFromLocalStorage(r)}
|
||||||
|
>
|
||||||
|
{r.name}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setRosters(
|
||||||
|
stringifyJSON(rostersJSON.filter((_, i) => i !== index)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="print-display-none absolute right-[-4px] top-[-4px] rounded-full border-0 bg-red-600 fill-white p-[2px] text-sm transition duration-150 ease-in-out hover:bg-red-600 hover:bg-red-800 focus:outline-none"
|
||||||
|
>
|
||||||
|
<svg height="16" viewBox="0 0 16 16" width="16">
|
||||||
|
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="print-display-none"></div>
|
||||||
<div
|
<div
|
||||||
className="print-display-none"
|
className="print-display-none"
|
||||||
style={{ display: "flex", width: "100%", gap: 8 }}
|
style={{ display: "flex", width: "100%", gap: 8 }}
|
||||||
@@ -206,6 +287,7 @@ function App() {
|
|||||||
Print roster
|
Print roster
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="print-display-none"
|
className="print-display-none"
|
||||||
style={{ display: roster ? "flex" : "none", width: "100%", gap: 16 }}
|
style={{ display: roster ? "flex" : "none", width: "100%", gap: 16 }}
|
||||||
@@ -221,6 +303,7 @@ function App() {
|
|||||||
<label style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
<label style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
|
className="rounded-sm border border-gray-400"
|
||||||
style={{ height: 24, width: 32, padding: "0 2px" }}
|
style={{ height: 24, width: 32, padding: "0 2px" }}
|
||||||
value={primaryColor}
|
value={primaryColor}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -254,9 +337,10 @@ function App() {
|
|||||||
{edition === 10 && (
|
{edition === 10 && (
|
||||||
<Roster10th roster={roster} onePerPage={onePerPage} />
|
<Roster10th roster={roster} onePerPage={onePerPage} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: "8px 0",
|
paddingTop: 8,
|
||||||
fontSize: "1.7rem",
|
fontSize: "1.7rem",
|
||||||
display: roster ? "none" : "flex",
|
display: roster ? "none" : "flex",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
export const Arrow = (
|
export const Arrow = (
|
||||||
<svg viewBox="0 0 16 8" width={16} height={8} fill="var(--primary-color)">
|
<svg
|
||||||
|
viewBox="0 0 16 8"
|
||||||
|
width={"1.2rem"}
|
||||||
|
height={8}
|
||||||
|
fill="var(--primary-color)"
|
||||||
|
>
|
||||||
<path d="m0 0h10l6 4-6 4h-10z" />
|
<path d="m0 0h10l6 4-6 4h-10z" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
function replacer(key, value) {
|
||||||
|
if (value instanceof Map) {
|
||||||
|
return {
|
||||||
|
dataType: "Map",
|
||||||
|
value: Array.from(value.entries()), // or with spread: value: [...value]
|
||||||
|
};
|
||||||
|
} else if (value instanceof Set) {
|
||||||
|
return ["__isSet", ...value];
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function reviver(key, value) {
|
||||||
|
if (value instanceof Array && value[0] === "__isSet") {
|
||||||
|
return new Set(value.slice(1));
|
||||||
|
}
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
if (value.dataType === "Map") {
|
||||||
|
return new Map(value.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseJSON = (string) => {
|
||||||
|
return JSON.parse(string, reviver);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stringifyJSON = (value) => {
|
||||||
|
return JSON.stringify(value, replacer);
|
||||||
|
};
|
||||||
+223
-200
@@ -1,213 +1,236 @@
|
|||||||
:root {
|
@tailwind base;
|
||||||
font-family: "Noto Sans", sans-serif, Inter, system-ui, Avenir, Helvetica,
|
@tailwind components;
|
||||||
Arial, sans-serif;
|
@tailwind utilities;
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
@layer base {
|
||||||
color: #111113;
|
:root {
|
||||||
background-color: #ffffff;
|
font-family: "Noto Sans", sans-serif, Inter, system-ui, Avenir, Helvetica,
|
||||||
|
Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
font-synthesis: none;
|
color-scheme: light dark;
|
||||||
text-rendering: optimizeLegibility;
|
color: #111113;
|
||||||
-webkit-font-smoothing: antialiased;
|
background-color: #ffffff;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
|
|
||||||
--size-20: 20px;
|
font-synthesis: none;
|
||||||
--size-24: 24px;
|
text-rendering: optimizeLegibility;
|
||||||
--size-32: 32px;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
|
||||||
@font-face {
|
--size-20: 20px;
|
||||||
font-family: "ConduitITCStd";
|
--size-24: 24px;
|
||||||
src: url("/fonts/ConduitITCStd ExtraBold.woff2") format("woff2");
|
--size-32: 32px;
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "ConduitITCStd";
|
|
||||||
src: url("/fonts/ConduitITCStd Bold.woff2") format("woff2");
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "ConduitITCStd";
|
|
||||||
src: url("/fonts/ConduitITCStd-Regular.woff2") format("woff2");
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
print-color-adjust: exact;
|
|
||||||
-webkit-print-color-adjust: exact;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-width: 600px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button,
|
|
||||||
.button {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
color: #111113;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 2px solid #999;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover,
|
|
||||||
.button:hover {
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
background-color: var(--primary-color-transparent);
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible,
|
|
||||||
.button:focus,
|
|
||||||
.button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: var(--size-24) var(--size-20);
|
|
||||||
max-width: 1150px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
width: 100%;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 1.7em;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subheader {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rosterUpload {
|
|
||||||
flex: 1;
|
|
||||||
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: var(--primary-color);
|
|
||||||
background-color: var(--primary-color-transparent);
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rosterUploaded,
|
|
||||||
.rosterUploaded:hover {
|
|
||||||
height: auto;
|
|
||||||
border: 2px solid #999;
|
|
||||||
font-size: 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weapons-table th {
|
|
||||||
font-size: 1.1em;
|
|
||||||
padding: 2px 6px;
|
|
||||||
height: 27px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weapons-table td {
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-top: 1px dotted #9e9fa1;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noBorderTop td {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emptyRow {
|
|
||||||
height: 22.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rowOtherColor {
|
|
||||||
background-color: #d0d1d3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avoid-page-break {
|
|
||||||
position: relative;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-break:not(.print-display-none) {
|
|
||||||
page-break-after: page;
|
|
||||||
break-after: page;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.print-display-none,
|
|
||||||
.print-display-none * {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
@font-face {
|
||||||
min-width: 1000px;
|
font-family: "ConduitITCStd";
|
||||||
|
src: url("/fonts/ConduitITCStd ExtraBold.woff2") format("woff2");
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "ConduitITCStd";
|
||||||
|
src: url("/fonts/ConduitITCStd Bold.woff2") format("woff2");
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "ConduitITCStd";
|
||||||
|
src: url("/fonts/ConduitITCStd-Regular.woff2") format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
.button {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #111113;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #999;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover,
|
||||||
|
.button:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background-color: var(--primary-color-transparent);
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible,
|
||||||
|
.button:focus,
|
||||||
|
.button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
padding: 0;
|
display: flex;
|
||||||
display: block;
|
flex-direction: column;
|
||||||
max-width: none;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: var(--size-24) var(--size-20);
|
||||||
|
max-width: 1150px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
.header {
|
||||||
:root {
|
display: flex;
|
||||||
font-size: 11px;
|
justify-content: space-between;
|
||||||
--size-20: 8px;
|
align-items: center;
|
||||||
--size-24: 12px;
|
padding: 8px var(--size-20);
|
||||||
--size-32: 16px;
|
background-color: var(--primary-color);
|
||||||
|
width: 100%;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.7em;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subheader {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rosterUpload {
|
||||||
|
flex: 1;
|
||||||
|
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: var(--primary-color);
|
||||||
|
background-color: var(--primary-color-transparent);
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rosterUploaded,
|
||||||
|
.rosterUploaded:hover {
|
||||||
|
height: auto;
|
||||||
|
border: 2px solid #999;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weapons-table th {
|
||||||
|
font-size: 1.1em;
|
||||||
|
padding: 2px 0.4rem;
|
||||||
|
height: 27px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weapons-table td {
|
||||||
|
padding: 1px 0.4rem;
|
||||||
|
border-top: 1px dotted #9e9fa1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noBorderTop td {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyRow {
|
||||||
|
height: 22.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rowOtherColor {
|
||||||
|
background-color: #d0d1d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avoid-page-break {
|
||||||
|
position: relative;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-break:not(.print-display-none) {
|
||||||
|
page-break-after: page;
|
||||||
|
break-after: page;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.print-display-none,
|
||||||
|
.print-display-none * {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
min-width: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
padding: 0;
|
||||||
|
display: block;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 800px) {
|
||||||
|
:root {
|
||||||
|
font-size: 11px;
|
||||||
|
--size-20: 8px;
|
||||||
|
--size-24: 12px;
|
||||||
|
--size-32: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
:root {
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1.3;
|
||||||
|
--size-20: 8px;
|
||||||
|
--size-24: 12px;
|
||||||
|
--size-32: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-14
@@ -168,7 +168,7 @@ export class Model extends BaseNotes {
|
|||||||
]) {
|
]) {
|
||||||
if (
|
if (
|
||||||
!deduped.some(
|
!deduped.some(
|
||||||
(e) => upgrade.getSelectionName() === e.getSelectionName()
|
(e) => upgrade.getSelectionName() === e.getSelectionName(),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
deduped.push(upgrade);
|
deduped.push(upgrade);
|
||||||
@@ -232,7 +232,7 @@ export class Unit extends BaseNotes {
|
|||||||
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
|
||||||
@@ -315,7 +315,7 @@ 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.rangedWeapons = this.models
|
this.rangedWeapons = this.models
|
||||||
.map((m) => m.rangedWeapons)
|
.map((m) => m.rangedWeapons)
|
||||||
@@ -330,13 +330,13 @@ export class Unit extends BaseNotes {
|
|||||||
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), []),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,6 +353,7 @@ export class Force extends BaseNotes {
|
|||||||
export class Roster40k extends BaseNotes {
|
export class Roster40k extends BaseNotes {
|
||||||
cost = new Costs();
|
cost = new Costs();
|
||||||
forces = [];
|
forces = [];
|
||||||
|
gameType = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Costs {
|
export class Costs {
|
||||||
@@ -386,12 +387,12 @@ export class Costs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Create40kRoster10th(doc) {
|
export function Create40kRoster10th(doc, gameType) {
|
||||||
// Determine roster type (game system).
|
// Determine roster type (game system).
|
||||||
let info = doc.querySelector("roster");
|
let info = doc.querySelector("roster");
|
||||||
if (info) {
|
if (info) {
|
||||||
const roster = new Roster40k();
|
const roster = new Roster40k();
|
||||||
|
roster.gameType = gameType;
|
||||||
const name = info.getAttributeNode("name")?.nodeValue;
|
const name = info.getAttributeNode("name")?.nodeValue;
|
||||||
if (name) {
|
if (name) {
|
||||||
roster.name = name;
|
roster.name = name;
|
||||||
@@ -688,7 +689,7 @@ function ParseUnit(root) {
|
|||||||
|
|
||||||
// First, find model stats. These have typeName=Unit.
|
// First, find model stats. These have typeName=Unit.
|
||||||
const modelStatsProfiles = Array.from(
|
const modelStatsProfiles = Array.from(
|
||||||
root.querySelectorAll('profile[typeName="Unit"],profile[typeName="Model"]')
|
root.querySelectorAll('profile[typeName="Unit"],profile[typeName="Model"]'),
|
||||||
);
|
);
|
||||||
ParseModelStatsProfiles(modelStatsProfiles, unit, unitName);
|
ParseModelStatsProfiles(modelStatsProfiles, unit, unitName);
|
||||||
seenProfiles.push(...modelStatsProfiles);
|
seenProfiles.push(...modelStatsProfiles);
|
||||||
@@ -711,7 +712,7 @@ function ParseUnit(root) {
|
|||||||
// Some units are under a root selection with type="upgrade".
|
// Some units are under a root selection with type="upgrade".
|
||||||
if (modelSelections.length === 0) {
|
if (modelSelections.length === 0) {
|
||||||
modelSelections.push(
|
modelSelections.push(
|
||||||
...Array.from(root.querySelectorAll('selection[type="model"]'))
|
...Array.from(root.querySelectorAll('selection[type="model"]')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Some single-model units have type="unit" or type="upgrade".
|
// Some single-model units have type="unit" or type="upgrade".
|
||||||
@@ -726,7 +727,7 @@ function ParseUnit(root) {
|
|||||||
// Now, parse the model -- profiles for stats, and selections for upgrades.
|
// Now, parse the model -- profiles for stats, and selections for upgrades.
|
||||||
for (const modelSelection of modelSelections) {
|
for (const modelSelection of modelSelections) {
|
||||||
const profiles = Array.from(
|
const profiles = Array.from(
|
||||||
modelSelection.querySelectorAll("profiles>profile")
|
modelSelection.querySelectorAll("profiles>profile"),
|
||||||
);
|
);
|
||||||
const unseenProfiles = profiles.filter((e) => !seenProfiles.includes(e));
|
const unseenProfiles = profiles.filter((e) => !seenProfiles.includes(e));
|
||||||
seenProfiles.push(...unseenProfiles);
|
seenProfiles.push(...unseenProfiles);
|
||||||
@@ -742,13 +743,13 @@ function ParseUnit(root) {
|
|||||||
// Find all upgrades on the model. This may include weapons that were
|
// Find all upgrades on the model. This may include weapons that were
|
||||||
// parsed from profiles (above), so dedupe those in nameAndGear().
|
// parsed from profiles (above), so dedupe those in nameAndGear().
|
||||||
for (const upgradeSelection of modelSelection.querySelectorAll(
|
for (const upgradeSelection of modelSelection.querySelectorAll(
|
||||||
'selections>selection[type="upgrade"]'
|
'selections>selection[type="upgrade"]',
|
||||||
)) {
|
)) {
|
||||||
// Ignore selections without abilities but with sub-selection upgrades,
|
// Ignore selections without abilities but with sub-selection upgrades,
|
||||||
// since those sub-selections will be picked up individually.
|
// since those sub-selections will be picked up individually.
|
||||||
if (
|
if (
|
||||||
upgradeSelection.querySelector(
|
upgradeSelection.querySelector(
|
||||||
'selections>selection[type="upgrade"]'
|
'selections>selection[type="upgrade"]',
|
||||||
) &&
|
) &&
|
||||||
!HasImmediateProfileWithTypeName(upgradeSelection, "Abilities")
|
!HasImmediateProfileWithTypeName(upgradeSelection, "Abilities")
|
||||||
)
|
)
|
||||||
@@ -941,7 +942,7 @@ function ParseModelProfiles(profiles, model, unit) {
|
|||||||
profile,
|
profile,
|
||||||
profileName,
|
profileName,
|
||||||
typeName,
|
typeName,
|
||||||
unit.abilities.Abilities
|
unit.abilities.Abilities,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Everything else, like Prayers and Warlord Traits.
|
// Everything else, like Prayers and Warlord Traits.
|
||||||
@@ -950,7 +951,7 @@ function ParseModelProfiles(profiles, model, unit) {
|
|||||||
profile,
|
profile,
|
||||||
profileName,
|
profileName,
|
||||||
typeName,
|
typeName,
|
||||||
unit.abilities[typeName]
|
unit.abilities[typeName],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-13
@@ -190,7 +190,7 @@ export class Model extends BaseNotes {
|
|||||||
for (const upgrade of [...this.weapons, ...this.upgrades]) {
|
for (const upgrade of [...this.weapons, ...this.upgrades]) {
|
||||||
if (
|
if (
|
||||||
!deduped.some(
|
!deduped.some(
|
||||||
(e) => upgrade.getSelectionName() === e.getSelectionName()
|
(e) => upgrade.getSelectionName() === e.getSelectionName(),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
deduped.push(upgrade);
|
deduped.push(upgrade);
|
||||||
@@ -251,7 +251,7 @@ export class Unit extends BaseNotes {
|
|||||||
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
|
||||||
@@ -334,7 +334,7 @@ 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)
|
||||||
@@ -345,13 +345,13 @@ export class Unit extends BaseNotes {
|
|||||||
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), []),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,6 +368,7 @@ export class Force extends BaseNotes {
|
|||||||
export class Roster40k extends BaseNotes {
|
export class Roster40k extends BaseNotes {
|
||||||
cost = new Costs();
|
cost = new Costs();
|
||||||
forces = [];
|
forces = [];
|
||||||
|
gameType = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Costs {
|
export class Costs {
|
||||||
@@ -401,12 +402,12 @@ export class Costs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Create40kRoster(doc) {
|
export function Create40kRoster(doc, gameType) {
|
||||||
// Determine roster type (game system).
|
// Determine roster type (game system).
|
||||||
let info = doc.querySelector("roster");
|
let info = doc.querySelector("roster");
|
||||||
if (info) {
|
if (info) {
|
||||||
const roster = new Roster40k();
|
const roster = new Roster40k();
|
||||||
|
roster.gameType = gameType;
|
||||||
const name = info.getAttributeNode("name")?.nodeValue;
|
const name = info.getAttributeNode("name")?.nodeValue;
|
||||||
if (name) {
|
if (name) {
|
||||||
roster.name = name;
|
roster.name = name;
|
||||||
@@ -723,7 +724,7 @@ function ParseUnit(root) {
|
|||||||
|
|
||||||
// First, find model stats. These have typeName=Unit.
|
// First, find model stats. These have typeName=Unit.
|
||||||
const modelStatsProfiles = Array.from(
|
const modelStatsProfiles = Array.from(
|
||||||
root.querySelectorAll('profile[typeName="Unit"],profile[typeName="Model"]')
|
root.querySelectorAll('profile[typeName="Unit"],profile[typeName="Model"]'),
|
||||||
);
|
);
|
||||||
ParseModelStatsProfiles(modelStatsProfiles, unit, unitName);
|
ParseModelStatsProfiles(modelStatsProfiles, unit, unitName);
|
||||||
seenProfiles.push(...modelStatsProfiles);
|
seenProfiles.push(...modelStatsProfiles);
|
||||||
@@ -746,7 +747,7 @@ function ParseUnit(root) {
|
|||||||
// Some units are under a root selection with type="upgrade".
|
// Some units are under a root selection with type="upgrade".
|
||||||
if (modelSelections.length === 0) {
|
if (modelSelections.length === 0) {
|
||||||
modelSelections.push(
|
modelSelections.push(
|
||||||
...Array.from(root.querySelectorAll('selection[type="model"]'))
|
...Array.from(root.querySelectorAll('selection[type="model"]')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Some single-model units have type="unit" or type="upgrade".
|
// Some single-model units have type="unit" or type="upgrade".
|
||||||
@@ -761,7 +762,7 @@ function ParseUnit(root) {
|
|||||||
// Now, parse the model -- profiles for stats, and selections for upgrades.
|
// Now, parse the model -- profiles for stats, and selections for upgrades.
|
||||||
for (const modelSelection of modelSelections) {
|
for (const modelSelection of modelSelections) {
|
||||||
const profiles = Array.from(
|
const profiles = Array.from(
|
||||||
modelSelection.querySelectorAll("profiles>profile")
|
modelSelection.querySelectorAll("profiles>profile"),
|
||||||
);
|
);
|
||||||
const unseenProfiles = profiles.filter((e) => !seenProfiles.includes(e));
|
const unseenProfiles = profiles.filter((e) => !seenProfiles.includes(e));
|
||||||
seenProfiles.push(...unseenProfiles);
|
seenProfiles.push(...unseenProfiles);
|
||||||
@@ -777,13 +778,13 @@ function ParseUnit(root) {
|
|||||||
// Find all upgrades on the model. This may include weapons that were
|
// Find all upgrades on the model. This may include weapons that were
|
||||||
// parsed from profiles (above), so dedupe those in nameAndGear().
|
// parsed from profiles (above), so dedupe those in nameAndGear().
|
||||||
for (const upgradeSelection of modelSelection.querySelectorAll(
|
for (const upgradeSelection of modelSelection.querySelectorAll(
|
||||||
'selections>selection[type="upgrade"]'
|
'selections>selection[type="upgrade"]',
|
||||||
)) {
|
)) {
|
||||||
// Ignore selections without abilities but with sub-selection upgrades,
|
// Ignore selections without abilities but with sub-selection upgrades,
|
||||||
// since those sub-selections will be picked up individually.
|
// since those sub-selections will be picked up individually.
|
||||||
if (
|
if (
|
||||||
upgradeSelection.querySelector(
|
upgradeSelection.querySelector(
|
||||||
'selections>selection[type="upgrade"]'
|
'selections>selection[type="upgrade"]',
|
||||||
) &&
|
) &&
|
||||||
!HasImmediateProfileWithTypeName(upgradeSelection, "Abilities")
|
!HasImmediateProfileWithTypeName(upgradeSelection, "Abilities")
|
||||||
)
|
)
|
||||||
@@ -965,7 +966,7 @@ function ParseModelProfiles(profiles, model, unit) {
|
|||||||
profile,
|
profile,
|
||||||
profileName,
|
profileName,
|
||||||
typeName,
|
typeName,
|
||||||
unit.abilities.Abilities
|
unit.abilities.Abilities,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user