diff --git a/athena/lib/getAttributeRange.tsx b/athena/lib/getAttributeRange.tsx index 5e41d4ae..b15885b4 100644 --- a/athena/lib/getAttributeRange.tsx +++ b/athena/lib/getAttributeRange.tsx @@ -1,10 +1,18 @@ import maxBy from '@deities/hephaestus/maxBy.tsx'; -export type AttributeRangeValue = 1 | 2 | 3 | 4 | 5; +export type AttributeRange = 1 | 2 | 3 | 4 | 5; +export type AttributeRangeWithZero = 0 | 1 | 2 | 3 | 4 | 5; +export type LargeAttributeRangeWithZero = + | AttributeRangeWithZero + | 6 + | 7 + | 8 + | 9 + | 10; export function validateAttributeRange( value?: number | null, -): value is AttributeRangeValue { +): value is AttributeRange { return !!value && value >= 1 && value <= 5; } @@ -12,15 +20,16 @@ export default function getAttributeRange( list: ReadonlyArray, extract: (entry: T) => number, min: number = 0, + length: number = 5, ) { const entry = maxBy(list, extract); if (!entry) { return []; } - const step = (extract(entry) - min) / 4; + const step = (extract(entry) - min) / (length - 1); return Array.from( - { length: 5 }, + { length }, (_, index) => min + index * step - (index > 0 ? step / 2 : 0), ); } @@ -28,7 +37,25 @@ export default function getAttributeRange( export function getAttributeRangeValue( range: ReadonlyArray, value: number, -): AttributeRangeValue { - const index = range.findIndex((item) => item >= value); - return (index === -1 ? range.length : index + 1) as AttributeRangeValue; +) { + if (value < range[0]) { + return 0; + } + + const index = range.findLastIndex((item) => item <= value); + return (index === -1 ? range.length : index + 1) as AttributeRangeWithZero; +} + +export function getLargeAttributeRangeValue( + range: ReadonlyArray, + value: number, +) { + if (value < range[0]) { + return 0; + } + + const index = range.findLastIndex((item) => item <= value); + return ( + index === -1 ? range.length : index + 1 + ) as LargeAttributeRangeWithZero; } diff --git a/hera/campaign/CampaignEditor.tsx b/hera/campaign/CampaignEditor.tsx index d59378c8..a0362621 100644 --- a/hera/campaign/CampaignEditor.tsx +++ b/hera/campaign/CampaignEditor.tsx @@ -1,6 +1,6 @@ import { Scenario } from '@deities/apollo/Effects.tsx'; import { - AttributeRangeValue, + AttributeRangeWithZero, validateAttributeRange, } from '@deities/athena/lib/getAttributeRange.tsx'; import { DoubleSize, TileSize } from '@deities/athena/map/Configuration.tsx'; @@ -101,7 +101,7 @@ export default function CampaignEditor({ const [, startTransition] = useTransition(); const [hasChanges, setHasChanges] = useState(!data.id); const [mapHasChanges, setMapHasChanges] = useState(false); - const [difficulty, setDifficulty] = useState( + const [difficulty, setDifficulty] = useState( validateAttributeRange(data.difficulty) ? data.difficulty : 0, ); const [description, setDescription] = useState( diff --git a/hera/campaign/panels/CampaignEditorControlPanel.tsx b/hera/campaign/panels/CampaignEditorControlPanel.tsx index 0110ebe9..4c56b0cc 100644 --- a/hera/campaign/panels/CampaignEditorControlPanel.tsx +++ b/hera/campaign/panels/CampaignEditorControlPanel.tsx @@ -1,4 +1,7 @@ -import { AttributeRangeValue } from '@deities/athena/lib/getAttributeRange.tsx'; +import { + AttributeRange, + AttributeRangeWithZero, +} from '@deities/athena/lib/getAttributeRange.tsx'; import UnknownTypeError from '@deities/hephaestus/UnknownTypeError.tsx'; import { TypeaheadDataSource } from '@deities/ui/Typeahead.tsx'; import BottomDrawer from '../../bottom-drawer/BottomDrawer.tsx'; @@ -32,14 +35,14 @@ export default function CampaignEditorControlPanel({ campaignExists: boolean; campaignName: string; description: string; - difficulty: AttributeRangeValue | 0; + difficulty: AttributeRangeWithZero; exportMaps: () => void; isAdmin?: boolean; playStyle: PlayStyleType | null; saveCampaign: (type?: 'Export') => void; setCampaignName: (name: string) => void; setDescription: (description: string) => void; - setDifficulty: (rating: AttributeRangeValue) => void; + setDifficulty: (rating: AttributeRange) => void; setPlayStyle: (playStyle: PlayStyleType | null) => void; setTags: (tags: ReadonlyArray) => void; setUsers: (users: ReadonlyArray) => void; diff --git a/hera/campaign/panels/CampaignEditorSettingsPanel.tsx b/hera/campaign/panels/CampaignEditorSettingsPanel.tsx index 6bc432ec..369bd1ac 100644 --- a/hera/campaign/panels/CampaignEditorSettingsPanel.tsx +++ b/hera/campaign/panels/CampaignEditorSettingsPanel.tsx @@ -1,4 +1,7 @@ -import { AttributeRangeValue } from '@deities/athena/lib/getAttributeRange.tsx'; +import { + AttributeRange, + AttributeRangeWithZero, +} from '@deities/athena/lib/getAttributeRange.tsx'; import { DoubleSize } from '@deities/athena/map/Configuration.tsx'; import Box from '@deities/ui/Box.tsx'; import InlineLink from '@deities/ui/InlineLink.tsx'; @@ -49,14 +52,14 @@ export default function CampaignEditorSettingsPanel({ campaignExists: boolean; campaignName: string; description: string; - difficulty: AttributeRangeValue | 0; + difficulty: AttributeRangeWithZero; exportMaps: () => void; isAdmin?: boolean; playStyle: PlayStyleType | null; saveCampaign: (type?: 'Export') => void; setCampaignName: (name: string) => void; setDescription: (description: string) => void; - setDifficulty: (rating: AttributeRangeValue) => void; + setDifficulty: (rating: AttributeRange) => void; setPlayStyle: (playStyle: PlayStyleType | null) => void; setTags: (tags: ReadonlyArray) => void; setUsers: (users: ReadonlyArray) => void; diff --git a/hera/card/BuildingCard.tsx b/hera/card/BuildingCard.tsx index 2c9836ef..6f9f6483 100644 --- a/hera/card/BuildingCard.tsx +++ b/hera/card/BuildingCard.tsx @@ -68,7 +68,11 @@ const costRange = getAttributeRange( extractCost, minBy(createableBuildings, extractCost)?.configuration.cost || 0, ); -const defenseRange = getAttributeRange(buildings, ({ defense }) => defense); +const defenseRange = getAttributeRange( + buildings, + ({ defense }) => defense, + 0.01, +); export default memo(function BuildingCard({ biome, @@ -161,7 +165,6 @@ export default memo(function BuildingCard({ {limit > 0 ? ( <> diff --git a/hera/card/Range.tsx b/hera/card/Range.tsx index f5640a75..542cadde 100644 --- a/hera/card/Range.tsx +++ b/hera/card/Range.tsx @@ -1,4 +1,8 @@ -import { AttributeRangeValue } from '@deities/athena/lib/getAttributeRange.tsx'; +import { + AttributeRange, + AttributeRangeWithZero, + LargeAttributeRangeWithZero, +} from '@deities/athena/lib/getAttributeRange.tsx'; import Breakpoints from '@deities/ui/Breakpoints.tsx'; import { SquareButtonStyle } from '@deities/ui/Button.tsx'; import { applyVar } from '@deities/ui/cssVar.tsx'; @@ -26,6 +30,16 @@ const StarEmpty = ( /> ); +const StarHalf = ( + `, + height: 24, + width: 24, + }} + /> +); + const StarFull = ( ); +const toValue = (value: AttributeRangeWithZero) => (value === 0 ? 1 : value); + +const starIcon = (value: number, boundary: number) => + value >= boundary ? StarFull : StarEmpty; + +const halfStarIcon = (value: number, boundary: number) => + value >= boundary ? (value >= boundary + 1 ? StarFull : StarHalf) : StarEmpty; + export default memo(function Range({ end, invert, invertColors, value, wide, - zero, }: { end?: true; invert?: true; invertColors?: true; - value: AttributeRangeValue; + value: AttributeRangeWithZero; wide?: true; - zero?: true; }) { if (invert) { - value = (6 - value) as AttributeRangeValue; + value = (6 - toValue(value)) as AttributeRange; } + const style = { - color: colors[invertColors ? ((6 - value) as AttributeRangeValue) : value], + color: + colors[ + (invertColors ? 6 - toValue(value) : toValue(value)) as AttributeRange + ], }; + return (
- {value >= 1 && (!zero || value > 1) ? StarFull : StarEmpty} + {starIcon(value, 1)} +
+
+ {starIcon(value, 2)} +
+
+ {starIcon(value, 3)} +
+
+ {starIcon(value, 4)} +
+
+ {starIcon(value, 5)} +
+
+ ); +}); + +export const LargeRange = memo(function LargeRange({ + end, + value, +}: { + end?: true; + value: LargeAttributeRangeWithZero; +}) { + const style = { + color: colors[toValue(Math.ceil(value / 2) as AttributeRange)], + }; + + return ( + +
+ {halfStarIcon(value, 1)}
- {value >= 2 ? StarFull : StarEmpty} + {halfStarIcon(value, 3)}
- {value >= 3 ? StarFull : StarEmpty} + {halfStarIcon(value, 5)}
- {value >= 4 ? StarFull : StarEmpty} + {halfStarIcon(value, 7)}
- {value >= 5 ? StarFull : StarEmpty} + {halfStarIcon(value, 9)}
); @@ -92,15 +154,13 @@ export const RangeSelector = memo(function RangeSelector({ value: initialValue, }: { invert?: true; - onSelect: (value: AttributeRangeValue) => void; - value?: AttributeRangeValue | 0; + onSelect: (value: AttributeRange) => void; + value?: AttributeRangeWithZero; }) { const [isActive, setIsActive] = useState(false); - const [value, setValue] = useState( - initialValue || 0, - ); - const color = (value: AttributeRangeValue) => - colors[invert ? ((6 - value) as AttributeRangeValue) : value]; + const [value, setValue] = useState(initialValue || 0); + const color = (value: AttributeRange) => + colors[invert ? ((6 - value) as AttributeRange) : value]; return ( setValue(1)} style={{ color: color(1) }} > - {value >= 1 ? StarFull : StarEmpty} + {starIcon(value, 1)}
setValue(2)} style={{ color: color(2) }} > - {value >= 2 ? StarFull : StarEmpty} + {starIcon(value, 2)}
setValue(3)} style={{ color: color(3) }} > - {value >= 3 ? StarFull : StarEmpty} + {starIcon(value, 3)}
setValue(4)} style={{ color: color(4) }} > - {value >= 4 ? StarFull : StarEmpty} + {starIcon(value, 4)}
setValue(5)} style={{ color: color(5) }} > - {value >= 5 ? StarFull : StarEmpty} + {starIcon(value, 5)}
); diff --git a/hera/card/TileCard.tsx b/hera/card/TileCard.tsx index 9bf7b70e..187656d5 100644 --- a/hera/card/TileCard.tsx +++ b/hera/card/TileCard.tsx @@ -43,7 +43,7 @@ import CardTitle, { CardInfoHeading } from './CardTitle.tsx'; import CoverRange from './lib/CoverRange.tsx'; import tileFieldHasDecorator from './lib/tileFieldHasDecorator.tsx'; import MovementBox from './MovementBox.tsx'; -import Range from './Range.tsx'; +import Range, { LargeRange } from './Range.tsx'; import TilePreview from './TilePreview.tsx'; const tiles = getAllTiles(); @@ -120,7 +120,7 @@ export default memo(function TileCard({ {coverName ? `${coverName}: ${cover}` : cover} - + {vision === -1 ? ( <> diff --git a/hera/card/UnitCard.tsx b/hera/card/UnitCard.tsx index 0bd9224f..59ed623d 100644 --- a/hera/card/UnitCard.tsx +++ b/hera/card/UnitCard.tsx @@ -213,7 +213,7 @@ const visionRange = getAttributeRange( ({ configuration: { vision } }) => vision, 1, ); -const defenseRange = getAttributeRange(units, ({ defense }) => defense); +const defenseRange = getAttributeRange(units, ({ defense }) => defense, 0.01); const supplyRange = getAttributeRange( units, extractFuel, @@ -345,11 +345,7 @@ export default memo(function UnitCard({ Defense
{defense}
- + Movement diff --git a/hera/card/lib/CoverRange.tsx b/hera/card/lib/CoverRange.tsx index ce040719..d4dbc23b 100644 --- a/hera/card/lib/CoverRange.tsx +++ b/hera/card/lib/CoverRange.tsx @@ -1,10 +1,20 @@ import { getAllTiles } from '@deities/athena/info/Tile.tsx'; import getAttributeRange from '@deities/athena/lib/getAttributeRange.tsx'; +import minBy from '@deities/hephaestus/minBy.tsx'; + +const tiles = getAllTiles().filter( + ({ configuration: { cover } }) => cover < Number.POSITIVE_INFINITY, +); + +const min = + (minBy( + tiles.filter(({ configuration: { cover } }) => cover > 0), + ({ configuration: { cover } }) => cover, + )?.configuration.cover || 0) - 0.01; export default getAttributeRange( - getAllTiles().filter( - ({ configuration: { cover } }) => cover < Number.POSITIVE_INFINITY, - ), + tiles, ({ configuration: { cover } }) => cover, - 0, + min, + 10, ); diff --git a/hera/ui/MapInfo.tsx b/hera/ui/MapInfo.tsx index 26a29c3e..fe4e3ad8 100644 --- a/hera/ui/MapInfo.tsx +++ b/hera/ui/MapInfo.tsx @@ -3,7 +3,7 @@ import { TileField, TileInfo, } from '@deities/athena/info/Tile.tsx'; -import { getAttributeRangeValue } from '@deities/athena/lib/getAttributeRange.tsx'; +import { getLargeAttributeRangeValue } from '@deities/athena/lib/getAttributeRange.tsx'; import { Biome } from '@deities/athena/map/Biome.tsx'; import Building from '@deities/athena/map/Building.tsx'; import { @@ -38,7 +38,7 @@ import { css, cx } from '@emotion/css'; import React, { useEffect, useMemo, useRef } from 'react'; import BuildingTile from '../Building.tsx'; import CoverRange from '../card/lib/CoverRange.tsx'; -import Range from '../card/Range.tsx'; +import { LargeRange } from '../card/Range.tsx'; import Map from '../Map.tsx'; import Tick from '../Tick.tsx'; import { State } from '../Types.tsx'; @@ -104,10 +104,9 @@ const Tile = ({ Cover:
-