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) {
break;
}
case "win": {
onMsgWin(msg.win);
await onMsgWin(msg.win);
break;
}
......
import { fetchStrings, ygopro } from "@/api";
import { matStore } from "@/stores";
import { displayEndModal } from "@/ui/Duel/Message";
import MsgWin = ygopro.StocGameMessage.MsgWin;
export default (win: MsgWin) => {
export default async (win: MsgWin) => {
const { win_player, reason } = win;
matStore.result = {
isWin: matStore.isMe(win_player),
reason: fetchStrings("!victory", `0x${reason.toString(16)}`),
};
await displayEndModal(
matStore.isMe(win_player),
fetchStrings("!victory", `0x${reason.toString(16)}`)
);
};
......@@ -4,6 +4,7 @@
* */
import { adaptStoc } from "@/api/ocgcore/ocgAdapter/adapter";
import { YgoProPacket } from "@/api/ocgcore/ocgAdapter/packet";
import { matStore, replayStore } from "@/stores";
import handleGameMsg from "./duel/gameMsg";
import handleTimeLimit from "./duel/timeLimit";
......@@ -87,6 +88,11 @@ export default async function handleSocketMessage(e: MessageEvent) {
case "stoc_game_msg": {
await handleGameMsg(pb);
if (!matStore.isReplay) {
// 如果不是回放模式,则记录回放数据
replayStore.record(packet);
}
break;
}
case "stoc_time_limit": {
......
......@@ -3,6 +3,7 @@ import { proxy } from "valtio";
import { CardMeta, ygopro } from "@/api";
import type { Interactivity } from "./matStore/types";
import { NeosStore } from "./shared";
/**
* 场上某位置的状态
......@@ -26,7 +27,7 @@ export interface CardType {
selected: boolean; // 当前卡是否被选择成为效果的对象
}
class CardStore {
class CardStore implements NeosStore {
inner: CardType[] = [];
at(zone: ygopro.CardZone, controller: number): CardType[];
at(
......@@ -90,6 +91,9 @@ class CardStore {
card.location.is_overlay
);
}
reset(): void {
this.inner = [];
}
}
export const cardStore = proxy(new CardStore());
......
import { proxy } from "valtio";
export interface ChatState {
import { NeosStore } from "./shared";
export interface ChatState extends NeosStore {
message: string;
}
export const chatStore = proxy<ChatState>({
message: "",
reset() {
chatStore.message = "";
},
});
......@@ -5,6 +5,7 @@ export * from "./matStore";
export * from "./moraStore";
export * from "./placeStore";
export * from "./playerStore";
export * from "./replayStore";
import { devtools } from "valtio/utils";
......@@ -15,6 +16,7 @@ import { matStore } from "./matStore";
import { moraStore } from "./moraStore";
import { placeStore } from "./placeStore";
import { playerStore } from "./playerStore";
import { replayStore } from "./replayStore";
devtools(playerStore, { name: "player", enabled: true });
devtools(chatStore, { name: "chat", enabled: true });
......@@ -23,3 +25,16 @@ devtools(moraStore, { name: "mora", enabled: true });
devtools(matStore, { name: "mat", enabled: true });
devtools(cardStore, { name: "card", 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";
export interface JoinState {
import { NeosStore } from "./shared";
export interface JoinState extends NeosStore {
value: boolean;
}
export const joinStore = proxy<JoinState>({
value: false,
reset() {
joinStore.value = false;
},
});
......@@ -31,32 +31,27 @@ export const isMe = (controller: number): boolean => {
}
};
const initInfo: MatState["initInfo"] = (() => {
const defaultInitInfo = {
masterRule: "UNKNOWN",
name: "?",
life: -1, // 特地设置一个不可能的值
deckSize: 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 defaultInitInfo = {
masterRule: "UNKNOWN",
name: "?",
life: -1, // 特地设置一个不可能的值
deckSize: 0,
extraSize: 0,
};
/**
* 💡 决斗盘状态仓库,本文件核心,
* 具体介绍可以点进`MatState`去看
*/
export const matStore: MatState = proxy<MatState>({
const initInfo: MatState["initInfo"] = 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 initialState: Omit<MatState, "reset"> = {
chains: [],
timeLimits: {
......@@ -92,6 +87,39 @@ export const matStore: MatState = proxy<MatState>({
},
// methods
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 挂到全局,便于调试
......
import type { ygopro } from "@/api";
import { NeosStore } from "../shared";
// >>> play mat state >>>
export interface BothSide<T> {
......@@ -9,7 +11,7 @@ export interface BothSide<T> {
of: (controller: number) => T;
}
export interface MatState {
export interface MatState extends NeosStore {
selfType: number;
initInfo: BothSide<InitInfo> & {
......@@ -30,11 +32,6 @@ export interface MatState {
isReplay: boolean; // 是否是回放模式
result?: {
isWin: boolean;
reason: string;
};
unimplemented: number; // 未处理的`Message`
tossResult?: string; // 骰子/硬币结果
......
import { proxy } from "valtio";
export interface MoraState {
import { NeosStore } from "./shared";
export interface MoraState extends NeosStore {
duelStart: boolean;
selectHandAble: boolean;
selectTpAble: boolean;
}
export const moraStore = proxy<MoraState>({
const initialState = {
duelStart: false,
selectHandAble: 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 { ygopro } from "@/api";
import { matStore } from "@/stores";
import type { Interactivity } from "./matStore/types";
import { NeosStore } from "./shared";
export type PlaceInteractivity =
| Interactivity<{
......@@ -26,17 +28,28 @@ const genPLaces = (n: number): BlockState[] =>
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: {
[MZONE]: {
me: genPLaces(7),
op: genPLaces(7),
},
me: BlockState[];
op: BlockState[];
};
[SZONE]: {
me: genPLaces(6),
op: genPLaces(6),
},
},
me: BlockState[];
op: BlockState[];
};
} = initialState;
set(
zone: ygopro.CardZone.MZONE | ygopro.CardZone.SZONE,
controller: number,
......@@ -45,7 +58,7 @@ export const placeStore = proxy({
) {
placeStore.inner[zone][matStore.isMe(controller) ? "me" : "op"][sequence] =
state;
},
}
clearAllInteractivity() {
(["me", "op"] as const).forEach((who) => {
([MZONE, SZONE] as const).forEach((where) => {
......@@ -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 */
import { cloneDeep } from "lodash-es";
import { proxy } from "valtio";
import { ygopro } from "@/api";
import SelfType = ygopro.StocTypeChange.SelfType;
import { NeosStore } from "./shared";
export interface Player {
name?: string;
......@@ -17,7 +19,7 @@ export interface deckInfo {
sideCnt: number;
}
export interface PlayerState {
export interface PlayerState extends NeosStore {
player0: Player;
player1: Player;
observerCount: number;
......@@ -27,12 +29,16 @@ export interface PlayerState {
getOpPlayer: () => Player;
}
export const playerStore = proxy<PlayerState>({
const initialState = {
player0: {},
player1: {},
observerCount: 0,
isHost: false,
selfType: SelfType.UNKNOWN,
};
export const playerStore = proxy<PlayerState>({
...initialState,
getMePlayer() {
if (this.selfType === SelfType.PLAYER1) return this.player0;
return this.player1;
......@@ -41,4 +47,11 @@ export const playerStore = proxy<PlayerState>({
if (this.selfType === SelfType.PLAYER1) return this.player1;
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 {
CardListModal,
CardModal,
CheckCounterModal,
EndModal,
HintNotification,
OptionModal,
PositionModal,
......@@ -34,6 +35,7 @@ const NeosDuel = () => {
<SortCardModal />
<AnnounceModal />
<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 = () => {
const toss = snap.tossResult;
const handResults = snap.handResults;
const currentPhase = snap.phase.currentPhase;
const result = snap.result;
const [notify, notifyContextHolder] = notification.useNotification({
maxCount: NeosConfig.ui.hint.maxCount,
......@@ -85,17 +84,6 @@ export const HintNotification = () => {
}
}, [currentPhase]);
useEffect(() => {
if (result) {
const message = result.isWin ? "Win" : "Defeated" + " " + result.reason;
notify.open({
message,
placement: "bottom",
style: style,
});
}
}, [result]);
return (
<>
{notifyContextHolder}
......
......@@ -3,6 +3,7 @@ export * from "./AnnounceModal";
export * from "./CardListModal";
export * from "./CardModal";
export * from "./CheckCounterModal";
export * from "./EndModal";
export * from "./HintNotification";
export * from "./OptionModal";
export * from "./PositionModal";
......
......@@ -142,7 +142,7 @@ const WaitRoom = () => {
sendUpdateDeck(deck);
setChoseDeck(true);
window.myExtraDeckCodes = deck.extra;
window.myExtraDeckCodes = [...deck.extra];
};
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