Commit 65b053c2 authored by Chunchi Che's avatar Chunchi Che

Merge branch 'fix/announce_card' into 'main'

修复宣言卡片的逻辑处理

See merge request !365
parents a7340d82 88b5a209
import { ygopro } from "@/api";
import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType;
import { CardMeta } from "@/api";
//! 一些Neos中基础的数据结构
// 类型
......@@ -284,3 +285,164 @@ export const Phase2StringCodeMap: Map<number, number> = new Map([
[PhaseType.MAIN2, 22],
[PhaseType.END, 26],
]);
// For Announce Card
const OPCODE_ADD = 0x40000000;
const OPCODE_SUB = 0x40000001;
const OPCODE_MUL = 0x40000002;
const OPCODE_DIV = 0x40000003;
const OPCODE_AND = 0x40000004;
const OPCODE_OR = 0x40000005;
const OPCODE_NEG = 0x40000006;
const OPCODE_NOT = 0x40000007;
const OPCODE_ISCODE = 0x40000100;
const OPCODE_ISSETCARD = 0x40000101;
const OPCODE_ISTYPE = 0x40000102;
const OPCODE_ISRACE = 0x40000103;
const OPCODE_ISATTRIBUTE = 0x40000104;
const CARD_MARINE_DOLPHIN = 78734254;
const CARD_TWINKLE_MOSS = 13857930;
/*
* 判断一张卡是否能被宣言
* 用于处理`AnnounceCard`
* */
export function isDeclarable(card: CardMeta, opcodes: number[]): boolean {
const stack: number[] = [];
for (const opcode of opcodes) {
switch (opcode) {
case OPCODE_ADD: {
if (stack.length >= 2) {
const rhs = stack.pop()!;
const lhs = stack.pop()!;
stack.push(lhs + rhs);
}
break;
}
case OPCODE_SUB: {
if (stack.length >= 2) {
const rhs = stack.pop()!;
const lhs = stack.pop()!;
stack.push(lhs - rhs);
}
break;
}
case OPCODE_MUL: {
if (stack.length >= 2) {
const rhs = stack.pop()!;
const lhs = stack.pop()!;
stack.push(lhs * rhs);
}
break;
}
case OPCODE_DIV: {
if (stack.length >= 2) {
const rhs = stack.pop()!;
const lhs = stack.pop()!;
stack.push(lhs / rhs);
}
break;
}
case OPCODE_AND: {
if (stack.length >= 2) {
const rhs = stack.pop()!;
const lhs = stack.pop()!;
const b0 = rhs !== 0;
const b1 = lhs !== 0;
stack.push(Number(b0 && b1));
}
break;
}
case OPCODE_OR: {
if (stack.length >= 2) {
const rhs = stack.pop()!;
const lhs = stack.pop()!;
const b0 = rhs !== 0;
const b1 = lhs !== 0;
stack.push(Number(b0 || b1));
}
break;
}
case OPCODE_NEG: {
if (stack.length >= 1) {
const rhs = stack.pop()!;
stack.push(-rhs);
}
break;
}
case OPCODE_NOT: {
if (stack.length >= 1) {
const rhs = stack.pop()!;
stack.push(Number(rhs === 0));
}
break;
}
case OPCODE_ISCODE: {
if (stack.length >= 1) {
const code = stack.pop()!;
stack.push(Number(code === card.id));
}
break;
}
case OPCODE_ISSETCARD: {
if (stack.length >= 1) {
const setCode = stack.pop()!;
stack.push(Number(ifSetCard(setCode, card.data.setcode ?? 0)));
}
break;
}
case OPCODE_ISTYPE: {
if (stack.length >= 1) {
const type_ = stack.pop()!;
stack.push(Number((type_ & (card.data.type ?? 0)) > 0));
}
break;
}
case OPCODE_ISRACE: {
if (stack.length >= 1) {
const race_ = stack.pop()!;
stack.push(Number((race_ & (card.data.race ?? 0)) > 0));
}
break;
}
case OPCODE_ISATTRIBUTE: {
if (stack.length >= 1) {
const attribute_ = stack.pop()!;
stack.push(Number((attribute_ & (card.data.attribute ?? 0)) > 0));
}
break;
}
default: {
stack.push(opcode);
break;
}
}
}
if (stack.length !== 1 || stack.pop() === 0) return false;
return (
card.id === CARD_MARINE_DOLPHIN ||
card.id === CARD_TWINKLE_MOSS ||
(!(card.data.alias !== 0) &&
(card.data.type ?? 0 & (TYPE_MONSTER + TYPE_TOKEN)) !==
TYPE_MONSTER + TYPE_TOKEN)
);
}
function ifSetCard(setCodeToAnalyse: number, setCodeFromCard: number): boolean {
let res = false;
const settype = setCodeToAnalyse & 0xfff;
const setsubtype = setCodeToAnalyse & 0xf000;
let sc = setCodeFromCard;
while (sc !== 0) {
if ((sc & 0xfff) === settype && (sc & 0xf000 & setsubtype) === setsubtype)
res = true;
sc = sc >> 16;
}
return res;
}
......@@ -16,6 +16,17 @@ export interface FtsConditions {
atk: { min: number | null; max: number | null }; // 攻击力区间
def: { min: number | null; max: number | null }; // 防御力区间
}
export const emptySearchConditions: FtsConditions = {
atk: { min: null, max: null },
def: { min: null, max: null },
levels: [],
lscales: [],
races: [],
attributes: [],
types: [],
};
export interface FtsParams {
query: string; // 用于全文检索的query
conditions: FtsConditions; // 过滤条件
......
import { fetchCard, fetchStrings, Region, ygopro } from "@/api";
import { fetchStrings, Region, ygopro } from "@/api";
import { displayOptionModal } from "@/ui/Duel/Message";
import MsgAnnounce = ygopro.StocGameMessage.MsgAnnounce;
import { displayAnnounceModal } from "@/ui/Duel/Message/AnnounceModal";
export default async (announce: MsgAnnounce) => {
const type_ = announce.announce_type;
......@@ -38,17 +39,7 @@ export default async (announce: MsgAnnounce) => {
break;
}
case MsgAnnounce.AnnounceType.Card: {
const options = [];
for (const option of announce.options) {
const meta = fetchCard(option.code);
if (meta.text.name) {
options.push({
info: meta.text.name,
response: option.response,
});
}
}
await displayOptionModal(fetchStrings(Region.System, 564), options, min);
await displayAnnounceModal(announce.options.map((option) => option.code));
break;
}
......
......@@ -6,7 +6,8 @@ import { CardType } from "@/stores";
// 自动从code推断出meta
//
// TODO: 其实不是很推荐这样做,因为随着项目复杂度增加,这样可能会带来meta更新的时序问题
// TODO: 其实不是很推荐这样做,因为随着项目复杂度增加,
// 这样可能会带来meta更新的时序问题
export const genCard = (card: CardType) => {
const t = proxy(card);
subscribeKey(t, "code", async (code) => {
......
......@@ -33,7 +33,7 @@ import { subscribeKey } from "valtio/utils";
import { type CardMeta, searchCards } from "@/api";
import { isExtraDeckCard, isToken } from "@/common";
import { FtsConditions } from "@/middleware/sqlite/fts";
import { emptySearchConditions, FtsConditions } from "@/middleware/sqlite/fts";
import { deckStore, emptyDeck, type IDeck, initStore } from "@/stores";
import {
Background,
......@@ -327,15 +327,6 @@ export const DeckEditor: React.FC<{
const Search: React.FC = () => {
const { modal } = App.useApp();
const [searchWord, setSearchWord] = useState("");
const emptySearchConditions: FtsConditions = {
atk: { min: null, max: null },
def: { min: null, max: null },
levels: [],
lscales: [],
races: [],
attributes: [],
types: [],
};
const [searchConditions, setSearchConditions] = useState<FtsConditions>(
emptySearchConditions,
);
......
......@@ -18,6 +18,7 @@ import {
SortCardModal,
YesNoModal,
} from "./Message";
import { AnnounceModal } from "./Message/AnnounceModal";
import { LifeBar, Mat, Menu, Underlying } from "./PlayMat";
import { ChatBox } from "./PlayMat/ChatBox";
import { HandChain } from "./PlayMat/HandChain";
......@@ -58,6 +59,7 @@ export const Component: React.FC = () => {
<CheckCounterModal />
<SortCardModal />
<SimpleSelectCardsModal />
<AnnounceModal />
<EndModal />
<ChatBox />
<HandChain />
......
.container {
display: flex;
flex-direction: column;
.input {
display: flex;
}
}
import { SearchOutlined } from "@ant-design/icons";
import { Avatar, Button, Checkbox, Input, List } from "antd";
import React, { useState } from "react";
import { proxy, useSnapshot } from "valtio";
import { CardMeta, searchCards, sendSelectOptionResponse } from "@/api";
import { isDeclarable, isToken } from "@/common";
import { emptySearchConditions } from "@/middleware/sqlite/fts";
import { getCardImgUrl } from "@/ui/Shared";
import { NeosModal } from "../NeosModal";
import styles from "./index.module.scss";
const MAX_DESC_LEN = 20;
const PAGE_SIZE = 5;
interface Props {
isOpen: boolean;
opcodes: number[];
}
const defaultProps = {
isOpen: false,
opcodes: [],
};
const store = proxy<Props>(defaultProps);
export const AnnounceModal: React.FC = () => {
const { isOpen } = useSnapshot(store);
const [searchWord, setSearchWord] = useState("");
const [cardList, setCardList] = useState<CardMeta[]>([]);
const [selected, setSelected] = useState<number | undefined>(undefined);
const handleSearch = () => {
const result = searchCards({
query: searchWord,
conditions: emptySearchConditions,
}).filter(
(card) =>
isDeclarable(card, store.opcodes) && !isToken(card.data.type ?? 0),
);
// 清掉之前选中的记录
setSelected(undefined);
setCardList(result);
};
const onSummit = () => {
if (selected !== undefined) {
sendSelectOptionResponse(selected);
rs();
setSearchWord("");
setCardList([]);
}
};
return (
<NeosModal
title="请输入关键字并选择宣言的卡"
open={isOpen}
footer={
<Button disabled={selected === undefined} onClick={onSummit}>
确定
</Button>
}
>
<div className={styles.container}>
<Input
className={styles.input}
placeholder="请输入宣言卡名(或关键字)"
bordered={false}
value={searchWord}
onChange={(e) => setSearchWord(e.target.value)}
suffix={
<Button
type="text"
icon={<SearchOutlined />}
onClick={() => handleSearch()}
/>
}
onKeyUp={(e) => e.key === "Enter" && handleSearch()}
allowClear
/>
<List
pagination={{
position: "bottom",
align: "center",
pageSize: PAGE_SIZE,
}}
dataSource={cardList}
renderItem={(item, index) => (
<List.Item
key={index}
actions={[
<Checkbox
checked={item.id === selected}
onClick={() => {
if (item.id === selected) {
// 之前选的就是这个,则取消选中
setSelected(undefined);
} else {
// 选中
setSelected(item.id);
}
}}
/>,
]}
>
<List.Item.Meta
avatar={<Avatar src={getCardImgUrl(item.id)} />}
title={<a>{item.text.name}</a>}
description={item.text.desc?.substring(0, MAX_DESC_LEN) + "..."}
/>
</List.Item>
)}
/>
</div>
</NeosModal>
);
};
let rs: (v?: any) => void = () => {};
export const displayAnnounceModal = async (opcodes: number[]) => {
store.opcodes = opcodes;
store.isOpen = true;
await new Promise((resolve) => (rs = resolve));
store.isOpen = false;
store.opcodes = [];
};
......@@ -6,7 +6,7 @@ import { ygopro } from "@/api";
import { cardStore, CardType } from "@/stores";
import { YgoCard } from "@/ui/Shared";
import { showCardModal } from "./CardModal";
import { showCardModal } from "../CardModal";
const CARD_WIDTH = "6.25rem";
const DRAWER_WIDTH = "10rem";
......
......@@ -13,7 +13,7 @@ import {
sendSelectOptionResponse,
} from "@/api";
import { NeosModal } from "./NeosModal";
import { NeosModal } from "../NeosModal";
type Options = { info: string; response: number }[];
......
......@@ -6,7 +6,7 @@ import { proxy, useSnapshot } from "valtio";
import { sendSelectPositionResponse, ygopro } from "@/api";
import { NeosModal } from "./NeosModal";
import { NeosModal } from "../NeosModal";
interface PositionModalProps {
isOpen: boolean;
......
......@@ -24,7 +24,7 @@ import { sendSortCardResponse } from "@/api";
import { CardMeta } from "@/api/cards";
import { getCardImgUrl } from "@/ui/Shared";
import { NeosModal } from "./NeosModal";
import { NeosModal } from "../NeosModal";
interface SortOption {
meta: CardMeta;
......
......@@ -53,6 +53,7 @@ export const YgoCard: React.FC<Props> = (props) => {
const NeosConfig = useConfig();
// TODO: 这个函数应该从这个文件抽离出来作为公共的函数使用
export function getCardImgUrl(code: number, back = false) {
const ASSETS_BASE =
import.meta.env.BASE_URL === "/"
......
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