Commit 5a0a8822 authored by Chunchi Che's avatar Chunchi Che

Merge branch 'feat/ai_predict' into 'main'

人机模式增加AI预测功能

See merge request !390
parents 0541025d 7b5e4828
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
"entertainWatchUrl": "wss://tiramisu.moecube.com:7923", "entertainWatchUrl": "wss://tiramisu.moecube.com:7923",
"userApi": "https://sapi.moecube.com:444/accounts/users/{username}.json", "userApi": "https://sapi.moecube.com:444/accounts/users/{username}.json",
"mdproServer": "https://rarnu.xyz:38443", "mdproServer": "https://rarnu.xyz:38443",
"agentServer": "https://t1v-n-e71fca19-w-0.tail0aad8.ts.net",
"streamInterval": 20, "streamInterval": 20,
"startDelay": 1000, "startDelay": 1000,
"ui": { "ui": {
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
"entertainWatchUrl": "wss://tiramisu.moecube.com:7923", "entertainWatchUrl": "wss://tiramisu.moecube.com:7923",
"userApi": "https://sapi.moecube.com:444/accounts/users/{username}.json", "userApi": "https://sapi.moecube.com:444/accounts/users/{username}.json",
"mdproServer": "https://rarnu.xyz:38443", "mdproServer": "https://rarnu.xyz:38443",
"agentServer": "https://t1v-n-e71fca19-w-0.tail0aad8.ts.net",
"streamInterval": 20, "streamInterval": 20,
"startDelay": 1000, "startDelay": 1000,
"ui": { "ui": {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -3,9 +3,13 @@ ...@@ -3,9 +3,13 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.3.7",
"@ant-design/pro-components": "^2.6.12", "@ant-design/pro-components": "^2.6.12",
"@ant-design/pro-provider": "^2.14.7",
"@dnd-kit/core": "^6.0.8", "@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2", "@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.2",
"@react-spring/shared": "^9.7.3",
"@react-spring/web": "^9.7.3", "@react-spring/web": "^9.7.3",
"antd": "^5.8.3", "antd": "^5.8.3",
"classnames": "^2.3.2", "classnames": "^2.3.2",
...@@ -16,6 +20,7 @@ ...@@ -16,6 +20,7 @@
"i18next": "^23.11.4", "i18next": "^23.11.4",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"overlayscrollbars": "^2.9.1",
"overlayscrollbars-react": "^0.5.1", "overlayscrollbars-react": "^0.5.1",
"rdndmb-html5-to-touch": "^8.0.3", "rdndmb-html5-to-touch": "^8.0.3",
"react": "^18.2.0", "react": "^18.2.0",
......
...@@ -7,3 +7,16 @@ export * from "./ocgcore/idl/ocgcore"; ...@@ -7,3 +7,16 @@ export * from "./ocgcore/idl/ocgcore";
export * from "./ocgcore/ocgHelper"; export * from "./ocgcore/ocgHelper";
export * from "./strings"; export * from "./strings";
export * from "./superPreRelease"; export * from "./superPreRelease";
export * from "./ygoAgent";
export async function handleHttps<T>(
resp: Response,
api: string,
): Promise<T | undefined> {
if (!resp.ok) {
console.error(`Https error from api ${api}! status: ${resp.status}`);
return undefined;
} else {
return await resp.json();
}
}
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { handleHttps } from "..";
import { MdproResp } from "./schema"; import { MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util"; import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/sync/single"; const API_PATH = "/api/mdpro3/sync/single";
......
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { handleHttps } from "..";
import { MdproResp } from "./schema"; import { MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util"; import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
......
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { handleHttps } from "..";
import { MdproDeck, MdproResp } from "./schema"; import { MdproDeck, MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util"; import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
......
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { pfetch } from "@/infra"; import { pfetch } from "@/infra";
import { handleHttps } from "..";
import { MdproDeck, MdproResp } from "./schema"; import { MdproDeck, MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util"; import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/sync/"; const API_PATH = "/api/mdpro3/sync/";
......
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { pfetch } from "@/infra"; import { pfetch } from "@/infra";
import { handleHttps } from "..";
import { MdproDeckLike, MdproResp } from "./schema"; import { MdproDeckLike, MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util"; import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
const API_PATH = "api/mdpro3/deck/list"; const API_PATH = "api/mdpro3/deck/list";
......
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { handleHttps } from "..";
import { MdproResp } from "./schema"; import { MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util"; import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/sync/single"; const API_PATH = "/api/mdpro3/sync/single";
......
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { handleHttps } from "..";
import { MdproResp } from "./schema"; import { MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util"; import { mdproHeaders } from "./util";
const { mdproServer } = useConfig(); const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/deck/public"; const API_PATH = "/api/mdpro3/deck/public";
......
...@@ -5,17 +5,3 @@ export function mdproHeaders(): Headers { ...@@ -5,17 +5,3 @@ export function mdproHeaders(): Headers {
return myHeaders; return myHeaders;
} }
export async function handleHttps<T>(
resp: Response,
api: string,
): Promise<T | undefined> {
if (!resp.ok) {
console.error(
`[Mdpro] Https error from api ${api}! status: ${resp.status}`,
);
return undefined;
} else {
return await resp.json();
}
}
This diff is collapsed.
import { useConfig } from "@/config";
import { handleHttps } from "..";
import { agentHeader } from "./util";
const { agentServer } = useConfig();
const API_PATH = "v0/duels";
interface CreateResp {
duelId: string;
index: number;
}
export async function createDuel(): Promise<CreateResp | undefined> {
const headers = agentHeader();
const resp = await fetch(`${agentServer}/${API_PATH}`, {
method: "POST",
headers,
redirect: "follow",
});
return await handleHttps(resp, API_PATH);
}
import { useConfig } from "@/config";
import { handleHttps } from "..";
import { agentHeader } from "./util";
const { agentServer } = useConfig();
const API_PATH = "/v0/duels";
export async function deleteDuel(duelId: string): Promise<void | undefined> {
const headers = agentHeader();
const apiPath = `${agentServer}/${API_PATH}/${duelId}`;
const resp = await fetch(apiPath, {
method: "DELETE",
headers,
redirect: "follow",
});
return await handleHttps(resp, apiPath);
}
export * from "./create";
export * from "./delete";
export * from "./predict";
export * from "./transaction";
import { useConfig } from "@/config";
import { handleHttps } from "..";
import { Input, MsgResponse } from "./schema";
import { agentHeader } from "./util";
const { agentServer } = useConfig();
const apiPath = (duelId: string) => `v0/duels/${duelId}/predict`;
export interface PredictReq {
/**
* The index must be equal to the index from the previous response of the same duelId.
*/
index: number;
input: Input;
prev_action_idx: number;
}
interface PredictResp {
/**
* It will be equal to the request's index + 1.
*/
index: number;
predict_results: MsgResponse;
}
export async function predictDuel(
duelId: string,
req: PredictReq,
): Promise<PredictResp | undefined> {
const headers = {
...agentHeader(),
"Content-Type": "application/json",
};
const resp = await fetch(`${agentServer}/${apiPath(duelId)}`, {
method: "POST",
headers,
body: JSON.stringify(req),
redirect: "follow",
});
return await handleHttps(resp, apiPath(duelId));
}
// Data schema for YgoAgent Service
/**
* none for N/A or unknown or token.
*/
export enum Attribute {
None = "none",
Earth = "earth",
Water = "water",
Fire = "fire",
Wind = "wind",
Light = "light",
Dark = "dark",
Divine = "divine",
}
export enum Controller {
Me = "me",
Opponent = "opponent",
}
//
export enum Location {
Deck = "deck",
Extra = "extra",
Grave = "grave",
Hand = "hand",
MZone = "mzone",
Removed = "removed",
SZone = "szone",
}
interface Place {
controller: Controller;
location: Location;
/**
* Start from 0
*/
sequence: number;
}
interface Option {
code: number;
}
export interface CardLocation {
controller: Controller;
location: Location;
/**
* if is overlay, this is the overlay index, starting from 0, else -1.
*/
overlay_sequence: number;
/**
* Start from 0
*/
sequence: number;
}
/**
* If the monster is xyz material (overlay_sequence != -1), the position is faceup.
*/
export enum Position {
None = "none",
FaceupAttack = "faceup_attack",
FacedownAttack = "facedown_attack",
Attack = "attack",
FaceupDefense = "faceup_defense",
Faceup = "faceup",
FacedownDefense = "facedown_defense",
Facedown = "facedown",
Defense = "defense",
}
/**
* none for N/A or unknown or token.
*/
export enum Race {
Aqua = "aqua",
Beast = "beast",
BeastWarrior = "beast_warrior",
CreatorGod = "creator_god",
Cyberse = "cyberse",
Devine = "devine",
Dinosaur = "dinosaur",
Dragon = "dragon",
Fairy = "fairy",
Fiend = "fiend",
Fish = "fish",
Illusion = "illusion",
Insect = "insect",
Machine = "machine",
None = "none",
Plant = "plant",
Psycho = "psycho",
Pyro = "pyro",
Reptile = "reptile",
Rock = "rock",
SeaSerpent = "sea_serpent",
Spellcaster = "spellcaster",
Thunder = "thunder",
Warrior = "warrior",
Windbeast = "windbeast",
Wyrm = "wyrm",
Zombie = "zombie",
}
export enum Type {
Continuous = "continuous",
Counter = "counter",
Dual = "dual",
Effect = "effect",
Equip = "equip",
Field = "field",
Flip = "flip",
Fusion = "fusion",
Link = "link",
Monster = "monster",
Normal = "normal",
Pendulum = "pendulum",
QuickPlay = "quick_play",
Ritual = "ritual",
Special = "special",
Spell = "spell",
Spirit = "spirit",
Synchro = "synchro",
Token = "token",
Toon = "toon",
Trap = "trap",
TrapMonster = "trap_monster",
Tuner = "tuner",
Union = "union",
Xyz = "xyz",
}
export interface Card {
/**
* Card code from cards.cdb
*/
code: number;
location: Location;
/**
* Sequence in ocgcore, 0 is N/A or unknown, if not, shoud start from 1. Only non-zero for
* cards in mzone, szone and grave.
*/
sequence: number;
controller: Controller;
/**
* If the monster is xyz material (overlay_sequence != -1), the position is faceup.
*/
position: Position;
/**
* if is overlay, this is the overlay index, starting from 0, else -1.
*/
overlay_sequence: number;
/**
* none for N/A or unknown or token.
*/
attribute: Attribute;
/**
* none for N/A or unknown or token.
*/
race: Race;
/**
* Rank and link are also considered as level. 0 is N/A or unknown.
*/
level: number;
/**
* Number of counters. If there are 2 types of counters or more, we consider only the first
* type of counter.
*/
counter: number;
/**
* Whether the card effect is disabled or forbidden
*/
negated: boolean;
attack: number;
defense: number;
types: Type[];
}
export enum Phase {
Battle = "battle",
BattleStart = "battle_start",
BattleStep = "battle_step",
Damage = "damage",
DamageCalculation = "damage_calculation",
Draw = "draw",
End = "end",
Main1 = "main1",
Main2 = "main2",
Standby = "standby",
}
export interface Global {
/**
* Whether me is the first player
*/
is_first: boolean;
is_my_turn: boolean;
my_lp: number;
op_lp: number;
phase: Phase;
turn: number;
}
interface SelectAbleCard {
location: CardLocation;
response: number;
}
export interface MsgSelectCard {
msg_type: "select_card";
cancelable: boolean;
min: number;
max: number;
cards: SelectAbleCard[];
selected: number[];
}
export type MultiSelectMsg = MsgSelectCard | MsgSelectSum | MsgSelectTribute;
interface SelectTributeCard {
location: CardLocation;
level: number;
response: number;
}
export interface MsgSelectTribute {
msg_type: "select_tribute";
cancelable: boolean;
min: number;
max: number;
cards: SelectTributeCard[];
selected: number[];
}
interface SelectSumCard {
location: CardLocation;
level1: number;
level2: number;
response: number;
}
export interface MsgSelectSum {
msg_type: "select_sum";
overflow: boolean;
level_sum: number;
min: number;
max: number;
cards: SelectSumCard[];
must_cards: SelectSumCard[];
selected: number[];
}
export interface CardInfo {
code: number;
controller: Controller;
location: Location;
sequence: number;
}
export enum IdleCmdType {
Summon = "summon",
SpSummon = "sp_summon",
Reposition = "reposition",
Mset = "mset",
Set = "set",
Activate = "activate",
ToBp = "to_bp",
ToEp = "to_ep",
}
interface IdleCmdData {
card_info: CardInfo;
effect_description: number;
response: number;
}
export interface IdleCmd {
cmd_type: IdleCmdType;
data?: IdleCmdData;
}
export interface MsgSelectIdleCmd {
msg_type: "select_idlecmd";
idle_cmds: IdleCmd[];
}
export interface Chain {
code: number;
location: CardLocation;
effect_description: number;
response: number;
}
export interface MsgSelectChain {
msg_type: "select_chain";
forced: boolean;
chains: Chain[];
}
export interface MsgSelectPosition {
msg_type: "select_position";
code: number;
positions: Position[];
}
export interface MsgSelectYesNo {
msg_type: "select_yesno";
effect_description: number;
}
export interface MsgSelectEffectYn {
msg_type: "select_effectyn";
code: number;
location: CardLocation;
effect_description: number;
}
export enum BattleCmdType {
Attack = "attack",
Activate = "activate",
ToM2 = "to_m2",
ToEp = "to_ep",
}
export interface BattleCmdData {
card_info: CardInfo;
effect_description: number;
direct_attackable: boolean;
response: number;
}
export interface BattleCmd {
cmd_type: BattleCmdType;
data?: BattleCmdData;
}
export interface MsgSelectBattleCmd {
msg_type: "select_battlecmd";
battle_cmds: BattleCmd[];
}
export interface SelectUnselectCard {
location: CardLocation;
response: number;
}
export interface MsgSelectUnselectCard {
msg_type: "select_unselect_card";
finishable: boolean;
cancelable: boolean;
min: number;
max: number;
selected_cards: SelectUnselectCard[];
selectable_cards: SelectUnselectCard[];
}
interface Option {
code: number;
response: number;
}
export interface MsgSelectOption {
msg_type: "select_option";
options: Option[];
}
interface Place {
controller: Controller;
location: Location;
sequence: number;
}
export interface MsgSelectPlace {
msg_type: "select_place";
count: number;
places: Place[];
}
interface AnnounceAttrib {
attribute: Attribute;
response: number;
}
export interface MsgAnnounceAttrib {
msg_type: "announce_attrib";
count: number;
attributes: AnnounceAttrib[];
}
interface AnnounceNumber {
number: number;
response: number;
}
export interface MsgAnnounceNumber {
msg_type: "announce_number";
count: number;
numbers: AnnounceNumber[];
}
type ActionMsgData =
| MsgSelectCard
| MsgSelectTribute
| MsgSelectSum
| MsgSelectIdleCmd
| MsgSelectChain
| MsgSelectPosition
| MsgSelectYesNo
| MsgSelectEffectYn
| MsgSelectBattleCmd
| MsgSelectUnselectCard
| MsgSelectOption
| MsgSelectPlace
| MsgAnnounceAttrib
| MsgAnnounceNumber;
export interface ActionMsg {
data: ActionMsgData;
}
export interface Input {
action_msg: ActionMsg;
cards: Card[];
global: Global;
}
interface ActionPredict {
prob: number;
response: number;
can_finish: boolean;
}
export interface MsgResponse {
action_preds: ActionPredict[];
win_rate: number;
}
This diff is collapsed.
export function agentHeader(): Headers {
const myHeaders = new Headers();
myHeaders.append("User-Agent", "Apifox/1.0.0 (https://apifox.com)");
return myHeaders;
}
...@@ -3,31 +3,38 @@ import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType; ...@@ -3,31 +3,38 @@ import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType;
import { CardMeta } from "@/api"; import { CardMeta } from "@/api";
//! 一些Neos中基础的数据结构 //! 一些Neos中基础的数据结构
// Position
export const FACEUP_ATTACK = 0x1;
export const FACEDOWN_ATTACK = 0x2;
export const FACEUP_DEFENSE = 0x4;
export const FACEDOWN_DEFENSE = 0x8;
// 类型 // 类型
const TYPE_MONSTER = 0x1; // export const TYPE_MONSTER = 0x1; //
const TYPE_SPELL = 0x2; // export const TYPE_SPELL = 0x2; //
const TYPE_TRAP = 0x4; // export const TYPE_TRAP = 0x4; //
const TYPE_NORMAL = 0x10; // export const TYPE_NORMAL = 0x10; //
const TYPE_EFFECT = 0x20; // export const TYPE_EFFECT = 0x20; //
const TYPE_FUSION = 0x40; // export const TYPE_FUSION = 0x40; //
const TYPE_RITUAL = 0x80; // export const TYPE_RITUAL = 0x80; //
const TYPE_TRAPMONSTER = 0x100; // export const TYPE_TRAPMONSTER = 0x100; //
const TYPE_SPIRIT = 0x200; // export const TYPE_SPIRIT = 0x200; //
const TYPE_UNION = 0x400; // export const TYPE_UNION = 0x400; //
const TYPE_DUAL = 0x800; // export const TYPE_DUAL = 0x800; //
const TYPE_TUNER = 0x1000; // export const TYPE_TUNER = 0x1000; //
const TYPE_SYNCHRO = 0x2000; // export const TYPE_SYNCHRO = 0x2000; //
export const TYPE_TOKEN = 0x4000; // export const TYPE_TOKEN = 0x4000; //
const TYPE_QUICKPLAY = 0x10000; // export const TYPE_QUICKPLAY = 0x10000; //
const TYPE_CONTINUOUS = 0x20000; // export const TYPE_CONTINUOUS = 0x20000; //
const TYPE_EQUIP = 0x40000; // export const TYPE_EQUIP = 0x40000; //
const TYPE_FIELD = 0x80000; // export const TYPE_FIELD = 0x80000; //
const TYPE_COUNTER = 0x100000; // export const TYPE_COUNTER = 0x100000; //
const TYPE_FLIP = 0x200000; // export const TYPE_FLIP = 0x200000; //
const TYPE_TOON = 0x400000; // export const TYPE_TOON = 0x400000; //
const TYPE_XYZ = 0x800000; // export const TYPE_XYZ = 0x800000; //
const TYPE_PENDULUM = 0x1000000; // export const TYPE_PENDULUM = 0x1000000; //
const TYPE_SPSUMMON = 0x2000000; // export const TYPE_SPSUMMON = 0x2000000; //
export const TYPE_LINK = 0x4000000; // export const TYPE_LINK = 0x4000000; //
/* /*
...@@ -147,13 +154,13 @@ export function isPendulumMonster(typeCode: number): boolean { ...@@ -147,13 +154,13 @@ export function isPendulumMonster(typeCode: number): boolean {
// 属性 // 属性
// const ATTRIBUTE_ALL = 0x7f; // // const ATTRIBUTE_ALL = 0x7f; //
const ATTRIBUTE_EARTH = 0x01; // export const ATTRIBUTE_EARTH = 0x01; //
const ATTRIBUTE_WATER = 0x02; // export const ATTRIBUTE_WATER = 0x02; //
const ATTRIBUTE_FIRE = 0x04; // export const ATTRIBUTE_FIRE = 0x04; //
const ATTRIBUTE_WIND = 0x08; // export const ATTRIBUTE_WIND = 0x08; //
const ATTRIBUTE_LIGHT = 0x10; // export const ATTRIBUTE_LIGHT = 0x10; //
const ATTRIBUTE_DARK = 0x20; // export const ATTRIBUTE_DARK = 0x20; //
const ATTRIBUTE_DEVINE = 0x40; // export const ATTRIBUTE_DEVINE = 0x40; //
export const Attribute2StringCodeMap: Map<number, number> = new Map([ export const Attribute2StringCodeMap: Map<number, number> = new Map([
[ATTRIBUTE_EARTH, 1010], [ATTRIBUTE_EARTH, 1010],
...@@ -166,31 +173,31 @@ export const Attribute2StringCodeMap: Map<number, number> = new Map([ ...@@ -166,31 +173,31 @@ export const Attribute2StringCodeMap: Map<number, number> = new Map([
]); ]);
// 种族 // 种族
const RACE_WARRIOR = 0x1; // export const RACE_WARRIOR = 0x1; //
const RACE_SPELLCASTER = 0x2; // export const RACE_SPELLCASTER = 0x2; //
const RACE_FAIRY = 0x4; // export const RACE_FAIRY = 0x4; //
const RACE_FIEND = 0x8; // export const RACE_FIEND = 0x8; //
const RACE_ZOMBIE = 0x10; // export const RACE_ZOMBIE = 0x10; //
const RACE_MACHINE = 0x20; // export const RACE_MACHINE = 0x20; //
const RACE_AQUA = 0x40; // export const RACE_AQUA = 0x40; //
const RACE_PYRO = 0x80; // export const RACE_PYRO = 0x80; //
const RACE_ROCK = 0x100; // export const RACE_ROCK = 0x100; //
const RACE_WINDBEAST = 0x200; // export const RACE_WINDBEAST = 0x200; //
const RACE_PLANT = 0x400; // export const RACE_PLANT = 0x400; //
const RACE_INSECT = 0x800; // export const RACE_INSECT = 0x800; //
const RACE_THUNDER = 0x1000; // export const RACE_THUNDER = 0x1000; //
const RACE_DRAGON = 0x2000; // export const RACE_DRAGON = 0x2000; //
const RACE_BEAST = 0x4000; // export const RACE_BEAST = 0x4000; //
const RACE_BEASTWARRIOR = 0x8000; // export const RACE_BEASTWARRIOR = 0x8000; //
const RACE_DINOSAUR = 0x10000; // export const RACE_DINOSAUR = 0x10000; //
const RACE_FISH = 0x20000; // export const RACE_FISH = 0x20000; //
const RACE_SEASERPENT = 0x40000; // export const RACE_SEASERPENT = 0x40000; //
const RACE_REPTILE = 0x80000; // export const RACE_REPTILE = 0x80000; //
const RACE_PSYCHO = 0x100000; // export const RACE_PSYCHO = 0x100000; //
const RACE_DEVINE = 0x200000; // export const RACE_DEVINE = 0x200000; //
const RACE_CREATORGOD = 0x400000; // export const RACE_CREATORGOD = 0x400000; //
const RACE_WYRM = 0x800000; // export const RACE_WYRM = 0x800000; //
const RACE_CYBERSE = 0x1000000; // export const RACE_CYBERSE = 0x1000000; //
export const Race2StringCodeMap: Map<number, number> = new Map([ export const Race2StringCodeMap: Map<number, number> = new Map([
[RACE_WARRIOR, 1020], [RACE_WARRIOR, 1020],
......
import { WebSocketStream } from "@/infra";
import {
cardStore,
chatStore,
matStore,
placeStore,
roomStore,
} from "@/stores";
import { CONTAINERS } from ".";
import { Context } from "./context";
import { Container } from "./impl";
const UI_KEY = "NEOS_UI";
export function initUIContainer(conn: WebSocketStream) {
const context = new Context({
matStore,
cardStore,
placeStore,
roomStore,
chatStore,
});
const container = new Container(context, conn);
CONTAINERS.set(UI_KEY, container);
}
export function getUIContainer(): Container {
const container = CONTAINERS.get(UI_KEY);
if (container) {
return container;
} else {
throw Error("UI Container not initialized !!");
}
}
// Context of a Duel, containing datas and states
// that we need to interact with server and player
import {
CardStore,
ChatStore,
MatStore,
PlaceStore,
RoomStore,
SideStore,
} from "@/stores";
interface ContextInitInfo {
matStore?: MatStore;
cardStore?: CardStore;
placeStore?: PlaceStore;
roomStore?: RoomStore;
chatStore?: ChatStore;
sideStore?: SideStore;
}
export class Context {
public matStore: MatStore;
public cardStore: CardStore;
public placeStore: PlaceStore;
public roomStore: RoomStore;
public chatStore: ChatStore;
public sideStore: SideStore;
constructor();
constructor(initInfo: ContextInitInfo);
constructor(initInfo?: ContextInitInfo) {
const { matStore, cardStore, placeStore, roomStore, chatStore, sideStore } =
initInfo ?? {};
this.matStore = matStore ?? new MatStore();
this.cardStore = cardStore ?? new CardStore();
this.placeStore = placeStore ?? new PlaceStore();
this.roomStore = roomStore ?? new RoomStore();
this.chatStore = chatStore ?? new ChatStore();
this.sideStore = sideStore ?? new SideStore();
}
}
import { WebSocketStream } from "@/infra";
import { Context } from "./context";
export class Container {
public context: Context;
public conn: WebSocketStream;
// ref: https://yugioh.fandom.com/wiki/Kuriboh
private enableKuriboh: boolean = false;
constructor(context: Context, conn: WebSocketStream) {
this.context = context;
this.conn = conn;
}
public setEnableKuriboh(value: boolean) {
this.enableKuriboh = value;
}
public getEnableKuriboh(): boolean {
return this.enableKuriboh;
}
}
import { Container } from "./impl";
export { Context } from "./context";
export { Container } from "./impl";
// Global collection of `Container`s
export const CONTAINERS: Map<string, Container> = new Map();
...@@ -6,79 +6,36 @@ ...@@ -6,79 +6,36 @@
* */ * */
import { WebSocketStream } from "@/infra"; import { WebSocketStream } from "@/infra";
import handleSocketMessage from "../service/onSocketMessage";
import handleSocketOpen from "../service/onSocketOpen"; import handleSocketOpen from "../service/onSocketOpen";
export enum socketCmd { // FIXME: 应该有个返回值,告诉业务方本次请求的结果。比如建立长连接失败。
// 建立长连接 export function initSocket(initInfo: {
CONNECT, ip: string;
// 断开长连接 player: string;
DISCONNECT, passWd: string;
// 通过长连接发送数据 }): WebSocketStream {
SEND, const { ip, player, passWd } = initInfo;
return new WebSocketStream(ip, (conn, _event) =>
handleSocketOpen(conn, ip, player, passWd),
);
} }
export interface socketAction { export function initReplaySocket(replayInfo: {
cmd: socketCmd; url: string; // 提供回放服务的地址
// 创建长连接需要业务方传入的数据 data: ArrayBuffer; // 回放数据
initInfo?: { }): WebSocketStream {
ip: string; const { url, data } = replayInfo;
player: string; return new WebSocketStream(url, (conn, _event) => {
passWd: string; console.info("replay websocket open.");
}; conn.binaryType = "arraybuffer";
isReplay?: boolean; // 是否是回放模式 conn.send(data);
replayInfo?: { });
Url: string; // 提供回放服务的地址
data: ArrayBuffer; // 回放数据
};
// 通过长连接发送的数据
payload?: Uint8Array;
} }
let ws: WebSocketStream | null = null; export function sendSocketData(conn: WebSocketStream, payload: Uint8Array) {
conn.ws.send(payload);
// FIXME: 应该有个返回值,告诉业务方本次请求的结果。比如建立长连接失败。 }
export default async function (action: socketAction) {
switch (action.cmd) {
case socketCmd.CONNECT: {
const { initInfo: info, isReplay, replayInfo } = action;
if (info) {
ws = new WebSocketStream(info.ip, (conn, _event) =>
handleSocketOpen(conn, info.ip, info.player, info.passWd),
);
await ws.execute(handleSocketMessage);
} else if (isReplay && replayInfo) {
ws = new WebSocketStream(replayInfo.Url, (conn, _event) => {
console.info("replay websocket open.");
conn.binaryType = "arraybuffer";
conn.send(replayInfo.data);
});
await ws.execute(handleSocketMessage);
}
break;
}
case socketCmd.DISCONNECT: {
if (ws) {
ws.close();
}
break;
}
case socketCmd.SEND: {
const payload = action.payload;
if (ws && payload) {
ws.ws.send(payload);
}
break;
}
default: {
console.log("Unhandled socket command: " + action.cmd);
break; export function closeSocket(conn: WebSocketStream) {
} conn.close();
}
} }
// TODO: this middleware should be managed under `Container`, too.
import { isNil } from "lodash-es"; import { isNil } from "lodash-es";
import { Database } from "sql.js"; import { Database } from "sql.js";
......
This diff is collapsed.
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { Container } from "@/container";
import { replayStore } from "@/stores"; import { replayStore } from "@/stores";
import { showWaiting } from "@/ui/Duel/Message"; import { showWaiting } from "@/ui/Duel/Message";
import { YgoAgent } from "./agent";
import onAnnounce from "./announce"; import onAnnounce from "./announce";
import onMsgAttack from "./attack"; import onMsgAttack from "./attack";
import onMsgAttackDisable from "./attackDisable"; import onMsgAttackDisable from "./attackDisable";
...@@ -68,19 +70,6 @@ const ActiveList = [ ...@@ -68,19 +70,6 @@ const ActiveList = [
"select_battle_cmd", "select_battle_cmd",
"select_unselect_card", "select_unselect_card",
"select_yes_no", "select_yes_no",
];
const ReplayIgnoreMsg = [
"select_idle_cmd",
"select_place",
"select_card",
"select_chain",
"select_effect_yn",
"select_position",
"select_option",
"select_battle_cmd",
"select_unselect_card",
"select_yes_no",
"select_tribute", "select_tribute",
"select_counter", "select_counter",
"select_sum", "select_sum",
...@@ -90,20 +79,43 @@ const ReplayIgnoreMsg = [ ...@@ -90,20 +79,43 @@ const ReplayIgnoreMsg = [
]; ];
export default async function handleGameMsg( export default async function handleGameMsg(
container: Container,
pb: ygopro.YgoStocMsg, pb: ygopro.YgoStocMsg,
agent?: YgoAgent,
): Promise<void> { ): Promise<void> {
const msg = pb.stoc_game_msg; const msg = pb.stoc_game_msg;
if (ActiveList.includes(msg.gameMsg)) { if (ActiveList.includes(msg.gameMsg)) {
showWaiting(false); showWaiting(false);
}
if (replayStore.isReplay && ReplayIgnoreMsg.includes(msg.gameMsg)) return; if (replayStore.isReplay) return;
if (agent && !agent.getDisable()) {
console.info(`Handling msg: ${msg.gameMsg} with YgoAgent`);
const enableKuriboh = container.getEnableKuriboh();
try {
await agent.sendAIPredictAsResponse(container.conn, msg, enableKuriboh);
if (enableKuriboh) return;
} catch (e) {
console.error(`Erros occurs when handling msg ${msg.gameMsg}: ${e}`);
container.setEnableKuriboh(false);
// TODO: I18N
container.context.matStore.error = `AI模型监测到场上存在它没见过的卡片,
因此需要关掉AI辅助功能。\n
请耐心等待开发团队对模型进行优化,感谢!`;
agent.setDisable(true);
}
}
}
switch (msg.gameMsg) { switch (msg.gameMsg) {
case "start": { case "start": {
await onMsgStart(msg.start); await onMsgStart(msg.start);
// We should init agent when the MSG_START reached.
if (agent) await agent.init();
break; break;
} }
case "draw": { case "draw": {
...@@ -132,7 +144,7 @@ export default async function handleGameMsg( ...@@ -132,7 +144,7 @@ export default async function handleGameMsg(
break; break;
} }
case "select_place": { case "select_place": {
onMsgSelectPlace(msg.select_place); onMsgSelectPlace(container, msg.select_place);
break; break;
} }
...@@ -141,12 +153,12 @@ export default async function handleGameMsg( ...@@ -141,12 +153,12 @@ export default async function handleGameMsg(
break; break;
} }
case "select_card": { case "select_card": {
onMsgSelectCard(msg.select_card); onMsgSelectCard(container, msg.select_card);
break; break;
} }
case "select_chain": { case "select_chain": {
onMsgSelectChain(msg.select_chain); onMsgSelectChain(container, msg.select_chain);
break; break;
} }
...@@ -161,7 +173,7 @@ export default async function handleGameMsg( ...@@ -161,7 +173,7 @@ export default async function handleGameMsg(
break; break;
} }
case "select_option": { case "select_option": {
await onMsgSelectOption(msg.select_option); await onMsgSelectOption(container, msg.select_option);
break; break;
} }
......
...@@ -6,4 +6,5 @@ export default (newTurn: ygopro.StocGameMessage.MsgNewTurn) => { ...@@ -6,4 +6,5 @@ export default (newTurn: ygopro.StocGameMessage.MsgNewTurn) => {
playEffect(AudioActionType.SOUND_NEXT_TURN); playEffect(AudioActionType.SOUND_NEXT_TURN);
const player = newTurn.player; const player = newTurn.player;
matStore.currentPlayer = player; matStore.currentPlayer = player;
matStore.turnCount = matStore.turnCount + 1;
}; };
...@@ -8,11 +8,12 @@ import { ...@@ -8,11 +8,12 @@ import {
import MsgSelectBattleCmd = ygopro.StocGameMessage.MsgSelectBattleCmd; import MsgSelectBattleCmd = ygopro.StocGameMessage.MsgSelectBattleCmd;
export default (selectBattleCmd: MsgSelectBattleCmd) => { export default async (selectBattleCmd: MsgSelectBattleCmd) => {
const player = selectBattleCmd.player; const player = selectBattleCmd.player;
const cmds = selectBattleCmd.battle_cmds; const cmds = selectBattleCmd.battle_cmds;
// 先清掉之前的互动性 // 先清掉之前的互动性
// TODO: 确认这里在AI托管的模式下是否需要
cardStore.inner.forEach((card) => { cardStore.inner.forEach((card) => {
card.idleInteractivities = []; card.idleInteractivities = [];
}); });
......
import { sendSelectMultiResponse, ygopro } from "@/api"; import { sendSelectMultiResponse, ygopro } from "@/api";
import MsgSelectCard = ygopro.StocGameMessage.MsgSelectCard; import MsgSelectCard = ygopro.StocGameMessage.MsgSelectCard;
import { Container } from "@/container";
import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal"; import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
import { fetchCheckCardMeta } from "../utils"; import { fetchCheckCardMeta } from "../utils";
export default async (selectCard: MsgSelectCard) => { export default async (container: Container, selectCard: MsgSelectCard) => {
const { cancelable, min, max, cards } = selectCard; const { cancelable, min, max, cards } = selectCard;
const conn = container.conn;
// TODO: handle release_param // TODO: handle release_param
if (!cancelable && cards.length === 1) { if (!cancelable && cards.length === 1) {
// auto send // auto send
sendSelectMultiResponse([cards[0].response]); sendSelectMultiResponse(conn, [cards[0].response]);
return; return;
} }
......
import { sendSelectSingleResponse, ygopro } from "@/api"; import { sendSelectSingleResponse, ygopro } from "@/api";
import { Container } from "@/container";
import { ChainSetting, fetchSelectHintMeta, matStore } from "@/stores"; import { ChainSetting, fetchSelectHintMeta, matStore } from "@/stores";
import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal"; import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
import { fetchCheckCardMeta } from "../utils"; import { fetchCheckCardMeta } from "../utils";
type MsgSelectChain = ygopro.StocGameMessage.MsgSelectChain; type MsgSelectChain = ygopro.StocGameMessage.MsgSelectChain;
export default async (selectChain: MsgSelectChain) => { export default async (container: Container, selectChain: MsgSelectChain) => {
const conn = container.conn;
const spCount = selectChain.special_count; const spCount = selectChain.special_count;
const forced = selectChain.forced; const forced = selectChain.forced;
const _hint0 = selectChain.hint0; const _hint0 = selectChain.hint0;
...@@ -15,7 +17,7 @@ export default async (selectChain: MsgSelectChain) => { ...@@ -15,7 +17,7 @@ export default async (selectChain: MsgSelectChain) => {
if (chainSetting === ChainSetting.CHAIN_IGNORE) { if (chainSetting === ChainSetting.CHAIN_IGNORE) {
// 如果玩家配置了忽略连锁,直接回应后端并返回 // 如果玩家配置了忽略连锁,直接回应后端并返回
sendSelectSingleResponse(-1); sendSelectSingleResponse(conn, -1);
return; return;
} }
...@@ -60,7 +62,7 @@ export default async (selectChain: MsgSelectChain) => { ...@@ -60,7 +62,7 @@ export default async (selectChain: MsgSelectChain) => {
switch (handle_flag) { switch (handle_flag) {
case 0: { case 0: {
// 直接回答 // 直接回答
sendSelectSingleResponse(-1); sendSelectSingleResponse(conn, -1);
break; break;
} }
...@@ -86,7 +88,7 @@ export default async (selectChain: MsgSelectChain) => { ...@@ -86,7 +88,7 @@ export default async (selectChain: MsgSelectChain) => {
} }
case 4: { case 4: {
// 有一张强制发动的卡,直接回应 // 有一张强制发动的卡,直接回应
sendSelectSingleResponse(chains[0].response); sendSelectSingleResponse(conn, chains[0].response);
break; break;
} }
......
...@@ -8,11 +8,12 @@ import { ...@@ -8,11 +8,12 @@ import {
import MsgSelectIdleCmd = ygopro.StocGameMessage.MsgSelectIdleCmd; import MsgSelectIdleCmd = ygopro.StocGameMessage.MsgSelectIdleCmd;
export default (selectIdleCmd: MsgSelectIdleCmd) => { export default async (selectIdleCmd: MsgSelectIdleCmd) => {
const player = selectIdleCmd.player; const player = selectIdleCmd.player;
const cmds = selectIdleCmd.idle_cmds; const cmds = selectIdleCmd.idle_cmds;
// 先清掉之前的互动性 // 先清掉之前的互动性
// TODO: 确认这里是否需要在AI托管的时候调用
cardStore.inner.forEach((card) => { cardStore.inner.forEach((card) => {
card.idleInteractivities = []; card.idleInteractivities = [];
}); });
......
...@@ -5,16 +5,22 @@ import { ...@@ -5,16 +5,22 @@ import {
sendSelectOptionResponse, sendSelectOptionResponse,
type ygopro, type ygopro,
} from "@/api"; } from "@/api";
import { Container } from "@/container";
import { displayOptionModal } from "@/ui/Duel/Message"; import { displayOptionModal } from "@/ui/Duel/Message";
export default async (selectOption: ygopro.StocGameMessage.MsgSelectOption) => { export default async (
container: Container,
selectOption: ygopro.StocGameMessage.MsgSelectOption,
) => {
const conn = container.conn;
const options = selectOption.options; const options = selectOption.options;
if (options.length === 0) { if (options.length === 0) {
sendSelectOptionResponse(0); sendSelectOptionResponse(conn, 0);
return; return;
} }
if (options.length === 1) { if (options.length === 1) {
sendSelectOptionResponse(options[0].response); sendSelectOptionResponse(conn, options[0].response);
return; return;
} }
......
import { sendSelectPlaceResponse, ygopro } from "@/api"; import { sendSelectPlaceResponse, ygopro } from "@/api";
import { Container } from "@/container";
import { InteractType, placeStore } from "@/stores"; import { InteractType, placeStore } from "@/stores";
type MsgSelectPlace = ygopro.StocGameMessage.MsgSelectPlace; type MsgSelectPlace = ygopro.StocGameMessage.MsgSelectPlace;
export default (selectPlace: MsgSelectPlace) => { export default async (container: Container, selectPlace: MsgSelectPlace) => {
const conn = container.conn;
if (selectPlace.count !== 1) { if (selectPlace.count !== 1) {
console.warn(`Unhandled case: ${selectPlace}`); console.warn(`Unhandled case: ${selectPlace}`);
return; return;
...@@ -11,7 +13,7 @@ export default (selectPlace: MsgSelectPlace) => { ...@@ -11,7 +13,7 @@ export default (selectPlace: MsgSelectPlace) => {
if (selectPlace.places.length === 1) { if (selectPlace.places.length === 1) {
const place = selectPlace.places[0]; const place = selectPlace.places[0];
sendSelectPlaceResponse({ sendSelectPlaceResponse(conn, {
controller: place.controller, controller: place.controller,
zone: place.zone, zone: place.zone,
sequence: place.sequence, sequence: place.sequence,
......
...@@ -5,10 +5,10 @@ import { fetchCheckCardMeta } from "../utils"; ...@@ -5,10 +5,10 @@ import { fetchCheckCardMeta } from "../utils";
type MsgSelectTribute = ygopro.StocGameMessage.MsgSelectTribute; type MsgSelectTribute = ygopro.StocGameMessage.MsgSelectTribute;
export default async (selectTribute: MsgSelectTribute) => { export default async (selectTribute: MsgSelectTribute) => {
// TODO: 当玩家选择卡数大于`max`时,是否也合法?
const { selecteds, mustSelects, selectables } = await fetchCheckCardMeta( const { selecteds, mustSelects, selectables } = await fetchCheckCardMeta(
selectTribute.selectable_cards, selectTribute.selectable_cards,
); );
// TODO: 当玩家选择卡数大于`max`时,是否也合法?
await displaySelectActionsModal({ await displaySelectActionsModal({
overflow: true, overflow: true,
totalLevels: 0, totalLevels: 0,
......
...@@ -6,14 +6,15 @@ import { fetchCheckCardMeta } from "../utils"; ...@@ -6,14 +6,15 @@ import { fetchCheckCardMeta } from "../utils";
import { isAllOnField } from "./util"; import { isAllOnField } from "./util";
type MsgSelectUnselectCard = ygopro.StocGameMessage.MsgSelectUnselectCard; type MsgSelectUnselectCard = ygopro.StocGameMessage.MsgSelectUnselectCard;
export default async ({ export default async (selectUnselectCards: MsgSelectUnselectCard) => {
finishable, const {
cancelable, finishable,
min, cancelable,
max, min,
selectable_cards: selectableCards, max,
selected_cards: selectedCards, selectable_cards: selectableCards,
}: MsgSelectUnselectCard) => { selected_cards: selectedCards,
} = selectUnselectCards;
if ( if (
isAllOnField( isAllOnField(
selectableCards.concat(selectedCards).map((info) => info.location), selectableCards.concat(selectedCards).map((info) => info.location),
......
import { sendTimeConfirm, ygopro } from "@/api"; import { sendTimeConfirm, ygopro } from "@/api";
import { Container } from "@/container";
import { matStore } from "@/stores"; import { matStore } from "@/stores";
export default function handleTimeLimit(timeLimit: ygopro.StocTimeLimit) { export default function handleTimeLimit(
container: Container,
timeLimit: ygopro.StocTimeLimit,
) {
matStore.timeLimits.set(timeLimit.player, timeLimit.left_time); matStore.timeLimits.set(timeLimit.player, timeLimit.left_time);
if (matStore.isMe(timeLimit.player)) { if (matStore.isMe(timeLimit.player)) {
sendTimeConfirm(); sendTimeConfirm(container.conn);
} }
} }
...@@ -8,3 +8,48 @@ export function isAllOnField(locations: ygopro.CardLocation[]): boolean { ...@@ -8,3 +8,48 @@ export function isAllOnField(locations: ygopro.CardLocation[]): boolean {
return locations.find((location) => !isOnField(location)) === undefined; return locations.find((location) => !isOnField(location)) === undefined;
} }
export function computeSetDifference(set1: number[], set2: number[]): number[] {
const freq1 = new Map<number, number>();
const freq2 = new Map<number, number>();
for (const num of set1) {
freq1.set(num, (freq1.get(num) || 0) + 1);
}
for (const num of set2) {
freq2.set(num, (freq2.get(num) || 0) + 1);
}
for (const [num, count] of freq2) {
if (freq1.has(num)) {
freq1.set(num, freq1.get(num)! - count);
}
}
const difference: number[] = [];
for (const [num, count] of freq1) {
if (count > 0) {
difference.push(...Array(count).fill(num));
}
}
return difference;
}
export function argmax<T>(arr: T[], getValue: (item: T) => number): number {
if (arr.length === 0) {
throw new Error("Array is empty");
}
let maxIndex = 0;
let maxValue = getValue(arr[0]);
for (let i = 1; i < arr.length; i++) {
const currentValue = getValue(arr[i]);
if (currentValue > maxValue) {
maxValue = currentValue;
maxIndex = i;
}
}
return maxIndex;
}
import { Container } from "@/container";
import { YgoAgent } from "./duel/agent";
import handleSocketMessage from "./onSocketMessage";
export async function pollSocketLooper(container: Container) {
await container.conn.execute((event) =>
handleSocketMessage(container, event),
);
}
export async function pollSocketLooperWithAgent(container: Container) {
const agent = new YgoAgent();
agent.attachContext(container.context);
await container.conn.execute((event) =>
handleSocketMessage(container, event, agent),
);
}
...@@ -4,8 +4,10 @@ ...@@ -4,8 +4,10 @@
* */ * */
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 { Container } from "@/container";
import { replayStore } from "@/stores"; import { replayStore } from "@/stores";
import { YgoAgent } from "./duel/agent";
import handleGameMsg from "./duel/gameMsg"; import handleGameMsg from "./duel/gameMsg";
import handleTimeLimit from "./duel/timeLimit"; import handleTimeLimit from "./duel/timeLimit";
import handleDeckCount from "./mora/deckCount"; import handleDeckCount from "./mora/deckCount";
...@@ -32,12 +34,21 @@ import { handleWaitingSide } from "./side/waitingSide"; ...@@ -32,12 +34,21 @@ import { handleWaitingSide } from "./side/waitingSide";
let animation: Promise<void> = Promise.resolve(); let animation: Promise<void> = Promise.resolve();
export default async function handleSocketMessage(e: MessageEvent) { export default async function handleSocketMessage(
container: Container,
e: MessageEvent,
agent?: YgoAgent,
) {
// 确保按序执行 // 确保按序执行
animation = animation.then(() => _handle(e)); animation = animation.then(() => _handle(container, e, agent));
} }
async function _handle(e: MessageEvent) { // FIXME: 下面的所有`handler`中访问`Store`的时候都应该通过`Container`进行访问
async function _handle(
container: Container,
e: MessageEvent,
agent?: YgoAgent,
) {
const packets = YgoProPacket.deserialize(e.data); const packets = YgoProPacket.deserialize(e.data);
for (const packet of packets) { for (const packet of packets) {
...@@ -97,12 +108,12 @@ async function _handle(e: MessageEvent) { ...@@ -97,12 +108,12 @@ async function _handle(e: MessageEvent) {
// 如果不是回放模式,则记录回放数据 // 如果不是回放模式,则记录回放数据
replayStore.record(packet); replayStore.record(packet);
} }
await handleGameMsg(pb); await handleGameMsg(container, pb, agent);
break; break;
} }
case "stoc_time_limit": { case "stoc_time_limit": {
handleTimeLimit(pb.stoc_time_limit); handleTimeLimit(container, pb.stoc_time_limit);
break; break;
} }
case "stoc_error_msg": { case "stoc_error_msg": {
......
...@@ -24,7 +24,7 @@ export interface CardType { ...@@ -24,7 +24,7 @@ export interface CardType {
}; };
} }
class CardStore implements NeosStore { export class CardStore implements NeosStore {
inner: CardType[] = []; inner: CardType[] = [];
at(zone: ygopro.CardZone, controller: number): CardType[]; at(zone: ygopro.CardZone, controller: number): CardType[];
at( at(
......
...@@ -2,15 +2,13 @@ import { proxy } from "valtio"; ...@@ -2,15 +2,13 @@ import { proxy } from "valtio";
import { type NeosStore } from "./shared"; import { type NeosStore } from "./shared";
export interface ChatState extends NeosStore { export class ChatStore implements NeosStore {
sender: number; sender: number = -1;
message: string; message: string = "";
reset(): void {
this.message = "";
}
} }
export const chatStore = proxy<ChatState>({ export const chatStore = proxy<ChatStore>(new ChatStore());
sender: -1,
message: "",
reset() {
chatStore.message = "";
},
});
...@@ -18,6 +18,7 @@ const getWhom = (controller: number): "me" | "op" => ...@@ -18,6 +18,7 @@ const getWhom = (controller: number): "me" | "op" =>
* 原本名字叫judgeSelf * 原本名字叫judgeSelf
*/ */
export const isMe = (controller: number): boolean => { export const isMe = (controller: number): boolean => {
// FIXME: all of the `matStore` need to access with container
switch (matStore.selfType) { switch (matStore.selfType) {
case 1: case 1:
// 自己是先攻 // 自己是先攻
...@@ -93,9 +94,11 @@ const initialState: Omit<MatState, "reset"> = { ...@@ -93,9 +94,11 @@ const initialState: Omit<MatState, "reset"> = {
duelEnd: false, duelEnd: false,
// methods // methods
isMe, isMe,
turnCount: 0,
error: "",
}; };
class MatStore implements MatState, NeosStore { export class MatStore implements MatState, NeosStore {
chains = initialState.chains; chains = initialState.chains;
chainSetting = initialState.chainSetting; chainSetting = initialState.chainSetting;
timeLimits = initialState.timeLimits; timeLimits = initialState.timeLimits;
...@@ -109,6 +112,9 @@ class MatStore implements MatState, NeosStore { ...@@ -109,6 +112,9 @@ class MatStore implements MatState, NeosStore {
tossResult = initialState.tossResult; tossResult = initialState.tossResult;
selectUnselectInfo = initialState.selectUnselectInfo; selectUnselectInfo = initialState.selectUnselectInfo;
duelEnd = initialState.duelEnd; duelEnd = initialState.duelEnd;
turnCount = initialState.turnCount;
error = initialState.error;
// methods // methods
isMe = initialState.isMe; isMe = initialState.isMe;
reset(): void { reset(): void {
...@@ -137,6 +143,8 @@ class MatStore implements MatState, NeosStore { ...@@ -137,6 +143,8 @@ class MatStore implements MatState, NeosStore {
selectedList: [], selectedList: [],
}; };
this.duelEnd = false; this.duelEnd = false;
this.turnCount = 0;
this.error = initialState.error;
} }
} }
......
...@@ -49,6 +49,9 @@ export interface MatState { ...@@ -49,6 +49,9 @@ export interface MatState {
/** 根据自己的先后手判断是否是自己 */ /** 根据自己的先后手判断是否是自己 */
isMe: (player: number) => boolean; isMe: (player: number) => boolean;
turnCount: number;
error: string;
} }
export interface InitInfo { export interface InitInfo {
......
...@@ -58,7 +58,7 @@ const initialState = { ...@@ -58,7 +58,7 @@ const initialState = {
}, },
}; };
class PlaceStore implements NeosStore { export class PlaceStore implements NeosStore {
inner: { inner: {
[zone: number]: { [zone: number]: {
me: BlockState[]; me: BlockState[];
...@@ -70,14 +70,15 @@ class PlaceStore implements NeosStore { ...@@ -70,14 +70,15 @@ class PlaceStore implements NeosStore {
controller: number; controller: number;
sequence: number; sequence: number;
}): BlockState | undefined { }): BlockState | undefined {
return placeStore.inner[location.zone][ return this.inner[location.zone][
// FIXME: inject `matStore`
matStore.isMe(location.controller) ? "me" : "op" matStore.isMe(location.controller) ? "me" : "op"
][location.sequence]; ][location.sequence];
} }
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) => {
placeStore.inner[where][who].forEach( this.inner[where][who].forEach(
(block) => (block.interactivity = undefined), (block) => (block.interactivity = undefined),
); );
}); });
...@@ -87,7 +88,7 @@ class PlaceStore implements NeosStore { ...@@ -87,7 +88,7 @@ class PlaceStore implements NeosStore {
const resetObj = cloneDeep(initialState); const resetObj = cloneDeep(initialState);
Object.keys(resetObj).forEach((key) => { Object.keys(resetObj).forEach((key) => {
// @ts-ignore // @ts-ignore
placeStore.inner[key] = resetObj[key]; this.inner[key] = resetObj[key];
}); });
} }
} }
......
...@@ -33,7 +33,7 @@ export enum RoomStage { ...@@ -33,7 +33,7 @@ export enum RoomStage {
DUEL_START = 6, // 决斗开始 DUEL_START = 6, // 决斗开始
} }
class RoomStore implements NeosStore { export class RoomStore implements NeosStore {
joined: boolean = false; // 是否已经加入房间 joined: boolean = false; // 是否已经加入房间
players: (Player | undefined)[] = Array.from({ length: 4 }).map( players: (Player | undefined)[] = Array.from({ length: 4 }).map(
(_) => undefined, (_) => undefined,
......
...@@ -15,7 +15,7 @@ export enum SideStage { ...@@ -15,7 +15,7 @@ export enum SideStage {
WAITING = 8, // 观战者等待双方玩家 WAITING = 8, // 观战者等待双方玩家
} }
class SideStore implements NeosStore { export class SideStore implements NeosStore {
stage: SideStage = SideStage.NONE; stage: SideStage = SideStage.NONE;
// 因为在上一局可能会出现断线重连, // 因为在上一局可能会出现断线重连,
......
...@@ -4,11 +4,13 @@ import { useNavigate } from "react-router-dom"; ...@@ -4,11 +4,13 @@ import { useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { sendSurrender } from "@/api"; import { sendSurrender } from "@/api";
import { getUIContainer } from "@/container/compat";
import { matStore } from "@/stores"; import { matStore } from "@/stores";
export const Alert = () => { export const Alert = () => {
const matSnap = useSnapshot(matStore); const matSnap = useSnapshot(matStore);
const unimplemented = matSnap.unimplemented; const unimplemented = matSnap.unimplemented;
const container = getUIContainer();
const navigate = useNavigate(); const navigate = useNavigate();
...@@ -24,7 +26,7 @@ export const Alert = () => { ...@@ -24,7 +26,7 @@ export const Alert = () => {
banner banner
afterClose={() => { afterClose={() => {
// 发送投降信号 // 发送投降信号
sendSurrender(); sendSurrender(container.conn);
navigate("/match"); navigate("/match");
}} }}
/> />
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
sendSelectOptionResponse, sendSelectOptionResponse,
} from "@/api"; } from "@/api";
import { isDeclarable, isToken } from "@/common"; import { isDeclarable, isToken } from "@/common";
import { getUIContainer } from "@/container/compat";
import { emptySearchConditions } from "@/middleware/sqlite/fts"; import { emptySearchConditions } from "@/middleware/sqlite/fts";
import { NeosModal } from "../NeosModal"; import { NeosModal } from "../NeosModal";
...@@ -35,6 +36,7 @@ export const AnnounceModal: React.FC = () => { ...@@ -35,6 +36,7 @@ export const AnnounceModal: React.FC = () => {
const [searchWord, setSearchWord] = useState(""); const [searchWord, setSearchWord] = useState("");
const [cardList, setCardList] = useState<CardMeta[]>([]); const [cardList, setCardList] = useState<CardMeta[]>([]);
const [selected, setSelected] = useState<number | undefined>(undefined); const [selected, setSelected] = useState<number | undefined>(undefined);
const container = getUIContainer();
const handleSearch = () => { const handleSearch = () => {
const result = searchCards({ const result = searchCards({
...@@ -51,7 +53,7 @@ export const AnnounceModal: React.FC = () => { ...@@ -51,7 +53,7 @@ export const AnnounceModal: React.FC = () => {
}; };
const onSummit = () => { const onSummit = () => {
if (selected !== undefined) { if (selected !== undefined) {
sendSelectOptionResponse(selected); sendSelectOptionResponse(container.conn, selected);
rs(); rs();
setSearchWord(""); setSearchWord("");
setCardList([]); setCardList([]);
......
...@@ -5,6 +5,7 @@ import React, { useEffect, useState } from "react"; ...@@ -5,6 +5,7 @@ import React, { useEffect, useState } from "react";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { fetchStrings, Region, sendSelectCounterResponse } from "@/api"; import { fetchStrings, Region, sendSelectCounterResponse } from "@/api";
import { getUIContainer } from "@/container/compat";
import { YgoCard } from "@/ui/Shared"; import { YgoCard } from "@/ui/Shared";
import { NeosModal } from "../NeosModal"; import { NeosModal } from "../NeosModal";
...@@ -27,6 +28,7 @@ const defaultProps = { ...@@ -27,6 +28,7 @@ const defaultProps = {
const localStore = proxy<CheckCounterModalProps>(defaultProps); const localStore = proxy<CheckCounterModalProps>(defaultProps);
export const CheckCounterModal = () => { export const CheckCounterModal = () => {
const container = getUIContainer();
const snapCheckCounterModal = useSnapshot(localStore); const snapCheckCounterModal = useSnapshot(localStore);
const isOpen = snapCheckCounterModal.isOpen; const isOpen = snapCheckCounterModal.isOpen;
...@@ -46,7 +48,7 @@ export const CheckCounterModal = () => { ...@@ -46,7 +48,7 @@ export const CheckCounterModal = () => {
}, [options]); }, [options]);
const onFinish = () => { const onFinish = () => {
sendSelectCounterResponse(selected); sendSelectCounterResponse(container.conn, selected);
rs(); rs();
}; };
......
...@@ -18,6 +18,7 @@ export const HintNotification = () => { ...@@ -18,6 +18,7 @@ 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 error = snap.error;
const [msgApi, msgContextHolder] = message.useMessage({ const [msgApi, msgContextHolder] = message.useMessage({
maxCount: NeosConfig.ui.hint.maxCount, maxCount: NeosConfig.ui.hint.maxCount,
...@@ -61,6 +62,12 @@ export const HintNotification = () => { ...@@ -61,6 +62,12 @@ export const HintNotification = () => {
} }
}, [currentPhase]); }, [currentPhase]);
useEffect(() => {
if (error !== "") {
msgApi.error(error);
}
}, [error]);
return <>{msgContextHolder}</>; return <>{msgContextHolder}</>;
}; };
......
...@@ -35,7 +35,7 @@ export const NeosModal: React.FC<ModalProps> = (props) => { ...@@ -35,7 +35,7 @@ export const NeosModal: React.FC<ModalProps> = (props) => {
maskClosable={true} maskClosable={true}
onCancel={() => setMini(!mini)} onCancel={() => setMini(!mini)}
closeIcon={mini ? <UpOutlined /> : <MinusOutlined />} closeIcon={mini ? <UpOutlined /> : <MinusOutlined />}
bodyStyle={{ padding: "10px 0" }} style={{ padding: "10px 0" }}
mask={!mini} mask={!mini}
wrapClassName={classNames({ [styles.wrap]: mini })} wrapClassName={classNames({ [styles.wrap]: mini })}
closable={true} closable={true}
......
...@@ -12,6 +12,8 @@ import { ...@@ -12,6 +12,8 @@ import {
sendSelectIdleCmdResponse, sendSelectIdleCmdResponse,
sendSelectOptionResponse, sendSelectOptionResponse,
} from "@/api"; } from "@/api";
import { Container } from "@/container";
import { getUIContainer } from "@/container/compat";
import { NeosModal } from "../NeosModal"; import { NeosModal } from "../NeosModal";
...@@ -29,6 +31,7 @@ const store = proxy(defaultStore); ...@@ -29,6 +31,7 @@ const store = proxy(defaultStore);
const MAX_NUM_PER_PAGE = 4; const MAX_NUM_PER_PAGE = 4;
export const OptionModal = () => { export const OptionModal = () => {
const container = getUIContainer();
const snap = useSnapshot(store); const snap = useSnapshot(store);
const { title, isOpen, min, options } = snap; const { title, isOpen, min, options } = snap;
// options可能太多,因此分页展示 // options可能太多,因此分页展示
...@@ -41,7 +44,7 @@ export const OptionModal = () => { ...@@ -41,7 +44,7 @@ export const OptionModal = () => {
const responses = selecteds.flat(); const responses = selecteds.flat();
if (responses.length > 0) { if (responses.length > 0) {
const response = responses.reduce((res, current) => res | current, 0); // 多个选择求或 const response = responses.reduce((res, current) => res | current, 0); // 多个选择求或
sendSelectOptionResponse(response); sendSelectOptionResponse(container.conn, response);
rs(); rs();
} }
}; };
...@@ -132,6 +135,7 @@ export const displayOptionModal = async ( ...@@ -132,6 +135,7 @@ export const displayOptionModal = async (
}; };
export const handleEffectActivation = async ( export const handleEffectActivation = async (
container: Container,
meta: CardMeta, meta: CardMeta,
effectInteractivies: { effectInteractivies: {
desc: string; desc: string;
...@@ -144,7 +148,7 @@ export const handleEffectActivation = async ( ...@@ -144,7 +148,7 @@ export const handleEffectActivation = async (
} }
if (effectInteractivies.length === 1) { if (effectInteractivies.length === 1) {
// 如果只有一个效果,点击直接触发 // 如果只有一个效果,点击直接触发
sendSelectIdleCmdResponse(effectInteractivies[0].response); sendSelectIdleCmdResponse(container.conn, effectInteractivies[0].response);
} else { } else {
// optionsModal // optionsModal
const options = effectInteractivies.map((effect) => { const options = effectInteractivies.map((effect) => {
......
...@@ -5,6 +5,7 @@ import React, { useState } from "react"; ...@@ -5,6 +5,7 @@ import React, { useState } from "react";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { sendSelectPositionResponse, ygopro } from "@/api"; import { sendSelectPositionResponse, ygopro } from "@/api";
import { getUIContainer } from "@/container/compat";
import { NeosModal } from "../NeosModal"; import { NeosModal } from "../NeosModal";
...@@ -91,6 +92,7 @@ const translations: Translations = { ...@@ -91,6 +92,7 @@ const translations: Translations = {
}; };
export const PositionModal = () => { export const PositionModal = () => {
const container = getUIContainer();
const { isOpen, positions } = useSnapshot(localStore); const { isOpen, positions } = useSnapshot(localStore);
const [selected, setSelected] = useState<ygopro.CardPosition | undefined>( const [selected, setSelected] = useState<ygopro.CardPosition | undefined>(
undefined, undefined,
...@@ -105,7 +107,7 @@ export const PositionModal = () => { ...@@ -105,7 +107,7 @@ export const PositionModal = () => {
disabled={selected === undefined} disabled={selected === undefined}
onClick={() => { onClick={() => {
if (selected !== undefined) { if (selected !== undefined) {
sendSelectPositionResponse(selected); sendSelectPositionResponse(container.conn, selected);
rs(); rs();
} }
}} }}
......
import { INTERNAL_Snapshot as Snapshot, proxy, useSnapshot } from "valtio"; import { INTERNAL_Snapshot as Snapshot, proxy, useSnapshot } from "valtio";
import { sendSelectMultiResponse, sendSelectSingleResponse } from "@/api"; import { sendSelectMultiResponse, sendSelectSingleResponse } from "@/api";
import { getUIContainer } from "@/container/compat";
import { import {
type Option, type Option,
...@@ -32,25 +33,26 @@ const defaultProps: Omit< ...@@ -32,25 +33,26 @@ const defaultProps: Omit<
const localStore = proxy(defaultProps); const localStore = proxy(defaultProps);
export const SelectActionsModal: React.FC = () => { export const SelectActionsModal: React.FC = () => {
const container = getUIContainer();
const snap = useSnapshot(localStore); const snap = useSnapshot(localStore);
const onSubmit = (options: Snapshot<Option[]>) => { const onSubmit = (options: Snapshot<Option[]>) => {
const values = options.map((option) => option.response!); const values = options.map((option) => option.response!);
if (localStore.isChain) { if (localStore.isChain) {
sendSelectSingleResponse(values[0]); sendSelectSingleResponse(container.conn, values[0]);
} else { } else {
sendSelectMultiResponse(values); sendSelectMultiResponse(container.conn, values);
} }
rs(); rs();
}; };
const onFinish = () => { const onFinish = () => {
sendSelectSingleResponse(FINISH_RESPONSE); sendSelectSingleResponse(container.conn, FINISH_RESPONSE);
rs(); rs();
}; };
const onCancel = () => { const onCancel = () => {
sendSelectSingleResponse(CANCEL_RESPONSE); sendSelectSingleResponse(container.conn, CANCEL_RESPONSE);
rs(); rs();
}; };
......
...@@ -22,6 +22,7 @@ import { proxy, useSnapshot } from "valtio"; ...@@ -22,6 +22,7 @@ import { proxy, useSnapshot } from "valtio";
import { sendSortCardResponse } from "@/api"; import { sendSortCardResponse } from "@/api";
import { CardMeta, getCardImgUrl } from "@/api/cards"; import { CardMeta, getCardImgUrl } from "@/api/cards";
import { getUIContainer } from "@/container/compat";
import { NeosModal } from "../NeosModal"; import { NeosModal } from "../NeosModal";
...@@ -41,6 +42,7 @@ const defaultProps = { ...@@ -41,6 +42,7 @@ const defaultProps = {
const localStore = proxy<SortCardModalProps>(defaultProps); const localStore = proxy<SortCardModalProps>(defaultProps);
export const SortCardModal = () => { export const SortCardModal = () => {
const container = getUIContainer();
const { isOpen, options } = useSnapshot(localStore); const { isOpen, options } = useSnapshot(localStore);
const [items, setItems] = useState(options); const [items, setItems] = useState(options);
const sensors = useSensors( const sensors = useSensors(
...@@ -51,7 +53,10 @@ export const SortCardModal = () => { ...@@ -51,7 +53,10 @@ export const SortCardModal = () => {
); );
const onFinish = () => { const onFinish = () => {
sendSortCardResponse(items.map((item) => item.response)); sendSortCardResponse(
container.conn,
items.map((item) => item.response),
);
rs(); rs();
}; };
const onDragEnd = (event: DragEndEvent) => { const onDragEnd = (event: DragEndEvent) => {
......
...@@ -3,6 +3,7 @@ import React from "react"; ...@@ -3,6 +3,7 @@ import React from "react";
import { proxy, useSnapshot } from "valtio"; import { proxy, useSnapshot } from "valtio";
import { sendSelectEffectYnResponse } from "@/api"; import { sendSelectEffectYnResponse } from "@/api";
import { getUIContainer } from "@/container/compat";
import { matStore } from "@/stores"; import { matStore } from "@/stores";
import { NeosModal } from "../NeosModal"; import { NeosModal } from "../NeosModal";
...@@ -16,6 +17,7 @@ const defaultProps = { isOpen: false }; ...@@ -16,6 +17,7 @@ const defaultProps = { isOpen: false };
const localStore = proxy<YesNoModalProps>(defaultProps); const localStore = proxy<YesNoModalProps>(defaultProps);
export const YesNoModal: React.FC = () => { export const YesNoModal: React.FC = () => {
const container = getUIContainer();
const { isOpen, msg } = useSnapshot(localStore); const { isOpen, msg } = useSnapshot(localStore);
const hint = useSnapshot(matStore.hint); const hint = useSnapshot(matStore.hint);
...@@ -31,7 +33,7 @@ export const YesNoModal: React.FC = () => { ...@@ -31,7 +33,7 @@ export const YesNoModal: React.FC = () => {
<> <>
<Button <Button
onClick={() => { onClick={() => {
sendSelectEffectYnResponse(false); sendSelectEffectYnResponse(container.conn, false);
rs(); rs();
}} }}
> >
...@@ -40,7 +42,7 @@ export const YesNoModal: React.FC = () => { ...@@ -40,7 +42,7 @@ export const YesNoModal: React.FC = () => {
<Button <Button
type="primary" type="primary"
onClick={() => { onClick={() => {
sendSelectEffectYnResponse(true); sendSelectEffectYnResponse(container.conn, true);
rs(); rs();
}} }}
> >
......
...@@ -2,6 +2,8 @@ import classnames from "classnames"; ...@@ -2,6 +2,8 @@ import classnames from "classnames";
import { type INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio"; import { type INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
import { sendSelectPlaceResponse, ygopro } from "@/api"; import { sendSelectPlaceResponse, ygopro } from "@/api";
import { Container } from "@/container";
import { getUIContainer } from "@/container/compat";
import { import {
type BlockState, type BlockState,
cardStore, cardStore,
...@@ -46,6 +48,7 @@ const BgExtraRow: React.FC<{ ...@@ -46,6 +48,7 @@ const BgExtraRow: React.FC<{
meSnap: Snapshot<BlockState[]>; meSnap: Snapshot<BlockState[]>;
opSnap: Snapshot<BlockState[]>; opSnap: Snapshot<BlockState[]>;
}> = ({ meSnap, opSnap }) => { }> = ({ meSnap, opSnap }) => {
const container = getUIContainer();
return ( return (
<div className={classnames(styles.row)}> <div className={classnames(styles.row)}>
{Array.from({ length: 2 }).map((_, i) => ( {Array.from({ length: 2 }).map((_, i) => (
...@@ -53,8 +56,8 @@ const BgExtraRow: React.FC<{ ...@@ -53,8 +56,8 @@ const BgExtraRow: React.FC<{
key={i} key={i}
className={styles.extra} className={styles.extra}
onClick={() => { onClick={() => {
onBlockClick(meSnap[i].interactivity); onBlockClick(container, meSnap[i].interactivity);
onBlockClick(opSnap[1 - i].interactivity); onBlockClick(container, opSnap[1 - i].interactivity);
}} }}
disabled={meSnap[i].disabled || opSnap[1 - i].disabled} disabled={meSnap[i].disabled || opSnap[1 - i].disabled}
highlight={!!meSnap[i].interactivity || !!opSnap[1 - i].interactivity} highlight={!!meSnap[i].interactivity || !!opSnap[1 - i].interactivity}
...@@ -71,23 +74,27 @@ const BgRow: React.FC<{ ...@@ -71,23 +74,27 @@ const BgRow: React.FC<{
szone?: boolean; szone?: boolean;
opponent?: boolean; opponent?: boolean;
snap: Snapshot<BlockState[]>; snap: Snapshot<BlockState[]>;
}> = ({ szone = false, opponent = false, snap }) => ( }> = ({ szone = false, opponent = false, snap }) => {
<div className={classnames(styles.row, { [styles.opponent]: opponent })}> const container = getUIContainer();
{Array.from({ length: 5 }).map((_, i) => ( return (
<BgBlock <div className={classnames(styles.row, { [styles.opponent]: opponent })}>
key={i} {Array.from({ length: 5 }).map((_, i) => (
className={classnames({ [styles.szone]: szone })} <BgBlock
onClick={() => onBlockClick(snap[i].interactivity)} key={i}
disabled={snap[i].disabled} className={classnames({ [styles.szone]: szone })}
highlight={!!snap[i].interactivity} onClick={() => onBlockClick(container, snap[i].interactivity)}
chains={{ chains: snap[i].chainIndex }} disabled={snap[i].disabled}
/> highlight={!!snap[i].interactivity}
))} chains={{ chains: snap[i].chainIndex }}
</div> />
); ))}
</div>
);
};
const BgOtherBlocks: React.FC<{ op?: boolean }> = ({ op }) => { const BgOtherBlocks: React.FC<{ op?: boolean }> = ({ op }) => {
useSnapshot(cardStore); useSnapshot(cardStore);
const container = getUIContainer();
const meController = isMe(0) ? 0 : 1; const meController = isMe(0) ? 0 : 1;
const judgeGlowing = (zone: ygopro.CardZone) => const judgeGlowing = (zone: ygopro.CardZone) =>
!!cardStore !!cardStore
...@@ -134,7 +141,7 @@ const BgOtherBlocks: React.FC<{ op?: boolean }> = ({ op }) => { ...@@ -134,7 +141,7 @@ const BgOtherBlocks: React.FC<{ op?: boolean }> = ({ op }) => {
/> />
<BgBlock <BgBlock
className={styles.field} className={styles.field}
onClick={() => onBlockClick(field.interactivity)} onClick={() => onBlockClick(container, field.interactivity)}
disabled={field.disabled} disabled={field.disabled}
highlight={!!field.interactivity} highlight={!!field.interactivity}
chains={{ chains: field.chainIndex, op }} chains={{ chains: field.chainIndex, op }}
...@@ -171,9 +178,12 @@ export const Bg: React.FC = () => { ...@@ -171,9 +178,12 @@ export const Bg: React.FC = () => {
); );
}; };
const onBlockClick = (placeInteractivity: PlaceInteractivity) => { const onBlockClick = (
container: Container,
placeInteractivity: PlaceInteractivity,
) => {
if (placeInteractivity) { if (placeInteractivity) {
sendSelectPlaceResponse(placeInteractivity.response); sendSelectPlaceResponse(container.conn, placeInteractivity.response);
cardStore.inner.forEach((card) => (card.idleInteractivities = [])); cardStore.inner.forEach((card) => (card.idleInteractivities = []));
placeStore.clearAllInteractivity(); placeStore.clearAllInteractivity();
} }
......
...@@ -11,6 +11,8 @@ import { ...@@ -11,6 +11,8 @@ import {
sendSelectIdleCmdResponse, sendSelectIdleCmdResponse,
ygopro, ygopro,
} from "@/api"; } from "@/api";
import { Container } from "@/container";
import { getUIContainer } from "@/container/compat";
import { eventbus, Task } from "@/infra"; import { eventbus, Task } from "@/infra";
import { cardStore, CardType, Interactivity, InteractType } from "@/stores"; import { cardStore, CardType, Interactivity, InteractType } from "@/stores";
import { showCardModal as displayCardModal } from "@/ui/Duel/Message/CardModal"; import { showCardModal as displayCardModal } from "@/ui/Duel/Message/CardModal";
...@@ -40,6 +42,7 @@ import type { SpringApiProps } from "./springs/types"; ...@@ -40,6 +42,7 @@ import type { SpringApiProps } from "./springs/types";
const { HAND, GRAVE, REMOVED, EXTRA, MZONE, SZONE, TZONE } = ygopro.CardZone; const { HAND, GRAVE, REMOVED, EXTRA, MZONE, SZONE, TZONE } = ygopro.CardZone;
export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => { export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
const container = getUIContainer();
const card = cardStore.inner[idx]; const card = cardStore.inner[idx];
const snap = useSnapshot(card); const snap = useSnapshot(card);
...@@ -161,7 +164,10 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => { ...@@ -161,7 +164,10 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
if (!isField) { if (!isField) {
// 单卡: 直接召唤/特殊召唤/... // 单卡: 直接召唤/特殊召唤/...
const card = cards[0]; const card = cards[0];
sendSelectIdleCmdResponse(getNonEffectResponse(action, card)); sendSelectIdleCmdResponse(
container.conn,
getNonEffectResponse(action, card),
);
clearAllIdleInteractivities(); clearAllIdleInteractivities();
} else { } else {
// 场地: 选择卡片 // 场地: 选择卡片
...@@ -174,7 +180,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => { ...@@ -174,7 +180,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
})), })),
}); });
if (option.length > 0) { if (option.length > 0) {
sendSelectIdleCmdResponse(option[0].response!); sendSelectIdleCmdResponse(container.conn, option[0].response!);
clearAllIdleInteractivities(); clearAllIdleInteractivities();
} }
} }
...@@ -223,6 +229,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => { ...@@ -223,6 +229,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
} }
// 选择发动哪个效果 // 选择发动哪个效果
handleEffectActivation( handleEffectActivation(
container,
tmpCard.idleInteractivities tmpCard.idleInteractivities
.filter( .filter(
({ interactType }) => interactType === InteractType.ACTIVATE, ({ interactType }) => interactType === InteractType.ACTIVATE,
...@@ -246,7 +253,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => { ...@@ -246,7 +253,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
const selectInfo = card.selectInfo; const selectInfo = card.selectInfo;
if (selectInfo.selectable || selectInfo.selected) { if (selectInfo.selectable || selectInfo.selected) {
if (selectInfo.response !== undefined) { if (selectInfo.response !== undefined) {
sendSelectMultiResponse([selectInfo.response]); sendSelectMultiResponse(container.conn, [selectInfo.response]);
clearSelectInfo(); clearSelectInfo();
} else { } else {
console.error("card is selectable but the response is undefined!"); console.error("card is selectable but the response is undefined!");
...@@ -358,13 +365,14 @@ type DropdownItem = NonNullable<MenuProps["items"]>[number] & { ...@@ -358,13 +365,14 @@ type DropdownItem = NonNullable<MenuProps["items"]>[number] & {
}; };
const handleEffectActivation = ( const handleEffectActivation = (
container: Container,
effectInteractivies: Interactivy[], effectInteractivies: Interactivy[],
meta?: CardMeta, meta?: CardMeta,
) => { ) => {
if (!effectInteractivies.length) return; if (!effectInteractivies.length) return;
else if (effectInteractivies.length === 1) { else if (effectInteractivies.length === 1) {
// 如果只有一个效果,点击直接触发 // 如果只有一个效果,点击直接触发
sendSelectIdleCmdResponse(effectInteractivies[0].response); sendSelectIdleCmdResponse(container.conn, effectInteractivies[0].response);
} else { } else {
// optionsModal // optionsModal
const options = effectInteractivies.map((effect) => { const options = effectInteractivies.map((effect) => {
......
...@@ -3,6 +3,8 @@ import { ...@@ -3,6 +3,8 @@ import {
CheckOutlined, CheckOutlined,
CloseCircleFilled, CloseCircleFilled,
MessageFilled, MessageFilled,
RobotFilled,
RobotOutlined,
StepForwardFilled, StepForwardFilled,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { import {
...@@ -33,6 +35,8 @@ import styles from "./index.module.scss"; ...@@ -33,6 +35,8 @@ import styles from "./index.module.scss";
import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType; import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType;
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getUIContainer } from "@/container/compat";
import { clearAllIdleInteractivities, clearSelectInfo } from "../../utils"; import { clearAllIdleInteractivities, clearSelectInfo } from "../../utils";
import { openChatBox } from "../ChatBox"; import { openChatBox } from "../ChatBox";
...@@ -215,6 +219,7 @@ const initialPhaseBind: [ ...@@ -215,6 +219,7 @@ const initialPhaseBind: [
]; ];
export const Menu = () => { export const Menu = () => {
const container = getUIContainer();
const { t: i18n } = useTranslation("Menu"); const { t: i18n } = useTranslation("Menu");
const { const {
currentPlayer, currentPlayer,
...@@ -226,6 +231,10 @@ export const Menu = () => { ...@@ -226,6 +231,10 @@ export const Menu = () => {
[], [],
); );
const [enableKuriboh, setEnableKuriboh] = useState(
container.getEnableKuriboh(),
);
useEffect(() => { useEffect(() => {
const endResponse = [ const endResponse = [
PhaseType.BATTLE_START, PhaseType.BATTLE_START,
...@@ -266,8 +275,9 @@ export const Menu = () => { ...@@ -266,8 +275,9 @@ export const Menu = () => {
label, label,
disabled: disabled, disabled: disabled,
onClick: () => { onClick: () => {
if (response === 2) sendSelectIdleCmdResponse(response); if (response === 2)
else sendSelectBattleCmdResponse(response); sendSelectIdleCmdResponse(container.conn, response);
else sendSelectBattleCmdResponse(container.conn, response);
clearAllIdleInteractivities(); clearAllIdleInteractivities();
}, },
icon: disabled ? <CheckOutlined /> : <ArrowRightOutlined />, icon: disabled ? <CheckOutlined /> : <ArrowRightOutlined />,
...@@ -299,12 +309,20 @@ export const Menu = () => { ...@@ -299,12 +309,20 @@ export const Menu = () => {
{ {
label: i18n("Confirm"), label: i18n("Confirm"),
danger: true, danger: true,
onClick: sendSurrender, onClick: () => {
sendSurrender(container.conn);
},
}, },
].map((item, i) => ({ key: i, ...item })); ].map((item, i) => ({ key: i, ...item }));
const globalDisable = !matStore.isMe(currentPlayer); const globalDisable = !matStore.isMe(currentPlayer);
const switchAutoSelect = () => {
const newValue = !enableKuriboh;
setEnableKuriboh(newValue);
container.setEnableKuriboh(newValue);
};
return ( return (
<div className={styles["menu-container"]}> <div className={styles["menu-container"]}>
<SelectManager /> <SelectManager />
...@@ -331,6 +349,13 @@ export const Menu = () => { ...@@ -331,6 +349,13 @@ export const Menu = () => {
type="text" type="text"
></Button> ></Button>
</DropdownWithTitle> </DropdownWithTitle>
<Tooltip title="AI">
<Button
icon={enableKuriboh ? <RobotFilled /> : <RobotOutlined />}
onClick={switchAutoSelect}
type="text"
></Button>
</Tooltip>
<Tooltip title={i18n("ChatRoom")}> <Tooltip title={i18n("ChatRoom")}>
<Button <Button
icon={<MessageFilled />} icon={<MessageFilled />}
...@@ -401,10 +426,11 @@ const ChainIcon: React.FC<{ chainSetting: ChainSetting }> = ({ ...@@ -401,10 +426,11 @@ const ChainIcon: React.FC<{ chainSetting: ChainSetting }> = ({
}; };
const SelectManager: React.FC = () => { const SelectManager: React.FC = () => {
const container = getUIContainer();
const { t: i18n } = useTranslation("Menu"); const { t: i18n } = useTranslation("Menu");
const { finishable, cancelable } = useSnapshot(matStore.selectUnselectInfo); const { finishable, cancelable } = useSnapshot(matStore.selectUnselectInfo);
const onFinishOrCancel = () => { const onFinishOrCancel = () => {
sendSelectSingleResponse(FINISH_CANCEL_RESPONSE); sendSelectSingleResponse(container.conn, FINISH_CANCEL_RESPONSE);
clearSelectInfo(); clearSelectInfo();
}; };
return ( return (
......
...@@ -81,8 +81,8 @@ ...@@ -81,8 +81,8 @@
"Select": "请选择", "Select": "请选择",
"Minimum": "最小值", "Minimum": "最小值",
"Maximum": "最大值", "Maximum": "最大值",
"Confirm": "确 定", "Confirm": "确定",
"Cancel": "取 消" "Cancel": "取消"
}, },
"CardDetails": { "CardDetails": {
"Level": "等级", "Level": "等级",
...@@ -167,7 +167,9 @@ ...@@ -167,7 +167,9 @@
"UltraPreemptiveServer": "超先行服", "UltraPreemptiveServer": "超先行服",
"PlayerNickname": "玩家昵称", "PlayerNickname": "玩家昵称",
"RoomPasswordOptional": "房间密码(可选)", "RoomPasswordOptional": "房间密码(可选)",
"JoinRoom": "加入房间" "JoinRoom": "加入房间",
"EnableAIAssist": "启用AI辅助功能",
"BetaTest": "Beta测试"
}, },
"ReplayModal": { "ReplayModal": {
"SelectReplay": "选择回放", "SelectReplay": "选择回放",
......
...@@ -15,4 +15,16 @@ ...@@ -15,4 +15,16 @@
.select { .select {
margin: 0.25rem 0; margin: 0.25rem 0;
} }
.ai-assist-container {
display: flex;
align-items: center;
gap: 1rem;
margin: 0.25rem 0;
span {
font-size: 1rem;
color: #d7e70b;
}
}
} }
import { App, Button, Input, Modal } from "antd"; import { App, Button, Input, Modal, Switch } from "antd";
import React, { ChangeEvent, useEffect, useState } from "react"; import React, { ChangeEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
...@@ -43,6 +43,7 @@ export const MatchModal: React.FC = ({}) => { ...@@ -43,6 +43,7 @@ export const MatchModal: React.FC = ({}) => {
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const { t: i18n } = useTranslation("MatchModal"); const { t: i18n } = useTranslation("MatchModal");
const [enableKuriboh, setEnableKuriBoh] = useState(false);
const handlePlayerChange = (event: ChangeEvent<HTMLInputElement>) => { const handlePlayerChange = (event: ChangeEvent<HTMLInputElement>) => {
setPlayer(event.target.value); setPlayer(event.target.value);
...@@ -60,6 +61,7 @@ export const MatchModal: React.FC = ({}) => { ...@@ -60,6 +61,7 @@ export const MatchModal: React.FC = ({}) => {
player, player,
ip: genServerAddress(serverId), ip: genServerAddress(serverId),
passWd: passwd, passWd: passwd,
enableKuriboh,
}); });
}; };
...@@ -116,6 +118,14 @@ export const MatchModal: React.FC = ({}) => { ...@@ -116,6 +118,14 @@ export const MatchModal: React.FC = ({}) => {
]} ]}
onChange={handleServerChange} onChange={handleServerChange}
/> />
<div className={styles["ai-assist-container"]}>
<span>{i18n("EnableAIAssist")}</span>
<span style={{ color: "red" }}>{`(${i18n("BetaTest")}!!)`}</span>
<Switch
value={enableKuriboh}
onChange={(value) => setEnableKuriBoh(value)}
/>
</div>
<Input <Input
className={styles.input} className={styles.input}
type="text" type="text"
......
...@@ -101,7 +101,7 @@ export const WatchContent: React.FC = () => { ...@@ -101,7 +101,7 @@ export const WatchContent: React.FC = () => {
<Input <Input
className={styles.input} className={styles.input}
placeholder={i18n("SearchRoomByPlayerUsername")} placeholder={i18n("SearchRoomByPlayerUsername")}
bordered={false} variant="borderless"
suffix={<Button type="text" icon={<SearchOutlined />} />} suffix={<Button type="text" icon={<SearchOutlined />} />}
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
......
...@@ -2,8 +2,13 @@ import rustInit from "rust-src"; ...@@ -2,8 +2,13 @@ import rustInit from "rust-src";
import { initStrings, initSuperPrerelease } from "@/api"; import { initStrings, initSuperPrerelease } from "@/api";
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import socketMiddleWare, { socketCmd } from "@/middleware/socket"; import { getUIContainer, initUIContainer } from "@/container/compat";
import { initReplaySocket, initSocket } from "@/middleware/socket";
import sqliteMiddleWare, { sqliteCmd } from "@/middleware/sqlite"; import sqliteMiddleWare, { sqliteCmd } from "@/middleware/sqlite";
import {
pollSocketLooper,
pollSocketLooperWithAgent,
} from "@/service/executor";
const NeosConfig = useConfig(); const NeosConfig = useConfig();
...@@ -12,6 +17,7 @@ export const connectSrvpro = async (params: { ...@@ -12,6 +17,7 @@ export const connectSrvpro = async (params: {
ip: string; ip: string;
player: string; player: string;
passWd: string; passWd: string;
enableKuriboh?: boolean;
replay?: boolean; replay?: boolean;
replayData?: ArrayBuffer; replayData?: ArrayBuffer;
}) => { }) => {
...@@ -38,20 +44,32 @@ export const connectSrvpro = async (params: { ...@@ -38,20 +44,32 @@ export const connectSrvpro = async (params: {
await initSuperPrerelease(); await initSuperPrerelease();
if (params.replay && params.replayData) { if (params.replay && params.replayData) {
// 连接回放websocket服务 // connect to replay Server
socketMiddleWare({ const conn = initReplaySocket({
cmd: socketCmd.CONNECT, url: NeosConfig.replayUrl,
isReplay: true, data: params.replayData,
replayInfo: {
Url: NeosConfig.replayUrl,
data: params.replayData,
},
}); });
// initialize the UI Container
initUIContainer(conn);
// execute the event looper
pollSocketLooper(getUIContainer());
} else { } else {
// 通过socket中间件向ygopro服务端请求建立长连接 // connect to the ygopro Server
socketMiddleWare({ const conn = initSocket(params);
cmd: socketCmd.CONNECT,
initInfo: params, // initialize the UI Contaner
}); initUIContainer(conn);
// execute the event looper
if (params.enableKuriboh) {
const container = getUIContainer();
container.setEnableKuriboh(true);
pollSocketLooperWithAgent(container);
} else {
pollSocketLooper(getUIContainer());
}
} }
}; };
...@@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "react"; ...@@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "react";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { sendChat } from "@/api"; import { sendChat } from "@/api";
import { getUIContainer } from "@/container/compat";
import { chatStore, isMe, roomStore } from "@/stores"; import { chatStore, isMe, roomStore } from "@/stores";
interface ChatItem { interface ChatItem {
...@@ -14,6 +15,7 @@ interface ChatItem { ...@@ -14,6 +15,7 @@ interface ChatItem {
} }
export const useChat = (isDuel: boolean = false) => { export const useChat = (isDuel: boolean = false) => {
const container = getUIContainer();
const [chatList, setChatList] = useState<ChatItem[]>([]); const [chatList, setChatList] = useState<ChatItem[]>([]);
const [input, setInput] = useState<string | undefined>(undefined); const [input, setInput] = useState<string | undefined>(undefined);
const chat = useSnapshot(chatStore); const chat = useSnapshot(chatStore);
...@@ -31,7 +33,7 @@ export const useChat = (isDuel: boolean = false) => { ...@@ -31,7 +33,7 @@ export const useChat = (isDuel: boolean = false) => {
/** 发信息 */ /** 发信息 */
const onSend = () => { const onSend = () => {
if (input !== undefined) { if (input !== undefined) {
sendChat(input); sendChat(container.conn, input);
setInput(""); setInput("");
} }
}; };
......
...@@ -3,11 +3,13 @@ import React from "react"; ...@@ -3,11 +3,13 @@ import React from "react";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { sendTpResult } from "@/api"; import { sendTpResult } from "@/api";
import { getUIContainer } from "@/container/compat";
import { SideStage, sideStore } from "@/stores"; import { SideStage, sideStore } from "@/stores";
import styles from "./TpModal.module.scss"; import styles from "./TpModal.module.scss";
export const TpModal: React.FC = () => { export const TpModal: React.FC = () => {
const container = getUIContainer();
const { stage } = useSnapshot(sideStore); const { stage } = useSnapshot(sideStore);
return ( return (
...@@ -20,7 +22,7 @@ export const TpModal: React.FC = () => { ...@@ -20,7 +22,7 @@ export const TpModal: React.FC = () => {
<div className={styles.container}> <div className={styles.container}>
<Button <Button
onClick={() => { onClick={() => {
sendTpResult(true); sendTpResult(container.conn, true);
sideStore.stage = SideStage.TP_SELECTED; sideStore.stage = SideStage.TP_SELECTED;
}} }}
> >
...@@ -28,7 +30,7 @@ export const TpModal: React.FC = () => { ...@@ -28,7 +30,7 @@ export const TpModal: React.FC = () => {
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
sendTpResult(false); sendTpResult(container.conn, false);
sideStore.stage = SideStage.TP_SELECTED; sideStore.stage = SideStage.TP_SELECTED;
}} }}
> >
......
...@@ -8,6 +8,7 @@ import { useSnapshot } from "valtio"; ...@@ -8,6 +8,7 @@ import { useSnapshot } from "valtio";
import { CardMeta, fetchCard, sendUpdateDeck } from "@/api"; import { CardMeta, fetchCard, sendUpdateDeck } from "@/api";
import { isExtraDeckCard } from "@/common"; import { isExtraDeckCard } from "@/common";
import { getUIContainer } from "@/container/compat";
import { AudioActionType, changeScene } from "@/infra/audio"; import { AudioActionType, changeScene } from "@/infra/audio";
import { IDeck, roomStore, SideStage, sideStore } from "@/stores"; import { IDeck, roomStore, SideStage, sideStore } from "@/stores";
...@@ -24,6 +25,7 @@ export const loader: LoaderFunction = async () => { ...@@ -24,6 +25,7 @@ export const loader: LoaderFunction = async () => {
}; };
export const Component: React.FC = () => { export const Component: React.FC = () => {
const container = getUIContainer();
const { message } = App.useApp(); const { message } = App.useApp();
const initialDeck = sideStore.getSideDeck(); const initialDeck = sideStore.getSideDeck();
const { stage } = useSnapshot(sideStore); const { stage } = useSnapshot(sideStore);
...@@ -65,7 +67,7 @@ export const Component: React.FC = () => { ...@@ -65,7 +67,7 @@ export const Component: React.FC = () => {
message.info("重置成功"); message.info("重置成功");
}; };
const onSummit = () => { const onSummit = () => {
sendUpdateDeck(deck); sendUpdateDeck(container.conn, deck);
sideStore.setSideDeck(deck); sideStore.setSideDeck(deck);
}; };
......
...@@ -11,7 +11,6 @@ import { ...@@ -11,7 +11,6 @@ import {
sendUpdateDeck, sendUpdateDeck,
ygopro, ygopro,
} from "@/api"; } from "@/api";
import socketMiddleWare, { socketCmd } from "@/middleware/socket";
import PlayerState = ygopro.StocHsPlayerChange.State; import PlayerState = ygopro.StocHsPlayerChange.State;
import SelfType = ygopro.StocTypeChange.SelfType; import SelfType = ygopro.StocTypeChange.SelfType;
import { App, Avatar, Button, Skeleton, Space } from "antd"; import { App, Avatar, Button, Skeleton, Space } from "antd";
...@@ -22,7 +21,9 @@ import { LoaderFunction, useNavigate } from "react-router-dom"; ...@@ -22,7 +21,9 @@ import { LoaderFunction, useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { getUIContainer } from "@/container/compat";
import { AudioActionType, changeScene } from "@/infra/audio"; import { AudioActionType, changeScene } from "@/infra/audio";
import { closeSocket } from "@/middleware/socket";
import { import {
accountStore, accountStore,
deckStore, deckStore,
...@@ -48,6 +49,7 @@ export const loader: LoaderFunction = async () => { ...@@ -48,6 +49,7 @@ export const loader: LoaderFunction = async () => {
}; };
export const Component: React.FC = () => { export const Component: React.FC = () => {
const container = getUIContainer();
const { t: i18n } = useTranslation("WaitRoom"); const { t: i18n } = useTranslation("WaitRoom");
const { message } = App.useApp(); const { message } = App.useApp();
const { user } = useSnapshot(accountStore); const { user } = useSnapshot(accountStore);
...@@ -63,7 +65,7 @@ export const Component: React.FC = () => { ...@@ -63,7 +65,7 @@ export const Component: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const updateDeck = (deck: IDeck) => { const updateDeck = (deck: IDeck) => {
sendUpdateDeck(deck); sendUpdateDeck(container.conn, deck);
// 设置side里面的卡组 // 设置side里面的卡组
sideStore.setSideDeck(deck); sideStore.setSideDeck(deck);
}; };
...@@ -71,7 +73,7 @@ export const Component: React.FC = () => { ...@@ -71,7 +73,7 @@ export const Component: React.FC = () => {
const onDeckSelected = (deckName: string) => { const onDeckSelected = (deckName: string) => {
const newDeck = deckStore.get(deckName); const newDeck = deckStore.get(deckName);
if (newDeck) { if (newDeck) {
sendHsNotReady(); sendHsNotReady(container.conn);
updateDeck(newDeck); updateDeck(newDeck);
setDeck(newDeck); setDeck(newDeck);
} else { } else {
...@@ -83,12 +85,12 @@ export const Component: React.FC = () => { ...@@ -83,12 +85,12 @@ export const Component: React.FC = () => {
if (me?.state === PlayerState.NO_READY) { if (me?.state === PlayerState.NO_READY) {
if (deck) { if (deck) {
updateDeck(deck); updateDeck(deck);
sendHsReady(); sendHsReady(container.conn);
} else { } else {
message.error("请先选择卡组"); message.error("请先选择卡组");
} }
} else { } else {
sendHsNotReady(); sendHsNotReady(container.conn);
} }
}; };
...@@ -96,7 +98,7 @@ export const Component: React.FC = () => { ...@@ -96,7 +98,7 @@ export const Component: React.FC = () => {
// 组件初始化时发一次更新卡组的包 // 组件初始化时发一次更新卡组的包
// //
// 否则娱乐匹配准备会有问题(原因不明) // 否则娱乐匹配准备会有问题(原因不明)
if (deck) sendUpdateDeck(deck); if (deck) sendUpdateDeck(container.conn, deck);
}, []); }, []);
useEffect(() => { useEffect(() => {
if (room.stage === RoomStage.DUEL_START) { if (room.stage === RoomStage.DUEL_START) {
...@@ -184,11 +186,11 @@ export const Component: React.FC = () => { ...@@ -184,11 +186,11 @@ export const Component: React.FC = () => {
</div> </div>
<ActionButton <ActionButton
onMoraSelect={(mora) => { onMoraSelect={(mora) => {
sendHandResult(mora); sendHandResult(container.conn, mora);
roomStore.stage = RoomStage.HAND_SELECTED; roomStore.stage = RoomStage.HAND_SELECTED;
}} }}
onTpSelect={(tp) => { onTpSelect={(tp) => {
sendTpResult(tp === Tp.First); sendTpResult(container.conn, tp === Tp.First);
roomStore.stage = RoomStage.TP_SELECTED; roomStore.stage = RoomStage.TP_SELECTED;
}} }}
/> />
...@@ -263,6 +265,7 @@ const MoraAvatar: React.FC<{ mora?: Mora }> = ({ mora }) => ( ...@@ -263,6 +265,7 @@ const MoraAvatar: React.FC<{ mora?: Mora }> = ({ mora }) => (
const Controller: React.FC<{ onDeckChange: (deckName: string) => void }> = ({ const Controller: React.FC<{ onDeckChange: (deckName: string) => void }> = ({
onDeckChange, onDeckChange,
}) => { }) => {
const container = getUIContainer();
const { t: i18n } = useTranslation("WaitRoom"); const { t: i18n } = useTranslation("WaitRoom");
const snapDeck = useSnapshot(deckStore); const snapDeck = useSnapshot(deckStore);
const snapRoom = useSnapshot(roomStore); const snapRoom = useSnapshot(roomStore);
...@@ -287,9 +290,9 @@ const Controller: React.FC<{ onDeckChange: (deckName: string) => void }> = ({ ...@@ -287,9 +290,9 @@ const Controller: React.FC<{ onDeckChange: (deckName: string) => void }> = ({
icon={<IconFont type="icon-record" size={18} />} icon={<IconFont type="icon-record" size={18} />}
onClick={() => { onClick={() => {
if (snapRoom.selfType !== SelfType.OBSERVER) { if (snapRoom.selfType !== SelfType.OBSERVER) {
sendHsToObserver(); sendHsToObserver(container.conn);
} else { } else {
sendHsToDuelList(); sendHsToDuelList(container.conn);
} }
}} }}
> >
...@@ -326,8 +329,8 @@ const SideButtons: React.FC<{ ...@@ -326,8 +329,8 @@ const SideButtons: React.FC<{
</span> </span>
} }
onClick={() => { onClick={() => {
// 断开websocket🔗 // 断开websocket🔗
socketMiddleWare({ cmd: socketCmd.DISCONNECT }); closeSocket(getUIContainer().conn);
// 重置stores // 重置stores
resetUniverse(); resetUniverse();
// 返回上一个路由 // 返回上一个路由
...@@ -355,6 +358,7 @@ const ActionButton: React.FC<{ ...@@ -355,6 +358,7 @@ const ActionButton: React.FC<{
onMoraSelect: (mora: Mora) => void; onMoraSelect: (mora: Mora) => void;
onTpSelect: (tp: Tp) => void; onTpSelect: (tp: Tp) => void;
}> = ({ onMoraSelect, onTpSelect }) => { }> = ({ onMoraSelect, onTpSelect }) => {
const container = getUIContainer();
const room = useSnapshot(roomStore); const room = useSnapshot(roomStore);
const { stage, isHost } = room; const { stage, isHost } = room;
const { t: i18n } = useTranslation("WaitRoom"); const { t: i18n } = useTranslation("WaitRoom");
...@@ -371,7 +375,7 @@ const ActionButton: React.FC<{ ...@@ -371,7 +375,7 @@ const ActionButton: React.FC<{
room.getOpPlayer()?.state !== PlayerState.READY)) room.getOpPlayer()?.state !== PlayerState.READY))
} }
onClick={() => { onClick={() => {
sendHsStart(); sendHsStart(container.conn);
}} }}
> >
{stage === RoomStage.WAITING ? ( {stage === RoomStage.WAITING ? (
......
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