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

Merge branch 'dev/async' into 'main'

更新一波动画和样式

See merge request !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 @@
1,
6,
7,
32,
34,
54,
55,
......
......@@ -23,7 +23,6 @@
1,
6,
7,
32,
34,
54,
55,
......
......@@ -27,6 +27,7 @@
"google-protobuf": "^3.21.2",
"lodash-es": "^4.17.21",
"react": "^18.2.0",
"react-animated-numbers": "^0.16.0",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.5",
"react-router-dom": "^6.10.0",
......@@ -20627,6 +20628,19 @@
"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": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-0.2.2.tgz",
......@@ -21260,6 +21274,14 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz",
"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": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
......@@ -44269,6 +44291,15 @@
"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": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-0.2.2.tgz",
......@@ -44763,6 +44794,12 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz",
"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": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
This diff is collapsed.
......@@ -48,7 +48,7 @@ export class YgoProPacket {
);
}
} catch (e) {
console.log(e);
console.error(e);
}
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":{
"protoType":"move",
"fields":[
{
"fieldName":"code",
"fieldType":"uint32"
},
{
"fieldName":"from",
"fieldType":"CardLocation"
},
{
"fieldName":"to",
"fieldType":"CardLocation"
},
{
"fieldName":"reason",
"fieldType":"uint8"
}
]
},
"33":{
"protoType":"shuffle_hand",
"fields":[
{
"fieldName":"player",
"fieldType":"uint8"
},
{
"fieldName":"hands",
"fieldType":"repeated",
"repeatedType":"uint32"
}
]
},
"53":{
"protoType":"pos_change",
"fields":[
{
"fieldName":"card_info",
"fieldType":"CardInfo"
},
{
"fieldName":"pre_position",
"fieldType":"CardPosition"
},
{
"fieldName":"cur_position",
"fieldType":"CardPosition"
}
]
},
"13":{
"protoType":"select_yes_no",
"fields":[
{
"fieldName":"player",
"fieldType":"uint8"
},
{
"fieldName":"effect_description",
"fieldType":"uint32"
}
]
},
"54":{
"protoType":"set",
"fields":[
]
},
"55":{
"protoType":"swap",
"fields":[
]
},
"60":{
"protoType":"summoning",
"fields":[
{
"fieldName":"code",
"fieldType":"uint32"
},
{
"fieldName":"location",
"fieldType":"CardLocation"
}
]
},
"61":{
"protoType":"summoned",
"fields":[
]
},
"62":{
"protoType":"sp_summoning",
"fields":[
{
"fieldName":"code",
"fieldType":"uint32"
},
{
"fieldName":"location",
"fieldType":"CardLocation"
}
]
},
"63":{
"protoType":"sp_summoned",
"fields":[
]
},
"64":{
"protoType":"flip_summoning",
"fields":[
{
"fieldName":"code",
"fieldType":"uint32"
},
{
"fieldName":"location",
"fieldType":"CardLocation"
}
]
},
"65":{
"protoType":"flip_summoned",
"fields":[
]
},
"70":{
"protoType":"chaining",
"fields":[
{
"fieldName":"code",
"fieldType":"uint32"
},
{
"fieldName":"location",
"fieldType":"CardLocation"
}
]
},
"112":{
"protoType":"attack_disable",
"fields":[
]
},
"73":{
"protoType":"chain_solved",
"fields":[
{
"fieldName":"solved_index",
"fieldType":"uint8"
}
]
},
"74":{
"protoType":"chain_end",
"fields":[
]
},
"75":{
"protoType":"chain_solved",
"fields":[
{
"fieldName":"solved_index",
"fieldType":"uint8"
}
]
},
"76":{
"protoType":"chain_solved",
"fields":[
{
"fieldName":"solved_index",
"fieldType":"uint8"
}
]
},
"94":{
"protoType":"lp_update",
"fields":[
{
"fieldName":"player",
"fieldType":"uint8"
},
{
"fieldName":"new_lp",
"fieldType":"uint32"
}
]
},
"30":{
"protoType":"confirm_cards",
"fields":[
{
"fieldName":"player",
"fieldType":"uint8"
},
{
"fieldName":"cards",
"fieldType":"repeated",
"repeatedType":"CardInfo"
}
]
},
"31":{
"protoType":"confirm_cards",
"fields":[
{
"fieldName":"player",
"fieldType":"uint8"
},
{
"fieldName":"cards",
"fieldType":"repeated",
"repeatedType":"CardInfo"
}
]
},
"83":{
"protoType":"become_target",
"fields":[
{
"fieldName":"locations",
"fieldType":"repeated",
"repeatedType":"CardLocation"
}
]
},
"32":{
"protoType":"shuffle_deck",
"fields":[
{
"fieldName":"player",
"fieldType":"uint8"
}
]
}
"50": {
"protoType": "move",
"fields": [
{
"fieldName": "code",
"fieldType": "uint32"
},
{
"fieldName": "from",
"fieldType": "CardLocation"
},
{
"fieldName": "to",
"fieldType": "CardLocation"
},
{
"fieldName": "reason",
"fieldType": "uint8"
}
]
},
"33": {
"protoType": "shuffle_hand",
"fields": [
{
"fieldName": "player",
"fieldType": "uint8"
},
{
"fieldName": "hands",
"fieldType": "repeated",
"repeatedType": "uint32"
}
]
},
"53": {
"protoType": "pos_change",
"fields": [
{
"fieldName": "card_info",
"fieldType": "CardInfo"
},
{
"fieldName": "pre_position",
"fieldType": "CardPosition"
},
{
"fieldName": "cur_position",
"fieldType": "CardPosition"
}
]
},
"13": {
"protoType": "select_yes_no",
"fields": [
{
"fieldName": "player",
"fieldType": "uint8"
},
{
"fieldName": "effect_description",
"fieldType": "uint32"
}
]
},
"54": {
"protoType": "set",
"fields": []
},
"55": {
"protoType": "swap",
"fields": []
},
"60": {
"protoType": "summoning",
"fields": [
{
"fieldName": "code",
"fieldType": "uint32"
},
{
"fieldName": "location",
"fieldType": "CardLocation"
}
]
},
"61": {
"protoType": "summoned",
"fields": []
},
"62": {
"protoType": "sp_summoning",
"fields": [
{
"fieldName": "code",
"fieldType": "uint32"
},
{
"fieldName": "location",
"fieldType": "CardLocation"
}
]
},
"63": {
"protoType": "sp_summoned",
"fields": []
},
"64": {
"protoType": "flip_summoning",
"fields": [
{
"fieldName": "code",
"fieldType": "uint32"
},
{
"fieldName": "location",
"fieldType": "CardLocation"
}
]
},
"65": {
"protoType": "flip_summoned",
"fields": []
},
"70": {
"protoType": "chaining",
"fields": [
{
"fieldName": "code",
"fieldType": "uint32"
},
{
"fieldName": "location",
"fieldType": "CardLocation"
}
]
},
"112": {
"protoType": "attack_disable",
"fields": []
},
"73": {
"protoType": "chain_solved",
"fields": [
{
"fieldName": "solved_index",
"fieldType": "uint8"
}
]
},
"74": {
"protoType": "chain_end",
"fields": []
},
"75": {
"protoType": "chain_solved",
"fields": [
{
"fieldName": "solved_index",
"fieldType": "uint8"
}
]
},
"76": {
"protoType": "chain_solved",
"fields": [
{
"fieldName": "solved_index",
"fieldType": "uint8"
}
]
},
"94": {
"protoType": "lp_update",
"fields": [
{
"fieldName": "player",
"fieldType": "uint8"
},
{
"fieldName": "new_lp",
"fieldType": "uint32"
}
]
},
"30": {
"protoType": "confirm_cards",
"fields": [
{
"fieldName": "player",
"fieldType": "uint8"
},
{
"fieldName": "cards",
"fieldType": "repeated",
"repeatedType": "CardInfo"
}
]
},
"31": {
"protoType": "confirm_cards",
"fields": [
{
"fieldName": "player",
"fieldType": "uint8"
},
{
"fieldName": "cards",
"fieldType": "repeated",
"repeatedType": "CardInfo"
}
]
},
"83": {
"protoType": "become_target",
"fields": [
{
"fieldName": "locations",
"fieldType": "repeated",
"repeatedType": "CardLocation"
}
]
},
"32": {
"protoType": "shuffle_deck",
"fields": [
{
"fieldName": "player",
"fieldType": "uint8"
}
]
},
"132": {
"protoType": "rock_paper_scissors",
"fields": [
{
"fieldName": "player",
"fieldType": "uint8"
}
]
}
}
......@@ -36,6 +36,7 @@ const MsgConstructorMap: Map<string, Constructor> = new Map([
["confirm_cards", ygopro.StocGameMessage.MsgConfirmCards],
["become_target", ygopro.StocGameMessage.MsgBecomeTarget],
["shuffle_deck", ygopro.StocGameMessage.MsgShuffleDeck],
["rock_paper_scissors", ygopro.StocGameMessage.MsgRockPaperScissors],
]);
export interface penetrateType {
......
......@@ -47,7 +47,7 @@ export class WebSocketStream {
reader.read().then(async function process({ done, value }): Promise<void> {
if (done) {
if (ws.readyState == WebSocket.CLOSED) {
if (ws.readyState === WebSocket.CLOSED) {
// websocket connection has been closed
console.info("WebSocket closed, stream complete.");
......
......@@ -19,6 +19,7 @@
* 在进行代码开发的时候需要注意这点。
*
* */
import { ProConfigProvider } from "@ant-design/pro-provider";
import { ConfigProvider, theme } from "antd";
import zhCN from "antd/locale/zh_CN";
import React from "react";
......@@ -30,10 +31,13 @@ import Neos from "./ui/Neos";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<BrowserRouter>
<ConfigProvider theme={{ algorithm: theme.darkAlgorithm }} locale={zhCN}>
<Neos />
<ProConfigProvider dark>
<Neos />
</ProConfigProvider>
</ConfigProvider>
</BrowserRouter>
);
import { fetchCard, fetchStrings, ygopro } from "@/api";
import { messageStore } from "@/stores";
import { displayAnnounceModal } from "@/ui/Duel/Message";
import MsgAnnounce = ygopro.StocGameMessage.MsgAnnounce;
const { announceModal } = messageStore;
export default async (announce: MsgAnnounce) => {
const type_ = announce.announce_type;
let min = announce.min;
if (
type_ == MsgAnnounce.AnnounceType.Card ||
type_ == MsgAnnounce.AnnounceType.Number
type_ === MsgAnnounce.AnnounceType.Card ||
type_ === MsgAnnounce.AnnounceType.Number
) {
min = 1;
}
announceModal.min = min;
switch (type_) {
case MsgAnnounce.AnnounceType.RACE: {
announceModal.title = fetchStrings("!system", 563);
announceModal.options = announce.options.map((option) => ({
info: fetchStrings("!system", 1200 + option.code),
response: option.response,
}));
announceModal.isOpen = true;
await displayAnnounceModal({
min,
title: fetchStrings("!system", 563),
options: announce.options.map((option) => ({
info: fetchStrings("!system", 1200 + option.code),
response: option.response,
})),
});
break;
}
case MsgAnnounce.AnnounceType.Attribute: {
announceModal.title = fetchStrings("!system", 562);
announceModal.options = announce.options.map((option) => ({
info: fetchStrings("!system", 1010 + option.code),
response: option.response,
}));
announceModal.isOpen = true;
await displayAnnounceModal({
min,
title: fetchStrings("!system", 562),
options: announce.options.map((option) => ({
info: fetchStrings("!system", 1010 + option.code),
response: option.response,
})),
});
break;
}
case MsgAnnounce.AnnounceType.Card: {
announceModal.title = fetchStrings("!system", 564);
const options = [];
for (const option of announce.options) {
const meta = await fetchCard(option.code);
if (meta.text.name) {
announceModal.options.push({
options.push({
info: meta.text.name,
response: option.response,
});
}
}
announceModal.isOpen = true;
await displayAnnounceModal({
min,
title: fetchStrings("!system", 564),
options,
});
break;
}
case MsgAnnounce.AnnounceType.Number: {
announceModal.title = fetchStrings("!system", 565);
announceModal.options = announce.options.map((option) => ({
info: option.code.toString(),
response: option.response,
}));
announceModal.isOpen = true;
await displayAnnounceModal({
min,
title: fetchStrings("!system", 565),
options: announce.options.map((option) => ({
info: option.code.toString(),
response: option.response,
})),
});
break;
}
......
import { ygopro } from "@/api";
import { sleep } from "@/infra";
import { matStore } from "@/stores";
import { showWaiting } from "@/ui/Duel/Message";
import onAnnounce from "./announce";
import onMsgAttack from "./attack";
......@@ -14,6 +14,7 @@ import onMsgDraw from "./draw";
import onMsgFieldDisabled from "./fieldDisabled";
import onMsgFilpSummoned from "./flipSummoned";
import onMsgFlipSummoning from "./flipSummoning";
import onMsgHandResult from "./handResult";
import onMsgHint from "./hint";
import onLpUpdate from "./lpUpdate";
import onMsgMove from "./move";
......@@ -21,6 +22,7 @@ import onMsgNewPhase from "./newPhase";
import onMsgNewTurn from "./newTurn";
import onMsgPosChange from "./posChange";
import onMsgReloadField from "./reloadField";
import onMsgRockPaperScissors from "./rockPaperScissors";
import onMsgSelectBattleCmd from "./selectBattleCmd";
import onMsgSelectCard from "./selectCard";
import onMsgSelectChain from "./selectChain";
......@@ -77,7 +79,7 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
const msg = pb.stoc_game_msg;
if (ActiveList.includes(msg.gameMsg)) {
matStore.waiting = false;
showWaiting(false);
}
switch (msg.gameMsg) {
......@@ -102,7 +104,7 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break;
}
case "hint": {
onMsgHint(msg.hint);
await onMsgHint(msg.hint);
break;
}
......@@ -133,12 +135,12 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break;
}
case "select_effect_yn": {
onMsgSelectEffectYn(msg.select_effect_yn);
await onMsgSelectEffectYn(msg.select_effect_yn);
break;
}
case "select_position": {
onMsgSelectPosition(msg.select_position);
await onMsgSelectPosition(msg.select_position);
break;
}
......@@ -168,7 +170,7 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break;
}
case "select_yes_no": {
onMsgSelectYesNo(msg.select_yes_no);
await onMsgSelectYesNo(msg.select_yes_no);
break;
}
......@@ -213,12 +215,12 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break;
}
case "select_counter": {
onMsgSelectCounter(msg.select_counter);
await onMsgSelectCounter(msg.select_counter);
break;
}
case "sort_card": {
onMsgSortCard(msg.sort_card);
await onMsgSortCard(msg.sort_card);
break;
}
......@@ -327,6 +329,16 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break;
}
case "rock_paper_scissors": {
onMsgRockPaperScissors(msg.rock_paper_scissors);
break;
}
case "hand_res": {
onMsgHandResult(msg.hand_res);
break;
}
case "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 {
import MsgHint = ygopro.StocGameMessage.MsgHint;
export default (hint: MsgHint) => {
export default async (hint: MsgHint) => {
switch (hint.hint_type) {
case MsgHint.HintType.HINT_EVENT: {
fetchEsHintMeta({ originMsg: hint.hint_data });
await fetchEsHintMeta({ originMsg: hint.hint_data });
break;
}
case MsgHint.HintType.HINT_MESSAGE: {
......@@ -18,7 +18,7 @@ export default (hint: MsgHint) => {
break;
}
case MsgHint.HintType.HINT_SELECTMSG: {
fetchSelectHintMeta({
await fetchSelectHintMeta({
selectHintData: hint.hint_data,
esHint: "",
});
......
......@@ -37,26 +37,24 @@ export default async (move: MsgMove) => {
const meta = await fetchCard(code);
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;
}
if (to.zone == DECK) {
if (to.zone === DECK) {
// 衍生物离开场上的场合,设置`to.zone`为`TZONE`
to.zone = TZONE;
}
}
// log出来看看,后期删掉即可
await (async () => {
console.color("green")(
`${meta.text.name} ${ygopro.CardZone[from.zone]}:${from.sequence}:${
from.is_overlay ? from.overlay_sequence : ""
}${ygopro.CardZone[to.zone]}:${to.sequence}:${
to.is_overlay ? to.overlay_sequence : ""
}`
);
})();
// log出来看看
console.color("green")(
`${meta.text.name} ${ygopro.CardZone[from.zone]}:${from.sequence}${
from.is_overlay ? ":" + from.overlay_sequence : ""
}${ygopro.CardZone[to.zone]}:${to.sequence}${
to.is_overlay ? ":" + to.overlay_sequence : ""
}`
);
let target: CardType;
......@@ -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;
overlayStack.push(to);
}
......@@ -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(
from.zone,
from.controller,
......
import { ygopro } from "@/api";
export default (mora: ygopro.StocGameMessage.MsgRockPaperScissors) => {
console.log(mora);
// TODO
};
import { ygopro } from "@/api";
import MsgSelectCard = ygopro.StocGameMessage.MsgSelectCard;
import { fetchCheckCardMeta, messageStore } from "@/stores";
export default (selectCard: MsgSelectCard) => {
const cancelable = selectCard.cancelable;
const min = selectCard.min;
const max = selectCard.max;
const cards = selectCard.cards;
import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
import { fetchCheckCardMeta } from "../utils";
export default async (selectCard: MsgSelectCard) => {
const { cancelable, min, max, cards } = selectCard;
// TODO: handle release_param
messageStore.selectCardActions.min = min;
messageStore.selectCardActions.max = max;
messageStore.selectCardActions.cancelAble = cancelable;
for (const card of cards) {
fetchCheckCardMeta({
code: card.code,
location: card.location,
response: card.response,
});
}
messageStore.selectCardActions.isValid = true;
messageStore.selectCardActions.isOpen = true;
const { selecteds, mustSelects, selectables } = await fetchCheckCardMeta(
cards
);
await displaySelectActionsModal({
cancelable,
min,
max,
selecteds,
mustSelects,
selectables,
});
};
import { sendSelectSingleResponse, ygopro } from "@/api";
import { useConfig } from "@/config";
import {
fetchCheckCardMeta,
fetchSelectHintMeta,
messageStore,
} from "@/stores";
import { fetchSelectHintMeta } from "@/stores";
import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
import { fetchCheckCardMeta } from "../utils";
const NeosConfig = useConfig();
type MsgSelectChain = ygopro.StocGameMessage.MsgSelectChain;
export default (selectChain: MsgSelectChain) => {
export default async (selectChain: MsgSelectChain) => {
const spCount = selectChain.special_count;
const forced = selectChain.forced;
const _hint0 = selectChain.hint0;
......@@ -19,9 +18,9 @@ export default (selectChain: MsgSelectChain) => {
let handle_flag = 0;
if (!forced) {
// 无强制发动的卡
if (spCount == 0) {
if (spCount === 0) {
// 无关键卡
if (chains.length == 0) {
if (chains.length === 0) {
// 直接回答
handle_flag = 0;
} else {
......@@ -35,7 +34,7 @@ export default (selectChain: MsgSelectChain) => {
}
} else {
// 有关键卡
if (chains.length == 0) {
if (chains.length === 0) {
// 根本没卡,直接回答
handle_flag = 0;
} else {
......@@ -45,7 +44,7 @@ export default (selectChain: MsgSelectChain) => {
}
} else {
// 有强制发动的卡
if (chains.length == 1) {
if (chains.length === 1) {
// 只有一个强制发动的连锁项,直接回应
handle_flag = 4;
} else {
......@@ -64,26 +63,21 @@ export default (selectChain: MsgSelectChain) => {
case 2: // 处理多张
case 3: {
// 处理强制发动的卡
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({
await fetchSelectHintMeta({
selectHintData: 203,
});
messageStore.selectCardActions.isValid = true;
messageStore.selectCardActions.isOpen = true;
const { selecteds, mustSelects, selectables } = await fetchCheckCardMeta(
chains
);
await displaySelectActionsModal({
isChain: true,
cancelable: !forced,
min: 1,
max: 1,
selecteds,
mustSelects,
selectables,
});
break;
}
case 4: {
......
import { ygopro } from "@/api";
import { cardStore, messageStore } from "@/stores";
import { cardStore } from "@/stores";
import { displayCheckCounterModal } from "@/ui/Duel/Message";
type MsgSelectCounter = ygopro.StocGameMessage.MsgSelectCounter;
export default (selectCounter: MsgSelectCounter) => {
messageStore.checkCounterModal.counterType = selectCounter.counter_type;
messageStore.checkCounterModal.min = selectCounter.min;
messageStore.checkCounterModal.options = selectCounter.options!.map(
({ location, code, counter_count }) => {
export default async (selectCounter: MsgSelectCounter) => {
await displayCheckCounterModal({
counterType: selectCounter.counter_type,
min: selectCounter.min,
options: selectCounter.options!.map(({ location, code, counter_count }) => {
const id = cardStore.find(location)?.code;
const newCode = code ? code : id || 0;
......@@ -14,7 +15,6 @@ export default (selectCounter: MsgSelectCounter) => {
code: newCode,
max: counter_count!,
};
}
);
messageStore.checkCounterModal.isOpen = true;
}),
});
};
import { fetchStrings, ygopro } from "@/api";
import { CardMeta, fetchCard } from "@/api/cards";
import { messageStore } from "@/stores";
import { displayYesNoModal } from "@/ui/Duel/Message";
type MsgSelectEffectYn = ygopro.StocGameMessage.MsgSelectEffectYn;
......@@ -11,7 +11,7 @@ export default async (selectEffectYn: MsgSelectEffectYn) => {
const effect_description = selectEffectYn.effect_description;
const textGenerator =
effect_description == 0 || effect_description == 221
effect_description === 0 || effect_description === 221
? (
desc: string,
cardMeta: CardMeta,
......@@ -33,6 +33,5 @@ export default async (selectEffectYn: MsgSelectEffectYn) => {
const desc = fetchStrings("!system", effect_description);
const meta = await fetchCard(code);
messageStore.yesNoModal.msg = textGenerator(desc, meta, location);
messageStore.yesNoModal.isOpen = true;
await displayYesNoModal(textGenerator(desc, meta, location));
};
import { fetchCard, getCardStr, ygopro } from "@/api";
import MsgSelectOption = ygopro.StocGameMessage.MsgSelectOption;
import { messageStore } from "@/stores";
import { displayOptionModal } from "@/ui/Duel/Message";
export default async (selectOption: MsgSelectOption) => {
const options = selectOption.options;
await Promise.all(
options.map(async ({ code, response }) => {
const meta = await fetchCard(code >> 4);
const msg = getCardStr(meta, code & 0xf) || "[?]";
const newResponse = { msg, response };
messageStore.optionModal.options.push(newResponse);
})
await displayOptionModal(
await Promise.all(
options.map(async ({ code, response }) => {
const meta = await fetchCard(code >> 4);
const msg = getCardStr(meta, code & 0xf) || "[?]";
return { msg, response };
})
)
);
messageStore.optionModal.isOpen = true;
};
import { ygopro } from "@/api";
import { messageStore } from "@/stores";
import { displayPositionModal } from "@/ui/Duel/Message";
type MsgSelectPosition = ygopro.StocGameMessage.MsgSelectPosition;
export default (selectPosition: MsgSelectPosition) => {
export default async (selectPosition: MsgSelectPosition) => {
const _player = selectPosition.player;
const positions = selectPosition.positions;
messageStore.positionModal.positions = positions.map(
const positions = selectPosition.positions.map(
(position) => position.position
);
messageStore.positionModal.isOpen = true;
await displayPositionModal(positions);
};
import { ygopro } from "@/api";
import { fetchCheckCardMeta, messageStore } from "@/stores";
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;
import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
for (const option of selectSum.must_select_cards) {
fetchCheckCardMeta(option, false, true);
}
for (const option of selectSum.selectable_cards) {
fetchCheckCardMeta(option);
}
import { fetchCheckCardMeta } from "../utils";
type MsgSelectSum = ygopro.StocGameMessage.MsgSelectSum;
messageStore.selectCardActions.isValid = true;
messageStore.selectCardActions.isOpen = true;
export default async (selectSum: MsgSelectSum) => {
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 { fetchCheckCardMeta, messageStore } from "@/stores";
import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
import { fetchCheckCardMeta } from "../utils";
type MsgSelectTribute = ygopro.StocGameMessage.MsgSelectTribute;
export default (selectTribute: MsgSelectTribute) => {
export default async (selectTribute: MsgSelectTribute) => {
// TODO: 当玩家选择卡数大于`max`时,是否也合法?
messageStore.selectCardActions.overflow = true;
messageStore.selectCardActions.totalLevels = 0;
messageStore.selectCardActions.min = selectTribute.min;
messageStore.selectCardActions.max = selectTribute.max;
for (const option of selectTribute.selectable_cards) {
fetchCheckCardMeta(option);
}
messageStore.selectCardActions.isValid = true;
messageStore.selectCardActions.isOpen = true;
const { selecteds, mustSelects, selectables } = await fetchCheckCardMeta(
selectTribute.selectable_cards
);
await displaySelectActionsModal({
overflow: true,
totalLevels: 0,
min: selectTribute.min,
max: selectTribute.max,
selecteds,
mustSelects,
selectables,
});
};
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;
export default async ({
......@@ -11,20 +12,24 @@ export default async ({
selectable_cards: selectableCards,
selected_cards: selectedCards,
}: MsgSelectUnselectCard) => {
messageStore.selectCardActions.finishAble = finishable;
messageStore.selectCardActions.cancelAble = cancelable;
messageStore.selectCardActions.min = min;
messageStore.selectCardActions.max = max;
messageStore.selectCardActions.single = true;
for (const option of selectableCards) {
await fetchCheckCardMeta(option);
}
for (const option of selectedCards) {
await fetchCheckCardMeta(option, true);
}
messageStore.selectCardActions.isValid = true;
messageStore.selectCardActions.isOpen = true;
const {
selecteds: selecteds1,
mustSelects: mustSelect1,
selectables: selectable1,
} = await fetchCheckCardMeta(selectableCards);
const {
selecteds: selecteds2,
mustSelects: mustSelect2,
selectables: selectable2,
} = await fetchCheckCardMeta(selectedCards, true);
await displaySelectActionsModal({
finishable,
cancelable,
min: min,
max: max,
single: true,
selecteds: [...selecteds1, ...selecteds2],
mustSelects: [...mustSelect1, ...mustSelect2],
selectables: [...selectable1, ...selectable2],
});
};
import { getStrings, ygopro } from "@/api";
import { messageStore } from "@/stores";
import { displayYesNoModal } from "@/ui/Duel/Message";
type MsgSelectYesNo = ygopro.StocGameMessage.MsgSelectYesNo;
......@@ -7,6 +7,6 @@ export default async (selectYesNo: MsgSelectYesNo) => {
const _player = selectYesNo.player;
const effect_description = selectYesNo.effect_description;
messageStore.yesNoModal.msg = await getStrings(effect_description);
messageStore.yesNoModal.isOpen = true;
const msg = await getStrings(effect_description);
await displayYesNoModal(msg);
};
import { fetchCard, ygopro } from "@/api";
import { messageStore } from "@/stores";
import { displaySortCardModal } from "@/ui/Duel/Message";
type MsgSortCard = ygopro.StocGameMessage.MsgSortCard;
export default async (sortCard: MsgSortCard) => {
await Promise.all(
const options = await Promise.all(
sortCard.options.map(async ({ code, response }) => {
const meta = await fetchCard(code!);
messageStore.sortCardModal.options.push({
return {
meta,
response: response!,
});
};
})
);
messageStore.sortCardModal.isOpen = true;
await displaySortCardModal(options);
};
......@@ -6,15 +6,14 @@ import { subscribeKey } from "valtio/utils";
import { fetchCard, ygopro } from "@/api";
import { useConfig } from "@/config";
import { sleep } from "@/infra";
import { cardStore, CardType, store } from "@/stores";
const { matStore } = store;
import { cardStore, CardType, matStore } from "@/stores";
const TOKEN_SIZE = 13; // 每人场上最多就只可能有13个token
export default async (start: ygopro.StocGameMessage.MsgStart) => {
// 先初始化`matStore`
matStore.selfType = start.playerType;
const opponent =
start.playerType == ygopro.StocGameMessage.MsgStart.PlayerType.FirstStrike
start.playerType === ygopro.StocGameMessage.MsgStart.PlayerType.FirstStrike
? 1
: 0;
......
......@@ -10,9 +10,9 @@ export default async (toss: MsgToss) => {
const prefix = fetchStrings("!system", matStore.isMe(player) ? 102 : 103);
for (const x of toss.res) {
if (tossType == MsgToss.TossType.DICE) {
if (tossType === MsgToss.TossType.DICE) {
matStore.tossResult = prefix + fetchStrings("!system", 1624) + x;
} else if (tossType == MsgToss.TossType.COIN) {
} else if (tossType === MsgToss.TossType.COIN) {
matStore.tossResult =
prefix +
fetchStrings("!system", 1623) +
......
......@@ -12,7 +12,7 @@ export default async (updateData: MsgUpdateData) => {
const sequence = action.location?.sequence;
if (typeof sequence !== "undefined") {
const target = field
.filter((card) => card.location.sequence == sequence)
.filter((card) => card.location.sequence === sequence)
.at(0);
if (target) {
// 目前只更新以下字段
......
......@@ -4,10 +4,10 @@ import { fetchEsHintMeta, matStore } from "@/stores";
import MsgUpdateHp = ygopro.StocGameMessage.MsgUpdateHp;
export default (msgUpdateHp: MsgUpdateHp) => {
if (msgUpdateHp.type_ == MsgUpdateHp.ActionType.DAMAGE) {
if (msgUpdateHp.type_ === MsgUpdateHp.ActionType.DAMAGE) {
fetchEsHintMeta({ originMsg: "玩家收到伤害时" }); // TODO: i18n
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
matStore.initInfo.of(msgUpdateHp.player).life += msgUpdateHp.value;
}
......
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) => {
for (const card of cardStore.inner) {
card.idleInteractivities = [];
}
matStore.waiting = true;
showWaiting(true);
};
......@@ -19,7 +19,7 @@ export default function handleSocketOpen(
) {
console.log("WebSocket opened.");
if (ws && ws.readyState == 1) {
if (ws && ws.readyState === 1) {
ws.binaryType = "arraybuffer";
sendPlayerInfo(ws, player);
......
......@@ -29,23 +29,23 @@ export default function handleHsPlayerChange(pb: ygopro.YgoStocMsg) {
break;
}
case ygopro.StocHsPlayerChange.State.READY: {
playerStore[change.pos == 0 ? "player0" : "player1"].state =
playerStore[change.pos === 0 ? "player0" : "player1"].state =
READY_STATE;
break;
}
case ygopro.StocHsPlayerChange.State.NO_READY: {
playerStore[change.pos == 0 ? "player0" : "player1"].state =
playerStore[change.pos === 0 ? "player0" : "player1"].state =
NO_READY_STATE;
break;
}
case ygopro.StocHsPlayerChange.State.LEAVE: {
playerStore[change.pos == 0 ? "player0" : "player1"] = {};
playerStore[change.pos === 0 ? "player0" : "player1"] = {};
break;
}
case ygopro.StocHsPlayerChange.State.TO_OBSERVER: {
playerStore[change.pos == 0 ? "player0" : "player1"] = {}; // todo: 有没有必要?
playerStore[change.pos === 0 ? "player0" : "player1"] = {}; // TODO: 有没有必要?
playerStore.observerCount += 1;
break;
}
......
......@@ -8,6 +8,6 @@ export default function handleHsPlayerEnter(pb: ygopro.YgoStocMsg) {
if (pos > 1) {
console.log("Currently only supported 2v2 mode.");
} 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 { cardStore, messageStore } from "@/stores";
import { cardStore } from "@/stores";
import type { Option } from "@/ui/Duel/Message";
export const fetchCheckCardMeta = async (
const helper = async (
{
code,
location,
level1,
level2,
response,
effectDescCode,
effect_description,
}: {
code: number;
location: ygopro.CardLocation;
level1?: number;
level2?: number;
response: number;
effectDescCode?: number;
effect_description?: number;
},
selecteds: Option[],
mustSelects: Option[],
selectables: Option[],
selected?: boolean,
mustSelect?: boolean
) => {
......@@ -28,12 +32,12 @@ export const fetchCheckCardMeta = async (
: cardStore.at(location.zone, controller, location.sequence)?.code || 0;
const meta = await fetchCard(newID);
const effectDesc = effectDescCode
? getCardStr(meta, effectDescCode & 0xf)
const effectDesc = effect_description
? getCardStr(meta, effect_description & 0xf)
: undefined;
const newOption = {
const newOption: Option = {
meta,
location: location.toObject(),
location,
level1,
level2,
effectDesc,
......@@ -41,10 +45,38 @@ export const fetchCheckCardMeta = async (
};
if (selected) {
messageStore.selectCardActions.selecteds.push(newOption);
selecteds.push(newOption);
} else if (mustSelect) {
messageStore.selectCardActions.mustSelects.push(newOption);
mustSelects.push(newOption);
} 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 {
card.location.zone === zone &&
card.location.controller === controller &&
card.location.sequence === sequence &&
card.location.is_overlay == true &&
card.location.overlay_sequence == overlay_sequence
card.location.is_overlay === true &&
card.location.overlay_sequence === overlay_sequence
)
.at(0);
} else {
......@@ -60,7 +60,7 @@ class CardStore {
card.location.zone === zone &&
card.location.controller === controller &&
card.location.sequence === sequence &&
card.location.is_overlay == false
card.location.is_overlay === false
)
.at(0);
}
......@@ -69,7 +69,7 @@ class CardStore {
(card) =>
card.location.zone === zone &&
card.location.controller === controller &&
card.location.is_overlay == false
card.location.is_overlay === false
);
}
}
......@@ -84,9 +84,9 @@ class CardStore {
): CardType[] {
return this.inner.filter(
(card) =>
card.location.zone == zone &&
card.location.controller == controller &&
card.location.sequence == sequence &&
card.location.zone === zone &&
card.location.controller === controller &&
card.location.sequence === sequence &&
card.location.is_overlay
);
}
......
......@@ -2,33 +2,24 @@ export * from "./cardStore";
export * from "./chatStore";
export * from "./joinStore";
export * from "./matStore";
export * from "./messageStore";
export * from "./methods";
export * from "./moraStore";
export * from "./placeStore";
export * from "./playerStore";
import { proxy } from "valtio";
import { devtools } from "valtio/utils";
import { cardStore } from "./cardStore";
import { chatStore } from "./chatStore";
import { joinStore } from "./joinStore";
import { matStore } from "./matStore";
import { messageStore } from "./messageStore";
import { moraStore } from "./moraStore";
import { placeStore } from "./placeStore";
import { playerStore } from "./playerStore";
export const store = proxy({
playerStore,
chatStore,
joinStore,
moraStore,
matStore, // 决斗盘
messageStore, // 决斗的信息,包括模态框
cardStore,
placeStore,
});
devtools(store, { name: "valtio store", enabled: true });
devtools(playerStore, { name: "player", enabled: true });
devtools(chatStore, { name: "chat", enabled: true });
devtools(joinStore, { name: "join", enabled: true });
devtools(moraStore, { name: "mora", enabled: true });
devtools(matStore, { name: "mat", enabled: true });
devtools(cardStore, { name: "card", enabled: true });
devtools(placeStore, { name: "place", enabled: true });
......@@ -75,12 +75,11 @@ export const matStore: MatState = proxy<MatState>({
hint: { code: -1 },
currentPlayer: -1,
phase: {
currentPhase: ygopro.StocGameMessage.MsgNewPhase.PhaseType.UNKNOWN, // TODO 当前的阶段 应该改成enum
currentPhase: ygopro.StocGameMessage.MsgNewPhase.PhaseType.UNKNOWN,
enableBp: false, // 允许进入战斗阶段
enableM2: false, // 允许进入M2阶段
enableEp: false, // 允许回合结束
},
waiting: false,
unimplemented: 0,
// methods
isMe,
......
......@@ -33,8 +33,6 @@ export interface MatState {
reason: string;
};
waiting: boolean;
unimplemented: number; // 未处理的`Message`
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>({
isHost: false,
selfType: SelfType.UNKNOWN,
getMePlayer() {
if (this.selfType == SelfType.PLAYER1) return this.player0;
if (this.selfType === SelfType.PLAYER1) return this.player0;
return this.player1;
},
getOpPlayer() {
if (this.selfType == SelfType.PLAYER1) return this.player1;
if (this.selfType === SelfType.PLAYER1) return this.player1;
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
// thanks!
@charset "utf-8";
@import url("https://fonts.googleapis.com/css2?family=Electrolize&display=swap");
ol,
ul {
......@@ -33,8 +34,9 @@ table {
body {
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font: 87.5%/1.5em "Open Sans", sans-serif;
background-color: #141414;
// font: 87.5%/1.5em "Open Sans", sans-serif;
font-size: 14px;
display: flex;
margin: 0;
place-items: center;
......@@ -89,3 +91,21 @@ p {
width: 100%;
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 {
OptionModal,
PositionModal,
SelectActionsModal,
SimpleSelectCardsModal,
SortCardModal,
YesNoModal,
} from "./Message";
......@@ -18,6 +19,7 @@ import { LifeBar, Mat, Menu } from "./PlayMat";
const NeosDuel = () => {
return (
<>
<SelectActionsModal />
<Alert />
<Menu />
<LifeBar />
......@@ -25,13 +27,13 @@ const NeosDuel = () => {
<CardModal />
<CardListModal />
<HintNotification />
<SelectActionsModal />
<YesNoModal />
<PositionModal />
<OptionModal />
<CheckCounterModal />
<SortCardModal />
<AnnounceModal />
<SimpleSelectCardsModal />
</>
);
};
......
import { CheckCard } from "@ant-design/pro-components";
import { Button } from "antd";
import React, { useState } from "react";
import { useSnapshot } from "valtio";
import { proxy, useSnapshot } from "valtio";
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 = () => {
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[]>([]);
return (
<DragModal
<NeosModal
title={title}
open={isOpen}
closable={false}
footer={
<Button
disabled={selected.length != min}
onClick={() => {
let response = selected.reduce((res, current) => res | current, 0); // 多个选择求或
sendSelectOptionResponse(response);
announceModal.isOpen = false;
announceModal.title = undefined;
announceModal.options = [];
rs();
}}
>
submit
......@@ -51,6 +58,23 @@ export const AnnounceModal = () => {
<CheckCard key={idx} title={option.info} value={option.response} />
))}
</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 { useSnapshot } from "valtio";
import { proxy, useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { messageStore } from "@/stores";
import { ygopro } from "@/api";
import { cardStore, CardType } from "@/stores";
import { YgoCard } from "@/ui/Shared";
import { EffectButton } from "./EffectButton";
const NeosConfig = useConfig();
import { showCardModal } from "./CardModal";
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 = () => {
const snap = useSnapshot(cardListModal);
const isOpen = snap.isOpen;
const list = snap.list as typeof cardListModal.list;
const { zone, monster, isOpen, isZone, controller } = useSnapshot(store);
let cardList: CardType[] = [];
if (isZone) {
cardList = cardStore.at(zone, controller);
} else {
// 看超量素材
cardList = cardStore.findOverlay(
monster.location.zone,
monster.location.controller,
monster.location.sequence
);
}
const handleOkOrCancel = () => {
cardListModal.isOpen = false;
store.isOpen = false;
};
return (
<Drawer open={isOpen} onClose={handleOkOrCancel}>
<List
itemLayout="horizontal"
dataSource={list}
renderItem={(item) => (
<List.Item
actions={[
<EffectButton
effectInteractivies={item.interactivies}
meta={item.meta}
/>,
]}
extra={
<img
alt={item.meta?.text.name}
src={
item.meta?.id
? `${NeosConfig.cardImgUrl}/${item.meta.id}.jpg`
: `${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
open={isOpen}
onClose={handleOkOrCancel}
// headerStyle={{ display: "none" }}
width={CARD_WIDTH + 66}
style={{ maxHeight: "100%" }}
mask={false}
>
<Space direction="vertical">
{cardList.map((card) => (
<YgoCard
code={card.code}
key={card.uuid}
width={CARD_WIDTH}
onClick={() => showCardModal(card)}
/>
))}
</Space>
</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 { useSnapshot } from "valtio";
import { proxy, useSnapshot } from "valtio";
import { fetchStrings, sendSelectIdleCmdResponse } from "@/api";
import { useConfig } from "@/config";
import { cardStore, messageStore } from "@/stores";
import { type CardMeta, fetchStrings } from "@/api";
import { YgoCard } from "@/ui/Shared";
import {
Attribute2StringCodeMap,
extraCardTypes,
Race2StringCodeMap,
Type2StringCodeMap,
} from "../../../common";
import { EffectButton } from "./EffectButton";
} from "../../../../common";
import { Desc } from "./Desc";
const { cardModal } = messageStore;
const NeosConfig = useConfig();
const CARD_WIDTH = 200;
const CARD_WIDTH = 140;
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 = () => {
const snap = useSnapshot(cardModal);
const snap = useSnapshot(store);
const isOpen = snap.isOpen;
const meta = snap.meta;
const { isOpen, meta, counters: _counters } = snap;
const name = meta?.text.name;
const types = meta?.data.type;
......@@ -33,40 +50,46 @@ export const CardModal = () => {
const desc = meta?.text.desc;
const atk = meta?.data.atk;
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 (
<div
className={classnames("card-modal")}
style={
{
"--visibility": isOpen ? "visible" : "hidden",
"--opacity": isOpen ? 1 : 0,
} as any
}
// TODO: 宽度要好好设置 根据屏幕宽度
<Drawer
open={isOpen}
placement="left"
onClose={() => (store.isOpen = false)}
rootClassName="card-modal-root"
className="card-modal-drawer"
mask={false}
title={name}
closeIcon={<LeftOutlined />}
width={350}
>
<div className="card-modal-container">
<img src={imgUrl} width={CARD_WIDTH} />
<div className="card-modal-name">{name}</div>
<AttLine
types={extraCardTypes(types || 0)}
race={race}
attribute={attribute}
/>
<AtkLine atk={atk} def={def} />
<CounterLine counters={counters} />
<div className="card-modal-effect">{desc}</div>
{nonEffectInteractivies.map((interactive, idx) => {
<Space
align="start"
size={18}
style={{ position: "relative", display: "flex" }}
>
<YgoCard
code={meta?.id}
width={CARD_WIDTH}
style={{ borderRadius: 4 }}
/>
<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 (
<button
key={idx}
......@@ -85,9 +108,9 @@ export const CardModal = () => {
</button>
);
})}
<EffectButton meta={meta} effectInteractivies={effectInteractivies} />
<EffectButton meta={meta} effectInteractivies={effectInteractivies} /> */}
</div>
</div>
</Drawer>
);
};
......@@ -98,25 +121,37 @@ const AttLine = (props: {
}) => {
const race = props.race
? fetchStrings("!system", Race2StringCodeMap.get(props.race) || 0)
: "?";
: undefined;
const attribute = props.attribute
? fetchStrings("!system", Attribute2StringCodeMap.get(props.attribute) || 0)
: "?";
: undefined;
const types = props.types
.map((t) => fetchStrings("!system", Type2StringCodeMap.get(t) || 0))
.join("|");
.join("/");
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 }) => (
<div className="card-modal-atk">{`ATK/${
props.atk !== undefined ? props.atk : "?"
} DEF/${props.def !== undefined ? props.def : "?"}`}</div>
<Space size={10} className="atkLine" direction="vertical">
<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 = [];
for (const counterType in props.counters) {
const count = props.counters[counterType];
......@@ -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 React, { useState } from "react";
import { useSnapshot } from "valtio";
import { proxy, useSnapshot } from "valtio";
import { fetchStrings, sendSelectCounterResponse } from "@/api";
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();
export const CheckCounterModal = () => {
const snapCheckCounterModal = useSnapshot(checkCounterModal);
const snapCheckCounterModal = useSnapshot(localStore);
const isOpen = snapCheckCounterModal.isOpen;
const min = snapCheckCounterModal.min || 0;
......@@ -24,21 +39,17 @@ export const CheckCounterModal = () => {
const [selected, setSelected] = useState(new Array(options.length));
const sum = selected.reduce((sum, current) => sum + current, 0);
const finishable = sum == min;
const finishable = sum === min;
const onFinish = () => {
sendSelectCounterResponse(selected);
messageStore.checkCounterModal.isOpen = false;
messageStore.checkCounterModal.min = undefined;
messageStore.checkCounterModal.counterType = undefined;
messageStore.checkCounterModal.options = [];
rs();
};
return (
<DragModal
<NeosModal
title={`请移除${min}个${counterName}`}
open={isOpen}
closable={false}
footer={
<Button disabled={!finishable} onClick={onFinish}>
finish
......@@ -75,6 +86,23 @@ export const CheckCounterModal = () => {
);
})}
</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 { useSnapshot } from "valtio";
......@@ -14,21 +16,27 @@ const style = {
};
const NeosConfig = useConfig();
let globalMsgApi: ReturnType<typeof message.useMessage>[0] | undefined;
export const HintNotification = () => {
const snap = useSnapshot(matStore);
const hintState = snap.hint;
const toss = snap.tossResult;
const currentPhase = snap.phase.currentPhase;
const waiting = snap.waiting;
// const waiting = snap.waiting;
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,
});
globalMsgApi = msgApi;
useEffect(() => {
if (hintState && hintState.msg) {
api.open({
notify.open({
message: `${hintState.msg}`,
placement: "topLeft",
style: style,
......@@ -38,7 +46,7 @@ export const HintNotification = () => {
useEffect(() => {
if (toss) {
api.open({
notify.open({
message: `${toss}`,
placement: "topLeft",
style: style,
......@@ -52,7 +60,7 @@ export const HintNotification = () => {
"!system",
Phase2StringCodeMap.get(currentPhase) ?? 0
);
api.open({
notify.open({
message,
placement: "topRight",
style: style,
......@@ -63,21 +71,10 @@ export const HintNotification = () => {
}
}, [currentPhase]);
useEffect(() => {
if (waiting) {
api.open({
message: fetchStrings("!system", 1390),
placement: "top",
duration: NeosConfig.ui.hint.waitingDuration,
style: style,
});
}
}, [waiting]);
useEffect(() => {
if (result) {
const message = result.isWin ? "Win" : "Defeated" + " " + result.reason;
api.open({
notify.open({
message,
placement: "bottom",
style: style,
......@@ -85,5 +82,38 @@ export const HintNotification = () => {
}
}, [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 { Button } from "antd";
import React, { useState } from "react";
import { useSnapshot } from "valtio";
import { proxy, useSnapshot } from "valtio";
import { sendSelectOptionResponse } from "@/api";
import { messageStore } from "@/stores";
import {
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 = () => {
const snapOptionModal = useSnapshot(optionModal);
const snap = useSnapshot(store);
const isOpen = snapOptionModal.isOpen;
const options = snapOptionModal.options;
const { isOpen, options } = snap;
const [selected, setSelected] = useState<number | undefined>(undefined);
const onClick = () => {
if (selected !== undefined) {
sendSelectOptionResponse(selected);
rs();
}
};
return (
<DragModal
<NeosModal
title="请选择需要发动的效果"
open={isOpen}
closable={false}
footer={
<Button
disabled={selected === undefined}
onClick={() => {
if (selected !== undefined) {
sendSelectOptionResponse(selected);
optionModal.isOpen = false;
optionModal.options = [];
}
}}
>
submit
<Button disabled={selected === undefined} onClick={onClick}>
确定
</Button>
}
>
<CheckCard.Group
bordered
size="small"
onChange={(value) => {
// @ts-ignore
setSelected(value);
}}
>
<CheckCard.Group bordered size="small" onChange={setSelected as any}>
{options.map((option, idx) => (
<CheckCard key={idx} title={option.msg} value={option.response} />
))}
</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 { Button } from "antd";
import React, { useState } from "react";
import { useSnapshot } from "valtio";
import { proxy, useSnapshot } from "valtio";
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 snapPositionModal = useSnapshot(positionModal);
const isOpen = snapPositionModal.isOpen;
const positions = snapPositionModal.positions;
const localStore = proxy<PositionModalProps>(defaultProps);
export const PositionModal = () => {
const { isOpen, positions } = useSnapshot(localStore);
const [selected, setSelected] = useState<ygopro.CardPosition | undefined>(
undefined
);
return (
<DragModal
<NeosModal
title="请选择表示形式"
open={isOpen}
closable={false}
footer={
<Button
disabled={selected === undefined}
onClick={() => {
if (selected !== undefined) {
sendSelectPositionResponse(selected);
positionModal.isOpen = false;
positionModal.positions = [];
rs();
}
}}
>
......@@ -55,12 +56,13 @@ export const PositionModal = () => {
/>
))}
</CheckCard.Group>
</DragModal>
</NeosModal>
);
};
function cardPositionToChinese(position: ygopro.CardPosition): string {
switch (position) {
// TODO: i18n
case ygopro.CardPosition.FACEUP_ATTACK: {
return "正面攻击形式";
}
......@@ -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 {
closestCenter,
DndContext,
......@@ -15,23 +16,35 @@ import {
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Button, Card, Modal } from "antd";
import { Button, Card } from "antd";
import React, { useEffect, useState } from "react";
import { useSnapshot } from "valtio";
import { proxy, useSnapshot } from "valtio";
import { sendSortCardResponse } from "@/api";
import { CardMeta } from "@/api/cards";
import { useConfig } from "@/config";
import { messageStore } from "@/stores";
import { NeosModal } from "./NeosModal";
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 = () => {
const snapSortCardModal = useSnapshot(sortCardModal);
const isOpen = snapSortCardModal.isOpen;
const options = snapSortCardModal.options;
const { isOpen, options } = useSnapshot(localStore);
const [items, setItems] = useState(options);
const sensors = useSensors(
useSensor(PointerSensor),
......@@ -42,15 +55,14 @@ export const SortCardModal = () => {
const onFinish = () => {
sendSortCardResponse(items.map((item) => item.response));
sortCardModal.isOpen = false;
sortCardModal.options = [];
rs();
};
const onDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
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);
// @ts-ignore
return arrayMove(items, oldIndex, newIndex);
......@@ -63,10 +75,9 @@ export const SortCardModal = () => {
}, [options]);
return (
<Modal
<NeosModal
title="请为下列卡牌排序"
open={isOpen}
closable={false}
footer={<Button onClick={onFinish}>finish</Button>}
>
<DndContext
......@@ -87,7 +98,7 @@ export const SortCardModal = () => {
))}
</SortableContext>
</DndContext>
</Modal>
</NeosModal>
);
};
......@@ -114,3 +125,13 @@ const SortableItem = (props: { id: number; meta: CardMeta }) => {
</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 React from "react";
import { useSnapshot } from "valtio";
import { proxy, useSnapshot } from "valtio";
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 snapYesNoModal = useSnapshot(yesNoModal);
const isOpen = snapYesNoModal.isOpen;
const msg = snapYesNoModal.msg;
const localStore = proxy<YesNoModalProps>(defaultProps);
export const YesNoModal: React.FC = () => {
const { isOpen, msg } = useSnapshot(localStore);
const hint = useSnapshot(matStore.hint);
const preHintMsg = hint?.esHint || "";
return (
<DragModal
<NeosModal
title={`${preHintMsg} ${msg}`}
open={isOpen}
closable={false}
width={400}
footer={
<>
<Button
onClick={() => {
sendSelectEffectYnResponse(true);
// dispatch(setYesNoModalIsOpen(false));
yesNoModal.isOpen = false;
sendSelectEffectYnResponse(false);
rs();
}}
>
Yes
取消
</Button>
<Button
type="primary"
onClick={() => {
sendSelectEffectYnResponse(false);
// dispatch(setYesNoModalIsOpen(false));
yesNoModal.isOpen = false;
sendSelectEffectYnResponse(true);
rs();
}}
>
No
确认
</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";
export * from "./CardListModal";
export * from "./CardModal";
export * from "./CheckCounterModal";
export * from "./DragModal";
export * from "./HintNotification";
export * from "./OptionModal";
export * from "./PositionModal";
export * from "./SelectActionsModal";
export { type Option } from "./SelectCardsModal";
export * from "./SimpleSelectCardsModal";
export * from "./SortCardModal";
export * from "./YesNoModal";
......@@ -19,7 +19,9 @@ section#mat {
height: var(--block-height-m);
width: var(--block-width);
// 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 {
margin-inline: calc(var(--block-width) / 2 + var(--col-gap) / 2);
}
......@@ -27,8 +29,46 @@ section#mat {
height: var(--block-height-s);
}
&.highlight {
box-shadow: 0 0 0 1px #00b0ff, 0 0 13px 0px #0077ff,
0 0 11px 0 skyblue inset;
// box-shadow: 0 0 0 1px #00b0ff, 0 0 13px 0px #0077ff,
// 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 classnames from "classnames";
import { type CSSProperties, type FC } from "react";
import { type CSSProperties } from "react";
import { type INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
import { sendSelectPlaceResponse, ygopro } from "@/api";
......@@ -31,7 +31,7 @@ const BgDisabledStyle = {
)`,
};
const BgExtraRow: FC<{
const BgExtraRow: React.FC<{
meSnap: Snapshot<BlockState[]>;
opSnap: Snapshot<BlockState[]>;
}> = ({ meSnap, opSnap }) => {
......@@ -52,13 +52,15 @@ const BgExtraRow: FC<{
onBlockClick(meSnap[i].interactivity);
onBlockClick(opSnap[i].interactivity);
}}
></div>
>
{<DecoTriangles />}
</div>
))}
</div>
);
};
const BgRow: FC<{
const BgRow: React.FC<{
isSzone?: boolean;
opponent?: boolean;
snap: Snapshot<BlockState[]>;
......@@ -73,12 +75,14 @@ const BgRow: FC<{
})}
style={snap[i].disabled ? (BgDisabledStyle as CSSProperties) : {}}
onClick={() => onBlockClick(snap[i].interactivity)}
></div>
>
{<DecoTriangles />}
</div>
))}
</div>
);
export const Bg: FC = () => {
export const Bg: React.FC = () => {
const snap = useSnapshot(placeStore.inner);
return (
<div className="mat-bg">
......@@ -101,3 +105,11 @@ const onBlockClick = (placeInteractivity: PlaceInteractivity) => {
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 { type CardType, matStore } from "@/stores";
......@@ -9,11 +7,11 @@ import { asyncStart } from "./utils";
/** 发动效果的动画 */
export const focus = async (props: { card: CardType; api: SpringApi }) => {
const { card, api } = props;
const current = api.current[0].get();
if (
card.location.zone == ygopro.CardZone.HAND ||
card.location.zone == ygopro.CardZone.DECK
) {
const current = api.current[0].get();
await asyncStart(api)({
y: current.y + (matStore.isMe(card.location.controller) ? -1 : 1) * 200, // TODO: 放到config之中
ry: 0,
......@@ -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 });
} else {
await asyncStart(api)({ z: 200, config: config.gentle });
await asyncStart(api)({ z: current.z, config: config.default });
await asyncStart(api)({
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 }) => {
const negativeX = Math.sin(angle) * r;
const negativeY = Math.cos(angle) * r + HAND_CARD_HEIGHT.value / 2;
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;
......
import { type SpringRef } from "@react-spring/web";
export type SpringApi = SpringRef<{
export interface SpringApiProps {
x: number;
y: number;
z: number;
......@@ -9,4 +9,11 @@ export type SpringApi = SpringRef<{
rz: number;
zIndex: number;
height: number;
}>;
// >>> focus
focusScale: number;
focusDisplay: string;
focusOpacity: number;
// <<< focus
}
export type SpringApi = SpringRef<SpringApiProps>;
#life-bar-container {
position: fixed;
display: flex;
gap: 20px;
top: 20px;
right: 20px;
font-size: 1.5em;
font-weight: 500;
font-family: inherit;
top: 0;
left: 0;
height: 100vh; // FIXME: 100% on safari
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 20px 35px;
pointer-events: none;
z-index: 100;
}
#life-bar {
padding: 0.8em 1.6em;
background-color: #a9a9a9;
.life-bar {
width: 160px;
color: white;
background-color: #323232;
font-family: var(--theme-font);
border: 1px solid #222;
padding: 1rem;
padding-bottom: 0.6rem;
border-radius: 8px;
text-align: left;
border: 1px solid transparent;
color: black;
opacity: 0.4;
display: flex;
flex-direction: column;
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[],
getKey: (cur: V, idx: number, src: readonly V[]) => K
): [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