Commit fef43d79 authored by timel's avatar timel

feat: valtio store logic 55%

parent 1802f14a
......@@ -7,7 +7,10 @@ import {
import { fetchCheckCardMeta } from "@/reducers/duel/modal/mod";
import { AppDispatch } from "@/store";
import MsgSelectCard = ygopro.StocGameMessage.MsgSelectCard;
import { messageStore } from "@/valtioStores";
import {
messageStore,
fetchCheckCardMeta as FIXME_fetchCheckCardMeta,
} from "@/valtioStores";
import { CardZoneToChinese } from "./util";
......@@ -39,7 +42,15 @@ export default (selectCard: MsgSelectCard, dispatch: AppDispatch) => {
},
})
);
// FIXME: rename
FIXME_fetchCheckCardMeta(card.location.location, {
code: card.code,
location: card.location,
response: card.response,
});
}
dispatch(setCheckCardModalIsOpen(true));
messageStore.checkCardModal.isOpen = true;
};
......@@ -12,8 +12,14 @@ import { fetchCheckCardMeta } from "@/reducers/duel/modal/mod";
import { AppDispatch } from "@/store";
import { CardZoneToChinese } from "./util";
import MsgSelectChain = ygopro.StocGameMessage.MsgSelectChain;
import {
messageStore,
fetchCheckCardMeta as FIXME_fetchCheckCardMeta,
fetchSelectHintMeta as FIXME_fetchSelectHintMeta,
} from "@/valtioStores";
type MsgSelectChain = ygopro.StocGameMessage.MsgSelectChain;
export default (selectChain: MsgSelectChain, dispatch: AppDispatch) => {
const player = selectChain.player;
const spCount = selectChain.special_count;
......@@ -71,6 +77,12 @@ export default (selectChain: MsgSelectChain, dispatch: AppDispatch) => {
dispatch(setCheckCardMOdalCancelAble(!forced));
dispatch(setCheckCardModalCancelResponse(-1));
messageStore.checkCardModal.selectMin = 1;
messageStore.checkCardModal.selectMax = 1;
messageStore.checkCardModal.onSubmit = "sendSelectChainResponse";
messageStore.checkCardModal.cancelAble = !forced;
messageStore.checkCardModal.cancelResponse = -1;
for (const chain of chains) {
const tagName = CardZoneToChinese(chain.location.location);
dispatch(
......@@ -84,14 +96,24 @@ export default (selectChain: MsgSelectChain, dispatch: AppDispatch) => {
},
})
);
FIXME_fetchCheckCardMeta(chain.location.location, {
code: chain.code,
location: chain.location,
response: chain.response,
effectDescCode: chain.effect_description,
});
}
dispatch(
fetchSelectHintMeta({
selectHintData: 203,
})
);
FIXME_fetchSelectHintMeta({
selectHintData: 203,
});
dispatch(setCheckCardModalIsOpen(true));
messageStore.checkCardModal.isOpen = true;
break;
}
......
export * from "./types";
import { proxy } from "valtio";
import { type CardMeta, fetchCard } from "@/api/cards";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { DESCRIPTION_LIMIT, fetchStrings, getStrings } from "@/api/strings";
import type {
BothSide,
CardsBothSide,
CardState,
DuelFieldState,
InitInfo,
MatState,
} from "./types";
import { InteractType } from "./types";
/**
* 生成一个指定长度的卡片数组
*/
const genBlock = (
location: ygopro.CardZone,
n: number = 5
): BothSide<DuelFieldState> => {
return {
me: Array(n)
.fill(null)
.map((_) => ({
location: {
location,
},
idleInteractivities: [],
counters: {},
})),
op: Array(n)
.fill(null)
.map((_) => ({
location: {
location,
},
idleInteractivities: [],
counters: {},
})),
};
};
const initInfo: MatState["initInfo"] = proxy({
me: {
masterRule: "UNKNOWN",
life: -1, // 特地设置一个不可能的值
deckSize: 0,
extraSize: 0,
},
op: {
masterRule: "UNKNOWN",
life: -1, // 特地设置一个不可能的值
deckSize: 0,
extraSize: 0,
},
set: (controller: number, obj: Partial<InitInfo>) => {
initInfo[getWhom(controller)] = {
...initInfo[getWhom(controller)],
...obj,
};
},
});
const hint: MatState["hint"] = proxy({
code: -1,
fetchCommonHintMeta: (code: number) => {
hint.code = code;
hint.msg = fetchStrings("!system", code);
},
fetchSelectHintMeta: async ({ selectHintData, esHint }) => {
let selectHintMeta = "";
if (selectHintData > DESCRIPTION_LIMIT) {
// 针对`MSG_SELECT_PLACE`的特化逻辑
const cardMeta = await fetchCard(selectHintData, true);
selectHintMeta = fetchStrings("!system", 569).replace(
"[%ls]",
cardMeta.text.name || "[?]"
);
} else {
selectHintMeta = await getStrings(selectHintData);
}
hint.code = selectHintData;
if (hint.code > DESCRIPTION_LIMIT) {
// 针对`MSG_SELECT_PLACE`的特化逻辑
hint.msg = selectHintMeta;
} else {
hint.esSelectHint = selectHintMeta;
hint.esHint = esHint;
}
},
fetchEsHintMeta: async ({ originMsg, location, cardID }) => {
const newOriginMsg =
typeof originMsg === "string"
? originMsg
: fetchStrings("!system", originMsg);
const cardMeta = cardID ? await fetchCard(cardID) : undefined;
let esHint = newOriginMsg;
if (cardMeta?.text.name) {
esHint = esHint.replace("[?]", cardMeta.text.name);
}
if (location) {
const fieldMeta = matStore
.getZone(location.location)
.at(location.controler)
.at(location.sequence);
if (fieldMeta?.occupant?.text.name) {
esHint = esHint.replace("[?]", fieldMeta.occupant.text.name);
}
}
hint.esHint = esHint;
},
});
/**
* 在决斗盘仓库之中,
* 给 `{me: [...], op: [...]}` 这种类型的对象添加一些方法。
* 具体的方法可以看`CardsBothSide`的类型定义
*/
const wrap = <T extends DuelFieldState>(
entity: BothSide<T>,
zone: ygopro.CardZone
): CardsBothSide<T> => {
/**
* 生成一个卡片,根据`id`获取卡片信息
*/
const genCard = async (controller: number, id: number) => ({
occupant: await fetchCard(id, true),
location: {
controler: controller,
location: zone,
},
counters: {},
idleInteractivities: [],
});
const res: CardsBothSide<T> = proxy({
...entity,
at: (controller: number) => {
return res[getWhom(controller)];
},
remove: (controller: number, sequence: number) => {
res[getWhom(controller)].splice(sequence, 1);
},
insert: async (controller: number, sequence: number, id: number) => {
const card = await genCard(controller, id);
res[getWhom(controller)].splice(sequence, 0, card);
},
add: async (controller: number, ids: number[]) => {
const cards = await Promise.all(
ids.map(async (id) => genCard(controller, id))
);
res[getWhom(controller)].splice(
res[getWhom(controller)].length,
0,
...cards
);
},
setOccupant: async (
controller: number,
sequence: number,
id: number,
position?: ygopro.CardPosition
) => {
const meta = await fetchCard(id);
const target = res[getWhom(controller)][sequence];
target.occupant = meta;
if (position) {
target.location.position = position;
}
},
removeOccupant: (controller: number, sequence: number) => {
res[getWhom(controller)][sequence].occupant = undefined;
},
addIdleInteractivity: (
controller: number,
sequence: number,
interactivity: CardState["idleInteractivities"][number]
) => {
res[getWhom(controller)][sequence].idleInteractivities.push(
interactivity
);
},
clearIdleInteractivities: (controller: number, sequence: number) => {
res[getWhom(controller)][sequence].idleInteractivities = [];
},
setPlaceInteractivityType: (
controller: number,
sequence: number,
interactType: InteractType
) => {
res[getWhom(controller)][sequence].placeInteractivity = {
interactType: interactType,
response: {
controler: controller,
zone,
sequence,
},
};
},
clearPlaceInteractivity: (controller: number, sequence: number) => {
res[getWhom(controller)][sequence].placeInteractivity = undefined;
},
});
return res;
};
/**
* zone -> matStore
*/
const getZone = (zone: ygopro.CardZone) => {
switch (zone) {
case ygopro.CardZone.MZONE:
return matStore.monsters;
case ygopro.CardZone.SZONE:
return matStore.magics;
case ygopro.CardZone.HAND:
return matStore.hands;
case ygopro.CardZone.DECK:
return matStore.decks;
case ygopro.CardZone.GRAVE:
return matStore.graveyards;
case ygopro.CardZone.REMOVED:
return matStore.banishedZones;
case ygopro.CardZone.EXTRA:
return matStore.extraDecks;
default:
console.error("in error", zone);
return matStore.extraDecks;
}
};
/**
* 💡 决斗盘状态仓库,本文件核心,
* 具体介绍可以点进`PlayMatState`去看
*/
export const matStore: MatState = proxy<MatState>({
magics: wrap(genBlock(ygopro.CardZone.SZONE, 6), ygopro.CardZone.SZONE),
monsters: wrap(genBlock(ygopro.CardZone.MZONE, 7), ygopro.CardZone.MZONE),
graveyards: wrap({ me: [], op: [] }, ygopro.CardZone.GRAVE),
banishedZones: wrap({ me: [], op: [] }, ygopro.CardZone.REMOVED),
hands: wrap({ me: [], op: [] }, ygopro.CardZone.HAND),
decks: wrap({ me: [], op: [] }, ygopro.CardZone.DECK),
extraDecks: wrap({ me: [], op: [] }, ygopro.CardZone.EXTRA),
timeLimits: {
// 时间限制
me: 0,
op: 0,
},
initInfo,
selfType: ygopro.StocTypeChange.SelfType.UNKNOWN,
hint,
currentPlayer: -1,
phase: {
currentPhase: "UNKNOWN", // TODO 当前的阶段 应该改成enum
enableBp: false, // 允许进入战斗阶段
enableM2: false, // 允许进入M2阶段
enableEp: false, // 允许回合结束
},
result: ygopro.StocGameMessage.MsgWin.ActionType.UNKNOWN,
waiting: false,
unimplemented: 0,
// methods
getZone,
});
/**
* 根据controller判断是自己还是对方
* 不要往外export,尽量逻辑收拢在store内部
*/
const getWhom = (controller: number): "me" | "op" =>
judgeSelf(controller, matStore.selfType) ? "me" : "op";
const judgeSelf = (player: number, selfType: number): boolean => {
switch (selfType) {
case 1:
// 自己是先攻
return player === 0;
case 2:
// 自己是后攻
return player === 1;
default:
// 目前不可能出现这种情况
console.error("judgeSelf error", player, selfType);
return false;
}
};
export * from "./store";
export * from "./methods";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { matStore, messageStore } from "@/valtioStores";
import { fetchCard, getCardStr } from "@/api/cards";
function CardZoneToChinese(zone: ygopro.CardZone): string {
switch (zone) {
case ygopro.CardZone.DECK: {
return "卡组";
}
case ygopro.CardZone.HAND: {
return "手牌";
}
case ygopro.CardZone.EXTRA: {
return "额外卡组";
}
case ygopro.CardZone.GRAVE: {
return "墓地";
}
case ygopro.CardZone.FZONE: {
return "FZONE";
}
case ygopro.CardZone.MZONE: {
return "怪兽区";
}
case ygopro.CardZone.SZONE: {
return "魔法陷阱区";
}
case ygopro.CardZone.REMOVED: {
return "除外区";
}
case ygopro.CardZone.OVERLAY: {
return "超量区";
}
case ygopro.CardZone.PZONE: {
return "灵摆区";
}
case ygopro.CardZone.ONFIELD: {
return "场地区";
}
default: {
return "未知区域";
}
}
}
type Location =
| ygopro.CardLocation
| ReturnType<typeof ygopro.CardLocation.prototype.toObject>;
function cmpCardLocation(
left: Location,
right?: Location,
strict?: boolean
): boolean {
if (strict) {
return JSON.stringify(left) === JSON.stringify(right);
} else {
return (
left.controler === right?.controler &&
left.location === right?.location &&
left.sequence === right?.sequence
);
}
}
/**
* 这段代码定义了一个异步函数 fetchCheckCardMeta,它的作用是获取一张卡片的元数据并将其添加到某个名为 messageStore.checkCardModal 的对象上。
该函数的第一个参数是一个枚举值 ygopro.CardZone,表示卡片所在的区域。其余参数是一个包含卡片编号、位置、响应码和效果描述代码等信息的对象。
首先,这个函数会根据区域类型调用 CardZoneToChinese() 函数生成一个中文名称。然后,它会调用 fetchCard() 异步函数来获取指定卡片的元数据 meta。
接下来,函数会根据传递进来的 location 对象获取卡片所属的控制者,并根据控制者判断这张卡片是我方的还是对方的。然后,它会根据卡片的位置信息获取卡片的实际 ID,并构造一个新的选项 newOption。
接着,函数会遍历已有的 messageStore.checkCardModal.tags,查找是否存在名为 combinedTagName 的标签。如果找到了,则将新选项 newOption 加入该标签的选项列表中并立即返回。如果找不到,则创建一个新标签,并将新选项 newOption 添加到其中。
最后,函数会再次遍历所有标签,查找是否存在名为 combinedTagName 的标签。如果找到了,则遍历该标签中的所有选项,并查找是否存在与 location 对象中指定的卡片位置信息完全相同的选项。如果找到了,则更新该选项的元数据和效果描述等信息。
*/
export const fetchCheckCardMeta = async (
zone: ygopro.CardZone,
{
code,
location,
response,
effectDescCode,
}: {
code: number;
location: ygopro.CardLocation;
response: number;
effectDescCode?: number;
}
) => {
const tagName = CardZoneToChinese(zone);
const meta = await fetchCard(code);
const controller = location.controler;
const combinedTagName = matStore.isMe(controller)
? `我方的${tagName}`
: `对方的${tagName}`;
const newID =
code != 0
? code
: matStore.getZone(location.location).at(controller)[location.sequence]
?.occupant?.id || 0;
const newOption = {
meta: { id: newID, data: {}, text: {} },
location: location.toObject(),
effectDescCode,
response,
};
for (const tag of messageStore.checkCardModal.tags) {
if (tag.tagName === combinedTagName) {
tag.options.push(newOption);
return;
}
}
messageStore.checkCardModal.tags.push({
tagName: combinedTagName,
options: [newOption],
});
for (const tag of messageStore.checkCardModal.tags) {
if (tag.tagName === combinedTagName) {
for (const old of tag.options) {
if (meta.id == old.meta.id && cmpCardLocation(location, old.location)) {
const cardID = old.meta.id;
old.meta = meta;
old.meta.id = cardID;
const effectDescCode = old.effectDescCode;
const effectDesc = effectDescCode
? getCardStr(old.meta, effectDescCode & 0xf)
: undefined;
old.effectDesc = effectDesc;
}
}
}
}
};
import { matStore } from "@/valtioStores";
import { fetchCard } from "@/api/cards";
import { DESCRIPTION_LIMIT, fetchStrings, getStrings } from "@/api/strings";
import type { ygopro } from "@/api/ocgcore/idl/ocgcore";
const { hint } = matStore;
export const fetchCommonHintMeta = (code: number) => {
hint.code = code;
hint.msg = fetchStrings("!system", code);
};
export const fetchSelectHintMeta = async ({
selectHintData,
esHint,
}: {
selectHintData: number;
esHint?: string;
}) => {
let selectHintMeta = "";
if (selectHintData > DESCRIPTION_LIMIT) {
// 针对`MSG_SELECT_PLACE`的特化逻辑
const cardMeta = await fetchCard(selectHintData, true);
selectHintMeta = fetchStrings("!system", 569).replace(
"[%ls]",
cardMeta.text.name || "[?]"
);
} else {
selectHintMeta = await getStrings(selectHintData);
}
hint.code = selectHintData;
if (hint.code > DESCRIPTION_LIMIT) {
// 针对`MSG_SELECT_PLACE`的特化逻辑
hint.msg = selectHintMeta;
} else {
hint.esSelectHint = selectHintMeta;
hint.esHint = esHint;
}
};
export const fetchEsHintMeta = async ({
originMsg,
location,
cardID,
}: {
originMsg: string | number;
location?: ygopro.CardLocation;
cardID?: number;
}) => {
const newOriginMsg =
typeof originMsg === "string"
? originMsg
: fetchStrings("!system", originMsg);
const cardMeta = cardID ? await fetchCard(cardID) : undefined;
let esHint = newOriginMsg;
if (cardMeta?.text.name) {
esHint = esHint.replace("[?]", cardMeta.text.name);
}
if (location) {
const fieldMeta = matStore
.getZone(location.location)
.at(location.controler)
.at(location.sequence);
if (fieldMeta?.occupant?.text.name) {
esHint = esHint.replace("[?]", fieldMeta.occupant.text.name);
}
}
hint.esHint = esHint;
};
export * from "./fetchCheckCardMeta";
export * from "./fetchHint";
import { proxy } from "valtio";
import { fetchCard } from "@/api/cards";
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import type {
BothSide,
CardsBothSide,
CardState,
DuelFieldState,
InitInfo,
MatState,
} from "./types";
import { InteractType } from "./types";
/**
* 根据controller判断是自己还是对方。
* 这个无需export,尽量逻辑收拢在store内部。
*/
const getWhom = (controller: number): "me" | "op" =>
isMe(controller) ? "me" : "op";
/**
* 根据自己的先后手判断是否是自己
*/
const isMe = (player: number): boolean => {
switch (matStore.selfType) {
case 1:
// 自己是先攻
return player === 0;
case 2:
// 自己是后攻
return player === 1;
default:
// 目前不可能出现这种情况
console.error("judgeSelf error", player, matStore.selfType);
return false;
}
};
/**
* 生成一个指定长度的卡片数组
*/
const genBlock = (
location: ygopro.CardZone,
n: number
): BothSide<DuelFieldState> => {
return {
me: Array(n)
.fill(null)
.map((_) => ({
location: {
location,
},
idleInteractivities: [],
counters: {},
})),
op: Array(n)
.fill(null)
.map((_) => ({
location: {
location,
},
idleInteractivities: [],
counters: {},
})),
};
};
const initInfo: MatState["initInfo"] = proxy({
me: {
masterRule: "UNKNOWN",
life: -1, // 特地设置一个不可能的值
deckSize: 0,
extraSize: 0,
},
op: {
masterRule: "UNKNOWN",
life: -1, // 特地设置一个不可能的值
deckSize: 0,
extraSize: 0,
},
set: (controller: number, obj: Partial<InitInfo>) => {
initInfo[getWhom(controller)] = {
...initInfo[getWhom(controller)],
...obj,
};
},
});
const hint: MatState["hint"] = proxy({
code: -1,
});
/**
* 在决斗盘仓库之中,
* 给 `{me: [...], op: [...]}` 这种类型的对象添加一些方法。
* 具体的方法可以看`CardsBothSide`的类型定义
*/
const wrap = <T extends DuelFieldState>(
entity: BothSide<T>,
zone: ygopro.CardZone
): CardsBothSide<T> => {
/**
* 生成一个卡片,根据`id`获取卡片信息
*/
const genCard = async (controller: number, id: number) => ({
occupant: await fetchCard(id, true),
location: {
controler: controller,
location: zone,
},
counters: {},
idleInteractivities: [],
});
const res: CardsBothSide<T> = proxy({
...entity,
at: (controller: number) => {
return res[getWhom(controller)];
},
remove: (controller: number, sequence: number) => {
res[getWhom(controller)].splice(sequence, 1);
},
insert: async (controller: number, sequence: number, id: number) => {
const card = await genCard(controller, id);
res[getWhom(controller)].splice(sequence, 0, card);
},
add: async (controller: number, ids: number[]) => {
const cards = await Promise.all(
ids.map(async (id) => genCard(controller, id))
);
res[getWhom(controller)].splice(
res[getWhom(controller)].length,
0,
...cards
);
},
setOccupant: async (
controller: number,
sequence: number,
id: number,
position?: ygopro.CardPosition
) => {
const meta = await fetchCard(id);
const target = res[getWhom(controller)][sequence];
target.occupant = meta;
if (position) {
target.location.position = position;
}
},
removeOccupant: (controller: number, sequence: number) => {
res[getWhom(controller)][sequence].occupant = undefined;
},
addIdleInteractivity: (
controller: number,
sequence: number,
interactivity: CardState["idleInteractivities"][number]
) => {
res[getWhom(controller)][sequence].idleInteractivities.push(
interactivity
);
},
clearIdleInteractivities: (controller: number, sequence: number) => {
res[getWhom(controller)][sequence].idleInteractivities = [];
},
setPlaceInteractivityType: (
controller: number,
sequence: number,
interactType: InteractType
) => {
res[getWhom(controller)][sequence].placeInteractivity = {
interactType: interactType,
response: {
controler: controller,
zone,
sequence,
},
};
},
clearPlaceInteractivity: (controller: number, sequence: number) => {
res[getWhom(controller)][sequence].placeInteractivity = undefined;
},
});
return res;
};
/**
* zone -> matStore
*/
const getZone = (zone: ygopro.CardZone) => {
switch (zone) {
case ygopro.CardZone.MZONE:
return matStore.monsters;
case ygopro.CardZone.SZONE:
return matStore.magics;
case ygopro.CardZone.HAND:
return matStore.hands;
case ygopro.CardZone.DECK:
return matStore.decks;
case ygopro.CardZone.GRAVE:
return matStore.graveyards;
case ygopro.CardZone.REMOVED:
return matStore.banishedZones;
case ygopro.CardZone.EXTRA:
return matStore.extraDecks;
default:
console.error("in error", zone);
return matStore.extraDecks;
}
};
/**
* 💡 决斗盘状态仓库,本文件核心,
* 具体介绍可以点进`PlayMatState`去看
*/
export const matStore: MatState = proxy<MatState>({
magics: wrap(genBlock(ygopro.CardZone.SZONE, 6), ygopro.CardZone.SZONE),
monsters: wrap(genBlock(ygopro.CardZone.MZONE, 7), ygopro.CardZone.MZONE),
graveyards: wrap({ me: [], op: [] }, ygopro.CardZone.GRAVE),
banishedZones: wrap({ me: [], op: [] }, ygopro.CardZone.REMOVED),
hands: wrap({ me: [], op: [] }, ygopro.CardZone.HAND),
decks: wrap({ me: [], op: [] }, ygopro.CardZone.DECK),
extraDecks: wrap({ me: [], op: [] }, ygopro.CardZone.EXTRA),
timeLimits: {
// 时间限制
me: 0,
op: 0,
},
initInfo,
selfType: ygopro.StocTypeChange.SelfType.UNKNOWN,
hint,
currentPlayer: -1,
phase: {
currentPhase: "UNKNOWN", // TODO 当前的阶段 应该改成enum
enableBp: false, // 允许进入战斗阶段
enableM2: false, // 允许进入M2阶段
enableEp: false, // 允许回合结束
},
result: ygopro.StocGameMessage.MsgWin.ActionType.UNKNOWN,
waiting: false,
unimplemented: 0,
// methods
getZone,
isMe,
});
......@@ -67,7 +67,7 @@ export interface MatState {
timeLimits: BothSide<number>; // 双方的时间限制
hint: HintState & HintMethods;
hint: HintState;
currentPlayer: number; // 当前的操作方
......@@ -79,7 +79,11 @@ export interface MatState {
unimplemented: number; // 未处理的`Message`
getZone: (zone: ygopro.CardZone) => CardsBothSide<DuelFieldState>; // 是否在某个区域
// >>> methods >>>
/** 根据zone获取hands/masters/gy... */
getZone: (zone: ygopro.CardZone) => CardsBothSide<DuelFieldState>;
/** 根据自己的先后手判断是否是自己 */
isMe: (player: number) => boolean;
}
export interface InitInfo {
......@@ -162,19 +166,6 @@ export interface HintState {
esHint?: string;
esSelectHint?: string;
}
// 和hint相关的方法
export interface HintMethods {
fetchCommonHintMeta: (hintData: number) => void;
fetchSelectHintMeta: (args: {
selectHintData: number;
esHint?: string;
}) => Promise<void>;
fetchEsHintMeta: (args: {
originMsg: string | number;
location?: ygopro.CardLocation;
cardID?: number;
}) => Promise<void>;
}
export type PhaseName =
keyof typeof ygopro.StocGameMessage.MsgNewPhase.PhaseType;
......
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