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

Firearms support #37

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions src/components/EquipmentList/EquipmentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, { useContext } from 'react'

import CommandBar from '@/components/CommandBar/CommandBar'
import ArmorGrid from '@/components/EquipmentList/ArmorGrid'
import FirearmWeaponsGrid from '@/components/EquipmentList/FirearmWeaponsGrid/FirearmWeaponsGrid'
import MeleeWeaponsGrid from '@/components/EquipmentList/MeleeWeaponsGrid'
import MiscEquipmentGrid from '@/components/EquipmentList/MiscEquipmentGrid'
import MissileWeaponsGrid from '@/components/EquipmentList/MissileWeaponsGrid'
Expand Down Expand Up @@ -46,6 +47,11 @@ export default function EquipmentList() {
key: 'miscEquipment',
title: t`Miscellaneous`,
},
{
content: <FirearmWeaponsGrid />,
key: 'firearm',
title: t`Firearms`,
},
]}
/>
</>
Expand Down
111 changes: 111 additions & 0 deletions src/components/EquipmentList/FirearmWeaponsGrid/FirearmWeaponsGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Trans } from '@lingui/macro'
import React, { useMemo, useState } from 'react'

import DataGrid from '@/components/DataGrid/DataGrid'
import type { DataGridSortFunction } from '@/components/DataGrid/helpers'
import {
cityCostColumn,
columns,
defaultFilterValues,
ruralCostColumn,
} from '@/components/EquipmentList/FirearmWeaponsGrid/consts'
import FirearmsFilterPanel from '@/components/EquipmentList/FirearmWeaponsGrid/FirearmsFilterPanel'
import type { GridDataProcessor } from '@/components/EquipmentList/FirearmWeaponsGrid/helpers'
import {
getFilteredData,
getFirearmsCostCoefficient,
handlePistols,
} from '@/components/EquipmentList/FirearmWeaponsGrid/helpers'
import type { FilterValues } from '@/components/EquipmentList/FirearmWeaponsGrid/types'
import {
handleAddEquipmentItemClick,
sortWeapons,
} from '@/components/EquipmentList/helpers'
import type { FirearmWeaponItem } from '@/domain/weapon'
import useTailwindBreakpoint from '@/shared/hooks/useTailwindBreakpoint'
import { useInventoryState } from '@/state/InventoryState'

const gridDataProcessors: Array<GridDataProcessor> = [handlePistols]

const MissileWeaponsGrid = () => {
const {
state: { isCostRural },
} = useInventoryState()
const [filterValues, setFilterValues] =
useState<FilterValues>(defaultFilterValues)
const breakpoint = useTailwindBreakpoint()

const columnsFilteredByCost = useMemo(() => {
const costCol = isCostRural.get() ? ruralCostColumn : cityCostColumn
const lastIndex = columns.length - 1

// put Cost before the last column
return [...columns.slice(0, lastIndex), costCol, columns[lastIndex]]
}, [isCostRural])

const costCoeff = useMemo(
() => getFirearmsCostCoefficient(filterValues),
[filterValues],
)

const gridData = useMemo(() => {
const data = getFilteredData(isCostRural.get())
const { firingMechanism, riffled } = filterValues

if (costCoeff !== 1) {
for (const firearmWeaponItem of data) {
firearmWeaponItem.isRiffled = riffled
firearmWeaponItem.firingMechanism = firingMechanism

firearmWeaponItem.cityCostCp *= costCoeff
if (firearmWeaponItem.ruralCostCp) {
firearmWeaponItem.ruralCostCp *= costCoeff
}
}
}

return gridDataProcessors.reduce(
(acc, fn) => fn(acc, firingMechanism),
data,
)
}, [filterValues, isCostRural, costCoeff])

const onFilterChange = (values: FilterValues) => {
setFilterValues(values)
}

const isSmallViewport = 'xs' === breakpoint
const colSpan = isSmallViewport
? columnsFilteredByCost.length - 2
: columnsFilteredByCost.length

return (
<>
<div className='pb-4 pt-6 text-gray-800'>
<p className='mb-4'>
<Trans>
Targets at Medium range are –4 to hit, –8 to hit at Long range.
Rifled barrels halve the range penalties, but cost twice as much.
</Trans>
</p>
<div className={'mb-2'}>
<FirearmsFilterPanel onChange={onFilterChange} />
<span className='ph-color-muted text-sm'>
<Trans>Cost coefficient</Trans>: {costCoeff}
</span>
</div>
</div>

<DataGrid<FirearmWeaponItem>
data={gridData}
columns={columnsFilteredByCost}
onAddClick={handleAddEquipmentItemClick}
handleSort={sortWeapons as DataGridSortFunction}
spanDetails={colSpan}
noFilter
/>
</>
)
}

export default MissileWeaponsGrid
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Trans } from '@lingui/macro'
import React, { useEffect, useState } from 'react'

import { defaultFilterValues } from '@/components/EquipmentList/FirearmWeaponsGrid/consts'
import type { FilterValues } from '@/components/EquipmentList/FirearmWeaponsGrid/types'
import { FiringMechanism, YearPeriod } from '@/domain/firearms'

type FirearmsFilterPanelProps = {
onChange: (v: FilterValues) => void
}

const FirearmsFilterPanel = ({ onChange }: FirearmsFilterPanelProps) => {
const [formValues, setFormValues] =
useState<FilterValues>(defaultFilterValues)

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = event.target

setFormValues((prevState) => ({
...prevState,
[name]: type === 'checkbox' ? checked : value,
}))
}

useEffect(() => {
onChange(formValues)
}, [formValues, onChange])

return (
<div data-testid='FirearmsFilterPanel'>
<div
data-testid='FirearmsFilterPanel__firstLine'
className='my-4 flex flex-wrap justify-start lg:my-2'
>
<div className='w-full md:w-auto'>
<h3 className='ph-color-accent mr-4 font-semibold'>
<Trans>Firearm mechanism</Trans>
</h3>
</div>
{[
FiringMechanism.Matchlock,
FiringMechanism.Wheellock,
FiringMechanism.Flintlock,
].map((mechanism) => (
<div key={mechanism} className='mr-4 flex items-center'>
<input
id={`${mechanism}-radio`}
type='radio'
value={mechanism}
checked={formValues.firingMechanism === mechanism}
onChange={handleChange}
name='firingMechanism'
className='h-4 w-4 focus:ring-red-500'
/>
<label
htmlFor={`${mechanism}-radio`}
className='ml-2 cursor-pointer text-sm text-gray-900'
>
{mechanism}
</label>
</div>
))}
</div>
<div
data-testid='FirearmsFilterPanel__secondLine'
className='my-4 flex flex-wrap justify-start lg:my-2'
>
<div className='w-full md:w-auto'>
<h3 className='ph-color-accent mr-4 font-semibold'>
<label htmlFor='year-checkbox'>
<Trans>Year</Trans>
</label>
</h3>
</div>
{[
YearPeriod['1610-1630'],
YearPeriod['1631-1660'],
YearPeriod['> 1661'],
].map((year) => (
<div key={year} className='mr-4 flex items-center'>
<input
id={`${year}-radio`}
type='radio'
value={year}
checked={formValues.year === year}
onChange={handleChange}
name='year'
className='h-4 w-4 focus:ring-red-500'
/>
<label
htmlFor={`${year}-radio`}
className='ml-2 cursor-pointer text-sm text-gray-900'
>
{year}
</label>
</div>
))}
</div>

<div
data-testid='FirearmsFilterPanel__thirdLine'
className='my-4 flex flex-wrap justify-start lg:my-2'
>
<div className='mr-4 flex items-center'>
<h3 className='ph-color-accent font-semibold'>
<label htmlFor='riffled-checkbox'>
<Trans>Riffled</Trans>
</label>
</h3>
</div>
<div className='mr-4 flex items-center'>
<input
id='riffled-checkbox'
type='checkbox'
checked={formValues.riffled}
onChange={handleChange}
name='riffled'
className='h-4 w-4 focus:ring-red-500'
/>
</div>
</div>
</div>
)
}

export default FirearmsFilterPanel
76 changes: 76 additions & 0 deletions src/components/EquipmentList/FirearmWeaponsGrid/consts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { t } from '@lingui/macro'
import React from 'react'

import DamageFragment from '@/components/DamageFragment'
import type { DataGridColumn } from '@/components/DataGrid/types'
import {
renderCostGridCol,
renderDetailsBody,
renderWeightGridCol,
} from '@/components/EquipmentList/gridHelpers'
import RangeFragment from '@/components/RangeFragment'
import { FiringMechanism, YearPeriod } from '@/domain/firearms'
import type { FirearmWeaponItem } from '@/domain/weapon'

export const columns: ReadonlyArray<DataGridColumn<FirearmWeaponItem>> = [
{
className: 'w-1/3',
key: 'name',
shouldRenderDetails: (item) => !!item.details || !!item.range,
renderDetails: renderDetailsBody,
get title() {
return t`Name`
},
},
{
className: 'w-1/6',
key: 'damage',
render: (item: FirearmWeaponItem) => (
<DamageFragment damage={item.damage} />
),
get title() {
return t`Damage`
},
},
{
className: 'w-1/6 hidden sm:table-cell',
key: 'range',
render: (item: FirearmWeaponItem) => (
<RangeFragment range={item.range} compact />
),
get title() {
return t`Range`
},
},
{
className: 'hidden sm:table-cell sm:w-1/6',
key: 'points',
render: renderWeightGridCol,
get title() {
return t`Weight`
},
},
]

export const cityCostColumn: DataGridColumn<FirearmWeaponItem> = {
className: 'w-1/6',
key: 'cityCostCp',
render: renderCostGridCol,
get title() {
return t`Cost, sp`
},
}
export const ruralCostColumn: DataGridColumn<FirearmWeaponItem> = {
className: 'w-1/6',
key: 'ruralCostCp',
render: renderCostGridCol,
get title() {
return t`Cost, sp`
},
}

export const defaultFilterValues = {
firingMechanism: FiringMechanism.Wheellock,
riffled: false,
year: YearPeriod['1610-1630'],
}
41 changes: 41 additions & 0 deletions src/components/EquipmentList/FirearmWeaponsGrid/helpers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { t } from '@lingui/macro'

import type { FilterValues } from '@/components/EquipmentList/FirearmWeaponsGrid/types'
import Equipment from '@/config/Equipment'
import { FirearmCostCoeff, RiffledCostCoeff } from '@/config/Firearms'
import { FiringMechanism } from '@/domain/firearms'
import type { FirearmWeaponItem } from '@/domain/weapon'

export const getFilteredData = (isCostRural: boolean) => {
const data = Object.values(Equipment.FirearmWeapons)

return isCostRural ? data.filter((i) => i.ruralCostCp !== null) : data
}

export const getFirearmsCostCoefficient = (
filterValues: FilterValues,
): number => {
const { firingMechanism } = filterValues
let coefficient =
FirearmCostCoeff[firingMechanism][filterValues.year] ||
FirearmCostCoeff[firingMechanism].default

if (filterValues.riffled) {
coefficient *= RiffledCostCoeff
}

return coefficient
}

export type GridDataProcessor = (
data: Array<FirearmWeaponItem>,
firingMechanism: FiringMechanism,
) => Array<FirearmWeaponItem>

export const handlePistols: GridDataProcessor = (data, firingMechanism) => {
if (firingMechanism === FiringMechanism.Matchlock) {
return data.filter((item) => item.name !== t`Pistol`)
}

return data
}
7 changes: 7 additions & 0 deletions src/components/EquipmentList/FirearmWeaponsGrid/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { FiringMechanism, YearPeriod } from '@/domain/firearms'

export type FilterValues = {
firingMechanism: FiringMechanism
riffled: boolean
year: YearPeriod
}
Loading
Loading