feat: nominal deltas

This commit is contained in:
TrèsAbhi committed 2024-04-05 13:59:30 -05:00
1 parent cf419a9ebb
commit 1b88773e28
4 files changed
+90 -17

No files matched your search

@@ -8,7 +8,10 @@ import { Dialog, Flex, IconButton } from '@radix-ui/themes';
import { useState } from 'react'; import { useState } from 'react';
import { TankDefinition } from '../../../../core/blitzkrieg/tankDefinitions'; import { TankDefinition } from '../../../../core/blitzkrieg/tankDefinitions';
import { tankToDuelMember } from '../../../../core/blitzkrieg/tankToDuelMember'; import { tankToDuelMember } from '../../../../core/blitzkrieg/tankToDuelMember';
import mutateCompare, { CompareMember } from '../../../../stores/compare'; import {
CompareMember,
mutateCompareTemporary,
} from '../../../../stores/compare';
import { TankSearch } from '../../tankopedia/components/TankSearch'; import { TankSearch } from '../../tankopedia/components/TankSearch';
interface TankControlProps { interface TankControlProps {
@@ -26,7 +29,7 @@ export function TankControl({ index, tank, members }: TankControlProps) {
<IconButton <IconButton
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
mutateCompare((draft) => { mutateCompareTemporary((draft) => {
const item = draft.members[index]; const item = draft.members[index];
draft.members.splice(index, 1); draft.members.splice(index, 1);
draft.members.splice(index - 1, 0, item); draft.members.splice(index - 1, 0, item);
@@ -39,7 +42,7 @@ export function TankControl({ index, tank, members }: TankControlProps) {
<IconButton <IconButton
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
mutateCompare((draft) => { mutateCompareTemporary((draft) => {
draft.members.splice(index, 1); draft.members.splice(index, 1);
}); });
}} }}
@@ -67,7 +70,7 @@ export function TankControl({ index, tank, members }: TankControlProps) {
<TankSearch <TankSearch
compact compact
onSelect={(tank) => { onSelect={(tank) => {
mutateCompare((draft) => { mutateCompareTemporary((draft) => {
draft.members[index] = { draft.members[index] = {
...tankToDuelMember(tank), ...tankToDuelMember(tank),
crewSkills: {}, crewSkills: {},
@@ -84,7 +87,7 @@ export function TankControl({ index, tank, members }: TankControlProps) {
<IconButton <IconButton
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
mutateCompare((draft) => { mutateCompareTemporary((draft) => {
const item = draft.members[index]; const item = draft.members[index];
draft.members.splice(index, 1); draft.members.splice(index, 1);
draft.members.splice(index + 1, 0, item); draft.members.splice(index + 1, 0, item);
+53 -8
View File
@@ -22,7 +22,13 @@ import { tankDefinitions } from '../../../core/blitzkrieg/tankDefinitions';
import { tankIcon } from '../../../core/blitzkrieg/tankIcon'; import { tankIcon } from '../../../core/blitzkrieg/tankIcon';
import { tankToCompareMember } from '../../../core/blitzkrieg/tankToCompareMember'; import { tankToCompareMember } from '../../../core/blitzkrieg/tankToCompareMember';
import { theme } from '../../../stitches.config'; import { theme } from '../../../stitches.config';
import mutateCompare, { useCompare } from '../../../stores/compare'; import {
DeltaMode,
mutateComparePersistent,
mutateCompareTemporary,
useComparePersistent,
useCompareTemporary,
} from '../../../stores/compare';
import { TankSearch } from '../tankopedia/components/TankSearch'; import { TankSearch } from '../tankopedia/components/TankSearch';
import { TankControl } from './components/TankControl'; import { TankControl } from './components/TankControl';
@@ -32,11 +38,12 @@ export default function Page() {
const awaitedTankDefinitions = use(tankDefinitions); const awaitedTankDefinitions = use(tankDefinitions);
const awaitedSkillDefinitions = use(skillDefinitions); const awaitedSkillDefinitions = use(skillDefinitions);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const members = useCompare((state) => state.members); const members = useCompareTemporary((state) => state.members);
const [addTankDialogOpen, setAddTankDialogOpen] = useState(false); const [addTankDialogOpen, setAddTankDialogOpen] = useState(false);
const awaitedModelDefinitions = use(modelDefinitions); const awaitedModelDefinitions = use(modelDefinitions);
const awaitedEquipmentDefinitions = use(equipmentDefinitions); const awaitedEquipmentDefinitions = use(equipmentDefinitions);
const awaitedProvisionDefinitions = use(provisionDefinitions); const awaitedProvisionDefinitions = use(provisionDefinitions);
const deltaMode = useComparePersistent((state) => state.deltaMode);
const stats = useMemo( const stats = useMemo(
() => () =>
members.map((item) => members.map((item) =>
@@ -84,7 +91,7 @@ export default function Page() {
const tanksParam = searchParams.get('tanks'); const tanksParam = searchParams.get('tanks');
if (tanksParam) { if (tanksParam) {
mutateCompare((draft) => { mutateCompareTemporary((draft) => {
draft.members = tanksParam draft.members = tanksParam
.split(',') .split(',')
.map(Number) .map(Number)
@@ -104,6 +111,7 @@ export default function Page() {
display, display,
deltaType = 'higherIsBetter', deltaType = 'higherIsBetter',
decimals, decimals,
deltaNominalDisplay,
}: { }: {
name: string; name: string;
value: value:
@@ -116,6 +124,7 @@ export default function Page() {
) => number | string | undefined; ) => number | string | undefined;
deltaType?: 'higherIsBetter' | 'lowerIsBetter'; deltaType?: 'higherIsBetter' | 'lowerIsBetter';
decimals?: number; decimals?: number;
deltaNominalDisplay?: (delta: number) => string | number;
}) { }) {
const values = stats.map((stat) => const values = stats.map((stat) =>
typeof value === 'function' ? value(stat) : (stat[value] as number), typeof value === 'function' ? value(stat) : (stat[value] as number),
@@ -136,6 +145,7 @@ export default function Page() {
</Table.RowHeaderCell> </Table.RowHeaderCell>
{values.map((value, index) => { {values.map((value, index) => {
const delta = (value ?? 0) - (values[0] ?? 0);
const deltaPercentage = (value ?? 1) / (values[0] ?? 1) - 1; const deltaPercentage = (value ?? 1) / (values[0] ?? 1) - 1;
const normalizedDeltaPercentage = Math.round( const normalizedDeltaPercentage = Math.round(
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25), Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25),
@@ -163,6 +173,7 @@ export default function Page() {
<Flex <Flex
align="center" align="center"
justify="center" justify="center"
gap="2"
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
@@ -179,6 +190,24 @@ export default function Page() {
? value ? value
: value?.toFixed(decimals)} : value?.toFixed(decimals)}
</Text> </Text>
{delta !== 0 && (
<>
{deltaMode === 'nominal' && (
<Text color="gray">
(
{`${delta > 0 ? '+' : ''}${
deltaNominalDisplay
? deltaNominalDisplay(delta)
: decimals === undefined
? delta
: delta.toFixed(decimals)
}`}
)
</Text>
)}
</>
)}
</Flex> </Flex>
</Table.Cell> </Table.Cell>
); );
@@ -208,12 +237,21 @@ export default function Page() {
<Flex justify="center" gap="6" align="center"> <Flex justify="center" gap="6" align="center">
<Flex justify="center" gap="2" align="center"> <Flex justify="center" gap="2" align="center">
Delta:{' '} Delta:{' '}
<SegmentedControl.Root defaultValue="none"> <SegmentedControl.Root
<SegmentedControl.Item value="none">None</SegmentedControl.Item> value={deltaMode}
<SegmentedControl.Item value="percentage"> onValueChange={(value) => {
mutateComparePersistent((draft) => {
draft.deltaMode = value as DeltaMode;
});
}}
>
<SegmentedControl.Item value={'none' satisfies DeltaMode}>
None
</SegmentedControl.Item>
<SegmentedControl.Item value={'percentage' satisfies DeltaMode}>
Percentage Percentage
</SegmentedControl.Item> </SegmentedControl.Item>
<SegmentedControl.Item value="nominal"> <SegmentedControl.Item value={'nominal' satisfies DeltaMode}>
Nominal Nominal
</SegmentedControl.Item> </SegmentedControl.Item>
</SegmentedControl.Root> </SegmentedControl.Root>
@@ -240,7 +278,7 @@ export default function Page() {
<TankSearch <TankSearch
compact compact
onSelect={(tank) => { onSelect={(tank) => {
mutateCompare((draft) => { mutateCompareTemporary((draft) => {
draft.members.push( draft.members.push(
tankToCompareMember(tank, awaitedSkillDefinitions), tankToCompareMember(tank, awaitedSkillDefinitions),
); );
@@ -311,6 +349,7 @@ export default function Page() {
.shellReloads!.map((reload) => reload.toFixed(2)) .shellReloads!.map((reload) => reload.toFixed(2))
.join(', ') .join(', ')
} }
deltaNominalDisplay={(delta) => delta.toFixed(2)}
/> />
{hasNonRegularGun && ( {hasNonRegularGun && (
<Row <Row
@@ -401,17 +440,20 @@ export default function Page() {
value="fireChance" value="fireChance"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
display={(stats) => (stats.fireChance * 100).toFixed(0)} display={(stats) => (stats.fireChance * 100).toFixed(0)}
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/> />
<Row name="View range" value="viewRange" decimals={0} /> <Row name="View range" value="viewRange" decimals={0} />
<Row <Row
name="Camouflage still" name="Camouflage still"
value="camouflageStill" value="camouflageStill"
display={(stats) => (stats.camouflageStill * 100).toFixed(0)} display={(stats) => (stats.camouflageStill * 100).toFixed(0)}
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/> />
<Row <Row
name="Camouflage moving" name="Camouflage moving"
value="camouflageMoving" value="camouflageMoving"
display={(stats) => (stats.camouflageMoving * 100).toFixed(0)} display={(stats) => (stats.camouflageMoving * 100).toFixed(0)}
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/> />
<Row <Row
name="Camouflage shooting still" name="Camouflage shooting still"
@@ -419,6 +461,7 @@ export default function Page() {
display={(stats) => display={(stats) =>
(stats.camouflageShootingStill * 100).toFixed(0) (stats.camouflageShootingStill * 100).toFixed(0)
} }
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/> />
<Row <Row
name="Camouflage shooting moving" name="Camouflage shooting moving"
@@ -426,6 +469,7 @@ export default function Page() {
display={(stats) => display={(stats) =>
(stats.camouflageShootingMoving * 100).toFixed(0) (stats.camouflageShootingMoving * 100).toFixed(0)
} }
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/> />
<Row <Row
name="Camouflage caught on fire" name="Camouflage caught on fire"
@@ -433,6 +477,7 @@ export default function Page() {
display={(stats) => display={(stats) =>
(stats.camouflageCaughtOnFire * 100).toFixed(0) (stats.camouflageCaughtOnFire * 100).toFixed(0)
} }
deltaNominalDisplay={(delta) => (delta * 100).toFixed(0)}
/> />
<Row <Row
name="Width" name="Width"
+1
View File
@@ -0,0 +1 @@
export function EquipmentMatrixCompact() {}
+28 -4
View File
@@ -1,19 +1,43 @@
import { produce } from 'immer'; import { produce } from 'immer';
import { merge } from 'lodash';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware';
import { DuelMember } from './duel'; import { DuelMember } from './duel';
export interface CompareMember extends DuelMember { export interface CompareMember extends DuelMember {
crewSkills: Record<string, number>; crewSkills: Record<string, number>;
} }
export interface Compare { export interface CompareTemporary {
members: CompareMember[]; members: CompareMember[];
} }
export const useCompare = create<Compare>()(() => ({ export type DeltaMode = 'none' | 'percentage' | 'nominal';
export interface ComparePersistent {
deltaMode: DeltaMode;
}
export const useCompareTemporary = create<CompareTemporary>()(() => ({
members: [], members: [],
})); }));
export default function mutateCompare(recipe: (draft: Compare) => void) { export const useComparePersistent = create<ComparePersistent>()(
useCompare.setState(produce(recipe)); persist(
subscribeWithSelector<ComparePersistent>(() => ({
deltaMode: 'none',
})),
{ name: 'compare', merge: (a, b) => merge(b, a) },
),
);
export function mutateCompareTemporary(
recipe: (draft: CompareTemporary) => void,
) {
useCompareTemporary.setState(produce(recipe));
}
export function mutateComparePersistent(
recipe: (draft: ComparePersistent) => void,
) {
useComparePersistent.setState(produce(recipe));
} }