Commit 61035821 authored by timel's avatar timel

feat: card dropdown

parent c082dd4d
Pipeline #22392 failed with stages
in 11 minutes and 37 seconds
import { sendSelectSingleResponse, ygopro } from "@/api";
import { useConfig } from "@/config";
import { fetchSelectHintMeta, messageStore } from "@/stores";
import { fetchSelectHintMeta } from "@/stores";
import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
import { fetchCheckCardMeta } from "../utils";
......
import { ygopro } from "@/api";
import { cardStore, matStore } from "@/stores";
import { cardStore } from "@/stores";
import { showWaiting } from "@/ui/Duel/Message";
export default (_wait: ygopro.StocGameMessage.MsgWait) => {
......
import type { CardMeta, ygopro } from "@/api";
type CardLocation = ReturnType<typeof ygopro.CardLocation.prototype.toObject>;
interface Option {
// card id
meta: CardMeta;
location?: CardLocation;
// 效果
effectDesc?: string;
// 作为素材的cost,比如同调召唤的星级
level1?: number;
level2?: number;
response: number;
}
export interface ModalState {
// Yes or No弹窗
......
......@@ -10,6 +10,7 @@ import {
OptionModal,
PositionModal,
SelectActionsModal,
SimpleSelectCardsModal,
SortCardModal,
YesNoModal,
} from "./Message";
......@@ -32,6 +33,7 @@ const NeosDuel = () => {
<CheckCounterModal />
<SortCardModal />
<AnnounceModal />
<SimpleSelectCardsModal />
</>
);
};
......
import "./index.scss";
import { MinusOutlined, UpOutlined } from "@ant-design/icons";
import { Button, Modal, type ModalProps } from "antd";
import { Modal, type ModalProps } from "antd";
import classNames from "classnames";
import { type CSSProperties, type FC, useRef, useState } from "react";
import { useState } from "react";
interface Props extends ModalProps {
canBeMinimized?: boolean;
}
export const NeosModal: FC<Props> = (props) => {
export const NeosModal: React.FC<Props> = (props) => {
const { canBeMinimized = true } = props;
const [mini, setMini] = useState(false);
return (
......
import "./index.scss";
import { type FC } from "react";
import { INTERNAL_Snapshot as Snapshot, proxy, useSnapshot } from "valtio";
import { sendSelectMultiResponse, sendSelectSingleResponse } from "@/api";
import { type Option, SelectCardsModal } from "../SelectCardsModal";
import {
type Option,
SelectCardsModal,
type SelectCardsModalProps,
} from "../SelectCardsModal";
const CANCEL_RESPONSE = -1;
const FINISH_RESPONSE = -1;
const defaultProps = {
const defaultProps: Omit<
SelectCardsModalProps,
"onSubmit" | "onCancel" | "onFinish"
> = {
isOpen: false,
isChain: false,
min: 0,
......@@ -27,7 +33,7 @@ const defaultProps = {
const localStore = proxy(defaultProps);
export const SelectActionsModal: FC = () => {
export const SelectActionsModal: React.FC = () => {
const {
isOpen,
isChain,
......@@ -44,12 +50,13 @@ export const SelectActionsModal: FC = () => {
} = useSnapshot(localStore);
const onSubmit = (options: Snapshot<Option[]>) => {
const values = options.map((option) => option.response);
const values = options.map((option) => option.response!);
if (isChain) {
sendSelectSingleResponse(values[0]);
} else {
sendSelectMultiResponse(values);
}
console.log("here");
rs();
};
......
......@@ -2,19 +2,19 @@ import "./index.scss";
import { CheckCard } from "@ant-design/pro-components";
import { Button, Segmented, Space, Tooltip } from "antd";
import { type FC, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
import type { CardMeta, ygopro } from "@/api";
import { fetchStrings } from "@/api";
import { matStore } from "@/stores";
import { CardType, matStore } from "@/stores";
import { YgoCard } from "@/ui/Shared";
import { groupBy } from "../../utils";
import { showCardModal } from "../CardModal";
import { NeosModal } from "../NeosModal";
export const SelectCardsModal: FC<{
export interface SelectCardsModalProps {
isOpen: boolean;
isChain: boolean;
min: number;
......@@ -30,7 +30,9 @@ export const SelectCardsModal: FC<{
onSubmit: (options: Snapshot<Option[]>) => void;
onCancel: () => void;
onFinish: () => void;
}> = ({
}
export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({
isOpen,
isChain,
min,
......@@ -119,7 +121,7 @@ export const SelectCardsModal: FC<{
<Button
type="primary"
disabled={!submitable}
onClick={() => onSubmit(mustSelects)}
onClick={() => onSubmit([...mustSelects, ...result])}
>
{submitText}
</Button>
......@@ -141,7 +143,10 @@ export const SelectCardsModal: FC<{
options[0] === selectedZone && (
<div className="checkcard-container" key={i}>
<CheckCard.Group
onChange={setResult as any}
onChange={(res) => {
console.log("setresult", res);
setResult((single ? [res] : res) as any);
}}
// TODO 考虑如何设置默认值,比如只有一个的,就直接选中
multiple={!single}
style={{
......@@ -197,7 +202,7 @@ export const SelectCardsModal: FC<{
};
/** 选择区域 */
const Selector: FC<{
const Selector: React.FC<{
zoneOptions: {
value: ygopro.CardZone;
label: string;
......@@ -226,5 +231,7 @@ export interface Option {
// 作为素材的cost,比如同调召唤的星级
level1?: number;
level2?: number;
response: number;
response?: number;
// 便于直接返回这个信息
card?: CardType;
}
// import "./index.scss";
import { type FC } from "react";
import { INTERNAL_Snapshot as Snapshot, proxy, useSnapshot } from "valtio";
import { type Option, SelectCardsModal } from "../SelectCardsModal";
......@@ -12,7 +10,7 @@ const defaultProps = {
const localStore = proxy(defaultProps);
export const SimpleSelectCardsModal: FC = () => {
export const SimpleSelectCardsModal: React.FC = () => {
const { isOpen, selectables } = useSnapshot(localStore);
return (
<SelectCardsModal
......@@ -28,7 +26,7 @@ export const SimpleSelectCardsModal: FC = () => {
finishable={false}
totalLevels={1}
overflow
onSubmit={(options) => rs(options)}
onSubmit={rs}
onFinish={() => rs([])}
onCancel={() => rs([])}
/>
......@@ -37,11 +35,14 @@ export const SimpleSelectCardsModal: FC = () => {
let rs: (options: Snapshot<Option[]>) => void = () => {};
export const displaySimpleSelectActionsModal = async (
export const displaySimpleSelectCardsModal = async (
args: Omit<typeof defaultProps, "isOpen">
) => {
localStore.isOpen = true;
localStore.selectables = args.selectables;
await new Promise<Snapshot<Option[]>>((resolve) => (rs = resolve)); // 等待在组件内resolve
const res = await new Promise<Snapshot<Option[]>>(
(resolve) => (rs = resolve)
); // 等待在组件内resolve
localStore.isOpen = false;
return res;
};
......@@ -8,5 +8,6 @@ export * from "./HintNotification";
export * from "./OptionModal";
export * from "./PositionModal";
export * from "./SelectActionsModal";
export * from "./SimpleSelectCardsModal";
export * from "./SortCardModal";
export * from "./YesNoModal";
import "./index.scss";
import classnames from "classnames";
import { type CSSProperties, type FC } from "react";
import { type CSSProperties } from "react";
import { type INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
import { sendSelectPlaceResponse, ygopro } from "@/api";
......@@ -31,7 +31,7 @@ const BgDisabledStyle = {
)`,
};
const BgExtraRow: FC<{
const BgExtraRow: React.FC<{
meSnap: Snapshot<BlockState[]>;
opSnap: Snapshot<BlockState[]>;
}> = ({ meSnap, opSnap }) => {
......@@ -60,7 +60,7 @@ const BgExtraRow: FC<{
);
};
const BgRow: FC<{
const BgRow: React.FC<{
isSzone?: boolean;
opponent?: boolean;
snap: Snapshot<BlockState[]>;
......@@ -82,7 +82,7 @@ const BgRow: FC<{
</div>
);
export const Bg: FC = () => {
export const Bg: React.FC = () => {
const snap = useSnapshot(placeStore.inner);
return (
<div className="mat-bg">
......@@ -106,7 +106,7 @@ const onBlockClick = (placeInteractivity: PlaceInteractivity) => {
}
};
const DecoTriangles: FC = () => (
const DecoTriangles: React.FC = () => (
<>
{Array.from({ length: 4 }).map((_, i) => (
<div className="triangle" key={i} />
......
import "./index.scss";
import {
DownloadOutlined,
UploadOutlined,
UpOutlined,
} from "@ant-design/icons";
import { animated, to, useSpring } from "@react-spring/web";
import { Button, Dropdown, type MenuProps } from "antd";
import { Dropdown, type MenuProps } from "antd";
import classnames from "classnames";
import React, { type CSSProperties, type FC, useEffect, useState } from "react";
import React, { type CSSProperties, useEffect, useState } from "react";
import { proxy, useSnapshot } from "valtio";
import { getCardStr, ygopro } from "@/api";
import { type CardMeta, fetchStrings, sendSelectIdleCmdResponse } from "@/api";
import { getCardStr, sendSelectIdleCmdResponse, ygopro } from "@/api";
import { useConfig } from "@/config";
import { eventbus, Task } from "@/infra";
import { cardStore, CardType, messageStore } from "@/stores";
import {
closeCardModal,
showCardModal as displayCardModal,
} from "@/ui/Duel/Message/CardModal";
import { cardStore, CardType, Interactivity, InteractType } from "@/stores";
import { showCardModal as displayCardModal } from "@/ui/Duel/Message/CardModal";
import { YgoCard } from "@/ui/Shared";
import { displayCardListModal, displayOptionModal } from "../../Message";
import {
displayCardListModal,
displayOptionModal,
displaySimpleSelectCardsModal,
} from "../../Message";
import { interactTypeToString } from "../../utils";
import {
attack,
......@@ -38,7 +33,7 @@ const NeosConfig = useConfig();
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE } =
ygopro.CardZone;
export const Card: FC<{ idx: number }> = React.memo(({ idx }) => {
export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
const state = cardStore.inner[idx];
const snap = useSnapshot(state);
......@@ -83,7 +78,7 @@ export const Card: FC<{ idx: number }> = React.memo(({ idx }) => {
}
};
// 这里后期应该去掉?
// 每张卡都需要移动到初始位置
useEffect(() => {
move(state.location.zone);
}, []);
......@@ -183,7 +178,7 @@ export const Card: FC<{ idx: number }> = React.memo(({ idx }) => {
overlayClassName="card-dropdown"
arrow
trigger={["click"]}
disabled={!highlight}
// disabled={!highlight}
>
<div className={classnames("card-img-wrap", { focus: classFocus })}>
<YgoCard
......@@ -203,7 +198,7 @@ const onCardClick = (card: CardType) => {
// TODO: 对方的卡片/未知的卡片,点击应该是没有效果的
// TODO: 同一张卡片,是否重复点击会关闭CardModal?
displayCardModal(card);
displaySingleCardDropdown(card);
handleDropdownMenu([card], false);
// 侧边栏展示超量素材信息
const overlayMaterials = cardStore.findOverlay(
......@@ -225,13 +220,9 @@ const onFieldClick = (card: CardType) => {
zone: card.location.zone,
controller: card.location.controller,
});
// TODO: 收集这个zone的所有交互,并且在下拉菜单之中显示
// 收集这个zone的所有交互,并且在下拉菜单之中显示
const cards = cardStore.at(card.location.zone, card.location.controller);
// 所有的交互进行聚类
const interactivities = cards.map((card) => card.idleInteractivities);
// 构建新的dropdownMenu
// 发动效果 -> 发动哪张卡?(卡选择) -> 效果选择
// 召唤、特殊召唤同理
handleDropdownMenu(cards, true);
};
// >>> 下拉菜单:点击动作 >>>
......@@ -246,35 +237,6 @@ type DropdownItem = NonNullable<MenuProps["items"]>[number] & {
};
const dropdownMenu = proxy({ value: [] as DropdownItem[] });
const displaySingleCardDropdown = (card: CardType) => {
const interactivies = card.idleInteractivities.map((interactivity) => ({
desc: interactTypeToString(interactivity.interactType),
response: interactivity.response,
effectCode: interactivity.activateIndex,
}));
const EFFECT_ACTIVATION_DESC = "发动效果";
const nonEffectInteractivies = interactivies.filter(
(item) => item.desc !== EFFECT_ACTIVATION_DESC
);
const effectInteractivies = interactivies.filter(
(item) => item.desc === EFFECT_ACTIVATION_DESC
);
dropdownMenu.value = nonEffectInteractivies.map(
({ desc, response }, key) => ({
key,
label: desc,
onClick: () => sendSelectIdleCmdResponse(response),
})
);
if (effectInteractivies.length) {
dropdownMenu.value.push({
key: dropdownMenu.value.length,
label: EFFECT_ACTIVATION_DESC,
onClick: () => handleEffectActivation(effectInteractivies, card.meta),
});
}
};
const handleEffectActivation = (
effectInteractivies: Interactivy[],
meta?: any // FIXME: meta的类型
......@@ -298,4 +260,95 @@ const handleEffectActivation = (
displayOptionModal(options); // 主动发动效果,所以不需要await,但是以后可能要留心
}
};
// 发动效果
// 1. 下拉菜单里面选择[召唤 / 特殊召唤 /.../效果发动]
// 2. 如果是非效果发动,那么直接选择哪张卡(单张卡直接选择那张)
// 3. 如果是效果发动,那么选择哪张卡,然后选择效果
const handleDropdownMenu = (cards: CardType[], isField: boolean) => {
console.log("here");
const map = new Map<Interactivity<number>["interactType"], CardType[]>();
cards.forEach((card) => {
card.idleInteractivities.forEach(({ interactType }) => {
if (!map.has(interactType)) {
map.set(interactType, []);
}
map.get(interactType)?.push(card);
});
});
const actions = [...map.entries()];
const nonEffectActions = actions.filter(
([action]) => action !== InteractType.ACTIVATE
);
const getNonEffectResponse = (action: InteractType, card: CardType) =>
card.idleInteractivities.find((item) => item.interactType === action)!
.response;
const nonEffectItem: DropdownItem[] = nonEffectActions.map(
([action, cards], key) => ({
key,
label: interactTypeToString(action),
onClick: async () => {
if (!isField) {
// 单卡: 直接召唤/特殊召唤/...
const card = cards[0];
sendSelectIdleCmdResponse(getNonEffectResponse(action, card));
} else {
// 场地: 选择卡片
const option = await displaySimpleSelectCardsModal({
selectables: cards.map((card) => ({
meta: card.meta,
location: card.location,
response: getNonEffectResponse(action, card),
})),
});
sendSelectIdleCmdResponse(option[0].response!);
}
},
})
);
const hasEffect =
cards.reduce(
(prev, acc) => [
...prev,
...acc.idleInteractivities.filter(
({ interactType }) => interactType === InteractType.ACTIVATE
),
],
[] as Interactivity<number>[]
).length > 0;
const effectItem: DropdownItem = {
key: nonEffectItem.length,
label: interactTypeToString(InteractType.ACTIVATE),
onClick: async () => {
let card: CardType;
if (!isField) {
// 单卡: 直接发动这个卡的效果
card = cards[0];
} else {
// 场地: 选择卡片
const option = await displaySimpleSelectCardsModal({
selectables: cards.map((card) => ({
meta: card.meta,
location: card.location,
card,
})),
});
card = option[0].card! as any; // 一定会有的,有输入则定有输出
}
// 选择发动哪个效果
handleEffectActivation(
card.idleInteractivities
.filter(({ interactType }) => interactType === InteractType.ACTIVATE)
.map((x) => ({
desc: interactTypeToString(x.interactType),
response: x.response,
effectCode: x.activateIndex,
})),
card.meta
);
},
};
dropdownMenu.value = nonEffectItem;
hasEffect && dropdownMenu.value.push(effectItem);
};
// <<< 下拉菜单 <<<
import "./index.scss";
import type { FC, PropsWithChildren } from "react";
import { useSnapshot } from "valtio";
import { cardStore } from "@/stores";
import { Bg } from "../Bg";
import { Card } from "../Card";
import { matConfig, toCssProperties } from "../utils";
// 后面再改名
export const Mat: FC = () => {
export const Mat: React.FC = () => {
const snap = useSnapshot(cardStore.inner);
return (
<section
......@@ -31,12 +29,12 @@ export const Mat: FC = () => {
);
};
const Plane: FC<PropsWithChildren> = ({ children }) => (
const Plane: React.FC<React.PropsWithChildren> = ({ children }) => (
<div id="camera">
<div id="plane">{children}</div>
</div>
);
const CardContainer: FC<PropsWithChildren> = ({ children }) => (
const CardContainer: React.FC<React.PropsWithChildren> = ({ children }) => (
<div className="mat-card-container">{children}</div>
);
import "./index.scss";
import classNames from "classnames";
import { CSSProperties, type FC, useMemo } from "react";
import { CSSProperties, useMemo } from "react";
import { useConfig } from "@/config";
......@@ -14,7 +14,7 @@ interface Props {
onClick?: () => void;
}
export const YgoCard: FC<Props> = (props) => {
export const YgoCard: React.FC<Props> = (props) => {
const {
className,
code = 0,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment