added basic support for 10th

This commit is contained in:
NilsUeter
2023-09-28 21:30:28 +02:00
parent a0693d98e2
commit 1575df51f9
6 changed files with 2360 additions and 11 deletions
+961
View File
@@ -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";
};
+530
View File
@@ -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 attacks 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 attacks 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);
};