Commit 53a9bdf4 authored by Chunchi Che's avatar Chunchi Che

Merge branch 'optimize/replay' into 'main'

Optimize/replay

See merge request mycard/Neos!247
parents f6f681cc ea61194c
Pipeline #22707 passed with stages
in 9 minutes and 57 seconds
...@@ -203,7 +203,7 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) { ...@@ -203,7 +203,7 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break; break;
} }
case "win": { case "win": {
onMsgWin(msg.win); await onMsgWin(msg.win);
break; break;
} }
......
import { fetchStrings, ygopro } from "@/api"; import { fetchStrings, ygopro } from "@/api";
import { matStore } from "@/stores"; import { matStore } from "@/stores";
import { displayEndModal } from "@/ui/Duel/Message";
import MsgWin = ygopro.StocGameMessage.MsgWin; import MsgWin = ygopro.StocGameMessage.MsgWin;
export default (win: MsgWin) => { export default async (win: MsgWin) => {
const { win_player, reason } = win; const { win_player, reason } = win;
matStore.result = {
isWin: matStore.isMe(win_player), await displayEndModal(
reason: fetchStrings("!victory", `0x${reason.toString(16)}`), matStore.isMe(win_player),
}; fetchStrings("!victory", `0x${reason.toString(16)}`)
);
}; };
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
* */ * */
import { adaptStoc } from "@/api/ocgcore/ocgAdapter/adapter"; import { adaptStoc } from "@/api/ocgcore/ocgAdapter/adapter";
import { YgoProPacket } from "@/api/ocgcore/ocgAdapter/packet"; import { YgoProPacket } from "@/api/ocgcore/ocgAdapter/packet";
import { matStore, replayStore } from "@/stores";
import handleGameMsg from "./duel/gameMsg"; import handleGameMsg from "./duel/gameMsg";
import handleTimeLimit from "./duel/timeLimit"; import handleTimeLimit from "./duel/timeLimit";
...@@ -87,6 +88,11 @@ export default async function handleSocketMessage(e: MessageEvent) { ...@@ -87,6 +88,11 @@ export default async function handleSocketMessage(e: MessageEvent) {
case "stoc_game_msg": { case "stoc_game_msg": {
await handleGameMsg(pb); await handleGameMsg(pb);
if (!matStore.isReplay) {
// 如果不是回放模式,则记录回放数据
replayStore.record(packet);
}
break; break;
} }
case "stoc_time_limit": { case "stoc_time_limit": {
......
...@@ -3,6 +3,7 @@ import { proxy } from "valtio"; ...@@ -3,6 +3,7 @@ import { proxy } from "valtio";
import { CardMeta, ygopro } from "@/api"; import { CardMeta, ygopro } from "@/api";
import type { Interactivity } from "./matStore/types"; import type { Interactivity } from "./matStore/types";
import { NeosStore } from "./shared";
/** /**
* 场上某位置的状态 * 场上某位置的状态
...@@ -26,7 +27,7 @@ export interface CardType { ...@@ -26,7 +27,7 @@ export interface CardType {
selected: boolean; // 当前卡是否被选择成为效果的对象 selected: boolean; // 当前卡是否被选择成为效果的对象
} }
class CardStore { class CardStore implements NeosStore {
inner: CardType[] = []; inner: CardType[] = [];
at(zone: ygopro.CardZone, controller: number): CardType[]; at(zone: ygopro.CardZone, controller: number): CardType[];
at( at(
...@@ -90,6 +91,9 @@ class CardStore { ...@@ -90,6 +91,9 @@ class CardStore {
card.location.is_overlay card.location.is_overlay
); );
} }
reset(): void {
this.inner = [];
}
} }
export const cardStore = proxy(new CardStore()); export const cardStore = proxy(new CardStore());
......
import { proxy } from "valtio"; import { proxy } from "valtio";
export interface ChatState { import { NeosStore } from "./shared";
export interface ChatState extends NeosStore {
message: string; message: string;
} }
export const chatStore = proxy<ChatState>({ export const chatStore = proxy<ChatState>({
message: "", message: "",
reset() {
chatStore.message = "";
},
}); });
...@@ -5,6 +5,7 @@ export * from "./matStore"; ...@@ -5,6 +5,7 @@ export * from "./matStore";
export * from "./moraStore"; export * from "./moraStore";
export * from "./placeStore"; export * from "./placeStore";
export * from "./playerStore"; export * from "./playerStore";
export * from "./replayStore";
import { devtools } from "valtio/utils"; import { devtools } from "valtio/utils";
...@@ -15,6 +16,7 @@ import { matStore } from "./matStore"; ...@@ -15,6 +16,7 @@ import { matStore } from "./matStore";
import { moraStore } from "./moraStore"; import { moraStore } from "./moraStore";
import { placeStore } from "./placeStore"; import { placeStore } from "./placeStore";
import { playerStore } from "./playerStore"; import { playerStore } from "./playerStore";
import { replayStore } from "./replayStore";
devtools(playerStore, { name: "player", enabled: true }); devtools(playerStore, { name: "player", enabled: true });
devtools(chatStore, { name: "chat", enabled: true }); devtools(chatStore, { name: "chat", enabled: true });
...@@ -23,3 +25,16 @@ devtools(moraStore, { name: "mora", enabled: true }); ...@@ -23,3 +25,16 @@ devtools(moraStore, { name: "mora", enabled: true });
devtools(matStore, { name: "mat", enabled: true }); devtools(matStore, { name: "mat", enabled: true });
devtools(cardStore, { name: "card", enabled: true }); devtools(cardStore, { name: "card", enabled: true });
devtools(placeStore, { name: "place", enabled: true }); devtools(placeStore, { name: "place", enabled: true });
devtools(replayStore, { name: "replay", enabled: true });
// 重置所有`Store`
export const resetUniverse = () => {
cardStore.reset();
chatStore.reset();
joinStore.reset();
matStore.reset();
moraStore.reset();
placeStore.reset();
playerStore.reset();
replayStore.reset();
};
import { proxy } from "valtio"; import { proxy } from "valtio";
export interface JoinState { import { NeosStore } from "./shared";
export interface JoinState extends NeosStore {
value: boolean; value: boolean;
} }
export const joinStore = proxy<JoinState>({ export const joinStore = proxy<JoinState>({
value: false, value: false,
reset() {
joinStore.value = false;
},
}); });
...@@ -31,32 +31,27 @@ export const isMe = (controller: number): boolean => { ...@@ -31,32 +31,27 @@ export const isMe = (controller: number): boolean => {
} }
}; };
const initInfo: MatState["initInfo"] = (() => { const defaultInitInfo = {
const defaultInitInfo = { masterRule: "UNKNOWN",
masterRule: "UNKNOWN", name: "?",
name: "?", life: -1, // 特地设置一个不可能的值
life: -1, // 特地设置一个不可能的值 deckSize: 0,
deckSize: 0, extraSize: 0,
extraSize: 0, };
};
return proxy({
me: { ...defaultInitInfo },
op: { ...defaultInitInfo },
of: (controller: number) => initInfo[getWhom(controller)],
set: (controller: number, obj: Partial<InitInfo>) => {
initInfo[getWhom(controller)] = {
...initInfo[getWhom(controller)],
...obj,
};
},
});
})();
/** const initInfo: MatState["initInfo"] = proxy({
* 💡 决斗盘状态仓库,本文件核心, me: { ...defaultInitInfo },
* 具体介绍可以点进`MatState`去看 op: { ...defaultInitInfo },
*/ of: (controller: number) => initInfo[getWhom(controller)],
export const matStore: MatState = proxy<MatState>({ set: (controller: number, obj: Partial<InitInfo>) => {
initInfo[getWhom(controller)] = {
...initInfo[getWhom(controller)],
...obj,
};
},
});
const initialState: Omit<MatState, "reset"> = {
chains: [], chains: [],
timeLimits: { timeLimits: {
...@@ -92,6 +87,39 @@ export const matStore: MatState = proxy<MatState>({ ...@@ -92,6 +87,39 @@ export const matStore: MatState = proxy<MatState>({
}, },
// methods // methods
isMe, isMe,
};
/**
* 💡 决斗盘状态仓库,本文件核心,
* 具体介绍可以点进`MatState`去看
*/
export const matStore: MatState = proxy<MatState>({
...initialState,
reset() {
// const resetObj = _.cloneDeep(initialState);
// Object.keys(resetObj).forEach((key) => {
// // @ts-ignore
// matStore[key] = initialState[key];
// });
this.chains = [];
this.timeLimits.me = -1;
this.timeLimits.op = -1;
this.initInfo.me = defaultInitInfo;
this.initInfo.op = defaultInitInfo;
this.selfType = ygopro.StocTypeChange.SelfType.UNKNOWN;
this.hint = { code: -1 };
this.currentPlayer = -1;
this.phase = {
currentPhase: ygopro.StocGameMessage.MsgNewPhase.PhaseType.UNKNOWN,
enableBp: false, // 允许进入战斗阶段
enableM2: false, // 允许进入M2阶段
enableEp: false, // 允许回合结束
};
this.isReplay = false;
this.unimplemented = 0;
this.handResults.me = 0;
this.handResults.op = 0;
},
}); });
// @ts-ignore 挂到全局,便于调试 // @ts-ignore 挂到全局,便于调试
......
import type { ygopro } from "@/api"; import type { ygopro } from "@/api";
import { NeosStore } from "../shared";
// >>> play mat state >>> // >>> play mat state >>>
export interface BothSide<T> { export interface BothSide<T> {
...@@ -9,7 +11,7 @@ export interface BothSide<T> { ...@@ -9,7 +11,7 @@ export interface BothSide<T> {
of: (controller: number) => T; of: (controller: number) => T;
} }
export interface MatState { export interface MatState extends NeosStore {
selfType: number; selfType: number;
initInfo: BothSide<InitInfo> & { initInfo: BothSide<InitInfo> & {
...@@ -30,11 +32,6 @@ export interface MatState { ...@@ -30,11 +32,6 @@ export interface MatState {
isReplay: boolean; // 是否是回放模式 isReplay: boolean; // 是否是回放模式
result?: {
isWin: boolean;
reason: string;
};
unimplemented: number; // 未处理的`Message` unimplemented: number; // 未处理的`Message`
tossResult?: string; // 骰子/硬币结果 tossResult?: string; // 骰子/硬币结果
......
import { proxy } from "valtio"; import { proxy } from "valtio";
export interface MoraState { import { NeosStore } from "./shared";
export interface MoraState extends NeosStore {
duelStart: boolean; duelStart: boolean;
selectHandAble: boolean; selectHandAble: boolean;
selectTpAble: boolean; selectTpAble: boolean;
} }
export const moraStore = proxy<MoraState>({ const initialState = {
duelStart: false, duelStart: false,
selectHandAble: false, selectHandAble: false,
selectTpAble: false, selectTpAble: false,
};
export const moraStore = proxy<MoraState>({
...initialState,
reset() {
Object.keys(initialState).forEach((key) => {
// @ts-ignore
moraStore[key] = initialState[key];
});
},
}); });
import { cloneDeep } from "lodash-es";
import { proxy } from "valtio"; import { proxy } from "valtio";
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { matStore } from "@/stores"; import { matStore } from "@/stores";
import type { Interactivity } from "./matStore/types"; import type { Interactivity } from "./matStore/types";
import { NeosStore } from "./shared";
export type PlaceInteractivity = export type PlaceInteractivity =
| Interactivity<{ | Interactivity<{
...@@ -26,17 +28,28 @@ const genPLaces = (n: number): BlockState[] => ...@@ -26,17 +28,28 @@ const genPLaces = (n: number): BlockState[] =>
disabled: false, disabled: false,
})); }));
export const placeStore = proxy({ const initialState = {
[MZONE]: {
me: genPLaces(7),
op: genPLaces(7),
},
[SZONE]: {
me: genPLaces(6),
op: genPLaces(6),
},
};
class PlaceStore implements NeosStore {
inner: { inner: {
[MZONE]: { [MZONE]: {
me: genPLaces(7), me: BlockState[];
op: genPLaces(7), op: BlockState[];
}, };
[SZONE]: { [SZONE]: {
me: genPLaces(6), me: BlockState[];
op: genPLaces(6), op: BlockState[];
}, };
}, } = initialState;
set( set(
zone: ygopro.CardZone.MZONE | ygopro.CardZone.SZONE, zone: ygopro.CardZone.MZONE | ygopro.CardZone.SZONE,
controller: number, controller: number,
...@@ -45,7 +58,7 @@ export const placeStore = proxy({ ...@@ -45,7 +58,7 @@ export const placeStore = proxy({
) { ) {
placeStore.inner[zone][matStore.isMe(controller) ? "me" : "op"][sequence] = placeStore.inner[zone][matStore.isMe(controller) ? "me" : "op"][sequence] =
state; state;
}, }
clearAllInteractivity() { clearAllInteractivity() {
(["me", "op"] as const).forEach((who) => { (["me", "op"] as const).forEach((who) => {
([MZONE, SZONE] as const).forEach((where) => { ([MZONE, SZONE] as const).forEach((where) => {
...@@ -54,5 +67,14 @@ export const placeStore = proxy({ ...@@ -54,5 +67,14 @@ export const placeStore = proxy({
); );
}); });
}); });
}, }
}); reset(): void {
const resetObj = cloneDeep(initialState);
Object.keys(resetObj).forEach((key) => {
// @ts-ignore
placeStore.inner[key] = resetObj[key];
});
}
}
export const placeStore = proxy(new PlaceStore());
/* eslint valtio/avoid-this-in-proxy: 0 */ /* eslint valtio/avoid-this-in-proxy: 0 */
import { cloneDeep } from "lodash-es";
import { proxy } from "valtio"; import { proxy } from "valtio";
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import SelfType = ygopro.StocTypeChange.SelfType; import SelfType = ygopro.StocTypeChange.SelfType;
import { NeosStore } from "./shared";
export interface Player { export interface Player {
name?: string; name?: string;
...@@ -17,7 +19,7 @@ export interface deckInfo { ...@@ -17,7 +19,7 @@ export interface deckInfo {
sideCnt: number; sideCnt: number;
} }
export interface PlayerState { export interface PlayerState extends NeosStore {
player0: Player; player0: Player;
player1: Player; player1: Player;
observerCount: number; observerCount: number;
...@@ -27,12 +29,16 @@ export interface PlayerState { ...@@ -27,12 +29,16 @@ export interface PlayerState {
getOpPlayer: () => Player; getOpPlayer: () => Player;
} }
export const playerStore = proxy<PlayerState>({ const initialState = {
player0: {}, player0: {},
player1: {}, player1: {},
observerCount: 0, observerCount: 0,
isHost: false, isHost: false,
selfType: SelfType.UNKNOWN, selfType: SelfType.UNKNOWN,
};
export const playerStore = proxy<PlayerState>({
...initialState,
getMePlayer() { getMePlayer() {
if (this.selfType === SelfType.PLAYER1) return this.player0; if (this.selfType === SelfType.PLAYER1) return this.player0;
return this.player1; return this.player1;
...@@ -41,4 +47,11 @@ export const playerStore = proxy<PlayerState>({ ...@@ -41,4 +47,11 @@ export const playerStore = proxy<PlayerState>({
if (this.selfType === SelfType.PLAYER1) return this.player1; if (this.selfType === SelfType.PLAYER1) return this.player1;
return this.player0; return this.player0;
}, },
reset() {
const resetObj = cloneDeep(initialState);
Object.keys(resetObj).forEach((key) => {
// @ts-ignore
playerStore[key] = resetObj[key];
});
},
}); });
import { proxy, ref } from "valtio";
import { YgoProPacket } from "@/api/ocgcore/ocgAdapter/packet";
import { NeosStore } from "./shared";
// 对局中每一次状态改变的记录
interface ReplaySpot {
packet: ReplayPacket; // 将会保存在回放文件中的数据
}
// 保存回放信息的数据包
interface ReplayPacket {
func: number; // 对应的`GAME_MSG`编号
extraData: ArrayBuffer;
}
// 保存对局回放数据的`Store`
class ReplayStore implements NeosStore {
inner: ReplaySpot[] = ref([]);
record(ygoPacket: YgoProPacket) {
this.inner.push({
packet: ygoPacket2replayPacket(ygoPacket),
});
}
encode(): ArrayBuffer[] {
return this.inner.map((spot) => spot.packet).map(replayPacket2arrayBuffer);
}
reset() {
this.inner.splice(0);
}
}
const ygoPacket2replayPacket = (ygoPacket: YgoProPacket) => ({
func: ygoPacket.exData[0],
extraData: ygoPacket.exData.slice(1),
});
const replayPacket2arrayBuffer = (replayPacket: ReplayPacket) => {
const { func, extraData } = replayPacket;
const packetLen = 1 + 4 + extraData.byteLength;
const array = new Uint8Array(packetLen);
const dataview = new DataView(array.buffer);
dataview.setUint8(0, func);
dataview.setUint32(1, extraData.byteLength, true);
array.set(new Uint8Array(extraData), 5);
return array.buffer;
};
export const replayStore = proxy(new ReplayStore());
// Neos项目中所有Store需要实现的接口
// 用于统一管理状态的初始化和重置
export interface NeosStore {
reset(): void;
}
.circles {
position: relative;
margin: 50% auto;
width: var(--R);
height: var(--R);
font-size: 50px;
}
.circle {
position: absolute;
width: 8px;
height: 8px;
left: var(--x);
bottom: var(--y);
border-radius: 50%;
border: 2px solid #ffff00;
animation: flade 1s ease var(--ease) alternate infinite;
}
.font {
position: absolute;
margin: 25% 30%;
font-size: 40px;
font-style: initial;
color: #00BFFF;
-webkit-text-stroke: 2px #333;
}
@keyframes flade {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
CardListModal, CardListModal,
CardModal, CardModal,
CheckCounterModal, CheckCounterModal,
EndModal,
HintNotification, HintNotification,
OptionModal, OptionModal,
PositionModal, PositionModal,
...@@ -34,6 +35,7 @@ const NeosDuel = () => { ...@@ -34,6 +35,7 @@ const NeosDuel = () => {
<SortCardModal /> <SortCardModal />
<AnnounceModal /> <AnnounceModal />
<SimpleSelectCardsModal /> <SimpleSelectCardsModal />
<EndModal />
</> </>
); );
}; };
......
.end-container {
position: relative;
text-align: center;
.result {
font-size: 30px;
font-family: cursive;
color: var(--text-color);
}
.reason {
font-size: 20px;
font-family: "Gill Sans", sans-serif;
}
}
import "./index.scss";
import React, { CSSProperties } from "react";
import { useNavigate } from "react-router-dom";
import { proxy, useSnapshot } from "valtio";
import { fetchStrings } from "@/api";
import { matStore, replayStore, resetUniverse } from "@/stores";
import { NeosModal } from "../NeosModal";
interface EndProps {
isOpen: boolean;
isWin: boolean;
reason?: string;
}
const defaultProps: EndProps = {
isOpen: false,
isWin: false,
};
const localStore = proxy(defaultProps);
export const EndModal: React.FC = () => {
const { isOpen, isWin, reason } = useSnapshot(localStore);
const { isReplay } = useSnapshot(matStore);
const navigate = useNavigate();
const onReturn = () => {
resetUniverse();
rs();
navigate("/");
};
return (
<NeosModal
title={fetchStrings("!system", 1500)}
open={isOpen}
onOk={() => {
if (!isReplay) {
const replayBuffers = replayStore.encode();
const blob = new Blob(replayBuffers, {
type: "application/octet-stream",
});
const url = URL.createObjectURL(blob);
const anchorElement = document.createElement("a");
document.body.appendChild(anchorElement);
anchorElement.style.display = "none";
anchorElement.href = url;
anchorElement.download =
new Date().toLocaleString() + ".neos" + ".yrp3d";
anchorElement.click();
// download the replay file
window.URL.revokeObjectURL(url);
document.body.removeChild(anchorElement);
}
onReturn();
}}
onCancel={onReturn}
>
<div className="end-container">
<p
className="result"
style={{ "--text-color": isWin ? "blue" : "red" } as CSSProperties}
>
{isWin ? "Win" : "Defeated"}
</p>
<p className="reason">{reason}</p>
{isReplay ? <></> : <p>{fetchStrings("!system", 1340)}</p>}
</div>
</NeosModal>
);
};
let rs: (arg?: any) => void = () => {};
export const displayEndModal = async (isWin: boolean, reason?: string) => {
localStore.isWin = isWin;
localStore.reason = reason;
localStore.isOpen = true;
await new Promise<void>((resolve) => (rs = resolve)); // 等待在组件内resolve
localStore.isOpen = false;
localStore.isWin = false;
localStore.reason = undefined;
};
...@@ -24,7 +24,6 @@ export const HintNotification = () => { ...@@ -24,7 +24,6 @@ export const HintNotification = () => {
const toss = snap.tossResult; const toss = snap.tossResult;
const handResults = snap.handResults; const handResults = snap.handResults;
const currentPhase = snap.phase.currentPhase; const currentPhase = snap.phase.currentPhase;
const result = snap.result;
const [notify, notifyContextHolder] = notification.useNotification({ const [notify, notifyContextHolder] = notification.useNotification({
maxCount: NeosConfig.ui.hint.maxCount, maxCount: NeosConfig.ui.hint.maxCount,
...@@ -85,17 +84,6 @@ export const HintNotification = () => { ...@@ -85,17 +84,6 @@ export const HintNotification = () => {
} }
}, [currentPhase]); }, [currentPhase]);
useEffect(() => {
if (result) {
const message = result.isWin ? "Win" : "Defeated" + " " + result.reason;
notify.open({
message,
placement: "bottom",
style: style,
});
}
}, [result]);
return ( return (
<> <>
{notifyContextHolder} {notifyContextHolder}
......
...@@ -3,6 +3,7 @@ export * from "./AnnounceModal"; ...@@ -3,6 +3,7 @@ export * from "./AnnounceModal";
export * from "./CardListModal"; export * from "./CardListModal";
export * from "./CardModal"; export * from "./CardModal";
export * from "./CheckCounterModal"; export * from "./CheckCounterModal";
export * from "./EndModal";
export * from "./HintNotification"; export * from "./HintNotification";
export * from "./OptionModal"; export * from "./OptionModal";
export * from "./PositionModal"; export * from "./PositionModal";
......
...@@ -142,7 +142,7 @@ const WaitRoom = () => { ...@@ -142,7 +142,7 @@ const WaitRoom = () => {
sendUpdateDeck(deck); sendUpdateDeck(deck);
setChoseDeck(true); setChoseDeck(true);
window.myExtraDeckCodes = deck.extra; window.myExtraDeckCodes = [...deck.extra];
}; };
const handleChoseDeck = async (deckName: string) => { const handleChoseDeck = async (deckName: string) => {
......
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