Commit e2e6ef39 authored by timel's avatar timel

feat: validate and sort in build card

parent 80ddb009
Pipeline #23019 passed with stages
in 16 minutes and 54 seconds
......@@ -95,6 +95,29 @@ export function extraCardTypes(typeCode: number): number[] {
].filter((target) => (target & typeCode) > 0);
}
/** 这张卡能不能放入额外卡组 */
export function isExtraDeckCard(typeCode: number): boolean {
const extraTypes = [
TYPE_PENDULUM,
TYPE_LINK,
TYPE_SYNCHRO,
TYPE_XYZ,
TYPE_FUSION,
];
return extraTypes.reduce((acc, cur) => (acc | cur) & typeCode, 0) > 0;
}
/** 这张卡是怪兽、魔法、陷阱 */
export function tellCardBasicType(typeCode: number): number {
const basicTypes = [TYPE_MONSTER, TYPE_SPELL, TYPE_TRAP];
return basicTypes.reduce((acc, cur) => (acc | cur) & typeCode, 0);
}
/** 是不是衍生物 */
export function isToken(typeCode: number): boolean {
return (typeCode & TYPE_TOKEN) > 0;
}
// 属性
// const ATTRIBUTE_ALL = 0x7f; //
const ATTRIBUTE_EARTH = 0x01; //
......
......@@ -95,7 +95,7 @@
position: relative;
background-size: contain;
.cardname {
font-size: 0.9rem;
font-size: 12px;
position: absolute;
padding: 5px;
top: 0;
......
......@@ -37,10 +37,14 @@ import { CardDetail } from "./CardDetail";
import { DeckSelect } from "./DeckSelect";
import styles from "./index.module.scss";
import {
canAdd,
compareCards,
type EditingDeck,
editingDeckToIDeck,
iDeckToEditingDeck,
type Type,
} from "./utils";
import { isToken } from "@/common";
const theme: ThemeConfig = {
components: {
......@@ -87,7 +91,17 @@ export const Component: React.FC = () => {
</div>
<div className={styles.content}>
<div className={styles.deck}>
<DeckEditor deck={selectedDeck} onSave={() => {}} />
<DeckEditor
deck={selectedDeck}
onReset={async () => {
editDeckStore.set(await iDeckToEditingDeck(selectedDeck));
}}
onSave={async () => {
const tmpIDeck = editingDeckToIDeck(editDeckStore);
await deckStore.update(selectedDeck.deckName, tmpIDeck);
setSelectedDeck(tmpIDeck);
}}
/>
</div>
<div className={styles.select}>
{sqlite.progress === 1 ? (
......@@ -109,8 +123,9 @@ Component.displayName = "Build";
/** 正在编辑的卡组 */
const DeckEditor: React.FC<{
deck: IDeck;
onSave: (deck: IDeck) => void;
}> = ({ deck, onSave }) => {
onReset: () => void;
onSave: () => void;
}> = ({ deck, onReset, onSave }) => {
const snapEditDeck = useSnapshot(editDeckStore);
useEffect(() => {
iDeckToEditingDeck(deck).then(editDeckStore.set);
......@@ -132,17 +147,27 @@ const DeckEditor: React.FC<{
value={snapEditDeck.deckName}
/>
<Space style={{ marginRight: 6 }}>
<Button type="text" size="small" icon={<DeleteOutlined />}>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
onClick={editDeckStore.clear}
>
清空
</Button>
<Button type="text" size="small" icon={<UndoOutlined />}>
<Button
type="text"
size="small"
icon={<UndoOutlined />}
onClick={() => onReset()}
>
重置
</Button>
<Button
type="text"
size="small"
icon={<CheckOutlined />}
onClick={() => onSave(editingDeckToIDeck(editDeckStore))}
onClick={() => onSave()}
>
保存
</Button>
......@@ -162,7 +187,9 @@ const CardSelect: React.FC = () => {
const [searchWord, setSearchWord] = useState("");
const [searchResult, setSearchResult] = useState<CardMeta[]>([]);
const handleSearch = async () => {
const result = await searchCards(searchWord);
const result = (await searchCards(searchWord)).filter((card) =>
isToken(card.data.type ?? 0)
); // 衍生物不显示
setSearchResult(result);
};
return (
......@@ -218,24 +245,20 @@ const CardSelect: React.FC = () => {
/** 正在组卡的zone,包括main/extra/side */
const DeckZone: React.FC<{
type: "main" | "extra" | "side";
type: Type;
}> = ({ type }) => {
const cards = useSnapshot(editDeckStore)[type];
const [_, dropRef] = useDrop({
accept: ["Card"], // 指明该区域允许接收的拖放物。可以是单个,也可以是数组
// 里面的值就是useDrag所定义的type
// 当拖拽物在这个拖放区域放下时触发,这个item就是拖拽物的item(拖拽物携带的数据)
drop: ({
value,
source,
}: {
value: CardMeta;
source: "main" | "extra" | "side" | "search";
}) => {
drop: ({ value, source }: { value: CardMeta; source: Type | "search" }) => {
if (type === source) return;
editDeckStore.add(type, value);
if (source !== "search") {
editDeckStore.remove(source, value);
if (canAdd(value, type, editDeckStore)) {
editDeckStore.add(type, value);
if (source !== "search") {
editDeckStore.remove(source, value);
}
}
},
});
......@@ -243,7 +266,12 @@ const DeckZone: React.FC<{
<div className={styles[type]} ref={dropRef}>
<div className={styles["card-continer"]}>
{cards.map((item, i) => (
<Card value={item} key={i} source={type} />
<Card
value={item}
key={i}
source={type}
onRightClick={() => editDeckStore.remove(type, item)}
/>
))}
</div>
</div>
......@@ -282,8 +310,10 @@ const SearchResults: React.FC<{
/** 本组件内使用的单张卡片,增加了文字在图片下方 */
const Card: React.FC<{
value: CardMeta;
source: "main" | "extra" | "side" | "search";
}> = memo(({ value, source }) => {
source: Type | "search";
onClick?: () => void;
onRightClick?: () => void;
}> = memo(({ value, source, onClick, onRightClick }) => {
const ref = useRef<HTMLDivElement>(null);
const [{ isDragging }, drag] = useDrag({
type: "Card",
......@@ -297,7 +327,12 @@ const Card: React.FC<{
<div
className={styles.card}
ref={ref}
style={{ opacity: isDragging ? 0 : 1 }}
style={{ opacity: isDragging && source !== "search" ? 0 : 1 }}
onClick={onClick}
onContextMenu={(e) => {
e.preventDefault();
onRightClick?.();
}}
>
<div className={styles.cardname}>{value.text.name}</div>
<YgoCard className={styles.cardcover} code={value.id} />
......@@ -310,10 +345,11 @@ const editDeckStore = proxy({
main: [] as CardMeta[],
extra: [] as CardMeta[],
side: [] as CardMeta[],
add(type: "main" | "extra" | "side", card: CardMeta) {
add(type: Type, card: CardMeta) {
editDeckStore[type].push(card);
editDeckStore[type].sort(compareCards);
},
remove(type: "main" | "extra" | "side", card: CardMeta) {
remove(type: Type, card: CardMeta) {
const index = editDeckStore[type].findIndex((item) => item.id === card.id);
if (index !== -1) {
editDeckStore[type].splice(index, 1);
......@@ -321,8 +357,13 @@ const editDeckStore = proxy({
},
set(deck: EditingDeck) {
editDeckStore.deckName = deck.deckName;
editDeckStore.main = deck.main;
editDeckStore.extra = deck.extra;
editDeckStore.side = deck.side;
editDeckStore.main = deck.main.sort(compareCards);
editDeckStore.extra = deck.extra.sort(compareCards);
editDeckStore.side = deck.side.sort(compareCards);
},
clear() {
editDeckStore.main = [];
editDeckStore.extra = [];
editDeckStore.side = [];
},
}) satisfies EditingDeck;
import { type CardMeta, fetchCard } from "@/api";
import { isExtraDeckCard, isToken, tellCardBasicType } from "@/common";
import { type IDeck } from "@/stores";
export type Type = "main" | "extra" | "side";
/** 用在卡组编辑 */
export interface EditingDeck {
deckName: string;
......@@ -24,3 +27,49 @@ export const editingDeckToIDeck = (deck: EditingDeck): IDeck => ({
extra: deck.extra.map((card) => card.id),
side: deck.side.map((card) => card.id),
});
/** 能不能添加到正在编辑的卡组的区域 */
export const canAdd = (
card: CardMeta,
type: Type,
editDeckStore: EditingDeck
): { result: boolean; reason?: string } => {
let result = true,
reason;
const initialCards = editDeckStore[type];
// 如果是衍生物,则不能添加
if (isToken(card.data.type ?? 0)) {
result = false;
reason = "不能添加衍生物";
}
// 超出数量,则不能添加
const countLimit = type === "main" ? 60 : 15;
if (initialCards.length >= countLimit) {
result = false;
reason = `超过${countLimit}张的上限`;
}
// 接着需要检查卡的种类
if (
(type === "extra" && !isExtraDeckCard(card.data.type ?? 0)) ||
(type === "main" && isExtraDeckCard(card.data.type ?? 0))
) {
result = false;
reason = "卡片种类不符合";
}
// 同名卡不超过三张
const maxSameCard = 3; // TODO: 禁卡表
const sameCardCount = initialCards.filter((c) => c.id === card.id).length;
if (sameCardCount >= maxSameCard) {
result = false;
reason = `超过同名卡${maxSameCard}张的上限`;
}
return { result, reason };
};
/** 卡组内部排序,给array.sort用 */
export const compareCards = (a: CardMeta, b: CardMeta): number => {
const aType = tellCardBasicType(a.data.type ?? 0);
const bType = tellCardBasicType(b.data.type ?? 0);
if (aType !== bType) return aType - bType;
return a.id - b.id;
};
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