Commit 2ce5bffb authored by Chunchi Che's avatar Chunchi Che

Merge branch 'feat/sort_card' into 'main'

Feat/sort card

See merge request mycard/Neos!160
parents 1bac6c95 f83338e9
Pipeline #21058 passed with stages
in 23 minutes and 28 seconds
neos-protobuf @ e477dc8a
Subproject commit ff89698e6fb11829a7170fdf38de646e720bd96d
Subproject commit e477dc8ab6cc6ef898b34499f3e6ecd3b0d99481
......@@ -9,6 +9,8 @@
"version": "0.1.0",
"dependencies": {
"@ant-design/pro-components": "^2.3.49",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@react-spring/shared": "^9.6.1",
"@react-spring/types": "^9.6.1",
"@react-spring/web": "^9.6.1",
......@@ -2328,6 +2330,75 @@
"node": ">=10"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/accessibility/node_modules/tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/@dnd-kit/core": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
"dependencies": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core/node_modules/tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.7",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable/node_modules/tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities/node_modules/tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
......@@ -28814,6 +28885,69 @@
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz",
"integrity": "sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ=="
},
"@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"requires": {
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
}
}
},
"@dnd-kit/core": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
"requires": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
}
}
},
"@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"requires": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
}
}
},
"@dnd-kit/utilities": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
"requires": {
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
}
}
},
"@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
This diff is collapsed.
......@@ -11,6 +11,7 @@ import adaptSelectOptionResponse from "./selectOption";
import adaptSelectBattleCmdResponse from "./selectBattleCmd";
import adaptSelectUnselectCardResponse from "./selectUnselectCard";
import adaptSelectCounterResponse from "./selectCounter";
import adaptSortCardResponse from "./sortCard";
/*
* CTOS CTOS_RESPONSE
......@@ -80,6 +81,11 @@ export default class CtosResponsePacket extends YgoProPacket {
break;
}
case "sort_card": {
extraData = adaptSortCardResponse(response.sort_card);
break;
}
default: {
break;
}
......
import { ygopro } from "../../../idl/ocgcore";
// @ts-ignore
import { BufferWriter } from "rust-src";
export default (response: ygopro.CtosGameMsgResponse.SortCardResponse) => {
const writer = new BufferWriter();
for (const index of response.sorted_index) {
writer.writeUint8(index);
}
return writer.toArray();
};
......@@ -55,3 +55,4 @@ export const MSG_SELECT_SUM = 23;
export const MSG_ADD_COUNTER = 101;
export const MSG_REMOVE_COUNTER = 102;
export const MSG_SELECT_COUNTER = 22;
export const MSG_SORT_CARD = 25;
......@@ -31,6 +31,7 @@ import MsgSelectSum from "./selectSum";
import MsgAddCounter from "./addCounter";
import MsgRemoveCounter from "./removeCounter";
import MsgSelectCounter from "./selectCounter";
import MsgSortCard from "./sortCard";
import PENETRATE from "./penetrate";
/*
......@@ -184,6 +185,11 @@ export default class GameMsgAdapter implements StocAdapter {
break;
}
case GAME_MSG.MSG_SORT_CARD: {
gameMsg.sort_card = MsgSortCard(gameData);
break;
}
default: {
gameMsg.unimplemented = new ygopro.StocGameMessage.MsgUnimplemented({
command: func,
......
import { ygopro } from "../../../idl/ocgcore";
import { BufferReaderExt } from "../../bufferIO";
import MsgSortCard = ygopro.StocGameMessage.MsgSortCard;
/*
*
* Msg Sort Card
*
* @param - TODO
* @usage - TODO
* */
export default (data: Uint8Array) => {
const reader = new BufferReaderExt(data);
const player = reader.inner.readUint8();
const msg = new MsgSortCard({
player,
options: [],
});
const count = reader.inner.readUint8();
for (let i = 0; i < count; i++) {
const code = reader.inner.readUint32();
const location = reader.readCardShortLocation();
msg.options.push(
new MsgSortCard.Info({
code,
location,
response: i,
})
);
}
return msg;
};
......@@ -282,3 +282,16 @@ export function sendSelectCounterResponse(counts: number[]) {
socketMiddleWare({ cmd: socketCmd.SEND, payload });
}
export function sendSortCardResponse(sortedIndexes: number[]) {
const response = new ygopro.YgoCtosMsg({
ctos_response: new ygopro.CtosGameMsgResponse({
sort_card: new ygopro.CtosGameMsgResponse.SortCardResponse({
sorted_index: sortedIndexes,
}),
}),
});
const payload = new GameMsgResponse(response).serialize();
socketMiddleWare({ cmd: socketCmd.SEND, payload });
}
......@@ -63,6 +63,9 @@ import {
setCardModalCountersImpl,
setCheckCounterImpl,
clearCheckCounterImpl,
setSortCardModalIsOpenImpl,
resetSortCardModalImpl,
sortCardModalCase,
} from "./modal/mod";
import {
MonsterState,
......@@ -190,6 +193,10 @@ const initialState: DuelState = {
isOpen: false,
options: [],
},
sortCardModal: {
isOpen: false,
options: [],
},
},
};
......@@ -286,6 +293,8 @@ const duelSlice = createSlice({
setCardModalCounters: setCardModalCountersImpl,
setCheckCounter: setCheckCounterImpl,
clearCheckCounter: clearCheckCounterImpl,
setSortCardModalIsOpen: setSortCardModalIsOpenImpl,
resetSortCardModal: resetSortCardModalImpl,
// 通用的`Reducer`
clearAllIdleInteractivities: clearAllIdleInteractivitiesImpl,
......@@ -321,6 +330,7 @@ const duelSlice = createSlice({
optionModalCase(builder);
checkCardModalV2Case(builder);
checkCardModalV3Case(builder);
sortCardModalCase(builder);
},
});
......@@ -401,6 +411,8 @@ export const {
setCardModalCounters,
setCheckCounter,
clearCheckCounter,
setSortCardModalIsOpen,
resetSortCardModal,
} = duelSlice.actions;
export const selectDuelHsStart = (state: RootState) => {
return state.duel.meInitInfo != null;
......
......@@ -103,6 +103,14 @@ export interface ModalState {
max: number;
}[];
};
// 卡牌排序弹窗
sortCardModal: {
isOpen: boolean;
options: {
meta: CardMeta;
response: number;
}[];
};
}
export * from "./cardModalSlice";
......@@ -114,3 +122,4 @@ export * from "./optionModalSlice";
export * from "./checkCardModalV2Slice";
export * from "./checkCardModalV3Slice";
export * from "./checkCounterModalSlice";
export * from "./sortCardModalSlice";
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;
......@@ -28,6 +28,7 @@ import onMsgSelectSum from "./selectSum";
import onMsgSelectTribute from "./selectTribute";
import onMsgUpdateCounter from "./updateCounter";
import onMsgSelectCounter from "./selectCounter";
import onMsgSortCard from "./sortCard";
import { setWaiting } from "../../reducers/duel/mod";
const ActiveList = [
......@@ -187,6 +188,11 @@ export default function handleGameMsg(pb: ygopro.YgoStocMsg) {
break;
}
case "sort_card": {
onMsgSortCard(msg.sort_card, dispatch);
break;
}
case "unimplemented": {
onUnimplemented(msg.unimplemented, dispatch);
......
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;
export default (sortCard: MsgSortCard, dispatch: AppDispatch) => {
for (const option of sortCard.options) {
dispatch(fetchSortCardMeta(option.toObject()));
}
dispatch(setSortCardModalIsOpen(true));
};
......@@ -28,6 +28,7 @@ import PlayerStatus from "./status";
import Alert from "./alert";
import CheckCardModalV3 from "./checkCardModalV3";
import CheckCounterModal from "./checkCounterModal";
import SortCardModal from "./sortCardModal";
// Ref: https://github.com/brianzinn/react-babylonjs/issues/126
const NeosDuel = () => {
......@@ -50,6 +51,7 @@ const NeosDuel = () => {
<CheckCardModalV2 />
<CheckCardModalV3 />
<CheckCounterModal />
<SortCardModal />
</>
);
};
......
import React, { useEffect, useRef, useState } from "react";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useAppSelector } from "../../hook";
import { selectSortCardModal } from "../../reducers/duel/modal/sortCardModalSlice";
import { sendSortCardResponse } from "../../api/ocgcore/ocgHelper";
import { store } from "../../store";
import { resetSortCardModal } from "../../reducers/duel/mod";
import DragModal from "./dragModal";
import { Button, Card } from "antd";
import { CardMeta } from "../../api/cards";
import NeosConfig from "../../../neos.config.json";
const SortCardModal = () => {
const dispatch = store.dispatch;
const state = useAppSelector(selectSortCardModal);
const isOpen = state.isOpen;
const options = state.options;
const [items, setItems] = useState(options);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const draggleRef = useRef<HTMLDivElement>(null);
const onFinish = () => {
sendSortCardResponse(items.map((item) => item.response));
dispatch(resetSortCardModal());
};
const onDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
setItems((items) => {
const oldIndex = items.findIndex((item) => item.response == active.id);
const newIndex = items.findIndex((item) => item.response === over?.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
useEffect(() => {
setItems(options);
}, [options]);
return (
<DragModal
modalProps={{
title: "请为下列卡牌排序",
open: isOpen,
closable: false,
footer: <Button onClick={onFinish}>finish</Button>,
}}
dragRef={draggleRef}
draggable={false}
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
>
<SortableContext
items={items.map((item) => item.response)}
strategy={verticalListSortingStrategy}
>
{items.map((item) => (
<SortableItem
key={item.response}
id={item.response}
meta={item.meta}
/>
))}
</SortableContext>
</DndContext>
</DragModal>
);
};
const SortableItem = (props: { id: number; meta: CardMeta }) => {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: props.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
<Card
style={{ width: 100 }}
cover={
<img
alt={props.meta.id.toString()}
src={`${NeosConfig.cardImgUrl}/${props.meta.id}.jpg`}
/>
}
/>
</div>
);
};
export default SortCardModal;
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