Commit e24f0887 authored by Chunchi Che's avatar Chunchi Che

Merge branch 'optimize/ui/interaction' into 'main'

优化一些交互,玩家融合/超量/链接/同调召唤的时候可以通过点击场上的卡来完成操作

See merge request !368
parents 76de28e9 6d60d4ee
Pipeline #27008 passed with stages
in 10 minutes and 52 seconds
...@@ -10,7 +10,7 @@ export default (becomeTarget: ygopro.StocGameMessage.MsgBecomeTarget) => { ...@@ -10,7 +10,7 @@ export default (becomeTarget: ygopro.StocGameMessage.MsgBecomeTarget) => {
); );
if (target) { if (target) {
console.info(`${target.meta.text.name} become target`); console.info(`${target.meta.text.name} become target`);
target.selected = true; target.targeted = true;
} else { } else {
console.warn(`<BecomeTarget>target from ${location} is null`); console.warn(`<BecomeTarget>target from ${location} is null`);
} }
......
...@@ -23,6 +23,6 @@ export default (_chainEnd: ygopro.StocGameMessage.MsgChainEnd) => { ...@@ -23,6 +23,6 @@ export default (_chainEnd: ygopro.StocGameMessage.MsgChainEnd) => {
// //
// TODO: 这里每次都要全部遍历一遍,后续可以优化下 // TODO: 这里每次都要全部遍历一遍,后续可以优化下
for (const card of cardStore.inner) { for (const card of cardStore.inner) {
card.selected = false; card.targeted = false;
} }
}; };
...@@ -35,7 +35,11 @@ export default (field: MsgReloadField) => { ...@@ -35,7 +35,11 @@ export default (field: MsgReloadField) => {
idleInteractivities: [], idleInteractivities: [],
meta: { id: 0, data: {}, text: {} }, meta: { id: 0, data: {}, text: {} },
isToken: false, isToken: false,
selected: false, targeted: false,
selectInfo: {
selectable: false,
selected: false,
},
}), }),
), ),
) )
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { cardStore, matStore } from "@/stores";
import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal"; import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
import { fetchCheckCardMeta } from "../utils"; import { fetchCheckCardMeta } from "../utils";
type MsgSelectUnselectCard = ygopro.StocGameMessage.MsgSelectUnselectCard; type MsgSelectUnselectCard = ygopro.StocGameMessage.MsgSelectUnselectCard;
const { MZONE, SZONE, HAND } = ygopro.CardZone;
export default async ({ export default async ({
finishable, finishable,
...@@ -12,24 +14,58 @@ export default async ({ ...@@ -12,24 +14,58 @@ export default async ({
selectable_cards: selectableCards, selectable_cards: selectableCards,
selected_cards: selectedCards, selected_cards: selectedCards,
}: MsgSelectUnselectCard) => { }: MsgSelectUnselectCard) => {
const { if (
selecteds: selecteds1, selectableCards
mustSelects: mustSelect1, .concat(selectedCards)
selectables: selectable1, .find((info) => !isOnField(info.location)) === undefined
} = await fetchCheckCardMeta(selectableCards); ) {
const { // 所有可选卡和已选卡都是在场上或手牌
selecteds: selecteds2, // 通过让玩家点击场上的卡来进行选择
mustSelects: mustSelect2, for (const info of selectableCards) {
selectables: selectable2, const card = cardStore.find(info.location);
} = await fetchCheckCardMeta(selectedCards, true); if (card) {
await displaySelectActionsModal({ matStore.selectUnselectInfo.selectableList.push(info.location);
finishable, card.selectInfo.selectable = true;
cancelable, card.selectInfo.response = info.response;
min: min, }
max: max, }
single: true, for (const info of selectedCards) {
selecteds: [...selecteds1, ...selecteds2], const card = cardStore.find(info.location);
mustSelects: [...mustSelect1, ...mustSelect2], if (card) {
selectables: [...selectable1, ...selectable2], matStore.selectUnselectInfo.selectedList.push(info.location);
}); card.selectInfo.selected = true;
card.selectInfo.response = info.response;
}
}
matStore.selectUnselectInfo.finishable = finishable;
matStore.selectUnselectInfo.cancelable = cancelable;
} else {
// 有一些卡不在场上或手牌,因此无法通过点击卡片来选择
// 这里通过让玩家点击Modal中的卡来进行选择
const {
selecteds: selecteds1,
mustSelects: mustSelect1,
selectables: selectable1,
} = await fetchCheckCardMeta(selectableCards);
const {
selecteds: selecteds2,
mustSelects: mustSelect2,
selectables: selectable2,
} = await fetchCheckCardMeta(selectedCards, true);
await displaySelectActionsModal({
finishable,
cancelable,
min: min,
max: max,
single: true,
selecteds: [...selecteds1, ...selecteds2],
mustSelects: [...mustSelect1, ...mustSelect2],
selectables: [...selectable1, ...selectable2],
});
}
}; };
function isOnField(location: ygopro.CardLocation): boolean {
return [MZONE, SZONE, HAND].includes(location.zone);
}
...@@ -75,7 +75,11 @@ export default async (start: ygopro.StocGameMessage.MsgStart) => { ...@@ -75,7 +75,11 @@ export default async (start: ygopro.StocGameMessage.MsgStart) => {
text: {}, text: {},
}, },
isToken: !((i + 1) % 3), isToken: !((i + 1) % 3),
selected: false, targeted: false,
selectInfo: {
selectable: false,
selected: false,
},
}), }),
), ),
), ),
......
...@@ -16,7 +16,12 @@ export interface CardType { ...@@ -16,7 +16,12 @@ export interface CardType {
idleInteractivities: Interactivity<number>[]; // IDLE状态下的互动信息 idleInteractivities: Interactivity<number>[]; // IDLE状态下的互动信息
counters: { [type: number]: number }; // 指示器 counters: { [type: number]: number }; // 指示器
isToken: boolean; // 是否是token isToken: boolean; // 是否是token
selected: boolean; // 当前卡是否被选择成为效果的对象 targeted: boolean; // 当前卡是否被选择成为效果的对象
selectInfo: {
selectable: boolean; // 是否可以被选择
selected: boolean; // 是否已经被选择
response?: number; // 被选择时发送给服务器的值
};
} }
class CardStore implements NeosStore { class CardStore implements NeosStore {
......
...@@ -83,6 +83,12 @@ const initialState: Omit<MatState, "reset"> = { ...@@ -83,6 +83,12 @@ const initialState: Omit<MatState, "reset"> = {
}, },
}, },
tossResult: undefined, tossResult: undefined,
selectUnselectInfo: {
finishable: false,
cancelable: false,
selectableList: [],
selectedList: [],
},
chainSetting: ChainSetting.CHAIN_SMART, chainSetting: ChainSetting.CHAIN_SMART,
duelEnd: false, duelEnd: false,
// methods // methods
...@@ -101,6 +107,7 @@ class MatStore implements MatState, NeosStore { ...@@ -101,6 +107,7 @@ class MatStore implements MatState, NeosStore {
unimplemented = initialState.unimplemented; unimplemented = initialState.unimplemented;
handResults = initialState.handResults; handResults = initialState.handResults;
tossResult = initialState.tossResult; tossResult = initialState.tossResult;
selectUnselectInfo = initialState.selectUnselectInfo;
duelEnd = initialState.duelEnd; duelEnd = initialState.duelEnd;
// methods // methods
isMe = initialState.isMe; isMe = initialState.isMe;
...@@ -122,6 +129,13 @@ class MatStore implements MatState, NeosStore { ...@@ -122,6 +129,13 @@ class MatStore implements MatState, NeosStore {
this.unimplemented = 0; this.unimplemented = 0;
this.handResults.me = 0; this.handResults.me = 0;
this.handResults.op = 0; this.handResults.op = 0;
this.tossResult = undefined;
this.selectUnselectInfo = {
finishable: false,
cancelable: false,
selectableList: [],
selectedList: [],
};
this.duelEnd = false; this.duelEnd = false;
} }
} }
......
...@@ -34,6 +34,13 @@ export interface MatState { ...@@ -34,6 +34,13 @@ export interface MatState {
tossResult?: string; // 骰子/硬币结果 tossResult?: string; // 骰子/硬币结果
selectUnselectInfo: {
finishable: boolean; // 是否可以完成选择
cancelable: boolean; // 是否可以取消当前选择
selectableList: ygopro.CardLocation[]; // 记录当前可以选择的卡列表
selectedList: ygopro.CardLocation[]; // 记录当前已经选择的卡列表
};
handResults: BothSide<HandResult> & { handResults: BothSide<HandResult> & {
set: (controller: number, result: HandResult) => void; set: (controller: number, result: HandResult) => void;
}; // 猜拳结果 }; // 猜拳结果
......
...@@ -102,8 +102,7 @@ ...@@ -102,8 +102,7 @@
} }
} }
.mat-card.glowing .shadow { .frame {
--shadow-color: #0099ff;
display: block !important; display: block !important;
background: var(--shadow-color) !important; background: var(--shadow-color) !important;
border-radius: 5px; border-radius: 5px;
...@@ -111,6 +110,16 @@ ...@@ -111,6 +110,16 @@
transform: translateZ(calc((var(--z)) * 1px + 0.1px)); transform: translateZ(calc((var(--z)) * 1px + 0.1px));
} }
.mat-card.glowing .shadow {
--shadow-color: #0099ff;
@extend .frame;
}
.mat-card.shining {
--shadow-color: #f4ff00;
@extend .frame;
}
@keyframes focus { @keyframes focus {
0% { 0% {
filter: brightness(1) contrast(1); filter: brightness(1) contrast(1);
......
...@@ -4,7 +4,7 @@ import classnames from "classnames"; ...@@ -4,7 +4,7 @@ import classnames from "classnames";
import React, { type CSSProperties, useEffect, useRef, useState } from "react"; import React, { type CSSProperties, useEffect, useRef, useState } from "react";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { type CardMeta, Region } from "@/api"; import { type CardMeta, Region, sendSelectMultiResponse } from "@/api";
import { import {
fetchStrings, fetchStrings,
getCardStr, getCardStr,
...@@ -21,7 +21,12 @@ import { ...@@ -21,7 +21,12 @@ import {
displayOptionModal, displayOptionModal,
displaySimpleSelectCardsModal, displaySimpleSelectCardsModal,
} from "../../Message"; } from "../../Message";
import { interactTypeToIcon, interactTypeToString } from "../../utils"; import {
clearAllIdleInteractivities,
clearSelectInfo,
interactTypeToIcon,
interactTypeToString,
} from "../../utils";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import { import {
attack, attack,
...@@ -157,6 +162,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => { ...@@ -157,6 +162,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
// 单卡: 直接召唤/特殊召唤/... // 单卡: 直接召唤/特殊召唤/...
const card = cards[0]; const card = cards[0];
sendSelectIdleCmdResponse(getNonEffectResponse(action, card)); sendSelectIdleCmdResponse(getNonEffectResponse(action, card));
clearAllIdleInteractivities();
} else { } else {
// 场地: 选择卡片 // 场地: 选择卡片
// TODO: hint // TODO: hint
...@@ -167,7 +173,10 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => { ...@@ -167,7 +173,10 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
response: getNonEffectResponse(action, card), response: getNonEffectResponse(action, card),
})), })),
}); });
sendSelectIdleCmdResponse(option[0].response!); if (option.length > 0) {
sendSelectIdleCmdResponse(option[0].response!);
clearAllIdleInteractivities();
}
} }
}, },
}), }),
...@@ -234,6 +243,16 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => { ...@@ -234,6 +243,16 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
const onClick = () => { const onClick = () => {
const onCardClick = (card: CardType) => { const onCardClick = (card: CardType) => {
const selectInfo = card.selectInfo;
if (selectInfo.selectable || selectInfo.selected) {
if (selectInfo.response !== undefined) {
sendSelectMultiResponse([selectInfo.response]);
clearSelectInfo();
} else {
console.error("card is selectable but the response is undefined!");
}
}
// 中央弹窗展示选中卡牌信息 // 中央弹窗展示选中卡牌信息
// TODO: 同一张卡片,是否重复点击会关闭CardModal? // TODO: 同一张卡片,是否重复点击会关闭CardModal?
displayCardModal(card); displayCardModal(card);
...@@ -274,7 +293,11 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => { ...@@ -274,7 +293,11 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
return ( return (
<animated.div <animated.div
className={classnames(styles["mat-card"], { [styles.glowing]: glowing })} className={classnames(styles["mat-card"], {
/* 有可操作选项或者已被选中*/
[styles.glowing]: glowing || snap.selectInfo.selected,
[styles.shining]: snap.selectInfo.selectable, // 可以被选中
})}
style={ style={
{ {
transform: to( transform: to(
...@@ -313,13 +336,12 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => { ...@@ -313,13 +336,12 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
> >
<YgoCard <YgoCard
className={styles.cover} className={styles.cover}
// cardName={snap.meta.text.name}
code={snap.code === 0 ? snap.meta.id : snap.code} code={snap.code === 0 ? snap.meta.id : snap.code}
/> />
<YgoCard className={styles.back} isBack /> <YgoCard className={styles.back} isBack />
</div> </div>
</Dropdown> </Dropdown>
{snap.selected ? <div className={styles.streamer} /> : <></>} {snap.targeted ? <div className={styles.streamer} /> : <></>}
</animated.div> </animated.div>
); );
}); });
......
...@@ -15,3 +15,13 @@ ...@@ -15,3 +15,13 @@
:global(.ant-dropdown-menu-item) { :global(.ant-dropdown-menu-item) {
gap: 0.5rem; gap: 0.5rem;
} }
.select-manager {
.btn {
font-size: 0.9rem;
}
.cancle {
color: red;
}
}
...@@ -2,7 +2,6 @@ import { ...@@ -2,7 +2,6 @@ import {
ArrowRightOutlined, ArrowRightOutlined,
CheckOutlined, CheckOutlined,
CloseCircleFilled, CloseCircleFilled,
LogoutOutlined,
MessageFilled, MessageFilled,
StepForwardFilled, StepForwardFilled,
} from "@ant-design/icons"; } from "@ant-design/icons";
...@@ -16,29 +15,28 @@ import { ...@@ -16,29 +15,28 @@ import {
theme, theme,
Tooltip, Tooltip,
} from "antd"; } from "antd";
import classNames from "classnames";
import { cloneElement, useEffect, useState } from "react"; import { cloneElement, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { import {
sendSelectBattleCmdResponse, sendSelectBattleCmdResponse,
sendSelectIdleCmdResponse, sendSelectIdleCmdResponse,
sendSelectSingleResponse,
sendSurrender, sendSurrender,
ygopro, ygopro,
} from "@/api"; } from "@/api";
import { cardStore, ChainSetting, matStore } from "@/stores"; import { ChainSetting, matStore } from "@/stores";
import { IconFont } from "@/ui/Shared"; import { IconFont } from "@/ui/Shared";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType; import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType;
import { clearAllIdleInteractivities, clearSelectInfo } from "../../utils";
import { openChatBox } from "../ChatBox"; import { openChatBox } from "../ChatBox";
const { useToken } = theme; const { useToken } = theme;
const clearAllIdleInteractivities = () => {
for (const card of cardStore.inner) { const FINISH_CANCEL_RESPONSE = -1;
card.idleInteractivities = [];
}
};
// PhaseType, 中文, response, 是否显示,是否禁用 // PhaseType, 中文, response, 是否显示,是否禁用
const initialPhaseBind: [ const initialPhaseBind: [
...@@ -72,8 +70,6 @@ export const Menu = () => { ...@@ -72,8 +70,6 @@ export const Menu = () => {
[], [],
); );
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
const endResponse = [ const endResponse = [
PhaseType.BATTLE_START, PhaseType.BATTLE_START,
...@@ -154,10 +150,9 @@ export const Menu = () => { ...@@ -154,10 +150,9 @@ export const Menu = () => {
const globalDisable = !matStore.isMe(currentPlayer); const globalDisable = !matStore.isMe(currentPlayer);
const onExit = () => navigate("/match");
return ( return (
<div className={styles["menu-container"]}> <div className={styles["menu-container"]}>
<SelectManager />
<DropdownWithTitle <DropdownWithTitle
title="请选择要进入的阶段" title="请选择要进入的阶段"
menu={{ items: phaseSwitchItems }} menu={{ items: phaseSwitchItems }}
...@@ -194,13 +189,6 @@ export const Menu = () => { ...@@ -194,13 +189,6 @@ export const Menu = () => {
> >
<Button icon={<CloseCircleFilled />} type="text"></Button> <Button icon={<CloseCircleFilled />} type="text"></Button>
</DropdownWithTitle> </DropdownWithTitle>
<Tooltip title="退出页面">
<Button
icon={<LogoutOutlined style={{ color: "red" }} />}
type="text"
onClick={onExit}
></Button>
</Tooltip>
</div> </div>
); );
}; };
...@@ -256,3 +244,22 @@ const ChainIcon: React.FC<{ chainSetting: ChainSetting }> = ({ ...@@ -256,3 +244,22 @@ const ChainIcon: React.FC<{ chainSetting: ChainSetting }> = ({
return <IconFont type="icon-chain-broken" />; return <IconFont type="icon-chain-broken" />;
} }
}; };
const SelectManager: React.FC = () => {
const { finishable, cancelable } = useSnapshot(matStore.selectUnselectInfo);
const onFinishOrCancel = () => {
sendSelectSingleResponse(FINISH_CANCEL_RESPONSE);
clearSelectInfo();
};
return (
<div className={styles["select-manager"]}>
<Button
className={classNames(styles.btn, { [styles.cancle]: cancelable })}
disabled={!cancelable && !finishable}
onClick={onFinishOrCancel}
>
{finishable ? "完成选择" : "取消选择"}
</Button>
</div>
);
};
import { cardStore } from "@/stores";
export function clearAllIdleInteractivities() {
for (const card of cardStore.inner) {
card.idleInteractivities = [];
}
}
import { cardStore, matStore } from "@/stores";
export function clearSelectInfo() {
const selectUnselectInfo = matStore.selectUnselectInfo;
for (const location of selectUnselectInfo.selectableList) {
const card = cardStore.find(location);
if (card) {
card.selectInfo.selectable = false;
card.selectInfo.response = undefined;
}
}
for (const location of selectUnselectInfo.selectedList) {
const card = cardStore.find(location);
if (card) {
card.selectInfo.selected = false;
}
}
matStore.selectUnselectInfo.finishable = false;
matStore.selectUnselectInfo.cancelable = false;
matStore.selectUnselectInfo.selectableList = [];
matStore.selectUnselectInfo.selectedList = [];
}
export * from "./clearAllIdleInteractivities";
export * from "./clearSelectInfo";
export * from "./groupBy"; export * from "./groupBy";
export * from "./interactTypeToStringIcon"; export * from "./interactTypeToStringIcon";
export * from "./zip"; export * from "./zip";
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