Commit 04ca75ed authored by xiaoye's avatar xiaoye

Merge remote-tracking branch 'origin/main' into jwyxym/xiaoye-server

parents b63b4dbd 736537f0
Pipeline #42403 failed with stages
in 2 minutes and 36 seconds
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
"uuid": "^9.0.0", "uuid": "^9.0.0",
"valtio": "^1.11.2", "valtio": "^1.11.2",
"vite-plugin-svgr": "^3.2.0", "vite-plugin-svgr": "^3.2.0",
"ygopro-deck-encode": "^1.0.3" "ygopro-deck-encode": "^1.0.14"
}, },
"devDependencies": { "devDependencies": {
"@types/google-protobuf": "^3.15.6", "@types/google-protobuf": "^3.15.6",
...@@ -4631,11 +4631,6 @@ ...@@ -4631,11 +4631,6 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true "dev": true
}, },
"node_modules/js-base64": {
"version": "3.7.5",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz",
"integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA=="
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
...@@ -7243,12 +7238,9 @@ ...@@ -7243,12 +7238,9 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}, },
"node_modules/ygopro-deck-encode": { "node_modules/ygopro-deck-encode": {
"version": "1.0.3", "version": "1.0.14",
"resolved": "https://registry.npmjs.org/ygopro-deck-encode/-/ygopro-deck-encode-1.0.3.tgz", "resolved": "https://registry.npmjs.org/ygopro-deck-encode/-/ygopro-deck-encode-1.0.14.tgz",
"integrity": "sha512-5JpaAdrIO4d2VnQRm+6T9S3JEp8ByXrFmXumAtCScvGYSc/31ianjafe76XVIFA8nSDJJCO8bzJF+C6CBwUONg==", "integrity": "sha512-Q64f8U+okLBDKHw02eRYsdDMpALhYa55k0BhFqZ5k4ntRpPKFNvM9sNEbBlg2bmyi6LCf3rlEmISmLtlx9uDeA=="
"dependencies": {
"js-base64": "^3.7.5"
}
}, },
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
...@@ -10395,11 +10387,6 @@ ...@@ -10395,11 +10387,6 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true "dev": true
}, },
"js-base64": {
"version": "3.7.5",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz",
"integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA=="
},
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
...@@ -12160,12 +12147,9 @@ ...@@ -12160,12 +12147,9 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}, },
"ygopro-deck-encode": { "ygopro-deck-encode": {
"version": "1.0.3", "version": "1.0.14",
"resolved": "https://registry.npmjs.org/ygopro-deck-encode/-/ygopro-deck-encode-1.0.3.tgz", "resolved": "https://registry.npmjs.org/ygopro-deck-encode/-/ygopro-deck-encode-1.0.14.tgz",
"integrity": "sha512-5JpaAdrIO4d2VnQRm+6T9S3JEp8ByXrFmXumAtCScvGYSc/31ianjafe76XVIFA8nSDJJCO8bzJF+C6CBwUONg==", "integrity": "sha512-Q64f8U+okLBDKHw02eRYsdDMpALhYa55k0BhFqZ5k4ntRpPKFNvM9sNEbBlg2bmyi6LCf3rlEmISmLtlx9uDeA=="
"requires": {
"js-base64": "^3.7.5"
}
}, },
"yocto-queue": { "yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
"uuid": "^9.0.0", "uuid": "^9.0.0",
"valtio": "^1.11.2", "valtio": "^1.11.2",
"vite-plugin-svgr": "^3.2.0", "vite-plugin-svgr": "^3.2.0",
"ygopro-deck-encode": "^1.0.3" "ygopro-deck-encode": "^1.0.14"
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
......
# Neos 项目介绍
## 项目简介
Neos 是一个 **Web 版游戏王对战平台**,目标是让玩家无需下载客户端,直接在浏览器中进行游戏王卡牌对战。
核心功能:
- 竞技匹配(MyCard 天梯)
- 娱乐匹配
- MC 观战列表
- 单人模式(AI 对战)
- 自定义房间
- 录像回放
兼容萌卡社区的 [srvpro](https://github.com/mycard/srvpro) 服务器,可与 ygopro 客户端联机。
## 技术架构
```
┌─────────────────────────────────────────────────────────────┐
│ UI 模块 │
│ React 18 + Ant Design + React Router │
├─────────────────────────────────────────────────────────────┤
│ Service 模块 │
│ 业务逻辑(决斗事件处理) │
├─────────────────────────────────────────────────────────────┤
│ MiddleWare 模块 │
│ WebSocket 长连接处理 │
├─────────────────────────────────────────────────────────────┤
│ Adapter 模块 │
│ ygopro 协议 (二进制 ↔ TypeScript) │
├─────────────────────────────────────────────────────────────┤
│ API 模块 │
│ HTTP 请求(登录、卡片数据等) │
├─────────────────────────────────────────────────────────────┤
│ Store 模块 │
│ 全局状态管理 (Valtio) │
└─────────────────────────────────────────────────────────────┘
```
技术栈:
- **前端框架**: React 18 + TypeScript
- **状态管理**: Valtio
- **路由**: React Router 6
- **UI 组件**: Ant Design 5
- **构建工具**: Vite
- **数据库**: sql.js (WebAssembly 版 SQLite,用于卡片数据)
- **协议**: Google Protobuf (与服务器通信)
- **动画**: React Spring
## 目录结构
```
src/
├── api/ # API 接口(HTTP 请求、卡片数据、mdproDeck 等)
├── config/ # 配置文件
├── container/ # 依赖注入容器
├── hook/ # React Hooks
├── infra/ # 基础设施(buffer、stream、eventbus 等)
├── middleware/ # 中间件(WebSocket、SQLite)
├── service/ # 业务逻辑
│ ├── duel/ # 决斗相关事件处理(抽卡、召唤、攻击、连锁等)
│ ├── room/ # 房间相关
│ └── ...
├── stores/ # 状态管理
├── styles/ # 全局样式
├── types/ # TypeScript 类型定义
└── ui/ # UI 组件
├── BuildDeck/ # 卡组构建
├── Duel/ # 决斗界面
├── Match/ # 匹配界面
├── Shared/ # 共享组件
└── ...
```
## 部署
- https://neos.moecube.com (萌卡社区)
- https://www.neos.moe (Cloudflare)
## 相关链接
- [GitLab 仓库](https://code.mycard.moe/mycard/Neos)
- [项目文档](https://doc.neos.moe)
- [萌卡社区](https://mycard.moe/)
...@@ -5,13 +5,23 @@ import { MdproResp } from "./schema"; ...@@ -5,13 +5,23 @@ import { MdproResp } from "./schema";
import { mdproHeaders } from "./util"; import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/sync/single"; const API_PATH = "api/mdpro3/sync/single";
interface DeleteReq { interface DeleteReq {
userId: number; userId: number;
deckContributor: string;
deck: { deck: {
deckId: string; deckId: string;
deckName: string;
deckType: string;
deckCoverCard1: number;
deckCoverCard2: number;
deckCoverCard3: number;
deckCase: number;
deckProtector: number;
deckYdk: string;
isDelete: boolean; isDelete: boolean;
timestamp: number;
}; };
} }
...@@ -19,6 +29,7 @@ export async function deleteDeck( ...@@ -19,6 +29,7 @@ export async function deleteDeck(
userID: number, userID: number,
token: string, token: string,
deckID: string, deckID: string,
deckContributor: string = "", // Added parameter with default
): Promise<MdproResp<boolean> | undefined> { ): Promise<MdproResp<boolean> | undefined> {
const myHeaders = mdproHeaders(); const myHeaders = mdproHeaders();
myHeaders.append("Content-Type", "application/json"); myHeaders.append("Content-Type", "application/json");
...@@ -26,9 +37,19 @@ export async function deleteDeck( ...@@ -26,9 +37,19 @@ export async function deleteDeck(
const req: DeleteReq = { const req: DeleteReq = {
userId: userID, userId: userID,
deckContributor: deckContributor,
deck: { deck: {
deckId: deckID, deckId: deckID,
deckName: "", // Required but not used for delete
deckType: "", // Required field
deckCoverCard1: 0, // Required field
deckCoverCard2: 0, // Required field
deckCoverCard3: 0, // Required field
deckCase: 0, // Required field
deckProtector: 0, // Required field
deckYdk: "", // Required but not used for delete
isDelete: true, isDelete: true,
timestamp: Date.now(),
}, },
}; };
......
...@@ -6,7 +6,7 @@ import { mdproHeaders } from "./util"; ...@@ -6,7 +6,7 @@ import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/deck/deckId"; const API_PATH = "api/mdpro3/deck/deckId";
export async function generateDeck(): Promise<MdproResp<string> | undefined> { export async function generateDeck(): Promise<MdproResp<string> | undefined> {
const myHeaders = mdproHeaders(); const myHeaders = mdproHeaders();
......
...@@ -6,7 +6,7 @@ import { mdproHeaders } from "./util"; ...@@ -6,7 +6,7 @@ import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/deck"; const API_PATH = "api/mdpro3/deck";
export async function mgetDeck( export async function mgetDeck(
id: string, id: string,
......
...@@ -6,7 +6,7 @@ import { MdproDeck, MdproResp } from "./schema"; ...@@ -6,7 +6,7 @@ import { MdproDeck, MdproResp } from "./schema";
import { mdproHeaders } from "./util"; import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/sync/"; const API_PATH = "api/mdpro3/sync";
export interface PersonalListReq { export interface PersonalListReq {
/* ID of MyCard Account */ /* ID of MyCard Account */
......
...@@ -19,13 +19,19 @@ export interface MdproDeck { ...@@ -19,13 +19,19 @@ export interface MdproDeck {
deckName: string; deckName: string;
deckRank?: number; deckRank?: number;
deckLike?: number; deckLike?: number;
deckUploadDate?: string; deckUploadDate?: number; // Changed to number (13-digit timestamp)
deckUpdateDate?: string; deckUpdateDate?: number; // Changed to number (13-digit timestamp)
deckCoverCard1?: number; // NEW: Cover card 1 ID
deckCoverCard2?: number; // NEW: Cover card 2 ID
deckCoverCard3?: number; // NEW: Cover card 3 ID
/* Content of the deck. */ /* Content of the deck. */
deckYdk?: string; deckYdk?: string;
deckCase: number; deckCase: number;
deckProtector?: number; // NEW: Card protector/sleeve
/* User ID of MyCard Account */ /* User ID of MyCard Account */
userId: number; userId: number;
isDelete?: boolean; // NEW: Whether deck is deleted
isPublic?: boolean; // NEW: Whether deck is public
} }
export interface MdproDeckLike { export interface MdproDeckLike {
...@@ -33,6 +39,10 @@ export interface MdproDeckLike { ...@@ -33,6 +39,10 @@ export interface MdproDeckLike {
deckContributor: string; deckContributor: string;
deckName: string; deckName: string;
deckLike?: number; deckLike?: number;
deckCoverCard1?: number; // NEW: Cover card 1 ID
deckCoverCard2?: number; // NEW: Cover card 2 ID
deckCoverCard3?: number; // NEW: Cover card 3 ID
deckCase: number; deckCase: number;
lastDate?: string; deckProtector?: number; // NEW: Card protector/sleeve
lastDate?: number; // Changed to number (timestamp)
} }
...@@ -5,7 +5,7 @@ import { MdproResp } from "./schema"; ...@@ -5,7 +5,7 @@ import { MdproResp } from "./schema";
import { mdproHeaders } from "./util"; import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/sync/single"; const API_PATH = "api/mdpro3/sync/single";
export interface SyncReq { export interface SyncReq {
userId: number; userId: number;
...@@ -13,8 +13,15 @@ export interface SyncReq { ...@@ -13,8 +13,15 @@ export interface SyncReq {
deck: { deck: {
deckId: string; deckId: string;
deckName: string; deckName: string;
deckType?: string; // NEW: Deck type/category (optional, can use empty string)
deckCoverCard1?: number; // NEW: Cover card 1
deckCoverCard2?: number; // NEW: Cover card 2
deckCoverCard3?: number; // NEW: Cover card 3
deckCase: number; deckCase: number;
deckProtector?: number; // NEW: Card protector/sleeve
deckYdk: string; deckYdk: string;
isDelete?: boolean; // NEW: Whether this is a delete operation
timestamp?: number; // NEW: Update timestamp (10 or 13 digit)
}; };
} }
......
...@@ -5,7 +5,7 @@ import { MdproResp } from "./schema"; ...@@ -5,7 +5,7 @@ import { MdproResp } from "./schema";
import { mdproHeaders } from "./util"; import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/deck/public"; const API_PATH = "api/mdpro3/deck/public";
export interface UpdatePublicReq { export interface UpdatePublicReq {
userId: number; userId: number;
......
...@@ -32,8 +32,15 @@ export async function uploadDeck( ...@@ -32,8 +32,15 @@ export async function uploadDeck(
deck: { deck: {
deckId, deckId,
deckName: req.deck.deckName, deckName: req.deck.deckName,
deckType: "", // NEW: Default empty string for deck type
deckCoverCard1: 0, // NEW: Default to 0
deckCoverCard2: 0, // NEW: Default to 0
deckCoverCard3: 0, // NEW: Default to 0
deckCase: req.deck.deckCase, deckCase: req.deck.deckCase,
deckProtector: 0, // NEW: Default to 0
deckYdk: req.deck.deckYdk, deckYdk: req.deck.deckYdk,
isDelete: false, // NEW: Not a delete operation
timestamp: Date.now(), // NEW: Current timestamp
}, },
}, },
req.token, req.token,
......
...@@ -78,3 +78,5 @@ export const MSG_HAND_RES = 133; ...@@ -78,3 +78,5 @@ export const MSG_HAND_RES = 133;
export const MSG_SHUFFLE_HAND = 33; export const MSG_SHUFFLE_HAND = 33;
export const MSG_SHUFFLE_EXTRA = 39; export const MSG_SHUFFLE_EXTRA = 39;
export const MSG_SIBYL_NAME = 235; export const MSG_SIBYL_NAME = 235;
export const MSG_CONFIRM_CARDS = 30;
export const MSG_CONFIRM_DECKTOP = 31;
import { ygopro } from "../../../idl/ocgcore";
import { BufferReaderExt } from "../../bufferIO";
import MsgConfirmCards = ygopro.StocGameMessage.MsgConfirmCards;
/*
* Msg Confirm Cards
*
* @usage - 确认卡片(展示手牌、确认盖卡等)
* */
export default (data: Uint8Array) => {
const reader = new BufferReaderExt(data);
const player = reader.inner.readUint8();
// 新协议在 player 和 count 之间增加了一个字节,用途暂不明确。
// 参考 C# 实现:
// if (condition != Condition.Replay || CurrentReplayUseYRP2)
// reader.ReadByte();
// C# 中在非回放模式或使用 YRP2 格式回放时会跳过这个字节。
// 如果后续需要支持旧版回放文件,可能需要加条件判断。
reader.inner.readUint8();
const count = reader.inner.readUint8();
const cards: ygopro.CardInfo[] = [];
for (let i = 0; i < count; i++) {
cards.push(reader.readCardInfo());
}
return new MsgConfirmCards({
player,
cards,
});
};
...@@ -12,6 +12,7 @@ import MsgAnnounceCard from "./announceCard"; ...@@ -12,6 +12,7 @@ import MsgAnnounceCard from "./announceCard";
import MsgAnnounceNumber from "./announceNumber"; import MsgAnnounceNumber from "./announceNumber";
import MsgAnnounceRace from "./announceRace"; import MsgAnnounceRace from "./announceRace";
import MsgAttack from "./attack"; import MsgAttack from "./attack";
import MsgConfirmCardsAdapter from "./confirmCards";
import MsgDamage from "./damage"; import MsgDamage from "./damage";
import MsgDrawAdapter from "./draw"; import MsgDrawAdapter from "./draw";
import MsgFieldDisabledAdapter from "./fieldDisabled"; import MsgFieldDisabledAdapter from "./fieldDisabled";
...@@ -245,6 +246,11 @@ export default class GameMsgAdapter implements StocAdapter { ...@@ -245,6 +246,11 @@ export default class GameMsgAdapter implements StocAdapter {
gameMsg.sibyl_name = MsgSibylNameAdapter(gameData); gameMsg.sibyl_name = MsgSibylNameAdapter(gameData);
break; break;
} }
case GAME_MSG.MSG_CONFIRM_CARDS:
case GAME_MSG.MSG_CONFIRM_DECKTOP: {
gameMsg.confirm_cards = MsgConfirmCardsAdapter(gameData);
break;
}
default: { default: {
gameMsg.unimplemented = new ygopro.StocGameMessage.MsgUnimplemented({ gameMsg.unimplemented = new ygopro.StocGameMessage.MsgUnimplemented({
command: func, command: func,
......
...@@ -161,34 +161,7 @@ ...@@ -161,34 +161,7 @@
} }
] ]
}, },
"30": {
"protoType": "confirm_cards",
"fields": [
{
"fieldName": "player",
"fieldType": "uint8"
},
{
"fieldName": "cards",
"fieldType": "repeated",
"repeatedType": "CardInfo"
}
]
},
"31": {
"protoType": "confirm_cards",
"fields": [
{
"fieldName": "player",
"fieldType": "uint8"
},
{
"fieldName": "cards",
"fieldType": "repeated",
"repeatedType": "CardInfo"
}
]
},
"83": { "83": {
"protoType": "become_target", "protoType": "become_target",
"fields": [ "fields": [
......
...@@ -32,7 +32,6 @@ const MsgConstructorMap: Map<string, Constructor> = new Map([ ...@@ -32,7 +32,6 @@ const MsgConstructorMap: Map<string, Constructor> = new Map([
["chain_solved", ygopro.StocGameMessage.MsgChainSolved], ["chain_solved", ygopro.StocGameMessage.MsgChainSolved],
["chain_end", ygopro.StocGameMessage.MsgChainEnd], ["chain_end", ygopro.StocGameMessage.MsgChainEnd],
["lp_update", ygopro.StocGameMessage.MsgLpUpdate], ["lp_update", ygopro.StocGameMessage.MsgLpUpdate],
["confirm_cards", ygopro.StocGameMessage.MsgConfirmCards],
["become_target", ygopro.StocGameMessage.MsgBecomeTarget], ["become_target", ygopro.StocGameMessage.MsgBecomeTarget],
["shuffle_deck", ygopro.StocGameMessage.MsgShuffleDeck], ["shuffle_deck", ygopro.StocGameMessage.MsgShuffleDeck],
["rock_paper_scissors", ygopro.StocGameMessage.MsgRockPaperScissors], ["rock_paper_scissors", ygopro.StocGameMessage.MsgRockPaperScissors],
......
...@@ -17,35 +17,41 @@ export default (data: Uint8Array) => { ...@@ -17,35 +17,41 @@ export default (data: Uint8Array) => {
const player = reader.inner.readUint8(); const player = reader.inner.readUint8();
const count = reader.inner.readUint8(); const count = reader.inner.readUint8();
const spCount = reader.inner.readUint8(); const spCount = reader.inner.readUint8();
const forced = reader.inner.readUint8() !== 0;
const hint0 = reader.inner.readUint32(); const hint0 = reader.inner.readUint32();
const hint1 = reader.inner.readUint32(); const hint1 = reader.inner.readUint32();
const msg = new MsgSelectChain({ const msg = new MsgSelectChain({
player, player,
special_count: spCount, special_count: spCount,
forced, forced: false,
hint0, hint0,
hint1, hint1,
chains: [], chains: [],
}); });
let forceCount = 0;
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const flag = reader.inner.readUint8(); const flag = reader.inner.readUint8();
const code = reader.inner.readUint32(); const forced = reader.inner.readUint8();
forceCount += forced;
const code = reader.inner.readUint32() % 1000000000;
const location = reader.readCardLocation(); const location = reader.readCardLocation();
const effect_desc = reader.inner.readUint32(); const effect_desc = reader.inner.readUint32();
msg.chains.push( const chain = new MsgSelectChain.Chain({
new MsgSelectChain.Chain({ flag: numberToChainFlag(flag),
flag: numberToChainFlag(flag), code,
code, location,
location, effect_description: effect_desc,
effect_description: effect_desc, response: i,
response: i, });
}), // 由于 protobuf 定义中 Chain 没有 forced 字段,使用类型扩展
); (chain as any).forced = forced > 0;
msg.chains.push(chain);
} }
msg.forced = forceCount > 0;
return msg; return msg;
}; };
...@@ -34,12 +34,23 @@ export default (data: Uint8Array) => { ...@@ -34,12 +34,23 @@ export default (data: Uint8Array) => {
const location = reader.readCardShortLocation(); const location = reader.readCardShortLocation();
const para = reader.inner.readInt32(); const para = reader.inner.readInt32();
let level1 = para & 0xffff;
let level2 = para >> 16;
// 检查 0x80000000 标志位
if ((para & 0x80000000) !== 0) {
level1 = para & 0x7fffffff;
level2 = level1;
}
if (level2 === 0) {
level2 = level1;
}
msg.must_select_cards.push( msg.must_select_cards.push(
new MsgSelectSum.Info({ new MsgSelectSum.Info({
code, code,
location, location,
level1: para & 0xffff, level1,
level2: para >> 16, level2,
response: i, response: i,
}), }),
); );
...@@ -50,8 +61,17 @@ export default (data: Uint8Array) => { ...@@ -50,8 +61,17 @@ export default (data: Uint8Array) => {
const code = reader.inner.readInt32(); const code = reader.inner.readInt32();
const location = reader.readCardShortLocation(); const location = reader.readCardShortLocation();
const para = reader.inner.readInt32(); const para = reader.inner.readInt32();
const level1 = para & 0xffff;
const level2 = para >> 16 > 0 ? para >> 16 : level1; let level1 = para & 0xffff;
let level2 = para >> 16;
// 检查 0x80000000 标志位
if ((para & 0x80000000) !== 0) {
level1 = para & 0x7fffffff;
level2 = level1;
}
if (level2 === 0) {
level2 = level1;
}
msg.selectable_cards.push( msg.selectable_cards.push(
new MsgSelectSum.Info({ new MsgSelectSum.Info({
......
...@@ -10,20 +10,22 @@ export default async (container: Container, selectChain: MsgSelectChain) => { ...@@ -10,20 +10,22 @@ export default async (container: Container, selectChain: MsgSelectChain) => {
const conn = container.conn; const conn = container.conn;
const context = container.context; const context = container.context;
const spCount = selectChain.special_count; const spCount = selectChain.special_count;
const forced = selectChain.forced;
const _hint0 = selectChain.hint0; const _hint0 = selectChain.hint0;
const _hint1 = selectChain.hint1; const _hint1 = selectChain.hint1;
const chains = selectChain.chains; const chains = selectChain.chains;
const chainSetting = context.matStore.chainSetting; const chainSetting = context.matStore.chainSetting;
if (chainSetting === ChainSetting.CHAIN_IGNORE) { // 计算强制发动的卡片数量
// 如果玩家配置了忽略连锁,直接回应后端并返回 const forceCount = chains.filter((chain) => (chain as any).forced).length;
if (chainSetting === ChainSetting.CHAIN_IGNORE && forceCount === 0) {
// 如果玩家配置了忽略连锁,且没有强制发动的卡,直接回应后端并返回
sendSelectSingleResponse(conn, -1); sendSelectSingleResponse(conn, -1);
return; return;
} }
let handle_flag = 0; let handle_flag = 0;
if (!forced) { if (forceCount === 0) {
// 无强制发动的卡 // 无强制发动的卡
if (spCount === 0) { if (spCount === 0) {
// 无关键卡 // 无关键卡
...@@ -32,8 +34,12 @@ export default async (container: Container, selectChain: MsgSelectChain) => { ...@@ -32,8 +34,12 @@ export default async (container: Container, selectChain: MsgSelectChain) => {
handle_flag = 0; handle_flag = 0;
} else { } else {
if (chainSetting === ChainSetting.CHAIN_ALL) { if (chainSetting === ChainSetting.CHAIN_ALL) {
// 配置了全部连锁,则处理多张 // 配置了全部连锁,则处理
handle_flag = 2; if (chains.length === 1) {
handle_flag = 1;
} else {
handle_flag = 2;
}
} else { } else {
// 否则不连锁 // 否则不连锁
handle_flag = 0; handle_flag = 0;
...@@ -44,32 +50,45 @@ export default async (container: Container, selectChain: MsgSelectChain) => { ...@@ -44,32 +50,45 @@ export default async (container: Container, selectChain: MsgSelectChain) => {
if (chains.length === 0) { if (chains.length === 0) {
// 根本没卡,直接回答 // 根本没卡,直接回答
handle_flag = 0; handle_flag = 0;
} else if (chainSetting === ChainSetting.CHAIN_IGNORE) {
// 配置了忽略连锁
handle_flag = 0;
} else { } else {
// 处理多张 // 处理
handle_flag = 2; if (chains.length === 1) {
handle_flag = 1;
} else {
handle_flag = 2;
}
} }
} }
} else { } else {
// 有强制发动的卡 // 有强制发动的卡
if (chains.length === 1) { if (chains.length === 1) {
// 只有一个强制发动的连锁项,直接回应 // 只有一张卡需要处理(强制发动)
handle_flag = 4;
} else {
// 处理强制发动的卡
handle_flag = 3; handle_flag = 3;
} else {
// 多张卡需要处理(强制发动)
handle_flag = 4;
} }
} }
// handle_flag:
// 0 - 无卡,直接回应
// 1 - 一张卡需要处理
// 2 - 多张卡需要处理
// 3 - 一张卡需要处理(强制发动)
// 4 - 多张卡需要处理(强制发动)
switch (handle_flag) { switch (handle_flag) {
case 0: { case 0: {
// 直接回答 // 直接回答
sendSelectSingleResponse(conn, -1); sendSelectSingleResponse(conn, -1);
break; break;
} }
case 2: // 处理多张 case 1:
case 3: { case 2: {
// 处理强制发动的卡 // 处理可选连锁
fetchSelectHintMeta({ fetchSelectHintMeta({
selectHintData: 203, selectHintData: 203,
}); });
...@@ -79,7 +98,7 @@ export default async (container: Container, selectChain: MsgSelectChain) => { ...@@ -79,7 +98,7 @@ export default async (container: Container, selectChain: MsgSelectChain) => {
); );
await displaySelectActionsModal({ await displaySelectActionsModal({
isChain: true, isChain: true,
cancelable: !forced, cancelable: true,
min: 1, min: 1,
max: 1, max: 1,
selecteds, selecteds,
...@@ -88,10 +107,29 @@ export default async (container: Container, selectChain: MsgSelectChain) => { ...@@ -88,10 +107,29 @@ export default async (container: Container, selectChain: MsgSelectChain) => {
}); });
break; break;
} }
case 4: { case 3: {
// 一张强制发动的卡,直接回应 // 一张强制发动的卡,直接回应
sendSelectSingleResponse(conn, chains[0].response); sendSelectSingleResponse(conn, chains[0].response);
break;
}
case 4: {
// 多张强制发动的卡,弹窗选择
fetchSelectHintMeta({
selectHintData: 203,
});
const { selecteds, mustSelects, selectables } = await fetchCheckCardMeta(
context,
chains,
);
await displaySelectActionsModal({
isChain: true,
cancelable: false,
min: 1,
max: 1,
selecteds,
mustSelects,
selectables,
});
break; break;
} }
default: { default: {
......
...@@ -107,7 +107,12 @@ const MdproDeckBlock: React.FC<{ ...@@ -107,7 +107,12 @@ const MdproDeckBlock: React.FC<{
const onDelete = async () => { const onDelete = async () => {
if (user) { if (user) {
const resp = await deleteDeck(user.id, user.token, deck.deckId); const resp = await deleteDeck(
user.id,
user.token,
deck.deckId,
user.username,
);
if (resp?.code === 0 && resp.data === true) { if (resp?.code === 0 && resp.data === true) {
message.success( message.success(
...@@ -242,7 +247,9 @@ const copyMdproDeckToEditing = async (mdproDeck: MdproDeckLike) => { ...@@ -242,7 +247,9 @@ const copyMdproDeckToEditing = async (mdproDeck: MdproDeckLike) => {
if (resp?.code !== 0) { if (resp?.code !== 0) {
message.error(resp?.message); message.error(resp?.message);
} else if (resp.data?.deckYdk !== undefined) { } else if (resp.data?.deckYdk !== undefined) {
const deck = YGOProDeck.fromYdkString(resp.data.deckYdk); // 服务端返回的 YDK 可能包含转义的换行符(如 "\\r\\n"),需要转换为标准换行符
const ydkString = resp.data.deckYdk.replace(/\\r\\n|\\r|\\n/g, "\n");
const deck = YGOProDeck.fromYdkString(ydkString);
if (!(deck.main.length + deck.extra.length + deck.side.length === 0)) { if (!(deck.main.length + deck.extra.length + deck.side.length === 0)) {
const deckName = mdproDeck.deckName; const deckName = mdproDeck.deckName;
......
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