Skip to content

Commit 6b7c9b8

Browse files
committed
feat: display operator requirements in operation viewer
1 parent a691b78 commit 6b7c9b8

File tree

4 files changed

+184
-86
lines changed

4 files changed

+184
-86
lines changed

src/components/MasteryIcon.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,35 @@ import { FC } from 'react'
22

33
interface MasteryIconProps extends React.SVGProps<SVGSVGElement> {
44
mastery: number
5+
mainClassName?: string
6+
subClassName?: string
57
}
68

7-
export const MasteryIcon: FC<MasteryIconProps> = ({ mastery, ...props }) => {
9+
export const MasteryIcon: FC<MasteryIconProps> = ({
10+
mastery,
11+
mainClassName = 'fill-current',
12+
subClassName = 'fill-gray-300 dark:fill-gray-600',
13+
...props
14+
}) => {
815
return (
916
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" {...props}>
1017
<circle
11-
className={mastery >= 1 ? '' : 'sub-circle'}
18+
className={mastery >= 1 ? mainClassName : subClassName}
1219
cx="50"
1320
cy="27"
1421
r="22"
15-
fill={mastery >= 1 ? 'currentColor' : '#898989'}
1622
/>
1723
<circle
18-
className={mastery >= 2 ? '' : 'sub-circle'}
24+
className={mastery >= 2 ? mainClassName : subClassName}
1925
cx="75"
2026
cy="70"
2127
r="22"
22-
fill={mastery >= 2 ? 'currentColor' : '#898989'}
2328
/>
2429
<circle
25-
className={mastery >= 3 ? '' : 'sub-circle'}
30+
className={mastery >= 3 ? mainClassName : subClassName}
2631
cx="25"
2732
cy="70"
2833
r="22"
29-
fill={mastery >= 3 ? 'currentColor' : '#898989'}
3034
/>
3135
</svg>
3236
)

src/components/editor2/operator/OperatorItem.tsx

Lines changed: 59 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,65 @@ export const OperatorItem: FC<OperatorItemProps> = memo(
8282
className={clsx('relative flex items-start', isDragging && 'invisible')}
8383
>
8484
<div className="relative">
85+
<Popover2
86+
placement="top"
87+
content={
88+
<Menu>
89+
<MenuItem
90+
icon="star"
91+
text={t.components.editor2.OperatorItem.add_to_favorites}
92+
onClick={() => {
93+
setFavOperators((prev) => [...prev, operator])
94+
AppToaster.show({
95+
message:
96+
t.components.editor2.OperatorItem.added_to_favorites,
97+
intent: 'success',
98+
})
99+
}}
100+
/>
101+
<MenuItem
102+
icon="trash"
103+
text={t.common.delete}
104+
intent="danger"
105+
onClick={onRemove}
106+
/>
107+
</Menu>
108+
}
109+
>
110+
<Card
111+
interactive
112+
elevation={Elevation.ONE}
113+
className="relative w-24 p-0 !py-0 flex flex-col overflow-hidden select-none pointer-events-auto"
114+
{...attributes}
115+
{...listeners}
116+
>
117+
<OperatorAvatar
118+
id={info?.id}
119+
rarity={info?.rarity}
120+
className="w-24 h-24 rounded-b-none"
121+
fallback={operator.name}
122+
/>
123+
<h4
124+
className={clsx(
125+
'm-1 leading-5 font-bold pointer-events-none',
126+
operator.name && operator.name.length >= 7 && 'text-xs',
127+
)}
128+
>
129+
{operator.name}
130+
</h4>
131+
{info && info.prof !== 'TOKEN' && (
132+
<img
133+
className="absolute top-0 right-0 w-5 h-5 p-px bg-gray-600 pointer-events-none"
134+
src={'/assets/prof-icons/' + info.prof + '.png'}
135+
alt={info.prof}
136+
/>
137+
)}
138+
</Card>
139+
</Popover2>
140+
85141
{info?.prof !== 'TOKEN' && (
86142
<>
87-
<div className="absolute z-10 top-2 -left-5 ml-[2px] px-3 py-4 rounded-full bg-[radial-gradient(rgba(0,0,0,0.6)_10%,rgba(0,0,0,0.08)_35%,rgba(0,0,0,0)_50%)] pointer-events-none">
143+
<div className="absolute top-2 -left-5 ml-[2px] px-3 py-4 rounded-full bg-[radial-gradient(rgba(0,0,0,0.6)_10%,rgba(0,0,0,0.08)_35%,rgba(0,0,0,0)_50%)] pointer-events-none">
88144
<Button
89145
small
90146
minimal
@@ -115,7 +171,7 @@ export const OperatorItem: FC<OperatorItemProps> = memo(
115171
/>
116172
</Button>
117173
</div>
118-
<div className="absolute z-10 -top-2 -left-2 flex flex-col items-center">
174+
<div className="absolute -top-2 -left-2 flex flex-col items-center">
119175
<NumericInput2
120176
intOnly
121177
min={1}
@@ -164,61 +220,6 @@ export const OperatorItem: FC<OperatorItemProps> = memo(
164220
</div>
165221
</>
166222
)}
167-
<Popover2
168-
placement="top"
169-
content={
170-
<Menu>
171-
<MenuItem
172-
icon="star"
173-
text={t.components.editor2.OperatorItem.add_to_favorites}
174-
onClick={() => {
175-
setFavOperators((prev) => [...prev, operator])
176-
AppToaster.show({
177-
message:
178-
t.components.editor2.OperatorItem.added_to_favorites,
179-
intent: 'success',
180-
})
181-
}}
182-
/>
183-
<MenuItem
184-
icon="trash"
185-
text={t.common.delete}
186-
intent="danger"
187-
onClick={onRemove}
188-
/>
189-
</Menu>
190-
}
191-
>
192-
<Card
193-
interactive
194-
elevation={Elevation.ONE}
195-
className="relative w-24 p-0 !py-0 flex flex-col overflow-hidden select-none pointer-events-auto"
196-
{...attributes}
197-
{...listeners}
198-
>
199-
<OperatorAvatar
200-
id={info?.id}
201-
rarity={info?.rarity}
202-
className="w-24 h-24 rounded-b-none"
203-
fallback={operator.name}
204-
/>
205-
<h4
206-
className={clsx(
207-
'm-1 leading-5 font-bold pointer-events-none',
208-
operator.name && operator.name.length >= 7 && 'text-xs',
209-
)}
210-
>
211-
{operator.name}
212-
</h4>
213-
{info?.prof && info.prof !== 'TOKEN' && (
214-
<img
215-
className="absolute z-10 top-0 right-0 w-5 h-5 p-px bg-gray-600 pointer-events-none"
216-
src={'/assets/prof-icons/' + info?.prof + '.png'}
217-
alt=""
218-
/>
219-
)}
220-
</Card>
221-
</Popover2>
222223
<div className="flex h-6">
223224
{controlsEnabled && (
224225
<DetailedSelect
@@ -401,7 +402,7 @@ export const OperatorItem: FC<OperatorItemProps> = memo(
401402
/>
402403
{skillLevel > 7 && (
403404
<MasteryIcon
404-
className="absolute top-0 bottom-0 left-0 right-0 p-2 pointer-events-none [&_.sub-circle]:fill-gray-300 dark:[&_.sub-circle]:fill-gray-500"
405+
className="absolute top-0 bottom-0 left-0 right-0 p-2 pointer-events-none"
405406
mastery={skillLevel - 7}
406407
/>
407408
)}

src/components/viewer/OperationViewer.tsx

Lines changed: 110 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,17 @@ import { i18nDefer, useTranslation } from '../../i18n/i18n'
5151
import { CopilotDocV1 } from '../../models/copilot.schema'
5252
import { createCustomLevel, findLevelByStageName } from '../../models/level'
5353
import { Level } from '../../models/operation'
54-
import { OPERATORS, useLocalizedOperatorName } from '../../models/operator'
54+
import {
55+
OPERATORS,
56+
defaultSkills,
57+
getEliteIconUrl,
58+
useLocalizedOperatorName,
59+
withDefaultRequirements,
60+
} from '../../models/operator'
5561
import { formatError } from '../../utils/error'
5662
import { ActionCard } from '../ActionCard'
5763
import { Confirm } from '../Confirm'
64+
import { MasteryIcon } from '../MasteryIcon'
5865
import { ReLink } from '../ReLink'
5966
import { UserName } from '../UserName'
6067
import { CommentArea } from './comment/CommentArea'
@@ -329,24 +336,98 @@ export const OperationViewer: ComponentType<{
329336

330337
const OperatorCard: FC<{
331338
operator: CopilotDocV1.Operator
332-
}> = ({ operator }) => {
339+
version?: number
340+
}> = ({ operator, version = 1 }) => {
333341
const t = useTranslation()
334-
const { name, skill } = operator
335-
const info = OPERATORS.find((o) => o.name === name)
342+
const displayName = useLocalizedOperatorName(operator.name)
343+
const info = OPERATORS.find((o) => o.name === operator.name)
344+
const skills = info ? info.skills : defaultSkills
345+
const { level, elite, skillLevel, module } = withDefaultRequirements(
346+
operator.requirements,
347+
info?.rarity,
348+
)
336349

337350
return (
338-
<div className="min-w-24 flex flex-col items-center">
339-
<OperatorAvatar
340-
id={info?.id}
341-
rarity={info?.rarity}
342-
className="w-16 h-16 mb-1"
343-
/>
344-
<span className={clsx('mb-1 font-bold')}>
345-
{useLocalizedOperatorName(name)}
346-
</span>
347-
<span className="text-xs text-zinc-300">
348-
{t.models.operator.skill_number({ count: skill ?? 1 })}
349-
</span>
351+
<div className="relative flex items-start">
352+
<div className="relative w-20">
353+
<div className="relative rounded-lg overflow-hidden shadow-md">
354+
<OperatorAvatar
355+
id={info?.id}
356+
rarity={info?.rarity}
357+
className="w-20 h-20"
358+
fallback={displayName}
359+
/>
360+
{info?.equips && module !== 0 && (
361+
<div
362+
title={t.components.editor2.label.opers.requirements.module}
363+
className="absolute -bottom-1 right-1 font-serif font-bold text-lg text-white [text-shadow:0_0_3px_#a855f7,0_0_5px_#a855f7]"
364+
>
365+
{info.equips[module]}
366+
</div>
367+
)}
368+
</div>
369+
<h4 className="mt-1 -mx-2 font-semibold tracking-tighter text-center">
370+
{displayName}
371+
</h4>
372+
{info && info.prof !== 'TOKEN' && (
373+
<img
374+
className="absolute top-0 right-0 w-5 h-5 p-px bg-gray-600 rounded-tr-md"
375+
src={'/assets/prof-icons/' + info.prof + '.png'}
376+
alt={info.prof}
377+
/>
378+
)}
379+
</div>
380+
{version >= 2 && info?.prof !== 'TOKEN' && (
381+
<>
382+
<div className="absolute top-1 -left-5 ml-[2px] px-3 py-4 rounded-full bg-[radial-gradient(rgba(0,0,0,0.6)_10%,rgba(0,0,0,0.08)_30%,rgba(0,0,0,0)_45%)] pointer-events-none">
383+
<img
384+
className="w-7 h-6 object-contain"
385+
src={getEliteIconUrl(elite)}
386+
alt={t.models.operator.elite({ level: elite })}
387+
/>
388+
</div>
389+
<div className="absolute -top-2 -left-2 w-8 h-8 pr-px leading-7 rounded-full border-2 border-yellow-300 bg-black/50 text-lg text-white font-semibold text-center shadow-[0_1px_2px_rgba(0,0,0,0.9)]">
390+
{level}
391+
</div>
392+
</>
393+
)}
394+
395+
<ul className="flex flex-col gap-1 ml-1">
396+
{skills.map((_, index) => {
397+
const skillNumber = index + 1
398+
const selected = operator.skill === skillNumber
399+
return (
400+
<li
401+
key={index}
402+
className={clsx(
403+
'relative',
404+
selected
405+
? 'bg-purple-100 dark:bg-purple-900 dark:text-purple-200 text-purple-800'
406+
: 'bg-gray-300 dark:bg-gray-600 opacity-15 dark:opacity-25',
407+
)}
408+
title={t.models.operator.skill_number({ count: skillNumber })}
409+
>
410+
<div className="w-6 h-6 flex items-center justify-center font-bold text-xl border-2 border-current">
411+
{version >= 2 ? (
412+
selected ? (
413+
skillLevel <= 7 ? (
414+
skillLevel
415+
) : (
416+
<MasteryIcon
417+
className="w-4 h-4"
418+
mastery={skillLevel - 7}
419+
subClassName="fill-gray-300 dark:fill-gray-500"
420+
/>
421+
)
422+
) : undefined
423+
) : (
424+
skillNumber
425+
)}
426+
</div>
427+
</li>
428+
)
429+
})}
430+
</ul>
350431
</div>
351432
)
352433
}
@@ -531,7 +612,7 @@ function OperationViewerInnerDetails({ operation }: { operation: Operation }) {
531612
/>
532613
</H4>
533614
<Collapse isOpen={showOperators}>
534-
<div className="mt-2 flex flex-wrap -ml-4 gap-y-2">
615+
<div className="mt-2 flex flex-wrap gap-6">
535616
{!operation.parsedContent.opers?.length &&
536617
!operation.parsedContent.groups?.length && (
537618
<NonIdealState
@@ -545,7 +626,11 @@ function OperationViewerInnerDetails({ operation }: { operation: Operation }) {
545626
/>
546627
)}
547628
{operation.parsedContent.opers?.map((operator) => (
548-
<OperatorCard key={operator.name} operator={operator} />
629+
<OperatorCard
630+
key={operator.name}
631+
operator={operator}
632+
version={operation.parsedContent.version}
633+
/>
549634
))}
550635
</div>
551636
<div className="flex flex-wrap gap-4 mt-4">
@@ -555,12 +640,16 @@ function OperationViewerInnerDetails({ operation }: { operation: Operation }) {
555640
className="!p-2 flex flex-col items-center"
556641
key={group.name}
557642
>
558-
<H6 className="text-gray-800">{group.name}</H6>
559-
<div className="flex flex-wrap gap-y-2">
643+
<H6 className="mb-3 text-gray-800">{group.name}</H6>
644+
<div className="flex flex-wrap px-2 gap-6">
560645
{group.opers
561646
?.filter(Boolean)
562647
.map((operator) => (
563-
<OperatorCard key={operator.name} operator={operator} />
648+
<OperatorCard
649+
key={operator.name}
650+
operator={operator}
651+
version={operation.parsedContent.version}
652+
/>
564653
))}
565654

566655
{group.opers?.filter(Boolean).length === 0 && (

src/i18n/translations.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3417,6 +3417,10 @@
34173417
"other": "Unknown Skill"
34183418
}
34193419
},
3420+
"elite": {
3421+
"cn": "精英{{level}}",
3422+
"en": "Elite {{level}}"
3423+
},
34203424
"skill_usage": {
34213425
"none": {
34223426
"title": {

0 commit comments

Comments
 (0)