added basic support for 10th
This commit is contained in:
@@ -0,0 +1,961 @@
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import factionBackground from "../assets/factionBackground.png";
|
||||
import adeptusAstartesIcon from "../assets/adeptusAstartesIcon.png";
|
||||
import rangedIcon from "../assets/rangedIcon.png";
|
||||
import { Arrow, wavyLine } from "../assets/icons";
|
||||
import { Weapons, hasDifferentProfiles } from "./Weapons";
|
||||
|
||||
export const Roster = ({ roster, onePerPage }) => {
|
||||
if (!roster) {
|
||||
return null;
|
||||
}
|
||||
const { name, cost, forces } = roster;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="print-display-none"
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color)",
|
||||
color: "#fff",
|
||||
padding: "4px 16px",
|
||||
fontSize: " 1.7em",
|
||||
fontWeight: "800",
|
||||
textTransform: "uppercase",
|
||||
marginBottom: "12px",
|
||||
}}
|
||||
>
|
||||
{name} [{cost.commandPoints} CP, {cost.points} pts]
|
||||
</div>
|
||||
{forces.map((force, index) => (
|
||||
<Force key={index} force={force} onePerPage={onePerPage} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Force = ({ force, onePerPage }) => {
|
||||
const { units, factionRules, rules, catalog } = force;
|
||||
var mergedRules = new Map([...factionRules, ...rules]);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "contents",
|
||||
}}
|
||||
>
|
||||
{units.map((unit, index) => (
|
||||
<Unit
|
||||
key={unit.name + index}
|
||||
index={index}
|
||||
unit={unit}
|
||||
catalog={catalog}
|
||||
onePerPage={onePerPage}
|
||||
forceRules={rules}
|
||||
/>
|
||||
))}
|
||||
<ForceRules rules={mergedRules} onePerPage={onePerPage} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Unit = ({ unit, index, catalog, onePerPage, forceRules }) => {
|
||||
const [hide, setHide] = useState(false);
|
||||
const uploadRef = useRef();
|
||||
const [image, setImage] = useState(null);
|
||||
let {
|
||||
name,
|
||||
weapons,
|
||||
abilities,
|
||||
keywords,
|
||||
factions,
|
||||
rules,
|
||||
modelStats,
|
||||
modelList,
|
||||
psykers,
|
||||
spells,
|
||||
cost,
|
||||
woundTracker,
|
||||
} = unit;
|
||||
|
||||
const meleeWeapons = weapons
|
||||
.filter(
|
||||
(weapon) =>
|
||||
(weapon.range === "Melee" || weapon.type === "Melee") &&
|
||||
weapon.range !== ""
|
||||
)
|
||||
.sort((a, b) => a.selectionName.localeCompare(b.selectionName));
|
||||
if (meleeWeapons.length === 0) {
|
||||
meleeWeapons.push({
|
||||
name: "Close combat weapon",
|
||||
selectionName: "Close combat weapon",
|
||||
range: "Melee",
|
||||
damage: "1",
|
||||
type: "Melee",
|
||||
str: "+0",
|
||||
ap: "0",
|
||||
});
|
||||
}
|
||||
const rangedWeapons = weapons
|
||||
.filter(
|
||||
(weapon) =>
|
||||
weapon.range !== "Melee" && weapon.range !== "-" && weapon.range !== ""
|
||||
)
|
||||
.sort((a, b) => a.selectionName.localeCompare(b.selectionName));
|
||||
|
||||
const weaponDescriptions = weapons
|
||||
.filter(
|
||||
(weapon) =>
|
||||
(weapon.range === "-" || weapon.range === "") && weapon.abilities
|
||||
)
|
||||
.sort((a, b) => a.selectionName.localeCompare(b.selectionName));
|
||||
const modelsWithDifferentProfiles = weapons.filter((weapon, index) => {
|
||||
const { selectionName, name } = weapon;
|
||||
const previousWeapon = weapons[index - 1];
|
||||
const nextWeapon = weapons[index + 1];
|
||||
return (
|
||||
hasDifferentProfiles(selectionName, name) &&
|
||||
((previousWeapon &&
|
||||
hasDifferentProfiles(
|
||||
previousWeapon.selectionName,
|
||||
previousWeapon.name
|
||||
) &&
|
||||
selectionName === previousWeapon.selectionName) ||
|
||||
(nextWeapon &&
|
||||
hasDifferentProfiles(nextWeapon.selectionName, nextWeapon.name) &&
|
||||
selectionName === nextWeapon.selectionName))
|
||||
);
|
||||
});
|
||||
|
||||
const overridePrimary = getOverridePrimary(factions, keywords);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if the browser is Safari, and if so, remove the accept attribute
|
||||
// from the file input element. This is because Safari doesn't support
|
||||
// extensions on the accept attribute for input type=file
|
||||
// (https://caniuse.com/input-file-accept). If set, they will not allow any
|
||||
// file to be selected.
|
||||
if (
|
||||
navigator.userAgent.match(/AppleWebKit.*Safari/) &&
|
||||
!navigator.userAgent.includes("Chrome")
|
||||
) {
|
||||
uploadRef.current?.removeAttribute("accept");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"avoid-page-break" +
|
||||
(onePerPage ? " page-break" : "") +
|
||||
(hide ? " print-display-none" : "")
|
||||
}
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
marginBottom: 32,
|
||||
|
||||
"--primary-color": overridePrimary,
|
||||
"--primary-color-transparent": overridePrimary
|
||||
? overridePrimary + "60"
|
||||
: "",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="print-display-none"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<label className="print-display-none">
|
||||
<input type="checkbox" onChange={() => setHide(!hide)} />
|
||||
Don't print this card.
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: "24px 0",
|
||||
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",
|
||||
minHeight: 200,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: "4px 16px",
|
||||
color: "#fff",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<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
|
||||
style={{
|
||||
fontFamily: "ConduitITCStd",
|
||||
fontSize: "2.5em",
|
||||
letterSpacing: ".1px",
|
||||
lineHeight: "1",
|
||||
fontWeight: 800,
|
||||
textTransform: "uppercase",
|
||||
zIndex: 1,
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
<span style={{ textTransform: "initial", fontSize: "1.2rem" }}>
|
||||
{cost.points}pts
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 6,
|
||||
zIndex: 1,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{modelStats.map((model, index) => (
|
||||
<ModelStats
|
||||
key={index}
|
||||
modelStats={modelStats}
|
||||
modelStat={model}
|
||||
modelList={modelList}
|
||||
index={index}
|
||||
showName={modelStats.length > 1}
|
||||
showWeapons={
|
||||
modelList.length > 1 || modelList[0].includes("x ") // show Weapons when multiple weapons of same type, for example (2x Atomiser Beam, Reanimator's Claws)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
<WoundTracker woundTracker={woundTracker} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: "100%",
|
||||
bottom: 0,
|
||||
width: "50%",
|
||||
}}
|
||||
>
|
||||
{image && (
|
||||
<img
|
||||
src={image}
|
||||
alt=""
|
||||
style={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||
/>
|
||||
)}
|
||||
<label
|
||||
className="button print-display-none"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
right: 0,
|
||||
border: "1px solid #999",
|
||||
top: 1,
|
||||
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",
|
||||
}}
|
||||
/>
|
||||
Upload image
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
border: "2px solid var(--primary-color)",
|
||||
borderTop: "none",
|
||||
backgroundColor: "#DFE0E2",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: "1",
|
||||
borderRight: "2px solid var(--primary-color)",
|
||||
position: "relative",
|
||||
paddingBottom: 40,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Spells title="PSYCHIC" spells={spells} />
|
||||
<table
|
||||
cellSpacing="0"
|
||||
className="weapons-table"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Weapons
|
||||
title="RANGED WEAPONS"
|
||||
weapons={rangedWeapons}
|
||||
modelStats={modelStats}
|
||||
forceRules={forceRules}
|
||||
/>
|
||||
<Weapons
|
||||
title="MELEE WEAPONS"
|
||||
weapons={meleeWeapons}
|
||||
modelStats={modelStats}
|
||||
forceRules={forceRules}
|
||||
/>
|
||||
</table>
|
||||
<div style={{ flex: "1" }}></div>
|
||||
<table
|
||||
cellSpacing="0"
|
||||
className="weapons-table"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<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 &&
|
||||
!weaponDescriptions.length && (
|
||||
<tr className="emptyRow noBorderTop">
|
||||
<td style={{ width: 37, borderTop: "none" }}>{Arrow}</td>
|
||||
<td
|
||||
colSpan={7}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
fontSize: ".8em",
|
||||
paddingLeft: 0,
|
||||
}}
|
||||
>
|
||||
Before selecting targets for this weapon, select one of
|
||||
its profiles to make attacks with.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Keywords keywords={keywords} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: "var(--size-20) var(--size-20) 50px var(--size-20)",
|
||||
flex: "1",
|
||||
maxWidth: 400,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "1.1em",
|
||||
padding: "1px 8px",
|
||||
backgroundColor: "var(--primary-color)",
|
||||
color: "#fff",
|
||||
fontWeight: 600,
|
||||
minHeight: 27,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
ABILITIES
|
||||
</div>
|
||||
<Rules rules={rules} />
|
||||
<Abilities abilities={abilities.Abilities} />
|
||||
<Psykers psykers={psykers} />
|
||||
<Factions factions={factions} />
|
||||
<FactionIcon catalog={catalog} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ModelStats = ({ modelStat, index, showName, showWeapons, modelList }) => {
|
||||
let { move, toughness, save, wounds, leadership, name, bs, ws, attacks } =
|
||||
modelStat;
|
||||
if (!wounds) {
|
||||
wounds = "/";
|
||||
}
|
||||
if (!name.includes("Sgt")) {
|
||||
modelList = modelList.filter((model) => !model.includes("Sgt"));
|
||||
}
|
||||
if (!name.includes("Sergeant")) {
|
||||
modelList = modelList.filter((model) => !model.includes("Sergeant"));
|
||||
}
|
||||
let modelListMatches = modelList.filter((model) =>
|
||||
model.includes(name + " w")
|
||||
);
|
||||
if (modelListMatches.length === 0) {
|
||||
modelListMatches = modelList.filter((model) => model.includes(name));
|
||||
}
|
||||
modelListMatches = modelListMatches.map((model) =>
|
||||
model.replaceAll(name, "")
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
|
||||
<Characteristic title="M" characteristic={move} index={index} />
|
||||
<Characteristic title="T" characteristic={toughness} index={index} />
|
||||
<Characteristic title="SV" characteristic={save} index={index} />
|
||||
<Characteristic title="W" characteristic={wounds} index={index} />
|
||||
<Characteristic title="LD" characteristic={leadership} index={index} />
|
||||
<div>
|
||||
{showName && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: index === 0 ? 16 : 0,
|
||||
marginLeft: -10,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
)}
|
||||
{showWeapons && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: index === 0 && !showName ? 16 : 0,
|
||||
marginLeft: -10,
|
||||
fontSize: "0.7em",
|
||||
}}
|
||||
>
|
||||
{modelListMatches.map((model, index) => (
|
||||
<div key={model}>{model}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FactionIcon = ({ catalog }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: -28,
|
||||
bottom: -16,
|
||||
width: 54,
|
||||
height: 54,
|
||||
transform: "rotate(-45deg)",
|
||||
border: "2px solid var(--primary-color)",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: catalog.includes("Imperium")
|
||||
? `url(${adeptusAstartesIcon})`
|
||||
: "#bcbcbe",
|
||||
backgroundPosition: "center",
|
||||
minHeight: "75px",
|
||||
minWidth: "76px",
|
||||
transform: "rotate(45deg)",
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Keywords = ({ keywords }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: -2,
|
||||
bottom: -16,
|
||||
padding: "0 8px",
|
||||
paddingRight: 42,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#BCBCBE",
|
||||
border: "2px solid var(--primary-color)",
|
||||
width: "calc(100% - 18px)",
|
||||
backgroundSize: "contain",
|
||||
minHeight: 54,
|
||||
gap: 3,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: "1.1em" }}>KEYWORDS:</span>
|
||||
<span style={{ fontSize: "1em", fontWeight: 800 }}>
|
||||
{[...keywords].join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Factions = ({ factions }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
bottom: -16,
|
||||
padding: "0 4px",
|
||||
paddingLeft: 42,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
background: `url(${factionBackground})`,
|
||||
color: "#fff",
|
||||
border: "2px solid var(--primary-color)",
|
||||
borderLeft: "none",
|
||||
width: "calc(100% - 18px)",
|
||||
backgroundSize: "contain",
|
||||
minHeight: 54,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: ".9em", lineHeight: 1.3 }}>
|
||||
FACTION KEYWORDS:
|
||||
</span>
|
||||
<span style={{ fontSize: ".9em", fontWeight: 600 }}>
|
||||
{[...factions].join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Rules = ({ rules }) => {
|
||||
if (!rules || rules.size === 0) return null;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "5px 2px",
|
||||
gap: 3,
|
||||
borderBottom: "1px dotted #9e9fa1",
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: ".8em" }}>RULES: </span>
|
||||
<span style={{ fontSize: ".8em", fontWeight: 700 }}>
|
||||
{[...rules.keys()].map((rule) => rule).join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Abilities = ({ abilities }) => {
|
||||
if (!abilities) return null;
|
||||
let keys = [...abilities.keys()];
|
||||
keys = keys.filter(
|
||||
(key) => key !== "Stratagem: Warlord Trait" && key !== "Stratagem: Relic"
|
||||
);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
padding: "4px 2px",
|
||||
borderBottom: "1px dotted rgb(158, 159, 161)",
|
||||
}}
|
||||
>
|
||||
{abilities &&
|
||||
keys.map((ability) => (
|
||||
<div
|
||||
key={ability}
|
||||
style={{
|
||||
fontSize: ".8em",
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 700 }}>{ability}:</span>{" "}
|
||||
{abilities.get(ability)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Characteristic = ({ title, characteristic, index }) => {
|
||||
return (
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", alignItems: "center" }}
|
||||
>
|
||||
{index === 0 && (
|
||||
<div style={{ fontSize: ".9em", fontWeight: 800 }}>{title}</div>
|
||||
)}
|
||||
<FancyBox>{characteristic}</FancyBox>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FancyBox = ({ children }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
color: "var(--primary-color)",
|
||||
padding: 2,
|
||||
background:
|
||||
"linear-gradient(-45deg, transparent 4px, var(--primary-color) 0)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
minWidth: 40,
|
||||
minHeight: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "linear-gradient(315deg, transparent 3px, #E8EDE7 0)",
|
||||
padding: 3,
|
||||
fontSize: "1.6em",
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Psykers = ({ title, psykers }) => {
|
||||
return (
|
||||
<table
|
||||
cellSpacing="0"
|
||||
className="weapons-table"
|
||||
style={{ width: "100%", margin: "4px 2px" }}
|
||||
>
|
||||
{psykers.length > 0 && (
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color)",
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
<th style={{ textAlign: "left" }}>PSYKER</th>
|
||||
<th>CAST</th>
|
||||
<th>DENY</th>
|
||||
<th style={{ textAlign: "left", width: "100%" }} colSpan="4">
|
||||
POWERS KNOWN
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
)}
|
||||
<tbody>
|
||||
{psykers.map((psyker, index) => (
|
||||
<Psyker key={psyker.name} psyker={psyker} index={index} />
|
||||
))}
|
||||
{psykers.length > 0 && (
|
||||
<tr className="emptyRow">
|
||||
<td colSpan={4}></td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const Psyker = ({ psyker, index }) => {
|
||||
let { name, cast, deny, powers } = psyker;
|
||||
|
||||
return (
|
||||
<tr
|
||||
className={index % 2 ? "rowOtherColor" : ""}
|
||||
style={{ fontSize: ".8em" }}
|
||||
>
|
||||
<td style={{ textAlign: "left" }}></td>
|
||||
<td>{cast}</td>
|
||||
<td>{deny}</td>
|
||||
<td style={{ textAlign: "left" }} colSpan="4">
|
||||
{powers}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const WoundTracker = ({ woundTracker }) => {
|
||||
if (!woundTracker || woundTracker.length === 0) return null;
|
||||
const uniqueWoundTracker = woundTracker.filter((woundTrack, index) => {
|
||||
const firstIndex = woundTracker.findIndex(
|
||||
(woundTrack2) => woundTrack2.name === woundTrack.name
|
||||
);
|
||||
return firstIndex === index;
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<table
|
||||
cellSpacing="0"
|
||||
className="weapons-table"
|
||||
style={{ width: "100%", margin: "4px 2px", marginBottom: -16 }}
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color)",
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
{[...uniqueWoundTracker[0].table.keys()].map((key) => (
|
||||
<th key={key}>{key}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{uniqueWoundTracker.map((woundTrack, index) => (
|
||||
<tr key={index}>
|
||||
{[...woundTrack.table.values()].map((entry) => (
|
||||
<td key={entry} style={{ borderTop: "none" }}>
|
||||
{entry}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Spells = ({ title, spells }) => {
|
||||
return (
|
||||
<table
|
||||
cellSpacing="0"
|
||||
className="weapons-table"
|
||||
style={{ width: "100%", marginTop: "var(--size-20)" }}
|
||||
>
|
||||
{spells.length > 0 && (
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color)",
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
<th style={{ width: 37 }}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<img src={rangedIcon} />
|
||||
</div>
|
||||
</th>
|
||||
<th style={{ textAlign: "left" }}>{title}</th>
|
||||
<th>RANGE</th>
|
||||
<th>WARP CHARGE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
)}
|
||||
<tbody>
|
||||
{spells.map((spell, index) => (
|
||||
<Spell
|
||||
key={spell.name}
|
||||
spell={spell}
|
||||
index={index}
|
||||
className={getSpellClassNames(spells, index)}
|
||||
/>
|
||||
))}
|
||||
{spells.length > 0 && (
|
||||
<tr className="emptyRow">
|
||||
<td style={{ width: 37, borderTop: "none" }}></td>
|
||||
<td colSpan={7}></td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const Spell = ({ spell, index, className }) => {
|
||||
let { name, range, manifest, details } = spell;
|
||||
return (
|
||||
<>
|
||||
<tr className={className}>
|
||||
<td style={{ borderTop: "none", backgroundColor: "#dfe0e2" }}></td>
|
||||
<td style={{ textAlign: "left" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: "0 4px",
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</td>
|
||||
<td>{range}</td>
|
||||
<td>{manifest}</td>
|
||||
</tr>
|
||||
<tr className={className + " noBorderTop"}>
|
||||
<td style={{ backgroundColor: "#dfe0e2" }}></td>
|
||||
<td
|
||||
colSpan="7"
|
||||
style={{
|
||||
textAlign: "left",
|
||||
fontSize: "0.8em",
|
||||
paddingTop: 0,
|
||||
paddingBottom: 1,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
{details}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getSpellClassNames = (spells, index) => {
|
||||
let differentColor = false;
|
||||
for (let i = 1; i <= index; i++) {
|
||||
let { name } = spells[i];
|
||||
if (name !== spells[i - 1].name) {
|
||||
differentColor = !differentColor;
|
||||
}
|
||||
}
|
||||
const classes = [];
|
||||
if (differentColor) classes.push("rowOtherColor");
|
||||
if (index === 0) classes.push("noBorderTop");
|
||||
if (index > 0 && spells[index].name === spells[index - 1].name)
|
||||
classes.push("noBorderTop");
|
||||
return classes.join(" ");
|
||||
};
|
||||
|
||||
const ForceRules = ({ rules, onePerPage }) => {
|
||||
const [hide, setHide] = useState(false);
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"avoid-page-break " +
|
||||
(onePerPage ? "page-break" : "") +
|
||||
(hide ? " print-display-none" : "")
|
||||
}
|
||||
>
|
||||
<label
|
||||
className="print-display-none"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<input type="checkbox" onChange={() => setHide(!hide)} />
|
||||
<span className="print-display-none">Don't print this card.</span>
|
||||
</label>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
padding: "var(--size-20)",
|
||||
backgroundColor: "#dfe0e2",
|
||||
border: "2px solid var(--primary-color)",
|
||||
marginBottom: 32,
|
||||
}}
|
||||
>
|
||||
{[...rules.keys()].map((rule) => (
|
||||
<div
|
||||
key={rule}
|
||||
style={{
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 700 }}>{rule}:</span> {rules.get(rule)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getOverridePrimary = (factions, keywords) => {
|
||||
if (
|
||||
factions.has("Khorne") &&
|
||||
factions.has("Tzeentch") &&
|
||||
factions.has("Nurgle") &&
|
||||
factions.has("Slaanesh")
|
||||
)
|
||||
return;
|
||||
if (factions.has("Khorne") || keywords.has("Khorne")) return "#883531";
|
||||
if (factions.has("Tzeentch") || keywords.has("Tzeentch")) return "#015d68";
|
||||
if (factions.has("Nurgle") || keywords.has("Nurgle")) return "#5c672b";
|
||||
if (factions.has("Slaanesh") || keywords.has("Slaanesh")) return "#634c74";
|
||||
};
|
||||
@@ -0,0 +1,530 @@
|
||||
import meleeIcon from "../assets/meleeIcon.png";
|
||||
import rangedIcon from "../assets/rangedIcon.png";
|
||||
import { Arrow } from "../assets/icons";
|
||||
|
||||
export const Weapons = ({ title, weapons, modelStats, forceRules }) => {
|
||||
const isMelee = title === "MELEE WEAPONS";
|
||||
return (
|
||||
<>
|
||||
{weapons.length > 0 && (
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
backgroundColor: "var(--primary-color)",
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
<th style={{ width: 37 }}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<img src={isMelee ? meleeIcon : rangedIcon} />
|
||||
</div>
|
||||
</th>
|
||||
<th style={{ textAlign: "left" }}>{title}</th>
|
||||
<th>RANGE</th>
|
||||
<th>A</th>
|
||||
<th>{isMelee ? "WS" : "BS"}</th>
|
||||
<th>S</th>
|
||||
<th>AP</th>
|
||||
<th>D</th>
|
||||
</tr>
|
||||
</thead>
|
||||
)}
|
||||
<tbody>
|
||||
{weapons.map((weapon, index) => (
|
||||
<Weapon
|
||||
key={weapon.name}
|
||||
weapon={weapon}
|
||||
previousWeapon={weapons[index - 1]}
|
||||
nextWeapon={weapons[index + 1]}
|
||||
modelStats={modelStats}
|
||||
isMelee={isMelee}
|
||||
className={getWeaponClassNames(weapons, index)}
|
||||
forceRules={forceRules}
|
||||
/>
|
||||
))}
|
||||
{weapons.length > 0 && (
|
||||
<tr className="emptyRow">
|
||||
<td style={{ width: 37, borderTop: "none" }}></td>
|
||||
<td colSpan={7}></td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const hasDifferentProfiles = (selectionName, name) => {
|
||||
return (
|
||||
selectionName &&
|
||||
selectionName.toLowerCase() !== name.toLowerCase() &&
|
||||
!name.includes("(Shooting)") &&
|
||||
!name.includes("(Melee)")
|
||||
);
|
||||
};
|
||||
|
||||
const Weapon = ({
|
||||
weapon,
|
||||
previousWeapon,
|
||||
nextWeapon,
|
||||
modelStats,
|
||||
isMelee,
|
||||
className,
|
||||
forceRules,
|
||||
}) => {
|
||||
let { name, selectionName, range, type, str, ap, damage, abilities } = weapon;
|
||||
var lastWhiteSpace = type.lastIndexOf(" ");
|
||||
const attacks = type.substring(lastWhiteSpace + 1);
|
||||
type = type.substring(0, lastWhiteSpace);
|
||||
|
||||
let bs = modelStats.map((modelStat) => modelStat.bs);
|
||||
let ws = modelStats.map((modelStat) => modelStat.ws);
|
||||
const strModel = modelStats[0].str;
|
||||
let meleeAttacks = modelStats.map((modelStat) => modelStat.attacks);
|
||||
|
||||
if (name === "Krak grenades") {
|
||||
name = "Krak grenade";
|
||||
}
|
||||
|
||||
const differentProfiles =
|
||||
hasDifferentProfiles(selectionName, name) &&
|
||||
((previousWeapon &&
|
||||
hasDifferentProfiles(previousWeapon.selectionName, previousWeapon.name) &&
|
||||
selectionName === previousWeapon.selectionName) ||
|
||||
(nextWeapon &&
|
||||
hasDifferentProfiles(nextWeapon.selectionName, nextWeapon.name) &&
|
||||
selectionName === nextWeapon.selectionName));
|
||||
|
||||
if (differentProfiles && name.endsWith(" grenades")) {
|
||||
name = name.replace(" grenades", "");
|
||||
}
|
||||
if (differentProfiles && name.endsWith(" grenade")) {
|
||||
name = name.replace(" grenade", "");
|
||||
}
|
||||
if (name.startsWith("Missile launcher, ")) {
|
||||
name = name.replace("Missile launcher, ", "");
|
||||
}
|
||||
if (differentProfiles && name.includes(" - ")) {
|
||||
name = name.split(" - ")[1];
|
||||
}
|
||||
|
||||
const replaceAbilityWithType = (
|
||||
occurences,
|
||||
typeName,
|
||||
ruleTextForForceRules
|
||||
) => {
|
||||
if (!abilities) return;
|
||||
if (occurences.some((occurence) => abilities.includes(occurence))) {
|
||||
for (let i = 0; i < occurences.length; i++) {
|
||||
abilities = abilities.replaceAll(occurences[i], "");
|
||||
}
|
||||
if (type) {
|
||||
type += `, ${typeName}`;
|
||||
} else {
|
||||
type = typeName;
|
||||
}
|
||||
|
||||
if (ruleTextForForceRules) {
|
||||
forceRules.set(typeName, ruleTextForForceRules);
|
||||
}
|
||||
}
|
||||
|
||||
return abilities;
|
||||
};
|
||||
|
||||
replaceAbilityWithType(["Blast.", "Blast, ", "Blast"], "Blast");
|
||||
replaceAbilityWithType(
|
||||
[
|
||||
"Each time an attack is made with this weapon, that attack automatically hits the target.",
|
||||
"Each time an attack is made with this weapon profile, that attack automatically hits the target.",
|
||||
"This weapon automatically hits its target.",
|
||||
"This weapon automatically hits its targets.",
|
||||
"This weapon hits automatically",
|
||||
"When resolving an attack made with this weapon, do not make a hit roll: it automatically scores a hit.",
|
||||
],
|
||||
"Torrent",
|
||||
"When resolving an attack made with this weapon, do not make a hit roll: it automatically scores a hit."
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
[
|
||||
"Each time an attack is made with this weapon, you can re-roll the wound roll.",
|
||||
],
|
||||
"Twin-Linked",
|
||||
"Each time an attack is made with this weapon, you can re-roll the wound roll."
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
["This weapon can target units that are not visible to the bearer."],
|
||||
"Indirect Fire",
|
||||
"This weapon can target units that are not visible to the bearer."
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
[
|
||||
"Each time an attack is made with this weapon, the target does not receive the benefits of cover against that attack.",
|
||||
],
|
||||
"Ignores Cover",
|
||||
"Each time an attack is made with this weapon, the target does not receive the benefits of cover against that attack."
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
[
|
||||
"The bearer can only shoot with this weapon once per battle.",
|
||||
"The bearer can only shoot with this weapon once per battle",
|
||||
],
|
||||
"One Shoot",
|
||||
"The bearer can only shoot with this weapon once per battle."
|
||||
);
|
||||
|
||||
/* replaceAbilityWithType(
|
||||
[
|
||||
"Each time the bearer fights, it makes 1 additional attack with this weapon.",
|
||||
"Each time the bearer fights, it makes 1 additional attack with this weapon",
|
||||
],
|
||||
"Extra Attacks 1",
|
||||
"Each time the bearer fights, it makes 1 additional attack with this weapon."
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
[
|
||||
"Each time the bearer fights, it makes 2 additional attacks with this weapon.",
|
||||
"Each time the bearer fights, it makes 2 additional attacks with this weapon",
|
||||
],
|
||||
"Extra Attacks 2",
|
||||
"Each time the bearer fights, it makes 2 additional attacks with this weapon."
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
[
|
||||
"Each time the bearer fights, it makes 3 additional attacks with this weapon.",
|
||||
"Each time the bearer fights, it makes 3 additional attacks with this weapon",
|
||||
],
|
||||
"Extra Attacks 3",
|
||||
"Each time the bearer fights, it makes 3 additional attacks with this weapon."
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
[
|
||||
"Each time the bearer fights, it makes 4 additional attacks with this weapon.",
|
||||
"Each time the bearer fights, it makes 4 additional attacks with this weapon",
|
||||
],
|
||||
"Extra Attacks 4",
|
||||
"Each time the bearer fights, it makes 4 additional attacks with this weapon."
|
||||
); */
|
||||
|
||||
replaceAbilityWithType(["Plague Weapon.", "Plague Weapon"], "Plague Weapon");
|
||||
replaceAbilityWithType(["Sonic Weapon.", "Sonic Weapon"], "Sonic Weapon");
|
||||
replaceAbilityWithType(["Shuriken.", "Shuriken"], "Shuriken");
|
||||
replaceAbilityWithType(
|
||||
["Turret Weapon.", "Turret weapon.", "Turret Weapon", "Turret weapon"],
|
||||
"Turret Weapon"
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
["Poisoned Weapon (2+).", "Poisoned Weapon (2+)"],
|
||||
"Poisoned Weapon 2+"
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
["Poisoned Weapon (3+).", "Poisoned Weapon (3+)"],
|
||||
"Poisoned Weapon 3+"
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
["Poisoned Weapon (4+).", "Poisoned Weapon (4+)"],
|
||||
"Poisoned Weapon 4+"
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
[
|
||||
"Each time an attack made with this weapon targets an enemy within half range, that attack has a Damage characteristic of D6+2.",
|
||||
"Each time an attack made with this weapon targets a unit within half range, that attack has a Damage characteristic of D6+2.",
|
||||
"Each time an attack made with this weapon targets a unit within half range, that attack has a Damage characteristic of D6+2",
|
||||
],
|
||||
"Melta 2",
|
||||
"Each time an attack made with this weapon targets a unit within half range, that attack attack has a Damage characteristic of D6+2."
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
[
|
||||
"Each time an attack made with this weapon targets an enemy within half range, that attack has a Damage characteristic of D6+4.",
|
||||
"Each time an attack made with this weapon targets a unit within half range, that attack has a Damage characteristic of D6+4.",
|
||||
"Each time an attack made with this weapon targets a unit within half range, that attack has a Damage characteristic of D6+4",
|
||||
],
|
||||
"Melta 4",
|
||||
"Each time an attack made with this weapon targets a unit within half range, that attack attack has a Damage characteristic of D6+4."
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
[
|
||||
"Each time an attack is made with this weapon, an unmodified hit roll of 6 automatically wounds the target.",
|
||||
"Each time an attack is made with this weapon profile, an unmodified hit roll of 6 automatically wounds the target.",
|
||||
"Each unmodified hit roll of 6 made for this weapon's attacks automatically hits and results in a wound (do not make a wound roll for that attack).",
|
||||
],
|
||||
"Lethal Hits",
|
||||
"Each time an attack is made with this weapon profile, an unmodified hit roll of 6 automatically wounds the target."
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
[
|
||||
"When resolving an attack made with this weapon, an unmofidied hit roll of 6 scores 1 additional hit.",
|
||||
"When resolving an attack made with this weapon, an unmofidied hit roll of 6 scores one additional hit.",
|
||||
"Each time an attack is made with this weapon, an unmodified hit roll of 6 scores 1 additional hits.",
|
||||
"Each time an attack is made with this weapon, an unmodified hit roll of 6 scores 1 additional hit.",
|
||||
"Each time an attack is made with this weapon, an unmodified hit roll of 6 scores one additional hits.",
|
||||
"Each time an attack is made with this weapon, an unmodified hit roll of 6 scores one additional hit.",
|
||||
],
|
||||
"Sustained Hits 1",
|
||||
"When resolving an attack made with this weapon, an unmofidied hit roll of 6 scores 1 additional hit."
|
||||
);
|
||||
replaceAbilityWithType(
|
||||
[
|
||||
"When resolving an attack made with this weapon, an unmofidied hit roll of 6 scores 2 additional hit.",
|
||||
"Each time an attack is made with this weapon, an unmodified hit roll of 6 scores 2 additional hits.",
|
||||
],
|
||||
"Sustained Hits 2",
|
||||
"When resolving an attack made with this weapon, an unmofidied hit roll of 6 scores 2 additional hit."
|
||||
);
|
||||
|
||||
if (
|
||||
abilities?.includes(
|
||||
"Each time an attack is made with this weapon, subtract 1 from that attack’s hit roll."
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"Each time an attack is made with this weapon, subtract 1 from that attack's hit roll."
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"Each time an attack is made with this weapon, subtract 1 from that attack's hit roll"
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"Each time an attack is made with this weapon profile, subtract 1 from that attack's hit roll."
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"Each time an attack is made with this weapon, you must subtract 1 from the hit roll."
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"When attacking with this weapon, you must subtract 1 from the hit rolls."
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"Each time an attack roll is made with this weapon, subtract 1 from the attack's hit roll"
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"When resolving an attack made with this weapon, subtract 1 from the hit roll"
|
||||
)
|
||||
) {
|
||||
abilities = abilities
|
||||
.replaceAll(
|
||||
"Each time an attack is made with this weapon, subtract 1 from that attack’s hit roll.",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"Each time an attack is made with this weapon, subtract 1 from that attack's hit roll.",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"Each time an attack is made with this weapon, subtract 1 from that attack's hit roll",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"Each time an attack is made with this weapon profile, subtract 1 from that attack's hit roll.",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"Each time an attack is made with this weapon, you must subtract 1 from the hit roll.",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"When attacking with this weapon, you must subtract 1 from the hit rolls.",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"Each time an attack roll is made with this weapon, subtract 1 from the attack's hit roll",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"When resolving an attack made with this weapon, subtract 1 from the hit roll",
|
||||
""
|
||||
);
|
||||
ws = ws.map((ws) => `${parseInt(ws, 10) + 1}+`);
|
||||
}
|
||||
|
||||
if (
|
||||
abilities?.includes(
|
||||
"Each time an attack is made with this weapon profile, make 2 hit rolls instead of 1."
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"Each time an attack is made with this weapon, make 2 attack rolls instead of 1."
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"Each time an attack is made with this weapon, make 2 hit rolls instead of 1."
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"Each time an attack is made wit this weapon profile, make 2 hit rolls instead of 1."
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"Each time an attack is made with this profile, make 2 hit rolls instead of 1."
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"Make 2 hit rolls for each attack made with this profile, instead of 1"
|
||||
)
|
||||
) {
|
||||
abilities = abilities
|
||||
.replaceAll(
|
||||
"Each time an attack is made with this weapon profile, make 2 hit rolls instead of 1.",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"Each time an attack is made with this weapon, make 2 attack rolls instead of 1.",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"Each time an attack is made with this weapon, make 2 hit rolls instead of 1.",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"Each time an attack is made wit this weapon profile, make 2 hit rolls instead of 1.",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"Each time an attack is made with this profile, make 2 hit rolls instead of 1."
|
||||
)
|
||||
.replaceAll(
|
||||
"Make 2 hit rolls for each attack made with this profile, instead of 1",
|
||||
""
|
||||
);
|
||||
|
||||
meleeAttacks = meleeAttacks.map((meleeAttack) => meleeAttack * 2);
|
||||
}
|
||||
if (
|
||||
abilities?.includes(
|
||||
"Each time an attack is made with this weapon profile, make 3 hit rolls instead of 1."
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"Each time an attack is made with this weapon profile, made 3 hit rolls instead of 1."
|
||||
) ||
|
||||
abilities?.includes(
|
||||
"Each time an attack is made with this weapon, make 3 hit rolls instead of 1."
|
||||
)
|
||||
) {
|
||||
abilities = abilities
|
||||
.replaceAll(
|
||||
"Each time an attack is made with this weapon profile, make 3 hit rolls instead of 1.",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"Each time an attack is made with this weapon profile, made 3 hit rolls instead of 1.",
|
||||
""
|
||||
)
|
||||
.replaceAll(
|
||||
"Each time an attack is made with this weapon, make 3 hit rolls instead of 1.",
|
||||
""
|
||||
);
|
||||
|
||||
meleeAttacks = meleeAttacks.map((meleeAttack) => meleeAttack * 3);
|
||||
}
|
||||
if (meleeAttacks.every((meleeAttack) => meleeAttack === meleeAttacks[0])) {
|
||||
meleeAttacks = meleeAttacks[0];
|
||||
}
|
||||
if (bs.every((b) => b === bs[0])) {
|
||||
bs = bs[0];
|
||||
}
|
||||
if (type.includes("Torrent")) {
|
||||
bs = "N/A";
|
||||
}
|
||||
if (ws.every((w) => w === ws[0])) {
|
||||
ws = ws[0];
|
||||
}
|
||||
|
||||
name = name.replaceAll(" (Shooting)", "").replaceAll(" (Melee)", "");
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className={className}>
|
||||
<td style={{ borderTop: "none", backgroundColor: "#dfe0e2" }}>
|
||||
{differentProfiles && Arrow}
|
||||
</td>
|
||||
<td style={{ textAlign: "left" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: "0 4px",
|
||||
}}
|
||||
>
|
||||
{differentProfiles && selectionName + " - "}
|
||||
{differentProfiles
|
||||
? name.replaceAll(selectionName, "").replaceAll(", ", "")
|
||||
: name}
|
||||
{type && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: ".8em",
|
||||
fontWeight: 700,
|
||||
color: "var(--primary-color)",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
[{type}]
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>{range}</td>
|
||||
<td>
|
||||
{isMelee
|
||||
? meleeAttacks.join
|
||||
? meleeAttacks.join("|")
|
||||
: meleeAttacks
|
||||
: attacks}
|
||||
</td>
|
||||
<td>
|
||||
{isMelee
|
||||
? ws.join
|
||||
? ws.join("|")
|
||||
: ws
|
||||
: bs.join
|
||||
? bs.join("|")
|
||||
: bs}
|
||||
</td>
|
||||
<td>{calculateWeaponStrength(strModel, str)}</td>
|
||||
<td>{ap}</td>
|
||||
<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) => {
|
||||
let differentColor = false;
|
||||
for (let i = 1; i <= index; i++) {
|
||||
let { selectionName } = weapons[i];
|
||||
if (selectionName !== weapons[i - 1].selectionName) {
|
||||
differentColor = !differentColor;
|
||||
}
|
||||
}
|
||||
const classes = [];
|
||||
if (differentColor) classes.push("rowOtherColor");
|
||||
if (index === 0) classes.push("noBorderTop");
|
||||
if (
|
||||
index > 0 &&
|
||||
weapons[index].selectionName === weapons[index - 1].selectionName
|
||||
)
|
||||
classes.push("noBorderTop");
|
||||
return classes.join(" ");
|
||||
};
|
||||
|
||||
const calculateWeaponStrength = (strModel, strWeapon) => {
|
||||
if (strWeapon.includes("User")) return strModel;
|
||||
if (strWeapon.startsWith("x")) {
|
||||
return strModel * parseInt(strWeapon.replace("x", ""));
|
||||
}
|
||||
if (strWeapon.includes("+")) {
|
||||
return strModel + parseInt(strWeapon, 10);
|
||||
}
|
||||
if (!strWeapon) return strModel;
|
||||
return parseInt(strWeapon, 10);
|
||||
};
|
||||
Reference in New Issue
Block a user