Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Roster selection #611

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a32a16d
Determine if a battlescribe file is for a roster or not
g3rg Oct 12, 2021
42b60da
Display roster operatives
g3rg Oct 12, 2021
5f9136b
Merge branch 'main' into roster-selection
g3rg Oct 13, 2021
7b8bfc0
Playing with operative display
g3rg Oct 13, 2021
e851e0a
Add parse and store BattleScribe UUID for use in Roster/Kill team sel…
g3rg Oct 13, 2021
2e9b03d
Merge branch 'main' into roster-selection
g3rg Oct 13, 2021
cde7594
Move roster selection to a new component
g3rg Oct 15, 2021
f2eee71
Merge branch 'main' into roster-selection
g3rg Oct 15, 2021
a66eab1
Use table for Roster list
g3rg Oct 18, 2021
b9dfdef
Roster selection/deselection working
g3rg Oct 18, 2021
8c02119
Switched to use checkbox to take up less room, and grey out row when …
g3rg Oct 18, 2021
f7f5777
Allow hiding of roster list when printing, and add setting for it
g3rg Oct 18, 2021
be9ac48
Merge branch 'main' into roster-selection
g3rg Oct 18, 2021
b2e5f99
Use better key
g3rg Oct 19, 2021
3eb5a14
Boy scout lint fixes
g3rg Oct 19, 2021
648956e
Remove console.log
g3rg Oct 19, 2021
a0b422c
Merge branch 'main' into roster-selection
g3rg Oct 19, 2021
10c7fe5
Merge branch 'main' into roster-selection
g3rg Oct 19, 2021
b61652b
Merge branch 'main' into roster-selection
g3rg Oct 20, 2021
4389079
Merge branch 'main' into roster-selection
g3rg Oct 21, 2021
73df4b7
Add setting to turn roster selection view on / off and marked as WIP
g3rg Oct 21, 2021
66a8658
Some linting issues, though line is now super long
g3rg Oct 21, 2021
e10d8e3
Update key for datasheet, using just datacard name caused problems wh…
g3rg Oct 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Settings } from './types/Settings'
export function App (): JSX.Element {
const [roster, setRoster] = useState<Roster2018|Roster2021|null>(null)

const [settings, setSettings] = useState({ showWoundTrack: true })
const [settings, setSettings] = useState({ showWoundTrack: true, printRosterList: false })

useEffect(() => {
setSettings(loadSettingsFromLocalStorage())
Expand All @@ -31,9 +31,9 @@ export function App (): JSX.Element {

const loadSettingsFromLocalStorage = (): Settings => {
try {
return JSON.parse(localStorage.getItem('settings') ?? '{ showWoundTrack: true, touchscreenMode: false, dropboxSelector: false }')
return JSON.parse(localStorage.getItem('settings') ?? '{ showWoundTrack: true, printRosterList: false }')
} catch (e) {
return { showWoundTrack: false }
return { showWoundTrack: true, printRosterList: false }
}
}

Expand Down Expand Up @@ -76,7 +76,7 @@ export function App (): JSX.Element {
<Container fluid='lg'>
{roster === null ? <Homepage onUpload={handleUpload} settings={settings} setSettings={setAndSaveSettings} /> : <></>}
{(roster != null) && isRosterKT18(roster) ? <RosterView2018 name={roster.name} models={roster.models} onClose={handleClose} forceRules={roster.forceRules} onSelectionChanged={handleSelectionChanged} /> : <></>}
{(roster != null) && isRosterKT21(roster) ? <RosterView2021 name={roster.name} operatives={roster.operatives} psychicPowers={roster.psychicPowers} faction={roster.faction} fireteams={roster.fireteams} onClose={handleClose} showWoundTrack={settings.showWoundTrack} /> : <></>}
{(roster != null) && isRosterKT21(roster) ? <RosterView2021 name={roster.name} operatives={roster.operatives} psychicPowers={roster.psychicPowers} faction={roster.faction} fireteams={roster.fireteams} isRoster={roster.isRoster} onClose={handleClose} showWoundTrack={settings.showWoundTrack} printRosterList={settings.printRosterList} /> : <></>}
</Container>
</Route>
</Switch>
Expand Down
10 changes: 6 additions & 4 deletions src/components/Homepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ interface Props {
setSettings: (settings: Settings) => void
}

const GIT_SHA = process.env.REACT_APP_GIT_SHA ?? ''

function fileDropZone (props: Props): JSX.Element {
return (
<Dropzone onDrop={props.onUpload} accept='.ros,.rosz'>
Expand Down Expand Up @@ -50,10 +52,10 @@ function Homepage (props: Props): JSX.Element {
</ul>
</Card.Text>
<Card.Text>
<Button variant="outline-secondary" onClick={() => setShowSettings(true)}>Edit Display Settings</Button>
<Button variant='outline-secondary' onClick={() => setShowSettings(true)}>Edit Display Settings</Button>
<SettingsDialog
show={showSettings} setShowSettings={setShowSettings}
settings={props.settings} setSettings={props.setSettings}
show={showSettings} setShowSettings={setShowSettings}
settings={props.settings} setSettings={props.setSettings}
/>
</Card.Text>
</Card.Body>
Expand Down Expand Up @@ -89,7 +91,7 @@ function Homepage (props: Props): JSX.Element {
Icons courtesy of <a href='https://killteam.app/'>Companion for Kill Team</a>.
Released as <a href='https://github.com/floppy/dataslate'>Open Source</a>,
report problems <a href='https://github.com/floppy/dataslate/issues/new'>on GitHub</a>.
Version: <em><a {...{ href: `https://github.com/floppy/dataslate/tree/${process.env.REACT_APP_GIT_SHA}` }}>{process.env.REACT_APP_GIT_SHA}</a></em>
Version: <em><a {...{ href: `https://github.com/floppy/dataslate/tree/${GIT_SHA}` }}>{GIT_SHA}</a></em>
</footer>
</>
)
Expand Down
52 changes: 39 additions & 13 deletions src/components/KillTeam2021/Roster.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { MouseEvent } from 'react'
import React, { MouseEvent, useEffect, useState } from 'react'
import { Col, Card } from 'react-bootstrap'
import { CloseButton } from '../CloseButton'
import { Operative, Datacard, PsychicPower } from '../../types/KillTeam2021'
Expand All @@ -8,6 +8,7 @@ import { PowerList } from './PowerList'
import hash from 'node-object-hash'
import _ from 'lodash'
import { FactionSpecificData } from './FactionSpecificData'
import { RosterSelection } from './RosterSelection'

interface Props {
name: string
Expand All @@ -16,15 +17,18 @@ interface Props {
psychicPowers: PsychicPower[]
fireteams: string[]
onClose: (event: MouseEvent<HTMLButtonElement>) => void
isRoster?: boolean
showWoundTrack: boolean
printRosterList: boolean
}

const groupByDatacard = (operatives: Operative[]): Datacard[] => {
const groupedOperatives = _.groupBy(operatives, (o) => (hash().hash({ datacard: o.datacard, weapons: o.weapons, equipment: o.equipment })))
const groupByDatacard = (operatives: Operative[], selectedOperatives: string[]): Datacard[] => {
const filteredOperatives = operatives.filter((op) => { return selectedOperatives.includes(op.id) })
const groupedOperatives = _.groupBy(filteredOperatives, (o) => (hash().hash({ datacard: o.datacard, weapons: o.weapons, equipment: o.equipment })))
return _.map(groupedOperatives, (ops, hash) => ({
...ops[0],
name: ops[0].datacard,
operativeNames: ops.map((c) => (c.name)).sort()
operativeNames: ops.map((op) => (op.name)).sort()
}))
}

Expand All @@ -36,7 +40,20 @@ export function Roster (props: Props): JSX.Element {
width: '100%',
display: 'flex'
}
const datacards = groupByDatacard(props.operatives)

const [selectedOperatives, setSelectedOperatives] = useState(props.operatives.map((operative, index) => { return operative.id }))
const [datacards, setDataCards] = useState<Datacard[] | []>([])

const updateSelectedOperatives = (operativeIds: string[]): void => {
setSelectedOperatives(operativeIds)
setDataCards(groupByDatacard(props.operatives, selectedOperatives))
}

useEffect(() => {
setDataCards(groupByDatacard(props.operatives, selectedOperatives))
}, [props.operatives, selectedOperatives])

const rosterClassName = props.printRosterList ? '' : 'noprint'

return (
<>
Expand All @@ -48,21 +65,30 @@ export function Roster (props: Props): JSX.Element {
<CloseButton onClose={props.onClose} />
</Col>
</h1>
{_.orderBy(datacards, ['leader', 'name'], ['desc', 'asc']).map((datacard: Datacard) => (
<Datasheet datacard={datacard} showWoundTrack={props.showWoundTrack} />
{(props.isRoster ?? false) && (
<Card className={rosterClassName}>
<Card.Header style={{ ...headingStyle, breakBefore: 'always' }} as='h2'>Roster</Card.Header>
<Card.Body>
<RosterSelection operatives={props.operatives} selectedOperatives={selectedOperatives} setSelectedOperatives={updateSelectedOperatives} />
</Card.Body>
</Card>
)}
{_.orderBy(datacards, ['leader', 'name'], ['desc', 'asc']).map((datacard: Datacard, idx) => (
<Datasheet key={idx} datacard={datacard} showWoundTrack={props.showWoundTrack} />
))}
<Card>
<Card.Header style={{ ...headingStyle, breakBefore: 'always' }} as='h2'>Rules</Card.Header>
<Card.Body>
<RuleList rules={_.uniqBy(_.flatten(datacards.map((m) => (m.rules))), 'name')} />
</Card.Body>
</Card>
{props.psychicPowers.length > 0 && <Card>
<Card.Header style={{ ...headingStyle }} as='h2'>Psychic Powers</Card.Header>
<Card.Body>
<PowerList powers={props.psychicPowers} />
</Card.Body>
</Card>}
{props.psychicPowers.length > 0 &&
<Card>
<Card.Header style={{ ...headingStyle }} as='h2'>Psychic Powers</Card.Header>
<Card.Body>
<PowerList powers={props.psychicPowers} />
</Card.Body>
</Card>}

<FactionSpecificData faction={props.faction} fireteams={props.fireteams} />
</>
Expand Down
79 changes: 79 additions & 0 deletions src/components/KillTeam2021/RosterSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Operative, Weapon } from '../../types/KillTeam2021'
import { Form, Row, Table } from 'react-bootstrap'
import React from 'react'

interface Props {
operatives: Operative[]
selectedOperatives: string[]
setSelectedOperatives: (opIds: string[]) => void
}

const weaponNames = (weapons: Weapon[]): JSX.Element[] => {
return weapons.map((weapon, index) => {
return <Row key={weapon.name}>{weapon.name}</Row>
})
}

const operativeName = (operative: Operative): JSX.Element => {
// TODO: Hasn't been tested with a roster with actual names for units, I think this is only supported if you pay for battlescribe
const name = operative.name.includes(operative.datacard) ? operative.name : `${operative.name} [${operative.datacard}]`
return <span>{name}</span>
}

const flipSelection = (selectedOperatives: string[], opId: string): string[] => {
return selectionChanged(selectedOperatives, opId, !selectedOperatives.includes(opId))
}

const selectionChanged = (selectedOperatives: string[], opId: string, selected: boolean): string[] => {
let selectedOps = []
if (selected) {
selectedOps = [
opId,
...selectedOperatives
]
} else {
const idx = selectedOperatives.indexOf(opId)
if (idx > -1) {
selectedOperatives.splice(idx, 1)
}
selectedOps = selectedOperatives
}
return selectedOps
}

export function RosterSelection (props: Props) {
return (
<Table>
<thead>
<tr>
<th>Operative</th>
<th>Weapons</th>
<th />
</tr>
</thead>
<tbody>
{props.operatives.map((op) => {
const selected = props.selectedOperatives.includes(op.id)
const className = selected ? '' : 'unselected'
return (
<tr
key={op.id} onClick={(event) => { props.setSelectedOperatives(flipSelection(props.selectedOperatives, op.id)) }}
className={className}
>
<td>{operativeName(op)}</td>
<td>{weaponNames(op.weapons)}</td>
<td>
<Form.Check
type='checkbox'
id='operative'
label=''
checked={selected}
/>
</td>
</tr>
)
})}
</tbody>
</Table>
)
}
2 changes: 1 addition & 1 deletion src/components/KillTeam2021/TacOpsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function TacOpsList (props: Props): JSX.Element {
return (
<Row xs={1} sm={2} md={3} className='g-4'>
{props.tacOps.map((x: TacOp) => (
<Col>
<Col key={x.id}>
<Card border='info' bg='light'>
<Card.Header style={{ background: '#17a2b8', color: 'white', display: 'flex', justifyContent: 'space-between' }} as='h4'>
<span>Tac Op {x.id}</span>
Expand Down
19 changes: 18 additions & 1 deletion src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@ function SettingsDialog (props: Props): JSX.Element {
const showWoundTrack: boolean = target.checked
const newSettings = {
...props.settings,
showWoundTrack: showWoundTrack
showWoundTrack
}
props.setSettings(newSettings)
}

const handlePrintRosterChange = (event: any): void => {
const target = event.target
const printRosterList: boolean = target.checked
const newSettings = {
...props.settings,
printRosterList
}
props.setSettings(newSettings)
}
Expand Down Expand Up @@ -43,6 +53,13 @@ function SettingsDialog (props: Props): JSX.Element {
onChange={handleWoundTrackChange}
checked={props.settings.showWoundTrack}
/>
<Form.Check
type='checkbox'
id='printRosterList'
label='Print Roster List'
onChange={handlePrintRosterChange}
checked={props.settings.printRosterList}
/>
</Form>
</Row>
</Col>
Expand Down
10 changes: 10 additions & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ h1, h2, h3, h4, h5 {
}
}

.unselected {
opacity: 0.25;
}

.noprint {
@media print {
display: none;
}
}

.card {
margin-bottom: 1em;
break-inside: avoid;
Expand Down
7 changes: 6 additions & 1 deletion src/parsers/KillTeam2021/BattlescribeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const parseOperative = (model: Element): Operative => {
const faction = _.intersection(allKeywords, factionKeywords).pop() || allKeywords.find((k) => (k === k.toUpperCase())) || null
const keywords = _.remove(allKeywords, (x) => (x !== faction))
const details = {
id: xpSelect('string(@id)', model, true).toString(),
datacard: xpSelect('string(@name)', model, true).toString(),
name: xpSelect('string(@customName)', model, true).toString(),
stats: {
Expand Down Expand Up @@ -126,6 +127,8 @@ export const parseBattlescribeXML = (doc: Document): Roster => {
const operatives = []
const name = xpSelect('string(/bs:roster/@name)', doc, true).toString()
const faction = xpSelect('string(//bs:force/@catalogueName)', doc, true).toString()
const isRoster = xpSelect('string(//bs:force/@name)', doc, true).toString() === 'Roster'

for (const model of xpSelect('//bs:selection[@type=\'model\']', doc) as Element[]) {
operatives.push(parseOperative(model))
}
Expand All @@ -149,12 +152,14 @@ export const parseBattlescribeXML = (doc: Document): Roster => {
o.name = o.datacard + ' ' + romanNumerals[counts[o.datacard]++]
}
}
console.log(`Is Roster? ${isRoster}`)
g3rg marked this conversation as resolved.
Show resolved Hide resolved
return {
system: 'KillTeam2021',
name,
faction,
operatives,
psychicPowers,
fireteams
fireteams,
isRoster
}
}
2 changes: 2 additions & 0 deletions src/types/KillTeam2021.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface Equipment {
}

export interface Operative {
id: string
datacard: string
name: string
stats: Stats
Expand Down Expand Up @@ -89,6 +90,7 @@ export interface Roster {
operatives: Operative[]
psychicPowers: PsychicPower[]
fireteams: string[]
isRoster?: boolean
}

export enum Archetype {
Expand Down
1 change: 1 addition & 0 deletions src/types/Settings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export interface Settings {
showWoundTrack: boolean
printRosterList: boolean
}