Commit 89b0468c authored by Chunchi Che's avatar Chunchi Che

Merge branch 'dev/async' into 'main'

更新一波动画和样式

See merge request mycard/Neos!226
parents 8e322240 3fa99bc2
Pipeline #22485 passed with stages
in 14 minutes and 10 seconds
neos-protobuf @ 759a1db5
Subproject commit 975e4a815f7fc394247f5af6ccb22a0742379f33 Subproject commit 759a1db5cbb32e84711f9a5fba3d19ee069fa635
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
1, 1,
6, 6,
7, 7,
32,
34, 34,
54, 54,
55, 55,
......
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
1, 1,
6, 6,
7, 7,
32,
34, 34,
54, 54,
55, 55,
......
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
"google-protobuf": "^3.21.2", "google-protobuf": "^3.21.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"react": "^18.2.0", "react": "^18.2.0",
"react-animated-numbers": "^0.16.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-draggable": "^4.4.5", "react-draggable": "^4.4.5",
"react-router-dom": "^6.10.0", "react-router-dom": "^6.10.0",
...@@ -20627,6 +20628,19 @@ ...@@ -20627,6 +20628,19 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-animated-numbers": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/react-animated-numbers/-/react-animated-numbers-0.16.0.tgz",
"integrity": "sha512-MUoOsf8fLzwyUL9l6NEMma+29QtfbeYmt8x2LLt4IeLHQWJQfGa4WIUXB/VDVBXEhg74BhCRytdyvhHR3IiHsw==",
"dependencies": {
"@react-spring/web": "^9.4.5",
"react-intersection-observer": "^8.33.1"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-app-polyfill": { "node_modules/react-app-polyfill": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-0.2.2.tgz", "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-0.2.2.tgz",
...@@ -21260,6 +21274,14 @@ ...@@ -21260,6 +21274,14 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz",
"integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q==" "integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q=="
}, },
"node_modules/react-intersection-observer": {
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-8.34.0.tgz",
"integrity": "sha512-TYKh52Zc0Uptp5/b4N91XydfSGKubEhgZRtcg1rhTKABXijc4Sdr1uTp5lJ8TN27jwUsdXxjHXtHa0kPj704sw==",
"peerDependencies": {
"react": "^15.0.0 || ^16.0.0 || ^17.0.0|| ^18.0.0"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
...@@ -44269,6 +44291,15 @@ ...@@ -44269,6 +44291,15 @@
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
} }
}, },
"react-animated-numbers": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/react-animated-numbers/-/react-animated-numbers-0.16.0.tgz",
"integrity": "sha512-MUoOsf8fLzwyUL9l6NEMma+29QtfbeYmt8x2LLt4IeLHQWJQfGa4WIUXB/VDVBXEhg74BhCRytdyvhHR3IiHsw==",
"requires": {
"@react-spring/web": "^9.4.5",
"react-intersection-observer": "^8.33.1"
}
},
"react-app-polyfill": { "react-app-polyfill": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-0.2.2.tgz", "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-0.2.2.tgz",
...@@ -44763,6 +44794,12 @@ ...@@ -44763,6 +44794,12 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz",
"integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q==" "integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q=="
}, },
"react-intersection-observer": {
"version": "8.34.0",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-8.34.0.tgz",
"integrity": "sha512-TYKh52Zc0Uptp5/b4N91XydfSGKubEhgZRtcg1rhTKABXijc4Sdr1uTp5lJ8TN27jwUsdXxjHXtHa0kPj704sw==",
"requires": {}
},
"react-is": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
This diff is collapsed.
...@@ -48,7 +48,7 @@ export class YgoProPacket { ...@@ -48,7 +48,7 @@ export class YgoProPacket {
); );
} }
} catch (e) { } catch (e) {
console.log(e); console.error(e);
} }
const dataView = new DataView(array); const dataView = new DataView(array);
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { BufferReader } from "../../../../../../rust-src/pkg/rust_src";
/*
* Msg Hand Result
* @param - TODO
*
* @usage - 后端告诉前端玩家选择的猜拳结果
* */
export default (data: Uint8Array) => {
const reader = new BufferReader(data);
const x = reader.readUint8();
const result1 = x & 0x3;
const result2 = (x >> 2) & 0x3;
return new ygopro.StocGameMessage.MsgHandResult({
result1,
result2,
});
};
{ {
"50":{ "50": {
"protoType":"move", "protoType": "move",
"fields":[ "fields": [
{ {
"fieldName":"code", "fieldName": "code",
"fieldType":"uint32" "fieldType": "uint32"
}, },
{ {
"fieldName":"from", "fieldName": "from",
"fieldType":"CardLocation" "fieldType": "CardLocation"
}, },
{ {
"fieldName":"to", "fieldName": "to",
"fieldType":"CardLocation" "fieldType": "CardLocation"
}, },
{ {
"fieldName":"reason", "fieldName": "reason",
"fieldType":"uint8" "fieldType": "uint8"
} }
] ]
}, },
"33":{ "33": {
"protoType":"shuffle_hand", "protoType": "shuffle_hand",
"fields":[ "fields": [
{ {
"fieldName":"player", "fieldName": "player",
"fieldType":"uint8" "fieldType": "uint8"
}, },
{ {
"fieldName":"hands", "fieldName": "hands",
"fieldType":"repeated", "fieldType": "repeated",
"repeatedType":"uint32" "repeatedType": "uint32"
} }
] ]
}, },
"53":{ "53": {
"protoType":"pos_change", "protoType": "pos_change",
"fields":[ "fields": [
{ {
"fieldName":"card_info", "fieldName": "card_info",
"fieldType":"CardInfo" "fieldType": "CardInfo"
}, },
{ {
"fieldName":"pre_position", "fieldName": "pre_position",
"fieldType":"CardPosition" "fieldType": "CardPosition"
}, },
{ {
"fieldName":"cur_position", "fieldName": "cur_position",
"fieldType":"CardPosition" "fieldType": "CardPosition"
} }
] ]
}, },
"13":{ "13": {
"protoType":"select_yes_no", "protoType": "select_yes_no",
"fields":[ "fields": [
{ {
"fieldName":"player", "fieldName": "player",
"fieldType":"uint8" "fieldType": "uint8"
}, },
{ {
"fieldName":"effect_description", "fieldName": "effect_description",
"fieldType":"uint32" "fieldType": "uint32"
} }
] ]
}, },
"54":{ "54": {
"protoType":"set", "protoType": "set",
"fields":[ "fields": []
},
] "55": {
}, "protoType": "swap",
"55":{ "fields": []
"protoType":"swap", },
"fields":[ "60": {
"protoType": "summoning",
] "fields": [
}, {
"60":{ "fieldName": "code",
"protoType":"summoning", "fieldType": "uint32"
"fields":[ },
{ {
"fieldName":"code", "fieldName": "location",
"fieldType":"uint32" "fieldType": "CardLocation"
}, }
{ ]
"fieldName":"location", },
"fieldType":"CardLocation" "61": {
} "protoType": "summoned",
] "fields": []
}, },
"61":{ "62": {
"protoType":"summoned", "protoType": "sp_summoning",
"fields":[ "fields": [
{
] "fieldName": "code",
}, "fieldType": "uint32"
"62":{ },
"protoType":"sp_summoning", {
"fields":[ "fieldName": "location",
{ "fieldType": "CardLocation"
"fieldName":"code", }
"fieldType":"uint32" ]
}, },
{ "63": {
"fieldName":"location", "protoType": "sp_summoned",
"fieldType":"CardLocation" "fields": []
} },
] "64": {
}, "protoType": "flip_summoning",
"63":{ "fields": [
"protoType":"sp_summoned", {
"fields":[ "fieldName": "code",
"fieldType": "uint32"
] },
}, {
"64":{ "fieldName": "location",
"protoType":"flip_summoning", "fieldType": "CardLocation"
"fields":[ }
{ ]
"fieldName":"code", },
"fieldType":"uint32" "65": {
}, "protoType": "flip_summoned",
{ "fields": []
"fieldName":"location", },
"fieldType":"CardLocation" "70": {
} "protoType": "chaining",
] "fields": [
}, {
"65":{ "fieldName": "code",
"protoType":"flip_summoned", "fieldType": "uint32"
"fields":[ },
{
] "fieldName": "location",
}, "fieldType": "CardLocation"
"70":{ }
"protoType":"chaining", ]
"fields":[ },
{ "112": {
"fieldName":"code", "protoType": "attack_disable",
"fieldType":"uint32" "fields": []
}, },
{ "73": {
"fieldName":"location", "protoType": "chain_solved",
"fieldType":"CardLocation" "fields": [
} {
] "fieldName": "solved_index",
}, "fieldType": "uint8"
"112":{ }
"protoType":"attack_disable", ]
"fields":[ },
"74": {
] "protoType": "chain_end",
}, "fields": []
"73":{ },
"protoType":"chain_solved", "75": {
"fields":[ "protoType": "chain_solved",
{ "fields": [
"fieldName":"solved_index", {
"fieldType":"uint8" "fieldName": "solved_index",
} "fieldType": "uint8"
] }
}, ]
"74":{ },
"protoType":"chain_end", "76": {
"fields":[ "protoType": "chain_solved",
"fields": [
] {
}, "fieldName": "solved_index",
"75":{ "fieldType": "uint8"
"protoType":"chain_solved", }
"fields":[ ]
{ },
"fieldName":"solved_index", "94": {
"fieldType":"uint8" "protoType": "lp_update",
} "fields": [
] {
}, "fieldName": "player",
"76":{ "fieldType": "uint8"
"protoType":"chain_solved", },
"fields":[ {
{ "fieldName": "new_lp",
"fieldName":"solved_index", "fieldType": "uint32"
"fieldType":"uint8" }
} ]
] },
}, "30": {
"94":{ "protoType": "confirm_cards",
"protoType":"lp_update", "fields": [
"fields":[ {
{ "fieldName": "player",
"fieldName":"player", "fieldType": "uint8"
"fieldType":"uint8" },
}, {
{ "fieldName": "cards",
"fieldName":"new_lp", "fieldType": "repeated",
"fieldType":"uint32" "repeatedType": "CardInfo"
} }
] ]
}, },
"30":{ "31": {
"protoType":"confirm_cards", "protoType": "confirm_cards",
"fields":[ "fields": [
{ {
"fieldName":"player", "fieldName": "player",
"fieldType":"uint8" "fieldType": "uint8"
}, },
{ {
"fieldName":"cards", "fieldName": "cards",
"fieldType":"repeated", "fieldType": "repeated",
"repeatedType":"CardInfo" "repeatedType": "CardInfo"
} }
] ]
}, },
"31":{ "83": {
"protoType":"confirm_cards", "protoType": "become_target",
"fields":[ "fields": [
{ {
"fieldName":"player", "fieldName": "locations",
"fieldType":"uint8" "fieldType": "repeated",
}, "repeatedType": "CardLocation"
{ }
"fieldName":"cards", ]
"fieldType":"repeated", },
"repeatedType":"CardInfo" "32": {
} "protoType": "shuffle_deck",
] "fields": [
}, {
"83":{ "fieldName": "player",
"protoType":"become_target", "fieldType": "uint8"
"fields":[ }
{ ]
"fieldName":"locations", },
"fieldType":"repeated", "132": {
"repeatedType":"CardLocation" "protoType": "rock_paper_scissors",
} "fields": [
] {
}, "fieldName": "player",
"32":{ "fieldType": "uint8"
"protoType":"shuffle_deck", }
"fields":[ ]
{ }
"fieldName":"player",
"fieldType":"uint8"
}
]
}
} }
...@@ -36,6 +36,7 @@ const MsgConstructorMap: Map<string, Constructor> = new Map([ ...@@ -36,6 +36,7 @@ const MsgConstructorMap: Map<string, Constructor> = new Map([
["confirm_cards", ygopro.StocGameMessage.MsgConfirmCards], ["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],
]); ]);
export interface penetrateType { export interface penetrateType {
......
...@@ -47,7 +47,7 @@ export class WebSocketStream { ...@@ -47,7 +47,7 @@ export class WebSocketStream {
reader.read().then(async function process({ done, value }): Promise<void> { reader.read().then(async function process({ done, value }): Promise<void> {
if (done) { if (done) {
if (ws.readyState == WebSocket.CLOSED) { if (ws.readyState === WebSocket.CLOSED) {
// websocket connection has been closed // websocket connection has been closed
console.info("WebSocket closed, stream complete."); console.info("WebSocket closed, stream complete.");
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
* 在进行代码开发的时候需要注意这点。 * 在进行代码开发的时候需要注意这点。
* *
* */ * */
import { ProConfigProvider } from "@ant-design/pro-provider";
import { ConfigProvider, theme } from "antd"; import { ConfigProvider, theme } from "antd";
import zhCN from "antd/locale/zh_CN"; import zhCN from "antd/locale/zh_CN";
import React from "react"; import React from "react";
...@@ -30,10 +31,13 @@ import Neos from "./ui/Neos"; ...@@ -30,10 +31,13 @@ import Neos from "./ui/Neos";
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement document.getElementById("root") as HTMLElement
); );
root.render( root.render(
<BrowserRouter> <BrowserRouter>
<ConfigProvider theme={{ algorithm: theme.darkAlgorithm }} locale={zhCN}> <ConfigProvider theme={{ algorithm: theme.darkAlgorithm }} locale={zhCN}>
<Neos /> <ProConfigProvider dark>
<Neos />
</ProConfigProvider>
</ConfigProvider> </ConfigProvider>
</BrowserRouter> </BrowserRouter>
); );
import { fetchCard, fetchStrings, ygopro } from "@/api"; import { fetchCard, fetchStrings, ygopro } from "@/api";
import { messageStore } from "@/stores"; import { displayAnnounceModal } from "@/ui/Duel/Message";
import MsgAnnounce = ygopro.StocGameMessage.MsgAnnounce; import MsgAnnounce = ygopro.StocGameMessage.MsgAnnounce;
const { announceModal } = messageStore;
export default async (announce: MsgAnnounce) => { export default async (announce: MsgAnnounce) => {
const type_ = announce.announce_type; const type_ = announce.announce_type;
let min = announce.min; let min = announce.min;
if ( if (
type_ == MsgAnnounce.AnnounceType.Card || type_ === MsgAnnounce.AnnounceType.Card ||
type_ == MsgAnnounce.AnnounceType.Number type_ === MsgAnnounce.AnnounceType.Number
) { ) {
min = 1; min = 1;
} }
announceModal.min = min;
switch (type_) { switch (type_) {
case MsgAnnounce.AnnounceType.RACE: { case MsgAnnounce.AnnounceType.RACE: {
announceModal.title = fetchStrings("!system", 563); await displayAnnounceModal({
announceModal.options = announce.options.map((option) => ({ min,
info: fetchStrings("!system", 1200 + option.code), title: fetchStrings("!system", 563),
response: option.response, options: announce.options.map((option) => ({
})); info: fetchStrings("!system", 1200 + option.code),
announceModal.isOpen = true; response: option.response,
})),
});
break; break;
} }
case MsgAnnounce.AnnounceType.Attribute: { case MsgAnnounce.AnnounceType.Attribute: {
announceModal.title = fetchStrings("!system", 562); await displayAnnounceModal({
announceModal.options = announce.options.map((option) => ({ min,
info: fetchStrings("!system", 1010 + option.code), title: fetchStrings("!system", 562),
response: option.response, options: announce.options.map((option) => ({
})); info: fetchStrings("!system", 1010 + option.code),
announceModal.isOpen = true; response: option.response,
})),
});
break; break;
} }
case MsgAnnounce.AnnounceType.Card: { case MsgAnnounce.AnnounceType.Card: {
announceModal.title = fetchStrings("!system", 564); const options = [];
for (const option of announce.options) { for (const option of announce.options) {
const meta = await fetchCard(option.code); const meta = await fetchCard(option.code);
if (meta.text.name) { if (meta.text.name) {
announceModal.options.push({ options.push({
info: meta.text.name, info: meta.text.name,
response: option.response, response: option.response,
}); });
} }
} }
announceModal.isOpen = true; await displayAnnounceModal({
min,
title: fetchStrings("!system", 564),
options,
});
break; break;
} }
case MsgAnnounce.AnnounceType.Number: { case MsgAnnounce.AnnounceType.Number: {
announceModal.title = fetchStrings("!system", 565); await displayAnnounceModal({
announceModal.options = announce.options.map((option) => ({ min,
info: option.code.toString(), title: fetchStrings("!system", 565),
response: option.response, options: announce.options.map((option) => ({
})); info: option.code.toString(),
announceModal.isOpen = true; response: option.response,
})),
});
break; break;
} }
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { sleep } from "@/infra"; import { sleep } from "@/infra";
import { matStore } from "@/stores"; import { showWaiting } from "@/ui/Duel/Message";
import onAnnounce from "./announce"; import onAnnounce from "./announce";
import onMsgAttack from "./attack"; import onMsgAttack from "./attack";
...@@ -14,6 +14,7 @@ import onMsgDraw from "./draw"; ...@@ -14,6 +14,7 @@ import onMsgDraw from "./draw";
import onMsgFieldDisabled from "./fieldDisabled"; import onMsgFieldDisabled from "./fieldDisabled";
import onMsgFilpSummoned from "./flipSummoned"; import onMsgFilpSummoned from "./flipSummoned";
import onMsgFlipSummoning from "./flipSummoning"; import onMsgFlipSummoning from "./flipSummoning";
import onMsgHandResult from "./handResult";
import onMsgHint from "./hint"; import onMsgHint from "./hint";
import onLpUpdate from "./lpUpdate"; import onLpUpdate from "./lpUpdate";
import onMsgMove from "./move"; import onMsgMove from "./move";
...@@ -21,6 +22,7 @@ import onMsgNewPhase from "./newPhase"; ...@@ -21,6 +22,7 @@ import onMsgNewPhase from "./newPhase";
import onMsgNewTurn from "./newTurn"; import onMsgNewTurn from "./newTurn";
import onMsgPosChange from "./posChange"; import onMsgPosChange from "./posChange";
import onMsgReloadField from "./reloadField"; import onMsgReloadField from "./reloadField";
import onMsgRockPaperScissors from "./rockPaperScissors";
import onMsgSelectBattleCmd from "./selectBattleCmd"; import onMsgSelectBattleCmd from "./selectBattleCmd";
import onMsgSelectCard from "./selectCard"; import onMsgSelectCard from "./selectCard";
import onMsgSelectChain from "./selectChain"; import onMsgSelectChain from "./selectChain";
...@@ -77,7 +79,7 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) { ...@@ -77,7 +79,7 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
const msg = pb.stoc_game_msg; const msg = pb.stoc_game_msg;
if (ActiveList.includes(msg.gameMsg)) { if (ActiveList.includes(msg.gameMsg)) {
matStore.waiting = false; showWaiting(false);
} }
switch (msg.gameMsg) { switch (msg.gameMsg) {
...@@ -102,7 +104,7 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) { ...@@ -102,7 +104,7 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break; break;
} }
case "hint": { case "hint": {
onMsgHint(msg.hint); await onMsgHint(msg.hint);
break; break;
} }
...@@ -133,12 +135,12 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) { ...@@ -133,12 +135,12 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break; break;
} }
case "select_effect_yn": { case "select_effect_yn": {
onMsgSelectEffectYn(msg.select_effect_yn); await onMsgSelectEffectYn(msg.select_effect_yn);
break; break;
} }
case "select_position": { case "select_position": {
onMsgSelectPosition(msg.select_position); await onMsgSelectPosition(msg.select_position);
break; break;
} }
...@@ -168,7 +170,7 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) { ...@@ -168,7 +170,7 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break; break;
} }
case "select_yes_no": { case "select_yes_no": {
onMsgSelectYesNo(msg.select_yes_no); await onMsgSelectYesNo(msg.select_yes_no);
break; break;
} }
...@@ -213,12 +215,12 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) { ...@@ -213,12 +215,12 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break; break;
} }
case "select_counter": { case "select_counter": {
onMsgSelectCounter(msg.select_counter); await onMsgSelectCounter(msg.select_counter);
break; break;
} }
case "sort_card": { case "sort_card": {
onMsgSortCard(msg.sort_card); await onMsgSortCard(msg.sort_card);
break; break;
} }
...@@ -327,6 +329,16 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) { ...@@ -327,6 +329,16 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break; break;
} }
case "rock_paper_scissors": {
onMsgRockPaperScissors(msg.rock_paper_scissors);
break;
}
case "hand_res": {
onMsgHandResult(msg.hand_res);
break;
}
case "unimplemented": { case "unimplemented": {
onUnimplemented(msg.unimplemented); onUnimplemented(msg.unimplemented);
......
import { ygopro } from "@/api";
import MsgHandResult = ygopro.StocGameMessage.MsgHandResult;
export default (res: MsgHandResult) => {
console.log(res);
// TODO
};
...@@ -7,10 +7,10 @@ import { ...@@ -7,10 +7,10 @@ import {
import MsgHint = ygopro.StocGameMessage.MsgHint; import MsgHint = ygopro.StocGameMessage.MsgHint;
export default (hint: MsgHint) => { export default async (hint: MsgHint) => {
switch (hint.hint_type) { switch (hint.hint_type) {
case MsgHint.HintType.HINT_EVENT: { case MsgHint.HintType.HINT_EVENT: {
fetchEsHintMeta({ originMsg: hint.hint_data }); await fetchEsHintMeta({ originMsg: hint.hint_data });
break; break;
} }
case MsgHint.HintType.HINT_MESSAGE: { case MsgHint.HintType.HINT_MESSAGE: {
...@@ -18,7 +18,7 @@ export default (hint: MsgHint) => { ...@@ -18,7 +18,7 @@ export default (hint: MsgHint) => {
break; break;
} }
case MsgHint.HintType.HINT_SELECTMSG: { case MsgHint.HintType.HINT_SELECTMSG: {
fetchSelectHintMeta({ await fetchSelectHintMeta({
selectHintData: hint.hint_data, selectHintData: hint.hint_data,
esHint: "", esHint: "",
}); });
......
...@@ -37,26 +37,24 @@ export default async (move: MsgMove) => { ...@@ -37,26 +37,24 @@ export default async (move: MsgMove) => {
const meta = await fetchCard(code); const meta = await fetchCard(code);
if (meta.data.type !== undefined && (meta.data.type & TYPE_TOKEN) > 0) { if (meta.data.type !== undefined && (meta.data.type & TYPE_TOKEN) > 0) {
// 衍生物 // 衍生物
if (from.zone == DECK) { if (from.zone === DECK) {
// 衍生物出场的场景,设置`from.zone`为`TZONE` // 衍生物出场的场景,设置`from.zone`为`TZONE`
from.zone = TZONE; from.zone = TZONE;
} }
if (to.zone == DECK) { if (to.zone === DECK) {
// 衍生物离开场上的场合,设置`to.zone`为`TZONE` // 衍生物离开场上的场合,设置`to.zone`为`TZONE`
to.zone = TZONE; to.zone = TZONE;
} }
} }
// log出来看看,后期删掉即可 // log出来看看
await (async () => { console.color("green")(
console.color("green")( `${meta.text.name} ${ygopro.CardZone[from.zone]}:${from.sequence}${
`${meta.text.name} ${ygopro.CardZone[from.zone]}:${from.sequence}:${ from.is_overlay ? ":" + from.overlay_sequence : ""
from.is_overlay ? from.overlay_sequence : "" }${ygopro.CardZone[to.zone]}:${to.sequence}${
}${ygopro.CardZone[to.zone]}:${to.sequence}:${ to.is_overlay ? ":" + to.overlay_sequence : ""
to.is_overlay ? to.overlay_sequence : "" }`
}` );
);
})();
let target: CardType; let target: CardType;
...@@ -91,9 +89,9 @@ export default async (move: MsgMove) => { ...@@ -91,9 +89,9 @@ export default async (move: MsgMove) => {
} }
// 超量 // 超量
if (to.is_overlay && from.zone == MZONE) { if (to.is_overlay && from.zone === MZONE) {
// 准备超量召唤,超量素材入栈 // 准备超量召唤,超量素材入栈
if (reason == REASON_MATERIAL) { if (reason === REASON_MATERIAL) {
to.zone = MZONE; to.zone = MZONE;
overlayStack.push(to); overlayStack.push(to);
} }
...@@ -170,7 +168,7 @@ export default async (move: MsgMove) => { ...@@ -170,7 +168,7 @@ export default async (move: MsgMove) => {
} }
// 超量素材位置跟随超量怪兽移动 // 超量素材位置跟随超量怪兽移动
if (from.zone == MZONE && !from.is_overlay) { if (from.zone === MZONE && !from.is_overlay) {
for (const overlay of cardStore.findOverlay( for (const overlay of cardStore.findOverlay(
from.zone, from.zone,
from.controller, from.controller,
......
import { ygopro } from "@/api";
export default (mora: ygopro.StocGameMessage.MsgRockPaperScissors) => {
console.log(mora);
// TODO
};
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import MsgSelectCard = ygopro.StocGameMessage.MsgSelectCard; import MsgSelectCard = ygopro.StocGameMessage.MsgSelectCard;
import { fetchCheckCardMeta, messageStore } from "@/stores";
export default (selectCard: MsgSelectCard) => { import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
const cancelable = selectCard.cancelable;
const min = selectCard.min; import { fetchCheckCardMeta } from "../utils";
const max = selectCard.max;
const cards = selectCard.cards; export default async (selectCard: MsgSelectCard) => {
const { cancelable, min, max, cards } = selectCard;
// TODO: handle release_param // TODO: handle release_param
messageStore.selectCardActions.min = min;
messageStore.selectCardActions.max = max;
messageStore.selectCardActions.cancelAble = cancelable;
for (const card of cards) { const { selecteds, mustSelects, selectables } = await fetchCheckCardMeta(
fetchCheckCardMeta({ cards
code: card.code, );
location: card.location, await displaySelectActionsModal({
response: card.response, cancelable,
}); min,
} max,
messageStore.selectCardActions.isValid = true; selecteds,
messageStore.selectCardActions.isOpen = true; mustSelects,
selectables,
});
}; };
import { sendSelectSingleResponse, ygopro } from "@/api"; import { sendSelectSingleResponse, ygopro } from "@/api";
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { import { fetchSelectHintMeta } from "@/stores";
fetchCheckCardMeta, import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
fetchSelectHintMeta,
messageStore, import { fetchCheckCardMeta } from "../utils";
} from "@/stores";
const NeosConfig = useConfig(); const NeosConfig = useConfig();
type MsgSelectChain = ygopro.StocGameMessage.MsgSelectChain; type MsgSelectChain = ygopro.StocGameMessage.MsgSelectChain;
export default (selectChain: MsgSelectChain) => { export default async (selectChain: MsgSelectChain) => {
const spCount = selectChain.special_count; const spCount = selectChain.special_count;
const forced = selectChain.forced; const forced = selectChain.forced;
const _hint0 = selectChain.hint0; const _hint0 = selectChain.hint0;
...@@ -19,9 +18,9 @@ export default (selectChain: MsgSelectChain) => { ...@@ -19,9 +18,9 @@ export default (selectChain: MsgSelectChain) => {
let handle_flag = 0; let handle_flag = 0;
if (!forced) { if (!forced) {
// 无强制发动的卡 // 无强制发动的卡
if (spCount == 0) { if (spCount === 0) {
// 无关键卡 // 无关键卡
if (chains.length == 0) { if (chains.length === 0) {
// 直接回答 // 直接回答
handle_flag = 0; handle_flag = 0;
} else { } else {
...@@ -35,7 +34,7 @@ export default (selectChain: MsgSelectChain) => { ...@@ -35,7 +34,7 @@ export default (selectChain: MsgSelectChain) => {
} }
} else { } else {
// 有关键卡 // 有关键卡
if (chains.length == 0) { if (chains.length === 0) {
// 根本没卡,直接回答 // 根本没卡,直接回答
handle_flag = 0; handle_flag = 0;
} else { } else {
...@@ -45,7 +44,7 @@ export default (selectChain: MsgSelectChain) => { ...@@ -45,7 +44,7 @@ export default (selectChain: MsgSelectChain) => {
} }
} else { } else {
// 有强制发动的卡 // 有强制发动的卡
if (chains.length == 1) { if (chains.length === 1) {
// 只有一个强制发动的连锁项,直接回应 // 只有一个强制发动的连锁项,直接回应
handle_flag = 4; handle_flag = 4;
} else { } else {
...@@ -64,26 +63,21 @@ export default (selectChain: MsgSelectChain) => { ...@@ -64,26 +63,21 @@ export default (selectChain: MsgSelectChain) => {
case 2: // 处理多张 case 2: // 处理多张
case 3: { case 3: {
// 处理强制发动的卡 // 处理强制发动的卡
await fetchSelectHintMeta({
messageStore.selectCardActions.isChain = true;
messageStore.selectCardActions.min = 1;
messageStore.selectCardActions.max = 1;
messageStore.selectCardActions.cancelAble = !forced;
for (const chain of chains) {
fetchCheckCardMeta({
code: chain.code,
location: chain.location,
response: chain.response,
effectDescCode: chain.effect_description,
});
}
fetchSelectHintMeta({
selectHintData: 203, selectHintData: 203,
}); });
messageStore.selectCardActions.isValid = true; const { selecteds, mustSelects, selectables } = await fetchCheckCardMeta(
messageStore.selectCardActions.isOpen = true; chains
);
await displaySelectActionsModal({
isChain: true,
cancelable: !forced,
min: 1,
max: 1,
selecteds,
mustSelects,
selectables,
});
break; break;
} }
case 4: { case 4: {
......
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { cardStore, messageStore } from "@/stores"; import { cardStore } from "@/stores";
import { displayCheckCounterModal } from "@/ui/Duel/Message";
type MsgSelectCounter = ygopro.StocGameMessage.MsgSelectCounter; type MsgSelectCounter = ygopro.StocGameMessage.MsgSelectCounter;
export default (selectCounter: MsgSelectCounter) => { export default async (selectCounter: MsgSelectCounter) => {
messageStore.checkCounterModal.counterType = selectCounter.counter_type; await displayCheckCounterModal({
messageStore.checkCounterModal.min = selectCounter.min; counterType: selectCounter.counter_type,
messageStore.checkCounterModal.options = selectCounter.options!.map( min: selectCounter.min,
({ location, code, counter_count }) => { options: selectCounter.options!.map(({ location, code, counter_count }) => {
const id = cardStore.find(location)?.code; const id = cardStore.find(location)?.code;
const newCode = code ? code : id || 0; const newCode = code ? code : id || 0;
...@@ -14,7 +15,6 @@ export default (selectCounter: MsgSelectCounter) => { ...@@ -14,7 +15,6 @@ export default (selectCounter: MsgSelectCounter) => {
code: newCode, code: newCode,
max: counter_count!, max: counter_count!,
}; };
} }),
); });
messageStore.checkCounterModal.isOpen = true;
}; };
import { fetchStrings, ygopro } from "@/api"; import { fetchStrings, ygopro } from "@/api";
import { CardMeta, fetchCard } from "@/api/cards"; import { CardMeta, fetchCard } from "@/api/cards";
import { messageStore } from "@/stores"; import { displayYesNoModal } from "@/ui/Duel/Message";
type MsgSelectEffectYn = ygopro.StocGameMessage.MsgSelectEffectYn; type MsgSelectEffectYn = ygopro.StocGameMessage.MsgSelectEffectYn;
...@@ -11,7 +11,7 @@ export default async (selectEffectYn: MsgSelectEffectYn) => { ...@@ -11,7 +11,7 @@ export default async (selectEffectYn: MsgSelectEffectYn) => {
const effect_description = selectEffectYn.effect_description; const effect_description = selectEffectYn.effect_description;
const textGenerator = const textGenerator =
effect_description == 0 || effect_description == 221 effect_description === 0 || effect_description === 221
? ( ? (
desc: string, desc: string,
cardMeta: CardMeta, cardMeta: CardMeta,
...@@ -33,6 +33,5 @@ export default async (selectEffectYn: MsgSelectEffectYn) => { ...@@ -33,6 +33,5 @@ export default async (selectEffectYn: MsgSelectEffectYn) => {
const desc = fetchStrings("!system", effect_description); const desc = fetchStrings("!system", effect_description);
const meta = await fetchCard(code); const meta = await fetchCard(code);
messageStore.yesNoModal.msg = textGenerator(desc, meta, location); await displayYesNoModal(textGenerator(desc, meta, location));
messageStore.yesNoModal.isOpen = true;
}; };
import { fetchCard, getCardStr, ygopro } from "@/api"; import { fetchCard, getCardStr, ygopro } from "@/api";
import MsgSelectOption = ygopro.StocGameMessage.MsgSelectOption; import MsgSelectOption = ygopro.StocGameMessage.MsgSelectOption;
import { messageStore } from "@/stores"; import { displayOptionModal } from "@/ui/Duel/Message";
export default async (selectOption: MsgSelectOption) => { export default async (selectOption: MsgSelectOption) => {
const options = selectOption.options; const options = selectOption.options;
await displayOptionModal(
await Promise.all( await Promise.all(
options.map(async ({ code, response }) => { options.map(async ({ code, response }) => {
const meta = await fetchCard(code >> 4); const meta = await fetchCard(code >> 4);
const msg = getCardStr(meta, code & 0xf) || "[?]"; const msg = getCardStr(meta, code & 0xf) || "[?]";
const newResponse = { msg, response }; return { msg, response };
messageStore.optionModal.options.push(newResponse); })
}) )
); );
messageStore.optionModal.isOpen = true;
}; };
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { messageStore } from "@/stores"; import { displayPositionModal } from "@/ui/Duel/Message";
type MsgSelectPosition = ygopro.StocGameMessage.MsgSelectPosition; type MsgSelectPosition = ygopro.StocGameMessage.MsgSelectPosition;
export default (selectPosition: MsgSelectPosition) => { export default async (selectPosition: MsgSelectPosition) => {
const _player = selectPosition.player; const _player = selectPosition.player;
const positions = selectPosition.positions; const positions = selectPosition.positions.map(
messageStore.positionModal.positions = positions.map(
(position) => position.position (position) => position.position
); );
messageStore.positionModal.isOpen = true; await displayPositionModal(positions);
}; };
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { fetchCheckCardMeta, messageStore } from "@/stores"; import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
type MsgSelectSum = ygopro.StocGameMessage.MsgSelectSum;
export default (selectSum: MsgSelectSum) => {
messageStore.selectCardActions.overflow = selectSum.overflow != 0;
messageStore.selectCardActions.totalLevels = selectSum.level_sum;
messageStore.selectCardActions.min = selectSum.min;
messageStore.selectCardActions.max = selectSum.max;
for (const option of selectSum.must_select_cards) { import { fetchCheckCardMeta } from "../utils";
fetchCheckCardMeta(option, false, true); type MsgSelectSum = ygopro.StocGameMessage.MsgSelectSum;
}
for (const option of selectSum.selectable_cards) {
fetchCheckCardMeta(option);
}
messageStore.selectCardActions.isValid = true; export default async (selectSum: MsgSelectSum) => {
messageStore.selectCardActions.isOpen = true; const {
selecteds: selecteds1,
mustSelects: mustSelect1,
selectables: selectable1,
} = await fetchCheckCardMeta(selectSum.must_select_cards, false, true);
const {
selecteds: selecteds2,
mustSelects: mustSelect2,
selectables: selectable2,
} = await fetchCheckCardMeta(selectSum.selectable_cards);
await displaySelectActionsModal({
overflow: selectSum.overflow !== 0,
totalLevels: selectSum.level_sum,
min: selectSum.min,
max: selectSum.max,
selecteds: [...selecteds1, ...selecteds2],
mustSelects: [...mustSelect1, ...mustSelect2],
selectables: [...selectable1, ...selectable2],
});
}; };
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { fetchCheckCardMeta, messageStore } from "@/stores"; import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
import { fetchCheckCardMeta } from "../utils";
type MsgSelectTribute = ygopro.StocGameMessage.MsgSelectTribute; type MsgSelectTribute = ygopro.StocGameMessage.MsgSelectTribute;
export default (selectTribute: MsgSelectTribute) => { export default async (selectTribute: MsgSelectTribute) => {
// TODO: 当玩家选择卡数大于`max`时,是否也合法? // TODO: 当玩家选择卡数大于`max`时,是否也合法?
messageStore.selectCardActions.overflow = true; const { selecteds, mustSelects, selectables } = await fetchCheckCardMeta(
messageStore.selectCardActions.totalLevels = 0; selectTribute.selectable_cards
messageStore.selectCardActions.min = selectTribute.min; );
messageStore.selectCardActions.max = selectTribute.max; await displaySelectActionsModal({
overflow: true,
for (const option of selectTribute.selectable_cards) { totalLevels: 0,
fetchCheckCardMeta(option); min: selectTribute.min,
} max: selectTribute.max,
selecteds,
messageStore.selectCardActions.isValid = true; mustSelects,
messageStore.selectCardActions.isOpen = true; selectables,
});
}; };
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { fetchCheckCardMeta, messageStore } from "@/stores"; import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
import { fetchCheckCardMeta } from "../utils";
type MsgSelectUnselectCard = ygopro.StocGameMessage.MsgSelectUnselectCard; type MsgSelectUnselectCard = ygopro.StocGameMessage.MsgSelectUnselectCard;
export default async ({ export default async ({
...@@ -11,20 +12,24 @@ export default async ({ ...@@ -11,20 +12,24 @@ export default async ({
selectable_cards: selectableCards, selectable_cards: selectableCards,
selected_cards: selectedCards, selected_cards: selectedCards,
}: MsgSelectUnselectCard) => { }: MsgSelectUnselectCard) => {
messageStore.selectCardActions.finishAble = finishable; const {
messageStore.selectCardActions.cancelAble = cancelable; selecteds: selecteds1,
messageStore.selectCardActions.min = min; mustSelects: mustSelect1,
messageStore.selectCardActions.max = max; selectables: selectable1,
messageStore.selectCardActions.single = true; } = await fetchCheckCardMeta(selectableCards);
const {
for (const option of selectableCards) { selecteds: selecteds2,
await fetchCheckCardMeta(option); mustSelects: mustSelect2,
} selectables: selectable2,
} = await fetchCheckCardMeta(selectedCards, true);
for (const option of selectedCards) { await displaySelectActionsModal({
await fetchCheckCardMeta(option, true); finishable,
} cancelable,
min: min,
messageStore.selectCardActions.isValid = true; max: max,
messageStore.selectCardActions.isOpen = true; single: true,
selecteds: [...selecteds1, ...selecteds2],
mustSelects: [...mustSelect1, ...mustSelect2],
selectables: [...selectable1, ...selectable2],
});
}; };
import { getStrings, ygopro } from "@/api"; import { getStrings, ygopro } from "@/api";
import { messageStore } from "@/stores"; import { displayYesNoModal } from "@/ui/Duel/Message";
type MsgSelectYesNo = ygopro.StocGameMessage.MsgSelectYesNo; type MsgSelectYesNo = ygopro.StocGameMessage.MsgSelectYesNo;
...@@ -7,6 +7,6 @@ export default async (selectYesNo: MsgSelectYesNo) => { ...@@ -7,6 +7,6 @@ export default async (selectYesNo: MsgSelectYesNo) => {
const _player = selectYesNo.player; const _player = selectYesNo.player;
const effect_description = selectYesNo.effect_description; const effect_description = selectYesNo.effect_description;
messageStore.yesNoModal.msg = await getStrings(effect_description); const msg = await getStrings(effect_description);
messageStore.yesNoModal.isOpen = true; await displayYesNoModal(msg);
}; };
import { fetchCard, ygopro } from "@/api"; import { fetchCard, ygopro } from "@/api";
import { messageStore } from "@/stores"; import { displaySortCardModal } from "@/ui/Duel/Message";
type MsgSortCard = ygopro.StocGameMessage.MsgSortCard; type MsgSortCard = ygopro.StocGameMessage.MsgSortCard;
export default async (sortCard: MsgSortCard) => { export default async (sortCard: MsgSortCard) => {
await Promise.all( const options = await Promise.all(
sortCard.options.map(async ({ code, response }) => { sortCard.options.map(async ({ code, response }) => {
const meta = await fetchCard(code!); const meta = await fetchCard(code!);
messageStore.sortCardModal.options.push({ return {
meta, meta,
response: response!, response: response!,
}); };
}) })
); );
messageStore.sortCardModal.isOpen = true; await displaySortCardModal(options);
}; };
...@@ -6,15 +6,14 @@ import { subscribeKey } from "valtio/utils"; ...@@ -6,15 +6,14 @@ import { subscribeKey } from "valtio/utils";
import { fetchCard, ygopro } from "@/api"; import { fetchCard, ygopro } from "@/api";
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { sleep } from "@/infra"; import { sleep } from "@/infra";
import { cardStore, CardType, store } from "@/stores"; import { cardStore, CardType, matStore } from "@/stores";
const { matStore } = store;
const TOKEN_SIZE = 13; // 每人场上最多就只可能有13个token const TOKEN_SIZE = 13; // 每人场上最多就只可能有13个token
export default async (start: ygopro.StocGameMessage.MsgStart) => { export default async (start: ygopro.StocGameMessage.MsgStart) => {
// 先初始化`matStore` // 先初始化`matStore`
matStore.selfType = start.playerType; matStore.selfType = start.playerType;
const opponent = const opponent =
start.playerType == ygopro.StocGameMessage.MsgStart.PlayerType.FirstStrike start.playerType === ygopro.StocGameMessage.MsgStart.PlayerType.FirstStrike
? 1 ? 1
: 0; : 0;
......
...@@ -10,9 +10,9 @@ export default async (toss: MsgToss) => { ...@@ -10,9 +10,9 @@ export default async (toss: MsgToss) => {
const prefix = fetchStrings("!system", matStore.isMe(player) ? 102 : 103); const prefix = fetchStrings("!system", matStore.isMe(player) ? 102 : 103);
for (const x of toss.res) { for (const x of toss.res) {
if (tossType == MsgToss.TossType.DICE) { if (tossType === MsgToss.TossType.DICE) {
matStore.tossResult = prefix + fetchStrings("!system", 1624) + x; matStore.tossResult = prefix + fetchStrings("!system", 1624) + x;
} else if (tossType == MsgToss.TossType.COIN) { } else if (tossType === MsgToss.TossType.COIN) {
matStore.tossResult = matStore.tossResult =
prefix + prefix +
fetchStrings("!system", 1623) + fetchStrings("!system", 1623) +
......
...@@ -12,7 +12,7 @@ export default async (updateData: MsgUpdateData) => { ...@@ -12,7 +12,7 @@ export default async (updateData: MsgUpdateData) => {
const sequence = action.location?.sequence; const sequence = action.location?.sequence;
if (typeof sequence !== "undefined") { if (typeof sequence !== "undefined") {
const target = field const target = field
.filter((card) => card.location.sequence == sequence) .filter((card) => card.location.sequence === sequence)
.at(0); .at(0);
if (target) { if (target) {
// 目前只更新以下字段 // 目前只更新以下字段
......
...@@ -4,10 +4,10 @@ import { fetchEsHintMeta, matStore } from "@/stores"; ...@@ -4,10 +4,10 @@ import { fetchEsHintMeta, matStore } from "@/stores";
import MsgUpdateHp = ygopro.StocGameMessage.MsgUpdateHp; import MsgUpdateHp = ygopro.StocGameMessage.MsgUpdateHp;
export default (msgUpdateHp: MsgUpdateHp) => { export default (msgUpdateHp: MsgUpdateHp) => {
if (msgUpdateHp.type_ == MsgUpdateHp.ActionType.DAMAGE) { if (msgUpdateHp.type_ === MsgUpdateHp.ActionType.DAMAGE) {
fetchEsHintMeta({ originMsg: "玩家收到伤害时" }); // TODO: i18n fetchEsHintMeta({ originMsg: "玩家收到伤害时" }); // TODO: i18n
matStore.initInfo.of(msgUpdateHp.player).life -= msgUpdateHp.value; matStore.initInfo.of(msgUpdateHp.player).life -= msgUpdateHp.value;
} else if (msgUpdateHp.type_ == MsgUpdateHp.ActionType.RECOVER) { } else if (msgUpdateHp.type_ === MsgUpdateHp.ActionType.RECOVER) {
fetchEsHintMeta({ originMsg: "玩家生命值回复时" }); // TODO: i18n fetchEsHintMeta({ originMsg: "玩家生命值回复时" }); // TODO: i18n
matStore.initInfo.of(msgUpdateHp.player).life += msgUpdateHp.value; matStore.initInfo.of(msgUpdateHp.player).life += msgUpdateHp.value;
} }
......
import { ygopro } from "@/api"; 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) => { export default (_wait: ygopro.StocGameMessage.MsgWait) => {
for (const card of cardStore.inner) { for (const card of cardStore.inner) {
card.idleInteractivities = []; card.idleInteractivities = [];
} }
matStore.waiting = true; showWaiting(true);
}; };
...@@ -19,7 +19,7 @@ export default function handleSocketOpen( ...@@ -19,7 +19,7 @@ export default function handleSocketOpen(
) { ) {
console.log("WebSocket opened."); console.log("WebSocket opened.");
if (ws && ws.readyState == 1) { if (ws && ws.readyState === 1) {
ws.binaryType = "arraybuffer"; ws.binaryType = "arraybuffer";
sendPlayerInfo(ws, player); sendPlayerInfo(ws, player);
......
...@@ -29,23 +29,23 @@ export default function handleHsPlayerChange(pb: ygopro.YgoStocMsg) { ...@@ -29,23 +29,23 @@ export default function handleHsPlayerChange(pb: ygopro.YgoStocMsg) {
break; break;
} }
case ygopro.StocHsPlayerChange.State.READY: { case ygopro.StocHsPlayerChange.State.READY: {
playerStore[change.pos == 0 ? "player0" : "player1"].state = playerStore[change.pos === 0 ? "player0" : "player1"].state =
READY_STATE; READY_STATE;
break; break;
} }
case ygopro.StocHsPlayerChange.State.NO_READY: { case ygopro.StocHsPlayerChange.State.NO_READY: {
playerStore[change.pos == 0 ? "player0" : "player1"].state = playerStore[change.pos === 0 ? "player0" : "player1"].state =
NO_READY_STATE; NO_READY_STATE;
break; break;
} }
case ygopro.StocHsPlayerChange.State.LEAVE: { case ygopro.StocHsPlayerChange.State.LEAVE: {
playerStore[change.pos == 0 ? "player0" : "player1"] = {}; playerStore[change.pos === 0 ? "player0" : "player1"] = {};
break; break;
} }
case ygopro.StocHsPlayerChange.State.TO_OBSERVER: { case ygopro.StocHsPlayerChange.State.TO_OBSERVER: {
playerStore[change.pos == 0 ? "player0" : "player1"] = {}; // todo: 有没有必要? playerStore[change.pos === 0 ? "player0" : "player1"] = {}; // TODO: 有没有必要?
playerStore.observerCount += 1; playerStore.observerCount += 1;
break; break;
} }
......
...@@ -8,6 +8,6 @@ export default function handleHsPlayerEnter(pb: ygopro.YgoStocMsg) { ...@@ -8,6 +8,6 @@ export default function handleHsPlayerEnter(pb: ygopro.YgoStocMsg) {
if (pos > 1) { if (pos > 1) {
console.log("Currently only supported 2v2 mode."); console.log("Currently only supported 2v2 mode.");
} else { } else {
playerStore[pos == 0 ? "player0" : "player1"].name = name; playerStore[pos === 0 ? "player0" : "player1"].name = name;
} }
} }
import { ygopro } from "@/api"; import type { ygopro } from "@/api";
import { fetchCard, getCardStr } from "@/api/cards"; import { fetchCard, getCardStr } from "@/api/cards";
import { cardStore, messageStore } from "@/stores"; import { cardStore } from "@/stores";
import type { Option } from "@/ui/Duel/Message";
export const fetchCheckCardMeta = async ( const helper = async (
{ {
code, code,
location, location,
level1, level1,
level2, level2,
response, response,
effectDescCode, effect_description,
}: { }: {
code: number; code: number;
location: ygopro.CardLocation; location: ygopro.CardLocation;
level1?: number; level1?: number;
level2?: number; level2?: number;
response: number; response: number;
effectDescCode?: number; effect_description?: number;
}, },
selecteds: Option[],
mustSelects: Option[],
selectables: Option[],
selected?: boolean, selected?: boolean,
mustSelect?: boolean mustSelect?: boolean
) => { ) => {
...@@ -28,12 +32,12 @@ export const fetchCheckCardMeta = async ( ...@@ -28,12 +32,12 @@ export const fetchCheckCardMeta = async (
: cardStore.at(location.zone, controller, location.sequence)?.code || 0; : cardStore.at(location.zone, controller, location.sequence)?.code || 0;
const meta = await fetchCard(newID); const meta = await fetchCard(newID);
const effectDesc = effectDescCode const effectDesc = effect_description
? getCardStr(meta, effectDescCode & 0xf) ? getCardStr(meta, effect_description & 0xf)
: undefined; : undefined;
const newOption = { const newOption: Option = {
meta, meta,
location: location.toObject(), location,
level1, level1,
level2, level2,
effectDesc, effectDesc,
...@@ -41,10 +45,38 @@ export const fetchCheckCardMeta = async ( ...@@ -41,10 +45,38 @@ export const fetchCheckCardMeta = async (
}; };
if (selected) { if (selected) {
messageStore.selectCardActions.selecteds.push(newOption); selecteds.push(newOption);
} else if (mustSelect) { } else if (mustSelect) {
messageStore.selectCardActions.mustSelects.push(newOption); mustSelects.push(newOption);
} else { } else {
messageStore.selectCardActions.selectables.push(newOption); selectables.push(newOption);
}
};
export const fetchCheckCardMeta = async (
cards: {
code: number;
location: ygopro.CardLocation;
level1?: number;
level2?: number;
response: number;
effect_description?: number;
}[],
selected?: boolean,
mustSelect?: boolean
) => {
const selecteds: Option[] = [];
const mustSelects: Option[] = [];
const selectables: Option[] = [];
for (const card of cards) {
await helper(
card,
selecteds,
mustSelects,
selectables,
selected,
mustSelect
); // TODO: 研究下改成并行
} }
return { selecteds, mustSelects, selectables };
}; };
...@@ -49,8 +49,8 @@ class CardStore { ...@@ -49,8 +49,8 @@ class CardStore {
card.location.zone === zone && card.location.zone === zone &&
card.location.controller === controller && card.location.controller === controller &&
card.location.sequence === sequence && card.location.sequence === sequence &&
card.location.is_overlay == true && card.location.is_overlay === true &&
card.location.overlay_sequence == overlay_sequence card.location.overlay_sequence === overlay_sequence
) )
.at(0); .at(0);
} else { } else {
...@@ -60,7 +60,7 @@ class CardStore { ...@@ -60,7 +60,7 @@ class CardStore {
card.location.zone === zone && card.location.zone === zone &&
card.location.controller === controller && card.location.controller === controller &&
card.location.sequence === sequence && card.location.sequence === sequence &&
card.location.is_overlay == false card.location.is_overlay === false
) )
.at(0); .at(0);
} }
...@@ -69,7 +69,7 @@ class CardStore { ...@@ -69,7 +69,7 @@ class CardStore {
(card) => (card) =>
card.location.zone === zone && card.location.zone === zone &&
card.location.controller === controller && card.location.controller === controller &&
card.location.is_overlay == false card.location.is_overlay === false
); );
} }
} }
...@@ -84,9 +84,9 @@ class CardStore { ...@@ -84,9 +84,9 @@ class CardStore {
): CardType[] { ): CardType[] {
return this.inner.filter( return this.inner.filter(
(card) => (card) =>
card.location.zone == zone && card.location.zone === zone &&
card.location.controller == controller && card.location.controller === controller &&
card.location.sequence == sequence && card.location.sequence === sequence &&
card.location.is_overlay card.location.is_overlay
); );
} }
......
...@@ -2,33 +2,24 @@ export * from "./cardStore"; ...@@ -2,33 +2,24 @@ export * from "./cardStore";
export * from "./chatStore"; export * from "./chatStore";
export * from "./joinStore"; export * from "./joinStore";
export * from "./matStore"; export * from "./matStore";
export * from "./messageStore";
export * from "./methods";
export * from "./moraStore"; export * from "./moraStore";
export * from "./placeStore"; export * from "./placeStore";
export * from "./playerStore"; export * from "./playerStore";
import { proxy } from "valtio";
import { devtools } from "valtio/utils"; import { devtools } from "valtio/utils";
import { cardStore } from "./cardStore"; import { cardStore } from "./cardStore";
import { chatStore } from "./chatStore"; import { chatStore } from "./chatStore";
import { joinStore } from "./joinStore"; import { joinStore } from "./joinStore";
import { matStore } from "./matStore"; import { matStore } from "./matStore";
import { messageStore } from "./messageStore";
import { moraStore } from "./moraStore"; import { moraStore } from "./moraStore";
import { placeStore } from "./placeStore"; import { placeStore } from "./placeStore";
import { playerStore } from "./playerStore"; import { playerStore } from "./playerStore";
export const store = proxy({ devtools(playerStore, { name: "player", enabled: true });
playerStore, devtools(chatStore, { name: "chat", enabled: true });
chatStore, devtools(joinStore, { name: "join", enabled: true });
joinStore, devtools(moraStore, { name: "mora", enabled: true });
moraStore, devtools(matStore, { name: "mat", enabled: true });
matStore, // 决斗盘 devtools(cardStore, { name: "card", enabled: true });
messageStore, // 决斗的信息,包括模态框 devtools(placeStore, { name: "place", enabled: true });
cardStore,
placeStore,
});
devtools(store, { name: "valtio store", enabled: true });
...@@ -75,12 +75,11 @@ export const matStore: MatState = proxy<MatState>({ ...@@ -75,12 +75,11 @@ export const matStore: MatState = proxy<MatState>({
hint: { code: -1 }, hint: { code: -1 },
currentPlayer: -1, currentPlayer: -1,
phase: { phase: {
currentPhase: ygopro.StocGameMessage.MsgNewPhase.PhaseType.UNKNOWN, // TODO 当前的阶段 应该改成enum currentPhase: ygopro.StocGameMessage.MsgNewPhase.PhaseType.UNKNOWN,
enableBp: false, // 允许进入战斗阶段 enableBp: false, // 允许进入战斗阶段
enableM2: false, // 允许进入M2阶段 enableM2: false, // 允许进入M2阶段
enableEp: false, // 允许回合结束 enableEp: false, // 允许回合结束
}, },
waiting: false,
unimplemented: 0, unimplemented: 0,
// methods // methods
isMe, isMe,
......
...@@ -33,8 +33,6 @@ export interface MatState { ...@@ -33,8 +33,6 @@ export interface MatState {
reason: string; reason: string;
}; };
waiting: boolean;
unimplemented: number; // 未处理的`Message` unimplemented: number; // 未处理的`Message`
tossResult?: string; // 骰子/硬币结果 tossResult?: string; // 骰子/硬币结果
......
export * from "./methods";
export * from "./store";
import { messageStore } from "../store";
const { selectCardActions } = messageStore;
export const clearSelectActions = () => {
selectCardActions.isOpen = false;
selectCardActions.isValid = false;
selectCardActions.isChain = undefined;
selectCardActions.min = undefined;
selectCardActions.max = undefined;
selectCardActions.cancelAble = false;
selectCardActions.totalLevels = undefined;
selectCardActions.selecteds = [];
selectCardActions.selectables = [];
selectCardActions.mustSelects = [];
selectCardActions.finishAble = false;
selectCardActions.overflow = false;
selectCardActions.single = undefined;
};
export * from "./clearSelectActions";
import { proxy } from "valtio";
import type { ModalState } from "./types";
export const messageStore = proxy<ModalState>({
cardModal: { isOpen: false, interactivies: [], counters: {} },
cardListModal: { isOpen: false, list: [] },
selectCardActions: {
isOpen: false,
isValid: false,
cancelAble: false,
finishAble: false,
selecteds: [],
selectables: [],
mustSelects: [],
},
yesNoModal: { isOpen: false },
positionModal: { isOpen: false, positions: [] },
optionModal: { isOpen: false, options: [] },
checkCounterModal: {
isOpen: false,
options: [],
},
sortCardModal: {
isOpen: false,
options: [],
},
announceModal: {
isOpen: false,
min: 1,
options: [],
},
});
// >>> modal types >>>
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 {
// 卡牌弹窗
cardModal: {
isOpen: boolean;
meta?: CardMeta;
interactivies: { desc: string; response: number; effectCode?: number }[];
counters: { [type: number]: number };
};
// 卡牌列表弹窗
cardListModal: {
isOpen: boolean;
list: {
meta?: CardMeta;
interactivies: { desc: string; response: number }[];
}[];
};
// 卡牌选择状态
selectCardActions: {
// 是否打开
isOpen: boolean;
// 是否有效,当有`MSG_SELECT_xxx`到前端时为true,用户选择完成后设置为false
isValid: boolean;
// 如果是连锁,发response给后端的方式稍微有点不同,这里标记下
isChain?: boolean;
min?: number;
max?: number;
// 是否只能选择单个
single?: boolean;
cancelAble: boolean;
finishAble: boolean;
// 上级/同调/超量/链接召唤的总cost
totalLevels?: number;
// cost是否可以溢出,比如同调召唤是false,某些链接召唤是true
overflow?: boolean;
// 已经选择的列表
selecteds: Option[];
// 可以选择的列表
selectables: Option[];
// 必须选择的列表
mustSelects: Option[];
};
// Yes or No弹窗
yesNoModal: {
isOpen: boolean;
msg?: string;
};
// 表示形式选择弹窗
positionModal: {
isOpen: boolean;
positions: ygopro.CardPosition[];
};
// 选项选择弹窗
optionModal: {
isOpen: boolean;
options: { msg: string; response: number }[];
};
// 指示器选择弹窗
checkCounterModal: {
isOpen: boolean;
counterType?: number;
min?: number;
options: {
code: number;
max: number;
}[];
};
// 卡牌排序弹窗
sortCardModal: {
isOpen: boolean;
options: {
meta: CardMeta;
response: number;
}[];
};
// 宣言弹窗
announceModal: {
isOpen: boolean;
title?: string;
min: number;
options: {
info: string;
response: number;
}[];
};
}
...@@ -34,11 +34,11 @@ export const playerStore = proxy<PlayerState>({ ...@@ -34,11 +34,11 @@ export const playerStore = proxy<PlayerState>({
isHost: false, isHost: false,
selfType: SelfType.UNKNOWN, selfType: SelfType.UNKNOWN,
getMePlayer() { getMePlayer() {
if (this.selfType == SelfType.PLAYER1) return this.player0; if (this.selfType === SelfType.PLAYER1) return this.player0;
return this.player1; return this.player1;
}, },
getOpPlayer() { getOpPlayer() {
if (this.selfType == SelfType.PLAYER1) return this.player1; if (this.selfType === SelfType.PLAYER1) return this.player1;
return this.player0; return this.player0;
}, },
}); });
.card-modal {
position: fixed;
display: flex;
left: 5%;
top: 20%;
border-style: groove;
border-radius: 8px;
width: 200px;
flex-wrap: wrap;
background-color: #303030;
padding: 1%;
--visibility: hidden;
visibility: var(--visibility);
--opacity: 0;
opacity: var(--opacity);
transition:visibility 0.3s linear, opacity 0.3s linear;
}
.card-modal-container {
display: flex;
flex-direction: column;
gap: 5px;
}
.card-modal-name {
font-weight: bold;
}
.card-modal-attribute {
font-weight: bold;
}
.card-modal-atk {
font-weight: bold;
}
.card-modal-counter {
font-weight: bold;
}
.card-modal-effect {
font-weight: lighter;
text-align: left;
max-height: 200px;
overflow: auto;
}
.card-modal-btn {
margin: 1px 5px;
font-size: 80%;
border-color: yellow;
}
// ref: https://github.com/jvcjunior/login-react-redux // ref: https://github.com/jvcjunior/login-react-redux
// thanks! // thanks!
@charset "utf-8"; @charset "utf-8";
@import url("https://fonts.googleapis.com/css2?family=Electrolize&display=swap");
ol, ol,
ul { ul {
...@@ -33,8 +34,9 @@ table { ...@@ -33,8 +34,9 @@ table {
body { body {
color-scheme: light dark; color-scheme: light dark;
color: rgba(255, 255, 255, 0.87); color: rgba(255, 255, 255, 0.87);
background-color: #242424; background-color: #141414;
font: 87.5%/1.5em "Open Sans", sans-serif; // font: 87.5%/1.5em "Open Sans", sans-serif;
font-size: 14px;
display: flex; display: flex;
margin: 0; margin: 0;
place-items: center; place-items: center;
...@@ -89,3 +91,21 @@ p { ...@@ -89,3 +91,21 @@ p {
width: 100%; width: 100%;
max-width: 1000px; max-width: 1000px;
} }
img {
user-select: none;
-webkit-user-drag: none;
}
div,
p,
section,
span,
image,
img {
box-sizing: border-box;
}
body {
--theme-font: "Electrolize", sans-serif;
}
.select-modal {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 60px;
background: rgba(#333, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: 0.4s;
&-button {
font-family: "Nunito", sans-serif;
font-size: 18px;
cursor: pointer;
border: 0;
outline: 0;
padding: 10px 40px;
border-radius: 30px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.16);
transition: 0.3s;
}
}
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
OptionModal, OptionModal,
PositionModal, PositionModal,
SelectActionsModal, SelectActionsModal,
SimpleSelectCardsModal,
SortCardModal, SortCardModal,
YesNoModal, YesNoModal,
} from "./Message"; } from "./Message";
...@@ -18,6 +19,7 @@ import { LifeBar, Mat, Menu } from "./PlayMat"; ...@@ -18,6 +19,7 @@ import { LifeBar, Mat, Menu } from "./PlayMat";
const NeosDuel = () => { const NeosDuel = () => {
return ( return (
<> <>
<SelectActionsModal />
<Alert /> <Alert />
<Menu /> <Menu />
<LifeBar /> <LifeBar />
...@@ -25,13 +27,13 @@ const NeosDuel = () => { ...@@ -25,13 +27,13 @@ const NeosDuel = () => {
<CardModal /> <CardModal />
<CardListModal /> <CardListModal />
<HintNotification /> <HintNotification />
<SelectActionsModal />
<YesNoModal /> <YesNoModal />
<PositionModal /> <PositionModal />
<OptionModal /> <OptionModal />
<CheckCounterModal /> <CheckCounterModal />
<SortCardModal /> <SortCardModal />
<AnnounceModal /> <AnnounceModal />
<SimpleSelectCardsModal />
</> </>
); );
}; };
......
import { CheckCard } from "@ant-design/pro-components"; import { CheckCard } from "@ant-design/pro-components";
import { Button } from "antd"; import { Button } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { sendSelectOptionResponse } from "@/api"; import { sendSelectOptionResponse } from "@/api";
import { messageStore } from "@/stores";
import { DragModal } from "./DragModal"; import { NeosModal } from "./NeosModal";
const { announceModal } = messageStore; interface AnnounceModalProps {
isOpen: boolean;
title?: string;
min: number;
options: {
info: string;
response: number;
}[];
}
const defaultProps = {
isOpen: false,
min: 1,
options: [],
};
const localStore = proxy<AnnounceModalProps>(defaultProps);
export const AnnounceModal = () => { export const AnnounceModal = () => {
const snap = useSnapshot(announceModal); const { isOpen, title, min, options } = useSnapshot(localStore);
const isOpen = snap.isOpen;
const title = snap.title;
const min = snap.min;
const options = snap.options;
const [selected, setSelected] = useState<number[]>([]); const [selected, setSelected] = useState<number[]>([]);
return ( return (
<DragModal <NeosModal
title={title} title={title}
open={isOpen} open={isOpen}
closable={false}
footer={ footer={
<Button <Button
disabled={selected.length != min} disabled={selected.length != min}
onClick={() => { onClick={() => {
let response = selected.reduce((res, current) => res | current, 0); // 多个选择求或 let response = selected.reduce((res, current) => res | current, 0); // 多个选择求或
sendSelectOptionResponse(response); sendSelectOptionResponse(response);
announceModal.isOpen = false; rs();
announceModal.title = undefined;
announceModal.options = [];
}} }}
> >
submit submit
...@@ -51,6 +58,23 @@ export const AnnounceModal = () => { ...@@ -51,6 +58,23 @@ export const AnnounceModal = () => {
<CheckCard key={idx} title={option.info} value={option.response} /> <CheckCard key={idx} title={option.info} value={option.response} />
))} ))}
</CheckCard.Group> </CheckCard.Group>
</DragModal> </NeosModal>
); );
}; };
let rs: (arg?: any) => void = () => {};
export const displayAnnounceModal = async (
args: Omit<AnnounceModalProps, "isOpen">
) => {
Object.entries(args).forEach(([key, value]) => {
// @ts-ignore
localStore[key] = value;
});
localStore.isOpen = true;
await new Promise<void>((resolve) => (rs = resolve)); // 等待在组件内resolve
localStore.isOpen = false;
localStore.min = 1;
localStore.options = [];
localStore.title = undefined;
};
import { Drawer, List } from "antd"; import { Drawer, Space } from "antd";
import React from "react"; import React from "react";
import { useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { useConfig } from "@/config"; import { ygopro } from "@/api";
import { messageStore } from "@/stores"; import { cardStore, CardType } from "@/stores";
import { YgoCard } from "@/ui/Shared";
import { EffectButton } from "./EffectButton"; import { showCardModal } from "./CardModal";
const NeosConfig = useConfig();
const CARD_WIDTH = 100; const CARD_WIDTH = 100;
const { cardListModal } = messageStore; // TODO: 显示的位置还需要细细斟酌
const defaultStore = {
zone: ygopro.CardZone.HAND,
controller: 0,
monster: {} as CardType,
isOpen: false,
isZone: true,
};
const store = proxy(defaultStore);
export const CardListModal = () => { export const CardListModal = () => {
const snap = useSnapshot(cardListModal); const { zone, monster, isOpen, isZone, controller } = useSnapshot(store);
const isOpen = snap.isOpen; let cardList: CardType[] = [];
const list = snap.list as typeof cardListModal.list;
if (isZone) {
cardList = cardStore.at(zone, controller);
} else {
// 看超量素材
cardList = cardStore.findOverlay(
monster.location.zone,
monster.location.controller,
monster.location.sequence
);
}
const handleOkOrCancel = () => { const handleOkOrCancel = () => {
cardListModal.isOpen = false; store.isOpen = false;
}; };
return ( return (
<Drawer open={isOpen} onClose={handleOkOrCancel}> <Drawer
<List open={isOpen}
itemLayout="horizontal" onClose={handleOkOrCancel}
dataSource={list} // headerStyle={{ display: "none" }}
renderItem={(item) => ( width={CARD_WIDTH + 66}
<List.Item style={{ maxHeight: "100%" }}
actions={[ mask={false}
<EffectButton >
effectInteractivies={item.interactivies} <Space direction="vertical">
meta={item.meta} {cardList.map((card) => (
/>, <YgoCard
]} code={card.code}
extra={ key={card.uuid}
<img width={CARD_WIDTH}
alt={item.meta?.text.name} onClick={() => showCardModal(card)}
src={ />
item.meta?.id ))}
? `${NeosConfig.cardImgUrl}/${item.meta.id}.jpg` </Space>
: `${NeosConfig.assetsPath}/card_back.jpg`
}
style={{ width: CARD_WIDTH }}
/>
}
onClick={() => {
messageStore.cardModal.meta = item.meta;
messageStore.cardModal.interactivies = item.interactivies;
messageStore.cardModal.counters = [];
messageStore.cardModal.isOpen = true;
}}
>
<List.Item.Meta
title={item.meta?.text.name}
description={item.meta?.text.desc}
/>
</List.Item>
)}
></List>
</Drawer> </Drawer>
); );
}; };
export const displayCardListModal = ({
isZone,
monster,
zone,
controller,
}: Partial<Omit<typeof defaultStore, "isOpen">>) => {
store.isOpen = true;
isZone && (store.isZone = isZone);
monster && (store.monster = monster);
zone && (store.zone = zone);
controller && (store.controller = controller);
};
.card-modal-desc {
line-height: 1.6;
font-size: 14px;
font-family: var(--theme-font);
max-height: calc(100% - 237px);
overflow-y: overlay;
&:hover {
&::-webkit-scrollbar-thumb {
background: #535353;
}
}
& > div {
margin-bottom: 6px;
}
&::-webkit-scrollbar {
/*滚动条整体样式*/
width: 6px; /*高宽分别对应横竖滚动条的尺寸*/
height: 1px;
}
&::-webkit-scrollbar-thumb {
/*滚动条里面小方块*/
border-radius: 10px;
background: #5353533b;
cursor: pointer;
}
.maro-item {
display: flex;
gap: 8px;
}
}
import "./Desc.scss";
import { Fragment } from "react";
export const Desc: React.FC<{ desc?: string }> = ({ desc = "" }) => {
if (!desc) return <></>;
return (
<div className="card-modal-desc">
{/* https://125naroom.com/web/2877 */}
{/* 牛逼的丸文字css教程 */}
<RegexWrapper
text={addSpaces(desc)}
re={/(①|②|③|④|⑤|⑥|⑦|⑧|⑨|⑩):.+?(?=((①|②|③|④|⑤|⑥|⑦|⑧|⑨|⑩):|$))/gs}
Wrapper={MaroListItem}
/>
</div>
);
};
/** 使用re去提取文本,并且将提取到的文本用Wrapper进行环绕 */
const RegexWrapper: React.FC<{
text: string;
re: RegExp;
Wrapper: React.FunctionComponent<any>;
}> = ({ text, re, Wrapper }) => {
const matches = text.match(re);
if (!matches) return <>{text}</>;
const sepRe = new RegExp(
matches?.reduce((acc, cur) => `${acc}|${cur}`) ?? ""
);
const parts = text.split(sepRe);
return (
<>
{parts.map((part, index) => (
<Fragment key={`${part}-${index}`}>
<div>{part}</div>
{index !== parts.length - 1 && <Wrapper>{matches?.[index]}</Wrapper>}
</Fragment>
))}
</>
);
};
const MaroListItem: React.FC<{ children: string }> = ({ children }) => {
return (
<div className="maro-item">
<span>{children[0]}</span>
<span>
<RegexWrapper
text={children.slice(2)}
re={/●.+?(?=(●|$))/gs}
Wrapper={CircleListItem}
/>
</span>
</div>
);
};
const CircleListItem: React.FC<{ children: string }> = ({ children }) => {
return children ? (
<div className="maro-item">
<span>{children[0]}</span>
<span>{children.slice(1)}</span>
</div>
) : (
<></>
);
};
function addSpaces(str: string): string {
const regex = /\d+/g;
return str.replace(regex, (match) => ` ${match} `);
}
.card-modal-root {
.ant-drawer-content-wrapper {
box-shadow: none;
}
.card-modal-drawer {
width: 90%;
left: 10%;
--height: 640px;
top: calc((100% - var(--height)) / 2);
height: var(--height);
position: relative;
border-radius: 6px;
background: #242424;
.ant-drawer-header {
padding: 15px 0;
.ant-drawer-header-title {
flex-direction: row-reverse;
padding-left: 24px;
}
}
}
.card-modal-container {
position: relative;
height: 100%;
}
}
.card-modal-name {
font-weight: bold;
font-size: 1.2rem;
}
.atkLine {
.title,
.number {
font-family: var(--theme-font);
}
.number {
font-size: 30px;
line-height: 36px;
}
}
.attline {
display: flex;
flex-wrap: wrap;
row-gap: 10px;
}
.card-modal-info {
justify-content: space-between;
position: relative;
height: 204px; // TODO - fix this
}
import "@/styles/card-modal.scss"; import "./index.scss";
import classnames from "classnames"; import { LeftOutlined } from "@ant-design/icons";
import { Divider, Drawer, Space, Tag } from "antd";
import React from "react"; import React from "react";
import { useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { fetchStrings, sendSelectIdleCmdResponse } from "@/api"; import { type CardMeta, fetchStrings } from "@/api";
import { useConfig } from "@/config"; import { YgoCard } from "@/ui/Shared";
import { cardStore, messageStore } from "@/stores";
import { import {
Attribute2StringCodeMap, Attribute2StringCodeMap,
extraCardTypes, extraCardTypes,
Race2StringCodeMap, Race2StringCodeMap,
Type2StringCodeMap, Type2StringCodeMap,
} from "../../../common"; } from "../../../../common";
import { EffectButton } from "./EffectButton"; import { Desc } from "./Desc";
const { cardModal } = messageStore; const CARD_WIDTH = 140;
const NeosConfig = useConfig();
const CARD_WIDTH = 200; const defaultStore = {
isOpen: false,
meta: {
id: 0,
data: {},
text: {
name: "",
desc: "",
},
} satisfies CardMeta as CardMeta,
interactivies: [] as {
desc: string;
response: number;
effectCode?: number;
}[],
counters: {} as Record<number, number>,
};
const store = proxy(defaultStore);
export const CardModal = () => { export const CardModal = () => {
const snap = useSnapshot(cardModal); const snap = useSnapshot(store);
const isOpen = snap.isOpen; const { isOpen, meta, counters: _counters } = snap;
const meta = snap.meta;
const name = meta?.text.name; const name = meta?.text.name;
const types = meta?.data.type; const types = meta?.data.type;
...@@ -33,40 +50,46 @@ export const CardModal = () => { ...@@ -33,40 +50,46 @@ export const CardModal = () => {
const desc = meta?.text.desc; const desc = meta?.text.desc;
const atk = meta?.data.atk; const atk = meta?.data.atk;
const def = meta?.data.def; const def = meta?.data.def;
const counters = snap.counters;
const imgUrl = meta?.id
? `${NeosConfig.cardImgUrl}/${meta.id}.jpg`
: undefined;
const nonEffectInteractivies = snap.interactivies.filter(
(item) => item.desc != "发动效果"
);
const effectInteractivies = snap.interactivies.filter(
(item) => item.desc == "发动效果"
);
return ( return (
<div // TODO: 宽度要好好设置 根据屏幕宽度
className={classnames("card-modal")} <Drawer
style={ open={isOpen}
{ placement="left"
"--visibility": isOpen ? "visible" : "hidden", onClose={() => (store.isOpen = false)}
"--opacity": isOpen ? 1 : 0, rootClassName="card-modal-root"
} as any className="card-modal-drawer"
} mask={false}
title={name}
closeIcon={<LeftOutlined />}
width={350}
> >
<div className="card-modal-container"> <div className="card-modal-container">
<img src={imgUrl} width={CARD_WIDTH} /> <Space
<div className="card-modal-name">{name}</div> align="start"
<AttLine size={18}
types={extraCardTypes(types || 0)} style={{ position: "relative", display: "flex" }}
race={race} >
attribute={attribute} <YgoCard
/> code={meta?.id}
<AtkLine atk={atk} def={def} /> width={CARD_WIDTH}
<CounterLine counters={counters} /> style={{ borderRadius: 4 }}
<div className="card-modal-effect">{desc}</div> />
{nonEffectInteractivies.map((interactive, idx) => { <Space direction="vertical" className="card-modal-info">
<AtkLine atk={atk} def={def} />
<AttLine
types={extraCardTypes(types || 0)}
race={race}
attribute={attribute}
/>
{/* TODO: 只有怪兽卡需要展示攻击防御 */}
{/* TODO: 展示星级/LINK数 */}
{/* <CounterLine counters={counters} /> */}
</Space>
</Space>
<Divider style={{ margin: "14px 0" }}></Divider>
<Desc desc={desc} />
{/* {nonEffectInteractivies.map((interactive, idx) => {
return ( return (
<button <button
key={idx} key={idx}
...@@ -85,9 +108,9 @@ export const CardModal = () => { ...@@ -85,9 +108,9 @@ export const CardModal = () => {
</button> </button>
); );
})} })}
<EffectButton meta={meta} effectInteractivies={effectInteractivies} /> <EffectButton meta={meta} effectInteractivies={effectInteractivies} /> */}
</div> </div>
</div> </Drawer>
); );
}; };
...@@ -98,25 +121,37 @@ const AttLine = (props: { ...@@ -98,25 +121,37 @@ const AttLine = (props: {
}) => { }) => {
const race = props.race const race = props.race
? fetchStrings("!system", Race2StringCodeMap.get(props.race) || 0) ? fetchStrings("!system", Race2StringCodeMap.get(props.race) || 0)
: "?"; : undefined;
const attribute = props.attribute const attribute = props.attribute
? fetchStrings("!system", Attribute2StringCodeMap.get(props.attribute) || 0) ? fetchStrings("!system", Attribute2StringCodeMap.get(props.attribute) || 0)
: "?"; : undefined;
const types = props.types const types = props.types
.map((t) => fetchStrings("!system", Type2StringCodeMap.get(t) || 0)) .map((t) => fetchStrings("!system", Type2StringCodeMap.get(t) || 0))
.join("|"); .join("/");
return ( return (
<div className="card-modal-attribute">{`【 ${race} / ${types} 】【 ${attribute} 】`}</div> <div className="attline">
{attribute && <Tag>{attribute}</Tag>}
{race && <Tag>{race}</Tag>}
{types && <Tag>{types}</Tag>}
</div>
); );
}; };
const AtkLine = (props: { atk?: number; def?: number }) => ( const AtkLine = (props: { atk?: number; def?: number }) => (
<div className="card-modal-atk">{`ATK/${ <Space size={10} className="atkLine" direction="vertical">
props.atk !== undefined ? props.atk : "?" <div>
} DEF/${props.def !== undefined ? props.def : "?"}`}</div> <div className="title">ATK</div>
<div className="number">{props.atk ?? "?"}</div>
</div>
<div>
<div className="title">DEF</div>
<div className="number">{props.def ?? "?"}</div>
</div>
</Space>
); );
const CounterLine = (props: { counters: { [type: number]: number } }) => { // TODO: 未完成,研究一下怎么展示这个信息
const _CounterLine = (props: { counters: { [type: number]: number } }) => {
const counters = []; const counters = [];
for (const counterType in props.counters) { for (const counterType in props.counters) {
const count = props.counters[counterType]; const count = props.counters[counterType];
...@@ -134,3 +169,15 @@ const CounterLine = (props: { counters: { [type: number]: number } }) => { ...@@ -134,3 +169,15 @@ const CounterLine = (props: { counters: { [type: number]: number } }) => {
</> </>
); );
}; };
export const showCardModal = (
card: Partial<Pick<typeof store, "meta" | "counters">>
) => {
store.isOpen = true;
store.meta = card?.meta ?? defaultStore.meta;
store.counters = card?.counters ?? defaultStore.counters;
};
export const closeCardModal = () => {
store.isOpen = false;
};
// 指示器选择弹窗
import { Omit } from "@react-spring/web";
import { Button, Card, Col, InputNumber, Row } from "antd"; import { Button, Card, Col, InputNumber, Row } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { fetchStrings, sendSelectCounterResponse } from "@/api"; import { fetchStrings, sendSelectCounterResponse } from "@/api";
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { messageStore } from "@/stores";
import { DragModal } from "./DragModal"; import { NeosModal } from "./NeosModal";
const { checkCounterModal } = messageStore; interface CheckCounterModalProps {
isOpen: boolean;
counterType?: number;
min?: number;
options: {
code: number;
max: number;
}[];
}
const defaultProps = {
isOpen: false,
options: [],
};
const localStore = proxy<CheckCounterModalProps>(defaultProps);
const NeosConfig = useConfig(); const NeosConfig = useConfig();
export const CheckCounterModal = () => { export const CheckCounterModal = () => {
const snapCheckCounterModal = useSnapshot(checkCounterModal); const snapCheckCounterModal = useSnapshot(localStore);
const isOpen = snapCheckCounterModal.isOpen; const isOpen = snapCheckCounterModal.isOpen;
const min = snapCheckCounterModal.min || 0; const min = snapCheckCounterModal.min || 0;
...@@ -24,21 +39,17 @@ export const CheckCounterModal = () => { ...@@ -24,21 +39,17 @@ export const CheckCounterModal = () => {
const [selected, setSelected] = useState(new Array(options.length)); const [selected, setSelected] = useState(new Array(options.length));
const sum = selected.reduce((sum, current) => sum + current, 0); const sum = selected.reduce((sum, current) => sum + current, 0);
const finishable = sum == min; const finishable = sum === min;
const onFinish = () => { const onFinish = () => {
sendSelectCounterResponse(selected); sendSelectCounterResponse(selected);
messageStore.checkCounterModal.isOpen = false; rs();
messageStore.checkCounterModal.min = undefined;
messageStore.checkCounterModal.counterType = undefined;
messageStore.checkCounterModal.options = [];
}; };
return ( return (
<DragModal <NeosModal
title={`请移除${min}个${counterName}`} title={`请移除${min}个${counterName}`}
open={isOpen} open={isOpen}
closable={false}
footer={ footer={
<Button disabled={!finishable} onClick={onFinish}> <Button disabled={!finishable} onClick={onFinish}>
finish finish
...@@ -75,6 +86,23 @@ export const CheckCounterModal = () => { ...@@ -75,6 +86,23 @@ export const CheckCounterModal = () => {
); );
})} })}
</Row> </Row>
</DragModal> </NeosModal>
); );
}; };
let rs: (arg?: any) => void = () => {};
export const displayCheckCounterModal = async (
args: Omit<CheckCounterModalProps, "isOpen">
) => {
Object.entries(args).forEach(([key, value]) => {
// @ts-ignore
localStore[key] = value;
});
localStore.isOpen = true;
await new Promise<void>((resolve) => (rs = resolve)); // 等待在组件内resolve
localStore.isOpen = false;
localStore.options = [];
localStore.min = undefined;
localStore.counterType = undefined;
};
// 经过封装的可拖拽`Modal`
import { Modal, ModalProps } from "antd";
import React, { useRef, useState } from "react";
import type { DraggableData, DraggableEvent } from "react-draggable";
import Draggable from "react-draggable";
export interface DragModalProps extends ModalProps {}
const style = {
borderStyle: "groove",
borderRadius: "8px",
backgroundColor: "#303030",
};
export const DragModal = (props: DragModalProps) => {
const dragRef = useRef<HTMLDivElement>(null);
const [bounds, setBounds] = useState({
left: 0,
top: 0,
bottom: 0,
right: 0,
});
const onStart = (_event: DraggableEvent, uiData: DraggableData) => {
const { clientWidth, clientHeight } = window.document.documentElement;
const targetRect = dragRef.current?.getBoundingClientRect();
if (!targetRect) {
return;
}
setBounds({
left: -targetRect.left + uiData.x,
right: clientWidth - (targetRect.right - uiData.x),
top: -targetRect.top + uiData.y,
bottom: clientHeight - (targetRect.bottom - uiData.y),
});
};
return (
<Modal
{...props}
modalRender={(modal) => (
<Draggable bounds={bounds} onStart={onStart}>
<div ref={dragRef} style={style}>
{modal}
</div>
</Draggable>
)}
>
{props.children}
</Modal>
);
};
import "@/styles/card-modal.scss";
import React from "react";
import { CardMeta, getCardStr, sendSelectIdleCmdResponse } from "@/api";
import { cardStore, messageStore } from "@/stores";
const { cardModal } = messageStore;
export const EffectButton = (props: {
meta?: CardMeta;
effectInteractivies: {
desc: string;
response: number;
effectCode?: number;
}[];
}) => (
<>
{props.effectInteractivies.length > 0 ? (
props.effectInteractivies.length == 1 ? (
// 如果只有一个效果,点击直接触发
<button
className="card-modal-btn"
onClick={() => {
sendSelectIdleCmdResponse(props.effectInteractivies[0].response);
cardModal.isOpen = false;
// 清空互动性
for (const card of cardStore.inner) {
card.idleInteractivities = [];
}
}}
>
{props.effectInteractivies[0].desc}
</button>
) : (
// 如果有多个效果,点击后进入`OptionModal`选择
<button
className="card-modal-btn"
onClick={() => {
for (const effect of props.effectInteractivies) {
const effectMsg =
props.meta && effect.effectCode
? getCardStr(props.meta, effect.effectCode & 0xf) ?? "[:?]"
: "[:?]";
messageStore.optionModal.options.push({
msg: effectMsg,
response: effect.response,
});
}
cardModal.isOpen = false;
// 清空互动性
for (const card of cardStore.inner) {
card.idleInteractivities = [];
}
messageStore.optionModal.isOpen = true;
}}
>
发动效果
</button>
)
) : (
<></>
)}
</>
);
.neos-message .ant-message-notice-content {
background-color: #333;
}
import { notification } from "antd"; import "./index.scss";
import { message, notification } from "antd";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
...@@ -14,21 +16,27 @@ const style = { ...@@ -14,21 +16,27 @@ const style = {
}; };
const NeosConfig = useConfig(); const NeosConfig = useConfig();
let globalMsgApi: ReturnType<typeof message.useMessage>[0] | undefined;
export const HintNotification = () => { export const HintNotification = () => {
const snap = useSnapshot(matStore); const snap = useSnapshot(matStore);
const hintState = snap.hint; const hintState = snap.hint;
const toss = snap.tossResult; const toss = snap.tossResult;
const currentPhase = snap.phase.currentPhase; const currentPhase = snap.phase.currentPhase;
const waiting = snap.waiting; // const waiting = snap.waiting;
const result = snap.result; const result = snap.result;
const [api, contextHolder] = notification.useNotification({ const [notify, notifyContextHolder] = notification.useNotification({
maxCount: NeosConfig.ui.hint.maxCount,
});
const [msgApi, msgContextHolder] = message.useMessage({
maxCount: NeosConfig.ui.hint.maxCount, maxCount: NeosConfig.ui.hint.maxCount,
}); });
globalMsgApi = msgApi;
useEffect(() => { useEffect(() => {
if (hintState && hintState.msg) { if (hintState && hintState.msg) {
api.open({ notify.open({
message: `${hintState.msg}`, message: `${hintState.msg}`,
placement: "topLeft", placement: "topLeft",
style: style, style: style,
...@@ -38,7 +46,7 @@ export const HintNotification = () => { ...@@ -38,7 +46,7 @@ export const HintNotification = () => {
useEffect(() => { useEffect(() => {
if (toss) { if (toss) {
api.open({ notify.open({
message: `${toss}`, message: `${toss}`,
placement: "topLeft", placement: "topLeft",
style: style, style: style,
...@@ -52,7 +60,7 @@ export const HintNotification = () => { ...@@ -52,7 +60,7 @@ export const HintNotification = () => {
"!system", "!system",
Phase2StringCodeMap.get(currentPhase) ?? 0 Phase2StringCodeMap.get(currentPhase) ?? 0
); );
api.open({ notify.open({
message, message,
placement: "topRight", placement: "topRight",
style: style, style: style,
...@@ -63,21 +71,10 @@ export const HintNotification = () => { ...@@ -63,21 +71,10 @@ export const HintNotification = () => {
} }
}, [currentPhase]); }, [currentPhase]);
useEffect(() => {
if (waiting) {
api.open({
message: fetchStrings("!system", 1390),
placement: "top",
duration: NeosConfig.ui.hint.waitingDuration,
style: style,
});
}
}, [waiting]);
useEffect(() => { useEffect(() => {
if (result) { if (result) {
const message = result.isWin ? "Win" : "Defeated" + " " + result.reason; const message = result.isWin ? "Win" : "Defeated" + " " + result.reason;
api.open({ notify.open({
message, message,
placement: "bottom", placement: "bottom",
style: style, style: style,
...@@ -85,5 +82,38 @@ export const HintNotification = () => { ...@@ -85,5 +82,38 @@ export const HintNotification = () => {
} }
}, [result]); }, [result]);
return <>{contextHolder}</>; return (
<>
{notifyContextHolder}
{msgContextHolder}
</>
);
};
// 防抖的waiting msg
let isWaiting = false;
let destoryTimer: NodeJS.Timeout | undefined;
const waitingKey = "waiting";
export const showWaiting = (open: boolean) => {
if (open) {
if (!isWaiting) {
globalMsgApi?.open({
type: "loading",
content: fetchStrings("!system", 1390),
key: waitingKey,
className: "neos-message",
duration: 0,
});
clearTimeout(destoryTimer);
isWaiting = true;
destoryTimer = undefined;
}
} else {
if (!destoryTimer) {
destoryTimer = setTimeout(() => {
globalMsgApi?.destroy(waitingKey);
isWaiting = false;
}, 1000);
}
}
}; };
.neos-modal {
position: fixed;
left: 0;
right: 0;
top: 0 !important;
bottom: 0 !important;
top: -webkit-fill-available;
margin: auto;
height: fit-content;
transition: 0.3s;
}
.neos-modal-mini {
top: 100% !important;
bottom: 0 !important;
transform: translateY(calc(50% - 66px));
.ant-modal-body {
animation: fadeout 0.3s forwards;
}
}
.neos-modal-wrap {
pointer-events: none;
}
@keyframes fadeout {
0% {
opacity: 1;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
import "./index.scss";
import { MinusOutlined, UpOutlined } from "@ant-design/icons";
import { Modal, type ModalProps } from "antd";
import classNames from "classnames";
import { useState } from "react";
interface Props extends ModalProps {
canBeMinimized?: boolean;
}
export const NeosModal: React.FC<Props> = (props) => {
const { canBeMinimized = true } = props;
const [mini, setMini] = useState(false);
return (
<Modal
className={classNames("neos-modal", { "neos-modal-mini": mini })}
centered
maskClosable={true}
onCancel={() => setMini(!mini)}
closeIcon={mini ? <UpOutlined /> : <MinusOutlined />}
bodyStyle={{ padding: "10px 0" }}
mask={!mini}
wrapClassName={classNames({ "neos-modal-wrap": mini })}
closable={canBeMinimized}
{...props}
/>
);
};
import { CheckCard } from "@ant-design/pro-components"; import { CheckCard } from "@ant-design/pro-components";
import { Button } from "antd"; import { Button } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { sendSelectOptionResponse } from "@/api"; import {
import { messageStore } from "@/stores"; type CardMeta,
getCardStr,
sendSelectIdleCmdResponse,
sendSelectOptionResponse,
} from "@/api";
import { DragModal } from "./DragModal"; import { NeosModal } from "./NeosModal";
const { optionModal } = messageStore; type Options = { msg: string; response: number }[];
const defaultStore = {
isOpen: false,
options: [] satisfies Options as Options,
};
const store = proxy(defaultStore);
export const OptionModal = () => { export const OptionModal = () => {
const snapOptionModal = useSnapshot(optionModal); const snap = useSnapshot(store);
const isOpen = snapOptionModal.isOpen; const { isOpen, options } = snap;
const options = snapOptionModal.options;
const [selected, setSelected] = useState<number | undefined>(undefined); const [selected, setSelected] = useState<number | undefined>(undefined);
const onClick = () => {
if (selected !== undefined) {
sendSelectOptionResponse(selected);
rs();
}
};
return ( return (
<DragModal <NeosModal
title="请选择需要发动的效果" title="请选择需要发动的效果"
open={isOpen} open={isOpen}
closable={false}
footer={ footer={
<Button <Button disabled={selected === undefined} onClick={onClick}>
disabled={selected === undefined} 确定
onClick={() => {
if (selected !== undefined) {
sendSelectOptionResponse(selected);
optionModal.isOpen = false;
optionModal.options = [];
}
}}
>
submit
</Button> </Button>
} }
> >
<CheckCard.Group <CheckCard.Group bordered size="small" onChange={setSelected as any}>
bordered
size="small"
onChange={(value) => {
// @ts-ignore
setSelected(value);
}}
>
{options.map((option, idx) => ( {options.map((option, idx) => (
<CheckCard key={idx} title={option.msg} value={option.response} /> <CheckCard key={idx} title={option.msg} value={option.response} />
))} ))}
</CheckCard.Group> </CheckCard.Group>
</DragModal> </NeosModal>
); );
}; };
let rs: (v?: any) => void = () => {};
export const displayOptionModal = async (options: Options) => {
store.isOpen = true;
store.options = options;
await new Promise((resolve) => (rs = resolve));
store.isOpen = false;
};
export const handleEffectActivation = async (
meta: CardMeta,
effectInteractivies: {
desc: string;
response: number;
effectCode: number | undefined;
}[]
) => {
if (!effectInteractivies.length) {
return;
}
if (effectInteractivies.length === 1) {
// 如果只有一个效果,点击直接触发
sendSelectIdleCmdResponse(effectInteractivies[0].response);
} else {
// optionsModal
const options = effectInteractivies.map((effect) => {
const effectMsg =
meta && effect.effectCode
? getCardStr(meta, effect.effectCode & 0xf) ?? "[:?]"
: "[:?]";
return {
msg: effectMsg,
response: effect.response,
};
});
await displayOptionModal(options); // 主动发动效果,所以不需要await,但是以后可能要留心
}
};
// 表示形式选择弹窗
import { CheckCard } from "@ant-design/pro-components"; import { CheckCard } from "@ant-design/pro-components";
import { Button } from "antd"; import { Button } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { sendSelectPositionResponse, ygopro } from "@/api"; import { sendSelectPositionResponse, ygopro } from "@/api";
import { messageStore } from "@/stores";
import { DragModal } from "./DragModal"; import { NeosModal } from "./NeosModal";
const { positionModal } = messageStore; interface PositionModalProps {
isOpen: boolean;
positions: ygopro.CardPosition[];
}
const defaultProps = { isOpen: false, positions: [] };
export const PositionModal = () => { const localStore = proxy<PositionModalProps>(defaultProps);
const snapPositionModal = useSnapshot(positionModal);
const isOpen = snapPositionModal.isOpen;
const positions = snapPositionModal.positions;
export const PositionModal = () => {
const { isOpen, positions } = useSnapshot(localStore);
const [selected, setSelected] = useState<ygopro.CardPosition | undefined>( const [selected, setSelected] = useState<ygopro.CardPosition | undefined>(
undefined undefined
); );
return ( return (
<DragModal <NeosModal
title="请选择表示形式" title="请选择表示形式"
open={isOpen} open={isOpen}
closable={false}
footer={ footer={
<Button <Button
disabled={selected === undefined} disabled={selected === undefined}
onClick={() => { onClick={() => {
if (selected !== undefined) { if (selected !== undefined) {
sendSelectPositionResponse(selected); sendSelectPositionResponse(selected);
positionModal.isOpen = false; rs();
positionModal.positions = [];
} }
}} }}
> >
...@@ -55,12 +56,13 @@ export const PositionModal = () => { ...@@ -55,12 +56,13 @@ export const PositionModal = () => {
/> />
))} ))}
</CheckCard.Group> </CheckCard.Group>
</DragModal> </NeosModal>
); );
}; };
function cardPositionToChinese(position: ygopro.CardPosition): string { function cardPositionToChinese(position: ygopro.CardPosition): string {
switch (position) { switch (position) {
// TODO: i18n
case ygopro.CardPosition.FACEUP_ATTACK: { case ygopro.CardPosition.FACEUP_ATTACK: {
return "正面攻击形式"; return "正面攻击形式";
} }
...@@ -78,3 +80,15 @@ function cardPositionToChinese(position: ygopro.CardPosition): string { ...@@ -78,3 +80,15 @@ function cardPositionToChinese(position: ygopro.CardPosition): string {
} }
} }
} }
let rs: (arg?: any) => void = () => {};
export const displayPositionModal = async (
positions: ygopro.CardPosition[]
) => {
localStore.positions = positions;
localStore.isOpen = true;
await new Promise<void>((resolve) => (rs = resolve));
localStore.isOpen = false;
localStore.positions = [];
};
import "@/styles/select-modal.scss";
import { MinusOutlined, ThunderboltOutlined } from "@ant-design/icons";
import { CheckCard, CheckCardProps } from "@ant-design/pro-components";
import { Button, Card, Col, Popover, Row, Tabs } from "antd";
import React, { useState } from "react";
import { useSnapshot } from "valtio";
import {
fetchStrings,
sendSelectMultiResponse,
sendSelectSingleResponse,
} from "@/api";
import { useConfig } from "@/config";
import { clearSelectActions, matStore, messageStore } from "@/stores";
import { groupBy } from "../utils";
import { DragModal } from "./DragModal";
const NeosConfig = useConfig();
const CANCEL_RESPONSE = -1;
const FINISH_RESPONSE = -1;
const { selectCardActions, cardModal } = messageStore;
export const SelectActionsModal = () => {
const snap = useSnapshot(selectCardActions);
const isOpen = snap.isOpen;
const isValid = snap.isValid;
const isChain = snap.isChain;
const min = snap.min ?? 0;
const max = snap.max ?? 0;
const single = snap.single ?? false;
const selecteds = snap.selecteds;
const selectables = snap.selectables;
const mustSelects = snap.mustSelects;
const [response, setResponse] = useState([]);
const hint = useSnapshot(matStore.hint);
const preHintMsg = hint?.esHint || "";
const selectHintMsg = hint?.esSelectHint || "请选择卡片";
const cancelable = snap.cancelAble;
const finishable = snap.finishAble;
const totalLevels = snap.totalLevels ?? 0;
const overflow = snap.overflow || false;
const LevelSum1 = mustSelects
.concat(response)
.map((option) => option.level1 || 0)
.reduce((sum, current) => sum + current, 0);
const LevelSum2 = mustSelects
.concat(response)
.map((option) => option.level2 || 0)
.reduce((sum, current) => sum + current, 0);
const levelMatched = overflow
? LevelSum1 >= totalLevels || LevelSum2 >= totalLevels
: LevelSum1 == totalLevels || LevelSum2 == totalLevels;
const submitable = single
? response.length == 1
: response.length >= min && response.length <= max && levelMatched;
const grouped = groupBy(selectables, (option) => option.location?.zone!);
return (
<>
<DragModal
title={`${preHintMsg} ${selectHintMsg} ${min}-${max} ${
single ? "每次选择一张" : ""
}`}
open={isOpen && isValid}
onCancel={() => {
selectCardActions.isOpen = false;
}}
closeIcon={<MinusOutlined />}
footer={
<>
<Button
disabled={!submitable}
onClick={() => {
const values = mustSelects
.concat(response)
.map((option) => option.response);
if (isChain) {
sendSelectSingleResponse(values[0]);
} else {
sendSelectMultiResponse(values);
}
clearSelectActions();
}}
onFocus={() => {}}
onBlur={() => {}}
>
{fetchStrings("!system", 1211)}
</Button>
<Button
disabled={!finishable}
onClick={() => {
sendSelectSingleResponse(FINISH_RESPONSE);
clearSelectActions();
}}
onFocus={() => {}}
onBlur={() => {}}
>
{fetchStrings("!system", 1296)}
</Button>
<Button
disabled={!cancelable}
onClick={() => {
sendSelectSingleResponse(CANCEL_RESPONSE);
clearSelectActions();
}}
onFocus={() => {}}
onBlur={() => {}}
>
{fetchStrings("!system", 1295)}
</Button>
</>
}
width={800}
>
<CheckCard.Group
multiple
bordered
size="small"
onChange={(values: any) => {
if (values.length > 0) {
const meta = values[values.length - 1].meta;
cardModal.meta = meta;
cardModal.counters = {};
cardModal.interactivies = [];
cardModal.isOpen = true;
}
setResponse(values);
}}
>
<Tabs
type="card"
items={grouped.map((group, idx) => {
return {
label: fetchStrings("!system", group[0] + 1000),
key: idx.toString(),
children: (
<Row>
{group[1].map((option, idx) => {
return (
<Col span={4} key={idx}>
<HoverCheckCard
hoverContent={option.effectDesc}
style={{
width: 120,
backgroundColor:
option.location?.controller === 0
? "white"
: "grey",
}}
cover={
<img
alt={option.meta.id.toString()}
src={
option.meta.id
? `${NeosConfig.cardImgUrl}/${option.meta.id}.jpg`
: `${NeosConfig.assetsPath}/card_back.jpg`
}
style={{ width: 100 }}
/>
}
value={option}
/>
</Col>
);
})}
</Row>
),
};
})}
/>
<p>{selecteds.length > 0 ? fetchStrings("!system", 212) : ""}</p>
<Row>
{selecteds.concat(mustSelects).map((option, idx) => {
return (
<Col span={4} key={idx}>
<Card
style={{ width: 120 }}
cover={
<img
alt={option.meta.id.toString()}
src={
option.meta.id
? `${NeosConfig.cardImgUrl}/${option.meta.id}.jpg`
: `${NeosConfig.assetsPath}/card_back.jpg`
}
/>
}
/>
</Col>
);
})}
</Row>
</CheckCard.Group>
</DragModal>
{isValid && !isOpen ? (
<div className="select-modal">
<button
className="select-modal-button"
onClick={() => {
selectCardActions.isOpen = true;
}}
>
SCROLL UP
</button>
</div>
) : (
<></>
)}
</>
);
};
const HoverCheckCard = (props: CheckCardProps & { hoverContent?: string }) => {
const [hover, setHover] = useState(false);
const onMouseEnter = () => setHover(true);
const onMouseLeave = () => setHover(false);
return (
<>
<CheckCard {...props} />
{props.hoverContent ? (
<Popover content={<p>{props.hoverContent}</p>} open={hover}>
<Button
icon={<ThunderboltOutlined />}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
></Button>
</Popover>
) : (
<></>
)}
</>
);
};
import "./index.scss";
import { INTERNAL_Snapshot as Snapshot, proxy, useSnapshot } from "valtio";
import { sendSelectMultiResponse, sendSelectSingleResponse } from "@/api";
import {
type Option,
SelectCardsModal,
type SelectCardsModalProps,
} from "../SelectCardsModal";
const CANCEL_RESPONSE = -1;
const FINISH_RESPONSE = -1;
const defaultProps: Omit<
SelectCardsModalProps,
"onSubmit" | "onCancel" | "onFinish"
> & { isChain: boolean } = {
isOpen: false,
isChain: false,
min: 0, // 最少选择多少卡
max: 0, // 最多选择多少卡
single: false, // 是否只能单选
selecteds: [] as Option[],
selectables: [] as Option[],
mustSelects: [] as Option[],
cancelable: false, // 能否取消
finishable: false, // 选择足够了之后,能否确认
totalLevels: 0, // 需要的总等级数(用于同调/仪式/...)
overflow: false, // 选择等级时候,是否可以溢出
};
const localStore = proxy(defaultProps);
export const SelectActionsModal: React.FC = () => {
const {
isOpen,
isChain,
min,
max,
single,
selecteds,
selectables,
mustSelects,
cancelable,
finishable,
totalLevels,
overflow,
} = useSnapshot(localStore);
const onSubmit = (options: Snapshot<Option[]>) => {
const values = options.map((option) => option.response!);
if (isChain) {
sendSelectSingleResponse(values[0]);
} else {
sendSelectMultiResponse(values);
}
rs();
};
const onFinish = () => {
sendSelectSingleResponse(FINISH_RESPONSE);
rs();
};
const onCancel = () => {
sendSelectSingleResponse(CANCEL_RESPONSE);
rs();
};
return (
<SelectCardsModal
{...{
isOpen,
min,
max,
single,
selecteds,
selectables,
mustSelects,
cancelable,
finishable,
totalLevels,
overflow,
onSubmit,
onFinish,
onCancel,
}}
/>
);
};
let rs: (v?: any) => void = () => {};
export const displaySelectActionsModal = async (
args: Partial<Omit<typeof defaultProps, "isOpen">>
) => {
resetSelectActionsModal(); // 先重置为初始状态
Object.entries(args).forEach(([key, value]) => {
// @ts-ignore
localStore[key] = value;
});
localStore.isOpen = true;
await new Promise<void>((resolve) => (rs = resolve)); // 等待在组件内resolve
localStore.isOpen = false;
};
const resetSelectActionsModal = () => {
Object.keys(defaultProps).forEach((key) => {
// @ts-ignore
localStore[key] = defaultProps[key];
});
};
.checkcard-container {
position: relative;
// padding-left: 10px;
// &::after {
// position: absolute;
// width: 3px;
// height: 100%;
// content: "";
// z-index: 1;
// left: 0;
// top: 0;
// background-color: rgb(0, 54, 189);
// }
.btns {
width: 100%;
top: 50%;
display: flex;
justify-content: space-between;
padding: 0 10px;
box-sizing: border-box;
height: 0;
button {
transform: translateY(-50%);
}
}
.ant-pro-checkcard {
border-radius: 4px;
overflow: hidden;
}
// 多选卡片的样式
.ant-pro-checkcard-checked {
&::before {
position: absolute;
width: 100%;
height: 100%;
content: "";
z-index: 1;
background-color: #0023bf32;
box-shadow: 0 0 0 2px #0087e6 inset;
}
&::after {
display: none;
}
}
}
import "./index.scss";
import { CheckCard } from "@ant-design/pro-components";
import { Button, Card, Segmented, Space, Tooltip } from "antd";
import { CSSProperties, useEffect, useState } from "react";
import { INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
import type { CardMeta, ygopro } from "@/api";
import { fetchStrings } from "@/api";
import { CardType, matStore } from "@/stores";
import { YgoCard } from "@/ui/Shared";
import { groupBy } from "../../utils";
import { showCardModal } from "../CardModal";
import { NeosModal } from "../NeosModal";
const YgoCardStyle = {
width: "100%",
height: "100%",
position: "absolute",
left: 0,
top: 0,
};
const CheckCardStyle = {
width: 100,
aspectRatio: 5.9 / 8.6,
marginInlineEnd: 0,
marginBlockEnd: 0,
flexShrink: 0,
};
const CheckGroupStyle = {
display: "grid",
gridTemplateColumns: "repeat(6, 1fr)",
gap: 10,
};
export interface SelectCardsModalProps {
isOpen: boolean;
min: number;
max: number;
single: boolean;
selecteds: Snapshot<Option[]>; // 已经选择了的卡
selectables: Snapshot<Option[]>; // 最多选择多少卡
mustSelects: Snapshot<Option[]>; // 单选
cancelable: boolean; // 能否取消
finishable: boolean; // 选择足够了之后,能否确认
totalLevels: number; // 需要的总等级数(用于同调/仪式/...)
overflow: boolean; // 选择等级时候,是否可以溢出
onSubmit: (options: Snapshot<Option[]>) => void;
onCancel: () => void;
onFinish: () => void;
}
export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({
isOpen,
min,
max,
single,
selecteds,
selectables,
mustSelects,
cancelable,
finishable,
totalLevels,
overflow,
onSubmit,
onCancel,
onFinish,
}) => {
// FIXME: handle `selecteds`
const [result, setResult] = useState<Option[]>([]);
const [submitable, setSubmitable] = useState(false);
const hint = useSnapshot(matStore.hint);
const preHintMsg = hint.esHint || "";
const selectHintMsg = hint.esSelectHint || "请选择卡片";
const minMaxText = min === max ? min : `${min}-${max}`;
// const isMultiple = !single && max > 1;
// FIXME: 如果想上面这样鞋会panic,还不是很清楚原因,先放着后面再优化
const isMultiple = true;
// 判断是否可以提交
useEffect(() => {
const [sumLevel1, sumLevel2] = (["level1", "level2"] as const).map((key) =>
[...mustSelects, ...result]
.map((option) => option[key] || 0)
.reduce((sum, current) => sum + current, 0)
);
const levelMatched = overflow
? sumLevel1 >= totalLevels || sumLevel2 >= totalLevels
: sumLevel1 === totalLevels || sumLevel2 === totalLevels;
setSubmitable(
single
? result.length === 1
: result.length >= min && result.length <= max && levelMatched
);
}, [result.length]);
const grouped = groupBy(selectables, (option) => option.location?.zone!);
const zoneOptions = grouped.map((x) => ({
value: x[0],
label: fetchStrings("!system", x[0] + 1000),
}));
const [selectedZone, setSelectedZone] = useState(zoneOptions[0]?.value);
useEffect(() => {
setSelectedZone(zoneOptions[0]?.value);
}, [selectables]);
const [submitText, finishText, cancelText] = [1211, 1296, 1295].map((n) =>
fetchStrings("!system", n)
);
return (
<NeosModal
title={
<>
<span>{preHintMsg}</span>
<span>{selectHintMsg}</span>
<span>(请选择 {minMaxText} 张卡)</span>
<span>{single ? "每次选择一张" : ""}</span>
</>
} // TODO: 这里可以再细化一些
width={600}
okButtonProps={{
disabled: !submitable,
}}
open={isOpen}
footer={
<>
{cancelable && <Button onClick={onCancel}>{cancelText}</Button>}
{finishable && (
<Button type="primary" onClick={onFinish}>
{finishText}
</Button>
)}
<Button
type="primary"
disabled={!submitable}
onClick={() => onSubmit([...mustSelects, ...result])}
>
{submitText}
</Button>
</>
}
>
<div className="check-container">
<Space
direction="vertical"
style={{ width: "100%", overflow: "hidden" }}
>
<Selector
zoneOptions={zoneOptions}
selectedZone={selectedZone}
onChange={setSelectedZone as any}
/>
{grouped.map(
(options, i) =>
options[0] === selectedZone && (
<div className="checkcard-container" key={i}>
<CheckCard.Group
onChange={(res) => {
setResult((isMultiple ? res : [res]) as any);
}}
// TODO 考虑如何设置默认值,比如只有一个的,就直接选中
multiple={isMultiple}
style={CheckGroupStyle}
>
{options[1].map((card, j) => (
<Tooltip
title={card.effectDesc}
placement="bottom"
key={j}
>
{/* 这儿必须有一个div,不然tooltip不生效 */}
<div>
<CheckCard
cover={
<YgoCard
code={card.meta.id}
style={YgoCardStyle as CSSProperties}
/>
}
style={CheckCardStyle}
value={card}
onClick={() => {
showCardModal(card);
}}
/>
</div>
</Tooltip>
))}
</CheckCard.Group>
</div>
)
)}
<p>
<span>
{/* TODO: 这里的字体可以调整下 */}
{selecteds.length > 0 ? fetchStrings("!system", 212) : ""}
</span>
</p>
<div style={CheckGroupStyle}>
{selecteds.map((card, i) => (
<Tooltip
title={card.effectDesc}
placement="bottom"
key={grouped.length + i}
>
<div>
<Card
cover={
<YgoCard
code={card.meta.id}
style={YgoCardStyle as CSSProperties}
/>
}
style={CheckCardStyle}
onClick={() => {
showCardModal(card);
}}
/>
</div>
</Tooltip>
))}
</div>
</Space>
</div>
</NeosModal>
);
};
/** 选择区域 */
const Selector: React.FC<{
zoneOptions: {
value: ygopro.CardZone;
label: string;
}[];
selectedZone: ygopro.CardZone;
onChange: (value: ygopro.CardZone) => void;
}> = ({ zoneOptions, selectedZone, onChange }) =>
zoneOptions.length > 1 ? (
<Segmented
block
options={zoneOptions}
style={{ margin: "10px 0" }}
value={selectedZone}
onChange={onChange as any}
/>
) : (
<></>
);
export interface Option {
// card id
meta: CardMeta;
location?: ygopro.CardLocation;
// 效果
effectDesc?: string;
// 作为素材的cost,比如同调召唤的星级
level1?: number;
level2?: number;
response?: number;
// 便于直接返回这个信息
card?: CardType;
}
// import "./index.scss";
import { INTERNAL_Snapshot as Snapshot, proxy, useSnapshot } from "valtio";
import { type Option, SelectCardsModal } from "../SelectCardsModal";
const defaultProps = {
isOpen: false,
selectables: [] as Option[],
};
const localStore = proxy(defaultProps);
export const SimpleSelectCardsModal: React.FC = () => {
const { isOpen, selectables } = useSnapshot(localStore);
return (
<SelectCardsModal
isOpen={isOpen}
min={1}
max={1}
single={false}
selecteds={[]}
mustSelects={[]}
selectables={selectables}
cancelable
finishable={false}
totalLevels={0}
overflow
onSubmit={rs}
onFinish={() => rs([])}
onCancel={() => rs([])}
/>
);
};
let rs: (options: Snapshot<Option[]>) => void = () => {};
export const displaySimpleSelectCardsModal = async (
args: Omit<typeof defaultProps, "isOpen">
) => {
localStore.selectables = args.selectables;
localStore.isOpen = true;
const res = await new Promise<Snapshot<Option[]>>(
(resolve) => (rs = resolve)
); // 等待在组件内resolve
localStore.isOpen = false;
return res;
};
// 卡牌排序弹窗
import { import {
closestCenter, closestCenter,
DndContext, DndContext,
...@@ -15,23 +16,35 @@ import { ...@@ -15,23 +16,35 @@ import {
verticalListSortingStrategy, verticalListSortingStrategy,
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import { Button, Card, Modal } from "antd"; import { Button, Card } from "antd";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { sendSortCardResponse } from "@/api"; import { sendSortCardResponse } from "@/api";
import { CardMeta } from "@/api/cards"; import { CardMeta } from "@/api/cards";
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { messageStore } from "@/stores";
import { NeosModal } from "./NeosModal";
const NeosConfig = useConfig(); const NeosConfig = useConfig();
const { sortCardModal } = messageStore; interface SortOption {
meta: CardMeta;
response: number;
}
interface SortCardModalProps {
isOpen: boolean;
options: SortOption[];
}
const defaultProps = {
isOpen: false,
options: [],
};
const localStore = proxy<SortCardModalProps>(defaultProps);
export const SortCardModal = () => { export const SortCardModal = () => {
const snapSortCardModal = useSnapshot(sortCardModal); const { isOpen, options } = useSnapshot(localStore);
const isOpen = snapSortCardModal.isOpen;
const options = snapSortCardModal.options;
const [items, setItems] = useState(options); const [items, setItems] = useState(options);
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
...@@ -42,15 +55,14 @@ export const SortCardModal = () => { ...@@ -42,15 +55,14 @@ export const SortCardModal = () => {
const onFinish = () => { const onFinish = () => {
sendSortCardResponse(items.map((item) => item.response)); sendSortCardResponse(items.map((item) => item.response));
sortCardModal.isOpen = false; rs();
sortCardModal.options = [];
}; };
const onDragEnd = (event: DragEndEvent) => { const onDragEnd = (event: DragEndEvent) => {
const { active, over } = event; const { active, over } = event;
if (active.id !== over?.id) { if (active.id !== over?.id) {
setItems((items) => { setItems((items) => {
const oldIndex = items.findIndex((item) => item.response == active.id); const oldIndex = items.findIndex((item) => item.response === active.id);
const newIndex = items.findIndex((item) => item.response === over?.id); const newIndex = items.findIndex((item) => item.response === over?.id);
// @ts-ignore // @ts-ignore
return arrayMove(items, oldIndex, newIndex); return arrayMove(items, oldIndex, newIndex);
...@@ -63,10 +75,9 @@ export const SortCardModal = () => { ...@@ -63,10 +75,9 @@ export const SortCardModal = () => {
}, [options]); }, [options]);
return ( return (
<Modal <NeosModal
title="请为下列卡牌排序" title="请为下列卡牌排序"
open={isOpen} open={isOpen}
closable={false}
footer={<Button onClick={onFinish}>finish</Button>} footer={<Button onClick={onFinish}>finish</Button>}
> >
<DndContext <DndContext
...@@ -87,7 +98,7 @@ export const SortCardModal = () => { ...@@ -87,7 +98,7 @@ export const SortCardModal = () => {
))} ))}
</SortableContext> </SortableContext>
</DndContext> </DndContext>
</Modal> </NeosModal>
); );
}; };
...@@ -114,3 +125,13 @@ const SortableItem = (props: { id: number; meta: CardMeta }) => { ...@@ -114,3 +125,13 @@ const SortableItem = (props: { id: number; meta: CardMeta }) => {
</div> </div>
); );
}; };
let rs: (arg?: any) => void = () => {};
export const displaySortCardModal = async (options: SortOption[]) => {
localStore.options = options;
localStore.isOpen = true;
await new Promise<void>((resolve) => (rs = resolve));
localStore.isOpen = false;
localStore.options = [];
};
import { Button } from "antd"; import { Button } from "antd";
import React from "react"; import React from "react";
import { useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { sendSelectEffectYnResponse } from "@/api"; import { sendSelectEffectYnResponse } from "@/api";
import { matStore, messageStore } from "@/stores"; import { matStore } from "@/stores";
import { DragModal } from "./DragModal"; import { NeosModal } from "../NeosModal";
const { yesNoModal } = messageStore; interface YesNoModalProps {
isOpen: boolean;
msg?: string;
}
const defaultProps = { isOpen: false };
export const YesNoModal = () => { const localStore = proxy<YesNoModalProps>(defaultProps);
const snapYesNoModal = useSnapshot(yesNoModal);
const isOpen = snapYesNoModal.isOpen; export const YesNoModal: React.FC = () => {
const msg = snapYesNoModal.msg; const { isOpen, msg } = useSnapshot(localStore);
const hint = useSnapshot(matStore.hint); const hint = useSnapshot(matStore.hint);
const preHintMsg = hint?.esHint || ""; const preHintMsg = hint?.esHint || "";
return ( return (
<DragModal <NeosModal
title={`${preHintMsg} ${msg}`} title={`${preHintMsg} ${msg}`}
open={isOpen} open={isOpen}
closable={false} width={400}
footer={ footer={
<> <>
<Button <Button
onClick={() => { onClick={() => {
sendSelectEffectYnResponse(true); sendSelectEffectYnResponse(false);
// dispatch(setYesNoModalIsOpen(false)); rs();
yesNoModal.isOpen = false;
}} }}
> >
Yes 取消
</Button> </Button>
<Button <Button
type="primary"
onClick={() => { onClick={() => {
sendSelectEffectYnResponse(false); sendSelectEffectYnResponse(true);
// dispatch(setYesNoModalIsOpen(false)); rs();
yesNoModal.isOpen = false;
}} }}
> >
No 确认
</Button> </Button>
</> </>
} }
/> />
); );
}; };
let rs: (arg?: any) => void = () => {};
export const displayYesNoModal = async (msg: string) => {
localStore.msg = msg;
localStore.isOpen = true;
await new Promise<void>((resolve) => (rs = resolve)); // 等待在组件内resolve
localStore.isOpen = false;
};
...@@ -3,10 +3,11 @@ export * from "./AnnounceModal"; ...@@ -3,10 +3,11 @@ export * from "./AnnounceModal";
export * from "./CardListModal"; export * from "./CardListModal";
export * from "./CardModal"; export * from "./CardModal";
export * from "./CheckCounterModal"; export * from "./CheckCounterModal";
export * from "./DragModal";
export * from "./HintNotification"; export * from "./HintNotification";
export * from "./OptionModal"; export * from "./OptionModal";
export * from "./PositionModal"; export * from "./PositionModal";
export * from "./SelectActionsModal"; export * from "./SelectActionsModal";
export { type Option } from "./SelectCardsModal";
export * from "./SimpleSelectCardsModal";
export * from "./SortCardModal"; export * from "./SortCardModal";
export * from "./YesNoModal"; export * from "./YesNoModal";
...@@ -19,7 +19,9 @@ section#mat { ...@@ -19,7 +19,9 @@ section#mat {
height: var(--block-height-m); height: var(--block-height-m);
width: var(--block-width); width: var(--block-width);
// background-color: rgba(128, 128, 128, 0.447); // background-color: rgba(128, 128, 128, 0.447);
box-shadow: 0 0 0 1px purple; background: radial-gradient(#1d1d1d, #222);
// box-shadow: 0 0 0 1px purple;
position: relative;
&.extra { &.extra {
margin-inline: calc(var(--block-width) / 2 + var(--col-gap) / 2); margin-inline: calc(var(--block-width) / 2 + var(--col-gap) / 2);
} }
...@@ -27,8 +29,46 @@ section#mat { ...@@ -27,8 +29,46 @@ section#mat {
height: var(--block-height-s); height: var(--block-height-s);
} }
&.highlight { &.highlight {
box-shadow: 0 0 0 1px #00b0ff, 0 0 13px 0px #0077ff, // box-shadow: 0 0 0 1px #00b0ff, 0 0 13px 0px #0077ff,
0 0 11px 0 skyblue inset; // 0 0 11px 0 skyblue inset;
background: #102639;
cursor: pointer;
// animation: blink 1s linear infinite alternate;
.triangle {
--color: #006eff;
transform: scale(1.5);
}
&:hover {
opacity: 0.7;
.triangle {
transform: scale(1.2);
}
}
}
.triangle {
width: 0;
height: 0;
--color: #333;
border-width: 4px;
border-style: solid;
position: absolute;
transition: 0.3s;
&:nth-of-type(1) {
border-color: var(--color) transparent transparent var(--color);
}
&:nth-of-type(2) {
border-color: var(--color) var(--color) transparent transparent;
right: 0;
}
&:nth-of-type(3) {
border-color: transparent var(--color) var(--color) transparent;
right: 0;
bottom: 0;
}
&:nth-of-type(4) {
border-color: transparent transparent var(--color) var(--color);
bottom: 0;
}
} }
} }
} }
import "./index.scss"; import "./index.scss";
import classnames from "classnames"; 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 { type INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
import { sendSelectPlaceResponse, ygopro } from "@/api"; import { sendSelectPlaceResponse, ygopro } from "@/api";
...@@ -31,7 +31,7 @@ const BgDisabledStyle = { ...@@ -31,7 +31,7 @@ const BgDisabledStyle = {
)`, )`,
}; };
const BgExtraRow: FC<{ const BgExtraRow: React.FC<{
meSnap: Snapshot<BlockState[]>; meSnap: Snapshot<BlockState[]>;
opSnap: Snapshot<BlockState[]>; opSnap: Snapshot<BlockState[]>;
}> = ({ meSnap, opSnap }) => { }> = ({ meSnap, opSnap }) => {
...@@ -52,13 +52,15 @@ const BgExtraRow: FC<{ ...@@ -52,13 +52,15 @@ const BgExtraRow: FC<{
onBlockClick(meSnap[i].interactivity); onBlockClick(meSnap[i].interactivity);
onBlockClick(opSnap[i].interactivity); onBlockClick(opSnap[i].interactivity);
}} }}
></div> >
{<DecoTriangles />}
</div>
))} ))}
</div> </div>
); );
}; };
const BgRow: FC<{ const BgRow: React.FC<{
isSzone?: boolean; isSzone?: boolean;
opponent?: boolean; opponent?: boolean;
snap: Snapshot<BlockState[]>; snap: Snapshot<BlockState[]>;
...@@ -73,12 +75,14 @@ const BgRow: FC<{ ...@@ -73,12 +75,14 @@ const BgRow: FC<{
})} })}
style={snap[i].disabled ? (BgDisabledStyle as CSSProperties) : {}} style={snap[i].disabled ? (BgDisabledStyle as CSSProperties) : {}}
onClick={() => onBlockClick(snap[i].interactivity)} onClick={() => onBlockClick(snap[i].interactivity)}
></div> >
{<DecoTriangles />}
</div>
))} ))}
</div> </div>
); );
export const Bg: FC = () => { export const Bg: React.FC = () => {
const snap = useSnapshot(placeStore.inner); const snap = useSnapshot(placeStore.inner);
return ( return (
<div className="mat-bg"> <div className="mat-bg">
...@@ -101,3 +105,11 @@ const onBlockClick = (placeInteractivity: PlaceInteractivity) => { ...@@ -101,3 +105,11 @@ const onBlockClick = (placeInteractivity: PlaceInteractivity) => {
placeStore.clearAllInteractivity(); placeStore.clearAllInteractivity();
} }
}; };
const DecoTriangles: React.FC = () => (
<>
{Array.from({ length: 4 }).map((_, i) => (
<div className="triangle" key={i} />
))}
</>
);
This diff is collapsed.
This diff is collapsed.
import { config } from "@react-spring/web";
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { type CardType, matStore } from "@/stores"; import { type CardType, matStore } from "@/stores";
...@@ -9,11 +7,11 @@ import { asyncStart } from "./utils"; ...@@ -9,11 +7,11 @@ import { asyncStart } from "./utils";
/** 发动效果的动画 */ /** 发动效果的动画 */
export const focus = async (props: { card: CardType; api: SpringApi }) => { export const focus = async (props: { card: CardType; api: SpringApi }) => {
const { card, api } = props; const { card, api } = props;
const current = api.current[0].get();
if ( if (
card.location.zone == ygopro.CardZone.HAND || card.location.zone == ygopro.CardZone.HAND ||
card.location.zone == ygopro.CardZone.DECK card.location.zone == ygopro.CardZone.DECK
) { ) {
const current = api.current[0].get();
await asyncStart(api)({ await asyncStart(api)({
y: current.y + (matStore.isMe(card.location.controller) ? -1 : 1) * 200, // TODO: 放到config之中 y: current.y + (matStore.isMe(card.location.controller) ? -1 : 1) * 200, // TODO: 放到config之中
ry: 0, ry: 0,
...@@ -21,7 +19,11 @@ export const focus = async (props: { card: CardType; api: SpringApi }) => { ...@@ -21,7 +19,11 @@ export const focus = async (props: { card: CardType; api: SpringApi }) => {
}); });
await asyncStart(api)({ y: current.y, ry: current.ry, rz: current.rz }); await asyncStart(api)({ y: current.y, ry: current.ry, rz: current.rz });
} else { } else {
await asyncStart(api)({ z: 200, config: config.gentle }); await asyncStart(api)({
await asyncStart(api)({ z: current.z, config: config.default }); focusScale: 1.5,
focusDisplay: "block",
focusOpacity: 0,
});
api.set({ focusScale: 1, focusOpacity: 1, focusDisplay: "none" });
} }
}; };
...@@ -44,7 +44,7 @@ export const moveToHand = async (props: { card: CardType; api: SpringApi }) => { ...@@ -44,7 +44,7 @@ export const moveToHand = async (props: { card: CardType; api: SpringApi }) => {
const negativeX = Math.sin(angle) * r; const negativeX = Math.sin(angle) * r;
const negativeY = Math.cos(angle) * r + HAND_CARD_HEIGHT.value / 2; const negativeY = Math.cos(angle) * r + HAND_CARD_HEIGHT.value / 2;
const x = hand_circle_center_x + negativeX * (isMe(controller) ? 1 : -1); const x = hand_circle_center_x + negativeX * (isMe(controller) ? 1 : -1);
const y = hand_circle_center_y - negativeY + 130; // 常量 是手动调的 这里肯定有问题 有空来修 const y = hand_circle_center_y - negativeY + 140; // 常量 是手动调的 这里肯定有问题 有空来修
const _rz = (angle * 180) / Math.PI; const _rz = (angle * 180) / Math.PI;
......
import { type SpringRef } from "@react-spring/web"; import { type SpringRef } from "@react-spring/web";
export type SpringApi = SpringRef<{ export interface SpringApiProps {
x: number; x: number;
y: number; y: number;
z: number; z: number;
...@@ -9,4 +9,11 @@ export type SpringApi = SpringRef<{ ...@@ -9,4 +9,11 @@ export type SpringApi = SpringRef<{
rz: number; rz: number;
zIndex: number; zIndex: number;
height: number; height: number;
}>; // >>> focus
focusScale: number;
focusDisplay: string;
focusOpacity: number;
// <<< focus
}
export type SpringApi = SpringRef<SpringApiProps>;
#life-bar-container { #life-bar-container {
position: fixed; position: fixed;
display: flex; display: flex;
gap: 20px; top: 0;
top: 20px; left: 0;
right: 20px; height: 100vh; // FIXME: 100% on safari
font-size: 1.5em; display: flex;
font-weight: 500;
font-family: inherit;
flex-direction: column; flex-direction: column;
justify-content: space-between;
padding: 20px 35px;
pointer-events: none;
z-index: 100;
} }
#life-bar { .life-bar {
padding: 0.8em 1.6em; width: 160px;
background-color: #a9a9a9; color: white;
background-color: #323232;
font-family: var(--theme-font);
border: 1px solid #222;
padding: 1rem;
padding-bottom: 0.6rem;
border-radius: 8px; border-radius: 8px;
text-align: left; text-align: left;
border: 1px solid transparent; display: flex;
color: black; flex-direction: column;
opacity: 0.4; gap: 8px;
.name {
font-size: 0.8rem;
font-weight: bold;
}
.life {
font-size: 1.8rem;
}
} }
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
export const groupBy = <K, V>( export const groupBy = <K, V extends Record<string, any>>(
array: readonly V[], array: readonly V[],
getKey: (cur: V, idx: number, src: readonly V[]) => K getKey: (cur: V, idx: number, src: readonly V[]) => K
): [K, V[]][] => ): [K, V[]][] =>
......
.ygo-card {
aspect-ratio: var(--card-ratio);
}
This diff is collapsed.
export * from "./YgoCard";
This diff is collapsed.
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