Play liveChoose your opponent
Join
141 lines · 6.7 KB Raw Permalink History
1// Your seat along the bottom, as 无名杀's mobile layout: your general's portrait card at the left (name, faction, HP,
2// identity, judgement area), your equipment beside it as five slots, your hand across the middle, your skills as
3// buttons at the right. Which skills and equipment can be used now is the upstream's own logic (PlayerCardLarge and
4// EquipColumn, followed here): their actions, activeWhen and variants, and distribute picking equipment.
5import type { ReactNode } from 'react';
6import type { GameView, Json } from '../../../src/engine/types';
7import type { SkillActionDef } from '../../../src/client/skillActionRegistry';
8import type { HpChangeNumber } from '../../../src/client/hooks/useAnimationState';
9import { useGameView } from '../../../src/client/components/GameViewCtx';
10import { isActiveAction, isFreePlayWindow, skillActionVariants } from '../../../src/client/utils/gameViewHelpers';
11import { EQUIPMENT_SKILL_NAMES } from '../../../src/client/components/gameViewConstants';
12import { LORD_SKILLS } from '../../../src/engine/data/character-meta';
13import { DEFAULT_SKILLS as ENGINE_DEFAULT_SKILLS } from '../../../src/engine/atoms/选将';
14import { getSkillDescription } from '../../../src/engine/skills/lifecycle';
15import { useSkillDescReady } from '../../../src/client/hooks/useSkillDescReady';
16import { displaySkillName } from '../../../src/client/utils/skillDisplay';
17import { Portrait, Identity, SLOTS, seatState, equipName } from './Seat';
18import { isRed } from './Card';
19
20const DEFAULT_SKILLS = new Set(ENGINE_DEFAULT_SKILLS);
21type Player = GameView['players'][number];
22
23interface Props {
24 player: Player;
25 index: number;
26 isCurrent: boolean;
27 phase: string;
28 spectating: boolean;
29 onSkillAction: (action: SkillActionDef, extraParams?: Record<string, Json>) => void;
30 onEquipClick: (cardId: string) => void;
31 distCandidates: Set<string> | null;
32 distSelected: Set<string>;
33 isDistributeActive: boolean;
34 targetable: boolean;
35 selected: boolean;
36 onTarget: () => void;
37 damaged?: number;
38 healed?: number;
39 hpChange?: HpChangeNumber;
40 timer: ReactNode;
41 hand: ReactNode;
42}
43
44export function Me(props: Props) {
45 const { view, canOperate, skillActions } = useGameView();
46 useSkillDescReady();
47 const { player: p, index, isCurrent, phase, spectating, onSkillAction, onEquipClick, distCandidates, distSelected, isDistributeActive, targetable, selected, onTarget, damaged, healed, hpChange, timer, hand } = props;
48 const actionCtx = { view, perspectiveIdx: index };
49
50 // Skills (PlayerCardLarge): the active ones as buttons, those of the free play window that can't be used now greyed,
51 // the rest as labels.
52 const isLord = p.identity === '主公';
53 const visible = p.skills.filter((s) => !DEFAULT_SKILLS.has(s) && !EQUIPMENT_SKILL_NAMES.has(s) && (isLord || !LORD_SKILLS.has(s)));
54 const triggerable = skillActions.filter(
55 (a) =>
56 a.prompt.type === 'confirm' ||
57 a.prompt.type === 'chooseOption' ||
58 a.prompt.type === 'choosePlayer' ||
59 a.prompt.type === 'selectTarget' ||
60 (a.prompt.type === 'useCardAndTarget' && !!a.transform) ||
61 (a.prompt.type === 'useCard' && !!a.transform) ||
62 a.prompt.type === 'distribute',
63 );
64 const isSkillActive = (a: SkillActionDef) => canOperate && isActiveAction(a, actionCtx);
65 const inFreePlayWindow = canOperate && isFreePlayWindow({ isMyTurn: isCurrent, phase: view.phase, pending: view.pending });
66 const skills = visible.flatMap((s) => {
67 const btns = triggerable.filter((a) => a.skillId === s);
68 const desc = getSkillDescription(s) ?? btns[0]?.prompt.title;
69 const display = displaySkillName(s);
70 const variants = btns.flatMap((a) => skillActionVariants(a).map((v) => ({ ...v, action: a, label: v.label || (btns.length > 1 ? a.label || a.actionType : '') })));
71 const active = variants.filter((v) => isSkillActive(v.action));
72 if (active.length)
73 return active.map((v) => (
74 <button key={v.label ? `${s}:${v.label}` : s} className={`nm-skill on${v.action.style === 'danger' ? ' danger' : ''}`} title={v.description ?? desc} onClick={() => onSkillAction(v.action, v.params)}>
75 {v.label ? (v.label.startsWith(display) ? v.label : `${display}·${v.label}`) : display}
76 </button>
77 ));
78 if (btns[0] && inFreePlayWindow)
79 return [
80 <button key={s} className="nm-skill" disabled title={p.turnUsage?.[`${s}/usedThisTurn`] ? '本回合已发动过(每回合限一次)' : (desc ?? '')}>
81 {display}
82 </button>,
83 ];
84 return [
85 <span key={s} className="nm-skill passive" title={desc}>
86 {display}
87 </span>,
88 ];
89 });
90
91 // Equipment (EquipColumn): an equipment skill that can be used now, or a card distribute may pick, is clickable.
92 const activeEquip = new Map<string, SkillActionDef>();
93 for (const a of skillActions) if (EQUIPMENT_SKILL_NAMES.has(a.skillId) && canOperate && isActiveAction(a, actionCtx)) activeEquip.set(a.skillId, a);
94 const equips = SLOTS.map(([slot]) => {
95 const id = p.equipment[slot];
96 const c = id ? view.cardMap[id] : undefined;
97 if (!id) return <div key={slot} className="nm-myequip empty" title={slot} />;
98 const skill = isDistributeActive ? undefined : activeEquip.get(c?.name ?? '');
99 const cand = isDistributeActive && !!distCandidates?.has(id);
100 const desc = c && EQUIPMENT_SKILL_NAMES.has(c.name) ? getSkillDescription(c.name) : c?.description;
101 return (
102 <div
103 key={slot}
104 className={`nm-myequip${skill ? ' usable' : ''}${cand ? ' candidate' : ''}${distSelected.has(id) ? ' picked' : ''}`}
105 onClick={skill ? () => onSkillAction(skill) : cand ? () => onEquipClick(id) : undefined}
106 title={`${c?.name ?? id}(${slot})${desc ? `\n${desc}` : ''}`}
107 >
108 <span className="nm-equip-name">{c ? equipName(slot, c.name) : id}</span>
109 {c && (
110 <span className={`nm-equip-info${isRed(c.suit) ? ' red' : ''}`}>
111 {c.suit}
112 {c.rank}
113 </span>
114 )}
115 </div>
116 );
117 });
118
119 return (
120 <div className={`nm-me${spectating ? ' spectating' : ''}`}>
121 <div
122 key={damaged ? `d${damaged}` : undefined}
123 className={`nm-player mine ${seatState(p, { current: isCurrent, selectable: targetable, selected, damaged: !!damaged, healed: !!healed })}`}
124 data-seat-index={index}
125 data-player-name={p.name}
126 onClick={onTarget}
127 >
128 <Portrait p={p} view={view} isCurrent={isCurrent} phase={phase} hpChange={hpChange}>
129 <Identity p={p} mine />
130 </Portrait>
131 </div>
132 <div className="nm-myequips">{equips}</div>
133 <div className="nm-hand">
134 {timer}
135 {hand}
136 </div>
137 <div className="nm-skills">{skills}</div>
138 <div className="nm-me-label">{spectating ? `旁观 · ${p.name}` : p.name}</div>
139 </div>
140 );
141}