mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-30 05:59:51 +01:00
feat: consumables
This commit is contained in:
1 parent
db1e4af20c
commit
94558393d9
4 files changed
+247
-94
No files matched your search
+121
-17
@@ -4,6 +4,7 @@ import {
|
|||||||
CaretLeftIcon,
|
CaretLeftIcon,
|
||||||
CaretRightIcon,
|
CaretRightIcon,
|
||||||
CaretSortIcon,
|
CaretSortIcon,
|
||||||
|
ComponentPlaceholderIcon,
|
||||||
GroupIcon,
|
GroupIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
@@ -22,11 +23,14 @@ import {
|
|||||||
import { times } from 'lodash';
|
import { times } from 'lodash';
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { use, useCallback, useEffect, useMemo, useState } from 'react';
|
import { use, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { ConsumablesManager } from '../../../components/ConsumablesManager';
|
||||||
import { EquipmentManager } from '../../../components/EquipmentManager';
|
import { EquipmentManager } from '../../../components/EquipmentManager';
|
||||||
import PageWrapper from '../../../components/PageWrapper';
|
import PageWrapper from '../../../components/PageWrapper';
|
||||||
import { ProvisionsManager } from '../../../components/ProvisionsManager';
|
import { ProvisionsManager } from '../../../components/ProvisionsManager';
|
||||||
import { asset } from '../../../core/blitzkrieg/asset';
|
import { asset } from '../../../core/blitzkrieg/asset';
|
||||||
import { availableProvisions } from '../../../core/blitzkrieg/availableProvisions';
|
import { availableProvisions } from '../../../core/blitzkrieg/availableProvisions';
|
||||||
|
import { checkConsumableProvisionInclusivity } from '../../../core/blitzkrieg/checkConsumableProvisionInclusivity';
|
||||||
|
import { consumableDefinitions } from '../../../core/blitzkrieg/consumableDefinitions';
|
||||||
import { equipmentDefinitions } from '../../../core/blitzkrieg/equipmentDefinitions';
|
import { equipmentDefinitions } from '../../../core/blitzkrieg/equipmentDefinitions';
|
||||||
import { modelDefinitions } from '../../../core/blitzkrieg/modelDefinitions';
|
import { modelDefinitions } from '../../../core/blitzkrieg/modelDefinitions';
|
||||||
import { provisionDefinitions } from '../../../core/blitzkrieg/provisionDefinitions';
|
import { provisionDefinitions } from '../../../core/blitzkrieg/provisionDefinitions';
|
||||||
@@ -52,6 +56,7 @@ export default function Page() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const awaitedTankDefinitions = use(tankDefinitions);
|
const awaitedTankDefinitions = use(tankDefinitions);
|
||||||
const awaitedSkillDefinitions = use(skillDefinitions);
|
const awaitedSkillDefinitions = use(skillDefinitions);
|
||||||
|
const awaitedConsumableDefinitions = use(consumableDefinitions);
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const members = useCompareTemporary((state) => state.members);
|
const members = useCompareTemporary((state) => state.members);
|
||||||
const [addTankDialogOpen, setAddTankDialogOpen] = useState(false);
|
const [addTankDialogOpen, setAddTankDialogOpen] = useState(false);
|
||||||
@@ -442,7 +447,17 @@ export default function Page() {
|
|||||||
<Table.Cell />
|
<Table.Cell />
|
||||||
|
|
||||||
{members.map(
|
{members.map(
|
||||||
({ equipmentMatrix, tank, key, provisions, gun }, index) => {
|
(
|
||||||
|
{
|
||||||
|
equipmentMatrix,
|
||||||
|
tank,
|
||||||
|
key,
|
||||||
|
provisions,
|
||||||
|
gun,
|
||||||
|
consumables,
|
||||||
|
},
|
||||||
|
index,
|
||||||
|
) => {
|
||||||
const equipmentPreset =
|
const equipmentPreset =
|
||||||
awaitedEquipmentDefinitions.presets[tank.equipment];
|
awaitedEquipmentDefinitions.presets[tank.equipment];
|
||||||
const provisionsList = availableProvisions(
|
const provisionsList = availableProvisions(
|
||||||
@@ -450,6 +465,15 @@ export default function Page() {
|
|||||||
gun,
|
gun,
|
||||||
awaitedProvisionDefinitions,
|
awaitedProvisionDefinitions,
|
||||||
);
|
);
|
||||||
|
const consumablesList = Object.values(
|
||||||
|
awaitedConsumableDefinitions,
|
||||||
|
).filter((consumable) =>
|
||||||
|
checkConsumableProvisionInclusivity(
|
||||||
|
consumable,
|
||||||
|
tank,
|
||||||
|
gun,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Cell key={key}>
|
<Table.Cell key={key}>
|
||||||
@@ -462,6 +486,84 @@ export default function Page() {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
radius="large"
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
width: 12,
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex direction="column" justify="center">
|
||||||
|
{provisions.map((provision, index) => (
|
||||||
|
<img
|
||||||
|
key={provision}
|
||||||
|
src={asset(
|
||||||
|
`/icons/provisions/${provision}.webp`,
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
left: '50%',
|
||||||
|
/**
|
||||||
|
* max = 100% - 16px - 4px
|
||||||
|
* min = 4px
|
||||||
|
* diff = max - min
|
||||||
|
* = 100% - 16px - 4px - 4px
|
||||||
|
* = 100% - 24px
|
||||||
|
* coefficient = index / length - 1
|
||||||
|
* position = min + coefficient * diff
|
||||||
|
*/
|
||||||
|
top: `calc(4px + ${
|
||||||
|
provisions.length === 1
|
||||||
|
? 0.5
|
||||||
|
: index / (provisions.length - 1)
|
||||||
|
} * (100% - 24px))`,
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
position: 'absolute',
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
objectFit: 'contain',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{provisions.length === 0 && <GroupIcon />}
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
</Popover.Trigger>
|
||||||
|
|
||||||
|
<Popover.Content>
|
||||||
|
<ProvisionsManager
|
||||||
|
provisions={provisionsList.map(
|
||||||
|
(provision) => provision.id,
|
||||||
|
)}
|
||||||
|
selected={provisions}
|
||||||
|
disabled={tank.provisions === provisions.length}
|
||||||
|
onChange={(provisions) => {
|
||||||
|
mutateCompareTemporary((draft) => {
|
||||||
|
draft.members[index].provisions =
|
||||||
|
provisions;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Flex justify="end" mt="4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
mutateCompareTemporary((draft) => {
|
||||||
|
draft.members[index].provisions = [];
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
|
||||||
<Popover.Root>
|
<Popover.Root>
|
||||||
<Popover.Trigger>
|
<Popover.Trigger>
|
||||||
<Button variant="ghost" radius="large">
|
<Button variant="ghost" radius="large">
|
||||||
@@ -575,11 +677,11 @@ export default function Page() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex direction="column" justify="center">
|
<Flex direction="column" justify="center">
|
||||||
{provisions.map((provision, index) => (
|
{consumables.map((consumable, index) => (
|
||||||
<img
|
<img
|
||||||
key={provision}
|
key={consumable}
|
||||||
src={asset(
|
src={asset(
|
||||||
`/icons/provisions/${provision}.webp`,
|
`/icons/consumables/${consumable}.webp`,
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
left: '50%',
|
left: '50%',
|
||||||
@@ -593,9 +695,9 @@ export default function Page() {
|
|||||||
* position = min + coefficient * diff
|
* position = min + coefficient * diff
|
||||||
*/
|
*/
|
||||||
top: `calc(4px + ${
|
top: `calc(4px + ${
|
||||||
provisions.length === 1
|
consumables.length === 1
|
||||||
? 0.5
|
? 0.5
|
||||||
: index / (provisions.length - 1)
|
: index / (consumables.length - 1)
|
||||||
} * (100% - 24px))`,
|
} * (100% - 24px))`,
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -606,24 +708,26 @@ export default function Page() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{provisions.length === 0 && <GroupIcon />}
|
{consumables.length === 0 && (
|
||||||
|
<ComponentPlaceholderIcon />
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Button>
|
</Button>
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
|
|
||||||
<Popover.Content>
|
<Popover.Content>
|
||||||
<ProvisionsManager
|
<ConsumablesManager
|
||||||
provisions={provisionsList.map(
|
consumables={consumablesList}
|
||||||
(provision) => provision.id,
|
selected={consumables}
|
||||||
)}
|
onChange={(consumables) => {
|
||||||
selected={provisions}
|
|
||||||
disabled={tank.provisions === provisions.length}
|
|
||||||
onChange={(provisions) => {
|
|
||||||
mutateCompareTemporary((draft) => {
|
mutateCompareTemporary((draft) => {
|
||||||
draft.members[index].provisions =
|
draft.members[index].consumables =
|
||||||
provisions;
|
consumables;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
disabled={
|
||||||
|
tank.consumables === consumables.length
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Flex justify="end" mt="4">
|
<Flex justify="end" mt="4">
|
||||||
@@ -631,7 +735,7 @@ export default function Page() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareTemporary((draft) => {
|
mutateCompareTemporary((draft) => {
|
||||||
draft.members[index].provisions = [];
|
draft.members[index].consumables = [];
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+15
-43
@@ -8,7 +8,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from '@radix-ui/themes';
|
} from '@radix-ui/themes';
|
||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
import { ConsumableButton } from '../../../../../../../components/ModuleButtons/ConsumableButton';
|
import { ConsumablesManager } from '../../../../../../../components/ConsumablesManager';
|
||||||
import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableProvisionInclusivity';
|
import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableProvisionInclusivity';
|
||||||
import { consumableDefinitions } from '../../../../../../../core/blitzkrieg/consumableDefinitions';
|
import { consumableDefinitions } from '../../../../../../../core/blitzkrieg/consumableDefinitions';
|
||||||
import { useEquipment } from '../../../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../../../hooks/useEquipment';
|
||||||
@@ -68,48 +68,20 @@ export function Consumables() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex wrap="wrap">
|
<ConsumablesManager
|
||||||
{consumablesList.map((consumable, index) => {
|
consumables={consumablesList}
|
||||||
const selected = consumables.includes(consumable.id);
|
selected={consumables}
|
||||||
|
cooldownBooster={cooldownBooster}
|
||||||
return (
|
disabled={protagonist.tank.consumables === consumables.length}
|
||||||
<ConsumableButton
|
hasConsumableDeliverySystem={hasConsumableDeliverySystem}
|
||||||
special={consumable.duration === undefined}
|
hasHighEndConsumables={hasHighEndConsumables}
|
||||||
duration={
|
timers
|
||||||
consumable.duration === undefined
|
onChange={(consumables) => {
|
||||||
? undefined
|
mutateDuel((draft) => {
|
||||||
: consumable.duration * (hasHighEndConsumables ? 0.7 : 1)
|
draft.protagonist!.consumables = consumables;
|
||||||
}
|
});
|
||||||
cooldown={
|
}}
|
||||||
consumable.cooldown *
|
/>
|
||||||
(hasConsumableDeliverySystem ? 0.85 : 1) *
|
|
||||||
(1 - cooldownBooster * 0.1)
|
|
||||||
}
|
|
||||||
key={consumable.id}
|
|
||||||
first={index === 0}
|
|
||||||
last={index === consumablesList.length - 1}
|
|
||||||
rowChild
|
|
||||||
disabled={
|
|
||||||
protagonist.tank.consumables === consumables.length && !selected
|
|
||||||
}
|
|
||||||
consumable={consumable.id}
|
|
||||||
selected={selected}
|
|
||||||
onClick={() => {
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
if (selected) {
|
|
||||||
draft.protagonist!.consumables =
|
|
||||||
draft.protagonist!.consumables.filter(
|
|
||||||
(id) => id !== consumable.id,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
draft.protagonist!.consumables.push(consumable.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</ConfigurationChildWrapper>
|
</ConfigurationChildWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Flex } from '@radix-ui/themes';
|
||||||
|
import { ConsumableEntry } from '../core/blitzkrieg/consumableDefinitions';
|
||||||
|
import { ConsumableButton } from './ModuleButtons/ConsumableButton';
|
||||||
|
|
||||||
|
interface ConsumablesManagerProps {
|
||||||
|
consumables: ConsumableEntry[];
|
||||||
|
selected: number[];
|
||||||
|
onChange?: (consumables: number[]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
timers?: boolean;
|
||||||
|
hasHighEndConsumables?: boolean;
|
||||||
|
hasConsumableDeliverySystem?: boolean;
|
||||||
|
cooldownBooster?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConsumablesManager({
|
||||||
|
consumables,
|
||||||
|
selected,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
timers,
|
||||||
|
hasConsumableDeliverySystem,
|
||||||
|
cooldownBooster,
|
||||||
|
hasHighEndConsumables,
|
||||||
|
}: ConsumablesManagerProps) {
|
||||||
|
return (
|
||||||
|
<Flex wrap="wrap">
|
||||||
|
{consumables.map((consumable, index) => {
|
||||||
|
const isSelected = selected.some((id) => id === consumable.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConsumableButton
|
||||||
|
special={consumable.duration === undefined}
|
||||||
|
duration={
|
||||||
|
timers
|
||||||
|
? consumable.duration === undefined
|
||||||
|
? undefined
|
||||||
|
: consumable.duration * (hasHighEndConsumables ? 0.7 : 1)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
cooldown={
|
||||||
|
timers
|
||||||
|
? consumable.cooldown *
|
||||||
|
(hasConsumableDeliverySystem ? 0.85 : 1) *
|
||||||
|
(1 - cooldownBooster! * 0.1)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
key={consumable.id}
|
||||||
|
first={index === 0}
|
||||||
|
last={index === consumables.length - 1}
|
||||||
|
rowChild
|
||||||
|
disabled={disabled && !isSelected}
|
||||||
|
consumable={consumable.id}
|
||||||
|
selected={isSelected}
|
||||||
|
onClick={() => {
|
||||||
|
if (!onChange) return;
|
||||||
|
|
||||||
|
const draft = [...selected];
|
||||||
|
|
||||||
|
if (draft.some((id) => id === consumable.id)) {
|
||||||
|
draft.splice(draft.indexOf(consumable.id), 1);
|
||||||
|
} else {
|
||||||
|
draft.push(consumable.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(draft);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { TankComponentButtonProps } from './TankComponentButton';
|
|||||||
interface ConsumableButtonProps extends TankComponentButtonProps {
|
interface ConsumableButtonProps extends TankComponentButtonProps {
|
||||||
consumable: number;
|
consumable: number;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
cooldown: number;
|
cooldown?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConsumableButton({
|
export function ConsumableButton({
|
||||||
@@ -17,7 +17,7 @@ export function ConsumableButton({
|
|||||||
duration,
|
duration,
|
||||||
...props
|
...props
|
||||||
}: ConsumableButtonProps) {
|
}: ConsumableButtonProps) {
|
||||||
const cooldownDelta = useDelta(cooldown);
|
const cooldownDelta = useDelta(cooldown ?? 0);
|
||||||
const durationDelta = useDelta(duration ?? 0);
|
const durationDelta = useDelta(duration ?? 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -25,38 +25,42 @@ export function ConsumableButton({
|
|||||||
icon={asset(`icons/consumables/${consumable}.webp`)}
|
icon={asset(`icons/consumables/${consumable}.webp`)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Flex direction="column" style={{ transform: 'translateX(-10px)' }}>
|
{(cooldown || duration) && (
|
||||||
{duration && (
|
<Flex direction="column" style={{ transform: 'translateX(-10px)' }}>
|
||||||
<Text
|
{duration && (
|
||||||
color={
|
<Text
|
||||||
durationDelta < 0
|
color={
|
||||||
? 'green'
|
durationDelta < 0
|
||||||
: durationDelta > 0
|
? 'green'
|
||||||
? 'tomato'
|
: durationDelta > 0
|
||||||
: undefined
|
? 'tomato'
|
||||||
}
|
: undefined
|
||||||
>
|
}
|
||||||
<Flex align="center" gap="1" style={{ marginBottom: -4 }}>
|
>
|
||||||
<ClockIcon width={12} height={12} />
|
<Flex align="center" gap="1" style={{ marginBottom: -4 }}>
|
||||||
{Math.round(duration)}s
|
<ClockIcon width={12} height={12} />
|
||||||
</Flex>
|
{Math.round(duration)}s
|
||||||
</Text>
|
</Flex>
|
||||||
)}
|
</Text>
|
||||||
<Text
|
)}
|
||||||
color={
|
{cooldown && (
|
||||||
cooldownDelta < 0
|
<Text
|
||||||
? 'green'
|
color={
|
||||||
: cooldownDelta > 0
|
cooldownDelta < 0
|
||||||
? 'tomato'
|
? 'green'
|
||||||
: undefined
|
: cooldownDelta > 0
|
||||||
}
|
? 'tomato'
|
||||||
>
|
: undefined
|
||||||
<Flex align="center" gap="1">
|
}
|
||||||
<ReloadIcon width={12} height={12} />
|
>
|
||||||
{Math.round(cooldown)}s
|
<Flex align="center" gap="1">
|
||||||
</Flex>
|
<ReloadIcon width={12} height={12} />
|
||||||
</Text>
|
{Math.round(cooldown)}s
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
</GenericTankComponentButton>
|
</GenericTankComponentButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user