From 1575df51f98bd49d9ff627a06fcc73ab04c4c4dc Mon Sep 17 00:00:00 2001 From: NilsUeter Date: Thu, 28 Sep 2023 21:30:28 +0200 Subject: [PATCH] added basic support for 10th --- src/10th/Roster.jsx | 952 +++++++++++++++++++++++++++++ src/10th/Weapons.jsx | 200 +++++++ src/{ => 9th}/Roster.jsx | 8 +- src/{ => 9th}/Weapons.jsx | 6 +- src/App.jsx | 23 +- src/roster40k-10th.js | 1182 +++++++++++++++++++++++++++++++++++++ 6 files changed, 2360 insertions(+), 11 deletions(-) create mode 100644 src/10th/Roster.jsx create mode 100644 src/10th/Weapons.jsx rename src/{ => 9th}/Roster.jsx (98%) rename src/{ => 9th}/Weapons.jsx (99%) create mode 100644 src/roster40k-10th.js diff --git a/src/10th/Roster.jsx b/src/10th/Roster.jsx new file mode 100644 index 0000000..9a2151e --- /dev/null +++ b/src/10th/Roster.jsx @@ -0,0 +1,952 @@ +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 ( +
+
+ {name} [{cost.commandPoints} CP, {cost.points} pts] +
+ {forces.map((force, index) => ( + + ))} +
+ ); +}; + +const Force = ({ force, onePerPage }) => { + const { units, factionRules, rules, catalog } = force; + var mergedRules = new Map([...factionRules, ...rules]); + return ( +
+ {units.map((unit, index) => ( + + ))} + +
+ ); +}; + +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)); + + 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 ( +
+
+ +
+
+
+
+
+
+
+ {wavyLine} +
+
+
+
+ {name} + + {cost.points}pts + +
+
+ {modelStats.map((model, index) => ( + 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) + } + /> + ))} + + +
+
+
+ {image && ( + + )} + +
+
+
+
+ + + + +
+
+ + + {weaponDescriptions.map((weapon, index) => ( + + + + + ))} + {modelsWithDifferentProfiles.length > 0 && + !weaponDescriptions.length && ( + + + + + )} + +
{Arrow} + {weapon.name} - {weapon.abilities} +
{Arrow} + Before selecting targets for this weapon, select one of + its profiles to make attacks with. +
+ + +
+
+
+ ABILITIES +
+ + + + + +
+
+
+ ); +}; + +const ModelStats = ({ modelStat, index, showName, showWeapons, modelList }) => { + let { move, toughness, save, wounds, leadership, name, oc, 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 ( +
+ + + + + + +
+ {showName && ( +
+ {name} +
+ )} + {showWeapons && ( +
+ {modelListMatches.map((model, index) => ( +
{model}
+ ))} +
+ )} +
+
+ ); +}; + +const FactionIcon = ({ catalog }) => { + return ( +
+
+
+ ); +}; + +const Keywords = ({ keywords }) => { + return ( +
+ KEYWORDS: + + {[...keywords].join(", ")} + +
+ ); +}; + +const Factions = ({ factions }) => { + return ( +
+ + FACTION KEYWORDS: + + + {[...factions].join(", ")} + +
+ ); +}; + +const Rules = ({ rules }) => { + if (!rules || rules.size === 0) return null; + return ( +
+ RULES: + + {[...rules.keys()].map((rule) => rule).join(", ")} + +
+ ); +}; + +const Abilities = ({ abilities }) => { + if (!abilities) return null; + let keys = [...abilities.keys()]; + keys = keys.filter( + (key) => key !== "Stratagem: Warlord Trait" && key !== "Stratagem: Relic" + ); + return ( +
+ {abilities && + keys.map((ability) => ( +
+ {ability}:{" "} + {abilities.get(ability)} +
+ ))} +
+ ); +}; + +const Characteristic = ({ title, characteristic, index }) => { + return ( +
+ {index === 0 && ( +
{title}
+ )} + {characteristic} +
+ ); +}; + +const FancyBox = ({ children }) => { + return ( +
+
+ {children} +
+
+ ); +}; + +const Psykers = ({ title, psykers }) => { + return ( + + {psykers.length > 0 && ( + + + + + + + + + )} + + {psykers.map((psyker, index) => ( + + ))} + {psykers.length > 0 && ( + + + + )} + +
PSYKERCASTDENY + POWERS KNOWN +
+ ); +}; + +const Psyker = ({ psyker, index }) => { + let { name, cast, deny, powers } = psyker; + + return ( + + + {cast} + {deny} + + {powers} + + + ); +}; + +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 ( +
+
+ + + + {[...uniqueWoundTracker[0].table.keys()].map((key) => ( + + ))} + + + + {uniqueWoundTracker.map((woundTrack, index) => ( + + {[...woundTrack.table.values()].map((entry) => ( + + ))} + + ))} + +
{key}
+ {entry} +
+
+
+ ); +}; + +const Spells = ({ title, spells }) => { + return ( + + {spells.length > 0 && ( + + + + + + + + + )} + + {spells.map((spell, index) => ( + + ))} + {spells.length > 0 && ( + + + + + )} + +
+
+ +
+
{title}RANGEWARP CHARGE
+ ); +}; + +const Spell = ({ spell, index, className }) => { + let { name, range, manifest, details } = spell; + return ( + <> + + + +
+ {name} +
+ + {range} + {manifest} + + + + + {details} + + + + ); +}; + +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 ( +
+ +
+ {[...rules.keys()].map((rule) => ( +
+ {rule}: {rules.get(rule)} +
+ ))} +
+
+ ); +}; + +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"; +}; diff --git a/src/10th/Weapons.jsx b/src/10th/Weapons.jsx new file mode 100644 index 0000000..3f18604 --- /dev/null +++ b/src/10th/Weapons.jsx @@ -0,0 +1,200 @@ +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 && ( + + + +
+ +
+ + {title} + RANGE + A + {isMelee ? "WS" : "BS"} + S + AP + D + + + )} + + {weapons.map((weapon, index) => ( + + ))} + {weapons.length > 0 && ( + + + + + )} + + + ); +}; + +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, + bs, + ws, + attacks, + } = weapon; + + 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]; + } + + return ( + <> + + + {differentProfiles && Arrow} + + +
+ {differentProfiles && selectionName + " - "} + {differentProfiles + ? name.replaceAll(selectionName, "").replaceAll(", ", "") + : name} + {type && type !== "-" && ( + + [{type}] + + )} +
+ + {range} + {attacks} + + {isMelee + ? ws.join + ? ws.join("|") + : ws + : bs.join + ? bs.join("|") + : bs} + + {str} + {ap} + {damage} + + {abilities && abilities !== "-" && ( + + + + {abilities} + + + )} + + ); +}; + +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(" "); +}; diff --git a/src/Roster.jsx b/src/9th/Roster.jsx similarity index 98% rename from src/Roster.jsx rename to src/9th/Roster.jsx index e8c79ab..68ca06c 100644 --- a/src/Roster.jsx +++ b/src/9th/Roster.jsx @@ -1,8 +1,8 @@ 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 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 }) => { diff --git a/src/Weapons.jsx b/src/9th/Weapons.jsx similarity index 99% rename from src/Weapons.jsx rename to src/9th/Weapons.jsx index 9a66702..b0a399a 100644 --- a/src/Weapons.jsx +++ b/src/9th/Weapons.jsx @@ -1,6 +1,6 @@ -import meleeIcon from "./assets/meleeIcon.png"; -import rangedIcon from "./assets/rangedIcon.png"; -import { Arrow } from "./assets/icons"; +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"; diff --git a/src/App.jsx b/src/App.jsx index b20cd17..7dd909b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,13 +1,17 @@ import JSZip from "jszip"; import { Create40kRoster } from "./roster40k"; +import { Create40kRoster10th } from "./roster40k-10th"; import { useEffect, useState, useRef } from "react"; -import { Roster } from "./Roster"; + import Demo0 from "./assets/Demo0.png"; import Demo1 from "./assets/Demo1.png"; +import { Roster } from "./9th/Roster"; +import { Roster as Roster10th } from "./10th/Roster"; function App() { const [error, setError] = useState(); const [roster, setRoster] = useState(); + const [edition, setEdition] = useState(9); // [9, 10] const [onePerPage, setOnePerPage] = useState(false); const [primaryColor, setPrimaryColor] = useState("#536766"); const uploadRef = useRef(); @@ -73,13 +77,21 @@ function App() { if (rosterName) { document.title = `FancyScribe ${rosterName}`; } - + console.log(doc); if (gameType == "Warhammer 40,000 9th Edition") { - console.log(doc); const roster = Create40kRoster(doc); console.log(roster); if (roster && roster.forces.length > 0) { setRoster(roster); + setEdition(9); + setError(""); + } + } else if (gameType == "Warhammer 40,000 10th Edition") { + const roster = Create40kRoster10th(doc); + console.log(roster); + if (roster && roster.forces.length > 0) { + setRoster(roster); + setEdition(10); setError(""); } } else { @@ -214,7 +226,10 @@ function App() {
{error}
- + {edition === 9 && } + {edition === 10 && ( + + )}