Commit e24f0887 authored by Chunchi Che's avatar Chunchi Che

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

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

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