Commit 2aa3893b authored by timel's avatar timel

Merge branch 'dev/valtio' into 'main'

Valtio

See merge request mycard/Neos!169
parents 39c16d26 0d7db7ab
......@@ -14,7 +14,6 @@
"@react-spring/shared": "^9.7.2",
"@react-spring/types": "^9.7.2",
"@react-spring/web": "^9.7.2",
"@reduxjs/toolkit": "^1.9.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
......@@ -22,16 +21,15 @@
"@types/node": "^16.18.23",
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"@types/react-redux": "^7.1.25",
"@types/sql.js": "^1.4.4",
"antd": "^5.4.0",
"axios": "^0.27.2",
"google-protobuf": "^3.21.2",
"lodash-es": "^4.17.21",
"react": "^18.2.0",
"react-babylonjs": "^3.1.15",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.5",
"react-redux": "^8.0.5",
"react-router-dom": "^6.10.0",
"react-scripts": "^2.1.3",
"socket.io-client": "^4.6.1",
......@@ -45,6 +43,7 @@
"@babylonjs/core": "^5.54.0",
"@babylonjs/gui": "^5.54.0",
"@types/google-protobuf": "^3.15.6",
"@types/lodash-es": "^4.17.7",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@vitejs/plugin-react": "^3.1.0",
......@@ -3178,29 +3177,6 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@reduxjs/toolkit": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz",
"integrity": "sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==",
"dependencies": {
"immer": "^9.0.16",
"redux": "^4.2.0",
"redux-thunk": "^2.4.2",
"reselect": "^4.1.7"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18",
"react-redux": "^7.2.1 || ^8.0.2"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@remix-run/router": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.5.0.tgz",
......@@ -3626,15 +3602,6 @@
"integrity": "sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==",
"dev": true
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/jest": {
"version": "27.5.2",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz",
......@@ -3656,6 +3623,21 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.14.194",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz",
"integrity": "sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==",
"dev": true
},
"node_modules/@types/lodash-es": {
"version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz",
"integrity": "sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/node": {
"version": "16.18.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.23.tgz",
......@@ -3694,17 +3676,6 @@
"@types/react": "*"
}
},
"node_modules/@types/react-redux": {
"version": "7.1.25",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz",
"integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==",
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
......@@ -3738,11 +3709,6 @@
"@types/jest": "*"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.57.1.tgz",
......@@ -10988,14 +10954,6 @@
"node": ">=4.0.0"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/home-or-tmp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
......@@ -11551,15 +11509,6 @@
"node": ">= 4"
}
},
"node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz",
......@@ -15348,6 +15297,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash._reinterpolate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
......@@ -21390,49 +21344,6 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/react-redux": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz",
"integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==",
"dependencies": {
"@babel/runtime": "^7.12.1",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/use-sync-external-store": "^0.0.3",
"hoist-non-react-statics": "^3.3.2",
"react-is": "^18.0.0",
"use-sync-external-store": "^1.0.0"
},
"peerDependencies": {
"@types/react": "^16.8 || ^17.0 || ^18.0",
"@types/react-dom": "^16.8 || ^17.0 || ^18.0",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0",
"react-native": ">=0.59",
"redux": "^4"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-redux/node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
"node_modules/react-refresh": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
......@@ -23046,22 +22957,6 @@
"node": ">=8"
}
},
"node_modules/redux": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz",
"integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==",
"dependencies": {
"@babel/runtime": "^7.9.2"
}
},
"node_modules/redux-thunk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
"peerDependencies": {
"redux": "^4"
}
},
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
......@@ -23431,11 +23326,6 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"node_modules/reselect": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz",
"integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A=="
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
......@@ -30933,17 +30823,6 @@
"@react-spring/types": "~9.7.2"
}
},
"@reduxjs/toolkit": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.3.tgz",
"integrity": "sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg==",
"requires": {
"immer": "^9.0.16",
"redux": "^4.2.0",
"redux-thunk": "^2.4.2",
"reselect": "^4.1.7"
}
},
"@remix-run/router": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.5.0.tgz",
......@@ -31212,15 +31091,6 @@
"integrity": "sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==",
"dev": true
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"@types/jest": {
"version": "27.5.2",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz",
......@@ -31242,6 +31112,21 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"@types/lodash": {
"version": "4.14.194",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz",
"integrity": "sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==",
"dev": true
},
"@types/lodash-es": {
"version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz",
"integrity": "sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/node": {
"version": "16.18.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.23.tgz",
......@@ -31280,17 +31165,6 @@
"@types/react": "*"
}
},
"@types/react-redux": {
"version": "7.1.25",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz",
"integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==",
"requires": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
}
},
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
......@@ -31324,11 +31198,6 @@
"@types/jest": "*"
}
},
"@types/use-sync-external-store": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
},
"@typescript-eslint/eslint-plugin": {
"version": "5.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.57.1.tgz",
......@@ -37009,14 +36878,6 @@
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
"integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"requires": {
"react-is": "^16.7.0"
}
},
"home-or-tmp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
......@@ -37450,11 +37311,6 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz",
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ=="
},
"immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA=="
},
"immutable": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz",
......@@ -40451,6 +40307,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lodash._reinterpolate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
......@@ -45061,26 +44922,6 @@
}
}
},
"react-redux": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz",
"integrity": "sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==",
"requires": {
"@babel/runtime": "^7.12.1",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/use-sync-external-store": "^0.0.3",
"hoist-non-react-statics": "^3.3.2",
"react-is": "^18.0.0",
"use-sync-external-store": "^1.0.0"
},
"dependencies": {
"react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
}
}
},
"react-refresh": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
......@@ -46320,20 +46161,6 @@
"strip-indent": "^3.0.0"
}
},
"redux": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz",
"integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==",
"requires": {
"@babel/runtime": "^7.9.2"
}
},
"redux-thunk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
"requires": {}
},
"regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
......@@ -46620,11 +46447,6 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"reselect": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz",
"integrity": "sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A=="
},
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
......@@ -51,15 +51,14 @@ export interface CardText {
* */
export async function fetchCard(
id: number,
local?: boolean
local: boolean = true
): Promise<CardMeta> {
if (local) {
return await sqliteMiddleWare({
const res = await sqliteMiddleWare({
cmd: sqliteCmd.SELECT,
payload: { id },
}).then((res) =>
res.selectResult ? res.selectResult : { id, data: {}, text: {} }
);
});
return res.selectResult ? res.selectResult : { id, data: {}, text: {} };
}
const res = await axios.get<CardMeta>("http://localhost:3030/cards/" + id);
......
export * from "./cards";
export * from "./deck";
export * from "./ocgcore/idl/ocgcore";
export * from "./ocgcore/ocgHelper";
export * from "./strings";
......@@ -10,3 +10,10 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
// // 重新声明useSnapshot,暂时先这么写。原版的会把所有的改成readonly,引发一些棘手的类型报错。
// import "valtio/react";
// declare module "valtio/react" {
// export declare function useSnapshot<T extends object>(proxyObject: T): T;
// export {};
// }
export * from "./useApp";
export * from "./useEnv";
export * from "./useMeshClick";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { AppDispatch, RootState } from "@/store";
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
......@@ -23,12 +23,8 @@ import { ConfigProvider, theme } from "antd";
import zhCN from "antd/locale/zh_CN";
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import { ValtioProvider } from "@/valtioStores";
import { store } from "./store";
import Neos from "./ui/Neos";
const root = ReactDOM.createRoot(
......@@ -37,16 +33,9 @@ const root = ReactDOM.createRoot(
root.render(
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<ValtioProvider>
<ConfigProvider
theme={{ algorithm: theme.darkAlgorithm }}
locale={zhCN}
>
<Neos />
</ConfigProvider>
</ValtioProvider>
</Provider>
<ConfigProvider theme={{ algorithm: theme.darkAlgorithm }} locale={zhCN}>
<Neos />
</ConfigProvider>
</BrowserRouter>
</React.StrictMode>
);
......@@ -75,7 +75,10 @@ export default async function (action: sqliteAction): Promise<sqliteResult> {
selectResult: constructCardMeta(code, dataResult, textResult),
};
} else {
console.warn("ygo db not init or id not provied!");
if (action.payload?.id !== 0) {
// 0是无效的卡片ID,不需要报错,返回空即可
console.warn("ygo db not init or id not provied!");
}
}
return {};
......
/*
* Chat状态更新逻辑
*
* */
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "@/store";
export interface chatState {
message: string;
}
const initialState: chatState = {
message: "",
};
const chatSlice = createSlice({
name: "chat",
initialState,
reducers: {
postChat: (state, action: PayloadAction<string>) => {
state.message = action.payload;
},
},
});
export const { postChat } = chatSlice.actions;
export const selectChat = (state: RootState) => state.chat.message;
export default chatSlice.reducer;
import {
ActionReducerMapBuilder,
CaseReducer,
PayloadAction,
} from "@reduxjs/toolkit";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import {
createAsyncMetaThunk,
DuelFieldState,
DuelReducer,
extendIdleInteractivities,
extendMeta,
extendState,
Interactivity,
removeCard,
} from "./generic";
import { DuelState } from "./mod";
import { judgeSelf } from "./util";
export interface BanishedZoneState extends DuelFieldState {}
// 初始化除外区状态
export const initBanishedZoneImpl: CaseReducer<
DuelState,
PayloadAction<number>
> = (state, action) => {
const player = action.payload;
if (judgeSelf(player, state)) {
state.meBanishedZone = { inner: [] };
} else {
state.opBanishedZone = { inner: [] };
}
};
// 增加除外区
export const fetchBanishedZoneMeta = createAsyncMetaThunk(
"duel/fetchBanishedZoneMeta"
);
export const banishedZoneCase = (
builder: ActionReducerMapBuilder<DuelState>
) => {
builder.addCase(fetchBanishedZoneMeta.pending, (state, action) => {
// Meta结果没返回之前先更新`ID`
const controler = action.meta.arg.controler;
const sequence = action.meta.arg.sequence;
const code = action.meta.arg.code;
const newExclusion = {
occupant: { id: code, data: {}, text: {} },
location: {
controler,
location: ygopro.CardZone.REMOVED,
},
idleInteractivities: [],
counters: {},
};
if (judgeSelf(controler, state)) {
extendState(state.meBanishedZone, newExclusion, sequence);
} else {
extendState(state.opBanishedZone, newExclusion, sequence);
}
});
builder.addCase(fetchBanishedZoneMeta.fulfilled, (state, action) => {
const controler = action.payload.controler;
const sequence = action.payload.sequence;
const meta = action.payload.meta;
if (judgeSelf(controler, state)) {
extendMeta(state.meBanishedZone, meta, sequence);
} else {
extendMeta(state.opBanishedZone, meta, sequence);
}
});
};
// 删除除外区
export const removeBanishedZoneImpl: CaseReducer<
DuelState,
PayloadAction<{ controler: number; sequence: number }>
> = (state, action) => {
const banishedZone = judgeSelf(action.payload.controler, state)
? state.meBanishedZone
: state.opBanishedZone;
removeCard(banishedZone, action.payload.sequence);
};
export const addBanishedZoneIdleInteractivitiesImpl: DuelReducer<{
player: number;
sequence: number;
interactivity: Interactivity<number>;
}> = (state, action) => {
const banishedZone = judgeSelf(action.payload.player, state)
? state.meBanishedZone
: state.opBanishedZone;
extendIdleInteractivities(
banishedZone,
action.payload.sequence,
action.payload.interactivity
);
};
export const selectMeBanishedZone = (state: RootState) =>
state.duel.meBanishedZone || { inner: [] };
export const selectOpBanishedZone = (state: RootState) =>
state.duel.opBanishedZone || { inner: [] };
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import {
clearIdleInteractivities,
clearPlaceInteractivities,
DuelReducer,
reloadFieldMeta,
updateCardData,
} from "./generic";
import { judgeSelf } from "./util";
import MsgReloadField = ygopro.StocGameMessage.MsgReloadField;
type MsgUpdateData = ReturnType<
typeof ygopro.StocGameMessage.MsgUpdateData.prototype.toObject
>;
export const clearAllIdleInteractivitiesImpl: DuelReducer<number> = (
state,
action
) => {
const player = action.payload;
const states = judgeSelf(player, state)
? [
state.meHands,
state.meMonsters,
state.meMagics,
state.meGraveyard,
state.meBanishedZone,
state.meExtraDeck,
]
: [
state.opHands,
state.opMonsters,
state.opMagics,
state.opGraveyard,
state.opBanishedZone,
state.opExtraDeck,
];
states.forEach((item) => clearIdleInteractivities(item));
};
export const clearAllPlaceInteractivitiesImpl: DuelReducer<number> = (
state,
action
) => {
const player = action.payload;
const states = judgeSelf(player, state)
? [
state.meHands,
state.meMonsters,
state.meMagics,
state.meGraveyard,
state.meBanishedZone,
]
: [
state.opHands,
state.opMonsters,
state.opMagics,
state.opGraveyard,
state.opBanishedZone,
];
states.forEach((item) => clearPlaceInteractivities(item));
};
export const updateFieldDataImpl: DuelReducer<MsgUpdateData> = (
state,
action
) => {
const player = action.payload.player;
const zone = action.payload.zone;
const actions = action.payload.actions;
if (player !== undefined && zone !== undefined && actions !== undefined) {
switch (zone) {
case ygopro.CardZone.HAND: {
const hand = judgeSelf(player, state) ? state.meHands : state.opHands;
updateCardData(hand, actions);
break;
}
case ygopro.CardZone.EXTRA: {
const extra = judgeSelf(player, state)
? state.meExtraDeck
: state.opExtraDeck;
updateCardData(extra, actions);
break;
}
case ygopro.CardZone.MZONE: {
const monster = judgeSelf(player, state)
? state.meMonsters
: state.opMonsters;
updateCardData(monster, actions);
break;
}
case ygopro.CardZone.SZONE: {
const magics = judgeSelf(player, state)
? state.meMagics
: state.opMagics;
updateCardData(magics, actions);
break;
}
case ygopro.CardZone.GRAVE: {
const graveyard = judgeSelf(player, state)
? state.meGraveyard
: state.opGraveyard;
updateCardData(graveyard, actions);
break;
}
case ygopro.CardZone.REMOVED: {
const BanishedZone = judgeSelf(player, state)
? state.meBanishedZone
: state.opBanishedZone;
updateCardData(BanishedZone, actions);
break;
}
default: {
break;
}
}
}
};
export const reloadFieldImpl: DuelReducer<MsgReloadField> = (state, action) => {
const _duel_rule = action.payload.duel_rule;
// 初始化`DuelState`
state.meDeck = { inner: [] };
state.opDeck = { inner: [] };
state.meExtraDeck = { inner: [] };
state.opExtraDeck = { inner: [] };
state.meMonsters = { inner: [] };
state.opMonsters = { inner: [] };
state.meMagics = { inner: [] };
state.opMagics = { inner: [] };
state.meGraveyard = { inner: [] };
state.opGraveyard = { inner: [] };
state.meBanishedZone = { inner: [] };
state.opBanishedZone = { inner: [] };
state.meHands = { inner: [] };
state.opHands = { inner: [] };
for (const reload of action.payload.actions) {
const player = reload.player;
// DECK
const deck = judgeSelf(player, state) ? state.meDeck : state.opDeck;
reloadFieldMeta(
deck,
reload.zone_actions.filter((item) => item.zone == ygopro.CardZone.DECK),
player
);
// EXTRA_DECK
const extraDeck = judgeSelf(player, state)
? state.meExtraDeck
: state.opExtraDeck;
reloadFieldMeta(
extraDeck,
reload.zone_actions.filter((item) => item.zone == ygopro.CardZone.EXTRA),
player
);
// MZONE
const monster = judgeSelf(player, state)
? state.meMonsters
: state.opMonsters;
reloadFieldMeta(
monster,
reload.zone_actions.filter((item) => item.zone == ygopro.CardZone.MZONE),
player
);
// SZONE
const magics = judgeSelf(player, state) ? state.meMagics : state.opMagics;
reloadFieldMeta(
magics,
reload.zone_actions.filter((item) => item.zone == ygopro.CardZone.SZONE),
player
);
// GRAVE
const graveyard = judgeSelf(player, state)
? state.meGraveyard
: state.opGraveyard;
reloadFieldMeta(
graveyard,
reload.zone_actions.filter((item) => item.zone == ygopro.CardZone.GRAVE),
player
);
// REMOVED
const banishedZone = judgeSelf(player, state)
? state.meBanishedZone
: state.opBanishedZone;
reloadFieldMeta(
banishedZone,
reload.zone_actions.filter(
(item) => item.zone == ygopro.CardZone.REMOVED
),
player
);
// HANDS
const hands = judgeSelf(player, state) ? state.meHands : state.opHands;
reloadFieldMeta(
hands,
reload.zone_actions.filter((item) => item.zone == ygopro.CardZone.HAND),
player
);
}
};
import { CaseReducer, PayloadAction } from "@reduxjs/toolkit";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import { CardState, DuelFieldState } from "./generic";
import { DuelState } from "./mod";
import { judgeSelf } from "./util";
export interface DeckState extends DuelFieldState {}
// 初始化卡组状态
export const initDeckImpl: CaseReducer<
DuelState,
PayloadAction<{ player: number; deskSize: number }>
> = (state, action) => {
const player = action.payload.player;
const deckSize = action.payload.deskSize;
let deck: CardState[] = new Array(deckSize);
for (let i = 0; i < deckSize; i++) {
deck.push({
occupant: { id: 0, data: {}, text: {} },
location: {
controler: player,
location: ygopro.CardZone.DECK,
},
idleInteractivities: [],
counters: {},
});
}
if (judgeSelf(player, state)) {
state.meDeck = { inner: deck };
} else {
state.opDeck = { inner: deck };
}
};
export const selectMeDeck = (state: RootState) =>
state.duel.meDeck || { inner: [] };
export const selectOpDeck = (state: RootState) =>
state.duel.opDeck || { inner: [] };
import { ActionReducerMapBuilder } from "@reduxjs/toolkit";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import {
createAsyncMetaThunk,
createAsyncRepeatedMetaThunk,
DuelFieldState,
DuelReducer,
extendIdleInteractivities,
extendMeta,
extendState,
Interactivity,
removeCard,
updateCardMeta,
} from "./generic";
import { DuelState } from "./mod";
import { judgeSelf } from "./util";
export interface ExtraDeckState extends DuelFieldState {}
// 初始化额外卡组
export const initMeExtraDeckMeta = createAsyncRepeatedMetaThunk(
"duel/initExtraDeckMeta"
);
// 增加额外卡组
export const fetchExtraDeckMeta = createAsyncMetaThunk(
"duel/fetchExtraDeckMeta"
);
export const extraDeckCase = (builder: ActionReducerMapBuilder<DuelState>) => {
builder.addCase(initMeExtraDeckMeta.pending, (state, action) => {
const _ = action.meta.arg.controler;
const ids = action.meta.arg.codes;
const cards = ids.map((id) => {
return {
occupant: { id, data: {}, text: {} },
location: {
location: ygopro.CardZone.EXTRA,
},
idleInteractivities: [],
counters: {},
};
});
state.meExtraDeck = { inner: cards };
});
builder.addCase(initMeExtraDeckMeta.fulfilled, (state, action) => {
const _ = action.payload.controler;
const metas = action.payload.metas;
updateCardMeta(state.meExtraDeck, metas);
});
builder.addCase(fetchExtraDeckMeta.pending, (state, action) => {
const controler = action.meta.arg.controler;
const sequence = action.meta.arg.sequence;
const code = action.meta.arg.code;
const newExtraDeck = {
occupant: { id: code, data: {}, text: {} },
location: {
controler,
location: ygopro.CardZone.EXTRA,
},
idleInteractivities: [],
counters: {},
};
const extraDeck = judgeSelf(controler, state)
? state.meExtraDeck
: state.opExtraDeck;
extendState(extraDeck, newExtraDeck, sequence);
});
builder.addCase(fetchExtraDeckMeta.fulfilled, (state, action) => {
const controler = action.payload.controler;
const sequence = action.payload.sequence;
const meta = action.payload.meta;
const extraDeck = judgeSelf(controler, state)
? state.meExtraDeck
: state.opExtraDeck;
extendMeta(extraDeck, meta, sequence);
});
};
// 删除额外卡组
export const removeExtraDeckImpl: DuelReducer<{
controler: number;
sequence: number;
}> = (state, action) => {
const extraDeck = judgeSelf(action.payload.controler, state)
? state.meExtraDeck
: state.opExtraDeck;
removeCard(extraDeck, action.payload.sequence);
};
export const addExtraDeckIdleInteractivitiesImpl: DuelReducer<{
player: number;
sequence: number;
interactivity: Interactivity<number>;
}> = (state, action) => {
const extraDeck = judgeSelf(action.payload.player, state)
? state.meExtraDeck
: state.opExtraDeck;
extendIdleInteractivities(
extraDeck,
action.payload.sequence,
action.payload.interactivity
);
};
export const selectMeExtraDeck = (state: RootState) =>
state.duel.meExtraDeck || { inner: [] };
export const selectOpExtraDeck = (state: RootState) =>
state.duel.opExtraDeck || { inner: [] };
import {
AsyncThunk,
CaseReducer,
createAsyncThunk,
PayloadAction,
} from "@reduxjs/toolkit";
import { CardMeta, fetchCard } from "@/api/cards";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { DuelState } from "./mod";
import ReloadFieldAction = ygopro.StocGameMessage.MsgReloadField.ZoneAction;
type UpdateDataAction = ReturnType<
typeof ygopro.StocGameMessage.MsgUpdateData.Action.prototype.toObject
>;
export type DuelReducer<T> = CaseReducer<DuelState, PayloadAction<T>>;
export interface DuelFieldState {
inner: CardState[];
}
export interface CardState {
occupant?: CardMeta; // 占据此位置的卡牌元信息
location: {
controler?: number;
location?: number;
position?: ygopro.CardPosition;
overlay_sequence?: number;
}; // 位置信息
idleInteractivities: Interactivity<number>[]; // IDLE状态下的互动信息
placeInteractivities?: Interactivity<{
controler: number;
zone: ygopro.CardZone;
sequence: number;
}>; // 选择位置状态下的互动信息
overlay_materials?: CardMeta[]; // 超量素材
counters: { [type: number]: number }; // 指示器
reload?: boolean; // 这个字段会在收到MSG_RELOAD_FIELD的时候设置成true,在收到MSG_UPDATE_DATE的时候设置成false
}
export enum InteractType {
// 可普通召唤
SUMMON = 1,
// 可特殊召唤
SP_SUMMON = 2,
// 可改变表示形式
POS_CHANGE = 3,
// 可前场放置
MSET = 4,
// 可后场放置
SSET = 5,
// 可发动效果
ACTIVATE = 6,
// 可作为位置选择
PLACE_SELECTABLE = 7,
// 可攻击
ATTACK = 8,
}
export interface Interactivity<T> {
interactType: InteractType;
// 如果`interactType`是`ACTIVATE`,这个字段是对应的效果编号
activateIndex?: number;
// 如果`interactType`是`ATTACK`,这个字段表示是否可以直接攻击
directAttackAble?: boolean;
// 用户点击后,需要回传给服务端的`response`
response: T;
}
export function createAsyncMetaThunk(name: string): AsyncThunk<
{ controler: number; sequence: number; meta: CardMeta },
{
controler: number;
sequence: number;
position?: ygopro.CardPosition;
code: number;
},
{}
> {
return createAsyncThunk(
name,
async (param: {
controler: number;
sequence: number;
position?: ygopro.CardPosition;
code: number;
}) => {
const code = param.code;
const meta = await fetchCard(code, true);
const response = {
controler: param.controler,
sequence: param.sequence,
meta,
};
return response;
}
);
}
export function createAsyncRepeatedMetaThunk(
name: string
): AsyncThunk<
{ controler: number; metas: CardMeta[] },
{ controler: number; codes: number[] },
{}
> {
return createAsyncThunk(
name,
async (param: { controler: number; codes: number[] }) => {
const controler = param.controler;
const Ids = param.codes;
const metas = await Promise.all(
Ids.map(async (id) => {
if (id == 0) {
return { id, data: {}, text: {} };
} else {
return await fetchCard(id, true);
}
})
);
const response = { controler, metas };
return response;
}
);
}
/*
* 扩充决斗区域卡片内容
*
* @param state - 需要扩充的区域,比如`MonsterState`
* @param newState - 新增加的`CardState`
* @sequence - 新增加的卡片的序列号,可选,如果为空则补充到列表末尾
*
* */
export function extendState<T extends DuelFieldState>(
state: T | undefined,
newState: CardState,
sequence?: number
) {
if (state) {
let index = sequence !== undefined ? sequence : state.inner.length;
state.inner.splice(index, 0, newState);
}
}
export function extendOccupant<T extends DuelFieldState>(
state: T | undefined,
newMeta: CardMeta,
sequence: number,
position?: ygopro.CardPosition
) {
if (state) {
const target = state.inner.find((_, idx) => idx == sequence);
if (target) {
target.occupant = newMeta;
if (typeof position !== "undefined") {
target.location.position = position;
}
}
}
}
export function extendMeta<T extends DuelFieldState>(
state: T | undefined,
newMeta: CardMeta,
sequence: number
) {
if (state) {
const target = state.inner.find((_, idx) => idx == sequence);
if (target) {
target.occupant = newMeta;
}
}
}
export function extendPlaceInteractivity<T extends DuelFieldState>(
state: T | undefined,
controler: number,
sequence: number,
zone: ygopro.CardZone
) {
if (state) {
const target = state.inner.find((_, idx) => idx == sequence);
if (target) {
target.placeInteractivities = {
interactType: InteractType.PLACE_SELECTABLE,
response: {
controler,
zone,
sequence,
},
};
}
}
}
export function clearPlaceInteractivities<T extends DuelFieldState>(
state: T | undefined
) {
if (state) {
for (let item of state.inner) {
item.placeInteractivities = undefined;
}
}
}
export function removeCard<T extends DuelFieldState>(
state: T | undefined,
sequence: number
) {
if (state) {
state.inner = state.inner.filter((_, idx) => idx != sequence);
}
}
export function removeOccupant<T extends DuelFieldState>(
state: T | undefined,
sequence: number
) {
if (state) {
const target = state.inner.find((_, idx) => idx == sequence);
if (target) {
target.occupant = undefined;
}
}
}
export function removeOverlay<T extends DuelFieldState>(
state: T | undefined,
sequence: number
) {
if (state) {
const target = state.inner.find((_, idx) => idx == sequence);
if (target) {
target.overlay_materials = [];
}
}
}
export function insertCard<T extends DuelFieldState>(
state: T | undefined,
sequence: number,
card: CardState
) {
if (state) {
state.inner.splice(sequence, 0, card);
}
}
export function updateCardMeta<T extends DuelFieldState>(
state: T | undefined,
metas: CardMeta[]
) {
if (state) {
state.inner.forEach((item) => {
metas.forEach((meta) => {
if (item.occupant?.id === meta.id) {
item.occupant = meta;
}
});
});
}
}
export function extendIdleInteractivities<T extends DuelFieldState>(
state: T | undefined,
sequence: number,
interactivity: Interactivity<number>
) {
if (state) {
const target = state.inner.find((_, idx) => idx == sequence);
if (target) {
target.idleInteractivities.push(interactivity);
}
}
}
export function clearIdleInteractivities<T extends DuelFieldState>(
state: T | undefined
) {
if (state) {
state.inner.forEach((item) => {
item.idleInteractivities = [];
});
}
}
export function setPosition<T extends DuelFieldState>(
state: T | undefined,
sequence: number,
position: ygopro.CardPosition
) {
const target = state?.inner.find((_, idx) => idx == sequence);
if (target && target.occupant) {
target.location.position = position;
}
}
export function updateCardData<T extends DuelFieldState>(
state: T | undefined,
actions: UpdateDataAction[]
) {
for (const payload of actions) {
const sequence = payload.location?.sequence;
if (typeof sequence !== "undefined") {
const target = state?.inner.find((_, idx) => idx == sequence);
if (target && (target.occupant || target.reload)) {
if (target.occupant === undefined) {
target.occupant = { id: payload.code!, data: {}, text: {} };
}
const occupant = target.occupant;
// 目前只更新以下字段
if (payload.code !== undefined && payload.code >= 0) {
occupant.id = payload.code;
occupant.text.id = payload.code;
}
if (payload.location !== undefined) {
target.location.position = payload.location.position;
}
if (payload.type_ !== undefined && payload.type_ >= 0) {
occupant.data.type = payload.type_;
}
if (payload.level !== undefined && payload.level >= 0) {
occupant.data.level = payload.level;
}
if (payload.attribute !== undefined && payload.attribute >= 0) {
occupant.data.attribute = payload.attribute;
}
if (payload.race !== undefined && payload.race >= 0) {
occupant.data.race = payload.race;
}
if (payload.attack !== undefined && payload.attack >= 0) {
occupant.data.atk = payload.attack;
}
if (payload.defense !== undefined && payload.defense >= 0) {
occupant.data.def = payload.defense;
}
// TODO: counters
}
if (target?.reload) {
target.reload = false;
}
}
}
}
export function reloadFieldMeta<T extends DuelFieldState>(
state: T,
actions: ReloadFieldAction[],
controler: number
) {
actions.sort((a, b) => a.sequence - b.sequence);
const cards = actions.map((action) => {
// FIXME: OVERLAY
return {
location: {
controler,
location: action.zone,
position: action.position,
},
idleInteractivities: [],
counters: {},
reload: true,
};
});
state.inner = cards;
}
import {
ActionReducerMapBuilder,
CaseReducer,
PayloadAction,
} from "@reduxjs/toolkit";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import {
createAsyncMetaThunk,
DuelFieldState,
DuelReducer,
extendIdleInteractivities,
extendMeta,
extendState,
Interactivity,
removeCard,
} from "./generic";
import { DuelState } from "./mod";
import { judgeSelf } from "./util";
export interface GraveyardState extends DuelFieldState {}
// 初始化墓地状态
export const initGraveyardImpl: CaseReducer<
DuelState,
PayloadAction<number>
> = (state, action) => {
const player = action.payload;
if (judgeSelf(player, state)) {
state.meGraveyard = { inner: [] };
} else {
state.opGraveyard = { inner: [] };
}
};
// 增加墓地
export const fetchGraveyardMeta = createAsyncMetaThunk(
"duel/fetchGraveyardMeta"
);
export const graveyardCase = (builder: ActionReducerMapBuilder<DuelState>) => {
builder.addCase(fetchGraveyardMeta.pending, (state, action) => {
// Meta结果没返回之前先更新`ID`
const controler = action.meta.arg.controler;
const sequence = action.meta.arg.sequence;
const code = action.meta.arg.code;
const newGraveyard = {
occupant: { id: code, data: {}, text: {} },
location: {
controler,
location: ygopro.CardZone.GRAVE,
},
idleInteractivities: [],
counters: {},
};
if (judgeSelf(controler, state)) {
extendState(state.meGraveyard, newGraveyard, sequence);
} else {
extendState(state.opGraveyard, newGraveyard, sequence);
}
});
builder.addCase(fetchGraveyardMeta.fulfilled, (state, action) => {
const controler = action.payload.controler;
const sequence = action.payload.sequence;
const meta = action.payload.meta;
if (judgeSelf(controler, state)) {
extendMeta(state.meGraveyard, meta, sequence);
} else {
extendMeta(state.opGraveyard, meta, sequence);
}
});
};
// 删除墓地
export const removeGraveyardImpl: CaseReducer<
DuelState,
PayloadAction<{ controler: number; sequence: number }>
> = (state, action) => {
const graveyard = judgeSelf(action.payload.controler, state)
? state.meGraveyard
: state.opGraveyard;
removeCard(graveyard, action.payload.sequence);
};
export const addGraveyardIdleInteractivitiesImpl: DuelReducer<{
player: number;
sequence: number;
interactivity: Interactivity<number>;
}> = (state, action) => {
const graveyard = judgeSelf(action.payload.player, state)
? state.meGraveyard
: state.opGraveyard;
extendIdleInteractivities(
graveyard,
action.payload.sequence,
action.payload.interactivity
);
};
export const selectMeGraveyard = (state: RootState) =>
state.duel.meGraveyard || { inner: [] };
export const selectOpGraveyard = (state: RootState) =>
state.duel.opGraveyard || { inner: [] };
import {
ActionReducerMapBuilder,
CaseReducer,
PayloadAction,
} from "@reduxjs/toolkit";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import {
createAsyncMetaThunk,
createAsyncRepeatedMetaThunk,
DuelFieldState,
extendMeta,
insertCard,
Interactivity,
removeCard,
updateCardMeta,
} from "./generic";
import { DuelState } from "./mod";
import { judgeSelf } from "./util";
export interface HandState extends DuelFieldState {}
// 增加手牌
export const fetchHandsMeta = createAsyncRepeatedMetaThunk(
"duel/fetchHandsMeta"
);
// 清空手牌互动性
export const clearHandsIdleInteractivityImpl: CaseReducer<
DuelState,
PayloadAction<number>
> = (state, action) => {
const player = action.payload;
const hands = judgeSelf(player, state) ? state.meHands : state.opHands;
if (hands) {
for (let hand of hands.inner) {
hand.idleInteractivities = [];
}
}
};
// 添加手牌互动性
export const addHandsIdleInteractivityImpl: CaseReducer<
DuelState,
PayloadAction<{
player: number;
sequence: number;
interactivity: Interactivity<number>;
}>
> = (state, action) => {
const player = action.payload.player;
const hands = judgeSelf(player, state) ? state.meHands : state.opHands;
if (hands) {
const sequence = action.payload.sequence;
const interactivity = action.payload.interactivity;
hands.inner[sequence].idleInteractivities.push(interactivity);
}
};
// 删除手牌
export const removeHandImpl: CaseReducer<
DuelState,
PayloadAction<[number, number]>
> = (state, action) => {
const controler = action.payload[0];
const sequence = action.payload[1];
const hands = judgeSelf(controler, state) ? state.meHands : state.opHands;
removeCard(hands, sequence);
};
// 在特定位置增加手牌
export const insertHandMeta = createAsyncMetaThunk("duel/insertHandMeta");
export const updateHandsMeta = createAsyncRepeatedMetaThunk(
"duel/updateHandsMeta"
);
export const handsCase = (builder: ActionReducerMapBuilder<DuelState>) => {
builder.addCase(fetchHandsMeta.pending, (state, action) => {
// Meta结果没返回之前先更新手牌`ID`
const player = action.meta.arg.controler;
const ids = action.meta.arg.codes;
const cards = ids.map((id) => {
return {
occupant: { id, data: {}, text: {} },
location: {
controler: player,
location: ygopro.CardZone.HAND,
},
counters: {},
idleInteractivities: [],
};
});
if (judgeSelf(player, state)) {
if (state.meHands) {
state.meHands.inner = state.meHands.inner.concat(cards);
} else {
state.meHands = { inner: cards };
}
} else {
if (state.opHands) {
state.opHands.inner = state.opHands.inner.concat(cards);
} else {
state.opHands = { inner: cards };
}
}
});
builder.addCase(fetchHandsMeta.fulfilled, (state, action) => {
// `Meta`结果回来后更新手牌的`Meta`结果
const player = action.payload.controler;
const metas = action.payload.metas;
const hands = judgeSelf(player, state) ? state.meHands : state.opHands;
updateCardMeta(hands, metas);
});
builder.addCase(insertHandMeta.pending, (state, action) => {
const controler = action.meta.arg.controler;
const sequence = action.meta.arg.sequence;
const code = action.meta.arg.code;
const hands = judgeSelf(controler, state) ? state.meHands : state.opHands;
insertCard(hands, sequence, {
occupant: { id: code, data: {}, text: {} },
location: { controler },
idleInteractivities: [],
counters: {},
});
});
builder.addCase(insertHandMeta.fulfilled, (state, action) => {
const controler = action.payload.controler;
const sequence = action.payload.sequence;
const meta = action.payload.meta;
const hands = judgeSelf(controler, state) ? state.meHands : state.opHands;
extendMeta(hands, meta, sequence);
});
builder.addCase(updateHandsMeta.pending, (state, action) => {
const controler = action.meta.arg.controler;
const codes = action.meta.arg.codes;
const metas = codes.map((code) => {
return {
occupant: { id: code, data: {}, text: {} },
location: {
controler,
location: ygopro.CardZone.HAND,
},
idleInteractivities: [],
counters: {},
};
});
const hands = judgeSelf(controler, state) ? state.meHands : state.opHands;
if (hands) {
hands.inner = metas;
}
});
builder.addCase(updateHandsMeta.fulfilled, (state, action) => {
const controler = action.payload.controler;
const metas = action.payload.metas;
const hands = judgeSelf(controler, state) ? state.meHands : state.opHands;
updateCardMeta(hands, metas);
});
};
export const selectMeHands = (state: RootState) =>
state.duel.meHands || { inner: [] };
export const selectOpHands = (state: RootState) =>
state.duel.opHands || { inner: [] };
import { ActionReducerMapBuilder, createAsyncThunk } from "@reduxjs/toolkit";
import { fetchCard } from "@/api/cards";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { DESCRIPTION_LIMIT, fetchStrings, getStrings } from "@/api/strings";
import { RootState } from "@/store";
import { DuelReducer } from "./generic";
import { DuelState } from "./mod";
import { findCardByLocation } from "./util";
export interface HintState {
code: number;
msg?: string;
esHint?: string;
esSelectHint?: string;
}
export const initHintImpl: DuelReducer<void> = (state) => {
state.hint = { code: 0 };
};
export const fetchCommonHintMeta = createAsyncThunk(
"duel/fetchCommonHintMeta",
async (hintData: number) => {
return fetchStrings("!system", hintData);
}
);
export const fetchSelectHintMeta = createAsyncThunk(
"duel/fetchSelectHintMeta",
async (param: { selectHintData: number; esHint?: string }) => {
const selectHintData = param.selectHintData;
let selectHintMeta = "";
if (selectHintData > DESCRIPTION_LIMIT) {
// 针对`MSG_SELECT_PLACE`的特化逻辑
const cardMeta = await fetchCard(selectHintData, true);
selectHintMeta = fetchStrings("!system", 569).replace(
"[%ls]",
cardMeta.text.name || "[?]"
);
} else {
selectHintMeta = await getStrings(selectHintData);
}
return {
selectHintMeta,
esHint: param.esHint,
};
}
);
export const fetchEsHintMeta = createAsyncThunk(
"duel/fetchEsHintMeta",
async (param: {
originMsg: string | number;
location?: ygopro.CardLocation;
cardID?: number;
}) => {
const originMsg =
typeof param.originMsg === "string"
? param.originMsg
: fetchStrings("!system", param.originMsg);
const location = param.location;
if (param.cardID) {
const cardMeta = await fetchCard(param.cardID, true);
return { originMsg, cardMeta, location };
} else {
return { originMsg, location };
}
}
);
export const hintCase = (builder: ActionReducerMapBuilder<DuelState>) => {
builder.addCase(fetchCommonHintMeta.pending, (state, action) => {
const code = action.meta.arg;
if (state.hint) {
state.hint.code = code;
}
});
builder.addCase(fetchCommonHintMeta.fulfilled, (state, action) => {
const hintMeta = action.payload;
if (state.hint) {
state.hint.msg = hintMeta;
}
});
builder.addCase(fetchSelectHintMeta.pending, (state, action) => {
const code = action.meta.arg.selectHintData;
if (state.hint) {
state.hint.code = code;
}
});
builder.addCase(fetchSelectHintMeta.fulfilled, (state, action) => {
const selectHintMsg = action.payload.selectHintMeta;
const esHint = action.payload.esHint;
const hint = state.hint;
if (hint) {
if (hint.code > DESCRIPTION_LIMIT) {
// 针对`MSG_SELECT_PLACE`的特化逻辑
hint.msg = selectHintMsg;
} else {
hint.esSelectHint = selectHintMsg;
if (esHint) hint.esHint = esHint;
}
}
});
builder.addCase(fetchEsHintMeta.fulfilled, (state, action) => {
const originMsg = action.payload.originMsg;
const cardMeta = action.payload.cardMeta;
const location = action.payload.location;
const hint = state.hint;
if (hint) {
let esHint = originMsg;
if (cardMeta?.text.name) {
esHint = originMsg.replace("[?]", cardMeta.text.name);
}
if (location) {
const fieldMeta = findCardByLocation(state, location);
if (fieldMeta?.occupant?.text.name) {
esHint = originMsg.replace("[?]", fieldMeta.occupant.text.name);
}
}
hint.esHint = esHint;
}
});
};
export const selectHint = (state: RootState) => state.duel.hint;
import { CaseReducer, PayloadAction } from "@reduxjs/toolkit";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import { DuelState } from "./mod";
import { judgeSelf } from "./util";
import MsgUpdateHp = ygopro.StocGameMessage.MsgUpdateHp;
export interface InitInfo {
masterRule?: string;
life: number;
deckSize: number;
extraSize: number;
}
// 更新自己的初始生命值,卡组信息
export const infoInitImpl: CaseReducer<
DuelState,
PayloadAction<[number, InitInfo]>
> = (state, action) => {
const player = action.payload[0];
const initInfo = action.payload[1];
if (judgeSelf(player, state)) {
state.meInitInfo = initInfo;
} else {
state.opInitInfo = initInfo;
}
};
export const updateHpImpl: CaseReducer<
DuelState,
PayloadAction<ygopro.StocGameMessage.MsgUpdateHp>
> = (state, action) => {
const player = action.payload.player;
const actionType = action.payload.type_;
const value = action.payload.value;
const info = judgeSelf(player, state) ? state.meInitInfo : state.opInitInfo;
if (info) {
switch (actionType) {
case MsgUpdateHp.ActionType.DAMAGE: {
info.life = info.life - value;
break;
}
case MsgUpdateHp.ActionType.RECOVER: {
info.life = info.life + value;
break;
}
default: {
break;
}
}
}
};
export const selectMeInitInfo = (state: RootState) => state.duel.meInitInfo;
export const selectOpInitInfo = (state: RootState) => state.duel.opInitInfo;
import {
ActionReducerMapBuilder,
CaseReducer,
PayloadAction,
} from "@reduxjs/toolkit";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import {
clearIdleInteractivities,
clearPlaceInteractivities,
createAsyncMetaThunk,
DuelFieldState,
extendIdleInteractivities,
extendOccupant,
extendPlaceInteractivity,
Interactivity,
removeOccupant,
setPosition,
} from "./generic";
import { DuelState } from "./mod";
import { judgeSelf } from "./util";
export interface MagicState extends DuelFieldState {}
// 初始化自己的魔法陷阱区状态
export const initMagicsImpl: CaseReducer<DuelState, PayloadAction<number>> = (
state,
action
) => {
const player = action.payload;
const magics = {
inner: [
{
location: {
controler: player,
location: ygopro.CardZone.SZONE,
},
idleInteractivities: [],
counters: {},
},
{
location: {
controler: player,
location: ygopro.CardZone.SZONE,
},
idleInteractivities: [],
counters: {},
},
{
location: {
controler: player,
location: ygopro.CardZone.SZONE,
},
idleInteractivities: [],
counters: {},
},
{
location: {
controler: player,
location: ygopro.CardZone.SZONE,
},
idleInteractivities: [],
counters: {},
},
{
location: {
controler: player,
location: ygopro.CardZone.SZONE,
},
idleInteractivities: [],
counters: {},
},
{
// 场地区
location: {
controler: player,
location: ygopro.CardZone.SZONE,
},
idleInteractivities: [],
counters: {},
},
],
};
if (judgeSelf(player, state)) {
state.meMagics = magics;
} else {
state.opMagics = magics;
}
};
export const addMagicPlaceInteractivitiesImpl: CaseReducer<
DuelState,
PayloadAction<[number, number]>
> = (state, action) => {
const controler = action.payload[0];
const sequence = action.payload[1];
const magics = judgeSelf(controler, state) ? state.meMagics : state.opMagics;
extendPlaceInteractivity(magics, controler, sequence, ygopro.CardZone.SZONE);
};
export const clearMagicPlaceInteractivitiesImpl: CaseReducer<
DuelState,
PayloadAction<number>
> = (state, action) => {
const player = action.payload;
const magics = judgeSelf(player, state) ? state.meMagics : state.opMagics;
clearPlaceInteractivities(magics);
};
export const addMagicIdleInteractivitiesImpl: CaseReducer<
DuelState,
PayloadAction<{
player: number;
sequence: number;
interactivity: Interactivity<number>;
}>
> = (state, action) => {
const magics = judgeSelf(action.payload.player, state)
? state.meMagics
: state.opMagics;
extendIdleInteractivities(
magics,
action.payload.sequence,
action.payload.interactivity
);
};
export const clearMagicIdleInteractivitiesImpl: CaseReducer<
DuelState,
PayloadAction<number>
> = (state, action) => {
const magics = judgeSelf(action.payload, state)
? state.meMagics
: state.opMagics;
clearIdleInteractivities(magics);
};
// 增加魔法陷阱
export const fetchMagicMeta = createAsyncMetaThunk("duel/fetchMagicMeta");
export const magicCase = (builder: ActionReducerMapBuilder<DuelState>) => {
builder.addCase(fetchMagicMeta.pending, (state, action) => {
// Meta结果没返回之前先更新`ID`
const controler = action.meta.arg.controler;
const sequence = action.meta.arg.sequence;
const position = action.meta.arg.position;
const code = action.meta.arg.code;
const meta = { id: code, data: {}, text: {} };
if (judgeSelf(controler, state)) {
extendOccupant(state.meMagics, meta, sequence, position);
} else {
extendOccupant(state.opMagics, meta, sequence, position);
}
});
builder.addCase(fetchMagicMeta.fulfilled, (state, action) => {
const controler = action.payload.controler;
const sequence = action.payload.sequence;
const meta = action.payload.meta;
if (judgeSelf(controler, state)) {
extendOccupant(state.meMagics, meta, sequence);
} else {
extendOccupant(state.opMagics, meta, sequence);
}
});
};
// 删除魔法陷阱
export const removeMagicImpl: CaseReducer<
DuelState,
PayloadAction<{ controler: number; sequence: number }>
> = (state, action) => {
const controler = action.payload.controler;
const magics = judgeSelf(controler, state) ? state.meMagics : state.opMagics;
removeOccupant(magics, action.payload.sequence);
};
// 改变魔法表示形式
export const setMagicPositionImpl: CaseReducer<
DuelState,
PayloadAction<{
controler: number;
sequence: number;
position: ygopro.CardPosition;
}>
> = (state, action) => {
const controler = action.payload.controler;
const sequence = action.payload.sequence;
const position = action.payload.position;
const magics = judgeSelf(controler, state) ? state.meMagics : state.opMagics;
setPosition(magics, sequence, position);
};
export const selectMeMagics = (state: RootState) =>
state.duel.meMagics || { inner: [] };
export const selectOpMagics = (state: RootState) =>
state.duel.opMagics || { inner: [] };
/*
* 对局内的状态更新逻辑
*
* */
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import {
addBanishedZoneIdleInteractivitiesImpl,
banishedZoneCase,
BanishedZoneState,
initBanishedZoneImpl,
removeBanishedZoneImpl,
} from "./banishedZoneSlice";
import {
clearAllIdleInteractivitiesImpl,
clearAllPlaceInteractivitiesImpl,
reloadFieldImpl,
updateFieldDataImpl,
} from "./commonSlice";
import { DeckState, initDeckImpl } from "./deckSlice";
import {
addExtraDeckIdleInteractivitiesImpl,
extraDeckCase,
ExtraDeckState,
removeExtraDeckImpl,
} from "./extraDeckSlice";
import {
addGraveyardIdleInteractivitiesImpl,
graveyardCase,
GraveyardState,
initGraveyardImpl,
removeGraveyardImpl,
} from "./graveyardSlice";
import {
addHandsIdleInteractivityImpl,
clearHandsIdleInteractivityImpl,
handsCase,
HandState,
removeHandImpl,
} from "./handsSlice";
import { hintCase, HintState, initHintImpl } from "./hintSlice";
import { infoInitImpl, InitInfo, updateHpImpl } from "./initInfoSlice";
import {
addMagicIdleInteractivitiesImpl,
addMagicPlaceInteractivitiesImpl,
clearMagicIdleInteractivitiesImpl,
clearMagicPlaceInteractivitiesImpl,
initMagicsImpl,
magicCase,
MagicState,
removeMagicImpl,
setMagicPositionImpl,
} from "./magicSlice";
import {
checkCardModalCase,
checkCardModalV2Case,
checkCardModalV3Case,
clearCheckCounterImpl,
ModalState,
optionModalCase,
resetCheckCardModalImpl,
resetCheckCardModalV2Impl,
resetCheckCardModalV3Impl,
resetOptionModalImpl,
resetPositionModalImpl,
resetSortCardModalImpl,
setCardListModalInfoImpl,
setCardListModalIsOpenImpl,
setCardModalCountersImpl,
setCardModalInteractiviesImpl,
setCardModalIsOpenImpl,
setCardModalMetaImpl,
setCheckCardMOdalCancelAbleImpl,
setCheckCardModalCancelResponseImpl,
setCheckCardModalIsOpenImpl,
setCheckCardModalMinMaxImpl,
setCheckCardModalOnSubmitImpl,
setCheckCardModalV2CancelAbleImpl,
setCheckCardModalV2FinishAbleImpl,
setCheckCardModalV2IsOpenImpl,
setCheckCardModalV2MinMaxImpl,
setCheckCardModalV2ResponseAbleImpl,
setCheckCardModalV3AllLevelImpl,
setCheckCardModalV3IsOpenImpl,
setCheckCardModalV3MinMaxImpl,
setCheckCardModalV3OverFlowImpl,
setCheckCardModalV3ResponseAbleImpl,
setCheckCounterImpl,
setOptionModalIsOpenImpl,
setPositionModalIsOpenImpl,
setPositionModalPositionsImpl,
setSortCardModalIsOpenImpl,
setYesNoModalIsOpenImpl,
sortCardModalCase,
YesNoModalCase,
} from "./modal/mod";
import {
addMonsterIdleInteractivitiesImpl,
addMonsterPlaceInteractivitiesImpl,
clearMonsterIdleInteractivitiesImpl,
clearMonsterPlaceInteractivitiesImpl,
initMonstersImpl,
monsterCase,
MonsterState,
removeMonsterImpl,
removeOverlayImpl,
setMonsterPositionImpl,
updateMonsterCountersImpl,
} from "./monstersSlice";
import {
newPhaseImpl,
PhaseState,
setEnableBpImpl,
setEnableEpImpl,
setEnableM2Impl,
} from "./phaseSlice";
import { TimeLimit, updateTimeLimitImpl } from "./timeLimit";
import { newTurnImpl } from "./turnSlice";
import MsgWin = ygopro.StocGameMessage.MsgWin;
export interface DuelState {
selfType?: number;
meInitInfo?: InitInfo; // 自己的初始状态
opInitInfo?: InitInfo; // 对手的初始状态
meHands?: HandState; // 自己的手牌
opHands?: HandState; // 对手的手牌
meMonsters?: MonsterState; // 自己的怪兽区状态
opMonsters?: MonsterState; // 对手的怪兽区状态
meMagics?: MagicState; // 自己的魔法陷阱区状态
opMagics?: MagicState; // 对手的魔法陷阱区状态
meGraveyard?: GraveyardState; // 自己的墓地状态
opGraveyard?: GraveyardState; // 对手的墓地状态
meBanishedZone?: BanishedZoneState; // 自己的除外区状态
opBanishedZone?: BanishedZoneState; // 对手的除外区状态
meDeck?: DeckState; // 自己的卡组状态
opDeck?: DeckState; // 对手的卡组状态
meExtraDeck?: ExtraDeckState; // 自己的额外卡组状态
opExtraDeck?: ExtraDeckState; // 对手的额外卡组状态
meTimeLimit?: TimeLimit; // 自己的计时
opTimeLimit?: TimeLimit; // 对手的计时
hint?: HintState;
currentPlayer?: number; // 当前的操作方
phase?: PhaseState;
result?: MsgWin.ActionType;
waiting?: boolean;
unimplemented?: number; // 未处理的`Message`
// UI相关
modalState: ModalState;
}
const initialState: DuelState = {
modalState: {
cardModal: { isOpen: false, interactivies: [], counters: {} },
cardListModal: { isOpen: false, list: [] },
checkCardModal: { isOpen: false, cancelAble: false, tags: [] },
yesNoModal: { isOpen: false },
positionModal: { isOpen: false, positions: [] },
optionModal: { isOpen: false, options: [] },
checkCardModalV2: {
isOpen: false,
cancelAble: false,
finishAble: false,
responseable: false,
selectableOptions: [],
selectedOptions: [],
},
checkCardModalV3: {
isOpen: false,
overflow: false,
allLevel: 0,
mustSelectList: [],
selectAbleList: [],
},
checkCounterModal: {
isOpen: false,
options: [],
},
sortCardModal: {
isOpen: false,
options: [],
},
},
};
const duelSlice = createSlice({
name: "duel",
initialState,
reducers: {
setSelfType: (state, action: PayloadAction<number>) => {
state.selfType = action.payload;
},
infoInit: infoInitImpl,
updateHp: updateHpImpl,
updateTurn: newTurnImpl,
updateTimeLimit: updateTimeLimitImpl,
// 手牌相关`Reducer`
clearHandsIdleInteractivity: clearHandsIdleInteractivityImpl,
addHandsIdleInteractivity: addHandsIdleInteractivityImpl,
removeHand: removeHandImpl,
// 怪兽区相关`Reducer`
initMonsters: initMonstersImpl,
addMonsterPlaceInteractivities: addMonsterPlaceInteractivitiesImpl,
clearMonsterPlaceInteractivities: clearMonsterPlaceInteractivitiesImpl,
addMonsterIdleInteractivities: addMonsterIdleInteractivitiesImpl,
clearMonsterIdleInteractivities: clearMonsterIdleInteractivitiesImpl,
setMonsterPosition: setMonsterPositionImpl,
removeMonster: removeMonsterImpl,
removeOverlay: removeOverlayImpl,
updateMonsterCounters: updateMonsterCountersImpl,
// 魔法陷阱区相关`Reducer`
initMagics: initMagicsImpl,
addMagicPlaceInteractivities: addMagicPlaceInteractivitiesImpl,
clearMagicPlaceInteractivities: clearMagicPlaceInteractivitiesImpl,
addMagicIdleInteractivities: addMagicIdleInteractivitiesImpl,
clearMagicIdleInteractivities: clearMagicIdleInteractivitiesImpl,
setMagicPosition: setMagicPositionImpl,
removeMagic: removeMagicImpl,
// 墓地相关`Reducer`
initGraveyard: initGraveyardImpl,
removeGraveyard: removeGraveyardImpl,
addGraveyardIdleInteractivities: addGraveyardIdleInteractivitiesImpl,
// 除外区相关`Reducer`
initBanishedZone: initBanishedZoneImpl,
removeBanishedZone: removeBanishedZoneImpl,
addBanishedZoneIdleInteractivities: addBanishedZoneIdleInteractivitiesImpl,
// 卡组相关`Reducer`
initDeck: initDeckImpl,
// 额外卡组相关`Reducer`
removeExtraDeck: removeExtraDeckImpl,
addExtraDeckIdleInteractivities: addExtraDeckIdleInteractivitiesImpl,
// 阶段相关
updatePhase: newPhaseImpl,
setEnableBp: setEnableBpImpl,
setEnableM2: setEnableM2Impl,
setEnableEp: setEnableEpImpl,
// UI相关`Reducer`
setCardModalIsOpen: setCardModalIsOpenImpl,
setCardModalMeta: setCardModalMetaImpl,
setCardModalInteractivies: setCardModalInteractiviesImpl,
setCardListModalIsOpen: setCardListModalIsOpenImpl,
setCardListModalInfo: setCardListModalInfoImpl,
setCheckCardModalIsOpen: setCheckCardModalIsOpenImpl,
setCheckCardModalMinMax: setCheckCardModalMinMaxImpl,
setCheckCardModalOnSubmit: setCheckCardModalOnSubmitImpl,
setCheckCardMOdalCancelAble: setCheckCardMOdalCancelAbleImpl,
setCheckCardModalCancelResponse: setCheckCardModalCancelResponseImpl,
resetCheckCardModal: resetCheckCardModalImpl,
setYesNoModalIsOpen: setYesNoModalIsOpenImpl,
setPositionModalIsOpen: setPositionModalIsOpenImpl,
setPositionModalPositions: setPositionModalPositionsImpl,
resetPositionModal: resetPositionModalImpl,
setOptionModalIsOpen: setOptionModalIsOpenImpl,
resetOptionModal: resetOptionModalImpl,
setCheckCardModalV2FinishAble: setCheckCardModalV2FinishAbleImpl,
setCheckCardModalV2MinMax: setCheckCardModalV2MinMaxImpl,
setCheckCardModalV2CancelAble: setCheckCardModalV2CancelAbleImpl,
setCheckCardModalV2IsOpen: setCheckCardModalV2IsOpenImpl,
resetCheckCardModalV2: resetCheckCardModalV2Impl,
setCheckCardModalV2ResponseAble: setCheckCardModalV2ResponseAbleImpl,
setCheckCardModalV3IsOpen: setCheckCardModalV3IsOpenImpl,
setCheckCardModalV3MinMax: setCheckCardModalV3MinMaxImpl,
setCheckCardModalV3AllLevel: setCheckCardModalV3AllLevelImpl,
setCheckCardModalV3OverFlow: setCheckCardModalV3OverFlowImpl,
setCheckCardModalV3ResponseAble: setCheckCardModalV3ResponseAbleImpl,
resetCheckCardModalV3: resetCheckCardModalV3Impl,
setCardModalCounters: setCardModalCountersImpl,
setCheckCounter: setCheckCounterImpl,
clearCheckCounter: clearCheckCounterImpl,
setSortCardModalIsOpen: setSortCardModalIsOpenImpl,
resetSortCardModal: resetSortCardModalImpl,
// 提示相关`Reducer`
initHint: initHintImpl,
// 通用的`Reducer`
clearAllIdleInteractivities: clearAllIdleInteractivitiesImpl,
clearAllPlaceInteractivities: clearAllPlaceInteractivitiesImpl,
updateFieldData: updateFieldDataImpl,
reloadField: reloadFieldImpl,
// 对局结果`Reducer`
setResult: (state, action: PayloadAction<MsgWin.ActionType>) => {
state.result = action.payload;
},
// 等待状态`Reducer`
setWaiting: (state, action: PayloadAction<boolean>) => {
state.waiting = action.payload;
},
// 未处理状态`Reducer`
setUnimplemented: (state, action: PayloadAction<number>) => {
state.unimplemented = action.payload;
},
},
extraReducers(builder) {
handsCase(builder);
hintCase(builder);
monsterCase(builder);
magicCase(builder);
graveyardCase(builder);
banishedZoneCase(builder);
extraDeckCase(builder);
checkCardModalCase(builder);
YesNoModalCase(builder);
optionModalCase(builder);
checkCardModalV2Case(builder);
checkCardModalV3Case(builder);
sortCardModalCase(builder);
},
});
export const {
setSelfType,
infoInit,
updateHp,
updateTurn,
updatePhase,
setEnableBp,
setEnableM2,
setEnableEp,
clearHandsIdleInteractivity,
addHandsIdleInteractivity,
updateTimeLimit,
setCardModalIsOpen,
setCardModalMeta,
setCardModalInteractivies,
initMonsters,
addMonsterPlaceInteractivities,
clearMonsterPlaceInteractivities,
addMonsterIdleInteractivities,
clearMonsterIdleInteractivities,
setMonsterPosition,
removeMonster,
updateMonsterCounters,
removeOverlay,
initMagics,
addMagicPlaceInteractivities,
clearMagicPlaceInteractivities,
addMagicIdleInteractivities,
clearMagicIdleInteractivities,
setMagicPosition,
removeMagic,
removeHand,
initGraveyard,
removeGraveyard,
addGraveyardIdleInteractivities,
setCardListModalIsOpen,
setCardListModalInfo,
setCheckCardModalIsOpen,
setCheckCardModalMinMax,
setCheckCardModalOnSubmit,
setCheckCardMOdalCancelAble,
setCheckCardModalCancelResponse,
resetCheckCardModal,
setYesNoModalIsOpen,
setPositionModalIsOpen,
setPositionModalPositions,
resetPositionModal,
setOptionModalIsOpen,
resetOptionModal,
initDeck,
removeExtraDeck,
addExtraDeckIdleInteractivities,
initBanishedZone,
removeBanishedZone,
addBanishedZoneIdleInteractivities,
setCheckCardModalV2IsOpen,
setCheckCardModalV2MinMax,
setCheckCardModalV2CancelAble,
setCheckCardModalV2FinishAble,
resetCheckCardModalV2,
setCheckCardModalV2ResponseAble,
clearAllIdleInteractivities,
clearAllPlaceInteractivities,
setResult,
setWaiting,
setUnimplemented,
updateFieldData,
reloadField,
setCheckCardModalV3IsOpen,
setCheckCardModalV3MinMax,
setCheckCardModalV3AllLevel,
setCheckCardModalV3OverFlow,
setCheckCardModalV3ResponseAble,
resetCheckCardModalV3,
setCardModalCounters,
setCheckCounter,
clearCheckCounter,
setSortCardModalIsOpen,
resetSortCardModal,
initHint,
} = duelSlice.actions;
export const selectDuelHsStart = (state: RootState) => {
return state.duel.meInitInfo != null;
};
export const selectDuelResult = (state: RootState) => {
return state.duel.result;
};
export const selectWaiting = (state: RootState) => {
return state.duel.waiting;
};
export const selectUnimplemented = (state: RootState) => {
return state.duel.unimplemented;
};
export default duelSlice.reducer;
import { CaseReducer, PayloadAction } from "@reduxjs/toolkit";
import { CardMeta } from "@/api/cards";
import { RootState } from "@/store";
import { DuelState } from "../mod";
// 更新卡牌列表弹窗打开状态
export const setCardListModalIsOpenImpl: CaseReducer<
DuelState,
PayloadAction<boolean>
> = (state, action) => {
state.modalState.cardListModal.isOpen = action.payload;
};
// 更新卡牌列表数据
export const setCardListModalInfoImpl: CaseReducer<
DuelState,
PayloadAction<
{
meta?: CardMeta;
interactivies: { desc: string; response: number }[];
}[]
>
> = (state, action) => {
const list = action.payload;
state.modalState.cardListModal.list = list;
};
export const selectCardListModalIsOpen = (state: RootState) =>
state.duel.modalState.cardListModal.isOpen;
export const selectCardListModalInfo = (state: RootState) =>
state.duel.modalState.cardListModal.list;
import { CaseReducer, PayloadAction } from "@reduxjs/toolkit";
import { CardMeta } from "@/api/cards";
import { RootState } from "@/store";
import { DuelState } from "../mod";
// 更新卡牌弹窗打开状态
export const setCardModalIsOpenImpl: CaseReducer<
DuelState,
PayloadAction<boolean>
> = (state, action) => {
state.modalState.cardModal.isOpen = action.payload;
};
// 更新卡牌弹窗文本
export const setCardModalMetaImpl: CaseReducer<
DuelState,
PayloadAction<CardMeta>
> = (state, action) => {
state.modalState.cardModal.meta = action.payload;
};
// 更新卡牌弹窗互动选项
export const setCardModalInteractiviesImpl: CaseReducer<
DuelState,
PayloadAction<{ desc: string; response: number }[]>
> = (state, action) => {
state.modalState.cardModal.interactivies = action.payload;
};
// 更新卡牌弹窗指示器
export const setCardModalCountersImpl: CaseReducer<
DuelState,
PayloadAction<{ [type: number]: number }>
> = (state, action) => {
state.modalState.cardModal.counters = action.payload;
};
export const selectCardModalIsOpen = (state: RootState) =>
state.duel.modalState.cardModal.isOpen;
export const selectCardModalMeta = (state: RootState) =>
state.duel.modalState.cardModal.meta;
export const selectCardModalInteractivies = (state: RootState) =>
state.duel.modalState.cardModal.interactivies;
export const selectCardModalCounters = (state: RootState) =>
state.duel.modalState.cardModal.counters;
import {
ActionReducerMapBuilder,
CaseReducer,
createAsyncThunk,
PayloadAction,
} from "@reduxjs/toolkit";
import { fetchCard, getCardStr } from "@/api/cards";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import { DuelState } from "../mod";
import { cmpCardLocation, findCardByLocation, judgeSelf } from "../util";
// 更新卡牌选择弹窗打开状态
export const setCheckCardModalIsOpenImpl: CaseReducer<
DuelState,
PayloadAction<boolean>
> = (state, action) => {
state.modalState.checkCardModal.isOpen = action.payload;
};
// 更新卡牌选择弹窗选择数目状态
export const setCheckCardModalMinMaxImpl: CaseReducer<
DuelState,
PayloadAction<{ min: number; max: number }>
> = (state, action) => {
state.modalState.checkCardModal.selectMin = action.payload.min;
state.modalState.checkCardModal.selectMax = action.payload.max;
};
// 更新卡牌选择弹窗的提交回调
export const setCheckCardModalOnSubmitImpl: CaseReducer<
DuelState,
PayloadAction<string>
> = (state, action) => {
state.modalState.checkCardModal.onSubmit = action.payload;
};
// 更新卡牌选择弹窗是否可以取消
export const setCheckCardMOdalCancelAbleImpl: CaseReducer<
DuelState,
PayloadAction<boolean>
> = (state, action) => {
state.modalState.checkCardModal.cancelAble = action.payload;
};
// 更新卡牌选择弹窗取消时返回给服务端的`Response`
export const setCheckCardModalCancelResponseImpl: CaseReducer<
DuelState,
PayloadAction<number>
> = (state, action) => {
state.modalState.checkCardModal.cancelResponse = action.payload;
};
// 增加卡牌选择选项
export const fetchCheckCardMeta = createAsyncThunk(
"duel/fetchCheckCardMeta",
async (param: {
tagName: string;
option: {
code: number;
location: ygopro.CardLocation;
response: number;
effectDescCode?: number;
};
}) => {
// FIXME: 这里如果传的`controler`如果是对手,对应的`code`会为零,这时候就无法更新对应的`Meta`信息了,后续需要修复。`fetchCheckCardMetaV2`和`fetchCheckCardMetaV3`同理
const meta = await fetchCard(param.option.code, true);
const response = {
tagName: param.tagName,
option: {
meta,
location: param.option.location.toObject(),
},
};
return response;
}
);
export const checkCardModalCase = (
builder: ActionReducerMapBuilder<DuelState>
) => {
builder.addCase(fetchCheckCardMeta.pending, (state, action) => {
const tagName = action.meta.arg.tagName;
const code = action.meta.arg.option.code;
const location = action.meta.arg.option.location;
const controler = location.controler;
const effectDescCode = action.meta.arg.option.effectDescCode;
const response = action.meta.arg.option.response;
const combinedTagName = judgeSelf(controler, state)
? `我方的${tagName}`
: `对方的${tagName}`;
const newID =
code != 0 ? code : findCardByLocation(state, location)?.occupant?.id || 0;
const newOption = {
meta: { id: newID, data: {}, text: {} },
location: location.toObject(),
effectDescCode,
response,
};
for (const tag of state.modalState.checkCardModal.tags) {
if (tag.tagName === combinedTagName) {
tag.options.push(newOption);
return;
}
}
state.modalState.checkCardModal.tags.push({
tagName: combinedTagName,
options: [newOption],
});
});
builder.addCase(fetchCheckCardMeta.fulfilled, (state, action) => {
const tagName = action.payload.tagName;
const option = action.payload.option;
const controler = option.location.controler!;
const combinedTagName = judgeSelf(controler, state)
? `我方的${tagName}`
: `对方的${tagName}`;
for (const tag of state.modalState.checkCardModal.tags) {
if (tag.tagName === combinedTagName) {
for (const old of tag.options) {
if (
option.meta.id == old.meta.id &&
cmpCardLocation(option.location, old.location)
) {
const cardID = old.meta.id;
old.meta = option.meta;
old.meta.id = cardID;
const effectDescCode = old.effectDescCode;
const effectDesc = effectDescCode
? getCardStr(old.meta, effectDescCode & 0xf)
: undefined;
old.effectDesc = effectDesc;
}
}
}
}
});
};
export const resetCheckCardModalImpl: CaseReducer<DuelState> = (state) => {
state.modalState.checkCardModal.isOpen = false;
state.modalState.checkCardModal.selectMin = undefined;
state.modalState.checkCardModal.selectMax = undefined;
state.modalState.checkCardModal.cancelAble = false;
state.modalState.checkCardModal.cancelResponse = undefined;
state.modalState.checkCardModal.tags = [];
};
export const selectCheckCardModalIsOpen = (state: RootState) =>
state.duel.modalState.checkCardModal.isOpen;
export const selectCheckCardModalMinMax = (state: RootState) => {
return {
min: state.duel.modalState.checkCardModal.selectMin || 0,
max: state.duel.modalState.checkCardModal.selectMax || 0,
};
};
export const selectCheckCardModalTags = (state: RootState) =>
state.duel.modalState.checkCardModal.tags;
export const selectCheckCardModalOnSubmit = (state: RootState) =>
state.duel.modalState.checkCardModal.onSubmit;
export const selectCheckCardModalCancelAble = (state: RootState) =>
state.duel.modalState.checkCardModal.cancelAble;
export const selectCheckCardModalCacnelResponse = (state: RootState) =>
state.duel.modalState.checkCardModal.cancelResponse;
import {
ActionReducerMapBuilder,
CaseReducer,
createAsyncThunk,
} from "@reduxjs/toolkit";
import { fetchCard } from "@/api/cards";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import { DuelReducer } from "../generic";
import { DuelState } from "../mod";
import { findCardByLocation } from "../util";
// 更新打开状态
export const setCheckCardModalV2IsOpenImpl: DuelReducer<boolean> = (
state,
action
) => {
state.modalState.checkCardModalV2.isOpen = action.payload;
};
// 更新选择数目
export const setCheckCardModalV2MinMaxImpl: DuelReducer<{
min: number;
max: number;
}> = (state, action) => {
state.modalState.checkCardModalV2.selectMin = action.payload.min;
state.modalState.checkCardModalV2.selectMax = action.payload.max;
};
// 更新是否可以取消
export const setCheckCardModalV2CancelAbleImpl: DuelReducer<boolean> = (
state,
action
) => {
state.modalState.checkCardModalV2.cancelAble = action.payload;
};
// 更新是否可以结束
export const setCheckCardModalV2FinishAbleImpl: DuelReducer<boolean> = (
state,
action
) => {
state.modalState.checkCardModalV2.finishAble = action.payload;
};
// 更新是否可以回应
export const setCheckCardModalV2ResponseAbleImpl: DuelReducer<boolean> = (
state,
action
) => {
state.modalState.checkCardModalV2.responseable = action.payload;
};
// 增加卡牌选项
export const fetchCheckCardMetasV2 = createAsyncThunk(
"duel/fetchCheckCardMetaV2",
async (param: {
selected: boolean;
options: {
code: number;
location: ygopro.CardLocation;
response: number;
}[];
}) => {
const metas = await Promise.all(
param.options.map(async (option) => {
return await fetchCard(option.code, true);
})
);
const response = {
selected: param.selected,
metas,
};
return response;
}
);
export const checkCardModalV2Case = (
builder: ActionReducerMapBuilder<DuelState>
) => {
builder.addCase(fetchCheckCardMetasV2.pending, (state, action) => {
const selected = action.meta.arg.selected;
const options = action.meta.arg.options;
for (const option of options) {
if (option.code == 0) {
const newCode =
findCardByLocation(state, option.location)?.occupant?.id || 0;
option.code = newCode;
}
}
if (selected) {
state.modalState.checkCardModalV2.selectedOptions = options;
} else {
state.modalState.checkCardModalV2.selectableOptions = options;
}
});
builder.addCase(fetchCheckCardMetasV2.fulfilled, (state, action) => {
const selected = action.payload.selected;
const metas = action.payload.metas;
const options = selected
? state.modalState.checkCardModalV2.selectedOptions
: state.modalState.checkCardModalV2.selectableOptions;
options.forEach((option) => {
metas.forEach((meta) => {
if (option.code == meta.id) {
option.name = meta.text.name;
option.desc = meta.text.desc;
}
});
});
});
};
export const resetCheckCardModalV2Impl: CaseReducer<DuelState> = (state) => {
const modalState = state.modalState.checkCardModalV2;
modalState.isOpen = false;
modalState.finishAble = false;
modalState.cancelAble = false;
modalState.responseable = false;
modalState.selectableOptions = [];
modalState.selectedOptions = [];
};
export const selectCheckCardModalV2IsOpen = (state: RootState) =>
state.duel.modalState.checkCardModalV2.isOpen;
export const selectCheckCardModalV2MinMax = (state: RootState) => {
return {
min: state.duel.modalState.checkCardModalV2.selectMin || 0,
max: state.duel.modalState.checkCardModalV2.selectMax || 0,
};
};
export const selectCheckCardModalV2CancelAble = (state: RootState) =>
state.duel.modalState.checkCardModalV2.cancelAble;
export const selectCheckCardModalV2FinishAble = (state: RootState) =>
state.duel.modalState.checkCardModalV2.finishAble;
export const selectCheckCardModalV2ResponseAble = (state: RootState) =>
state.duel.modalState.checkCardModalV2.responseable;
export const selectCheckCardModalV2SelectAbleOptions = (state: RootState) =>
state.duel.modalState.checkCardModalV2.selectableOptions;
export const selectCheckCardModalV2SelectedOptions = (state: RootState) =>
state.duel.modalState.checkCardModalV2.selectedOptions;
import {
ActionReducerMapBuilder,
CaseReducer,
createAsyncThunk,
} from "@reduxjs/toolkit";
import { fetchCard } from "@/api/cards";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import { DuelReducer } from "../generic";
import { DuelState } from "../mod";
import { findCardByLocation } from "../util";
// 更新打开状态
export const setCheckCardModalV3IsOpenImpl: DuelReducer<boolean> = (
state,
action
) => {
state.modalState.checkCardModalV3.isOpen = action.payload;
};
// 更新选择数目
export const setCheckCardModalV3MinMaxImpl: DuelReducer<{
min: number;
max: number;
}> = (state, action) => {
state.modalState.checkCardModalV3.selectMin = action.payload.min;
state.modalState.checkCardModalV3.selectMax = action.payload.max;
};
export const setCheckCardModalV3AllLevelImpl: DuelReducer<number> = (
state,
action
) => {
state.modalState.checkCardModalV3.allLevel = action.payload;
};
export const setCheckCardModalV3OverFlowImpl: DuelReducer<boolean> = (
state,
action
) => {
state.modalState.checkCardModalV3.overflow = action.payload;
};
export const setCheckCardModalV3ResponseAbleImpl: DuelReducer<boolean> = (
state,
action
) => {
state.modalState.checkCardModalV3.responseable = action.payload;
};
// 增加卡牌选项
export const fetchCheckCardMetasV3 = createAsyncThunk(
"duel/fetchCheckCardMetaV3",
async (param: {
mustSelect: boolean;
options: {
code: number;
location: ygopro.CardLocation;
level1: number;
level2: number;
response: number;
}[];
}) => {
const metas = await Promise.all(
param.options.map(async (option) => {
return await fetchCard(option.code, true);
})
);
const response = {
mustSelect: param.mustSelect,
metas,
};
return response;
}
);
export const checkCardModalV3Case = (
builder: ActionReducerMapBuilder<DuelState>
) => {
builder.addCase(fetchCheckCardMetasV3.pending, (state, action) => {
const mustSelect = action.meta.arg.mustSelect;
const options = action.meta.arg.options.map((option) => {
if (option.code == 0) {
const newCode =
findCardByLocation(state, option.location)?.occupant?.id || 0;
option.code = newCode;
}
return {
meta: { id: option.code, data: {}, text: {} },
level1: option.level1,
level2: option.level2,
response: option.response,
};
});
if (mustSelect) {
state.modalState.checkCardModalV3.mustSelectList = options;
} else {
state.modalState.checkCardModalV3.selectAbleList = options;
}
});
builder.addCase(fetchCheckCardMetasV3.fulfilled, (state, action) => {
const mustSelect = action.payload.mustSelect;
const metas = action.payload.metas;
const options = mustSelect
? state.modalState.checkCardModalV3.mustSelectList
: state.modalState.checkCardModalV3.selectAbleList;
options.forEach((option) => {
metas.forEach((meta) => {
if (option.meta.id == meta.id) {
option.meta = meta;
}
});
});
});
};
export const resetCheckCardModalV3Impl: CaseReducer<DuelState> = (state) => {
const modalState = state.modalState.checkCardModalV3;
modalState.isOpen = false;
modalState.overflow = false;
modalState.allLevel = 0;
modalState.responseable = undefined;
modalState.mustSelectList = [];
modalState.selectAbleList = [];
};
export const selectCheckCardModalV3 = (state: RootState) =>
state.duel.modalState.checkCardModalV3;
// 后续对于`MSG_SELECT_XXX`的处理UI都尽量用`Babylon.js`实现而不会通过`Antd`的`Modal`实现,因此这里不追求工程质量,暂时简单实现下。
import { CaseReducer, PayloadAction } from "@reduxjs/toolkit";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import { DuelState } from "../mod";
import { findCardByLocation } from "../util";
type SelectCounter = ReturnType<
typeof ygopro.StocGameMessage.MsgSelectCounter.prototype.toObject
>;
export const setCheckCounterImpl: CaseReducer<
DuelState,
PayloadAction<SelectCounter>
> = (state, action) => {
const modal = state.modalState.checkCounterModal;
const payload = action.payload;
modal.counterType = payload.counter_type;
modal.min = payload.min;
modal.options = payload.options!.map((option) => {
const code = option.code
? option.code
: findCardByLocation(state, option.location!)?.occupant?.id || 0;
return {
code,
max: option.counter_count!,
};
});
modal.isOpen = true;
};
export const clearCheckCounterImpl: CaseReducer<DuelState> = (state) => {
state.modalState.checkCounterModal.isOpen = false;
state.modalState.checkCounterModal.min = undefined;
state.modalState.checkCounterModal.counterType = undefined;
state.modalState.checkCounterModal.options = [];
};
export const selectCheckCounterModal = (state: RootState) =>
state.duel.modalState.checkCounterModal;
import {
ActionReducerMapBuilder,
CaseReducer,
createAsyncThunk,
PayloadAction,
} from "@reduxjs/toolkit";
import { fetchCard, getCardStr } from "@/api/cards";
import { RootState } from "@/store";
import { DuelState } from "../mod";
export const setOptionModalIsOpenImpl: CaseReducer<
DuelState,
PayloadAction<boolean>
> = (state, action) => {
state.modalState.optionModal.isOpen = action.payload;
};
export const resetOptionModalImpl: CaseReducer<DuelState> = (state) => {
state.modalState.optionModal.options = [];
};
// 增加选项
export const fetchOptionMeta = createAsyncThunk(
"duel/fetchOptionMeta",
async (param: { code: number; response: number }) => {
const meta = await fetchCard(param.code >> 4, true);
const msg = getCardStr(meta, param.code & 0xf) || "[?]";
const response = { msg, response: param.response };
return response;
}
);
export const optionModalCase = (
builder: ActionReducerMapBuilder<DuelState>
) => {
builder.addCase(fetchOptionMeta.fulfilled, (state, action) => {
state.modalState.optionModal.options.push(action.payload);
});
};
export const selectOptionModalIsOpen = (state: RootState) =>
state.duel.modalState.optionModal.isOpen;
export const selectOptionModalOptions = (state: RootState) =>
state.duel.modalState.optionModal.options;
import { CaseReducer, PayloadAction } from "@reduxjs/toolkit";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import { DuelState } from "../mod";
export const setPositionModalIsOpenImpl: CaseReducer<
DuelState,
PayloadAction<boolean>
> = (state, action) => {
state.modalState.positionModal.isOpen = action.payload;
};
export const setPositionModalPositionsImpl: CaseReducer<
DuelState,
PayloadAction<ygopro.CardPosition[]>
> = (state, action) => {
state.modalState.positionModal.positions = action.payload;
};
export const resetPositionModalImpl: CaseReducer<DuelState> = (state) => {
state.modalState.positionModal.isOpen = false;
state.modalState.positionModal.positions = [];
};
export const selectPositionModalIsOpen = (state: RootState) =>
state.duel.modalState.positionModal.isOpen;
export const selectPositionModalPositions = (state: RootState) =>
state.duel.modalState.positionModal.positions;
import {
ActionReducerMapBuilder,
CaseReducer,
createAsyncThunk,
} from "@reduxjs/toolkit";
import { fetchCard } from "@/api/cards";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import { DuelReducer } from "../generic";
import { DuelState } from "../mod";
type SortCard = ReturnType<
typeof ygopro.StocGameMessage.MsgSortCard.Info.prototype.toObject
>;
export const setSortCardModalIsOpenImpl: DuelReducer<boolean> = (
state,
action
) => {
state.modalState.sortCardModal.isOpen = action.payload;
};
export const resetSortCardModalImpl: CaseReducer<DuelState> = (state) => {
state.modalState.sortCardModal.isOpen = false;
state.modalState.sortCardModal.options = [];
};
export const fetchSortCardMeta = createAsyncThunk(
"duel/fetchSortCardMeta",
async (param: SortCard) => {
const meta = await fetchCard(param.code!, true);
return {
meta,
response: param.response!,
};
}
);
export const sortCardModalCase = (
builder: ActionReducerMapBuilder<DuelState>
) => {
// 这里更合理的做法是`pending`的时候先更新`options`,等`meta`数据返回后再异步更新`meta`
builder.addCase(fetchSortCardMeta.fulfilled, (state, action) => {
state.modalState.sortCardModal.options.push(action.payload);
});
};
export const selectSortCardModal = (state: RootState) =>
state.duel.modalState.sortCardModal;
import {
ActionReducerMapBuilder,
CaseReducer,
createAsyncThunk,
PayloadAction,
} from "@reduxjs/toolkit";
import { CardMeta, fetchCard } from "@/api/cards";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchStrings, getStrings } from "@/api/strings";
import { RootState } from "@/store";
import { DuelState } from "../mod";
// 更新YesNo弹窗是否打开状态
export const setYesNoModalIsOpenImpl: CaseReducer<
DuelState,
PayloadAction<boolean>
> = (state, action) => {
state.modalState.yesNoModal.isOpen = action.payload;
};
// 设置YesNo弹窗展示内容
export const fetchYesNoMeta = createAsyncThunk(
"duel/fetchYesNoMeta",
async (param: {
code: number;
location: ygopro.CardLocation;
descCode: number;
textGenerator: (
desc: string,
cardMeta: CardMeta,
cardLocation: ygopro.CardLocation
) => string;
}) => {
const desc = fetchStrings("!system", param.descCode);
const meta = await fetchCard(param.code, true);
// TODO: 国际化文案
return param.textGenerator(desc, meta, param.location);
}
);
export const fetchYesNoMetaWithEffecDesc = createAsyncThunk(
"duel/fetchYesNoMetaWithEffecDesc",
async (effectDesc: number) => {
return getStrings(effectDesc);
}
);
export const YesNoModalCase = (builder: ActionReducerMapBuilder<DuelState>) => {
builder.addCase(fetchYesNoMeta.fulfilled, (state, action) => {
state.modalState.yesNoModal.msg = action.payload;
});
builder.addCase(fetchYesNoMetaWithEffecDesc.fulfilled, (state, action) => {
state.modalState.yesNoModal.msg = action.payload;
});
};
export const selectYesNoModalIsOpen = (state: RootState) =>
state.duel.modalState.yesNoModal.isOpen;
export const selectYesNOModalMsg = (state: RootState) =>
state.duel.modalState.yesNoModal.msg;
import {
ActionReducerMapBuilder,
CaseReducer,
createAsyncThunk,
PayloadAction,
} from "@reduxjs/toolkit";
import { fetchCard } from "@/api/cards";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { RootState } from "@/store";
import {
clearIdleInteractivities,
clearPlaceInteractivities,
createAsyncMetaThunk,
DuelFieldState,
extendIdleInteractivities,
extendOccupant,
extendPlaceInteractivity,
Interactivity,
removeOccupant,
removeOverlay,
setPosition,
} from "./generic";
import { DuelState } from "./mod";
import { judgeSelf } from "./util";
type MsgUpdateCounter = ReturnType<
typeof ygopro.StocGameMessage.MsgUpdateCounter.prototype.toObject
>;
export interface MonsterState extends DuelFieldState {}
// 初始化怪兽区状态
export const initMonstersImpl: CaseReducer<DuelState, PayloadAction<number>> = (
state,
action
) => {
const player = action.payload;
const monsters = {
inner: [
{
location: {
controler: player,
location: ygopro.CardZone.MZONE,
},
idleInteractivities: [],
counters: {},
},
{
location: {
controler: player,
location: ygopro.CardZone.MZONE,
},
idleInteractivities: [],
counters: {},
},
{
location: {
controler: player,
location: ygopro.CardZone.MZONE,
},
idleInteractivities: [],
counters: {},
},
{
location: {
controler: player,
location: ygopro.CardZone.MZONE,
},
idleInteractivities: [],
counters: {},
},
{
location: {
controler: player,
location: ygopro.CardZone.MZONE,
},
idleInteractivities: [],
counters: {},
},
{
location: {
controler: player,
location: ygopro.CardZone.MZONE,
},
idleInteractivities: [],
counters: {},
},
{
location: {
controler: player,
location: ygopro.CardZone.MZONE,
},
idleInteractivities: [],
counters: {},
},
],
};
if (judgeSelf(player, state)) {
state.meMonsters = monsters;
} else {
state.opMonsters = monsters;
}
};
export const addMonsterPlaceInteractivitiesImpl: CaseReducer<
DuelState,
PayloadAction<[number, number]>
> = (state, action) => {
const controler = action.payload[0];
const sequence = action.payload[1];
const monsters = judgeSelf(controler, state)
? state.meMonsters
: state.opMonsters;
extendPlaceInteractivity(
monsters,
controler,
sequence,
ygopro.CardZone.MZONE
);
};
export const clearMonsterPlaceInteractivitiesImpl: CaseReducer<
DuelState,
PayloadAction<number>
> = (state, action) => {
const player = action.payload;
const monsters = judgeSelf(player, state)
? state.meMonsters
: state.opMonsters;
clearPlaceInteractivities(monsters);
};
export const addMonsterIdleInteractivitiesImpl: CaseReducer<
DuelState,
PayloadAction<{
player: number;
sequence: number;
interactivity: Interactivity<number>;
}>
> = (state, action) => {
const monsters = judgeSelf(action.payload.player, state)
? state.meMonsters
: state.opMonsters;
extendIdleInteractivities(
monsters,
action.payload.sequence,
action.payload.interactivity
);
};
export const updateMonsterCountersImpl: CaseReducer<
DuelState,
PayloadAction<MsgUpdateCounter>
> = (state, action) => {
const monsters = judgeSelf(action.payload.location?.controler!, state)
? state.meMonsters
: state.opMonsters;
if (monsters) {
const target = monsters.inner.find(
(_, idx) => idx == action.payload.location?.sequence!
);
if (target) {
const count = action.payload.count!;
const counterType = action.payload.action_type!;
switch (action.payload.action_type!) {
case ygopro.StocGameMessage.MsgUpdateCounter.ActionType.ADD: {
if (counterType in target.counters) {
target.counters[counterType] += count;
} else {
target.counters[counterType] = count;
}
break;
}
case ygopro.StocGameMessage.MsgUpdateCounter.ActionType.REMOVE: {
if (counterType in target.counters) {
target.counters[counterType] -= count;
}
break;
}
default: {
break;
}
}
}
}
};
export const clearMonsterIdleInteractivitiesImpl: CaseReducer<
DuelState,
PayloadAction<number>
> = (state, action) => {
const monsters = judgeSelf(action.payload, state)
? state.meMonsters
: state.opMonsters;
clearIdleInteractivities(monsters);
};
// 增加怪兽
export const fetchMonsterMeta = createAsyncMetaThunk("duel/fetchMonsterMeta");
// 增加怪兽的超量素材
export const fetchOverlayMeta = createAsyncThunk(
"duel/fetchOverlayMeta",
async (param: {
controler: number;
sequence: number;
overlayCodes: number[];
append?: boolean;
}) => {
const controler = param.controler;
const sequence = param.sequence;
const overlayCodes = param.overlayCodes;
const metas = await Promise.all(
overlayCodes.map(async (id) => {
if (id == 0) {
return { id, data: {}, text: {} };
} else {
return await fetchCard(id, true);
}
})
);
const response = { controler, sequence, metas };
return response;
}
);
export const monsterCase = (builder: ActionReducerMapBuilder<DuelState>) => {
builder.addCase(fetchMonsterMeta.pending, (state, action) => {
// Meta结果没返回之前先更新`ID`
const controler = action.meta.arg.controler;
const sequence = action.meta.arg.sequence;
const position = action.meta.arg.position;
const code = action.meta.arg.code;
const meta = { id: code, data: {}, text: {} };
if (judgeSelf(controler, state)) {
extendOccupant(state.meMonsters, meta, sequence, position);
} else {
extendOccupant(state.opMonsters, meta, sequence, position);
}
});
builder.addCase(fetchMonsterMeta.fulfilled, (state, action) => {
const controler = action.payload.controler;
const sequence = action.payload.sequence;
const meta = action.payload.meta;
if (judgeSelf(controler, state)) {
extendOccupant(state.meMonsters, meta, sequence);
} else {
extendOccupant(state.opMonsters, meta, sequence);
}
});
builder.addCase(fetchOverlayMeta.pending, (state, action) => {
// Meta结果没返回之前先更新`ID`
const controler = action.meta.arg.controler;
const sequence = action.meta.arg.sequence;
const overlayCodes = action.meta.arg.overlayCodes;
const append = action.meta.arg.append;
const metas = overlayCodes.map((id) => {
return { id, data: {}, text: {} };
});
const monsters = judgeSelf(controler, state)
? state.meMonsters
: state.opMonsters;
if (monsters) {
const target = monsters.inner.find((_, idx) => idx == sequence);
if (target && target.occupant) {
if (append) {
target.overlay_materials = (target.overlay_materials || []).concat(
metas
);
} else {
target.overlay_materials = metas;
}
}
}
});
builder.addCase(fetchOverlayMeta.fulfilled, (state, action) => {
const controler = action.payload.controler;
const sequence = action.payload.sequence;
const overlayMetas = action.payload.metas;
const monsters = judgeSelf(controler, state)
? state.meMonsters
: state.opMonsters;
if (monsters) {
const target = monsters.inner.find((_, idx) => idx == sequence);
for (const meta of overlayMetas) {
for (const overlay of target?.overlay_materials || []) {
if (meta.id == overlay.id) {
overlay.data = meta.data;
overlay.text = meta.text;
}
}
}
}
});
};
// 删除怪兽
export const removeMonsterImpl: CaseReducer<
DuelState,
PayloadAction<{ controler: number; sequence: number }>
> = (state, action) => {
const controler = action.payload.controler;
const monsters = judgeSelf(controler, state)
? state.meMonsters
: state.opMonsters;
removeOccupant(monsters, action.payload.sequence);
removeOverlay(monsters, action.payload.sequence);
};
// 删除超量素材
export const removeOverlayImpl: CaseReducer<
DuelState,
PayloadAction<{
controler: number;
sequence: number;
overlaySequence: number;
}>
> = (state, action) => {
const controler = action.payload.controler;
const sequence = action.payload.sequence;
const overlaySequence = action.payload.overlaySequence;
const monsters = judgeSelf(controler, state)
? state.meMonsters
: state.opMonsters;
if (monsters) {
const target = monsters.inner.find((_, idx) => idx == sequence);
if (target && target.overlay_materials) {
target.overlay_materials = target.overlay_materials.filter(
(_, idx) => idx != overlaySequence
);
}
}
};
// 改变怪兽表示形式
export const setMonsterPositionImpl: CaseReducer<
DuelState,
PayloadAction<{
controler: number;
sequence: number;
position: ygopro.CardPosition;
}>
> = (state, action) => {
const controler = action.payload.controler;
const sequence = action.payload.sequence;
const position = action.payload.position;
const monsters = judgeSelf(controler, state)
? state.meMonsters
: state.opMonsters;
setPosition(monsters, sequence, position);
};
export const selectMeMonsters = (state: RootState) =>
state.duel.meMonsters || { inner: [] };
export const selectOpMonsters = (state: RootState) =>
state.duel.opMonsters || { inner: [] };
import { CaseReducer, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "@/store";
import { DuelState } from "./mod";
export interface PhaseState {
currentPhase: string; // 当前的阶段
enableBp: boolean; // 允许进入战斗阶段
enableM2: boolean; // 允许进入M2阶段
enableEp: boolean; // 允许回合结束
}
export const newPhaseImpl: CaseReducer<DuelState, PayloadAction<string>> = (
state,
action
) => {
if (state.phase) {
state.phase.currentPhase = action.payload;
} else {
state.phase = {
currentPhase: action.payload,
enableBp: false,
enableM2: false,
enableEp: false,
};
}
};
export const setEnableBpImpl: CaseReducer<DuelState, PayloadAction<boolean>> = (
state,
action
) => {
if (state.phase) {
state.phase.enableBp = action.payload;
}
};
export const setEnableM2Impl: CaseReducer<DuelState, PayloadAction<boolean>> = (
state,
action
) => {
if (state.phase) {
state.phase.enableM2 = action.payload;
}
};
export const setEnableEpImpl: CaseReducer<DuelState, PayloadAction<boolean>> = (
state,
action
) => {
if (state.phase) {
state.phase.enableEp = action.payload;
}
};
export const selectCurrentPhase = (state: RootState) =>
state.duel.phase?.currentPhase;
export const selectEnableBp = (state: RootState) =>
state.duel.phase?.enableBp || false;
export const selectEnableM2 = (state: RootState) =>
state.duel.phase?.enableBp || false;
export const selectEnableEp = (state: RootState) =>
state.duel.phase?.enableEp || false;
import { CaseReducer, PayloadAction } from "@reduxjs/toolkit";
import { DuelState } from "./mod";
import { judgeSelf } from "./util";
export interface TimeLimit {
leftTime: number;
}
// 更新计时
export const updateTimeLimitImpl: CaseReducer<
DuelState,
PayloadAction<[number, number]>
> = (state, action) => {
const player = action.payload[0];
const leftTime = action.payload[1];
if (judgeSelf(player, state)) {
state.meTimeLimit = { leftTime };
} else {
state.opTimeLimit = { leftTime };
}
};
import { CaseReducer, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "@/store";
import { DuelState } from "./mod";
import { judgeSelf } from "./util";
export const newTurnImpl: CaseReducer<DuelState, PayloadAction<number>> = (
state,
action
) => {
state.currentPlayer = action.payload;
};
export const selectCurrentPlayerIsMe = (state: RootState) =>
judgeSelf(state.duel.currentPlayer!, state.duel);
/*
* 对局内状态更新逻辑的一些共用函数和数据结构
*
* */
import { Draft } from "@reduxjs/toolkit";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { CardState } from "./generic";
import { DuelState } from "./mod";
type Location =
| ygopro.CardLocation
| ReturnType<typeof ygopro.CardLocation.prototype.toObject>;
/*
* 通过`player`和`selfType`判断是应该处理自己还是对手
* */
export function judgeSelf(player: number, state: Draft<DuelState>): boolean {
const selfType = state.selfType;
if (selfType === 1) {
// 自己是先攻
return player == 0;
} else if (selfType === 2) {
// 自己是后攻
return player == 1;
} else {
// currently never reach
return false;
}
}
/*
* 通过`controler`,`zone`和`sequence`获取卡牌状态*/
export function findCardByLocation(
state: Draft<DuelState>,
location: Location
): CardState | undefined {
const controler = location.controler!;
const zone = location.location;
const sequence = location.sequence;
const finder = (_: any, idx: number) => idx == sequence;
switch (zone) {
case ygopro.CardZone.HAND: {
const hands = judgeSelf(controler, state) ? state.meHands : state.opHands;
return hands?.inner.find(finder);
}
case ygopro.CardZone.MZONE: {
const monsters = judgeSelf(controler, state)
? state.meMonsters
: state.opMonsters;
return monsters?.inner.find(finder);
}
case ygopro.CardZone.SZONE: {
const magics = judgeSelf(controler, state)
? state.meMagics
: state.opMagics;
return magics?.inner.find(finder);
}
case ygopro.CardZone.REMOVED: {
const banishedZones = judgeSelf(controler, state)
? state.meBanishedZone
: state.opBanishedZone;
return banishedZones?.inner.find(finder);
}
case ygopro.CardZone.GRAVE: {
const cemerety = judgeSelf(controler, state)
? state.meGraveyard
: state.opGraveyard;
return cemerety?.inner.find(finder);
}
default: {
return undefined;
}
}
}
export function cmpCardLocation(
left: Location,
right?: Location,
strict?: boolean
): boolean {
if (strict) {
return JSON.stringify(left) === JSON.stringify(right);
} else {
return (
left.controler === right?.controler &&
left.location === right?.location &&
left.sequence === right?.sequence
);
}
}
/*
* 加入房间状态更新逻辑
*
* */
import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "@/store";
export interface JoinState {
value: boolean;
}
const initialState: JoinState = {
value: false,
};
const joinedSlice = createSlice({
name: "join",
initialState,
reducers: {
setJoined: (state) => {
state.value = true;
},
setUnJoined: (state) => {
state.value = false;
},
},
});
export const { setJoined, setUnJoined } = joinedSlice.actions;
export const selectJoined = (state: RootState) => state.join.value;
export default joinedSlice.reducer;
/*
* 猜拳页面的状态更新逻辑
*
* */
import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "@/store";
export interface moraState {
duelStart: boolean;
selectHandAble: boolean;
selectTpAble: boolean;
}
const initialState: moraState = {
duelStart: false,
selectHandAble: false,
selectTpAble: false,
};
const moraSlice = createSlice({
name: "mora",
initialState,
reducers: {
duelStart: (state) => {
state.duelStart = true;
},
selectHandAble: (state) => {
state.selectHandAble = true;
},
unSelectHandAble: (state) => {
state.selectHandAble = false;
},
selectTpAble: (state) => {
state.selectTpAble = true;
},
unSelectTpAble: (state) => {
state.selectTpAble = false;
},
},
});
export const {
duelStart,
selectHandAble,
unSelectHandAble,
selectTpAble,
unSelectTpAble,
} = moraSlice.actions;
export const selectDuelStart = (state: RootState) => state.mora.duelStart;
export const selectHandSelectAble = (state: RootState) =>
state.mora.selectHandAble;
export const selectTpSelectAble = (state: RootState) => state.mora.selectTpAble;
export default moraSlice.reducer;
/*
* 进入房间的玩家状态更新逻辑
*
* */
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "@/store";
export interface Player {
name?: string;
state?: string;
isHost?: boolean;
deckInfo?: deckInfo;
}
export interface deckInfo {
mainCnt: number;
extraCnt: number;
sideCnt: number;
}
export interface playerState {
player0: Player;
player1: Player;
observerCount: number;
isHost: boolean;
}
const initialState: playerState = {
player0: {},
player1: {},
observerCount: 0,
isHost: false,
};
const playerSlice = createSlice({
name: "player",
initialState,
reducers: {
player0Enter: (state, action: PayloadAction<string>) => {
state.player0.name = action.payload;
},
player1Enter: (state, action: PayloadAction<string>) => {
state.player1.name = action.payload;
},
player0Update: (state, action: PayloadAction<string>) => {
state.player0.state = action.payload;
},
player1Update: (state, action: PayloadAction<string>) => {
state.player1.state = action.payload;
},
player0Leave: (state) => {
state.player0 = {};
},
player1Leave: (state) => {
state.player1 = {};
},
player0DeckInfo: (state, action: PayloadAction<deckInfo>) => {
state.player0.deckInfo = action.payload;
},
player1DeckInfo: (state, action: PayloadAction<deckInfo>) => {
state.player1.deckInfo = action.payload;
},
hostChange: (state, action: PayloadAction<number>) => {
const i = action.payload;
if (i === 0) {
state.player0.isHost = true;
state.player1.isHost = false;
} else {
state.player1.isHost = true;
state.player0.isHost = false;
}
},
observerIncrement: (state) => {
state.observerCount += 1;
},
observerChange: (state, action: PayloadAction<number>) => {
state.observerCount = action.payload;
},
updateIsHost: (state, action: PayloadAction<boolean>) => {
state.isHost = action.payload;
},
},
});
export const {
player0Enter,
player1Enter,
player0Update,
player1Update,
player0Leave,
player1Leave,
player0DeckInfo,
player1DeckInfo,
hostChange,
observerIncrement,
observerChange,
updateIsHost,
} = playerSlice.actions;
export const selectPlayer0 = (state: RootState) => state.player.player0;
export const selectPlayer1 = (state: RootState) => state.player.player1;
export const selectIsHost = (state: RootState) => state.player.isHost;
export const selectObserverCount = (state: RootState) =>
state.player.observerCount;
export default playerSlice.reducer;
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores";
export default (
attack: ygopro.StocGameMessage.MsgAttack,
dispatch: AppDispatch
) => {
dispatch(
fetchEsHintMeta({ originMsg: "「[?]」攻击时", location: attack.location })
);
export default (attack: ygopro.StocGameMessage.MsgAttack) => {
fetchEsHintMeta({
originMsg: "「[?]」攻击时",
location: attack.location,
});
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores";
export default (
_: ygopro.StocGameMessage.MsgAttackDisabled,
dispatch: AppDispatch
) => {
dispatch(fetchEsHintMeta({ originMsg: "攻击被无效时" }));
export default (_: ygopro.StocGameMessage.MsgAttackDisabled) => {
fetchEsHintMeta({ originMsg: "攻击被无效时" });
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
export default (
chaining: ygopro.StocGameMessage.MsgChaining,
dispatch: AppDispatch
) => {
dispatch(
fetchEsHintMeta({ originMsg: "「[?]」被发动时", cardID: chaining.code })
);
import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores";
export default (chaining: ygopro.StocGameMessage.MsgChaining) => {
fetchEsHintMeta({
originMsg: "「[?]」被发动时",
cardID: chaining.code,
});
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchHandsMeta } from "@/reducers/duel/handsSlice";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { fetchEsHintMeta, matStore } from "@/stores";
export default (
draw: ygopro.StocGameMessage.MsgDraw,
dispatch: AppDispatch
) => {
dispatch(fetchEsHintMeta({ originMsg: "玩家抽卡时" }));
dispatch(fetchHandsMeta({ controler: draw.player, codes: draw.cards }));
export default (draw: ygopro.StocGameMessage.MsgDraw) => {
fetchEsHintMeta({ originMsg: "玩家抽卡时" });
matStore.hands.of(draw.player).add(draw.cards);
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores";
export default (
_: ygopro.StocGameMessage.MsgFlipSummoned,
dispatch: AppDispatch
) => {
dispatch(fetchEsHintMeta({ originMsg: 1608 }));
export default (_: ygopro.StocGameMessage.MsgFlipSummoned) => {
fetchEsHintMeta({ originMsg: 1608 });
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores";
export default (
flipSummoning: ygopro.StocGameMessage.MsgFlipSummoning,
dispatch: AppDispatch
) => {
dispatch(
fetchEsHintMeta({
originMsg: "「[?]」反转召唤宣言时",
cardID: flipSummoning.code,
})
);
export default (flipSummoning: ygopro.StocGameMessage.MsgFlipSummoning) => {
fetchEsHintMeta({
originMsg: "「[?]」反转召唤宣言时",
cardID: flipSummoning.code,
});
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { setWaiting } from "@/reducers/duel/mod";
import { store } from "@/store";
import { ygopro } from "@/api";
import { matStore } from "@/stores";
import onMsgAttack from "./attack";
import onMsgAttackDisable from "./attackDisable";
......@@ -57,211 +56,210 @@ const ActiveList = [
];
export default function handleGameMsg(pb: ygopro.YgoStocMsg) {
const dispatch = store.dispatch;
const msg = pb.stoc_game_msg;
if (ActiveList.includes(msg.gameMsg)) {
dispatch(setWaiting(false));
matStore.waiting = false;
}
switch (msg.gameMsg) {
case "start": {
onMsgStart(msg.start, dispatch);
onMsgStart(msg.start);
break;
}
case "draw": {
onMsgDraw(msg.draw, dispatch);
onMsgDraw(msg.draw);
break;
}
case "new_turn": {
onMsgNewTurn(msg.new_turn, dispatch);
onMsgNewTurn(msg.new_turn);
break;
}
case "new_phase": {
onMsgNewPhase(msg.new_phase, dispatch);
onMsgNewPhase(msg.new_phase);
break;
}
case "hint": {
onMsgHint(msg.hint, dispatch);
onMsgHint(msg.hint);
break;
}
case "select_idle_cmd": {
onMsgSelectIdleCmd(msg.select_idle_cmd, dispatch);
onMsgSelectIdleCmd(msg.select_idle_cmd);
break;
}
case "select_place": {
onMsgSelectPlace(msg.select_place, dispatch);
onMsgSelectPlace(msg.select_place);
break;
}
case "move": {
onMsgMove(msg.move, dispatch);
onMsgMove(msg.move);
break;
}
case "select_card": {
onMsgSelectCard(msg.select_card, dispatch);
onMsgSelectCard(msg.select_card);
break;
}
case "select_chain": {
onMsgSelectChain(msg.select_chain, dispatch);
onMsgSelectChain(msg.select_chain);
break;
}
case "select_effect_yn": {
onMsgSelectEffectYn(msg.select_effect_yn, dispatch);
onMsgSelectEffectYn(msg.select_effect_yn);
break;
}
case "select_position": {
onMsgSelectPosition(msg.select_position, dispatch);
onMsgSelectPosition(msg.select_position);
break;
}
case "select_option": {
onMsgSelectOption(msg.select_option, dispatch);
onMsgSelectOption(msg.select_option);
break;
}
case "shuffle_hand": {
onMsgShuffleHand(msg.shuffle_hand, dispatch);
onMsgShuffleHand(msg.shuffle_hand);
break;
}
case "select_battle_cmd": {
onMsgSelectBattleCmd(msg.select_battle_cmd, dispatch);
onMsgSelectBattleCmd(msg.select_battle_cmd);
break;
}
case "pos_change": {
onMsgPosChange(msg.pos_change, dispatch);
onMsgPosChange(msg.pos_change);
break;
}
case "select_unselect_card": {
onMsgSelectUnselectCard(msg.select_unselect_card, dispatch);
onMsgSelectUnselectCard(msg.select_unselect_card);
break;
}
case "select_yes_no": {
onMsgSelectYesNo(msg.select_yes_no, dispatch);
onMsgSelectYesNo(msg.select_yes_no);
break;
}
case "update_hp": {
onMsgUpdateHp(msg.update_hp, dispatch);
onMsgUpdateHp(msg.update_hp);
break;
}
case "win": {
onMsgWin(msg.win, dispatch);
onMsgWin(msg.win);
break;
}
case "wait": {
onMsgWait(msg.wait, dispatch);
onMsgWait(msg.wait);
break;
}
case "update_data": {
onMsgUpdateData(msg.update_data, dispatch);
onMsgUpdateData(msg.update_data);
break;
}
case "reload_field": {
onMsgReloadField(msg.reload_field, dispatch);
onMsgReloadField(msg.reload_field);
break;
}
case "select_sum": {
onMsgSelectSum(msg.select_sum, dispatch);
onMsgSelectSum(msg.select_sum);
break;
}
case "select_tribute": {
onMsgSelectTribute(msg.select_tribute, dispatch);
onMsgSelectTribute(msg.select_tribute);
break;
}
case "update_counter": {
onMsgUpdateCounter(msg.update_counter, dispatch);
onMsgUpdateCounter(msg.update_counter);
break;
}
case "select_counter": {
onMsgSelectCounter(msg.select_counter, dispatch);
onMsgSelectCounter(msg.select_counter);
break;
}
case "sort_card": {
onMsgSortCard(msg.sort_card, dispatch);
onMsgSortCard(msg.sort_card);
break;
}
case "set": {
onMsgSet(msg.set, dispatch);
onMsgSet(msg.set);
break;
}
case "swap": {
onMsgSwap(msg.swap, dispatch);
onMsgSwap(msg.swap);
break;
}
case "attack": {
onMsgAttack(msg.attack, dispatch);
onMsgAttack(msg.attack);
break;
}
case "attack_disable": {
onMsgAttackDisable(msg.attack_disable, dispatch);
onMsgAttackDisable(msg.attack_disable);
break;
}
case "chaining": {
onMsgChaining(msg.chaining, dispatch);
onMsgChaining(msg.chaining);
break;
}
case "summoning": {
onMsgSummoning(msg.summoning, dispatch);
onMsgSummoning(msg.summoning);
break;
}
case "summoned": {
onMsgSummoned(msg.summoned, dispatch);
onMsgSummoned(msg.summoned);
break;
}
case "flip_summoning": {
onMsgFlipSummoning(msg.flip_summoning, dispatch);
onMsgFlipSummoning(msg.flip_summoning);
break;
}
case "flip_summoned": {
onMsgFilpSummoned(msg.flip_summoned, dispatch);
onMsgFilpSummoned(msg.flip_summoned);
break;
}
case "sp_summoning": {
onMsgSpSummoning(msg.sp_summoning, dispatch);
onMsgSpSummoning(msg.sp_summoning);
break;
}
case "sp_summoned": {
onMsgSpSummoned(msg.sp_summoned, dispatch);
onMsgSpSummoned(msg.sp_summoned);
break;
}
case "unimplemented": {
onUnimplemented(msg.unimplemented, dispatch);
onUnimplemented(msg.unimplemented);
break;
}
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { ygopro } from "@/api";
import {
fetchCommonHintMeta,
fetchEsHintMeta,
fetchSelectHintMeta,
} from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
} from "@/stores";
import MsgHint = ygopro.StocGameMessage.MsgHint;
export default (hint: MsgHint, dispatch: AppDispatch) => {
export default (hint: MsgHint) => {
switch (hint.hint_type) {
case MsgHint.HintType.HINT_EVENT: {
dispatch(fetchEsHintMeta({ originMsg: hint.hint_data }));
fetchEsHintMeta({ originMsg: hint.hint_data });
break;
}
case MsgHint.HintType.HINT_MESSAGE: {
dispatch(fetchCommonHintMeta(hint.hint_data));
fetchCommonHintMeta(hint.hint_data);
break;
}
case MsgHint.HintType.HINT_SELECTMSG: {
dispatch(
fetchSelectHintMeta({ selectHintData: hint.hint_data, esHint: "" })
);
fetchSelectHintMeta({
selectHintData: hint.hint_data,
esHint: "",
});
break;
}
default: {
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import MsgMove = ygopro.StocGameMessage.MsgMove;
import { fetchBanishedZoneMeta } from "@/reducers/duel/banishedZoneSlice";
import { fetchExtraDeckMeta } from "@/reducers/duel/extraDeckSlice";
import { fetchGraveyardMeta } from "@/reducers/duel/graveyardSlice";
import { insertHandMeta } from "@/reducers/duel/handsSlice";
import { fetchMagicMeta } from "@/reducers/duel/magicSlice";
import {
removeBanishedZone,
removeExtraDeck,
removeGraveyard,
removeHand,
removeMagic,
removeMonster,
removeOverlay,
} from "@/reducers/duel/mod";
import {
fetchMonsterMeta,
fetchOverlayMeta,
} from "@/reducers/duel/monstersSlice";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { fetchOverlayMeta, store } from "@/stores";
type MsgMove = ygopro.StocGameMessage.MsgMove;
import { REASON_MATERIAL } from "../../common";
const { matStore } = store;
const OVERLAY_STACK: { code: number; sequence: number }[] = [];
export default (move: MsgMove, dispatch: AppDispatch) => {
export default (move: MsgMove) => {
const code = move.code;
const from = move.from;
const to = move.to;
const reason = move.reason;
switch (from.location) {
case ygopro.CardZone.HAND: {
dispatch(removeHand([from.controler, from.sequence]));
break;
}
case ygopro.CardZone.MZONE: {
dispatch(
removeMonster({ controler: from.controler, sequence: from.sequence })
);
// TODO: 如果后面做动画的话,要考虑DECK的情况。
// 现在不会对DECK做判断
break;
}
switch (from.location) {
case ygopro.CardZone.MZONE:
case ygopro.CardZone.SZONE: {
dispatch(
removeMagic({ controler: from.controler, sequence: from.sequence })
);
break;
}
case ygopro.CardZone.GRAVE: {
dispatch(
removeGraveyard({ controler: from.controler, sequence: from.sequence })
);
break;
}
case ygopro.CardZone.REMOVED: {
dispatch(
removeBanishedZone({
controler: from.controler,
sequence: from.sequence,
})
);
// 魔陷和怪兽需要清掉占用、清掉超量素材
const target = matStore.in(from.location).of(from.controler)[
from.sequence
];
target.occupant = undefined;
target.overlay_materials = [];
break;
}
case ygopro.CardZone.REMOVED:
case ygopro.CardZone.GRAVE:
case ygopro.CardZone.HAND:
case ygopro.CardZone.EXTRA: {
dispatch(
removeExtraDeck({ controler: from.controler, sequence: from.sequence })
);
// 其余区域就是在list删掉这张卡
matStore.in(from.location).of(from.controler).remove(from.sequence);
break;
}
// 仅仅去除超量素材
case ygopro.CardZone.OVERLAY: {
dispatch(
removeOverlay({
controler: from.controler,
sequence: from.sequence,
overlaySequence: from.overlay_sequence,
})
);
const target = matStore.monsters.of(from.controler)[from.sequence];
if (target && target.overlay_materials) {
target.overlay_materials.splice(from.overlay_sequence, 1);
}
break;
}
default: {
......@@ -92,81 +51,28 @@ export default (move: MsgMove, dispatch: AppDispatch) => {
}
switch (to.location) {
// @ts-ignore
case ygopro.CardZone.MZONE: {
dispatch(
fetchMonsterMeta({
controler: to.controler,
sequence: to.sequence,
position: to.position,
code,
})
);
// 处理超量素材
// 设置超量素材
const overlayMetarials = OVERLAY_STACK.splice(0, OVERLAY_STACK.length);
let sorted = overlayMetarials
const sorted = overlayMetarials
.sort((a, b) => a.sequence - b.sequence)
.map((overlay) => overlay.code);
dispatch(
fetchOverlayMeta({
controler: to.controler,
sequence: to.sequence,
overlayCodes: sorted,
})
);
break;
fetchOverlayMeta(to.controler, to.sequence, sorted);
// 设置Occupant,和魔陷区/其他区共用一个逻辑,特地不写break
}
case ygopro.CardZone.SZONE: {
dispatch(
fetchMagicMeta({
controler: to.controler,
sequence: to.sequence,
position: to.position,
code,
})
);
break;
}
case ygopro.CardZone.GRAVE: {
dispatch(
fetchGraveyardMeta({
controler: to.controler,
sequence: to.sequence,
code,
})
);
matStore
.in(to.location)
.of(to.controler)
.setOccupant(to.sequence, code, to.position);
break;
}
case ygopro.CardZone.REMOVED:
case ygopro.CardZone.GRAVE:
case ygopro.CardZone.EXTRA:
case ygopro.CardZone.HAND: {
dispatch(
insertHandMeta({ controler: to.controler, sequence: to.sequence, code })
);
break;
}
case ygopro.CardZone.REMOVED: {
dispatch(
fetchBanishedZoneMeta({
controler: to.controler,
sequence: to.sequence,
code,
})
);
break;
}
case ygopro.CardZone.EXTRA: {
dispatch(
fetchExtraDeckMeta({
controler: to.controler,
sequence: to.sequence,
code,
})
);
matStore.in(to.location).of(to.controler).insert(to.sequence, code);
break;
}
case ygopro.CardZone.OVERLAY: {
......@@ -176,21 +82,12 @@ export default (move: MsgMove, dispatch: AppDispatch) => {
OVERLAY_STACK.push({ code, sequence: to.overlay_sequence });
} else {
// 其他情况下,比如“宵星的机神 丁吉尔苏”的“补充超量素材”效果,直接更新状态中
dispatch(
fetchOverlayMeta({
controler: to.controler,
sequence: to.sequence,
overlayCodes: [code],
append: true,
})
);
fetchOverlayMeta(to.controler, to.sequence, [code], true);
}
break;
}
default: {
console.log(`Unhandled zone type ${to.location}`);
break;
}
}
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { updatePhase } from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { matStore, type PhaseName } from "@/stores";
export default (
newPhase: ygopro.StocGameMessage.MsgNewPhase,
dispatch: AppDispatch
) => {
dispatch(
updatePhase(
ygopro.StocGameMessage.MsgNewPhase.PhaseType[newPhase.phase_type]
)
);
export default (newPhase: ygopro.StocGameMessage.MsgNewPhase) => {
// ts本身还没有这么智能,所以需要手动指定类型
const currentPhase = ygopro.StocGameMessage.MsgNewPhase.PhaseType[
newPhase.phase_type
] as PhaseName;
matStore.phase.currentPhase = currentPhase;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { updateTurn } from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { matStore } from "@/stores";
export default (
newTurn: ygopro.StocGameMessage.MsgNewTurn,
dispatch: AppDispatch
) => {
export default (newTurn: ygopro.StocGameMessage.MsgNewTurn) => {
const player = newTurn.player;
dispatch(updateTurn(player));
matStore.currentPlayer = player;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { setMagicPosition, setMonsterPosition } from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import MsgPosChange = ygopro.StocGameMessage.MsgPosChange;
import { fetchEsHintMeta, matStore } from "@/stores";
export default (posChange: MsgPosChange) => {
const { location, controler, sequence } = posChange.card_info;
export default (posChange: MsgPosChange, dispatch: AppDispatch) => {
const cardInfo = posChange.card_info;
switch (cardInfo.location) {
switch (location) {
case ygopro.CardZone.MZONE: {
dispatch(
setMonsterPosition({
controler: cardInfo.controler,
sequence: cardInfo.sequence,
position: posChange.cur_position,
})
);
matStore.monsters.of(controler)[sequence].location.position =
posChange.cur_position;
break;
}
case ygopro.CardZone.SZONE: {
dispatch(
setMagicPosition({
controler: cardInfo.controler,
sequence: cardInfo.sequence,
position: posChange.cur_position,
})
);
matStore.magics.of(controler)[sequence].location.position =
posChange.cur_position;
break;
}
default: {
console.log(`Unhandled zone ${cardInfo.location}`);
console.log(`Unhandled zone ${location}`);
}
}
dispatch(fetchEsHintMeta({ originMsg: 1600 }));
fetchEsHintMeta({
originMsg: 1600,
});
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { reloadField } from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import MsgReloadField = ygopro.StocGameMessage.MsgReloadField;
import { ygopro } from "@/api";
import { matStore } from "@/stores";
export default (field: MsgReloadField, dispatch: AppDispatch) => {
dispatch(reloadField(field));
type MsgReloadField = ygopro.StocGameMessage.MsgReloadField;
type ZoneActions = ygopro.StocGameMessage.MsgReloadField.ZoneAction[];
export default (field: MsgReloadField) => {
const _duel_rule = field.duel_rule; // TODO: duel_rule
const gamers = ["me", "op"] as const;
gamers.forEach((gamer) => {
matStore.banishedZones[gamer].length = 0;
matStore.extraDecks[gamer].length = 0;
matStore.graveyards[gamer].length = 0;
matStore.hands[gamer].length = 0;
matStore.monsters[gamer].length = 0;
matStore.magics[gamer].length = 0;
});
const { MZONE, SZONE, HAND, DECK, GRAVE, REMOVED, EXTRA } = ygopro.CardZone;
const zones = [MZONE, SZONE, HAND, DECK, GRAVE, REMOVED, EXTRA] as const;
field.actions.forEach(({ player, zone_actions }) => {
zones.forEach((zone) => {
reloadDuelField(
zone,
zone_actions.filter((item) => item.zone === zone),
player
);
});
});
};
/** 可以理解成reload DuelFieldState */
function reloadDuelField(
cardZone: ygopro.CardZone,
zoneActions: ZoneActions,
controller: number
) {
zoneActions.sort((a, b) => a.sequence - b.sequence);
const cards = zoneActions.map((action) => {
// FIXME: OVERLAY
return {
location: {
controler: controller,
location: action.zone,
position: action.position,
},
idleInteractivities: [],
counters: {},
reload: true,
};
});
matStore.in(cardZone).of(controller).length = 0;
matStore
.in(cardZone)
.of(controller)
.push(...cards);
}
import { ActionCreatorWithPayload } from "@reduxjs/toolkit";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { Interactivity, InteractType } from "@/reducers/duel/generic";
import { ygopro } from "@/api";
import {
addHandsIdleInteractivity,
addMagicIdleInteractivities,
addMonsterIdleInteractivities,
clearAllIdleInteractivities,
setEnableEp,
setEnableM2,
} from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
clearAllIdleInteractivities as clearAllIdleInteractivities,
type Interactivity,
InteractType,
matStore,
} from "@/stores";
import MsgSelectBattleCmd = ygopro.StocGameMessage.MsgSelectBattleCmd;
export default (selectBattleCmd: MsgSelectBattleCmd, dispatch: AppDispatch) => {
export default (selectBattleCmd: MsgSelectBattleCmd) => {
const player = selectBattleCmd.player;
const cmds = selectBattleCmd.battle_cmds;
// 先清掉之前的互动性
dispatch(clearAllIdleInteractivities(player));
const dispatcher = (
battleData: MsgSelectBattleCmd.BattleCmd.BattleData,
interactType: InteractType | undefined,
actionCreator: ActionCreatorWithPayload<
{
player: number;
sequence: number;
interactivity: Interactivity<number>;
},
string
>
) => {
const cardInfo = battleData.card_info;
if (interactType === InteractType.ACTIVATE) {
dispatch(
actionCreator({
player,
sequence: cardInfo.sequence,
interactivity: {
interactType,
activateIndex: battleData.effect_description,
response: battleData.response,
},
})
);
} else if (interactType === InteractType.ATTACK) {
dispatch(
actionCreator({
player,
sequence: cardInfo.sequence,
interactivity: {
interactType,
directAttackAble: battleData.direct_attackable,
response: battleData.response,
},
})
);
} else {
console.log(`Unhandled InteractType`);
}
};
clearAllIdleInteractivities(player);
cmds.forEach((cmd) => {
const interactType = battleTypeToInteracType(cmd.battle_type);
cmd.battle_datas.forEach((data) => {
const cardInfo = data.card_info;
switch (cardInfo.location) {
case ygopro.CardZone.HAND: {
dispatcher(data, interactType, addHandsIdleInteractivity);
break;
}
case ygopro.CardZone.MZONE: {
dispatcher(data, interactType, addMonsterIdleInteractivities);
break;
}
case ygopro.CardZone.SZONE: {
dispatcher(data, interactType, addMagicIdleInteractivities);
const { location, sequence } = data.card_info;
break;
}
default: {
}
// valtio
if (interactType) {
const map: Partial<
Record<InteractType, undefined | Partial<Interactivity<number>>>
> = {
[InteractType.ACTIVATE]: { activateIndex: data.effect_description },
[InteractType.ATTACK]: { directAttackAble: data.direct_attackable },
};
const tmp = map[interactType]; // 添加额外信息
matStore
.in(location)
.of(player)
.addIdleInteractivity(sequence, {
...tmp,
interactType,
response: data.response,
});
} else {
console.warn(`Undefined InteractType`);
}
});
});
dispatch(setEnableM2(selectBattleCmd.enable_m2));
dispatch(setEnableEp(selectBattleCmd.enable_ep));
matStore.phase.enableM2 = selectBattleCmd.enable_m2;
matStore.phase.enableEp = selectBattleCmd.enable_ep;
};
function battleTypeToInteracType(
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import {
setCheckCardModalIsOpen,
setCheckCardModalMinMax,
setCheckCardModalOnSubmit,
} from "@/reducers/duel/mod";
import { fetchCheckCardMeta } from "@/reducers/duel/modal/mod";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import MsgSelectCard = ygopro.StocGameMessage.MsgSelectCard;
import { CardZoneToChinese } from "./util";
import { CardZoneToChinese, fetchCheckCardMeta, messageStore } from "@/stores";
export default (selectCard: MsgSelectCard, dispatch: AppDispatch) => {
export default (selectCard: MsgSelectCard) => {
const _player = selectCard.player;
const _cancelable = selectCard.cancelable; // TODO: 处理可取消逻辑
const min = selectCard.min;
......@@ -17,23 +10,17 @@ export default (selectCard: MsgSelectCard, dispatch: AppDispatch) => {
const cards = selectCard.cards;
// TODO: handle release_param
dispatch(setCheckCardModalMinMax({ min, max }));
dispatch(setCheckCardModalOnSubmit("sendSelectCardResponse"));
messageStore.checkCardModal.selectMin = min;
messageStore.checkCardModal.selectMax = max;
messageStore.checkCardModal.onSubmit = "sendSelectCardResponse";
for (const card of cards) {
const tagName = CardZoneToChinese(card.location.location);
dispatch(
fetchCheckCardMeta({
tagName,
option: {
code: card.code,
location: card.location,
response: card.response,
},
})
);
fetchCheckCardMeta(card.location.location, {
code: card.code,
location: card.location,
response: card.response,
});
}
dispatch(setCheckCardModalIsOpen(true));
messageStore.checkCardModal.isOpen = true;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { sendSelectChainResponse } from "@/api/ocgcore/ocgHelper";
import { fetchSelectHintMeta } from "@/reducers/duel/hintSlice";
import { sendSelectChainResponse, ygopro } from "@/api";
import {
setCheckCardMOdalCancelAble,
setCheckCardModalCancelResponse,
setCheckCardModalIsOpen,
setCheckCardModalMinMax,
setCheckCardModalOnSubmit,
} from "@/reducers/duel/mod";
import { fetchCheckCardMeta } from "@/reducers/duel/modal/mod";
import { AppDispatch } from "@/store";
fetchCheckCardMeta,
fetchSelectHintMeta,
messageStore,
} from "@/stores";
import { CardZoneToChinese } from "./util";
import MsgSelectChain = ygopro.StocGameMessage.MsgSelectChain;
export default (selectChain: MsgSelectChain, dispatch: AppDispatch) => {
type MsgSelectChain = ygopro.StocGameMessage.MsgSelectChain;
export default (selectChain: MsgSelectChain) => {
const player = selectChain.player;
const spCount = selectChain.special_count;
const forced = selectChain.forced;
......@@ -66,32 +58,24 @@ export default (selectChain: MsgSelectChain, dispatch: AppDispatch) => {
case 3: {
// 处理强制发动的卡
dispatch(setCheckCardModalMinMax({ min: 1, max: 1 }));
dispatch(setCheckCardModalOnSubmit("sendSelectChainResponse"));
dispatch(setCheckCardMOdalCancelAble(!forced));
dispatch(setCheckCardModalCancelResponse(-1));
messageStore.checkCardModal.selectMin = 1;
messageStore.checkCardModal.selectMax = 1;
messageStore.checkCardModal.onSubmit = "sendSelectChainResponse";
messageStore.checkCardModal.cancelAble = !forced;
messageStore.checkCardModal.cancelResponse = -1;
for (const chain of chains) {
const tagName = CardZoneToChinese(chain.location.location);
dispatch(
fetchCheckCardMeta({
tagName,
option: {
code: chain.code,
location: chain.location,
response: chain.response,
effectDescCode: chain.effect_description,
},
})
);
fetchCheckCardMeta(chain.location.location, {
code: chain.code,
location: chain.location,
response: chain.response,
effectDescCode: chain.effect_description,
});
}
dispatch(
fetchSelectHintMeta({
selectHintData: 203,
})
);
dispatch(setCheckCardModalIsOpen(true));
fetchSelectHintMeta({
selectHintData: 203,
});
messageStore.checkCardModal.isOpen = true;
break;
}
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { setCheckCounter } from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import MsgSelectCounter = ygopro.StocGameMessage.MsgSelectCounter;
import { ygopro } from "@/api";
import { getCardByLocation, messageStore } from "@/stores";
type MsgSelectCounter = ygopro.StocGameMessage.MsgSelectCounter;
export default (selectCounter: MsgSelectCounter, dispatch: AppDispatch) => {
dispatch(setCheckCounter(selectCounter.toObject()));
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 }) => {
const id = getCardByLocation(location)?.occupant?.id;
const newCode = code ? code : id || 0;
return {
code: newCode,
max: counter_count!,
};
}
);
messageStore.checkCounterModal.isOpen = true;
};
import { CardMeta } from "@/api/cards";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { setYesNoModalIsOpen } from "@/reducers/duel/mod";
import { fetchYesNoMeta } from "@/reducers/duel/modal/mod";
import { AppDispatch } from "@/store";
import { fetchStrings, ygopro } from "@/api";
import { CardMeta, fetchCard } from "@/api/cards";
import { CardZoneToChinese, messageStore } from "@/stores";
import { CardZoneToChinese } from "./util";
import MsgSelectEffectYn = ygopro.StocGameMessage.MsgSelectEffectYn;
type MsgSelectEffectYn = ygopro.StocGameMessage.MsgSelectEffectYn;
export default (selectEffectYn: MsgSelectEffectYn, dispatch: AppDispatch) => {
// 这里改成了 async 不知道有没有影响
export default async (selectEffectYn: MsgSelectEffectYn) => {
const player = selectEffectYn.player;
const code = selectEffectYn.code;
const location = selectEffectYn.location;
......@@ -31,13 +29,18 @@ export default (selectEffectYn: MsgSelectEffectYn, dispatch: AppDispatch) => {
const desc1 = desc.replace(`[%ls]`, cardMeta.text.name || "[?]");
return desc1;
};
dispatch(
fetchYesNoMeta({
code,
location,
descCode: effect_description,
textGenerator,
})
);
dispatch(setYesNoModalIsOpen(true));
// dispatch(
// fetchYesNoMeta({
// code,
// location,
// descCode: effect_description,
// textGenerator,
// })
// );
// TODO: 国际化文案
const desc = fetchStrings("!system", effect_description);
const meta = await fetchCard(code);
messageStore.yesNoModal.msg = textGenerator(desc, meta, location);
messageStore.yesNoModal.isOpen = true;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { Interactivity, InteractType } from "@/reducers/duel/generic";
import { ygopro } from "@/api";
import {
addBanishedZoneIdleInteractivities,
addExtraDeckIdleInteractivities,
addGraveyardIdleInteractivities,
addHandsIdleInteractivity,
addMagicIdleInteractivities,
addMonsterIdleInteractivities,
clearAllIdleInteractivities,
setEnableBp,
setEnableEp,
} from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
clearAllIdleInteractivities as clearAllIdleInteractivities,
type Interactivity,
InteractType,
matStore,
} from "@/stores";
import MsgSelectIdleCmd = ygopro.StocGameMessage.MsgSelectIdleCmd;
import { ActionCreatorWithPayload } from "@reduxjs/toolkit";
export default (selectIdleCmd: MsgSelectIdleCmd, dispatch: AppDispatch) => {
export default (selectIdleCmd: MsgSelectIdleCmd) => {
const player = selectIdleCmd.player;
const cmds = selectIdleCmd.idle_cmds;
// 先清掉之前的互动性
dispatch(clearAllIdleInteractivities(player));
const dispatcher = (
idleData: MsgSelectIdleCmd.IdleCmd.IdleData,
interactType: InteractType | undefined,
actionCreator: ActionCreatorWithPayload<
{
player: number;
sequence: number;
interactivity: Interactivity<number>;
},
string
>
) => {
const cardInfo = idleData.card_info;
if (interactType === InteractType.ACTIVATE) {
// 发动效果会多一个字段
dispatch(
actionCreator({
player,
sequence: cardInfo.sequence,
interactivity: {
interactType,
activateIndex: idleData.effect_description,
response: idleData.response,
},
})
);
} else if (interactType) {
dispatch(
actionCreator({
player,
sequence: cardInfo.sequence,
interactivity: { interactType, response: idleData.response },
})
);
} else {
console.log(`InteractType undefined`);
}
};
clearAllIdleInteractivities(player);
cmds.forEach((cmd) => {
const interactType = idleTypeToInteractType(cmd.idle_type);
cmd.idle_datas.forEach((data) => {
const cardInfo = data.card_info;
switch (cardInfo.location) {
case ygopro.CardZone.HAND: {
dispatcher(data, interactType, addHandsIdleInteractivity);
break;
}
case ygopro.CardZone.MZONE: {
dispatcher(data, interactType, addMonsterIdleInteractivities);
const { location, sequence } = data.card_info;
break;
}
case ygopro.CardZone.SZONE: {
dispatcher(data, interactType, addMagicIdleInteractivities);
break;
}
case ygopro.CardZone.GRAVE: {
dispatcher(data, interactType, addGraveyardIdleInteractivities);
break;
}
case ygopro.CardZone.REMOVED: {
dispatcher(data, interactType, addBanishedZoneIdleInteractivities);
break;
}
case ygopro.CardZone.EXTRA: {
dispatcher(data, interactType, addExtraDeckIdleInteractivities);
break;
}
default: {
console.log(`Unhandled zone type: ${cardInfo.location}`);
}
// valtio: 代码从 ./selectBattleCmd.ts 复制过来的
if (interactType) {
const map: Partial<
Record<InteractType, undefined | Partial<Interactivity<number>>>
> = {
[InteractType.ACTIVATE]: { activateIndex: data.effect_description },
};
const tmp = map[interactType];
matStore
.in(location)
.of(player)
.addIdleInteractivity(sequence, {
...tmp,
interactType,
response: data.response,
});
} else {
console.warn(`Undefined InteractType`);
}
});
});
dispatch(setEnableBp(selectIdleCmd.enable_bp));
dispatch(setEnableEp(selectIdleCmd.enable_ep));
matStore.phase.enableBp = selectIdleCmd.enable_bp;
matStore.phase.enableEp = selectIdleCmd.enable_ep;
};
function idleTypeToInteractType(
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { setOptionModalIsOpen } from "@/reducers/duel/mod";
import { fetchOptionMeta } from "@/reducers/duel/modal/mod";
import { AppDispatch } from "@/store";
import { fetchCard, getCardStr, ygopro } from "@/api";
import MsgSelectOption = ygopro.StocGameMessage.MsgSelectOption;
import { messageStore } from "@/stores";
export default (selectOption: MsgSelectOption, dispatch: AppDispatch) => {
export default async (selectOption: MsgSelectOption) => {
const player = selectOption.player;
const options = selectOption.options;
for (let option of options) {
dispatch(fetchOptionMeta(option));
}
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);
})
);
dispatch(setOptionModalIsOpen(true));
messageStore.optionModal.isOpen = true;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { AppDispatch } from "@/store";
import MsgSelectPlace = ygopro.StocGameMessage.MsgSelectPlace;
import {
addMagicPlaceInteractivities,
addMonsterPlaceInteractivities,
} from "@/reducers/duel/mod";
import { ygopro } from "@/api";
import { InteractType, matStore } from "@/stores";
export default (selectPlace: MsgSelectPlace, dispatch: AppDispatch) => {
type MsgSelectPlace = ygopro.StocGameMessage.MsgSelectPlace;
export default (selectPlace: MsgSelectPlace) => {
if (selectPlace.count != 1) {
console.warn(`Unhandled case: ${selectPlace}`);
return;
}
for (const place of selectPlace.places) {
switch (place.zone) {
case ygopro.CardZone.MZONE: {
dispatch(
addMonsterPlaceInteractivities([place.controler, place.sequence])
);
matStore.monsters
.of(place.controler)
.setPlaceInteractivityType(
place.sequence,
InteractType.PLACE_SELECTABLE
);
break;
}
case ygopro.CardZone.SZONE: {
dispatch(
addMagicPlaceInteractivities([place.controler, place.sequence])
);
matStore.magics
.of(place.controler)
.setPlaceInteractivityType(
place.sequence,
InteractType.PLACE_SELECTABLE
);
break;
}
default: {
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import {
setPositionModalIsOpen,
setPositionModalPositions,
} from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import MsgSelectPosition = ygopro.StocGameMessage.MsgSelectPosition;
import { ygopro } from "@/api";
import { messageStore } from "@/stores";
export default (selectPosition: MsgSelectPosition, dispatch: AppDispatch) => {
type MsgSelectPosition = ygopro.StocGameMessage.MsgSelectPosition;
export default (selectPosition: MsgSelectPosition) => {
const player = selectPosition.player;
const positions = selectPosition.positions;
dispatch(
setPositionModalPositions(positions.map((position) => position.position))
messageStore.positionModal.positions = positions.map(
(position) => position.position
);
dispatch(setPositionModalIsOpen(true));
messageStore.positionModal.isOpen = true;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import {
setCheckCardModalV3AllLevel,
setCheckCardModalV3IsOpen,
setCheckCardModalV3MinMax,
setCheckCardModalV3OverFlow,
} from "@/reducers/duel/mod";
import { fetchCheckCardMetasV3 } from "@/reducers/duel/modal/checkCardModalV3Slice";
import { AppDispatch } from "@/store";
import MsgSelectSum = ygopro.StocGameMessage.MsgSelectSum;
import { ygopro } from "@/api";
import { fetchCheckCardMetasV3, messageStore } from "@/stores";
type MsgSelectSum = ygopro.StocGameMessage.MsgSelectSum;
export default (selectSum: MsgSelectSum, dispatch: AppDispatch) => {
dispatch(setCheckCardModalV3OverFlow(selectSum.overflow != 0));
dispatch(setCheckCardModalV3AllLevel(selectSum.level_sum));
dispatch(
setCheckCardModalV3MinMax({ min: selectSum.min, max: selectSum.max })
);
dispatch(
fetchCheckCardMetasV3({
mustSelect: true,
options: selectSum.must_select_cards,
})
);
dispatch(
fetchCheckCardMetasV3({
mustSelect: false,
options: selectSum.selectable_cards,
})
);
dispatch(setCheckCardModalV3IsOpen(true));
export default (selectSum: MsgSelectSum) => {
messageStore.checkCardModalV3.overflow = selectSum.overflow != 0;
messageStore.checkCardModalV3.allLevel = selectSum.level_sum;
messageStore.checkCardModalV3.selectMin = selectSum.min;
messageStore.checkCardModalV3.selectMax = selectSum.max;
fetchCheckCardMetasV3({
mustSelect: true,
options: selectSum.must_select_cards,
});
fetchCheckCardMetasV3({
mustSelect: false,
options: selectSum.selectable_cards,
});
messageStore.checkCardModalV3.isOpen = true;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import {
setCheckCardModalV3AllLevel,
setCheckCardModalV3IsOpen,
setCheckCardModalV3MinMax,
setCheckCardModalV3OverFlow,
} from "@/reducers/duel/mod";
import { fetchCheckCardMetasV3 } from "@/reducers/duel/modal/checkCardModalV3Slice";
import { AppDispatch } from "@/store";
import MsgSelectTribute = ygopro.StocGameMessage.MsgSelectTribute;
import { ygopro } from "@/api";
import { fetchCheckCardMetasV3, messageStore } from "@/stores";
export default (selectTribute: MsgSelectTribute, dispatch: AppDispatch) => {
type MsgSelectTribute = ygopro.StocGameMessage.MsgSelectTribute;
export default (selectTribute: MsgSelectTribute) => {
// TODO: 当玩家选择卡数大于`max`时,是否也合法?
dispatch(setCheckCardModalV3OverFlow(true));
dispatch(setCheckCardModalV3AllLevel(0));
dispatch(
setCheckCardModalV3MinMax({
min: selectTribute.min,
max: selectTribute.max,
})
);
dispatch(
fetchCheckCardMetasV3({
mustSelect: false,
options: selectTribute.selectable_cards.map((card) => {
return {
code: card.code,
location: card.location,
level1: card.level,
level2: card.level,
response: card.response,
};
}),
})
);
dispatch(setCheckCardModalV3IsOpen(true));
messageStore.checkCardModalV3.overflow = true;
messageStore.checkCardModalV3.allLevel = 0;
messageStore.checkCardModalV3.selectMin = selectTribute.min;
messageStore.checkCardModalV3.selectMax = selectTribute.max;
fetchCheckCardMetasV3({
mustSelect: false,
options: selectTribute.selectable_cards.map((card) => {
return {
code: card.code,
location: card.location,
level1: card.level,
level2: card.level,
response: card.response,
};
}),
});
messageStore.checkCardModalV3.isOpen = true;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import {
setCheckCardModalV2CancelAble,
setCheckCardModalV2FinishAble,
setCheckCardModalV2IsOpen,
setCheckCardModalV2MinMax,
setCheckCardModalV2ResponseAble,
} from "@/reducers/duel/mod";
import { fetchCheckCardMetasV2 } from "@/reducers/duel/modal/checkCardModalV2Slice";
import { AppDispatch } from "@/store";
import MsgSelectUnselectCard = ygopro.StocGameMessage.MsgSelectUnselectCard;
import { ygopro } from "@/api";
import { fetchCheckCardMetasV2, messageStore } from "@/stores";
export default (
selectUnselectCard: MsgSelectUnselectCard,
dispatch: AppDispatch
) => {
const finishable = selectUnselectCard.finishable;
const cancelable = selectUnselectCard.cancelable;
const min = selectUnselectCard.min;
const max = selectUnselectCard.max;
const selectableCards = selectUnselectCard.selectable_cards;
const selectedCards = selectUnselectCard.selected_cards;
type MsgSelectUnselectCard = ygopro.StocGameMessage.MsgSelectUnselectCard;
dispatch(setCheckCardModalV2IsOpen(true));
dispatch(setCheckCardModalV2FinishAble(finishable));
dispatch(setCheckCardModalV2CancelAble(cancelable));
dispatch(setCheckCardModalV2MinMax({ min, max }));
export default ({
finishable,
cancelable,
min,
max,
selectable_cards: selectableCards,
selected_cards: selectedCards,
}: MsgSelectUnselectCard) => {
messageStore.checkCardModalV2.isOpen = true;
messageStore.checkCardModalV2.finishAble = finishable;
messageStore.checkCardModalV2.cancelAble = cancelable;
messageStore.checkCardModalV2.selectMin = min;
messageStore.checkCardModalV2.selectMax = max;
dispatch(
fetchCheckCardMetasV2({
selected: false,
options: selectableCards.map((card) => {
return {
code: card.code,
location: card.location,
response: card.response,
};
}),
})
);
fetchCheckCardMetasV2({
selected: false,
options: selectableCards.map((card) => {
return {
code: card.code,
location: card.location,
response: card.response,
};
}),
});
dispatch(
fetchCheckCardMetasV2({
selected: true,
options: selectedCards.map((card) => {
return {
code: card.code,
location: card.location,
response: card.response,
};
}),
})
);
fetchCheckCardMetasV2({
selected: true,
options: selectedCards.map((card) => {
return {
code: card.code,
location: card.location,
response: card.response,
};
}),
});
dispatch(setCheckCardModalV2ResponseAble(true));
messageStore.checkCardModalV2.responseable = true;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { setYesNoModalIsOpen } from "@/reducers/duel/mod";
import { fetchYesNoMetaWithEffecDesc } from "@/reducers/duel/modal/yesNoModalSlice";
import { AppDispatch } from "@/store";
import MsgSelectYesNo = ygopro.StocGameMessage.MsgSelectYesNo;
import { getStrings, ygopro } from "@/api";
import { messageStore } from "@/stores";
export default (selectYesNo: MsgSelectYesNo, dispatch: AppDispatch) => {
type MsgSelectYesNo = ygopro.StocGameMessage.MsgSelectYesNo;
export default async (selectYesNo: MsgSelectYesNo) => {
const player = selectYesNo.player;
const effect_description = selectYesNo.effect_description;
dispatch(fetchYesNoMetaWithEffecDesc(effect_description));
dispatch(setYesNoModalIsOpen(true));
messageStore.yesNoModal.msg = await getStrings(effect_description);
messageStore.yesNoModal.isOpen = true;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores";
export default (_set: ygopro.StocGameMessage.MsgSet, dispatch: AppDispatch) => {
dispatch(fetchEsHintMeta({ originMsg: 1601 }));
export default (_set: ygopro.StocGameMessage.MsgSet) => {
fetchEsHintMeta({ originMsg: 1601 });
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { updateHandsMeta } from "@/reducers/duel/handsSlice";
import { AppDispatch } from "@/store";
import MsgShuffleHand = ygopro.StocGameMessage.MsgShuffleHand;
export default (shuffleHand: MsgShuffleHand, dispatch: AppDispatch) => {
dispatch(
updateHandsMeta({ controler: shuffleHand.player, codes: shuffleHand.hands })
);
import { ygopro } from "@/api";
import { matStore } from "@/stores";
type MsgShuffleHand = ygopro.StocGameMessage.MsgShuffleHand;
export default (shuffleHand: MsgShuffleHand) => {
const { hands: codes, player: controller } = shuffleHand;
const metas = codes.map((code) => {
return {
occupant: { id: code, data: {}, text: {} },
location: {
controler: controller,
location: ygopro.CardZone.HAND,
},
idleInteractivities: [],
counters: {},
};
});
matStore.hands.of(controller).length = 0;
matStore.hands.of(controller).push(...metas);
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { setSortCardModalIsOpen } from "@/reducers/duel/mod";
import { fetchSortCardMeta } from "@/reducers/duel/modal/sortCardModalSlice";
import { AppDispatch } from "@/store";
import MsgSortCard = ygopro.StocGameMessage.MsgSortCard;
import { fetchCard, ygopro } from "@/api";
import { messageStore } from "@/stores";
export default (sortCard: MsgSortCard, dispatch: AppDispatch) => {
for (const option of sortCard.options) {
dispatch(fetchSortCardMeta(option.toObject()));
}
dispatch(setSortCardModalIsOpen(true));
type MsgSortCard = ygopro.StocGameMessage.MsgSortCard;
export default async (sortCard: MsgSortCard) => {
await Promise.all(
sortCard.options.map(async ({ code, response }) => {
const meta = await fetchCard(code!, true);
messageStore.sortCardModal.options.push({
meta,
response: response!,
});
})
);
messageStore.sortCardModal.isOpen = true;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores";
export default (
_: ygopro.StocGameMessage.MsgSpSummoned,
dispatch: AppDispatch
) => {
dispatch(fetchEsHintMeta({ originMsg: 1606 }));
export default (_: ygopro.StocGameMessage.MsgSpSummoned) => {
fetchEsHintMeta({ originMsg: 1606 });
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
export default (
spSummoning: ygopro.StocGameMessage.MsgSpSummoning,
dispatch: AppDispatch
) => {
dispatch(
fetchEsHintMeta({
originMsg: "「[?]」特殊召唤宣言时",
cardID: spSummoning.code,
})
);
import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores";
export default (spSummoning: ygopro.StocGameMessage.MsgSpSummoning) => {
fetchEsHintMeta({
originMsg: "「[?]」特殊召唤宣言时",
cardID: spSummoning.code,
});
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import {
infoInit,
initBanishedZone,
initDeck,
initGraveyard,
initHint,
initMagics,
initMonsters,
setSelfType,
} from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { store } from "@/stores";
export default (
start: ygopro.StocGameMessage.MsgStart,
dispatch: AppDispatch
) => {
dispatch(setSelfType(start.playerType));
dispatch(
infoInit([
0,
{
life: start.life1,
deckSize: start.deckSize1,
extraSize: start.extraSize1,
},
])
);
dispatch(
infoInit([
1,
{
life: start.life2,
deckSize: start.deckSize2,
extraSize: start.extraSize2,
},
])
);
dispatch(initMonsters(0));
dispatch(initMonsters(1));
dispatch(initMagics(0));
dispatch(initMagics(1));
dispatch(initGraveyard(0));
dispatch(initGraveyard(1));
dispatch(initDeck({ player: 0, deskSize: start.deckSize1 }));
dispatch(initDeck({ player: 1, deskSize: start.deckSize2 }));
dispatch(initBanishedZone(0));
dispatch(initBanishedZone(1));
dispatch(initHint());
const { matStore } = store;
export default (start: ygopro.StocGameMessage.MsgStart) => {
matStore.selfType = start.playerType;
matStore.initInfo.set(0, {
life: start.life1,
deckSize: start.deckSize1,
extraSize: start.extraSize1,
});
matStore.initInfo.set(1, {
life: start.life2,
deckSize: start.deckSize2,
extraSize: start.extraSize2,
});
matStore.monsters.of(0).forEach((x) => (x.location.controler = 0));
matStore.monsters.of(1).forEach((x) => (x.location.controler = 1));
matStore.magics.of(0).forEach((x) => (x.location.controler = 0));
matStore.magics.of(1).forEach((x) => (x.location.controler = 1));
matStore.decks.of(0).add(Array(start.deckSize1).fill(0));
matStore.decks.of(1).add(Array(start.deckSize2).fill(0));
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores";
export default (
_: ygopro.StocGameMessage.MsgSummoned,
dispatch: AppDispatch
) => {
dispatch(fetchEsHintMeta({ originMsg: 1604 }));
export default (_: ygopro.StocGameMessage.MsgSummoned) => {
fetchEsHintMeta({ originMsg: 1604 });
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores";
export default (
summoning: ygopro.StocGameMessage.MsgSummoning,
dispatch: AppDispatch
) => {
dispatch(
fetchEsHintMeta({
originMsg: "「[?]」通常召唤宣言时",
cardID: summoning.code,
})
);
export default (summoning: ygopro.StocGameMessage.MsgSummoning) => {
fetchEsHintMeta({
originMsg: "「[?]」通常召唤宣言时",
cardID: summoning.code,
});
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { fetchEsHintMeta } from "@/stores";
export default (
_swap: ygopro.StocGameMessage.MsgSwap,
dispatch: AppDispatch
) => {
dispatch(fetchEsHintMeta({ originMsg: 1602 }));
export default (_swap: ygopro.StocGameMessage.MsgSwap) => {
fetchEsHintMeta({ originMsg: 1602 });
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { sendTimeConfirm } from "@/api/ocgcore/ocgHelper";
import { updateTimeLimit } from "@/reducers/duel/mod";
import { store } from "@/store";
import { sendTimeConfirm, ygopro } from "@/api";
import { matStore } from "@/stores";
export default function handleTimeLimit(timeLimit: ygopro.StocTimeLimit) {
const dispatch = store.dispatch;
dispatch(updateTimeLimit([timeLimit.player, timeLimit.left_time]));
matStore.timeLimits.set(timeLimit.player, timeLimit.left_time);
sendTimeConfirm();
}
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { ygopro } from "@/api";
import { useConfig } from "@/config";
import { setUnimplemented } from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import { matStore } from "@/stores";
const NeosConfig = useConfig();
export default (
unimplemented: ygopro.StocGameMessage.MsgUnimplemented,
dispatch: AppDispatch
) => {
export default (unimplemented: ygopro.StocGameMessage.MsgUnimplemented) => {
if (!NeosConfig.unimplementedWhiteList.includes(unimplemented.command)) {
dispatch(setUnimplemented(unimplemented.command));
matStore.unimplemented = unimplemented.command;
}
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { updateMonsterCounters } from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import MsgUpdateCounter = ygopro.StocGameMessage.MsgUpdateCounter;
import { ygopro } from "@/api";
import { getCardByLocation } from "@/stores";
export default (updateCounter: MsgUpdateCounter, dispatch: AppDispatch) => {
dispatch(updateMonsterCounters(updateCounter.toObject()));
type MsgUpdateCounter = ygopro.StocGameMessage.MsgUpdateCounter;
export default (updateCounter: MsgUpdateCounter) => {
const { location, count, action_type: counterType } = updateCounter;
const target = getCardByLocation(location); // 不太确定这个后面能不能相应,我不好说
if (target) {
switch (counterType) {
case ygopro.StocGameMessage.MsgUpdateCounter.ActionType.ADD: {
if (counterType in target.counters) {
target.counters[counterType] += count;
} else {
target.counters[counterType] = count;
}
break;
}
case ygopro.StocGameMessage.MsgUpdateCounter.ActionType.REMOVE: {
if (counterType in target.counters) {
target.counters[counterType] -= count;
}
break;
}
default: {
break;
}
}
}
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { updateFieldData } from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import MsgUpdateData = ygopro.StocGameMessage.MsgUpdateData;
export default (updateData: MsgUpdateData, dispatch: AppDispatch) => {
dispatch(updateFieldData(updateData.toObject()));
import { matStore } from "@/stores";
export default (updateData: MsgUpdateData) => {
const { player: controller, zone, actions } = updateData;
if (controller !== undefined && zone !== undefined && actions !== undefined) {
const field = matStore.in(zone).of(controller);
actions.forEach((action) => {
const sequence = action.location?.sequence;
if (typeof sequence !== "undefined") {
const target = field[sequence];
if (target && (target.occupant || target.reload)) {
if (target.occupant === undefined) {
target.occupant = { id: action.code!, data: {}, text: {} };
}
const occupant = target.occupant;
// 目前只更新以下字段
if (action.code !== undefined && action.code >= 0) {
occupant.id = action.code;
occupant.text.id = action.code;
}
if (action.location !== undefined) {
target.location.position = action.location.position;
}
if (action.type_ !== undefined && action.type_ >= 0) {
occupant.data.type = action.type_;
}
if (action.level !== undefined && action.level >= 0) {
occupant.data.level = action.level;
}
if (action.attribute !== undefined && action.attribute >= 0) {
occupant.data.attribute = action.attribute;
}
if (action.race !== undefined && action.race >= 0) {
occupant.data.race = action.race;
}
if (action.attack !== undefined && action.attack >= 0) {
occupant.data.atk = action.attack;
}
if (action.defense !== undefined && action.defense >= 0) {
occupant.data.def = action.defense;
}
// TODO: counters
}
if (target?.reload) {
target.reload = false;
}
}
});
}
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { fetchEsHintMeta } from "@/reducers/duel/hintSlice";
import { updateHp } from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { fetchEsHintMeta, matStore } from "@/stores";
import MsgUpdateHp = ygopro.StocGameMessage.MsgUpdateHp;
export default (msgUpdateHp: MsgUpdateHp, dispatch: AppDispatch) => {
export default (msgUpdateHp: MsgUpdateHp) => {
if (msgUpdateHp.type_ == MsgUpdateHp.ActionType.DAMAGE) {
dispatch(fetchEsHintMeta({ originMsg: "玩家收到伤害时" })); // TODO: i18n
fetchEsHintMeta({ originMsg: "玩家收到伤害时" }); // TODO: i18n
matStore.initInfo.of(msgUpdateHp.player).life -= msgUpdateHp.value;
} else if (msgUpdateHp.type_ == MsgUpdateHp.ActionType.RECOVER) {
dispatch(fetchEsHintMeta({ originMsg: "玩家生命值回复时" })); // TODO: i18n
fetchEsHintMeta({ originMsg: "玩家生命值回复时" }); // TODO: i18n
matStore.initInfo.of(msgUpdateHp.player).life += msgUpdateHp.value;
}
dispatch(updateHp(msgUpdateHp));
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { clearAllIdleInteractivities, setWaiting } from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import {
clearAllIdleInteractivities as clearAllIdleInteractivities,
matStore,
} from "@/stores";
export default (
_wait: ygopro.StocGameMessage.MsgWait,
dispatch: AppDispatch
) => {
dispatch(clearAllIdleInteractivities(0));
dispatch(clearAllIdleInteractivities(1));
dispatch(setWaiting(true));
export default (_wait: ygopro.StocGameMessage.MsgWait) => {
clearAllIdleInteractivities(0);
clearAllIdleInteractivities(1);
matStore.waiting = true;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { setResult } from "@/reducers/duel/mod";
import { AppDispatch } from "@/store";
import { ygopro } from "@/api";
import { matStore } from "@/stores";
export default (win: ygopro.StocGameMessage.MsgWin, dispatch: AppDispatch) => {
dispatch(setResult(win.type_));
export default (win: ygopro.StocGameMessage.MsgWin) => {
matStore.result = win.type_;
};
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { player0DeckInfo, player1DeckInfo } from "@/reducers/playerSlice";
import { store } from "@/store";
import { playerStore } from "@/valtioStores";
import { ygopro } from "@/api";
import { playerStore } from "@/stores";
// FIXME: player0 不一定是当前玩家
export default function handleDeckCount(pb: ygopro.YgoStocMsg) {
const dispath = store.dispatch;
const deckCount = pb.stoc_deck_count;
dispath(
player0DeckInfo({
mainCnt: deckCount.meMain,
extraCnt: deckCount.meExtra,
sideCnt: deckCount.meSide,
})
);
playerStore.player0.deckInfo = {
mainCnt: deckCount.meMain,
extraCnt: deckCount.meExtra,
sideCnt: deckCount.meSide,
};
dispath(
player1DeckInfo({
mainCnt: deckCount.opMain,
extraCnt: deckCount.opExtra,
sideCnt: deckCount.opSide,
})
);
playerStore.player1.deckInfo = {
mainCnt: deckCount.opMain,
extraCnt: deckCount.opExtra,
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { selectHandAble } from "@/reducers/moraSlice";
import { store } from "@/store";
import { moraStore } from "@/valtioStores";
import { ygopro } from "@/api";
import { moraStore } from "@/stores";
export default function handleSelectHand(_: ygopro.YgoStocMsg) {
const dispatch = store.dispatch;
dispatch(selectHandAble());
moraStore.selectHandAble = true;
}
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { selectTpAble } from "@/reducers/moraSlice";
import { store } from "@/store";
import { moraStore } from "@/valtioStores";
import { ygopro } from "@/api";
import { moraStore } from "@/stores";
export default function handleSelectTp(_: ygopro.YgoStocMsg) {
const dispatch = store.dispatch;
dispatch(selectTpAble());
moraStore.selectTpAble = true;
}
......@@ -2,7 +2,7 @@
* 长连接建立事件订阅处理逻辑
*
* */
import { sendJoinGame, sendPlayerInfo } from "@/api/ocgcore/ocgHelper";
import { sendJoinGame, sendPlayerInfo } from "@/api";
import { useConfig } from "@/config";
const NeosConfig = useConfig();
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { postChat } from "@/reducers/chatSlice";
import { store } from "@/store";
import { chatStore } from "@/valtioStores";
import { ygopro } from "@/api";
import { chatStore } from "@/stores";
export default function handleChat(pb: ygopro.YgoStocMsg) {
const dispatch = store.dispatch;
const chat = pb.stoc_chat;
dispatch(postChat(chat.msg));
chatStore.message = chat.msg;
}
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { duelStart } from "@/reducers/moraSlice";
import { store } from "@/store";
import { moraStore } from "@/valtioStores";
import { ygopro } from "@/api";
import { moraStore } from "@/stores";
export default function handleDuelStart(_pb: ygopro.YgoStocMsg) {
const dispatch = store.dispatch;
dispatch(duelStart());
moraStore.duelStart = true;
}
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import {
observerIncrement,
player0Leave,
player0Update,
player1Leave,
player1Update,
} from "@/reducers/playerSlice";
import { store } from "@/store";
import { playerStore } from "@/valtioStores";
import { ygopro } from "@/api";
import { playerStore } from "@/stores";
const READY_STATE = "ready";
const NO_READY_STATE = "not ready";
export default function handleHsPlayerChange(pb: ygopro.YgoStocMsg) {
const dispatch = store.dispatch;
const change = pb.stoc_hs_player_change;
if (change.pos > 1) {
......@@ -35,42 +26,25 @@ export default function handleHsPlayerChange(pb: ygopro.YgoStocMsg) {
// todo
// if (src === 0 && dst === 1) {
// setPlayer1(player0);
// setPlayer0({});
// } else if (src === 1 && dst === 0) {
// setPlayer0(player1);
// setPlayer1({});
// }
break;
}
case ygopro.StocHsPlayerChange.State.READY: {
change.pos == 0
? dispatch(player0Update(READY_STATE))
: dispatch(player1Update(READY_STATE));
playerStore[change.pos == 0 ? "player0" : "player1"].state =
READY_STATE;
break;
}
case ygopro.StocHsPlayerChange.State.NO_READY: {
change.pos == 0
? dispatch(player0Update(NO_READY_STATE))
: dispatch(player1Update(NO_READY_STATE));
playerStore[change.pos == 0 ? "player0" : "player1"].state =
NO_READY_STATE;
break;
}
case ygopro.StocHsPlayerChange.State.LEAVE: {
change.pos == 0 ? dispatch(player0Leave) : dispatch(player1Leave);
playerStore[change.pos == 0 ? "player0" : "player1"] = {};
break;
}
case ygopro.StocHsPlayerChange.State.TO_OBSERVER: {
change.pos == 0 ? dispatch(player0Leave) : dispatch(player1Leave);
dispatch(observerIncrement());
playerStore[change.pos == 0 ? "player0" : "player1"] = {}; // todo: 有没有必要?
playerStore.observerCount += 1;
break;
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { player0Enter, player1Enter } from "@/reducers/playerSlice";
import { store } from "@/store";
import { playerStore } from "@/valtioStores";
import { ygopro } from "@/api";
import { playerStore } from "@/stores";
export default function handleHsPlayerEnter(pb: ygopro.YgoStocMsg) {
const dispatch = store.dispatch;
const name = pb.stoc_hs_player_enter.name;
const pos = pb.stoc_hs_player_enter.pos;
if (pos > 1) {
console.log("Currently only supported 2v2 mode.");
} else {
pos == 0 ? dispatch(player0Enter(name)) : dispatch(player1Enter(name));
playerStore[pos == 0 ? "player0" : "player1"].name = name;
}
}
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { observerChange } from "@/reducers/playerSlice";
import { store } from "@/store";
import { playerStore } from "@/valtioStores";
import { ygopro } from "@/api";
import { playerStore } from "@/stores";
export default function handleHsWatchChange(pb: ygopro.YgoStocMsg) {
const dispatch = store.dispatch;
const count = pb.stoc_hs_watch_change.count;
dispatch(observerChange(count));
playerStore.observerCount = count;
}
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { setJoined } from "@/reducers/joinSlice";
import { store } from "@/store";
import { joinStore } from "@/valtioStores";
import { ygopro } from "@/api";
import { joinStore } from "@/stores";
export default function handleJoinGame(pb: ygopro.YgoStocMsg) {
const dispatch = store.dispatch;
const msg = pb.stoc_join_game;
// todo
dispatch(setJoined());
joinStore.value = true;
}
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import {
hostChange,
player0Update,
player1Update,
updateIsHost,
} from "@/reducers/playerSlice";
import { store } from "@/store";
import { playerStore } from "@/valtioStores";
import { ygopro } from "@/api";
import { playerStore } from "@/stores";
const NO_READY_STATE = "not ready";
export default function handleTypeChange(pb: ygopro.YgoStocMsg) {
const dispatch = store.dispatch;
const selfType = pb.stoc_type_change.self_type;
const assertHost = pb.stoc_type_change.is_host;
dispatch(updateIsHost(assertHost));
playerStore.isHost = assertHost;
if (assertHost) {
switch (selfType) {
case ygopro.StocTypeChange.SelfType.PLAYER1: {
dispatch(hostChange(0));
dispatch(player0Update(NO_READY_STATE));
playerStore.player0.isHost = true;
playerStore.player1.isHost = false;
playerStore.player0.state = NO_READY_STATE;
break;
}
case ygopro.StocTypeChange.SelfType.PLAYER2: {
dispatch(hostChange(0));
dispatch(player1Update(NO_READY_STATE));
playerStore.player0.isHost = false;
playerStore.player1.isHost = true;
playerStore.player1.state = NO_READY_STATE;
break;
}
default: {
......
/*
* 全局状态存储模块
* */
import { configureStore, Unsubscribe } from "@reduxjs/toolkit";
import chatReducer from "./reducers/chatSlice";
import duelReducer from "./reducers/duel/mod";
import joinedReducer from "./reducers/joinSlice";
import moraReducer from "./reducers/moraSlice";
import playerReducer from "./reducers/playerSlice";
export const store = configureStore({
reducer: {
join: joinedReducer,
chat: chatReducer,
player: playerReducer,
mora: moraReducer,
duel: duelReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ["duel/updateHp", "duel/reloadField"],
},
}),
});
// Ref: https://github.com/reduxjs/redux/issues/303
export function observeStore<T>(
select: (state: RootState) => T,
onChange: (prev: T | null, cur: T) => void
): Unsubscribe {
let currentState: T | null = null;
const changeHook = () => {
const nextState = select(store.getState());
if (nextState !== currentState) {
onChange(currentState, nextState);
currentState = nextState;
}
};
const unsubscribe = store.subscribe(changeHook);
changeHook();
return unsubscribe;
}
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export * from "./chatStore";
export * from "./joinStore";
export * from "./matStore";
export * from "./messageStore";
export * from "./moraStore";
export * from "./playerStore";
import { createContext, type ReactNode, useRef } from "react";
import { proxy } from "valtio";
import { devtools } from "valtio/utils";
import { chatStore } from "./chatStore";
import { joinStore } from "./joinStore";
import { matStore } from "./matStore";
import { messageStore } from "./messageStore";
import { moraStore } from "./moraStore";
import { playerStore } from "./playerStore";
export const valtioStore = proxy({
export const store = proxy({
playerStore,
chatStore,
joinStore,
moraStore,
matStore, // 决斗盘
messageStore, // 决斗的信息,包括模态框
});
devtools(valtioStore, { name: "valtio store", enabled: true });
/**
* 在组件之中使用valtio store
*/
export const valtioContext = createContext<typeof valtioStore>({} as any);
/**
* 包裹根节点,使得所有子组件都可以使用valtio store
*/
export const ValtioProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const state = useRef(valtioStore).current;
return (
<valtioContext.Provider value={state}>{children}</valtioContext.Provider>
);
};
devtools(store, { name: "valtio store", enabled: true });
export * from "./methods";
export * from "./store";
export * from "./types";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { ygopro } from "@/api";
export function CardZoneToChinese(zone: ygopro.CardZone): string {
switch (zone) {
......
import { ygopro } from "@/api";
import { fetchCard, getCardStr } from "@/api/cards";
import { matStore, messageStore } from "@/stores";
import { CardZoneToChinese } from "./cardZoneToChinese";
type Location =
| ygopro.CardLocation
| ReturnType<typeof ygopro.CardLocation.prototype.toObject>;
function cmpCardLocation(
left: Location,
right?: Location,
strict?: boolean
): boolean {
if (strict) {
return JSON.stringify(left) === JSON.stringify(right);
} else {
return (
left.controler === right?.controler &&
left.location === right?.location &&
left.sequence === right?.sequence
);
}
}
/**
* 这段代码定义了一个异步函数 fetchCheckCardMeta,它的作用是获取一张卡片的元数据并将其添加到某个名为 messageStore.checkCardModal 的对象上。
该函数的第一个参数是一个枚举值 ygopro.CardZone,表示卡片所在的区域。其余参数是一个包含卡片编号、位置、响应码和效果描述代码等信息的对象。
首先,这个函数会根据区域类型调用 CardZoneToChinese() 函数生成一个中文名称。然后,它会调用 fetchCard() 异步函数来获取指定卡片的元数据 meta。
接下来,函数会根据传递进来的 location 对象获取卡片所属的控制者,并根据控制者判断这张卡片是我方的还是对方的。然后,它会根据卡片的位置信息获取卡片的实际 ID,并构造一个新的选项 newOption。
接着,函数会遍历已有的 messageStore.checkCardModal.tags,查找是否存在名为 combinedTagName 的标签。如果找到了,则将新选项 newOption 加入该标签的选项列表中并立即返回。如果找不到,则创建一个新标签,并将新选项 newOption 添加到其中。
最后,函数会再次遍历所有标签,查找是否存在名为 combinedTagName 的标签。如果找到了,则遍历该标签中的所有选项,并查找是否存在与 location 对象中指定的卡片位置信息完全相同的选项。如果找到了,则更新该选项的元数据和效果描述等信息。
*/
export const fetchCheckCardMeta = async (
zone: ygopro.CardZone,
{
code,
location,
response,
effectDescCode,
}: {
code: number;
location: ygopro.CardLocation;
response: number;
effectDescCode?: number;
}
) => {
const tagName = CardZoneToChinese(zone);
const meta = await fetchCard(code);
const controller = location.controler;
const combinedTagName = matStore.isMe(controller)
? `我方的${tagName}`
: `对方的${tagName}`;
const newID =
code != 0
? code
: matStore.in(location.location).of(controller)[location.sequence]
?.occupant?.id || 0;
const newOption = {
meta: { id: newID, data: {}, text: {} },
location: location.toObject(),
effectDescCode,
response,
};
for (const tag of messageStore.checkCardModal.tags) {
if (tag.tagName === combinedTagName) {
tag.options.push(newOption);
return;
}
}
messageStore.checkCardModal.tags.push({
tagName: combinedTagName,
options: [newOption],
});
for (const tag of messageStore.checkCardModal.tags) {
if (tag.tagName === combinedTagName) {
for (const old of tag.options) {
if (meta.id == old.meta.id && cmpCardLocation(location, old.location)) {
const cardID = old.meta.id;
old.meta = meta;
old.meta.id = cardID;
const effectDescCode = old.effectDescCode;
const effectDesc = effectDescCode
? getCardStr(old.meta, effectDescCode & 0xf)
: undefined;
old.effectDesc = effectDesc;
}
}
}
}
};
import type { ygopro } from "@/api";
import { DESCRIPTION_LIMIT, fetchStrings, getStrings } from "@/api";
import { fetchCard } from "@/api/cards";
import { matStore } from "../store";
const { hint } = matStore;
export const fetchCommonHintMeta = (code: number) => {
hint.code = code;
hint.msg = fetchStrings("!system", code);
};
export const fetchSelectHintMeta = async ({
selectHintData,
esHint,
}: {
selectHintData: number;
esHint?: string;
}) => {
let selectHintMeta = "";
if (selectHintData > DESCRIPTION_LIMIT) {
// 针对`MSG_SELECT_PLACE`的特化逻辑
const cardMeta = await fetchCard(selectHintData, true);
selectHintMeta = fetchStrings("!system", 569).replace(
"[%ls]",
cardMeta.text.name || "[?]"
);
} else {
selectHintMeta = await getStrings(selectHintData);
}
hint.code = selectHintData;
if (hint.code > DESCRIPTION_LIMIT) {
// 针对`MSG_SELECT_PLACE`的特化逻辑
hint.msg = selectHintMeta;
} else {
hint.esSelectHint = selectHintMeta;
hint.esHint = esHint;
}
};
export const fetchEsHintMeta = async ({
originMsg,
location,
cardID,
}: {
originMsg: string | number;
location?: ygopro.CardLocation;
cardID?: number;
}) => {
const newOriginMsg =
typeof originMsg === "string"
? originMsg
: fetchStrings("!system", originMsg);
const cardMeta = cardID ? await fetchCard(cardID) : undefined;
let esHint = newOriginMsg;
if (cardMeta?.text.name) {
esHint = esHint.replace("[?]", cardMeta.text.name);
}
if (location) {
const fieldMeta = matStore
.in(location.location)
.of(location.controler)
.at(location.sequence);
if (fieldMeta?.occupant?.text.name) {
esHint = esHint.replace("[?]", fieldMeta.occupant.text.name);
}
}
hint.esHint = esHint;
};
import { fetchCard } from "@/api";
import { matStore } from "@/stores";
export const fetchOverlayMeta = async (
controller: number,
sequence: number,
overlayCodes: number[],
append?: boolean
) => {
const metas = await Promise.all(
overlayCodes.map(async (id) => await fetchCard(id))
);
const target = matStore.monsters.of(controller)[sequence];
if (target && target.occupant) {
if (append) {
target.overlay_materials = (target.overlay_materials || []).concat(metas);
} else {
target.overlay_materials = metas;
}
}
};
import type { ygopro } from "@/api";
import { matStore } from "@/stores";
export const getCardByLocation = (location: ygopro.CardLocation) => {
return matStore.in(location.location).of(location.controler)[
location.sequence
];
};
export * from "./cardZoneToChinese";
export * from "./fetchCheckCardMeta";
export * from "./fetchHint";
export * from "./fetchOverlayMeta";
export * from "./getCardByLocation";
import { cloneDeep } from "lodash-es";
import { proxy } from "valtio";
import { ygopro } from "@/api";
import { fetchCard } from "@/api/cards";
import type {
CardState,
DuelFieldState as ArrayCardState,
InitInfo,
MatState,
} from "./types";
import { InteractType } from "./types";
/**
* 根据controller判断是自己还是对方。
* 这个无需export,尽量逻辑收拢在store内部。
*/
const getWhom = (controller: number): "me" | "op" =>
isMe(controller) ? "me" : "op";
/** 卡的列表,提供了一些方便的方法 */
class CardArray extends Array<CardState> implements ArrayCardState {
public __proto__ = CardArray.prototype;
public zone: ygopro.CardZone = ygopro.CardZone.MZONE;
public getController: () => number = () => 1;
private genCard = async (controller: number, id: number) => ({
occupant: await fetchCard(id, true),
location: {
controler: controller,
location: this.zone,
},
counters: {},
idleInteractivities: [],
});
// methods
remove(sequence: number) {
this.splice(sequence, 1);
}
async insert(sequence: number, id: number) {
const card = await this.genCard(this.getController(), id);
this.splice(sequence, 0, card);
}
async add(ids: number[]) {
const cards = await Promise.all(
ids.map(async (id) => this.genCard(this.getController(), id))
);
this.splice(this.length, 0, ...cards);
}
async setOccupant(
sequence: number,
id: number,
position?: ygopro.CardPosition
) {
const meta = await fetchCard(id);
const target = this[sequence];
target.occupant = meta;
if (position) {
target.location.position = position;
}
}
addIdleInteractivity(
sequence: number,
interactivity: CardState["idleInteractivities"][number]
) {
this[sequence].idleInteractivities.push(interactivity);
}
clearIdleInteractivities() {
this.forEach((card) => (card.idleInteractivities = []));
}
setPlaceInteractivityType(sequence: number, interactType: InteractType) {
this[sequence].placeInteractivity = {
interactType: interactType,
response: {
controler: this.getController(),
zone: this.zone,
sequence,
},
};
}
clearPlaceInteractivity() {
this.forEach((card) => (card.placeInteractivity = undefined));
}
}
const genDuelCardArray = (cardStates: CardState[], zone: ygopro.CardZone) => {
// 为什么不放在构造函数里面,是因为不想改造继承自Array的构造函数
const me = cloneDeep(new CardArray(...cardStates));
me.zone = zone;
me.getController = () => (matStore.selfType === 1 ? 0 : 1);
const op = cloneDeep(new CardArray(...cardStates));
op.zone = zone;
op.getController = () => (matStore.selfType === 1 ? 1 : 0);
const res = proxy({
me,
op,
of: (controller: number) => res[getWhom(controller)],
});
return res;
};
/**
* 根据自己的先后手判断是否是自己
* 原本名字叫judgeSelf
*/
const isMe = (controller: number): boolean => {
switch (matStore.selfType) {
case 1:
// 自己是先攻
return controller === 0;
case 2:
// 自己是后攻
return controller === 1;
default:
// 目前不可能出现这种情况
console.error("judgeSelf error", controller, matStore.selfType);
return false;
}
};
/**
* 生成一个指定长度的卡片数组
*/
const genBlock = (location: ygopro.CardZone, n: number) =>
Array(n)
.fill(null)
.map((_) => ({
location: {
location,
},
idleInteractivities: [],
counters: {},
}));
const initInfo: MatState["initInfo"] = (() => {
const defaultInitInfo = {
masterRule: "UNKNOWN",
life: -1, // 特地设置一个不可能的值
deckSize: 0,
extraSize: 0,
};
return proxy({
me: { ...defaultInitInfo },
op: { ...defaultInitInfo },
of: (controller: number) => initInfo[getWhom(controller)],
set: (controller: number, obj: Partial<InitInfo>) => {
initInfo[getWhom(controller)] = {
...initInfo[getWhom(controller)],
...obj,
};
},
});
})();
const hint: MatState["hint"] = proxy({
code: -1,
});
/**
* zone -> matStore
*/
const getZone = (zone: ygopro.CardZone) => {
switch (zone) {
case ygopro.CardZone.MZONE:
return matStore.monsters;
case ygopro.CardZone.SZONE:
return matStore.magics;
case ygopro.CardZone.HAND:
return matStore.hands;
case ygopro.CardZone.DECK:
return matStore.decks;
case ygopro.CardZone.GRAVE:
return matStore.graveyards;
case ygopro.CardZone.REMOVED:
return matStore.banishedZones;
case ygopro.CardZone.EXTRA:
return matStore.extraDecks;
default:
console.error("in error", zone);
return matStore.extraDecks;
}
};
const { SZONE, MZONE, GRAVE, REMOVED, HAND, DECK, EXTRA } = ygopro.CardZone;
/**
* 💡 决斗盘状态仓库,本文件核心,
* 具体介绍可以点进`MatState`去看
*/
export const matStore: MatState = proxy<MatState>({
magics: genDuelCardArray(genBlock(SZONE, 6), SZONE),
monsters: genDuelCardArray(genBlock(MZONE, 7), MZONE),
graveyards: genDuelCardArray([], GRAVE),
banishedZones: genDuelCardArray([], REMOVED),
hands: genDuelCardArray([], HAND),
decks: genDuelCardArray([], DECK),
extraDecks: genDuelCardArray([], EXTRA),
timeLimits: {
// 时间限制
me: -1,
op: -1,
of: (controller: number) => matStore.timeLimits[getWhom(controller)],
set: (controller: number, time: number) => {
matStore.timeLimits[getWhom(controller)] = time;
},
},
initInfo,
selfType: ygopro.StocTypeChange.SelfType.UNKNOWN,
hint,
currentPlayer: -1,
phase: {
currentPhase: "UNKNOWN", // TODO 当前的阶段 应该改成enum
enableBp: false, // 允许进入战斗阶段
enableM2: false, // 允许进入M2阶段
enableEp: false, // 允许回合结束
},
result: ygopro.StocGameMessage.MsgWin.ActionType.UNKNOWN,
waiting: false,
unimplemented: 0,
// methods
in: getZone,
isMe,
});
// @ts-ignore 挂到全局,便于调试
window.matStore = matStore;
// 修改原型链,因为valtio的proxy会把原型链改掉。这应该是valtio的一个bug...有空提issue去改
(["me", "op"] as const).forEach((who) => {
(
[
"hands",
"decks",
"extraDecks",
"graveyards",
"banishedZones",
"monsters",
"magics",
] as const
).forEach((zone) => {
matStore[zone][who].__proto__ = CardArray.prototype;
});
});
import type { ygopro } from "@/api";
import type { CardMeta } from "@/api/cards";
import type { ygopro } from "@/api/ocgcore/idl/ocgcore";
export interface DuelState {
selfType?: number;
meInitInfo?: InitInfo; // 自己的初始状态
opInitInfo?: InitInfo; // 对手的初始状态
// >>> play mat state >>>
meHands?: HandState; // 自己的手牌
opHands?: HandState; // 对手的手牌
export interface BothSide<T> {
me: T;
op: T;
/** 根据controller返回对应的数组,op或者me */
of: (controller: number) => T;
}
/**
* CardState的顺序index,被称为sequence
*/
export interface DuelFieldState extends Array<CardState> {
/** 移除特定位置的卡片 */
remove: (sequence: number) => void;
/** 在末尾添加卡片 */
insert: (sequence: number, id: number) => Promise<void>;
/** 在指定位置插入卡片 */
add: (ids: number[]) => Promise<void>;
/** 设置占据这个位置的卡片信息 */
setOccupant: (
sequence: number,
id: number,
position?: ygopro.CardPosition
) => Promise<void>;
/** 添加 idle 的交互性 */
addIdleInteractivity: (
sequence: number,
interactivity: CardState["idleInteractivities"][number]
) => void;
/** 移除 idle 的交互性 */
clearIdleInteractivities: () => void;
/** 设置 place 的交互种类 */
setPlaceInteractivityType: (
sequence: number,
interactType: InteractType
) => void;
/** 移除 place 的交互性 */
clearPlaceInteractivity: () => void;
// 让原型链不报错
__proto__?: DuelFieldState;
}
type test = DuelFieldState extends (infer S)[] ? S : never;
export interface MatState {
selfType: number;
initInfo: BothSide<InitInfo> & {
set: (controller: number, obj: Partial<InitInfo>) => void;
}; // 双方的初始化信息
hands: BothSide<HandState>; // 双方的手牌
meMonsters?: MonsterState; // 自己的怪兽区状态
opMonsters?: MonsterState; // 对手的怪兽区状态
monsters: BothSide<MonsterState>; // 双方的怪兽区状态
meMagics?: MagicState; // 自己的魔法陷阱区状态
opMagics?: MagicState; // 对手的魔法陷阱区状态
magics: BothSide<MagicState>; // 双方的魔法区状态
meGraveyard?: GraveyardState; // 自己的墓地状态
opGraveyard?: GraveyardState; // 对手的墓地状态
graveyards: BothSide<GraveyardState>; // 双方的墓地状态
meBanishedZone?: BanishedZoneState; // 自己的除外区状态
opBanishedZone?: BanishedZoneState; // 对手的除外区状态
banishedZones: BothSide<BanishedZoneState>; // 双方的除外区状态
meDeck?: DeckState; // 自己的卡组状态
opDeck?: DeckState; // 对手的卡组状态
decks: BothSide<DeckState>; // 双方的卡组状态
meExtraDeck?: ExtraDeckState; // 自己的额外卡组状态
opExtraDeck?: ExtraDeckState; // 对手的额外卡组状态
extraDecks: BothSide<ExtraDeckState>; // 双方的额外卡组状态
meTimeLimit?: TimeLimit; // 自己的计时
opTimeLimit?: TimeLimit; // 对手的计时
timeLimits: BothSide<number> & {
set: (controller: number, time: number) => void;
}; // 双方的时间限制
hint?: HintState;
hint: HintState;
currentPlayer?: number; // 当前的操作方
currentPlayer: number; // 当前的操作方
phase?: PhaseState;
phase: PhaseState;
result?: ygopro.StocGameMessage.MsgWin.ActionType;
result: ygopro.StocGameMessage.MsgWin.ActionType;
waiting?: boolean;
waiting: boolean;
unimplemented?: number; // 未处理的`Message`
unimplemented: number; // 未处理的`Message`
// >>> methods >>>
/** 根据zone获取hands/masters/magics... */
in: (zone: ygopro.CardZone) => BothSide<DuelFieldState>;
/** 根据自己的先后手判断是否是自己 */
isMe: (player: number) => boolean;
}
export interface InitInfo {
......@@ -50,16 +97,19 @@ export interface InitInfo {
extraSize: number;
}
/**
* 场上某位置的状态,
* 以后会更名为 BlockState
*/
export interface CardState {
occupant?: CardMeta; // 占据此位置的卡牌元信息
location: {
controler?: number;
location?: number;
position?: ygopro.CardPosition;
overlay_sequence?: number;
}; // 位置信息
controler?: number; // 控制这个位置的玩家,0或1
location: ygopro.CardZone; // 怪兽区/魔法陷阱区/手牌/卡组/墓地/除外区
position?: ygopro.CardPosition; // 卡片的姿势:攻击还是守备
}; // 位置信息,叫location的原因是为了和ygo对齐
idleInteractivities: Interactivity<number>[]; // IDLE状态下的互动信息
placeInteractivities?: Interactivity<{
placeInteractivity?: Interactivity<{
controler: number;
zone: ygopro.CardZone;
sequence: number;
......@@ -69,10 +119,6 @@ export interface CardState {
reload?: boolean; // 这个字段会在收到MSG_RELOAD_FIELD的时候设置成true,在收到MSG_UPDATE_DATE的时候设置成false
}
export interface DuelFieldState {
inner: CardState[];
}
export interface Interactivity<T> {
interactType: InteractType;
// 如果`interactType`是`ACTIVATE`,这个字段是对应的效果编号
......@@ -112,15 +158,21 @@ export interface ExtraDeckState extends DuelFieldState {}
export interface TimeLimit {
leftTime: number;
}
export interface HintState {
code: number;
msg?: string;
esHint?: string;
esSelectHint?: string;
}
export type PhaseName =
keyof typeof ygopro.StocGameMessage.MsgNewPhase.PhaseType;
export interface PhaseState {
currentPhase: string; // TODO 当前的阶段 应该改成enum
currentPhase: PhaseName; // TODO 当前的阶段 应该改成enum
enableBp: boolean; // 允许进入战斗阶段
enableM2: boolean; // 允许进入M2阶段
enableEp: boolean; // 允许回合结束
}
// <<< play mat state <<<
export * from "./methods";
export * from "./store";
import { matStore } from "@/stores";
export const clearAllIdleInteractivities = (controller: number) => {
matStore.banishedZones.of(controller).clearIdleInteractivities();
matStore.decks.of(controller).clearIdleInteractivities();
matStore.extraDecks.of(controller).clearIdleInteractivities();
matStore.graveyards.of(controller).clearIdleInteractivities();
matStore.hands.of(controller).clearIdleInteractivities();
matStore.magics.of(controller).clearIdleInteractivities();
matStore.monsters.of(controller).clearIdleInteractivities();
};
import { ygopro } from "@/api";
import { matStore } from "@/stores";
/** 清空所有place互动性,也可以删除某一个zone的互动性。zone为空则为清除所有。 */
export const clearAllPlaceInteradtivities = (
controller: number,
zone?: ygopro.CardZone
) => {
if (zone) {
matStore.in(zone).of(controller).clearPlaceInteractivity();
} else {
matStore.banishedZones.of(controller).clearPlaceInteractivity();
matStore.decks.of(controller).clearPlaceInteractivity();
matStore.extraDecks.of(controller).clearPlaceInteractivity();
matStore.graveyards.of(controller).clearPlaceInteractivity();
matStore.hands.of(controller).clearPlaceInteractivity();
matStore.magics.of(controller).clearPlaceInteractivity();
matStore.monsters.of(controller).clearPlaceInteractivity();
}
};
import { fetchCard, type ygopro } from "@/api";
import { getCardByLocation, messageStore } from "@/stores";
export const fetchCheckCardMetasV2 = async ({
selected,
options,
}: {
selected: boolean;
options: {
code: number;
location: ygopro.CardLocation;
response: number;
name?: string;
desc?: string;
}[];
}) => {
const metas = await Promise.all(
options.map(async (option) => {
return await fetchCard(option.code, true);
})
);
for (const option of options) {
if (option.code == 0) {
const newCode = getCardByLocation(option.location)?.occupant?.id || 0;
option.code = newCode;
}
}
options.forEach((option) => {
metas.forEach((meta) => {
if (option.code == meta.id) {
option.name = meta.text.name;
option.desc = meta.text.desc;
}
});
});
if (selected) {
messageStore.checkCardModalV2.selectedOptions = options;
} else {
messageStore.checkCardModalV2.selectableOptions = options;
}
};
import { fetchCard, type ygopro } from "@/api";
import { getCardByLocation, messageStore } from "@/stores";
export const fetchCheckCardMetasV3 = async ({
mustSelect,
options,
}: {
mustSelect: boolean;
options: {
code: number;
location: ygopro.CardLocation;
level1: number;
level2: number;
response: number;
}[];
}) => {
const metas = await Promise.all(
options.map(async (option) => {
return await fetchCard(option.code, true);
})
);
const newOptions = options.map((option) => {
if (option.code == 0) {
const newCode = getCardByLocation(option.location)?.occupant?.id || 0;
option.code = newCode;
}
return {
meta: { id: option.code, data: {}, text: {} },
level1: option.level1,
level2: option.level2,
response: option.response,
};
});
newOptions.forEach((option) => {
metas.forEach((meta) => {
if (option.meta.id == meta.id) {
option.meta = meta;
}
});
});
if (mustSelect) {
messageStore.checkCardModalV3.mustSelectList = newOptions;
} else {
messageStore.checkCardModalV3.selectAbleList = newOptions;
}
};
export * from "./clearAllIdleInteractivities";
export * from "./clearAllPlaceInteradtivities";
export * from "./fetchCheckCardMetasV2";
export * from "./fetchCheckCardMetasV3";
import { proxy } from "valtio";
import type { ModalState } from "./types";
export const messageStore = proxy<ModalState>({
cardModal: { isOpen: false, interactivies: [], counters: {} },
cardListModal: { isOpen: false, list: [] },
checkCardModal: { isOpen: false, cancelAble: false, tags: [] },
yesNoModal: { isOpen: false },
positionModal: { isOpen: false, positions: [] },
optionModal: { isOpen: false, options: [] },
checkCardModalV2: {
isOpen: false,
cancelAble: false,
finishAble: false,
responseable: false,
selectableOptions: [],
selectedOptions: [],
},
checkCardModalV3: {
isOpen: false,
overflow: false,
allLevel: 0,
mustSelectList: [],
selectAbleList: [],
},
checkCounterModal: {
isOpen: false,
options: [],
},
sortCardModal: {
isOpen: false,
options: [],
},
});
// >>> modal types >>>
import { CardMeta } from "@/api/cards";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import type { CardMeta, ygopro } from "@/api";
type CardLocation = ReturnType<typeof ygopro.CardLocation.prototype.toObject>;
export interface ModalState {
......@@ -113,14 +112,3 @@ export interface ModalState {
}[];
};
}
export * from "./cardListModalSlice";
export * from "./cardModalSlice";
export * from "./checkCardModalSlice";
export * from "./checkCardModalV2Slice";
export * from "./checkCardModalV3Slice";
export * from "./checkCounterModalSlice";
export * from "./optionModalSlice";
export * from "./positionModalSlice";
export * from "./sortCardModalSlice";
export * from "./yesNoModalSlice";
......@@ -8,6 +8,6 @@ export interface MoraState {
export const moraStore = proxy<MoraState>({
duelStart: false,
selectHandAble: false,
selectTpAble: false,
selectHandAble: true,
selectTpAble: true,
});
......@@ -2,7 +2,6 @@ import * as BABYLON from "@babylonjs/core";
import { Row } from "antd";
import React from "react";
import { Engine, Scene } from "react-babylonjs";
import { Provider, ReactReduxContext } from "react-redux";
import { useConfig } from "@/config";
......@@ -75,28 +74,22 @@ const NeosSider = () => (
);
const NeosCanvas = () => (
<ReactReduxContext.Consumer>
{({ store }) => (
<Engine antialias adaptToDeviceRatio canvasId="babylonJS">
<Scene>
<Provider store={store}>
<Camera />
<Light />
<Hands />
<Monsters />
<Magics />
<Field />
<CommonDeck />
<ExtraDeck />
<Graveyard />
<BanishedZone />
<Field />
<Ground />
</Provider>
</Scene>
</Engine>
)}
</ReactReduxContext.Consumer>
<Engine antialias adaptToDeviceRatio canvasId="babylonJS">
<Scene>
<Camera />
<Light />
<Hands />
<Monsters />
<Magics />
<Field />
<CommonDeck />
<ExtraDeck />
<Graveyard />
<BanishedZone />
<Field />
<Ground />
</Scene>
</Engine>
);
const Camera = () => (
......
import { Alert as AntdAlert } from "antd";
import React from "react";
import { useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio";
import { sendSurrender } from "@/api/ocgcore/ocgHelper";
import { useAppSelector } from "@/hook";
import { selectUnimplemented } from "@/reducers/duel/mod";
import { sendSurrender } from "@/api";
import { matStore } from "@/stores";
export const Alert = () => {
const unimplemented = useAppSelector(selectUnimplemented);
const matSnap = useSnapshot(matStore);
const unimplemented = matSnap.unimplemented;
const navigate = useNavigate();
return (
......
import { Button, Drawer, List } from "antd";
import React from "react";
import { useSnapshot } from "valtio";
import { sendSelectIdleCmdResponse } from "@/api/ocgcore/ocgHelper";
import { sendSelectIdleCmdResponse } from "@/api";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import {
clearAllIdleInteractivities,
setCardListModalIsOpen,
} from "@/reducers/duel/mod";
import {
selectCardListModalInfo,
selectCardListModalIsOpen,
} from "@/reducers/duel/modal/mod";
import { store } from "@/store";
clearAllIdleInteractivities as clearAllIdleInteractivities,
messageStore,
} from "@/stores";
const NeosConfig = useConfig();
const CARD_WIDTH = 100;
const { cardListModal } = messageStore;
export const CardListModal = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectCardListModalIsOpen);
const list = useAppSelector(selectCardListModalInfo);
const snapCardListModal = useSnapshot(cardListModal);
const isOpen = snapCardListModal.isOpen;
const list = snapCardListModal.list as typeof cardListModal.list;
const handleOkOrCancel = () => {
dispatch(setCardListModalIsOpen(false));
cardListModal.isOpen = false;
};
return (
......@@ -39,9 +36,9 @@ export const CardListModal = () => {
key={idx}
onClick={() => {
sendSelectIdleCmdResponse(interactivy.response);
dispatch(setCardListModalIsOpen(false));
dispatch(clearAllIdleInteractivities(0));
dispatch(clearAllIdleInteractivities(1));
cardListModal.isOpen = false;
clearAllIdleInteractivities(0);
clearAllIdleInteractivities(1);
}}
>
{interactivy.desc}
......
......@@ -3,22 +3,14 @@ import { Button, Card, Col, Modal, Row } from "antd";
import { ReactComponent as BattleSvg } from "neos-assets/battle-axe.svg";
import { ReactComponent as DefenceSvg } from "neos-assets/checked-shield.svg";
import React from "react";
import { useSnapshot } from "valtio";
import { sendSelectIdleCmdResponse } from "@/api/ocgcore/ocgHelper";
import { fetchStrings } from "@/api/strings";
import { fetchStrings, sendSelectIdleCmdResponse } from "@/api";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import {
clearAllIdleInteractivities,
setCardModalIsOpen,
} from "@/reducers/duel/mod";
import {
selectCardModalCounters,
selectCardModalInteractivies,
selectCardModalIsOpen,
selectCardModalMeta,
} from "@/reducers/duel/modal/mod";
import { store } from "@/store";
clearAllIdleInteractivities as clearAllIdleInteractivities,
messageStore,
} from "@/stores";
import {
Attribute2StringCodeMap,
......@@ -31,10 +23,18 @@ const NeosConfig = useConfig();
const { Meta } = Card;
const CARD_WIDTH = 240;
const { cardModal } = messageStore;
export const CardModal = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectCardModalIsOpen);
const meta = useAppSelector(selectCardModalMeta);
const snapCardModal = useSnapshot(cardModal);
// const dispatch = store.dispatch;
// const isOpen = useAppSelector(selectCardModalIsOpen);
// const meta = useAppSelector(selectCardModalMeta);
const isOpen = snapCardModal.isOpen;
const meta = snapCardModal.meta;
const name = meta?.text.name;
const types = meta?.data.type;
const race = meta?.data.race;
......@@ -43,14 +43,17 @@ export const CardModal = () => {
const desc = meta?.text.desc;
const atk = meta?.data.atk;
const def = meta?.data.def;
const counters = useAppSelector(selectCardModalCounters);
// const counters = useAppSelector(selectCardModalCounters);
const counters = snapCardModal.counters;
const imgUrl = meta?.id
? `${NeosConfig.cardImgUrl}/${meta.id}.jpg`
: undefined;
const interactivies = useAppSelector(selectCardModalInteractivies);
const interactivies = snapCardModal.interactivies;
const handleOkOrCancel = () => {
dispatch(setCardModalIsOpen(false));
cardModal.isOpen = false;
};
return (
......@@ -76,9 +79,9 @@ export const CardModal = () => {
key={idx}
onClick={() => {
sendSelectIdleCmdResponse(interactive.response);
dispatch(setCardModalIsOpen(false));
dispatch(clearAllIdleInteractivities(0));
dispatch(clearAllIdleInteractivities(1));
cardModal.isOpen = false;
clearAllIdleInteractivities(0);
clearAllIdleInteractivities(1);
}}
>
{interactive.desc}
......
......@@ -2,42 +2,31 @@ import { ThunderboltOutlined } from "@ant-design/icons";
import { CheckCard, CheckCardProps } from "@ant-design/pro-components";
import { Button, Col, Popover, Row } from "antd";
import React, { useState } from "react";
import { useSnapshot } from "valtio";
import {
sendSelectCardResponse,
sendSelectChainResponse,
} from "@/api/ocgcore/ocgHelper";
import { sendSelectCardResponse, sendSelectChainResponse } from "@/api";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import { selectHint } from "@/reducers/duel/hintSlice";
import {
resetCheckCardModal,
setCheckCardModalIsOpen,
} from "@/reducers/duel/mod";
import {
selectCheckCardModalCacnelResponse,
selectCheckCardModalCancelAble,
selectCheckCardModalIsOpen,
selectCheckCardModalMinMax,
selectCheckCardModalOnSubmit,
selectCheckCardModalTags,
} from "@/reducers/duel/modal/mod";
import { store } from "@/store";
import { matStore, messageStore } from "@/stores";
import { DragModal } from "./DragModal";
const NeosConfig = useConfig();
const { checkCardModal } = messageStore;
export const CheckCardModal = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectCheckCardModalIsOpen);
const { min, max } = useAppSelector(selectCheckCardModalMinMax);
const tabs = useAppSelector(selectCheckCardModalTags);
const onSubmit = useAppSelector(selectCheckCardModalOnSubmit);
const cancelAble = useAppSelector(selectCheckCardModalCancelAble);
const cancelResponse = useAppSelector(selectCheckCardModalCacnelResponse);
const snapCheckCardModal = useSnapshot(checkCardModal);
const isOpen = snapCheckCardModal.isOpen;
const min = snapCheckCardModal.selectMin ?? 0;
const max = snapCheckCardModal.selectMax ?? 10;
const tabs = snapCheckCardModal.tags;
const onSubmit = snapCheckCardModal.onSubmit;
const cancelAble = snapCheckCardModal.cancelAble;
const cancelResponse = snapCheckCardModal.cancelResponse;
const [response, setResponse] = useState<number[]>([]);
const defaultValue: number[] = [];
const hint = useAppSelector(selectHint);
const hint = useSnapshot(matStore.hint);
const preHintMsg = hint?.esHint || "";
const selectHintMsg = hint?.esSelectHint || "请选择卡片";
......@@ -60,6 +49,15 @@ export const CheckCardModal = () => {
}
};
const resetCheckCardModal = () => {
checkCardModal.isOpen = false;
checkCardModal.selectMin = undefined;
checkCardModal.selectMax = undefined;
checkCardModal.cancelAble = false;
checkCardModal.cancelResponse = undefined;
checkCardModal.tags = [];
};
return (
<DragModal
title={`${preHintMsg} ${selectHintMsg} ${min}-${max}`}
......@@ -71,8 +69,8 @@ export const CheckCardModal = () => {
disabled={response.length < min || response.length > max}
onClick={() => {
sendResponseHandler(onSubmit, response);
dispatch(setCheckCardModalIsOpen(false));
dispatch(resetCheckCardModal());
checkCardModal.isOpen = false;
resetCheckCardModal();
}}
onFocus={() => {}}
onBlur={() => {}}
......@@ -85,8 +83,8 @@ export const CheckCardModal = () => {
if (cancelResponse) {
sendResponseHandler(onSubmit, [cancelResponse]);
}
dispatch(setCheckCardModalIsOpen(false));
dispatch(resetCheckCardModal());
checkCardModal.isOpen = false;
resetCheckCardModal();
}}
onFocus={() => {}}
onBlur={() => {}}
......
import { CheckCard } from "@ant-design/pro-components";
import { Button, Card, Col, Row } from "antd";
import React from "react";
import { useSnapshot } from "valtio";
import { sendSelectUnselectCardResponse } from "@/api/ocgcore/ocgHelper";
import { sendSelectUnselectCardResponse } from "@/api";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import { selectHint } from "@/reducers/duel/hintSlice";
import {
resetCheckCardModalV2,
setCheckCardModalV2IsOpen,
setCheckCardModalV2ResponseAble,
} from "@/reducers/duel/mod";
import {
selectCheckCardModalV2CancelAble,
selectCheckCardModalV2FinishAble,
selectCheckCardModalV2IsOpen,
selectCheckCardModalV2MinMax,
selectCheckCardModalV2ResponseAble,
selectCheckCardModalV2SelectAbleOptions,
selectCheckCardModalV2SelectedOptions,
} from "@/reducers/duel/modal/checkCardModalV2Slice";
import { store } from "@/store";
import { matStore, messageStore } from "@/stores";
import { DragModal } from "./DragModal";
const { checkCardModalV2 } = messageStore;
const NeosConfig = useConfig();
export const CheckCardModalV2 = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectCheckCardModalV2IsOpen);
const { min, max } = useAppSelector(selectCheckCardModalV2MinMax);
const cancelable = useAppSelector(selectCheckCardModalV2CancelAble);
const finishable = useAppSelector(selectCheckCardModalV2FinishAble);
const selectableOptions = useAppSelector(
selectCheckCardModalV2SelectAbleOptions
);
const selectedOptions = useAppSelector(selectCheckCardModalV2SelectedOptions);
const responseable = useAppSelector(selectCheckCardModalV2ResponseAble);
const hint = useAppSelector(selectHint);
const snapCheckCardModalV2 = useSnapshot(checkCardModalV2);
const isOpen = snapCheckCardModalV2.isOpen;
const min = snapCheckCardModalV2.selectMin ?? 0;
const max = snapCheckCardModalV2.selectMax ?? 10;
const cancelable = snapCheckCardModalV2.cancelAble;
const finishable = snapCheckCardModalV2.finishAble;
const selectableOptions = snapCheckCardModalV2.selectableOptions;
const selectedOptions = snapCheckCardModalV2.selectedOptions;
const responseable = snapCheckCardModalV2.responseable;
const hint = useSnapshot(matStore.hint);
const preHintMsg = hint?.esHint || "";
const selectHintMsg = hint?.esSelectHint || "请选择卡片";
const resetCheckCardModalV2 = () => {
checkCardModalV2.isOpen = false;
checkCardModalV2.finishAble = false;
checkCardModalV2.cancelAble = false;
checkCardModalV2.responseable = false;
checkCardModalV2.selectableOptions = [];
checkCardModalV2.selectedOptions = [];
};
const onFinishOrCancel = () => {
sendSelectUnselectCardResponse({ cancel_or_finish: true });
dispatch(setCheckCardModalV2IsOpen(false));
dispatch(resetCheckCardModalV2());
dispatch(setCheckCardModalV2ResponseAble(false));
checkCardModalV2.isOpen = false;
checkCardModalV2.responseable = false;
resetCheckCardModalV2();
};
return (
......@@ -75,10 +72,10 @@ export const CheckCardModalV2 = () => {
size="small"
onChange={(value) => {
if (responseable) {
dispatch(setCheckCardModalV2IsOpen(false));
// @ts-ignore
sendSelectUnselectCardResponse({ selected_ptr: value });
dispatch(setCheckCardModalV2ResponseAble(false));
checkCardModalV2.isOpen = false;
checkCardModalV2.responseable = false;
}
}}
>
......
import { CheckCard } from "@ant-design/pro-components";
import { Button, Card, Col, Row } from "antd";
import React, { useState } from "react";
import { useSnapshot } from "valtio";
import { sendSelectCardResponse } from "@/api/ocgcore/ocgHelper";
import { sendSelectCardResponse } from "@/api";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import { selectHint } from "@/reducers/duel/hintSlice";
import {
resetCheckCardModalV3,
setCheckCardModalV3IsOpen,
setCheckCardModalV3ResponseAble,
} from "@/reducers/duel/mod";
import { selectCheckCardModalV3 } from "@/reducers/duel/modal/checkCardModalV3Slice";
import { store } from "@/store";
import { matStore, messageStore } from "@/stores";
import { DragModal } from "./DragModal";
const NeosConfig = useConfig();
const { checkCardModalV3 } = messageStore;
export const CheckCardModalV3 = () => {
const dispatch = store.dispatch;
const state = useAppSelector(selectCheckCardModalV3);
const isOpen = state.isOpen;
const min = state.selectMin || 0;
const max = state.selectMax || 0;
const mustSelectOptions = state.mustSelectList;
const selectAbleOptions = state.selectAbleList;
const snapCheckCardModalV3 = useSnapshot(checkCardModalV3);
const isOpen = snapCheckCardModalV3.isOpen;
const min = snapCheckCardModalV3.selectMin || 0;
const max = snapCheckCardModalV3.selectMax || 0;
const mustSelectOptions = snapCheckCardModalV3.mustSelectList;
const selectAbleOptions = snapCheckCardModalV3.selectAbleList;
const overflow = snapCheckCardModalV3.overflow;
const LevelSum = snapCheckCardModalV3.allLevel;
const [selectedOptions, setSelectedOptions] = useState([]);
const overflow = state.overflow;
const LevelSum = state.allLevel;
const Level1Sum = mustSelectOptions
.concat(selectedOptions)
.map((option) => option.level1)
......@@ -37,7 +33,7 @@ export const CheckCardModalV3 = () => {
.concat(selectedOptions)
.map((option) => option.level2)
.reduce((sum, current) => sum + current, 0);
const hint = useAppSelector(selectHint);
const hint = useSnapshot(matStore.hint);
const preHintMsg = hint?.esHint || "";
const selectHintMsg = hint?.esSelectHint || "请选择卡片";
......@@ -51,9 +47,12 @@ export const CheckCardModalV3 = () => {
sendSelectCardResponse(
mustSelectOptions.concat(selectedOptions).map((option) => option.response)
);
dispatch(setCheckCardModalV3IsOpen(false));
dispatch(resetCheckCardModalV3());
dispatch(setCheckCardModalV3ResponseAble(false));
checkCardModalV3.isOpen = false;
checkCardModalV3.responseable = false;
checkCardModalV3.overflow = false;
checkCardModalV3.allLevel = 0;
checkCardModalV3.mustSelectList = [];
checkCardModalV3.selectAbleList = [];
};
return (
......
import { Button, Card, Col, InputNumber, Row } from "antd";
import React, { useState } from "react";
import { useSnapshot } from "valtio";
import { sendSelectCounterResponse } from "@/api/ocgcore/ocgHelper";
import { fetchStrings } from "@/api/strings";
import { fetchStrings, sendSelectCounterResponse } from "@/api";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import { clearCheckCounter } from "@/reducers/duel/mod";
import { selectCheckCounterModal } from "@/reducers/duel/modal/checkCounterModalSlice";
import { store } from "@/store";
import { messageStore } from "@/stores";
import { DragModal } from "./DragModal";
const { checkCounterModal } = messageStore;
const NeosConfig = useConfig();
export const CheckCounterModal = () => {
const dispatch = store.dispatch;
const state = useAppSelector(selectCheckCounterModal);
const isOpen = state.isOpen;
const counterName = fetchStrings("!counter", `0x${state.counterType!}`);
const min = state.min || 0;
const options = state.options;
const snapCheckCounterModal = useSnapshot(checkCounterModal);
const isOpen = snapCheckCounterModal.isOpen;
const min = snapCheckCounterModal.min || 0;
const options = snapCheckCounterModal.options;
const counterName = fetchStrings(
"!counter",
`0x${snapCheckCounterModal.counterType!}`
);
const [selected, setSelected] = useState(new Array(options.length));
const sum = selected.reduce((sum, current) => sum + current, 0);
const finishable = sum == min;
const onFinish = () => {
sendSelectCounterResponse(selected);
dispatch(clearCheckCounter());
messageStore.checkCounterModal.isOpen = false;
messageStore.checkCounterModal.min = undefined;
messageStore.checkCounterModal.counterType = undefined;
messageStore.checkCounterModal.options = [];
};
return (
......
import { notification } from "antd";
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { useAppSelector } from "@/hook";
import { selectHint } from "@/reducers/duel/hintSlice";
import { selectDuelResult, selectWaiting } from "@/reducers/duel/mod";
import { selectCurrentPhase } from "@/reducers/duel/phaseSlice";
import MsgWin = ygopro.StocGameMessage.MsgWin;
import { ygopro } from "@/api";
import { useConfig } from "@/config";
import { matStore } from "@/stores";
const MsgWin = ygopro.StocGameMessage.MsgWin;
const NeosConfig = useConfig();
export const HintNotification = () => {
const hint = useAppSelector(selectHint);
const currentPhase = useAppSelector(selectCurrentPhase);
const waiting = useAppSelector(selectWaiting);
const result = useAppSelector(selectDuelResult);
const hintState = matStore.hint;
const hintSnap = useSnapshot(matStore.hint);
const currentPhase = matStore.phase.currentPhase;
const waiting = matStore.waiting;
const result = matStore.result;
const navigate = useNavigate();
const [api, contextHolder] = notification.useNotification({
maxCount: NeosConfig.ui.hint.maxCount,
});
useEffect(() => {
if (hint && hint.msg) {
if (hintState && hintState.msg) {
api.info({
message: `${hint.msg}`,
message: `${hintState.msg}`,
placement: "bottom",
});
}
}, [hint?.msg]);
}, [hintSnap?.msg]);
useEffect(() => {
if (currentPhase) {
......
import { CheckCard } from "@ant-design/pro-components";
import { Button } from "antd";
import React, { useState } from "react";
import { useSnapshot } from "valtio";
import { sendSelectOptionResponse } from "@/api/ocgcore/ocgHelper";
import { useAppSelector } from "@/hook";
import { resetOptionModal, setOptionModalIsOpen } from "@/reducers/duel/mod";
import {
selectOptionModalIsOpen,
selectOptionModalOptions,
} from "@/reducers/duel/modal/mod";
import { store } from "@/store";
import { sendSelectOptionResponse } from "@/api";
import { messageStore } from "@/stores";
import { DragModal } from "./DragModal";
const { optionModal } = messageStore;
export const OptionModal = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectOptionModalIsOpen);
const options = useAppSelector(selectOptionModalOptions);
const snapOptionModal = useSnapshot(optionModal);
const isOpen = snapOptionModal.isOpen;
const options = snapOptionModal.options;
const [selected, setSelected] = useState<number | undefined>(undefined);
return (
......@@ -30,8 +29,8 @@ export const OptionModal = () => {
onClick={() => {
if (selected !== undefined) {
sendSelectOptionResponse(selected);
dispatch(setOptionModalIsOpen(false));
dispatch(resetOptionModal());
optionModal.isOpen = false;
optionModal.options = [];
}
}}
>
......
......@@ -5,26 +5,17 @@ import { ReactComponent as EpSvg } from "neos-assets/power-button.svg";
import { ReactComponent as Main2Svg } from "neos-assets/sword-in-stone.svg";
import { ReactComponent as SurrenderSvg } from "neos-assets/truce.svg";
import React, { useState } from "react";
import { useSnapshot } from "valtio";
import {
sendSelectBattleCmdResponse,
sendSelectIdleCmdResponse,
sendSurrender,
} from "@/api/ocgcore/ocgHelper";
import { useAppSelector } from "@/hook";
} from "@/api";
import {
clearAllIdleInteractivities,
setEnableBp,
setEnableEp,
setEnableM2,
} from "@/reducers/duel/mod";
import {
selectCurrentPhase,
selectEnableBp,
selectEnableEp,
selectEnableM2,
} from "@/reducers/duel/phaseSlice";
import { store } from "@/store";
clearAllIdleInteractivities as clearAllIdleInteractivities,
matStore,
} from "@/stores";
const IconSize = "150%";
const SpaceSize = 16;
......@@ -47,12 +38,15 @@ const PhaseButton = (props: {
);
};
const { phase } = matStore;
export const Phase = () => {
const dispatch = store.dispatch;
const enableBp = useAppSelector(selectEnableBp);
const enableM2 = useAppSelector(selectEnableM2);
const enableEp = useAppSelector(selectEnableEp);
const currentPhase = useAppSelector(selectCurrentPhase);
const snapPhase = useSnapshot(phase);
const enableBp = snapPhase.enableBp;
const enableM2 = snapPhase.enableM2;
const enableEp = snapPhase.enableEp;
const currentPhase = snapPhase.currentPhase;
const [modalOpen, setModalOpen] = useState(false);
const response =
......@@ -65,25 +59,22 @@ export const Phase = () => {
: 7;
const onBp = () => {
dispatch(clearAllIdleInteractivities(0));
dispatch(clearAllIdleInteractivities(0));
sendSelectIdleCmdResponse(6);
dispatch(setEnableBp(false));
clearAllIdleInteractivities(0); // 为什么要clear两次?
clearAllIdleInteractivities(0);
phase.enableBp = false;
};
const onM2 = () => {
dispatch(clearAllIdleInteractivities(0));
dispatch(clearAllIdleInteractivities(0));
sendSelectBattleCmdResponse(2);
dispatch(setEnableM2(false));
clearAllIdleInteractivities(0);
clearAllIdleInteractivities(0);
phase.enableM2 = false;
};
const onEp = () => {
dispatch(clearAllIdleInteractivities(0));
dispatch(clearAllIdleInteractivities(0));
sendSelectIdleCmdResponse(response);
dispatch(setEnableEp(false));
clearAllIdleInteractivities(0);
clearAllIdleInteractivities(0);
phase.enableEp = false;
};
const onSurrender = () => {
setModalOpen(true);
......
import { CheckCard } from "@ant-design/pro-components";
import { Button } from "antd";
import React, { useState } from "react";
import { useSnapshot } from "valtio";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { sendSelectPositionResponse } from "@/api/ocgcore/ocgHelper";
import { useAppSelector } from "@/hook";
import {
resetPositionModal,
setPositionModalIsOpen,
} from "@/reducers/duel/mod";
import {
selectPositionModalIsOpen,
selectPositionModalPositions,
} from "@/reducers/duel/modal/mod";
import { store } from "@/store";
import { sendSelectPositionResponse, ygopro } from "@/api";
import { messageStore } from "@/stores";
import { DragModal } from "./DragModal";
const { positionModal } = messageStore;
export const PositionModal = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectPositionModalIsOpen);
const positions = useAppSelector(selectPositionModalPositions);
const snapPositionModal = useSnapshot(positionModal);
const isOpen = snapPositionModal.isOpen;
const positions = snapPositionModal.positions;
const [selected, setSelected] = useState<ygopro.CardPosition | undefined>(
undefined
);
......@@ -36,8 +30,8 @@ export const PositionModal = () => {
onClick={() => {
if (selected !== undefined) {
sendSelectPositionResponse(selected);
dispatch(setPositionModalIsOpen(false));
dispatch(resetPositionModal());
positionModal.isOpen = false;
positionModal.positions = [];
}
}}
>
......
......@@ -2,7 +2,7 @@ import { SendOutlined } from "@ant-design/icons";
import { Button, Col, Input, Row } from "antd";
import React, { useState } from "react";
import { sendChat } from "@/api/ocgcore/ocgHelper";
import { sendChat } from "@/api";
export const SendBox = () => {
const [content, setContent] = useState("");
......
......@@ -17,22 +17,21 @@ import {
import { CSS } from "@dnd-kit/utilities";
import { Button, Card, Modal } from "antd";
import React, { useEffect, useState } from "react";
import { useSnapshot } from "valtio";
import { sendSortCardResponse } from "@/api";
import { CardMeta } from "@/api/cards";
import { sendSortCardResponse } from "@/api/ocgcore/ocgHelper";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import { resetSortCardModal } from "@/reducers/duel/mod";
import { selectSortCardModal } from "@/reducers/duel/modal/sortCardModalSlice";
import { store } from "@/store";
import { messageStore } from "@/stores";
const NeosConfig = useConfig();
const { sortCardModal } = messageStore;
export const SortCardModal = () => {
const dispatch = store.dispatch;
const state = useAppSelector(selectSortCardModal);
const isOpen = state.isOpen;
const options = state.options;
const snapSortCardModal = useSnapshot(sortCardModal);
const isOpen = snapSortCardModal.isOpen;
const options = snapSortCardModal.options;
const [items, setItems] = useState(options);
const sensors = useSensors(
useSensor(PointerSensor),
......@@ -43,7 +42,8 @@ export const SortCardModal = () => {
const onFinish = () => {
sendSortCardResponse(items.map((item) => item.response));
dispatch(resetSortCardModal());
sortCardModal.isOpen = false;
sortCardModal.options = [];
};
const onDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
......@@ -52,7 +52,7 @@ export const SortCardModal = () => {
setItems((items) => {
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);
});
}
......
......@@ -4,12 +4,6 @@ import { Avatar } from "antd";
import React from "react";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import {
selectMeInitInfo,
selectOpInitInfo,
} from "@/reducers/duel/initInfoSlice";
import { selectWaiting } from "@/reducers/duel/mod";
const NeosConfig = useConfig();
......@@ -18,10 +12,14 @@ const avatarSize = 40;
const ME_VALUE = "myself";
const OP_VALUE = "opponent";
import { useSnapshot } from "valtio";
import { matStore } from "@/stores";
export const PlayerStatus = () => {
const meInfo = useAppSelector(selectMeInitInfo);
const opInfo = useAppSelector(selectOpInitInfo);
const waiting = useAppSelector(selectWaiting) || false;
const meInfo = useSnapshot(matStore.initInfo.me);
const opInfo = useSnapshot(matStore.initInfo.op);
const waiting = useSnapshot(matStore).waiting;
return (
<CheckCard.Group
......
import { MessageOutlined } from "@ant-design/icons";
import { Timeline, TimelineItemProps } from "antd";
import React, { useContext, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useSnapshot } from "valtio";
import { useAppSelector } from "@/hook";
import { selectChat } from "@/reducers/chatSlice";
import { valtioContext } from "@/valtioStores";
import { chatStore } from "@/stores";
export const DuelTimeLine = () => {
const [items, setItems] = useState<TimelineItemProps[]>([]);
const chat = useAppSelector(selectChat);
const stateChat = useContext(valtioContext).chatStore;
const stateChat = chatStore;
const snapChat = useSnapshot(stateChat);
// const chat = snapChat.message;
const chat = snapChat.message;
useEffect(() => {
setItems((prev) =>
......
import { Button } from "antd";
import React from "react";
import { useSnapshot } from "valtio";
import { sendSelectEffectYnResponse } from "@/api/ocgcore/ocgHelper";
import { useAppSelector } from "@/hook";
import { selectHint } from "@/reducers/duel/hintSlice";
import { setYesNoModalIsOpen } from "@/reducers/duel/mod";
import {
selectYesNoModalIsOpen,
selectYesNOModalMsg,
} from "@/reducers/duel/modal/mod";
import { store } from "@/store";
import { sendSelectEffectYnResponse } from "@/api";
import { matStore, messageStore } from "@/stores";
import { DragModal } from "./DragModal";
const { yesNoModal } = messageStore;
export const YesNoModal = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectYesNoModalIsOpen);
const msg = useAppSelector(selectYesNOModalMsg);
const hint = useAppSelector(selectHint);
const snapYesNoModal = useSnapshot(yesNoModal);
const isOpen = snapYesNoModal.isOpen;
const msg = snapYesNoModal.msg;
const hint = useSnapshot(matStore.hint);
const preHintMsg = hint?.esHint || "";
return (
......@@ -30,7 +27,8 @@ export const YesNoModal = () => {
<Button
onClick={() => {
sendSelectEffectYnResponse(true);
dispatch(setYesNoModalIsOpen(false));
// dispatch(setYesNoModalIsOpen(false));
yesNoModal.isOpen = false;
}}
>
Yes
......@@ -38,7 +36,8 @@ export const YesNoModal = () => {
<Button
onClick={() => {
sendSelectEffectYnResponse(false);
dispatch(setYesNoModalIsOpen(false));
// dispatch(setYesNoModalIsOpen(false));
yesNoModal.isOpen = false;
}}
>
No
......
import * as BABYLON from "@babylonjs/core";
import { useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import {
selectMeBanishedZone,
selectOpBanishedZone,
} from "@/reducers/duel/banishedZoneSlice";
import { matStore } from "@/stores";
import { cardSlotRotation } from "../utils";
import { Depth, SingleSlot } from "./SingleSlot";
const NeosConfig = useConfig();
export const BanishedZone = () => {
const meBanishedZone = useAppSelector(selectMeBanishedZone).inner;
const opBanishedZone = useAppSelector(selectOpBanishedZone).inner;
const meBanishedZone = useSnapshot(matStore.banishedZones.me);
const opBanishedZone = useSnapshot(matStore.banishedZones.op);
return (
<>
<SingleSlot
state={meBanishedZone}
// 因为singleSlot里面会有snap,所以这儿可以直接传入store
state={matStore.banishedZones.me}
position={banishedZonePosition(0, meBanishedZone.length)}
rotation={cardSlotRotation(false)}
/>
<SingleSlot
state={opBanishedZone}
state={matStore.banishedZones.op}
position={banishedZonePosition(1, opBanishedZone.length)}
rotation={cardSlotRotation(true)}
/>
......
import * as BABYLON from "@babylonjs/core";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import { selectMeDeck, selectOpDeck } from "@/reducers/duel/deckSlice";
import { cardSlotRotation } from "../utils";
import { Depth, SingleSlot } from "./SingleSlot";
const NeosConfig = useConfig();
import { useSnapshot } from "valtio";
import { matStore } from "@/stores";
export const CommonDeck = () => {
const meDeck = useAppSelector(selectMeDeck).inner;
const opDeck = useAppSelector(selectOpDeck).inner;
const meDeck = useSnapshot(matStore.decks.me);
const opDeck = useSnapshot(matStore.decks.op);
return (
<>
<SingleSlot
state={meDeck}
state={matStore.decks.me}
position={deckPosition(0, meDeck.length)}
rotation={cardSlotRotation(false)}
/>
<SingleSlot
state={opDeck}
state={matStore.decks.op}
position={deckPosition(1, opDeck.length)}
rotation={cardSlotRotation(true)}
/>
......
import * as BABYLON from "@babylonjs/core";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import {
selectMeExtraDeck,
selectOpExtraDeck,
} from "@/reducers/duel/extraDeckSlice";
import { cardSlotRotation } from "../utils";
import { Depth, SingleSlot } from "./SingleSlot";
const NeosConfig = useConfig();
import { useSnapshot } from "valtio";
import { matStore } from "@/stores";
export const ExtraDeck = () => {
const meExtraDeck = useAppSelector(selectMeExtraDeck).inner;
const opExtraDeck = useAppSelector(selectOpExtraDeck).inner;
const meExtraDeck = useSnapshot(matStore.extraDecks.me);
const opExtraDeck = useSnapshot(matStore.extraDecks.op);
return (
<>
<SingleSlot
state={meExtraDeck}
state={matStore.extraDecks.me}
position={extraDeckPosition(0, meExtraDeck.length)}
rotation={cardSlotRotation(false)}
/>
<SingleSlot
state={opExtraDeck}
state={matStore.extraDecks.op}
position={extraDeckPosition(1, opExtraDeck.length)}
rotation={cardSlotRotation(true)}
/>
......
import * as BABYLON from "@babylonjs/core";
import { useSnapshot } from "valtio";
import { ygopro } from "@/api";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import { selectMeMagics, selectOpMagics } from "@/reducers/duel/magicSlice";
import { clearMagicPlaceInteractivities } from "@/reducers/duel/mod";
import { clearAllPlaceInteradtivities, matStore } from "@/stores";
import { cardSlotRotation } from "../utils";
import { FixedSlot } from "./FixedSlot";
......@@ -11,33 +11,35 @@ import { Depth } from "./SingleSlot";
const NeosConfig = useConfig();
export const Field = () => {
const meField = useAppSelector(selectMeMagics).inner.find(
(_, sequence) => sequence == 5
);
const opField = useAppSelector(selectOpMagics).inner.find(
(_, sequence) => sequence == 5
);
// 这儿的find可能是出于某种考虑,以后再深思
const meFieldState = matStore.magics.me[5];
const meField = useSnapshot(meFieldState);
const opFieldState = matStore.magics.op[5];
const opField = useSnapshot(opFieldState);
const clearPlaceInteractivitiesAction = (controller: number) =>
clearAllPlaceInteradtivities(controller, ygopro.CardZone.MZONE); // 应该是对的
return (
<>
{meField ? (
<FixedSlot
state={meField}
state={meFieldState}
sequence={0}
position={fieldPosition(0)}
rotation={cardSlotRotation(false)}
clearPlaceInteractivitiesAction={clearMagicPlaceInteractivities}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
) : (
<></>
)}
{opField ? (
<FixedSlot
state={opField}
state={opFieldState}
sequence={0}
position={fieldPosition(1)}
rotation={cardSlotRotation(true)}
clearPlaceInteractivitiesAction={clearMagicPlaceInteractivities}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
) : (
<></>
......
import * as BABYLON from "@babylonjs/core";
import { ActionCreatorWithPayload } from "@reduxjs/toolkit";
import { useRef } from "react";
import { useSnapshot } from "valtio";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { sendSelectPlaceResponse } from "@/api/ocgcore/ocgHelper";
import { sendSelectPlaceResponse, ygopro } from "@/api";
import { useConfig } from "@/config";
import { useClick } from "@/hook";
import { CardState } from "@/reducers/duel/generic";
import {
setCardListModalInfo,
setCardListModalIsOpen,
setCardModalCounters,
setCardModalInteractivies,
setCardModalIsOpen,
setCardModalMeta,
} from "@/reducers/duel/mod";
import { store } from "@/store";
type CardState,
clearAllPlaceInteradtivities,
messageStore,
} from "@/stores";
import { interactTypeToString } from "../utils";
......@@ -35,68 +29,61 @@ export const FixedSlot = (props: {
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
deffenseRotation?: BABYLON.Vector3;
clearPlaceInteractivitiesAction: ActionCreatorWithPayload<number, string>;
clearPlaceInteractivitiesAction: (controller: number) => void;
}) => {
const planeRef = useRef(null);
const snapState = useSnapshot(props.state);
const rotation =
props.state.location.position === ygopro.CardPosition.DEFENSE ||
props.state.location.position === ygopro.CardPosition.FACEUP_DEFENSE ||
props.state.location.position === ygopro.CardPosition.FACEDOWN_DEFENSE
snapState.location.position === ygopro.CardPosition.DEFENSE ||
snapState.location.position === ygopro.CardPosition.FACEUP_DEFENSE ||
snapState.location.position === ygopro.CardPosition.FACEDOWN_DEFENSE
? props.deffenseRotation || cardDefenceRotation
: props.rotation;
const edgesWidth = 2.0;
const edgesColor = BABYLON.Color4.FromColor3(BABYLON.Color3.Yellow());
const dispatch = store.dispatch;
const faceDown =
props.state.location.position === ygopro.CardPosition.FACEDOWN_DEFENSE ||
props.state.location.position === ygopro.CardPosition.FACEDOWN_ATTACK ||
props.state.location.position === ygopro.CardPosition.FACEDOWN;
snapState.location.position === ygopro.CardPosition.FACEDOWN_DEFENSE ||
snapState.location.position === ygopro.CardPosition.FACEDOWN_ATTACK ||
snapState.location.position === ygopro.CardPosition.FACEDOWN;
useClick(
(_event) => {
if (props.state.placeInteractivities) {
sendSelectPlaceResponse(props.state.placeInteractivities.response);
dispatch(props.clearPlaceInteractivitiesAction(0));
dispatch(props.clearPlaceInteractivitiesAction(1));
} else if (props.state.occupant) {
if (snapState.placeInteractivity) {
sendSelectPlaceResponse(snapState.placeInteractivity.response);
// 其实不应该从外面传进来的...
// props.clearPlaceInteractivitiesAction(0);
// props.clearPlaceInteractivitiesAction(1);
clearAllPlaceInteradtivities(0);
clearAllPlaceInteradtivities(1);
} else if (snapState.occupant) {
// 中央弹窗展示选中卡牌信息
dispatch(setCardModalMeta(props.state.occupant));
dispatch(
setCardModalInteractivies(
props.state.idleInteractivities.map((interactivity) => {
return {
desc: interactTypeToString(interactivity.interactType),
response: interactivity.response,
};
})
)
);
dispatch(setCardModalCounters(props.state.counters));
dispatch(setCardModalIsOpen(true));
messageStore.cardModal.meta = snapState.occupant;
messageStore.cardModal.interactivies =
snapState.idleInteractivities.map((interactivity) => ({
desc: interactTypeToString(interactivity.interactType),
response: interactivity.response,
}));
messageStore.cardModal.counters = snapState.counters;
messageStore.cardModal.isOpen = true;
// 侧边栏展示超量素材信息
if (
props.state.overlay_materials &&
props.state.overlay_materials.length > 0
snapState.overlay_materials &&
snapState.overlay_materials.length > 0
) {
dispatch(
setCardListModalInfo(
props.state.overlay_materials?.map((overlay) => {
return {
meta: overlay,
interactivies: [],
};
}) || []
)
);
dispatch(setCardListModalIsOpen(true));
messageStore.cardListModal.list =
snapState.overlay_materials?.map((overlay) => ({
meta: overlay,
interactivies: [],
})) || [];
messageStore.cardListModal.isOpen = true;
}
}
},
planeRef,
[props.state]
[snapState]
);
return (
......@@ -109,8 +96,7 @@ export const FixedSlot = (props: {
rotation={rotation}
enableEdgesRendering
edgesWidth={
props.state.placeInteractivities ||
props.state.idleInteractivities.length > 0
snapState.placeInteractivity || snapState.idleInteractivities.length > 0
? edgesWidth
: 0
}
......@@ -119,15 +105,15 @@ export const FixedSlot = (props: {
<standardMaterial
name={`fixedslot-mat-${props.sequence}`}
diffuseTexture={
props.state.occupant
snapState.occupant
? faceDown
? new BABYLON.Texture(`${NeosConfig.assetsPath}/card_back.jpg`)
: new BABYLON.Texture(
`${NeosConfig.cardImgUrl}/${props.state.occupant.id}.jpg`
`${NeosConfig.cardImgUrl}/${snapState.occupant.id}.jpg`
)
: new BABYLON.Texture(`${NeosConfig.assetsPath}/card_slot.png`)
}
alpha={props.state.occupant ? 1 : 0}
alpha={snapState.occupant ? 1 : 0}
></standardMaterial>
</plane>
);
......
import * as BABYLON from "@babylonjs/core";
import { useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import {
selectMeGraveyard,
selectOpGraveyard,
} from "@/reducers/duel/graveyardSlice";
import { matStore } from "@/stores";
import { cardSlotRotation } from "../utils";
import { Depth, SingleSlot } from "./SingleSlot";
const NeosConfig = useConfig();
export const Graveyard = () => {
const meGraveyard = useAppSelector(selectMeGraveyard).inner;
const opGraveyard = useAppSelector(selectOpGraveyard).inner;
const meGraveyard = useSnapshot(matStore.graveyards.me);
const opGraveyard = useSnapshot(matStore.graveyards.op);
return (
<>
<SingleSlot
state={meGraveyard}
state={matStore.graveyards.me}
position={graveyardPosition(0, meGraveyard.length)}
rotation={cardSlotRotation(false)}
/>
<SingleSlot
state={opGraveyard}
state={matStore.graveyards.op}
position={graveyardPosition(1, opGraveyard.length)}
rotation={cardSlotRotation(true)}
/>
......
import * as BABYLON from "@babylonjs/core";
import { useEffect, useRef, useState } from "react";
import { useHover } from "react-babylonjs";
import { INTERNAL_Snapshot, useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { useAppSelector, useClick } from "@/hook";
import { CardState } from "@/reducers/duel/generic";
import { selectMeHands, selectOpHands } from "@/reducers/duel/handsSlice";
import {
setCardModalInteractivies,
setCardModalIsOpen,
setCardModalMeta,
} from "@/reducers/duel/mod";
import { store } from "@/store";
import { useClick } from "@/hook";
import { type CardState, matStore, messageStore } from "@/stores";
import { animated, useSpring } from "../spring";
import { interactTypeToString, zip } from "../utils";
......@@ -26,14 +20,16 @@ const handRotation = new BABYLON.Vector3(rotation.x, rotation.y, rotation.z);
const hoverScaling = NeosConfig.ui.card.handHoverScaling;
export const Hands = () => {
const meHands = useAppSelector(selectMeHands).inner;
const meHandPositions = handPositons(0, meHands);
const opHands = useAppSelector(selectOpHands).inner;
const opHandPositions = handPositons(1, opHands);
const meHandsState = matStore.hands.me;
const opHandsState = matStore.hands.op;
const meHandsSnap = useSnapshot(meHandsState);
const opHandsSnap = useSnapshot(opHandsState);
const meHandPositions = handPositons(0, meHandsSnap);
const opHandPositions = handPositons(1, opHandsSnap);
return (
<>
{zip(meHands, meHandPositions).map(([hand, position], idx) => {
{zip(meHandsState, meHandPositions).map(([hand, position], idx) => {
return (
<CHand
key={idx}
......@@ -41,11 +37,10 @@ export const Hands = () => {
sequence={idx}
position={position}
rotation={handRotation}
cover={(id) => `${NeosConfig.cardImgUrl}/${id}.jpg`}
/>
);
})}
{zip(opHands, opHandPositions).map(([hand, position], idx) => {
{zip(opHandsState, opHandPositions).map(([hand, position], idx) => {
return (
<CHand
key={idx}
......@@ -53,7 +48,7 @@ export const Hands = () => {
sequence={idx}
position={position}
rotation={handRotation}
cover={(_) => `${NeosConfig.assetsPath}/card_back.jpg`}
back={true}
/>
);
})}
......@@ -66,7 +61,7 @@ const CHand = (props: {
sequence: number;
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
cover: (id: number) => string;
back?: boolean;
}) => {
const hoverScale = new BABYLON.Vector3(
hoverScaling.x,
......@@ -80,7 +75,6 @@ const CHand = (props: {
const state = props.state;
const [hovered, setHovered] = useState(false);
const position = props.position;
const dispatch = store.dispatch;
const [spring, api] = useSpring(
() => ({
......@@ -115,19 +109,15 @@ const CHand = (props: {
useClick(
() => {
if (state.occupant) {
dispatch(setCardModalMeta(state.occupant));
messageStore.cardModal.meta = state.occupant;
}
dispatch(
setCardModalInteractivies(
state.idleInteractivities.map((interactive) => {
return {
desc: interactTypeToString(interactive.interactType),
response: interactive.response,
};
})
)
messageStore.cardModal.interactivies = state.idleInteractivities.map(
(interactive) => ({
desc: interactTypeToString(interactive.interactType),
response: interactive.response,
})
);
dispatch(setCardModalIsOpen(true));
messageStore.cardModal.isOpen = true;
},
planeRef,
[state]
......@@ -145,7 +135,7 @@ const CHand = (props: {
rotation={props.rotation}
enableEdgesRendering
edgesWidth={
state.idleInteractivities.length > 0 || state.placeInteractivities
state.idleInteractivities.length > 0 || state.placeInteractivity
? edgesWidth
: 0
}
......@@ -154,7 +144,11 @@ const CHand = (props: {
<animated.standardMaterial
name={`hand-mat-${props.sequence}`}
diffuseTexture={
new BABYLON.Texture(props.cover(state.occupant?.id || 0))
new BABYLON.Texture(
props.back
? `${NeosConfig.assetsPath}/card_back.jpg`
: `${NeosConfig.cardImgUrl}/${state.occupant?.id || 0}.jpg`
)
}
/>
</animated.plane>
......@@ -162,7 +156,10 @@ const CHand = (props: {
);
};
const handPositons = (player: number, hands: CardState[]) => {
const handPositons = (
player: number,
hands: INTERNAL_Snapshot<CardState[]>
) => {
const gap = groundShape.width / (hands.length - 1);
const x = (idx: number) =>
player == 0 ? left + gap * idx : -left - gap * idx;
......
import * as BABYLON from "@babylonjs/core";
import { type INTERNAL_Snapshot, useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import { CardState } from "@/reducers/duel/generic";
import { selectMeMagics, selectOpMagics } from "@/reducers/duel/magicSlice";
import { clearMagicPlaceInteractivities } from "@/reducers/duel/mod";
import { type CardState, matStore } from "@/stores";
import { cardSlotRotation, zip } from "../utils";
import { FixedSlot } from "./FixedSlot";
......@@ -16,14 +14,21 @@ const gap = 1.05;
const transform = NeosConfig.ui.card.transform;
export const Magics = () => {
const meMagics = useAppSelector(selectMeMagics).inner;
const meMagicPositions = magicPositions(0, meMagics);
const opMagics = useAppSelector(selectOpMagics).inner;
const opMagicPositions = magicPositions(1, opMagics);
const meMagicState = matStore.magics.me;
const opMagicState = matStore.magics.op;
const meMagicsSnap = useSnapshot(meMagicState);
const opMagicsSnap = useSnapshot(opMagicState);
const meMagicPositions = magicPositions(0, meMagicsSnap);
const opMagicPositions = magicPositions(1, opMagicsSnap);
const clearPlaceInteractivitiesAction = (controller: number) => {
console.warn("magic clearPlaceInteractivitiesAction");
matStore.magics.of(controller).clearPlaceInteractivity();
};
return (
<>
{zip(meMagics, meMagicPositions)
{zip(meMagicState, meMagicPositions)
.slice(0, 5)
.map(([magic, position], sequence) => {
return (
......@@ -33,11 +38,11 @@ export const Magics = () => {
sequence={sequence}
position={position}
rotation={cardSlotRotation(false)}
clearPlaceInteractivitiesAction={clearMagicPlaceInteractivities}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
);
})}
{zip(opMagics, opMagicPositions)
{zip(opMagicState, opMagicPositions)
.slice(0, 5)
.map(([magic, position], sequence) => {
return (
......@@ -47,7 +52,7 @@ export const Magics = () => {
sequence={sequence}
position={position}
rotation={cardSlotRotation(true)}
clearPlaceInteractivitiesAction={clearMagicPlaceInteractivities}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
);
})}
......@@ -55,7 +60,10 @@ export const Magics = () => {
);
};
const magicPositions = (player: number, magics: CardState[]) => {
const magicPositions = (
player: number,
magics: INTERNAL_Snapshot<CardState[]>
) => {
const x = (sequence: number) =>
player == 0 ? left + gap * sequence : -left - gap * sequence;
const y = transform.z / 2 + NeosConfig.ui.card.floating;
......
import "react-babylonjs";
import * as BABYLON from "@babylonjs/core";
import { type INTERNAL_Snapshot, useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import { CardState } from "@/reducers/duel/generic";
import { clearMonsterPlaceInteractivities } from "@/reducers/duel/mod";
import {
selectMeMonsters,
selectOpMonsters,
} from "@/reducers/duel/monstersSlice";
import { type CardState, matStore } from "@/stores";
import { cardSlotDefenceRotation, cardSlotRotation, zip } from "../utils";
import { FixedSlot } from "./FixedSlot";
......@@ -20,15 +15,22 @@ const floating = NeosConfig.ui.card.floating;
const left = -2.15; // TODO: config
const gap = 1.05;
const clearPlaceInteractivitiesAction = (controller: number) => {
console.warn("monster clearPlaceInteractivitiesAction");
matStore.monsters.of(controller).clearPlaceInteractivity();
};
export const Monsters = () => {
const meMonsters = useAppSelector(selectMeMonsters).inner;
const meMonsterPositions = monsterPositions(0, meMonsters);
const opMonsters = useAppSelector(selectOpMonsters).inner;
const opMonsterPositions = monsterPositions(1, opMonsters);
const meMonstersStore = matStore.monsters.me;
const opMonstersStore = matStore.monsters.op;
const meMonstersSnap = useSnapshot(meMonstersStore);
const opMonstersSnap = useSnapshot(opMonstersStore);
const meMonsterPositions = monsterPositions(0, meMonstersSnap);
const opMonsterPositions = monsterPositions(1, opMonstersSnap);
return (
<>
{zip(meMonsters, meMonsterPositions)
{zip(meMonstersStore, meMonsterPositions)
.slice(0, 5)
.map(([monster, position], sequence) => (
<FixedSlot
......@@ -38,10 +40,10 @@ export const Monsters = () => {
position={position}
rotation={cardSlotRotation(false)}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearMonsterPlaceInteractivities}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
))}
{zip(opMonsters, opMonsterPositions)
{zip(opMonstersStore, opMonsterPositions)
.slice(0, 5)
.map(([monster, position], sequence) => (
<FixedSlot
......@@ -51,10 +53,13 @@ export const Monsters = () => {
position={position}
rotation={cardSlotRotation(true)}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearMonsterPlaceInteractivities}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
))}
<ExtraMonsters meMonsters={meMonsters} opMonsters={opMonsters} />
<ExtraMonsters
meMonsters={meMonstersStore}
opMonsters={opMonstersStore}
/>
</>
);
};
......@@ -64,10 +69,10 @@ const ExtraMonsters = (props: {
meMonsters: CardState[];
opMonsters: CardState[];
}) => {
const meLeft = props.meMonsters.find((_, sequence) => sequence == 5);
const meRight = props.meMonsters.find((_, sequence) => sequence == 6);
const opLeft = props.opMonsters.find((_, sequence) => sequence == 5);
const opRight = props.opMonsters.find((_, sequence) => sequence == 6);
const meLeft = props.meMonsters[5];
const meRight = props.meMonsters[6];
const opLeft = props.opMonsters[5];
const opRight = props.opMonsters[6];
const leftPosition = new BABYLON.Vector3(-1.1, transform.z / 2 + floating, 0);
const rightPosition = new BABYLON.Vector3(1.1, transform.z / 2 + floating, 0);
......@@ -77,59 +82,46 @@ const ExtraMonsters = (props: {
return (
<>
{meLeft ? (
<FixedSlot
state={meLeft}
sequence={5}
position={leftPosition}
rotation={meRotation}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearMonsterPlaceInteractivities}
/>
) : (
<></>
)}
{meRight ? (
<FixedSlot
state={meRight}
sequence={6}
position={rightPosition}
rotation={meRotation}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearMonsterPlaceInteractivities}
/>
) : (
<></>
)}
{opLeft ? (
<FixedSlot
state={opLeft}
sequence={5}
position={rightPosition}
rotation={opRotation}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearMonsterPlaceInteractivities}
/>
) : (
<></>
)}
{opRight ? (
<FixedSlot
state={opRight}
sequence={6}
position={leftPosition}
rotation={opRotation}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearMonsterPlaceInteractivities}
/>
) : (
<></>
)}
<FixedSlot
state={meLeft}
sequence={5}
position={leftPosition}
rotation={meRotation}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
<FixedSlot
state={meRight}
sequence={6}
position={rightPosition}
rotation={meRotation}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
<FixedSlot
state={opLeft}
sequence={5}
position={rightPosition}
rotation={opRotation}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
<FixedSlot
state={opRight}
sequence={6}
position={leftPosition}
rotation={opRotation}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
</>
);
};
const monsterPositions = (player: number, monsters: CardState[]) => {
const monsterPositions = (
player: number,
monsters: INTERNAL_Snapshot<CardState[]>
) => {
const x = (sequence: number) =>
player == 0 ? left + gap * sequence : -left - gap * sequence;
const y = transform.z / 2 + floating;
......
import * as BABYLON from "@babylonjs/core";
import { useRef } from "react";
import { useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { useClick } from "@/hook";
import { CardState } from "@/reducers/duel/generic";
import {
setCardListModalInfo,
setCardListModalIsOpen,
} from "@/reducers/duel/mod";
import { store } from "@/store";
import { type CardState, messageStore } from "@/stores";
import { interactTypeToString } from "../utils";
......@@ -21,10 +17,10 @@ export const SingleSlot = (props: {
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
}) => {
const snapState = useSnapshot(props.state);
const boxRef = useRef(null);
const dispatch = store.dispatch;
const edgeRender =
props.state.find((item) =>
snapState.find((item) =>
item === undefined ? false : item.idleInteractivities.length > 0
) !== undefined;
const edgesWidth = 2.0;
......@@ -32,31 +28,24 @@ export const SingleSlot = (props: {
useClick(
(_event) => {
if (props.state.length != 0) {
dispatch(
setCardListModalInfo(
props.state
.filter(
(item) => item.occupant !== undefined && item.occupant.id !== 0
)
.map((item) => {
return {
meta: item.occupant,
interactivies: item.idleInteractivities.map((interactivy) => {
return {
desc: interactTypeToString(interactivy.interactType),
response: interactivy.response,
};
}),
};
})
if (snapState.length != 0) {
messageStore.cardListModal.list = snapState
.filter(
(item) => item.occupant !== undefined && item.occupant.id !== 0
)
);
dispatch(setCardListModalIsOpen(true));
.map((item) => ({
meta: item.occupant,
interactivies: item.idleInteractivities.map((interactivy) => ({
desc: interactTypeToString(interactivy.interactType),
response: interactivy.response,
})),
}));
// dispatch(setCardListModalIsOpen(true));
messageStore.cardListModal.isOpen = true;
}
},
boxRef,
[props.state]
[snapState]
);
return (
......@@ -64,11 +53,7 @@ export const SingleSlot = (props: {
name="single-slot"
ref={boxRef}
scaling={
new BABYLON.Vector3(
transform.x,
transform.y,
Depth * props.state.length
)
new BABYLON.Vector3(transform.x, transform.y, Depth * snapState.length)
}
position={props.position}
rotation={props.rotation}
......@@ -81,7 +66,7 @@ export const SingleSlot = (props: {
diffuseTexture={
new BABYLON.Texture(`${NeosConfig.assetsPath}/card_back.jpg`)
}
alpha={props.state.length == 0 ? 0 : 1}
alpha={snapState.length == 0 ? 0 : 1}
/>
</box>
);
......
import { InteractType } from "@/reducers/duel/generic";
import { InteractType } from "@/stores";
export function interactTypeToString(t: InteractType): string {
switch (t) {
......
......@@ -4,22 +4,13 @@ import {
TableOutlined,
} from "@ant-design/icons";
import { Button, Modal } from "antd";
import React, { useContext, useEffect } from "react";
import React, { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useSnapshot } from "valtio";
import { sendHandResult, sendTpResult } from "@/api/ocgcore/ocgHelper";
import { sendHandResult, sendTpResult } from "@/api";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import { selectDuelHsStart } from "@/reducers/duel/mod";
import {
selectHandSelectAble,
selectTpSelectAble,
unSelectHandAble,
unSelectTpAble,
} from "@/reducers/moraSlice";
import { store } from "@/store";
import { valtioContext } from "@/valtioStores";
import { matStore, moraStore } from "@/stores";
const {
automation: { isAiMode, isAiFirst },
......@@ -27,17 +18,11 @@ const {
} = useConfig();
const Mora = () => {
const stateMora = useContext(valtioContext).moraStore;
const snapMora = useSnapshot(stateMora);
const dispatch = store.dispatch;
const selectHandAble = useAppSelector(selectHandSelectAble);
const selectTpAble = useAppSelector(selectTpSelectAble);
const duelHsStart = useAppSelector(selectDuelHsStart);
const snapMora = useSnapshot(moraStore);
const snapMatInitInfo = useSnapshot(matStore.initInfo);
// const selectHandAble = snapMora.selectHandAble;
// const selectTpAble = snapMora.selectTpAble;
// const duelHsStart = snapMora.duelStart;
const selectHandAble = snapMora.selectHandAble;
const selectTpAble = snapMora.selectTpAble;
const navigate = useNavigate();
const { player, passWd, ip } = useParams<{
......@@ -48,21 +33,19 @@ const Mora = () => {
const handleSelectMora = (selected: string) => {
sendHandResult(selected);
dispatch(unSelectHandAble());
stateMora.selectHandAble = false;
moraStore.selectHandAble = false;
};
const handleSelectTp = (isFirst: boolean) => {
sendTpResult(isFirst);
dispatch(unSelectTpAble());
stateMora.selectTpAble = false;
moraStore.selectTpAble = false;
};
useEffect(() => {
// 若对局已经开始,自动跳转
if (duelHsStart) {
if (snapMatInitInfo.me.life > 0) {
navigate(`/duel/${player}/${passWd}/${ip}`);
}
}, [duelHsStart]);
}, [snapMatInitInfo.me]);
useEffect(() => {
if (isAiMode) {
......
......@@ -19,34 +19,18 @@ import {
Space,
Upload,
} from "antd";
import React, { useContext, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import rustInit from "rust-src";
import { useSnapshot } from "valtio";
import YGOProDeck from "ygopro-deck-encode";
import { initStrings, sendHsReady, sendHsStart, sendUpdateDeck } from "@/api";
import { DeckManager, fetchDeck, type IDeck } from "@/api/deck";
import {
sendHsReady,
sendHsStart,
sendUpdateDeck,
} from "@/api/ocgcore/ocgHelper";
import { initStrings } from "@/api/strings";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import socketMiddleWare, { socketCmd } from "@/middleware/socket";
import sqliteMiddleWare, { sqliteCmd } from "@/middleware/sqlite";
import { selectChat } from "@/reducers/chatSlice";
import { initMeExtraDeckMeta } from "@/reducers/duel/extraDeckSlice";
import { selectJoined } from "@/reducers/joinSlice";
import { selectDuelStart } from "@/reducers/moraSlice";
import {
selectIsHost,
selectPlayer0,
selectPlayer1,
} from "@/reducers/playerSlice";
import { store } from "@/store";
import { valtioContext } from "@/valtioStores";
import { store } from "@/stores";
const NeosConfig = useConfig();
......@@ -58,7 +42,7 @@ const {
} = useConfig();
const WaitRoom = () => {
const state = useContext(valtioContext);
const state = store;
const snap = useSnapshot(state);
const params = useParams<{
player?: string;
......@@ -103,21 +87,14 @@ const WaitRoom = () => {
}
}, []);
const dispatch = store.dispatch;
const joined = useAppSelector(selectJoined);
const chat = useAppSelector(selectChat);
const isHost = useAppSelector(selectIsHost);
const player0 = useAppSelector(selectPlayer0);
const player1 = useAppSelector(selectPlayer1);
const duelStart = useAppSelector(selectDuelStart);
const [api, contextHolder] = notification.useNotification();
// const joined = snap.joinStore.value;
// const chat = snap.chatStore.message;
// const isHost = snap.playerStore.isHost;
// const player0 = snap.playerStore.player0;
// const player1 = snap.playerStore.player1;
// const duelStart = snap.moraStore.duelStart;
const joined = snap.joinStore.value;
const chat = snap.chatStore.message;
const isHost = snap.playerStore.isHost;
const player0 = snap.playerStore.player0;
const player1 = snap.playerStore.player1;
const duelStart = snap.moraStore.duelStart;
// FIXME: 这些数据应该从`store`中获取
// TODO: 云卡组
......@@ -160,9 +137,7 @@ const WaitRoom = () => {
const onDeckReady = async (deck: IDeck) => {
sendUpdateDeck(deck);
await dispatch(
initMeExtraDeckMeta({ controler: 0, codes: deck.extra?.reverse() || [] })
);
store.matStore.extraDecks.me.add(deck.extra?.reverse() || []);
setChoseDeck(true);
};
......
import { proxy } from "valtio";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import type { DuelState } from "./types";
export const playMat = proxy<DuelState>({
opMagics: {
inner: Array(5)
.fill(null)
.map((_, i) => ({
location: {
location: ygopro.CardZone.SZONE,
},
idleInteractivities: [],
counters: {},
})),
},
});
import { proxy } from "valtio";
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