From f071a77fc0b76f2e8c0becce157ebc9b5802dadc Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:54:22 -0600 Subject: [PATCH] antagonist controls --- .../components/CustomShellButton/index.tsx | 63 +++++-- .../components/Discriminator/index.module.css | 3 - .../components/DropdownMenu/index.module.css | 8 + .../src/components/Select/index.module.css | 49 ++---- .../website/src/components/Select/index.tsx | 4 +- .../src/components/Slider/index.module.css | 10 ++ .../website/src/components/TankCard/index.tsx | 5 +- .../TankopediaAntagonistControl/index.tsx | 22 ++- .../components/QuickInputs/index.css | 8 - .../components/QuickInputs/index.tsx | 160 ------------------ .../TankopediaQuickInputs/index.module.css | 22 +++ .../TankopediaQuickInputs/index.tsx | 138 +++++++++++++++ .../components/TankopediaSandbox/index.tsx | 2 + .../TankopediaSkinSwitcher/index.tsx | 5 +- .../src/components/TextField/index.module.css | 27 +++ .../src/components/TextField/index.tsx | 31 ++-- .../src/components/Thicknesses/index.tsx | 7 +- .../website/src/hooks/useAbsoluteDisplay.ts | 11 ++ 18 files changed, 316 insertions(+), 259 deletions(-) delete mode 100644 packages/website/src/components/TankopediaOptions/components/QuickInputs/index.css delete mode 100644 packages/website/src/components/TankopediaOptions/components/QuickInputs/index.tsx create mode 100644 packages/website/src/components/TankopediaQuickInputs/index.module.css create mode 100644 packages/website/src/components/TankopediaQuickInputs/index.tsx create mode 100644 packages/website/src/hooks/useAbsoluteDisplay.ts diff --git a/packages/website/src/components/CustomShellButton/index.tsx b/packages/website/src/components/CustomShellButton/index.tsx index 61724e2e8..74b436c45 100644 --- a/packages/website/src/components/CustomShellButton/index.tsx +++ b/packages/website/src/components/CustomShellButton/index.tsx @@ -1,5 +1,8 @@ +import { alias } from "@blitzkit/core"; +import { type Strings } from "@blitzkit/i18n"; import { literals } from "@blitzkit/i18n/src/literals"; import { ShellType } from "@blitzkit/protos"; +import { CaretDownIcon, DotFilledIcon } from "@radix-ui/react-icons"; import { throttle } from "lodash-es"; import { useEffect, useMemo, useState, type KeyboardEvent } from "react"; import { DuelModule, useDuel } from "../../hooks/useDuel"; @@ -58,15 +61,24 @@ function NumberField({ value, suffix, onValueChange }: NumberFieldProps) { onValueChange(parsed); } }} + childrenAsSuffix > {suffix} ); } +const COMMON_SHELLS: [ShellType, keyof Strings["common"]["shells"]][] = [ + [ShellType.SHELL_TYPE_AP, "ap"], + [ShellType.SHELL_TYPE_APCR, "ap_cr"], + [ShellType.SHELL_TYPE_HEAT, "hc"], + [ShellType.SHELL_TYPE_HE, "he"], +]; + export function CustomShellButton() { const customShell = Tankopedia.use((state) => state.customShell); const gun = useDuel("protagonist", DuelModule.Gun); + const antagonistGun = useDuel("antagonist", DuelModule.Gun); const antagonistShell = useDuel("antagonist", DuelModule.Shell); const strings = useStrings(); @@ -100,7 +112,10 @@ export function CustomShellButton() { highContrast={customShell !== undefined} size="major" > - + @@ -126,18 +141,21 @@ export function CustomShellButton() { modified.explosion_radius = undefined; break; } + case ShellType.SHELL_TYPE_APCR: { modified.normalization = 2; modified.ricochet = 70; modified.explosion_radius = undefined; break; } + case ShellType.SHELL_TYPE_HEAT: { modified.normalization = undefined; modified.ricochet = undefined; modified.explosion_radius = undefined; break; } + case ShellType.SHELL_TYPE_HE: { modified.normalization = undefined; modified.ricochet = undefined; @@ -156,27 +174,36 @@ export function CustomShellButton() { > + + + + - + - {[ - [ShellType.SHELL_TYPE_AP, strings.common.shells.ap], - [ - ShellType.SHELL_TYPE_APCR, - strings.common.shells.ap_cr, - ], - [ - ShellType.SHELL_TYPE_HEAT, - strings.common.shells.hc, - ], - [ShellType.SHELL_TYPE_HE, strings.common.shells.he], - ].map(([type, label]) => ( - - {label} - - ))} + {COMMON_SHELLS.map(([type, label]) => { + return ( + + + + + + + {strings.common.shells[label]} + + + + + ); + })} diff --git a/packages/website/src/components/Discriminator/index.module.css b/packages/website/src/components/Discriminator/index.module.css index b6dca8df9..7d76b905f 100644 --- a/packages/website/src/components/Discriminator/index.module.css +++ b/packages/website/src/components/Discriminator/index.module.css @@ -3,10 +3,7 @@ right: var(--space-3); font-size: var(--font-size-minor); - line-height: 1.2; text-align: right; - text-shadow: 0 0 4px black; - color: var(--white-a12); pointer-events: none; diff --git a/packages/website/src/components/DropdownMenu/index.module.css b/packages/website/src/components/DropdownMenu/index.module.css index d75c072e1..562fd585e 100644 --- a/packages/website/src/components/DropdownMenu/index.module.css +++ b/packages/website/src/components/DropdownMenu/index.module.css @@ -75,6 +75,14 @@ border-bottom: var(--space-1) solid var(--gray-2); } + /** + * This is actually .ItemText, but its class name isn't being passed to the + * html, so I am selecting the span instead. + */ + span { + display: contents; + } + img, svg { width: 1em; diff --git a/packages/website/src/components/Select/index.module.css b/packages/website/src/components/Select/index.module.css index 4d19d9a29..9422525fd 100644 --- a/packages/website/src/components/Select/index.module.css +++ b/packages/website/src/components/Select/index.module.css @@ -1,3 +1,5 @@ +@import "../DropdownMenu/index.module.css"; + .Trigger { display: flex; align-items: center; @@ -18,45 +20,18 @@ outline: var(--outline-focus); box-shadow: var(--shadow-focus); } -} -.Content { - /* above popovers */ - z-index: 2; - - overflow: hidden; - background-color: var(--gray-2); - box-shadow: var(--shadow-subtle); - border-radius: var(--space-4); - outline: var(--outline-interactive) !important; -} - -.Item { - position: relative; - - display: flex; - align-items: center; - - padding: var(--space-3) var(--space-4) var(--space-3) var(--space-5); - background-color: var(--gray-3); - - font-size: var(--font-size-minor); - cursor: pointer; - user-select: none; - - &[data-highlighted] { - outline: none; - background-color: var(--gray-5); + svg, + img { + width: 1em; + height: 1em; } - &:not(:last-child) { - border-bottom: var(--space-1) solid var(--gray-2); + /* + * Another unselectable Radix component, being selected by span instead. + */ + .Icon, + span { + display: contents; } } - -.ItemIndicator { - position: absolute; - left: var(--space-3); - - display: flex; -} diff --git a/packages/website/src/components/Select/index.tsx b/packages/website/src/components/Select/index.tsx index 5e264acb5..e2ca25a94 100644 --- a/packages/website/src/components/Select/index.tsx +++ b/packages/website/src/components/Select/index.tsx @@ -2,4 +2,6 @@ import { Select as _ } from "radix-ui"; import { composeRadixComponents } from "../../ui/compose"; import styles from "./index.module.css"; -export const Select = composeRadixComponents(_, styles); +export const Select = composeRadixComponents(_, styles, { + Content: { position: "popper", align: "center" }, +}); diff --git a/packages/website/src/components/Slider/index.module.css b/packages/website/src/components/Slider/index.module.css index 7fe45f73b..aee65a8ca 100644 --- a/packages/website/src/components/Slider/index.module.css +++ b/packages/website/src/components/Slider/index.module.css @@ -7,6 +7,10 @@ height: var(--space-5); touch-action: none; user-select: none; + + html:active & { + cursor: grabbing; + } } .Track { @@ -35,6 +39,12 @@ background-color: var(--gray-12); box-shadow: var(--shadow-subtle); + cursor: grab; + + html:active & { + cursor: grabbing; + } + &:focus-visible { outline: var(--outline-focus); box-shadow: var(--shadow-focus); diff --git a/packages/website/src/components/TankCard/index.tsx b/packages/website/src/components/TankCard/index.tsx index 77e2599cb..20decce90 100644 --- a/packages/website/src/components/TankCard/index.tsx +++ b/packages/website/src/components/TankCard/index.tsx @@ -41,7 +41,6 @@ export const TankCard = (props: MaybeSkeletonComponentProps) => { : props.tank.type === TankType.TANK_TYPE_PREMIUM ? "amber" : "gray"; - const lowContrast = props.tank.type !== TankType.TANK_TYPE_RESEARCHABLE; const content = ( ) => { justifyContent: "flex-end", backgroundImage: `url(${alias( "api", - `/flags/scratched/${props.tank.nation}.webp` + `/flags/scratched/${props.tank.nation}.webp`, )})`, }} > @@ -78,7 +77,7 @@ export const TankCard = (props: MaybeSkeletonComponentProps) => { /> - + {name} diff --git a/packages/website/src/components/TankopediaAntagonistControl/index.tsx b/packages/website/src/components/TankopediaAntagonistControl/index.tsx index 6c1a62a8c..8323a8273 100644 --- a/packages/website/src/components/TankopediaAntagonistControl/index.tsx +++ b/packages/website/src/components/TankopediaAntagonistControl/index.tsx @@ -1,6 +1,7 @@ import { alias, romanize, uniqueGuns } from "@blitzkit/core"; import { literals } from "@blitzkit/i18n"; import { useMemo } from "react"; +import { useAbsoluteDisplay } from "../../hooks/useAbsoluteDisplay"; import { DuelModule, useDuel } from "../../hooks/useDuel"; import { useEquipment } from "../../hooks/useEquipment"; import { useStrings } from "../../hooks/useStrings"; @@ -28,7 +29,9 @@ export function TankopediaAntagonistControl({ const unwrap = useUnwrapper(); const showControls = Tankopedia.use((state) => state.showControls); - const display = Tankopedia.use((state) => state.loaders.display.loaded); + const isDynamicArmorDisplay = useAbsoluteDisplay( + TankopediaDisplay.DynamicArmor, + ); const hasCustomShell = Tankopedia.use( (state) => state.customShell !== undefined, ); @@ -46,7 +49,7 @@ export function TankopediaAntagonistControl({ [tank.turrets], ); - const isVisible = showControls && display === TankopediaDisplay.DynamicArmor; + const isVisible = showControls && isDynamicArmorDisplay; return ( @@ -83,7 +86,10 @@ export function TankopediaAntagonistControl({ }); }} > - + {romanize(entry.gun.tier)} @@ -127,7 +133,10 @@ export function TankopediaAntagonistControl({ - + - + state.protagonist); - const yawInput = useRef(null); - const pitchInput = useRef(null); - const tankModelDefinition = useTankModelDefinition(); - const turretModelDefinition = - tankModelDefinition.turrets[protagonist.turret.id]; - const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; - const initialTurretPitch = - tankModelDefinition.initial_turret_rotation?.pitch ?? 0; - const hasImprovedVerticalStabilizer = useEquipment("protagonist", 122); - const hasDownImprovedVerticalStabilizer = useEquipment("protagonist", 124); - - function displayPitch(pitch: number) { - return radToDeg(pitch) - initialTurretPitch; - } - - useEffect(() => { - function syncInputValues(data: ModelTransformEventData) { - if (!pitchInput.current || !yawInput.current) return; - - pitchInput.current.value = displayPitch(data.pitch).toFixed(1); - yawInput.current.value = radToDeg(data.yaw).toFixed(1); - } - - function handleTransformEvent( - event: QuicklimeEvent, - ) { - syncInputValues(event.data); - } - - modelTransformEvent.on(handleTransformEvent); - syncInputValues(modelTransformEvent.last!); - - return () => { - modelTransformEvent.off(handleTransformEvent); - }; - }, [initialTurretPitch]); - - return ( - - { - const value = Number(yawInput.current!.value); - - if (isNaN(value)) { - yawInput.current!.value = radToDeg( - modelTransformEvent.last!.yaw, - ).toFixed(1); - return; - } - - const [pitch, yaw] = applyPitchYawLimits( - modelTransformEvent.last!.pitch, - degToRad(value), - gunModelDefinition.pitch!, - turretModelDefinition.yaw, - hasImprovedVerticalStabilizer, - hasDownImprovedVerticalStabilizer, - ); - - modelTransformEvent.dispatch({ pitch, yaw }); - yawInput.current!.value = radToDeg( - modelTransformEvent.last!.yaw, - ).toFixed(1); - }} - onKeyDown={(event) => { - if (event.key === "Enter") { - yawInput.current?.blur(); - } - }} - onFocus={() => yawInput.current?.focus()} - ref={yawInput} - > - - - - - - - { - const value = Number(pitchInput.current!.value); - if (isNaN(value)) { - pitchInput.current!.value = displayPitch( - modelTransformEvent.last!.pitch, - ).toFixed(1); - return; - } - const [pitch, yaw] = applyPitchYawLimits( - degToRad(value + initialTurretPitch), - modelTransformEvent.last!.yaw, - gunModelDefinition.pitch!, - turretModelDefinition.yaw, - hasImprovedVerticalStabilizer, - hasDownImprovedVerticalStabilizer, - ); - modelTransformEvent.dispatch({ pitch, yaw }); - pitchInput.current!.value = displayPitch( - modelTransformEvent.last!.pitch, - ).toFixed(1); - }} - onKeyDown={(event) => { - if (event.key === "Enter") { - pitchInput.current?.blur(); - } - }} - onFocus={() => pitchInput.current?.focus()} - ref={pitchInput} - > - - - - - - - ); -} diff --git a/packages/website/src/components/TankopediaQuickInputs/index.module.css b/packages/website/src/components/TankopediaQuickInputs/index.module.css new file mode 100644 index 000000000..3e411febc --- /dev/null +++ b/packages/website/src/components/TankopediaQuickInputs/index.module.css @@ -0,0 +1,22 @@ +.quick-inputs { + pointer-events: auto; + + display: flex; + flex-direction: column; + gap: var(--space-3); + + min-width: 5rem; + + transition: var(--duration-brief); + + position: absolute; + top: 50%; + left: var(--space-4); + + transform: translate(calc(-100% - var(--space-4) - var(--space-0)), -50%); + + &[data-visible="true"], + &:focus-within { + transform: translate(0, -50%); + } +} diff --git a/packages/website/src/components/TankopediaQuickInputs/index.tsx b/packages/website/src/components/TankopediaQuickInputs/index.tsx new file mode 100644 index 000000000..6204e5735 --- /dev/null +++ b/packages/website/src/components/TankopediaQuickInputs/index.tsx @@ -0,0 +1,138 @@ +import { HeightIcon, WidthIcon } from "@radix-ui/react-icons"; +import type { QuicklimeEvent } from "quicklime"; +import { useEffect, useState, type KeyboardEvent, type ReactNode } from "react"; +import { degToRad, radToDeg } from "three/src/math/MathUtils.js"; +import { applyPitchYawLimits } from "../../core/blitz/applyPitchYawLimits"; +import { + modelTransformEvent, + type ModelTransformEventData, +} from "../../core/blitzkit/modelTransform"; +import { useDuelModel } from "../../hooks/useDuelModel"; +import { useEquipment } from "../../hooks/useEquipment"; +import { Tankopedia } from "../../stores/tankopedia"; +import { TextField } from "../TextField"; +import styles from "./index.module.css"; + +interface AngleFieldProps { + /** Degrees */ + value: number; + /** Receives degrees */ + onCommit(value: number): void; + children: ReactNode; +} + +function AngleField({ children, value, onCommit }: AngleFieldProps) { + const [text, setText] = useState(value.toFixed(1)); + const [editing, setEditing] = useState(false); + + // follow the tank while dragging, but never fight the user mid-typing + useEffect(() => { + if (!editing) setText(value.toFixed(1)); + }, [value, editing]); + + function commit(input: HTMLInputElement) { + const parsed = Number(text); + + if (text.trim() !== "" && Number.isFinite(parsed)) onCommit(parsed); + + setEditing(false); + setText(value.toFixed(1)); + input.blur(); + } + + function handleKeyDown(event: KeyboardEvent) { + // keep tankopedia hotkeys from firing while typing + event.stopPropagation(); + + const input = event.target as HTMLInputElement; + + if (event.key === "Enter") { + commit(input); + } else if (event.key === "Escape") { + setEditing(false); + setText(value.toFixed(1)); + input.blur(); + } + } + + return ( + { + setEditing(true); + (event.target as HTMLInputElement).select?.(); + }} + onBlur={(event) => commit(event.target as HTMLInputElement)} + onKeyDown={handleKeyDown} + onChange={(event) => setText(event.target.value)} + > + {children} + + ); +} + +export function TankopediaQuickInputs() { + const protagonist = Tankopedia.use((state) => state.protagonist); + const showControls = Tankopedia.use((state) => state.showControls); + + const tankModelDefinition = useDuelModel("protagonist"); + const turretModelDefinition = tankModelDefinition.turrets[protagonist.turret]; + const gunModelDefinition = turretModelDefinition.guns[protagonist.gun]; + const initialTurretPitch = + tankModelDefinition.initial_turret_rotation?.pitch ?? 0; + + const hasImprovedVerticalStabilizer = useEquipment("protagonist", 122); + const hasDownImprovedVerticalStabilizer = useEquipment("protagonist", 124); + + const [transform, setTransform] = useState(modelTransformEvent.last!); + + useEffect(() => { + function handleTransform(event: QuicklimeEvent) { + setTransform(event.data); + } + + modelTransformEvent.on(handleTransform); + setTransform(modelTransformEvent.last!); + + return () => { + modelTransformEvent.off(handleTransform); + }; + }, []); + + function apply(pitch: number, yaw: number) { + const [limitedPitch, limitedYaw] = applyPitchYawLimits( + pitch, + yaw, + gunModelDefinition.pitch!, + turretModelDefinition.yaw, + hasImprovedVerticalStabilizer, + hasDownImprovedVerticalStabilizer, + ); + + modelTransformEvent.dispatch({ pitch: limitedPitch, yaw: limitedYaw }); + } + + return ( + + apply(transform.pitch, degToRad(value))} + > + + + + + apply(degToRad(value + initialTurretPitch), transform.yaw) + } + > + + + + ); +} diff --git a/packages/website/src/components/TankopediaSandbox/index.tsx b/packages/website/src/components/TankopediaSandbox/index.tsx index 6b18245e9..8913439af 100644 --- a/packages/website/src/components/TankopediaSandbox/index.tsx +++ b/packages/website/src/components/TankopediaSandbox/index.tsx @@ -23,6 +23,7 @@ import { TankDynamicArmor } from "../TankDynamicArmor"; import { TankModel } from "../TankModel"; import { TankopediaAntagonistControl } from "../TankopediaAntagonistControl"; import { TankopediaOptions } from "../TankopediaOptions"; +import { TankopediaQuickInputs } from "../TankopediaQuickInputs"; import { TankopediaSkinSwitcher } from "../TankopediaSkinSwitcher"; import { TankSandboxLoader } from "../TankSandboxLoader"; import { TankStaticArmor, type ThicknessRange } from "../TankStaticArmor"; @@ -177,6 +178,7 @@ export function TankopediaSandbox({ + state.loaders.skin.requested); const showControls = Tankopedia.use((state) => state.showControls); - const display = Tankopedia.use((state) => state.loaders.display.loaded); + const isModelDisplay = useAbsoluteDisplay(TankopediaDisplay.Model); const loadedSkin = Tankopedia.use((state) => state.loaders.skin.loaded); const loadedAttachment = Tankopedia.use( (state) => state.loaders.attachment.loaded, @@ -42,7 +43,7 @@ export function TankopediaSkinSwitcher() { skins.filter((camouflage) => camouflage.attachment), ].filter((group) => group.length > 0); - const isVisible = showControls && display === TankopediaDisplay.Model; + const isVisible = showControls && isModelDisplay; const hasAnimations = hasSkinAnimations(tank.id, loadedSkin) || hasSkinAnimations(tank.id, loadedAttachment); diff --git a/packages/website/src/components/TextField/index.module.css b/packages/website/src/components/TextField/index.module.css index cd3d511a2..4185b6b70 100644 --- a/packages/website/src/components/TextField/index.module.css +++ b/packages/website/src/components/TextField/index.module.css @@ -29,6 +29,24 @@ } } + &[data-size="minor"] { + gap: var(--space-1); + font-size: var(--font-size-minor); + + &[data-variant="bubble"] input { + padding: var(--space-2) var(--space-3); + } + + &[data-variant="form"] input { + padding: var(--space-1); + } + + /* the icon's gap already separates it from the input */ + &[data-has-icon="true"] input { + padding-left: 0; + } + } + &[data-has-icon="true"] { padding-left: var(--space-3); @@ -37,6 +55,10 @@ } } + &[data-has-suffix="true"] { + padding-right: var(--space-3); + } + &:focus-within { outline: var(--outline-focus); box-shadow: var(--shadow-focus); @@ -60,6 +82,7 @@ input { min-width: 0; + width: auto; flex: 1; background: none; @@ -84,5 +107,9 @@ &[data-align="right"] { text-align: right; } + + &[data-auto-width="true"] { + field-sizing: content; + } } } diff --git a/packages/website/src/components/TextField/index.tsx b/packages/website/src/components/TextField/index.tsx index 67656b1c1..3249d4a94 100644 --- a/packages/website/src/components/TextField/index.tsx +++ b/packages/website/src/components/TextField/index.tsx @@ -1,20 +1,20 @@ import { forwardRef, - useCallback, useImperativeHandle, useRef, - type ChangeEvent, type ComponentProps, type ReactNode, } from "react"; import { classNames } from "../../ui/classNames"; import styles from "./index.module.css"; -export interface TextFieldProps extends ComponentProps<"input"> { +export interface TextFieldProps extends Omit, "size"> { children?: ReactNode; align?: "left" | "center" | "right"; autoWidth?: boolean; variant?: "bubble" | "form"; + size?: "regular" | "minor"; + childrenAsSuffix?: boolean; } export const TextField = forwardRef( @@ -27,25 +27,15 @@ export const TextField = forwardRef( align, disabled, value, - onChange, variant = "bubble", + size = "regular", + childrenAsSuffix, ...props }, ref, ) => { const input = useRef(null); - const handleChange = useCallback( - (event: ChangeEvent) => { - if (autoWidth && input.current) { - input.current.style.width = `${event.target.value.length || event.target.placeholder.length}ch`; - } - - onChange?.(event); - }, - [onChange], - ); - useImperativeHandle(ref, () => input.current!, []); return ( @@ -55,20 +45,21 @@ export const TextField = forwardRef( data-has-icon={children !== undefined} data-disabled={disabled} data-variant={variant} + data-size={size} + data-has-suffix={childrenAsSuffix} > - {children} + {!childrenAsSuffix && children} + + {childrenAsSuffix && children} ); }, diff --git a/packages/website/src/components/Thicknesses/index.tsx b/packages/website/src/components/Thicknesses/index.tsx index fa2bcd12f..7d2528725 100644 --- a/packages/website/src/components/Thicknesses/index.tsx +++ b/packages/website/src/components/Thicknesses/index.tsx @@ -1,6 +1,7 @@ import { literals } from "@blitzkit/i18n/src/literals"; import { EyeClosedIcon, EyeOpenIcon } from "@radix-ui/react-icons"; import { useCallback, type ReactNode } from "react"; +import { useAbsoluteDisplay } from "../../hooks/useAbsoluteDisplay"; import { useStrings } from "../../hooks/useStrings"; import { Tankopedia } from "../../stores/tankopedia"; import { TankopediaPersistent } from "../../stores/tankopediaPersistent"; @@ -61,7 +62,9 @@ export interface ThicknessesProps { export function Thicknesses({ thicknessRange }: ThicknessesProps) { const strings = useStrings(); - const display = Tankopedia.use((state) => state.loaders.display.loaded); + const isStaticArmorDisplay = useAbsoluteDisplay( + TankopediaDisplay.StaticArmor, + ); const showControls = Tankopedia.use((state) => state.showControls); const showPrimaryArmor = TankopediaPersistent.use( (state) => state.showPrimaryArmor, @@ -87,7 +90,7 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) { return ( + state.loaders.display.loaded === display && + state.loaders.display.requested === display, + ); +}