Commit ae0cafb9 authored by Chunchi Che's avatar Chunchi Che

Merge branch 'feat/ui/grave' into 'main'

Feat/ui/grave

See merge request mycard/Neos!59
parents 04463686 471d1f17
import { judgeSelf, Cemetery } from "./util";
import {
PayloadAction,
CaseReducer,
createAsyncThunk,
ActionReducerMapBuilder,
} from "@reduxjs/toolkit";
import { DuelState } from "./mod";
import { RootState } from "../../store";
import { fetchCard } from "../../api/cards";
export interface CemeteryState {
cemetery: Cemetery[];
}
// 初始化墓地状态
export const initCemeteryImpl: CaseReducer<DuelState, PayloadAction<number>> = (
state,
action
) => {
const player = action.payload;
if (judgeSelf(player, state)) {
state.meCemetery = { cemetery: [] };
} else {
state.opCemetery = { cemetery: [] };
}
};
// 增加墓地
export const fetchCemeteryMeta = createAsyncThunk(
"duel/fetchCemeteryMeta",
async (param: { controler: number; sequence: number; code: number }) => {
const code = param.code;
const meta = await fetchCard(code);
const response = {
controler: param.controler,
sequence: param.sequence,
meta,
};
return response;
}
);
export const cemeteryCase = (builder: ActionReducerMapBuilder<DuelState>) => {
builder.addCase(fetchCemeteryMeta.pending, (state, action) => {
// Meta结果没返回之前先更新`ID`
const controler = action.meta.arg.controler;
const sequence = action.meta.arg.sequence;
const code = action.meta.arg.code;
const meta = { id: code, data: {}, text: {} };
if (judgeSelf(controler, state)) {
if (state.meCemetery) {
state.meCemetery.cemetery.push({ sequence, meta });
} else {
state.meCemetery = { cemetery: [{ sequence, meta }] };
}
} else {
if (state.opCemetery) {
state.opCemetery.cemetery.push({ sequence, meta });
} else {
state.opCemetery = { cemetery: [{ sequence, meta }] };
}
}
});
builder.addCase(fetchCemeteryMeta.fulfilled, (state, action) => {
const controler = action.payload.controler;
const sequence = action.payload.sequence;
const meta = action.payload.meta;
if (judgeSelf(controler, state)) {
if (state.meCemetery) {
for (const cemetery of state.meCemetery.cemetery) {
if (cemetery.sequence == sequence) {
cemetery.meta = meta;
}
}
}
} else {
if (state.opCemetery) {
for (const cemetery of state.opCemetery.cemetery) {
if (cemetery.sequence == sequence) {
cemetery.meta = meta;
}
}
}
}
});
};
export const selectMeCemetery = (state: RootState) =>
state.duel.meCemetery || { cemetery: [] };
export const selectOpCemetery = (state: RootState) =>
state.duel.opCemetery || { cemetery: [] };
...@@ -23,6 +23,8 @@ import { ...@@ -23,6 +23,8 @@ import {
setCardModalTextImpl, setCardModalTextImpl,
setCardModalImgUrlImpl, setCardModalImgUrlImpl,
setCardModalInteractiviesImpl, setCardModalInteractiviesImpl,
setCardListModalIsOpenImpl,
setCardListModalInfoImpl,
} from "./modalSlice"; } from "./modalSlice";
import { import {
MonsterState, MonsterState,
...@@ -38,6 +40,7 @@ import { ...@@ -38,6 +40,7 @@ import {
clearMagicSelectInfoImpl, clearMagicSelectInfoImpl,
magicCase, magicCase,
} from "./magicSlice"; } from "./magicSlice";
import { CemeteryState, initCemeteryImpl, cemeteryCase } from "./cemeretySlice";
export interface DuelState { export interface DuelState {
selfType?: number; selfType?: number;
...@@ -53,6 +56,9 @@ export interface DuelState { ...@@ -53,6 +56,9 @@ export interface DuelState {
meMagics?: MagicState; // 自己的魔法陷阱区状态 meMagics?: MagicState; // 自己的魔法陷阱区状态
opMagics?: MagicState; // 对手的魔法陷阱区状态 opMagics?: MagicState; // 对手的魔法陷阱区状态
meCemetery?: CemeteryState; // 自己的墓地状态
opCemetery?: CemeteryState; // 对手的墓地状态
meTimeLimit?: TimeLimit; // 自己的计时 meTimeLimit?: TimeLimit; // 自己的计时
opTimeLimit?: TimeLimit; // 对手的计时 opTimeLimit?: TimeLimit; // 对手的计时
...@@ -69,6 +75,7 @@ export interface DuelState { ...@@ -69,6 +75,7 @@ export interface DuelState {
const initialState: DuelState = { const initialState: DuelState = {
modalState: { modalState: {
cardModal: { isOpen: false, interactivies: [] }, cardModal: { isOpen: false, interactivies: [] },
cardListModal: { isOpen: false, list: [] },
}, },
}; };
...@@ -99,17 +106,23 @@ const duelSlice = createSlice({ ...@@ -99,17 +106,23 @@ const duelSlice = createSlice({
addMagicPlaceSelectAble: addMagicPlaceSelectAbleImpl, addMagicPlaceSelectAble: addMagicPlaceSelectAbleImpl,
clearMagicSelectInfo: clearMagicSelectInfoImpl, clearMagicSelectInfo: clearMagicSelectInfoImpl,
// 墓地相关`Reducer`
initCemetery: initCemeteryImpl,
// UI相关`Reducer` // UI相关`Reducer`
setCardModalIsOpen: setCardModalIsOpenImpl, setCardModalIsOpen: setCardModalIsOpenImpl,
setCardModalText: setCardModalTextImpl, setCardModalText: setCardModalTextImpl,
setCardModalImgUrl: setCardModalImgUrlImpl, setCardModalImgUrl: setCardModalImgUrlImpl,
setCardModalInteractivies: setCardModalInteractiviesImpl, setCardModalInteractivies: setCardModalInteractiviesImpl,
setCardListModalIsOpen: setCardListModalIsOpenImpl,
setCardListModalInfo: setCardListModalInfoImpl,
}, },
extraReducers(builder) { extraReducers(builder) {
handsCase(builder); handsCase(builder);
hintCase(builder); hintCase(builder);
monsterCase(builder); monsterCase(builder);
magicCase(builder); magicCase(builder);
cemeteryCase(builder);
}, },
}); });
...@@ -132,6 +145,9 @@ export const { ...@@ -132,6 +145,9 @@ export const {
addMagicPlaceSelectAble, addMagicPlaceSelectAble,
clearMagicSelectInfo, clearMagicSelectInfo,
removeHand, removeHand,
initCemetery,
setCardListModalIsOpen,
setCardListModalInfo,
} = duelSlice.actions; } = duelSlice.actions;
export const selectDuelHsStart = (state: RootState) => { export const selectDuelHsStart = (state: RootState) => {
return state.duel.meInitInfo != null; return state.duel.meInitInfo != null;
......
...@@ -11,6 +11,15 @@ export interface ModalState { ...@@ -11,6 +11,15 @@ export interface ModalState {
imgUrl?: string; imgUrl?: string;
interactivies: { desc: string; response: number }[]; interactivies: { desc: string; response: number }[];
}; };
// 卡牌列表弹窗
cardListModal: {
isOpen: boolean;
list: {
name?: string;
desc?: string;
imgUrl?: string;
}[];
};
} }
// 更新卡牌弹窗打开状态 // 更新卡牌弹窗打开状态
...@@ -49,6 +58,24 @@ export const setCardModalInteractiviesImpl: CaseReducer< ...@@ -49,6 +58,24 @@ export const setCardModalInteractiviesImpl: CaseReducer<
state.modalState.cardModal.interactivies = action.payload; state.modalState.cardModal.interactivies = action.payload;
}; };
// 更新卡牌列表弹窗打开状态
export const setCardListModalIsOpenImpl: CaseReducer<
DuelState,
PayloadAction<boolean>
> = (state, action) => {
state.modalState.cardListModal.isOpen = action.payload;
};
// 更新卡牌列表文本
export const setCardListModalInfoImpl: CaseReducer<
DuelState,
PayloadAction<{ name?: string; desc?: string; imgUrl?: string }[]>
> = (state, action) => {
const list = action.payload;
state.modalState.cardListModal.list = list;
};
export const selectCardModalIsOpen = (state: RootState) => export const selectCardModalIsOpen = (state: RootState) =>
state.duel.modalState.cardModal.isOpen; state.duel.modalState.cardModal.isOpen;
export const selectCardModalName = (state: RootState) => export const selectCardModalName = (state: RootState) =>
...@@ -59,3 +86,7 @@ export const selectCardModalImgUrl = (state: RootState) => ...@@ -59,3 +86,7 @@ export const selectCardModalImgUrl = (state: RootState) =>
state.duel.modalState.cardModal.imgUrl; state.duel.modalState.cardModal.imgUrl;
export const selectCardModalInteractivies = (state: RootState) => export const selectCardModalInteractivies = (state: RootState) =>
state.duel.modalState.cardModal.interactivies; state.duel.modalState.cardModal.interactivies;
export const selectCardListModalIsOpen = (state: RootState) =>
state.duel.modalState.cardListModal.isOpen;
export const selectCardListModalInfo = (state: RootState) =>
state.duel.modalState.cardListModal.list;
...@@ -69,3 +69,8 @@ export interface SlotState { ...@@ -69,3 +69,8 @@ export interface SlotState {
export type Monster = SlotState; export type Monster = SlotState;
export type Magic = SlotState; export type Magic = SlotState;
export interface Cemetery {
sequence: number;
meta: CardMeta;
}
...@@ -4,6 +4,7 @@ import { AppDispatch } from "../../store"; ...@@ -4,6 +4,7 @@ import { AppDispatch } from "../../store";
import { fetchMonsterMeta } from "../../reducers/duel/monstersSlice"; import { fetchMonsterMeta } from "../../reducers/duel/monstersSlice";
import { removeHand } from "../../reducers/duel/mod"; import { removeHand } from "../../reducers/duel/mod";
import { fetchMagicMeta } from "../../reducers/duel/magicSlice"; import { fetchMagicMeta } from "../../reducers/duel/magicSlice";
import { fetchCemeteryMeta } from "../../reducers/duel/cemeretySlice";
export default (move: MsgMove, dispatch: AppDispatch) => { export default (move: MsgMove, dispatch: AppDispatch) => {
const code = move.code; const code = move.code;
...@@ -48,6 +49,17 @@ export default (move: MsgMove, dispatch: AppDispatch) => { ...@@ -48,6 +49,17 @@ export default (move: MsgMove, dispatch: AppDispatch) => {
break; break;
} }
case ygopro.CardZone.GRAVE: {
dispatch(
fetchCemeteryMeta({
controler: to.controler,
sequence: to.sequence,
code,
})
);
break;
}
default: { default: {
console.log(`Unhandled zone type ${to.location}`); console.log(`Unhandled zone type ${to.location}`);
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
setSelfType, setSelfType,
initMonsters, initMonsters,
initMagics, initMagics,
initCemetery,
} from "../../reducers/duel/mod"; } from "../../reducers/duel/mod";
export default ( export default (
...@@ -36,4 +37,6 @@ export default ( ...@@ -36,4 +37,6 @@ export default (
dispatch(initMonsters(1)); dispatch(initMonsters(1));
dispatch(initMagics(0)); dispatch(initMagics(0));
dispatch(initMagics(1)); dispatch(initMagics(1));
dispatch(initCemetery(0));
dispatch(initCemetery(1));
}; };
import React from "react";
import { useAppSelector } from "../../hook";
import { store } from "../../store";
import {
selectCardListModalIsOpen,
selectCardListModalInfo,
} from "../../reducers/duel/modalSlice";
import { setCardListModalIsOpen } from "../../reducers/duel/mod";
import { Modal, List, Popover, Card } from "antd";
const { Meta } = Card;
const CARD_WIDTH = 100;
const CardListModal = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectCardListModalIsOpen);
const list = useAppSelector(selectCardListModalInfo);
const handleOkOrCancel = () => {
dispatch(setCardListModalIsOpen(false));
};
return (
<Modal open={isOpen} onOk={handleOkOrCancel} onCancel={handleOkOrCancel}>
<List
itemLayout="horizontal"
dataSource={list}
renderItem={(item) => (
<Popover
content={
<Card
hoverable
style={{ width: CARD_WIDTH }}
cover={<img alt={item.name} src={item.imgUrl} />}
>
<Meta title={item.name} />
<p>{item.desc}</p>
</Card>
}
>
<List.Item>
<List.Item.Meta title={item.name} description={item.desc} />
</List.Item>
</Popover>
)}
></List>
</Modal>
);
};
export default CardListModal;
...@@ -31,32 +31,30 @@ const CardModal = () => { ...@@ -31,32 +31,30 @@ const CardModal = () => {
}; };
return ( return (
<> <Modal open={isOpen} onOk={handleOkOrCancel} onCancel={handleOkOrCancel}>
<Modal open={isOpen} onOk={handleOkOrCancel} onCancel={handleOkOrCancel}> <Card
<Card hoverable
hoverable style={{ width: CARD_WIDTH }}
style={{ width: CARD_WIDTH }} cover={<img alt={name} src={imgUrl} />}
cover={<img alt={name} src={imgUrl} />} >
> <Meta title={name} />
<Meta title={name} /> <p>{desc}</p>
<p>{desc}</p> </Card>
</Card> {interactivies.map((interactive, idx) => {
{interactivies.map((interactive, idx) => { return (
return ( <Button
<Button key={idx}
key={idx} onClick={() => {
onClick={() => { sendSelectIdleCmdResponse(interactive.response);
sendSelectIdleCmdResponse(interactive.response); dispatch(setCardModalIsOpen(false));
dispatch(setCardModalIsOpen(false)); dispatch(clearHandsInteractivity(0));
dispatch(clearHandsInteractivity(0)); }}
}} >
> {interactive.desc}
{interactive.desc} </Button>
</Button> );
); })}
})} </Modal>
</Modal>
</>
); );
}; };
......
import * as BABYLON from "@babylonjs/core"; import * as BABYLON from "@babylonjs/core";
import * as CONFIG from "../../config/ui"; import * as CONFIG from "../../config/ui";
import { Cemetery } from "../../reducers/duel/util";
import {
selectMeCemetery,
selectOpCemetery,
} from "../../reducers/duel/cemeretySlice";
import { store } from "../../store";
import { useAppSelector } from "../../hook";
import { useClick } from "./hook";
import { useRef } from "react";
import {
setCardListModalInfo,
setCardListModalIsOpen,
} from "../../reducers/duel/mod";
const Cemetery = () => { const shape = CONFIG.CemeterySlotShape();
const shape = CONFIG.CemeterySlotShape(); const depth = 0.02;
const position = new BABYLON.Vector3(
3.2, const Cemeteries = () => {
shape.depth / 2 + CONFIG.Floating, const meCemetery = useAppSelector(selectMeCemetery).cemetery;
-2.0 const opCemetery = useAppSelector(selectOpCemetery).cemetery;
return (
<>
<CCemetery
state={meCemetery}
position={cemeteryPosition(0, meCemetery.length)}
rotation={CONFIG.CardSlotRotation(false)}
/>
<CCemetery
state={opCemetery}
position={cemeteryPosition(1, opCemetery.length)}
rotation={CONFIG.CardSlotRotation(true)}
/>
</>
);
};
const CCemetery = (props: {
state: Cemetery[];
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
}) => {
const boxRef = useRef(null);
const dispatch = store.dispatch;
useClick(
(_event) => {
if (props.state.length != 0) {
dispatch(
setCardListModalInfo(
props.state.map((cemetery) => {
return {
name: cemetery.meta.text.name,
desc: cemetery.meta.text.desc,
imgUrl: `https://cdn02.moecube.com:444/images/ygopro-images-zh-CN/${cemetery.meta.id}.jpg`,
};
})
)
);
dispatch(setCardListModalIsOpen(true));
}
},
boxRef,
[props.state]
); );
const rotation = CONFIG.CemeterySlotRotation();
return ( return (
<box <box
name="cemetery" name="cemetery"
ref={boxRef}
width={shape.width} width={shape.width}
height={shape.height} height={shape.height}
depth={shape.depth} depth={depth * props.state.length}
position={position} position={props.position}
rotation={rotation} rotation={props.rotation}
> >
<standardMaterial <standardMaterial
name="cemetery-mat" name="cemetery-mat"
diffuseColor={CONFIG.CemeteryColor()} diffuseTexture={
new BABYLON.Texture(`http://localhost:3030/images/card_back.jpg`)
}
alpha={props.state.length == 0 ? 0 : 1}
/> />
</box> </box>
); );
}; };
export default Cemetery; const cemeteryPosition = (player: number, cemeteryLength: number) => {
const x = player == 0 ? 3.2 : -3.2;
const y = (depth * cemeteryLength) / 2 + CONFIG.Floating;
const z = player == 0 ? -2.0 : 2.0;
return new BABYLON.Vector3(x, y, z);
};
export default Cemeteries;
...@@ -11,6 +11,8 @@ import Magics from "./magics"; ...@@ -11,6 +11,8 @@ import Magics from "./magics";
import Field from "./field"; import Field from "./field";
import Deck from "./deck"; import Deck from "./deck";
import Exclusion from "./exclusion"; import Exclusion from "./exclusion";
import Cemeteries from "./cemetery";
import CardListModal from "./cardListModal";
// Ref: https://github.com/brianzinn/react-babylonjs/issues/126 // Ref: https://github.com/brianzinn/react-babylonjs/issues/126
const NeosDuel = () => ( const NeosDuel = () => (
...@@ -27,6 +29,7 @@ const NeosDuel = () => ( ...@@ -27,6 +29,7 @@ const NeosDuel = () => (
<Magics /> <Magics />
<Field /> <Field />
<Deck /> <Deck />
<Cemeteries />
<Exclusion /> <Exclusion />
<Ground /> <Ground />
</Provider> </Provider>
...@@ -35,6 +38,7 @@ const NeosDuel = () => ( ...@@ -35,6 +38,7 @@ const NeosDuel = () => (
)} )}
</ReactReduxContext.Consumer> </ReactReduxContext.Consumer>
<CardModal /> <CardModal />
<CardListModal />
<HintNotification /> <HintNotification />
</> </>
); );
......
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