Commit 7ace8dc5 authored by timel's avatar timel Committed by Chunchi Che

feat: progress 50%

parent 8e322240
neos-protobuf @ b67b483c
Subproject commit 975e4a815f7fc394247f5af6ccb22a0742379f33
Subproject commit b67b483ce9db064611619a2e1a8b8767b100d035
......@@ -24,16 +24,20 @@ import zhCN from "antd/locale/zh_CN";
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { ProConfigProvider } from "@ant-design/pro-provider";
import Neos from "./ui/Neos";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<BrowserRouter>
<ConfigProvider theme={{ algorithm: theme.darkAlgorithm }} locale={zhCN}>
<Neos />
<ProConfigProvider dark>
<Neos />
</ProConfigProvider>
</ConfigProvider>
</BrowserRouter>
);
......@@ -2,11 +2,11 @@ import { ygopro } from "@/api";
import MsgSelectCard = ygopro.StocGameMessage.MsgSelectCard;
import { fetchCheckCardMeta, messageStore } from "@/stores";
export default (selectCard: MsgSelectCard) => {
const cancelable = selectCard.cancelable;
const min = selectCard.min;
const max = selectCard.max;
const cards = selectCard.cards;
import { displaySelectActionsModal } from "@/ui/Duel/Message/NewSelectActionModal";
import { fetchCheckCardMeta as FIXME_fetchCheckCardMeta } from "../utils";
export default async (selectCard: MsgSelectCard) => {
const { cancelable, min, max, cards } = selectCard;
// TODO: handle release_param
messageStore.selectCardActions.min = min;
......@@ -22,4 +22,15 @@ export default (selectCard: MsgSelectCard) => {
}
messageStore.selectCardActions.isValid = true;
messageStore.selectCardActions.isOpen = true;
const { selecteds, mustSelects, selectables } =
await FIXME_fetchCheckCardMeta(cards);
await displaySelectActionsModal({
cancelable,
min,
max,
selecteds,
mustSelects,
selectables,
});
};
......@@ -5,11 +5,13 @@ import {
fetchSelectHintMeta,
messageStore,
} from "@/stores";
import { displaySelectActionsModal } from "@/ui/Duel/Message/NewSelectActionModal";
import { fetchCheckCardMeta as FIXME_fetchCheckCardMeta } from "../utils";
const NeosConfig = useConfig();
type MsgSelectChain = ygopro.StocGameMessage.MsgSelectChain;
export default (selectChain: MsgSelectChain) => {
export default async (selectChain: MsgSelectChain) => {
const spCount = selectChain.special_count;
const forced = selectChain.forced;
const _hint0 = selectChain.hint0;
......@@ -83,7 +85,17 @@ export default (selectChain: MsgSelectChain) => {
});
messageStore.selectCardActions.isValid = true;
messageStore.selectCardActions.isOpen = true;
const { selecteds, mustSelects, selectables } =
await FIXME_fetchCheckCardMeta(chains);
await displaySelectActionsModal({
isChain: true,
cancelable: !forced,
min: 1,
max: 1,
selecteds,
mustSelects,
selectables,
});
break;
}
case 4: {
......
import { ygopro } from "@/api";
import { fetchCheckCardMeta, messageStore } from "@/stores";
import { displaySelectActionsModal } from "@/ui/Duel/Message/NewSelectActionModal";
import { fetchCheckCardMeta as FIXME_fetchCheckCardMeta } from "../utils";
type MsgSelectSum = ygopro.StocGameMessage.MsgSelectSum;
export default (selectSum: MsgSelectSum) => {
export default async (selectSum: MsgSelectSum) => {
messageStore.selectCardActions.overflow = selectSum.overflow != 0;
messageStore.selectCardActions.totalLevels = selectSum.level_sum;
messageStore.selectCardActions.min = selectSum.min;
......@@ -18,4 +20,24 @@ export default (selectSum: MsgSelectSum) => {
messageStore.selectCardActions.isValid = true;
messageStore.selectCardActions.isOpen = true;
const {
selecteds: selecteds1,
mustSelects: mustSelect1,
selectables: selectable1,
} = await FIXME_fetchCheckCardMeta(selectSum.must_select_cards, false, true);
const {
selecteds: selecteds2,
mustSelects: mustSelect2,
selectables: selectable2,
} = await FIXME_fetchCheckCardMeta(selectSum.selectable_cards);
await displaySelectActionsModal({
overflow: selectSum.overflow !== 0,
totalLevels: selectSum.level_sum,
min: selectSum.min,
max: selectSum.max,
selecteds: [...selecteds1, ...selecteds2],
mustSelects: [...mustSelect1, ...mustSelect2],
selectables: [...selectable1, ...selectable2],
});
};
import { ygopro } from "@/api";
import { fetchCheckCardMeta, messageStore } from "@/stores";
import { displaySelectActionsModal } from "@/ui/Duel/Message/NewSelectActionModal";
import { fetchCheckCardMeta as FIXME_fetchCheckCardMeta } from "../utils";
type MsgSelectTribute = ygopro.StocGameMessage.MsgSelectTribute;
export default (selectTribute: MsgSelectTribute) => {
export default async (selectTribute: MsgSelectTribute) => {
// TODO: 当玩家选择卡数大于`max`时,是否也合法?
messageStore.selectCardActions.overflow = true;
messageStore.selectCardActions.totalLevels = 0;
......@@ -16,4 +17,16 @@ export default (selectTribute: MsgSelectTribute) => {
messageStore.selectCardActions.isValid = true;
messageStore.selectCardActions.isOpen = true;
const { selecteds, mustSelects, selectables } =
await FIXME_fetchCheckCardMeta(selectTribute.selectable_cards);
await displaySelectActionsModal({
overflow: true,
totalLevels: 0,
min: selectTribute.min,
max: selectTribute.max,
selecteds,
mustSelects,
selectables,
});
};
import { ygopro } from "@/api";
import { fetchCheckCardMeta, messageStore } from "@/stores";
import { displaySelectActionsModal } from "@/ui/Duel/Message/NewSelectActionModal";
import { fetchCheckCardMeta as FIXME_fetchCheckCardMeta } from "../utils";
type MsgSelectUnselectCard = ygopro.StocGameMessage.MsgSelectUnselectCard;
export default async ({
......@@ -27,4 +28,25 @@ export default async ({
messageStore.selectCardActions.isValid = true;
messageStore.selectCardActions.isOpen = true;
const {
selecteds: selecteds1,
mustSelects: mustSelect1,
selectables: selectable1,
} = await FIXME_fetchCheckCardMeta(selectableCards);
const {
selecteds: selecteds2,
mustSelects: mustSelect2,
selectables: selectable2,
} = await FIXME_fetchCheckCardMeta(selectedCards, true);
await displaySelectActionsModal({
finishable,
cancelable,
min: min,
max: max,
single: true,
selecteds: [...selecteds1, ...selecteds2],
mustSelects: [...mustSelect1, ...mustSelect2],
selectables: [...selectable1, ...selectable2],
});
};
import type { ygopro } from "@/api";
import { fetchCard, getCardStr } from "@/api/cards";
import { cardStore } from "@/stores";
import type { Option } from "@/ui/Duel/Message/NewSelectActionModal";
const helper = async (
{
code,
location,
level1,
level2,
response,
effectDescCode,
}: {
code: number;
location: ygopro.CardLocation;
level1?: number;
level2?: number;
response: number;
effectDescCode?: number;
},
selecteds: Option[],
mustSelects: Option[],
selectables: Option[],
selected?: boolean,
mustSelect?: boolean
) => {
const controller = location.controller;
const newID =
code != 0
? code
: cardStore.at(location.zone, controller, location.sequence)?.code || 0;
const meta = await fetchCard(newID);
const effectDesc = effectDescCode
? getCardStr(meta, effectDescCode & 0xf)
: undefined;
const newOption = {
meta,
location: location.toObject(),
level1,
level2,
effectDesc,
response,
};
if (selected) {
selecteds.push(newOption);
} else if (mustSelect) {
mustSelects.push(newOption);
} else {
selectables.push(newOption);
}
};
export const fetchCheckCardMeta = async (
cards: {
code: number;
location: ygopro.CardLocation;
level1?: number;
level2?: number;
response: number;
effectDescCode?: number;
}[],
selected?: boolean,
mustSelect?: boolean
) => {
const selecteds: Option[] = [];
const mustSelects: Option[] = [];
const selectables: Option[] = [];
for (const card of cards) {
await helper(
card,
selecteds,
mustSelects,
selectables,
selected,
mustSelect
); // TODO: 研究下改成并行
}
return { selecteds, mustSelects, selectables };
};
export * from "./fetchCheckCardMeta";
......@@ -33,7 +33,7 @@ table {
body {
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
background-color: #202022;
font: 87.5%/1.5em "Open Sans", sans-serif;
display: flex;
margin: 0;
......@@ -89,3 +89,8 @@ p {
width: 100%;
max-width: 1000px;
}
img {
user-select: none;
-webkit-user-drag: none;
}
......@@ -14,10 +14,18 @@ import {
YesNoModal,
} from "./Message";
import { LifeBar, Mat, Menu } from "./PlayMat";
import { NewSelectActionsModal } from "./Message/NewSelectActionModal";
const NeosDuel = () => {
return (
<>
<NewSelectActionsModal
isValid
isChain
selecteds={[]}
selectables={[]}
mustSelects={[]}
/>
<Alert />
<Menu />
<LifeBar />
......
.neos-modal {
position: fixed;
left: 0;
right: 0;
top: 0 !important;
bottom: 0 !important;
top: -webkit-fill-available;
margin: auto;
height: fit-content;
transition: 0.3s;
}
.neos-modal-mini {
top: 100% !important;
bottom: 0 !important;
transform: translateY(calc(50% - 66px));
}
import { Button, Modal, type ModalProps } from "antd";
import { type FC, useRef, useState, type CSSProperties } from "react";
import { MinusOutlined } from "@ant-design/icons";
import classNames from "classnames";
import "./index.scss";
interface Props extends ModalProps {
canBeMinimized?: boolean;
}
export const NewModal: FC<Props> = (props) => {
const { canBeMinimized = true } = props;
const [mini, setMini] = useState(false);
return (
<Modal
className={classNames("neos-modal", { "neos-modal-mini": mini })}
centered
onCancel={() => setMini(!mini)}
closeIcon={<MinusOutlined />}
bodyStyle={{ padding: "10px 0" }}
{...props}
/>
);
};
.checkcard-container {
position: relative;
// padding-left: 10px;
// &::after {
// position: absolute;
// width: 3px;
// height: 100%;
// content: "";
// z-index: 1;
// left: 0;
// top: 0;
// background-color: rgb(0, 54, 189);
// }
.btns {
width: 100%;
top: 50%;
display: flex;
justify-content: space-between;
padding: 0 10px;
box-sizing: border-box;
height: 0;
button {
transform: translateY(-50%);
}
}
.ant-pro-checkcard {
border-radius: 4px;
overflow: hidden;
}
// 多选卡片的样式
.ant-pro-checkcard-checked {
&::before {
position: absolute;
width: 100%;
height: 100%;
content: "";
z-index: 1;
background-color: #0023bf32;
box-shadow: 0 0 0 2px #0087e6 inset;
}
&::after {
display: none;
}
}
}
import { CheckCard, CheckCardProps } from "@ant-design/pro-components";
import {
Button,
Card,
Col,
Popover,
Row,
Tabs,
Segmented,
Space,
Typography,
} from "antd";
import { type FC, useState, useEffect } from "react";
import { useSnapshot, proxy } from "valtio";
import {
fetchStrings,
sendSelectMultiResponse,
sendSelectSingleResponse,
} from "@/api";
import type { CardMeta, ygopro } from "@/api";
import { useConfig } from "@/config";
import { matStore } from "@/stores";
import { groupBy } from "../../utils";
import { NewModal } from "../NewModal";
import { YgoCard } from "@/ui/Shared";
import "./index.scss";
const CANCEL_RESPONSE = -1;
const FINISH_RESPONSE = -1;
const defaultProps = {
isOpen: false,
title: "是否要进行连锁", // 模态框标题
isValid: false, // FIXME 看起来是最小化用的,看情况是否需要删掉
isChain: false,
min: 0,
max: 0,
single: true,
selecteds: [] as Option[], // 最少选择多少卡
selectables: [] as Option[], // 最多选择多少卡
mustSelects: [] as Option[], // 单选
cancelable: false, // 能否取消
finishable: false, // 选择足够了之后,能否确认
totalLevels: 0, // 需要的总等级数(用于同调/仪式/...)
overflow: false, // 选择等级时候,是否可以溢出
};
const localStore = proxy(defaultProps);
export const NewSelectActionsModal: FC = () => {
const {
title,
isOpen,
isValid,
isChain,
min,
max,
single,
selecteds,
selectables,
mustSelects,
cancelable,
finishable,
totalLevels,
overflow,
} = useSnapshot(localStore);
const [response, setResponse] = useState<Option[]>([]);
const [submitable, setSubmitable] = useState(false);
const hint = useSnapshot(matStore.hint);
const preHintMsg = hint?.esHint || "";
const selectHintMsg = hint?.esSelectHint || "请选择卡片";
// 判断是否可以提交
useEffect(() => {
const [sumLevel1, sumLevel2] = (["level1", "level2"] as const).map((key) =>
[...mustSelects, ...response]
.map((option) => option[key] || 0)
.reduce((sum, current) => sum + current, 0)
);
const levelMatched = overflow
? sumLevel1 >= totalLevels || sumLevel2 >= totalLevels
: sumLevel1 === totalLevels || sumLevel2 === totalLevels;
setSubmitable(
single
? response.length == 1
: response.length >= min && response.length <= max && levelMatched
);
}, [response.length]);
const grouped = groupBy(selectables, (option) => option.location?.zone!);
const zoneOptions = grouped.map((x) => ({
value: x[0],
label: fetchStrings("!system", x[0] + 1000),
}));
const [selectedZone, setSelectedZone] = useState(zoneOptions[0]?.value);
const onSubmit = () => {
const values = mustSelects
.concat(response)
.map((option) => option.response);
if (isChain) {
sendSelectSingleResponse(values[0]);
} else {
sendSelectMultiResponse(values);
}
rs();
};
const onFinish = () => {
sendSelectSingleResponse(FINISH_RESPONSE);
rs();
};
const onCancel = () => {
sendSelectSingleResponse(CANCEL_RESPONSE);
rs();
};
const [submitText, finishText, cancelText] = [1211, 1296, 1295].map((n) =>
fetchStrings("!system", n)
);
return (
<NewModal
title={title}
width={600}
okButtonProps={{
disabled: !submitable,
}}
open={isOpen}
footer={
<>
<Button danger disabled={!cancelable} onClick={onCancel}>
{cancelText}
</Button>
<Button type="dashed" disabled={!finishable} onClick={onFinish}>
{finishText}
</Button>
<Button type="primary" disabled={!submitable} onClick={onSubmit}>
{submitText}
</Button>
</>
}
>
<div className="check-container">
<Space
direction="vertical"
style={{ width: "100%", overflow: "hidden" }}
>
<Selector
zoneOptions={zoneOptions}
selectedZone={selectedZone}
onChange={setSelectedZone as any}
/>
{grouped.map((options, i) => (
<div className="checkcard-container" key={i}>
<CheckCard.Group
onChange={setResponse as any}
// TODO 考虑如何设置默认值,比如只有一个的,就直接选中
multiple
style={{
display: "grid",
gridTemplateColumns: "repeat(6, 1fr)",
gap: 10,
}}
>
{options[1].map((card, j) => (
<CheckCard
cover={<YgoCard code={card.meta.id} />}
style={{
width: 80,
aspectRatio: 5.9 / 8.6,
marginInlineEnd: 0,
marginBlockEnd: 0,
flexShrink: 0,
}}
key={j}
value={card}
/>
))}
</CheckCard.Group>
</div>
))}
</Space>
</div>
</NewModal>
);
};
/** 选择区域 */
const Selector: FC<{
zoneOptions: {
value: ygopro.CardZone;
label: string;
}[];
selectedZone: ygopro.CardZone;
onChange: (value: ygopro.CardZone) => void;
}> = ({ zoneOptions, selectedZone, onChange }) =>
zoneOptions.length > 1 ? (
<Segmented
block
options={zoneOptions}
style={{ margin: "10px 0" }}
value={selectedZone}
onChange={onChange as any}
/>
) : (
<></>
);
export interface Option {
// card id
meta: CardMeta;
location?: ygopro.CardLocation;
// 效果
effectDesc?: string;
// 作为素材的cost,比如同调召唤的星级
level1?: number;
level2?: number;
response: number;
}
const config = useConfig();
let rs: (v?: any) => void = () => {};
type Result = Option[];
export const displaySelectActionsModal = async (
args: Partial<Omit<typeof defaultProps, "isOpen">>
) => {
resetSelectActionsModal(); // 先重置为初始状态
localStore.isOpen = true;
Object.entries(args).forEach(([key, value]) => {
// @ts-ignore
localStore[key] = value;
});
await new Promise<Result>((resolve) => (rs = resolve)); // 等待在组件内resolve
console.log("here");
localStore.isOpen = false;
};
const resetSelectActionsModal = () => {
Object.keys(defaultProps).forEach((key) => {
// @ts-ignore
localStore[key] = defaultProps[key];
});
};
......@@ -19,7 +19,9 @@ section#mat {
height: var(--block-height-m);
width: var(--block-width);
// background-color: rgba(128, 128, 128, 0.447);
box-shadow: 0 0 0 1px purple;
background: radial-gradient(#232323, #212121);
// box-shadow: 0 0 0 1px purple;
position: relative;
&.extra {
margin-inline: calc(var(--block-width) / 2 + var(--col-gap) / 2);
}
......@@ -30,5 +32,29 @@ section#mat {
box-shadow: 0 0 0 1px #00b0ff, 0 0 13px 0px #0077ff,
0 0 11px 0 skyblue inset;
}
.triangle {
width: 0;
height: 0;
--color: #292929;
border-width: 5px;
border-style: solid;
position: absolute;
&:nth-of-type(1) {
border-color: var(--color) transparent transparent var(--color);
}
&:nth-of-type(2) {
border-color: var(--color) var(--color) transparent transparent;
right: 0;
}
&:nth-of-type(3) {
border-color: transparent var(--color) var(--color) transparent;
right: 0;
bottom: 0;
}
&:nth-of-type(4) {
border-color: transparent transparent var(--color) var(--color);
bottom: 0;
}
}
}
}
......@@ -52,7 +52,9 @@ const BgExtraRow: FC<{
onBlockClick(meSnap[i].interactivity);
onBlockClick(opSnap[i].interactivity);
}}
></div>
>
{<DecoTriangles />}
</div>
))}
</div>
);
......@@ -73,7 +75,9 @@ const BgRow: FC<{
})}
style={snap[i].disabled ? (BgDisabledStyle as CSSProperties) : {}}
onClick={() => onBlockClick(snap[i].interactivity)}
></div>
>
{<DecoTriangles />}
</div>
))}
</div>
);
......@@ -101,3 +105,11 @@ const onBlockClick = (placeInteractivity: PlaceInteractivity) => {
placeStore.clearAllInteractivity();
}
};
const DecoTriangles: FC = () => (
<>
{Array.from({ length: 4 }).map((_, i) => (
<div className="triangle" key={i} />
))}
</>
);
......@@ -20,6 +20,8 @@ import {
moveToOutside,
} from "./springs";
import { YgoCard } from "@/ui/Shared";
const NeosConfig = useConfig();
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE } =
......@@ -139,15 +141,12 @@ export const Card: FC<{ idx: number }> = React.memo(({ idx }) => {
}}
>
<div className="card-shadow" />
<div className={classnames("card-img-wrap", { highlight })}>
<img
<div className="card-img-wrap">
<YgoCard
className="card-cover"
onError={() => {
console.log("");
}}
src={getCardImgUrl(snap.code == 0 ? snap.meta.id : snap.code)}
code={snap.code === 0 ? snap.meta.id : snap.code}
/>
<img className="card-back" src={getCardImgUrl(0, true)} />
<YgoCard className="card-back" isBack />
</div>
{snap.selected ? <div className="card-streamer" /> : <></>}
</animated.div>
......
export const groupBy = <K, V>(
export const groupBy = <K, V extends Record<string, any>>(
array: readonly V[],
getKey: (cur: V, idx: number, src: readonly V[]) => K
): [K, V[]][] =>
......
.skeleton-cover {
background-color: gray;
}
.ygo-card {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
import { type FC, useMemo, CSSProperties } from "react";
import { useConfig } from "@/config";
import classNames from "classnames";
import "./index.scss";
interface Props {
className?: string;
isBack?: boolean;
code?: number;
style?: CSSProperties;
}
export const YgoCard: FC<Props> = (props) => {
const { className, code: cardCode = 0, isBack = false, style } = props;
return useMemo(
() => (
<>
{cardCode === 0 && !isBack ? (
<div
className={classNames("ygo-card", "skeleton-cover")}
style={style}
/>
) : (
<img
className={classNames("ygo-card", className)}
src={getCardImgUrl(cardCode, isBack)}
style={style}
/>
)}
</>
),
[cardCode]
);
};
const NeosConfig = useConfig();
function getCardImgUrl(code: number, back = false) {
const ASSETS_BASE =
import.meta.env.BASE_URL == "/"
? NeosConfig.assetsPath
: `${import.meta.env.BASE_URL}${NeosConfig.assetsPath}`;
if (back) {
return `${ASSETS_BASE}/card_back.jpg`;
}
return `${NeosConfig.cardImgUrl}/${code}.jpg`;
}
export * from "./YgoCard";
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