Commit f55f56b5 authored by Chunchi Che's avatar Chunchi Che

fix

parent 0756e4c7
import { Alert as AntdAlert } from "antd";
import React from "react";
import { useNavigate } from "react-router-dom";
import { sendSurrender } from "@/api/ocgcore/ocgHelper";
import { useAppSelector } from "@/hook";
import { selectUnimplemented } from "@/reducers/duel/mod";
export const Alert = () => {
const unimplemented = useAppSelector(selectUnimplemented);
const navigate = useNavigate();
return (
<>
{unimplemented ? (
<AntdAlert
message={`Unimplemented message with code=${unimplemented}`}
description="It seems that there's something unimplemented by Neos. Sincerely apologize for that. Contact use to fix this issue: <ccc@neos.moe>"
showIcon
type="error"
closable
banner
afterClose={() => {
// 发送投降信号
sendSurrender();
navigate("/");
}}
/>
) : (
<></>
)}
</>
);
};
import { Button, Drawer, List } from "antd";
import React from "react";
import { sendSelectIdleCmdResponse } from "@/api/ocgcore/ocgHelper";
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";
const NeosConfig = useConfig();
const CARD_WIDTH = 100;
export const CardListModal = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectCardListModalIsOpen);
const list = useAppSelector(selectCardListModalInfo);
const handleOkOrCancel = () => {
dispatch(setCardListModalIsOpen(false));
};
return (
<Drawer open={isOpen} onClose={handleOkOrCancel}>
<List
itemLayout="horizontal"
dataSource={list}
renderItem={(item) => (
<List.Item
actions={item.interactivies.map((interactivy, idx) => (
<Button
key={idx}
onClick={() => {
sendSelectIdleCmdResponse(interactivy.response);
dispatch(setCardListModalIsOpen(false));
dispatch(clearAllIdleInteractivities(0));
dispatch(clearAllIdleInteractivities(1));
}}
>
{interactivy.desc}
</Button>
))}
extra={
<img
alt={item.meta?.text.name}
src={`${NeosConfig.cardImgUrl}/${item.meta?.id}.jpg`}
style={{ width: CARD_WIDTH }}
/>
}
>
<List.Item.Meta
title={item.meta?.text.name}
description={item.meta?.text.desc}
/>
</List.Item>
)}
></List>
</Drawer>
);
};
import Icon, { StarOutlined } from "@ant-design/icons";
import { Button, Card, Col, Modal, Row } from "antd";
import React from "react";
import { sendSelectIdleCmdResponse } from "@/api/ocgcore/ocgHelper";
import { fetchStrings } from "@/api/strings";
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";
import { ReactComponent as BattleSvg } from "neos-assets/battle-axe.svg";
import { ReactComponent as DefenceSvg } from "neos-assets/checked-shield.svg";
import {
Attribute2StringCodeMap,
extraCardTypes,
Race2StringCodeMap,
Type2StringCodeMap,
} from "../../../common";
const NeosConfig = useConfig();
const { Meta } = Card;
const CARD_WIDTH = 240;
export const CardModal = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectCardModalIsOpen);
const meta = useAppSelector(selectCardModalMeta);
const name = meta?.text.name;
const types = meta?.data.type;
const race = meta?.data.race;
const attribute = meta?.data.attribute;
const level = meta?.data.level;
const desc = meta?.text.desc;
const atk = meta?.data.atk;
const def = meta?.data.def;
const counters = useAppSelector(selectCardModalCounters);
const imgUrl = meta?.id
? `${NeosConfig.cardImgUrl}/${meta.id}.jpg`
: undefined;
const interactivies = useAppSelector(selectCardModalInteractivies);
const handleOkOrCancel = () => {
dispatch(setCardModalIsOpen(false));
};
return (
<Modal open={isOpen} onOk={handleOkOrCancel} onCancel={handleOkOrCancel}>
<Card
hoverable
style={{ width: CARD_WIDTH }}
cover={<img alt={name} src={imgUrl} />}
>
<Meta title={name} />
<AttLine
types={extraCardTypes(types || 0)}
race={race}
attribute={attribute}
/>
<AtkLine level={level} atk={atk} def={def} />
<CounterLine counters={counters} />
<p>{desc}</p>
</Card>
{interactivies.map((interactive, idx) => {
return (
<Button
key={idx}
onClick={() => {
sendSelectIdleCmdResponse(interactive.response);
dispatch(setCardModalIsOpen(false));
dispatch(clearAllIdleInteractivities(0));
dispatch(clearAllIdleInteractivities(1));
}}
>
{interactive.desc}
</Button>
);
})}
</Modal>
);
};
const AtkLine = (props: { level?: number; atk?: number; def?: number }) => (
<Row gutter={8}>
{props.level ? (
<Col>
<StarOutlined />
{props.level}
</Col>
) : (
<></>
)}
{props.atk ? (
<Col>
<Icon component={BattleSvg} />
<a>{props.atk}</a>
</Col>
) : (
<></>
)}
<Col>/</Col>
{props.def ? (
<Col>
<Icon component={DefenceSvg} />
<a>{props.def}</a>
</Col>
) : (
<></>
)}
</Row>
);
const AttLine = (props: {
types: number[];
race?: number;
attribute?: number;
}) => {
const race = props.race
? fetchStrings("!system", Race2StringCodeMap.get(props.race) || 0)
: undefined;
const attribute = props.attribute
? fetchStrings("!system", Attribute2StringCodeMap.get(props.attribute) || 0)
: undefined;
const types = props.types
.map((t) => fetchStrings("!system", Type2StringCodeMap.get(t) || 0))
.join("|");
return (
<Row gutter={8}>
<Col>{`[${types}]`}</Col>
{race ? <Col>{race}</Col> : <></>}
<Col>/</Col>
{attribute ? <Col>{attribute}</Col> : <></>}
</Row>
);
};
const CounterLine = (props: { counters: { [type: number]: number } }) => {
const counters = [];
for (const counterType in props.counters) {
const count = props.counters[counterType];
if (count > 0) {
const counterStr = fetchStrings("!counter", `0x${counterType}`);
counters.push(`${counterStr}: ${count}`);
}
}
return counters.length > 0 ? (
<Row gutter={8}>
{counters.map((counter) => (
<Col>{counter}</Col>
))}
</Row>
) : (
<></>
);
};
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 {
sendSelectCardResponse,
sendSelectChainResponse,
} from "@/api/ocgcore/ocgHelper";
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 { DragModal } from "./DragModal";
const NeosConfig = useConfig();
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 [response, setResponse] = useState<number[]>([]);
const defaultValue: number[] = [];
const hint = useAppSelector(selectHint);
const preHintMsg = hint?.esHint || "";
const selectHintMsg = hint?.esSelectHint || "请选择卡片";
// TODO: 这里可以考虑更好地封装
const sendResponseHandler = (
handlerName: string | undefined,
response: number[]
) => {
switch (handlerName) {
case "sendSelectChainResponse": {
sendSelectChainResponse(response[0]);
break;
}
case "sendSelectCardResponse": {
sendSelectCardResponse(response);
break;
}
default: {
}
}
};
return (
<DragModal
title={`${preHintMsg} ${selectHintMsg} ${min}-${max}`}
open={isOpen}
closable={false}
footer={
<>
<Button
disabled={response.length < min || response.length > max}
onClick={() => {
sendResponseHandler(onSubmit, response);
dispatch(setCheckCardModalIsOpen(false));
dispatch(resetCheckCardModal());
}}
onFocus={() => {}}
onBlur={() => {}}
>
submit
</Button>
{cancelAble ? (
<Button
onClick={() => {
if (cancelResponse) {
sendResponseHandler(onSubmit, [cancelResponse]);
}
dispatch(setCheckCardModalIsOpen(false));
dispatch(resetCheckCardModal());
}}
onFocus={() => {}}
onBlur={() => {}}
>
cancel
</Button>
) : (
<></>
)}
</>
}
width={800}
>
<CheckCard.Group
multiple
bordered
size="small"
defaultValue={defaultValue}
onChange={(value) => {
// @ts-ignore
setResponse(value);
}}
>
{tabs.map((tab, idx) => {
return (
<Row key={idx}>
{tab.options.map((option, idx) => {
return (
<Col span={4} key={idx}>
<HoverCheckCard
hoverContent={option.effectDesc}
title={option.meta.text.name}
description={option.meta.text.desc}
style={{ width: 120 }}
cover={
<img
alt={option.meta.id.toString()}
src={
option.meta.id
? `${NeosConfig.cardImgUrl}/${option.meta.id}.jpg`
: `${NeosConfig.assetsPath}/card_back.jpg`
}
style={{ width: 100 }}
/>
}
value={option.response}
/>
</Col>
);
})}
</Row>
);
})}
</CheckCard.Group>
</DragModal>
);
};
const HoverCheckCard = (props: CheckCardProps & { hoverContent?: string }) => {
const [hover, setHover] = useState(false);
const onMouseEnter = () => setHover(true);
const onMouseLeave = () => setHover(false);
return (
<>
<CheckCard {...props} />
{props.hoverContent ? (
<Popover content={<p>{props.hoverContent}</p>} open={hover}>
<Button
icon={<ThunderboltOutlined />}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
></Button>
</Popover>
) : (
<></>
)}
</>
);
};
import { CheckCard } from "@ant-design/pro-components";
import { Button, Card, Col, Row } from "antd";
import React from "react";
import { sendSelectUnselectCardResponse } from "@/api/ocgcore/ocgHelper";
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 { DragModal } from "./DragModal";
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 preHintMsg = hint?.esHint || "";
const selectHintMsg = hint?.esSelectHint || "请选择卡片";
const onFinishOrCancel = () => {
sendSelectUnselectCardResponse({ cancel_or_finish: true });
dispatch(setCheckCardModalV2IsOpen(false));
dispatch(resetCheckCardModalV2());
dispatch(setCheckCardModalV2ResponseAble(false));
};
return (
<DragModal
title={`${preHintMsg} ${selectHintMsg} ${min}-${max}`}
open={isOpen}
closable={false}
footer={
<>
<Button
disabled={!finishable || !responseable}
onClick={onFinishOrCancel}
>
finish
</Button>
<Button
disabled={!cancelable || !responseable}
onClick={onFinishOrCancel}
>
cancel
</Button>
</>
}
width={800}
>
<CheckCard.Group
bordered
size="small"
onChange={(value) => {
if (responseable) {
dispatch(setCheckCardModalV2IsOpen(false));
// @ts-ignore
sendSelectUnselectCardResponse({ selected_ptr: value });
dispatch(setCheckCardModalV2ResponseAble(false));
}
}}
>
<Row>
{selectableOptions.map((option, idx) => {
return (
<Col span={4} key={idx}>
<CheckCard
title={option.name}
description={option.desc}
style={{ width: 120 }}
cover={
<img
alt={option.code.toString()}
src={`${NeosConfig.cardImgUrl}/${option.code}.jpg`}
style={{ width: 100 }}
/>
}
value={option.response}
/>
</Col>
);
})}
</Row>
</CheckCard.Group>
<p>已经选择的卡片</p>
<Row>
{selectedOptions.map((option, idx) => {
return (
<Col span={4} key={idx}>
<Card
hoverable
style={{ width: 120 }}
cover={
<img
alt={option.code.toString()}
src={`${NeosConfig.cardImgUrl}/${option.code}.jpg`}
/>
}
/>
</Col>
);
})}
</Row>
</DragModal>
);
};
import { CheckCard } from "@ant-design/pro-components";
import { Button, Card, Col, Row } from "antd";
import React, { useState } from "react";
import { sendSelectCardResponse } from "@/api/ocgcore/ocgHelper";
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 { DragModal } from "./DragModal";
const NeosConfig = useConfig();
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 [selectedOptions, setSelectedOptions] = useState([]);
const overflow = state.overflow;
const LevelSum = state.allLevel;
const Level1Sum = mustSelectOptions
.concat(selectedOptions)
.map((option) => option.level1)
.reduce((sum, current) => sum + current, 0);
const Level2Sum = mustSelectOptions
.concat(selectedOptions)
.map((option) => option.level2)
.reduce((sum, current) => sum + current, 0);
const hint = useAppSelector(selectHint);
const preHintMsg = hint?.esHint || "";
const selectHintMsg = hint?.esSelectHint || "请选择卡片";
const responseable =
(overflow
? Level1Sum >= LevelSum || Level2Sum >= LevelSum
: Level1Sum == LevelSum || Level2Sum == LevelSum) &&
selectedOptions.length <= max &&
selectedOptions.length >= min;
const onFinish = () => {
sendSelectCardResponse(
mustSelectOptions.concat(selectedOptions).map((option) => option.response)
);
dispatch(setCheckCardModalV3IsOpen(false));
dispatch(resetCheckCardModalV3());
dispatch(setCheckCardModalV3ResponseAble(false));
};
return (
<DragModal
title={`${preHintMsg} ${selectHintMsg} ${min}-${max}`}
open={isOpen}
closable={false}
footer={
<>
<Button disabled={!responseable} onClick={onFinish}>
finish
</Button>
</>
}
width={800}
>
<CheckCard.Group
bordered
size="small"
multiple={true}
onChange={(values: any) => {
console.log(values);
setSelectedOptions(values);
}}
>
<Row>
{selectAbleOptions.map((option, idx) => {
return (
<Col span={4} key={idx}>
<CheckCard
title={option.meta.text.name}
description={option.meta.text.desc}
style={{ width: 120 }}
cover={
<img
alt={option.meta.id.toString()}
src={`${NeosConfig.cardImgUrl}/${option.meta.id}.jpg`}
style={{ width: 100 }}
/>
}
value={option}
/>
</Col>
);
})}
</Row>
</CheckCard.Group>
<p>必须选择的卡片</p>
<Row>
{mustSelectOptions.map((option, idx) => {
return (
<Col span={4} key={idx}>
<Card
hoverable
style={{ width: 120 }}
cover={
<img
alt={option.meta.id.toString()}
src={`${NeosConfig.cardImgUrl}/${option.meta.id}.jpg`}
/>
}
/>
</Col>
);
})}
</Row>
</DragModal>
);
};
import { Button, Card, Col, InputNumber, Row } from "antd";
import React, { useState } from "react";
import { sendSelectCounterResponse } from "@/api/ocgcore/ocgHelper";
import { fetchStrings } from "@/api/strings";
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 { DragModal } from "./DragModal";
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 [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());
};
return (
<DragModal
title={`请移除${min}个${counterName}`}
open={isOpen}
closable={false}
footer={
<Button disabled={!finishable} onClick={onFinish}>
finish
</Button>
}
>
<Row>
{options.map((option, idx) => {
return (
<Col span={4} key={idx}>
<Card
hoverable
style={{ width: 120 }}
cover={
<img
alt={option.code.toString()}
src={`${NeosConfig.cardImgUrl}/${option.code}.jpg`}
/>
}
>
<InputNumber
min={0}
max={option.max}
defaultValue={0}
onChange={(value) => {
let newSelected = [...selected];
newSelected[idx] = value || 0;
setSelected(newSelected);
}}
/>
</Card>
</Col>
);
})}
</Row>
</DragModal>
);
};
// 经过封装的可拖拽`Modal`
import { Modal, ModalProps } from "antd";
import React, { useRef, useState } from "react";
import type { DraggableData, DraggableEvent } from "react-draggable";
import Draggable from "react-draggable";
export interface DragModalProps extends ModalProps {}
export const DragModal = (props: DragModalProps) => {
const dragRef = useRef<HTMLDivElement>(null);
const [bounds, setBounds] = useState({
left: 0,
top: 0,
bottom: 0,
right: 0,
});
const onStart = (_event: DraggableEvent, uiData: DraggableData) => {
const { clientWidth, clientHeight } = window.document.documentElement;
const targetRect = dragRef.current?.getBoundingClientRect();
if (!targetRect) {
return;
}
setBounds({
left: -targetRect.left + uiData.x,
right: clientWidth - (targetRect.right - uiData.x),
top: -targetRect.top + uiData.y,
bottom: clientHeight - (targetRect.bottom - uiData.y),
});
};
return (
<Modal
{...props}
modalRender={(modal) => (
<Draggable bounds={bounds} onStart={onStart}>
<div ref={dragRef}>{modal}</div>
</Draggable>
)}
>
{props.children}
</Modal>
);
};
import { notification } from "antd";
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
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 { useConfig } from "@/config";
const NeosConfig = useConfig();
export const HintNotification = () => {
const hint = useAppSelector(selectHint);
const currentPhase = useAppSelector(selectCurrentPhase);
const waiting = useAppSelector(selectWaiting);
const result = useAppSelector(selectDuelResult);
const navigate = useNavigate();
const [api, contextHolder] = notification.useNotification({
maxCount: NeosConfig.ui.hint.maxCount,
});
useEffect(() => {
if (hint && hint.msg) {
api.info({
message: `${hint.msg}`,
placement: "bottom",
});
}
}, [hint?.msg]);
useEffect(() => {
if (currentPhase) {
api.info({
message: `<当前阶段>${currentPhase}`,
placement: "topRight",
});
}
}, [currentPhase]);
useEffect(() => {
if (waiting) {
api.info({
message: "...等待对方行动中...",
placement: "top",
duration: NeosConfig.ui.hint.waitingDuration,
});
}
}, [waiting]);
useEffect(() => {
if (result) {
const message =
result == MsgWin.ActionType.Win
? "胜利"
: MsgWin.ActionType.Defeated
? "失败"
: "未知结果";
api.info({
message,
placement: "bottom",
onClose() {
navigate("/");
},
});
}
}, [result]);
return <>{contextHolder}</>;
};
import { CheckCard } from "@ant-design/pro-components";
import { Button } from "antd";
import React, { useState } from "react";
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 { DragModal } from "./DragModal";
export const OptionModal = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectOptionModalIsOpen);
const options = useAppSelector(selectOptionModalOptions);
const [selected, setSelected] = useState<number | undefined>(undefined);
return (
<DragModal
title="请选择需要发动的效果"
open={isOpen}
closable={false}
footer={
<Button
disabled={selected === undefined}
onClick={() => {
if (selected !== undefined) {
sendSelectOptionResponse(selected);
dispatch(setOptionModalIsOpen(false));
dispatch(resetOptionModal());
}
}}
>
submit
</Button>
}
>
<CheckCard.Group
bordered
size="small"
onChange={(value) => {
// @ts-ignore
setSelected(value);
}}
>
{options.map((option, idx) => (
<CheckCard key={idx} title={option.msg} value={option.response} />
))}
</CheckCard.Group>
</DragModal>
);
};
import Icon from "@ant-design/icons";
import { Button, Modal, Space } from "antd";
import React, { useState } from "react";
import {
sendSelectBattleCmdResponse,
sendSelectIdleCmdResponse,
sendSurrender,
} from "@/api/ocgcore/ocgHelper";
import { useAppSelector } from "@/hook";
import {
clearAllIdleInteractivities,
setEnableBp,
setEnableEp,
setEnableM2,
} from "@/reducers/duel/mod";
import {
selectCurrentPhase,
selectEnableBp,
selectEnableEp,
selectEnableM2,
} from "@/reducers/duel/phaseSlice";
import { store } from "@/store";
import { ReactComponent as BattleSvg } from "neos-assets/crossed-swords.svg";
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";
const IconSize = "150%";
const SpaceSize = 16;
const PhaseButton = (props: {
text: string;
enable: boolean;
onClick: () => void;
icon?: React.ReactNode;
}) => {
return (
<Button
icon={props.icon}
disabled={!props.enable}
onClick={props.onClick}
size="large"
>
{props.text}
</Button>
);
};
export const Phase = () => {
const dispatch = store.dispatch;
const enableBp = useAppSelector(selectEnableBp);
const enableM2 = useAppSelector(selectEnableM2);
const enableEp = useAppSelector(selectEnableEp);
const currentPhase = useAppSelector(selectCurrentPhase);
const [modalOpen, setModalOpen] = useState(false);
const response =
currentPhase === "BATTLE_START" ||
currentPhase === "BATTLE_STEP" ||
currentPhase === "DAMAGE" ||
currentPhase === "DAMAGE_GAL" ||
currentPhase === "BATTLE"
? 3
: 7;
const onBp = () => {
dispatch(clearAllIdleInteractivities(0));
dispatch(clearAllIdleInteractivities(0));
sendSelectIdleCmdResponse(6);
dispatch(setEnableBp(false));
};
const onM2 = () => {
dispatch(clearAllIdleInteractivities(0));
dispatch(clearAllIdleInteractivities(0));
sendSelectBattleCmdResponse(2);
dispatch(setEnableM2(false));
};
const onEp = () => {
dispatch(clearAllIdleInteractivities(0));
dispatch(clearAllIdleInteractivities(0));
sendSelectIdleCmdResponse(response);
dispatch(setEnableEp(false));
};
const onSurrender = () => {
setModalOpen(true);
};
return (
<Space wrap size={SpaceSize}>
<PhaseButton
icon={<Icon component={BattleSvg} style={{ fontSize: IconSize }} />}
enable={enableBp}
text="战斗阶段"
onClick={onBp}
/>
<PhaseButton
icon={<Icon component={Main2Svg} style={{ fontSize: IconSize }} />}
enable={enableM2}
text="主要阶段2"
onClick={onM2}
/>
<PhaseButton
icon={<Icon component={EpSvg} style={{ fontSize: IconSize }} />}
enable={enableEp}
text="结束回合"
onClick={onEp}
/>
<PhaseButton
icon={<Icon component={SurrenderSvg} style={{ fontSize: IconSize }} />}
enable={true}
text="投降"
onClick={onSurrender}
/>
<Modal
title="是否确认要投降?"
open={modalOpen}
closable={false}
footer={
<>
<Button
onClick={() => {
sendSurrender();
setModalOpen(false);
}}
>
Yes
</Button>
<Button
onClick={() => {
setModalOpen(false);
}}
>
No
</Button>
</>
}
/>
</Space>
);
};
import { CheckCard } from "@ant-design/pro-components";
import { Button } from "antd";
import React, { useState } from "react";
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 { DragModal } from "./DragModal";
export const PositionModal = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectPositionModalIsOpen);
const positions = useAppSelector(selectPositionModalPositions);
const [selected, setSelected] = useState<ygopro.CardPosition | undefined>(
undefined
);
return (
<DragModal
title="请选择表示形式"
open={isOpen}
closable={false}
footer={
<Button
disabled={selected === undefined}
onClick={() => {
if (selected !== undefined) {
sendSelectPositionResponse(selected);
dispatch(setPositionModalIsOpen(false));
dispatch(resetPositionModal());
}
}}
>
submit
</Button>
}
>
<CheckCard.Group
bordered
size="small"
onChange={(value) => {
// @ts-ignore
setSelected(value);
}}
>
{positions.map((position, idx) => (
<CheckCard
key={idx}
title={cardPositionToChinese(position)}
value={position}
/>
))}
</CheckCard.Group>
</DragModal>
);
};
function cardPositionToChinese(position: ygopro.CardPosition): string {
switch (position) {
case ygopro.CardPosition.FACEUP_ATTACK: {
return "正面攻击形式";
}
case ygopro.CardPosition.FACEUP_DEFENSE: {
return "正面防守形式";
}
case ygopro.CardPosition.FACEDOWN_ATTACK: {
return "背面攻击形式";
}
case ygopro.CardPosition.FACEDOWN_DEFENSE: {
return "背面防守形式";
}
default: {
return "[?]";
}
}
}
import { SendOutlined } from "@ant-design/icons";
import { Button, Col, Input, Row } from "antd";
import React, { useState } from "react";
import { sendChat } from "@/api/ocgcore/ocgHelper";
export const SendBox = () => {
const [content, setContent] = useState("");
return (
<>
<Row>
<Input.TextArea
placeholder="Message to sent..."
autoSize={{ minRows: 3, maxRows: 4 }}
value={content}
onChange={(e) => {
setContent(e.target.value);
}}
/>
</Row>
<Row>
<Col>
<Button
icon={<SendOutlined />}
onClick={() => {
sendChat(content);
setContent("");
}}
disabled={!content}
/>
</Col>
</Row>
</>
);
};
import {
closestCenter,
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Button, Card, Modal } from "antd";
import React, { useEffect, useState } from "react";
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";
const NeosConfig = useConfig();
export 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 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 (
<Modal
title="请为下列卡牌排序"
open={isOpen}
closable={false}
footer={<Button onClick={onFinish}>finish</Button>}
>
<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>
</Modal>
);
};
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>
);
};
import { UserOutlined } from "@ant-design/icons";
import { CheckCard } from "@ant-design/pro-components";
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();
const Config = NeosConfig.ui.status;
const avatarSize = 40;
const ME_VALUE = "myself";
const OP_VALUE = "opponent";
export const PlayerStatus = () => {
const meInfo = useAppSelector(selectMeInitInfo);
const opInfo = useAppSelector(selectOpInitInfo);
const waiting = useAppSelector(selectWaiting) || false;
return (
<CheckCard.Group
bordered
style={{ height: `${NeosConfig.ui.layout.header.height}` }}
value={waiting ? OP_VALUE : ME_VALUE}
>
<CheckCard
avatar={
<Avatar
size={avatarSize}
style={{ backgroundColor: Config.opAvatarColor }}
icon={<UserOutlined />}
/>
}
title={OP_VALUE}
description={`Lp: ${opInfo?.life || 0}`}
value={OP_VALUE}
style={{
position: "absolute",
left: `${NeosConfig.ui.layout.sider.width}px`,
}}
/>
<CheckCard
avatar={
<Avatar
size={avatarSize}
style={{ backgroundColor: Config.meAvatarColor }}
icon={<UserOutlined />}
/>
}
title={ME_VALUE}
description={`Lp: ${meInfo?.life || 0}`}
value={ME_VALUE}
style={{
position: "absolute",
right: "0px",
}}
/>
</CheckCard.Group>
);
};
import { MessageOutlined } from "@ant-design/icons";
import { Timeline, TimelineItemProps } from "antd";
import React, { useEffect, useState } from "react";
import { useAppSelector } from "@/hook";
import { selectChat } from "@/reducers/chatSlice";
export const DuelTimeLine = () => {
const [items, setItems] = useState<TimelineItemProps[]>([]);
const chat = useAppSelector(selectChat);
useEffect(() => {
setItems((prev) =>
prev.concat([
{
dot: <MessageOutlined />,
children: chat,
color: "green",
},
])
);
}, [chat]);
return <Timeline items={items} />;
};
import { Button } from "antd";
import React from "react";
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 { DragModal } from "./DragModal";
export const YesNoModal = () => {
const dispatch = store.dispatch;
const isOpen = useAppSelector(selectYesNoModalIsOpen);
const msg = useAppSelector(selectYesNOModalMsg);
const hint = useAppSelector(selectHint);
const preHintMsg = hint?.esHint || "";
return (
<DragModal
title={`${preHintMsg} ${msg}`}
open={isOpen}
closable={false}
footer={
<>
<Button
onClick={() => {
sendSelectEffectYnResponse(true);
dispatch(setYesNoModalIsOpen(false));
}}
>
Yes
</Button>
<Button
onClick={() => {
sendSelectEffectYnResponse(false);
dispatch(setYesNoModalIsOpen(false));
}}
>
No
</Button>
</>
}
/>
);
};
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();
export const CommonDeck = () => {
const meDeck = useAppSelector(selectMeDeck).inner;
const opDeck = useAppSelector(selectOpDeck).inner;
return (
<>
<SingleSlot
state={meDeck}
position={deckPosition(0, meDeck.length)}
rotation={cardSlotRotation(false)}
/>
<SingleSlot
state={opDeck}
position={deckPosition(1, opDeck.length)}
rotation={cardSlotRotation(true)}
/>
</>
);
};
const deckPosition = (player: number, deckLength: number) => {
const x = player == 0 ? 3.2 : -3.2;
const y = (Depth * deckLength) / 2 + NeosConfig.ui.card.floating;
const z = player == 0 ? -3.3 : 3.3;
return new BABYLON.Vector3(x, y, z);
};
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();
export const ExtraDeck = () => {
const meExtraDeck = useAppSelector(selectMeExtraDeck).inner;
const opExtraDeck = useAppSelector(selectOpExtraDeck).inner;
return (
<>
<SingleSlot
state={meExtraDeck}
position={extraDeckPosition(0, meExtraDeck.length)}
rotation={cardSlotRotation(false)}
/>
<SingleSlot
state={opExtraDeck}
position={extraDeckPosition(1, opExtraDeck.length)}
rotation={cardSlotRotation(true)}
/>
</>
);
};
const extraDeckPosition = (player: number, deckLength: number) => {
const x = player == 0 ? -3.3 : 3.3;
const y = (Depth & deckLength) / 2 + NeosConfig.ui.card.floating;
const z = player == 0 ? -3.3 : 3.3;
return new BABYLON.Vector3(x, y, z);
};
import * as BABYLON from "@babylonjs/core";
import { useConfig } from "@/config";
import { useAppSelector } from "@/hook";
import { selectMeMagics, selectOpMagics } from "@/reducers/duel/magicSlice";
import { clearMagicPlaceInteractivities } from "@/reducers/duel/mod";
import { cardSlotRotation } from "../utils";
import { FixedSlot } from "./FixedSlot";
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
);
return (
<>
{meField ? (
<FixedSlot
state={meField}
sequence={0}
position={fieldPosition(0)}
rotation={cardSlotRotation(false)}
clearPlaceInteractivitiesAction={clearMagicPlaceInteractivities}
/>
) : (
<></>
)}
{opField ? (
<FixedSlot
state={opField}
sequence={0}
position={fieldPosition(1)}
rotation={cardSlotRotation(true)}
clearPlaceInteractivitiesAction={clearMagicPlaceInteractivities}
/>
) : (
<></>
)}
</>
);
};
const fieldPosition = (player: number) => {
const x = player == 0 ? -3.3 : 3.3;
const y = Depth / 2 + NeosConfig.ui.card.floating;
const z = player == 0 ? -2.0 : 2.0;
return new BABYLON.Vector3(x, y, z);
};
import * as BABYLON from "@babylonjs/core";
import { ActionCreatorWithPayload } from "@reduxjs/toolkit";
import { useRef } from "react";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { sendSelectPlaceResponse } from "@/api/ocgcore/ocgHelper";
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";
import { interactTypeToString } from "../utils";
const NeosConfig = useConfig();
const transform = NeosConfig.ui.card.transform;
const defenceRotation = NeosConfig.ui.card.defenceRotation;
const cardDefenceRotation = new BABYLON.Vector3(
defenceRotation.x,
defenceRotation.y,
defenceRotation.z
);
export const FixedSlot = (props: {
state: CardState;
sequence: number;
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
deffenseRotation?: BABYLON.Vector3;
clearPlaceInteractivitiesAction: ActionCreatorWithPayload<number, string>;
}) => {
const planeRef = useRef(null);
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
? 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;
useClick(
(_event) => {
if (props.state.placeInteractivities) {
sendSelectPlaceResponse(props.state.placeInteractivities.response);
dispatch(props.clearPlaceInteractivitiesAction(0));
dispatch(props.clearPlaceInteractivitiesAction(1));
} else if (props.state.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));
// 侧边栏展示超量素材信息
if (
props.state.overlay_materials &&
props.state.overlay_materials.length > 0
) {
dispatch(
setCardListModalInfo(
props.state.overlay_materials?.map((overlay) => {
return {
meta: overlay,
interactivies: [],
};
}) || []
)
);
dispatch(setCardListModalIsOpen(true));
}
}
},
planeRef,
[props.state]
);
return (
<plane
name={`fixedslot-${props.sequence}`}
ref={planeRef}
width={transform.x}
height={transform.y}
position={props.position}
rotation={rotation}
enableEdgesRendering
edgesWidth={
props.state.placeInteractivities ||
props.state.idleInteractivities.length > 0
? edgesWidth
: 0
}
edgesColor={edgesColor}
>
<standardMaterial
name={`fixedslot-mat-${props.sequence}`}
diffuseTexture={
props.state.occupant
? faceDown
? new BABYLON.Texture(`${NeosConfig.assetsPath}/card_back.jpg`)
: new BABYLON.Texture(
`${NeosConfig.cardImgUrl}/${props.state.occupant.id}.jpg`
)
: new BABYLON.Texture(`${NeosConfig.assetsPath}/card_slot.png`)
}
alpha={props.state.occupant ? 1 : 0}
></standardMaterial>
</plane>
);
};
import * as BABYLON from "@babylonjs/core";
import { useEffect, useRef, useState } from "react";
import { useHover } from "react-babylonjs";
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 { animated, useSpring } from "../spring";
import { interactTypeToString, zip } from "../utils";
const NeosConfig = useConfig();
const groundShape = NeosConfig.ui.ground;
const left = -(groundShape.width / 2);
const handShape = NeosConfig.ui.card.transform;
const rotation = NeosConfig.ui.card.handRotation;
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);
return (
<>
{zip(meHands, meHandPositions).map(([hand, position], idx) => {
return (
<CHand
key={idx}
state={hand}
sequence={idx}
position={position}
rotation={handRotation}
cover={(id) => `${NeosConfig.cardImgUrl}/${id}.jpg`}
/>
);
})}
{zip(opHands, opHandPositions).map(([hand, position], idx) => {
return (
<CHand
key={idx}
state={hand}
sequence={idx}
position={position}
rotation={handRotation}
cover={(_) => `${NeosConfig.assetsPath}/card_back.jpg`}
/>
);
})}
</>
);
};
const CHand = (props: {
state: CardState;
sequence: number;
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
cover: (id: number) => string;
}) => {
const hoverScale = new BABYLON.Vector3(
hoverScaling.x,
hoverScaling.y,
hoverScaling.z
);
const defaultScale = new BABYLON.Vector3(1, 1, 1);
const edgesWidth = 2.0;
const edgesColor = BABYLON.Color4.FromColor3(BABYLON.Color3.Yellow());
const planeRef = useRef(null);
const state = props.state;
const [hovered, setHovered] = useState(false);
const position = props.position;
const dispatch = store.dispatch;
const [spring, api] = useSpring(
() => ({
from: {
position,
},
config: {
mass: 1.0,
tension: 170,
friction: 900,
precision: 0.01,
velocity: 0.0,
clamp: true,
duration: 2000,
},
}),
[]
);
useEffect(() => {
api.start({
position,
});
}, [position]);
useHover(
() => setHovered(true),
() => setHovered(false),
planeRef
);
useClick(
() => {
if (state.occupant) {
dispatch(setCardModalMeta(state.occupant));
}
dispatch(
setCardModalInteractivies(
state.idleInteractivities.map((interactive) => {
return {
desc: interactTypeToString(interactive.interactType),
response: interactive.response,
};
})
)
);
dispatch(setCardModalIsOpen(true));
},
planeRef,
[state]
);
return (
// @ts-ignore
<animated.transformNode name="">
<animated.plane
name={`hand-${props.sequence}`}
ref={planeRef}
width={handShape.x}
height={handShape.y}
scaling={hovered ? hoverScale : defaultScale}
position={spring.position}
rotation={props.rotation}
enableEdgesRendering
edgesWidth={
state.idleInteractivities.length > 0 || state.placeInteractivities
? edgesWidth
: 0
}
edgesColor={edgesColor}
>
<animated.standardMaterial
name={`hand-mat-${props.sequence}`}
diffuseTexture={
new BABYLON.Texture(props.cover(state.occupant?.id || 0))
}
/>
</animated.plane>
</animated.transformNode>
);
};
const handPositons = (player: number, hands: CardState[]) => {
const gap = groundShape.width / (hands.length - 1);
const x = (idx: number) =>
player == 0 ? left + gap * idx : -left - gap * idx;
const y = handShape.y / 2;
const z =
player == 0 ? -(groundShape.height / 2) - 1 : groundShape.height / 2 + 1;
return hands.map((_, idx) => new BABYLON.Vector3(x(idx), y, z));
};
import * as BABYLON from "@babylonjs/core";
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 { cardSlotRotation, zip } from "../utils";
import { FixedSlot } from "./FixedSlot";
const NeosConfig = useConfig();
// TODO: use config
const left = -2.15;
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);
return (
<>
{zip(meMagics, meMagicPositions)
.slice(0, 5)
.map(([magic, position], sequence) => {
return (
<FixedSlot
state={magic}
key={sequence}
sequence={sequence}
position={position}
rotation={cardSlotRotation(false)}
clearPlaceInteractivitiesAction={clearMagicPlaceInteractivities}
/>
);
})}
{zip(opMagics, opMagicPositions)
.slice(0, 5)
.map(([magic, position], sequence) => {
return (
<FixedSlot
state={magic}
key={sequence}
sequence={sequence}
position={position}
rotation={cardSlotRotation(true)}
clearPlaceInteractivitiesAction={clearMagicPlaceInteractivities}
/>
);
})}
</>
);
};
const magicPositions = (player: number, magics: CardState[]) => {
const x = (sequence: number) =>
player == 0 ? left + gap * sequence : -left - gap * sequence;
const y = transform.z / 2 + NeosConfig.ui.card.floating;
const z = player == 0 ? -2.6 : 2.6;
return magics.map((_, sequence) => new BABYLON.Vector3(x(sequence), y, z));
};
import "react-babylonjs";
import * as BABYLON from "@babylonjs/core";
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 { cardSlotDefenceRotation, cardSlotRotation, zip } from "../utils";
import { FixedSlot } from "./FixedSlot";
const NeosConfig = useConfig();
const transform = NeosConfig.ui.card.transform;
const floating = NeosConfig.ui.card.floating;
const left = -2.15; // TODO: config
const gap = 1.05;
export const Monsters = () => {
const meMonsters = useAppSelector(selectMeMonsters).inner;
const meMonsterPositions = monsterPositions(0, meMonsters);
const opMonsters = useAppSelector(selectOpMonsters).inner;
const opMonsterPositions = monsterPositions(1, opMonsters);
return (
<>
{zip(meMonsters, meMonsterPositions)
.slice(0, 5)
.map(([monster, position], sequence) => (
<FixedSlot
state={monster}
key={sequence}
sequence={sequence}
position={position}
rotation={cardSlotRotation(false)}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearMonsterPlaceInteractivities}
/>
))}
{zip(opMonsters, opMonsterPositions)
.slice(0, 5)
.map(([monster, position], sequence) => (
<FixedSlot
state={monster}
key={sequence}
sequence={sequence}
position={position}
rotation={cardSlotRotation(true)}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearMonsterPlaceInteractivities}
/>
))}
<ExtraMonsters meMonsters={meMonsters} opMonsters={opMonsters} />
</>
);
};
// TODO: use props and redux
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 leftPosition = new BABYLON.Vector3(-1.1, transform.z / 2 + floating, 0);
const rightPosition = new BABYLON.Vector3(1.1, transform.z / 2 + floating, 0);
const meRotation = cardSlotRotation(false);
const opRotation = cardSlotRotation(true);
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}
/>
) : (
<></>
)}
</>
);
};
const monsterPositions = (player: number, monsters: CardState[]) => {
const x = (sequence: number) =>
player == 0 ? left + gap * sequence : -left - gap * sequence;
const y = transform.z / 2 + floating;
const z = player == 0 ? -1.35 : 1.35;
return monsters.map((_, sequence) => new BABYLON.Vector3(x(sequence), y, z));
};
import * as BABYLON from "@babylonjs/core";
import { useRef } from "react";
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 { interactTypeToString } from "../utils";
const NeosConfig = useConfig();
const transform = NeosConfig.ui.card.transform;
export const Depth = 0.005;
export const SingleSlot = (props: {
state: CardState[];
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
}) => {
const boxRef = useRef(null);
const dispatch = store.dispatch;
const edgeRender =
props.state.find((item) =>
item === undefined ? false : item.idleInteractivities.length > 0
) !== undefined;
const edgesWidth = 2.0;
const edgesColor = BABYLON.Color4.FromColor3(BABYLON.Color3.Yellow());
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,
};
}),
};
})
)
);
dispatch(setCardListModalIsOpen(true));
}
},
boxRef,
[props.state]
);
return (
<box
name="single-slot"
ref={boxRef}
scaling={
new BABYLON.Vector3(
transform.x,
transform.y,
Depth * props.state.length
)
}
position={props.position}
rotation={props.rotation}
enableEdgesRendering
edgesWidth={edgeRender ? edgesWidth : 0}
edgesColor={edgesColor}
>
<standardMaterial
name="single-slot-mat"
diffuseTexture={
new BABYLON.Texture(`${NeosConfig.assetsPath}/card_back.jpg`)
}
alpha={props.state.length == 0 ? 0 : 1}
/>
</box>
);
};
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";
import NeosLayout from "./Layout";
import {
Alert,
CardListModal,
CardModal,
CheckCardModal,
CheckCardModalV2,
CheckCardModalV3,
CheckCounterModal,
DuelTimeLine,
HintNotification,
OptionModal,
Phase,
PlayerStatus,
PositionModal,
SendBox,
SortCardModal,
YesNoModal,
} from "./Message";
import {
ExtraDeck,
Field,
Graveyard,
Hands,
Magics,
Monsters,
} from "./PlayMat";
import { BanishedZone } from "./PlayMat/BanishedZone";
import { CommonDeck } from "./PlayMat/Deck";
const NeosConfig = useConfig();
// Ref: https://github.com/brianzinn/react-babylonjs/issues/126
const NeosDuel = () => {
return (
<>
<Alert />
<NeosLayout
sider={<NeosSider />}
header={<PlayerStatus />}
content={<NeosCanvas />}
footer={<Phase />}
/>
<CardModal />
<CardListModal />
<HintNotification />
<CheckCardModal />
<YesNoModal />
<PositionModal />
<OptionModal />
<CheckCardModalV2 />
<CheckCardModalV3 />
<CheckCounterModal />
<SortCardModal />
</>
);
};
const NeosSider = () => (
<>
<Row>
<DuelTimeLine />
</Row>
<Row>
<SendBox />
</Row>
</>
);
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>
);
const Camera = () => (
<freeCamera
name="duel-camera"
position={new BABYLON.Vector3(0, 8, -10)}
target={BABYLON.Vector3.Zero()}
></freeCamera>
);
const Light = () => (
<hemisphericLight
name="duel-light"
direction={new BABYLON.Vector3(1, 2.5, 1)}
intensity={0.7}
></hemisphericLight>
);
const Ground = () => {
const shape = NeosConfig.ui.ground;
const texture = new BABYLON.Texture(`${NeosConfig.assetsPath}/newfield.png`);
texture.hasAlpha = true;
return (
<ground name="duel-ground" width={shape.width} height={shape.height}>
<standardMaterial
name="duel-ground-mat"
diffuseTexture={texture}
></standardMaterial>
</ground>
);
};
export default NeosDuel;
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