mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02: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,
|
||||
CaretRightIcon,
|
||||
CaretSortIcon,
|
||||
ComponentPlaceholderIcon,
|
||||
GroupIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
@@ -22,11 +23,14 @@ import {
|
||||
import { times } from 'lodash';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { use, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ConsumablesManager } from '../../../components/ConsumablesManager';
|
||||
import { EquipmentManager } from '../../../components/EquipmentManager';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { ProvisionsManager } from '../../../components/ProvisionsManager';
|
||||
import { asset } from '../../../core/blitzkrieg/asset';
|
||||
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 { modelDefinitions } from '../../../core/blitzkrieg/modelDefinitions';
|
||||
import { provisionDefinitions } from '../../../core/blitzkrieg/provisionDefinitions';
|
||||
@@ -52,6 +56,7 @@ export default function Page() {
|
||||
const router = useRouter();
|
||||
const awaitedTankDefinitions = use(tankDefinitions);
|
||||
const awaitedSkillDefinitions = use(skillDefinitions);
|
||||
const awaitedConsumableDefinitions = use(consumableDefinitions);
|
||||
const searchParams = useSearchParams();
|
||||
const members = useCompareTemporary((state) => state.members);
|
||||
const [addTankDialogOpen, setAddTankDialogOpen] = useState(false);
|
||||
@@ -442,7 +447,17 @@ export default function Page() {
|
||||
<Table.Cell />
|
||||
|
||||
{members.map(
|
||||
({ equipmentMatrix, tank, key, provisions, gun }, index) => {
|
||||
(
|
||||
{
|
||||
equipmentMatrix,
|
||||
tank,
|
||||
key,
|
||||
provisions,
|
||||
gun,
|
||||
consumables,
|
||||
},
|
||||
index,
|
||||
) => {
|
||||
const equipmentPreset =
|
||||
awaitedEquipmentDefinitions.presets[tank.equipment];
|
||||
const provisionsList = availableProvisions(
|
||||
@@ -450,6 +465,15 @@ export default function Page() {
|
||||
gun,
|
||||
awaitedProvisionDefinitions,
|
||||
);
|
||||
const consumablesList = Object.values(
|
||||
awaitedConsumableDefinitions,
|
||||
).filter((consumable) =>
|
||||
checkConsumableProvisionInclusivity(
|
||||
consumable,
|
||||
tank,
|
||||
gun,
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<Table.Cell key={key}>
|
||||
@@ -462,6 +486,84 @@ export default function Page() {
|
||||
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.Trigger>
|
||||
<Button variant="ghost" radius="large">
|
||||
@@ -575,11 +677,11 @@ export default function Page() {
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" justify="center">
|
||||
{provisions.map((provision, index) => (
|
||||
{consumables.map((consumable, index) => (
|
||||
<img
|
||||
key={provision}
|
||||
key={consumable}
|
||||
src={asset(
|
||||
`/icons/provisions/${provision}.webp`,
|
||||
`/icons/consumables/${consumable}.webp`,
|
||||
)}
|
||||
style={{
|
||||
left: '50%',
|
||||
@@ -593,9 +695,9 @@ export default function Page() {
|
||||
* position = min + coefficient * diff
|
||||
*/
|
||||
top: `calc(4px + ${
|
||||
provisions.length === 1
|
||||
consumables.length === 1
|
||||
? 0.5
|
||||
: index / (provisions.length - 1)
|
||||
: index / (consumables.length - 1)
|
||||
} * (100% - 24px))`,
|
||||
transform: 'translateX(-50%)',
|
||||
position: 'absolute',
|
||||
@@ -606,24 +708,26 @@ export default function Page() {
|
||||
/>
|
||||
))}
|
||||
|
||||
{provisions.length === 0 && <GroupIcon />}
|
||||
{consumables.length === 0 && (
|
||||
<ComponentPlaceholderIcon />
|
||||
)}
|
||||
</Flex>
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Content>
|
||||
<ProvisionsManager
|
||||
provisions={provisionsList.map(
|
||||
(provision) => provision.id,
|
||||
)}
|
||||
selected={provisions}
|
||||
disabled={tank.provisions === provisions.length}
|
||||
onChange={(provisions) => {
|
||||
<ConsumablesManager
|
||||
consumables={consumablesList}
|
||||
selected={consumables}
|
||||
onChange={(consumables) => {
|
||||
mutateCompareTemporary((draft) => {
|
||||
draft.members[index].provisions =
|
||||
provisions;
|
||||
draft.members[index].consumables =
|
||||
consumables;
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
tank.consumables === consumables.length
|
||||
}
|
||||
/>
|
||||
|
||||
<Flex justify="end" mt="4">
|
||||
@@ -631,7 +735,7 @@ export default function Page() {
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateCompareTemporary((draft) => {
|
||||
draft.members[index].provisions = [];
|
||||
draft.members[index].consumables = [];
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
+15
-43
@@ -8,7 +8,7 @@ import {
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { use } from 'react';
|
||||
import { ConsumableButton } from '../../../../../../../components/ModuleButtons/ConsumableButton';
|
||||
import { ConsumablesManager } from '../../../../../../../components/ConsumablesManager';
|
||||
import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkrieg/checkConsumableProvisionInclusivity';
|
||||
import { consumableDefinitions } from '../../../../../../../core/blitzkrieg/consumableDefinitions';
|
||||
import { useEquipment } from '../../../../../../../hooks/useEquipment';
|
||||
@@ -68,48 +68,20 @@ export function Consumables() {
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Flex wrap="wrap">
|
||||
{consumablesList.map((consumable, index) => {
|
||||
const selected = consumables.includes(consumable.id);
|
||||
|
||||
return (
|
||||
<ConsumableButton
|
||||
special={consumable.duration === undefined}
|
||||
duration={
|
||||
consumable.duration === undefined
|
||||
? undefined
|
||||
: consumable.duration * (hasHighEndConsumables ? 0.7 : 1)
|
||||
}
|
||||
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>
|
||||
<ConsumablesManager
|
||||
consumables={consumablesList}
|
||||
selected={consumables}
|
||||
cooldownBooster={cooldownBooster}
|
||||
disabled={protagonist.tank.consumables === consumables.length}
|
||||
hasConsumableDeliverySystem={hasConsumableDeliverySystem}
|
||||
hasHighEndConsumables={hasHighEndConsumables}
|
||||
timers
|
||||
onChange={(consumables) => {
|
||||
mutateDuel((draft) => {
|
||||
draft.protagonist!.consumables = consumables;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</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 {
|
||||
consumable: number;
|
||||
duration?: number;
|
||||
cooldown: number;
|
||||
cooldown?: number;
|
||||
}
|
||||
|
||||
export function ConsumableButton({
|
||||
@@ -17,7 +17,7 @@ export function ConsumableButton({
|
||||
duration,
|
||||
...props
|
||||
}: ConsumableButtonProps) {
|
||||
const cooldownDelta = useDelta(cooldown);
|
||||
const cooldownDelta = useDelta(cooldown ?? 0);
|
||||
const durationDelta = useDelta(duration ?? 0);
|
||||
|
||||
return (
|
||||
@@ -25,38 +25,42 @@ export function ConsumableButton({
|
||||
icon={asset(`icons/consumables/${consumable}.webp`)}
|
||||
{...props}
|
||||
>
|
||||
<Flex direction="column" style={{ transform: 'translateX(-10px)' }}>
|
||||
{duration && (
|
||||
<Text
|
||||
color={
|
||||
durationDelta < 0
|
||||
? 'green'
|
||||
: durationDelta > 0
|
||||
? 'tomato'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Flex align="center" gap="1" style={{ marginBottom: -4 }}>
|
||||
<ClockIcon width={12} height={12} />
|
||||
{Math.round(duration)}s
|
||||
</Flex>
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
color={
|
||||
cooldownDelta < 0
|
||||
? 'green'
|
||||
: cooldownDelta > 0
|
||||
? 'tomato'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Flex align="center" gap="1">
|
||||
<ReloadIcon width={12} height={12} />
|
||||
{Math.round(cooldown)}s
|
||||
</Flex>
|
||||
</Text>
|
||||
</Flex>
|
||||
{(cooldown || duration) && (
|
||||
<Flex direction="column" style={{ transform: 'translateX(-10px)' }}>
|
||||
{duration && (
|
||||
<Text
|
||||
color={
|
||||
durationDelta < 0
|
||||
? 'green'
|
||||
: durationDelta > 0
|
||||
? 'tomato'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Flex align="center" gap="1" style={{ marginBottom: -4 }}>
|
||||
<ClockIcon width={12} height={12} />
|
||||
{Math.round(duration)}s
|
||||
</Flex>
|
||||
</Text>
|
||||
)}
|
||||
{cooldown && (
|
||||
<Text
|
||||
color={
|
||||
cooldownDelta < 0
|
||||
? 'green'
|
||||
: cooldownDelta > 0
|
||||
? 'tomato'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Flex align="center" gap="1">
|
||||
<ReloadIcon width={12} height={12} />
|
||||
{Math.round(cooldown)}s
|
||||
</Flex>
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</GenericTankComponentButton>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user