Commit 7b5e4828 authored by Chunchi Che's avatar Chunchi Che

人机模式增加AI预测功能

parent 0541025d
......@@ -37,6 +37,7 @@
"entertainWatchUrl": "wss://tiramisu.moecube.com:7923",
"userApi": "https://sapi.moecube.com:444/accounts/users/{username}.json",
"mdproServer": "https://rarnu.xyz:38443",
"agentServer": "https://t1v-n-e71fca19-w-0.tail0aad8.ts.net",
"streamInterval": 20,
"startDelay": 1000,
"ui": {
......
......@@ -37,6 +37,7 @@
"entertainWatchUrl": "wss://tiramisu.moecube.com:7923",
"userApi": "https://sapi.moecube.com:444/accounts/users/{username}.json",
"mdproServer": "https://rarnu.xyz:38443",
"agentServer": "https://t1v-n-e71fca19-w-0.tail0aad8.ts.net",
"streamInterval": 20,
"startDelay": 1000,
"ui": {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -7,3 +7,16 @@ export * from "./ocgcore/idl/ocgcore";
export * from "./ocgcore/ocgHelper";
export * from "./strings";
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 { handleHttps } from "..";
import { MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
import { mdproHeaders } from "./util";
const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/sync/single";
......
import { useConfig } from "@/config";
import { handleHttps } from "..";
import { MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
import { mdproHeaders } from "./util";
const { mdproServer } = useConfig();
......
import { useConfig } from "@/config";
import { handleHttps } from "..";
import { MdproDeck, MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
import { mdproHeaders } from "./util";
const { mdproServer } = useConfig();
......
import { useConfig } from "@/config";
import { pfetch } from "@/infra";
import { handleHttps } from "..";
import { MdproDeck, MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
import { mdproHeaders } from "./util";
const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/sync/";
......
import { useConfig } from "@/config";
import { pfetch } from "@/infra";
import { handleHttps } from "..";
import { MdproDeckLike, MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
import { mdproHeaders } from "./util";
const { mdproServer } = useConfig();
const API_PATH = "api/mdpro3/deck/list";
......
import { useConfig } from "@/config";
import { handleHttps } from "..";
import { MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
import { mdproHeaders } from "./util";
const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/sync/single";
......
import { useConfig } from "@/config";
import { handleHttps } from "..";
import { MdproResp } from "./schema";
import { handleHttps, mdproHeaders } from "./util";
import { mdproHeaders } from "./util";
const { mdproServer } = useConfig();
const API_PATH = "/api/mdpro3/deck/public";
......
......@@ -5,17 +5,3 @@ export function mdproHeaders(): Headers {
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;
import { CardMeta } from "@/api";
//! 一些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; //
const TYPE_SPELL = 0x2; //
const TYPE_TRAP = 0x4; //
const TYPE_NORMAL = 0x10; //
const TYPE_EFFECT = 0x20; //
const TYPE_FUSION = 0x40; //
const TYPE_RITUAL = 0x80; //
const TYPE_TRAPMONSTER = 0x100; //
const TYPE_SPIRIT = 0x200; //
const TYPE_UNION = 0x400; //
const TYPE_DUAL = 0x800; //
const TYPE_TUNER = 0x1000; //
const TYPE_SYNCHRO = 0x2000; //
export const TYPE_MONSTER = 0x1; //
export const TYPE_SPELL = 0x2; //
export const TYPE_TRAP = 0x4; //
export const TYPE_NORMAL = 0x10; //
export const TYPE_EFFECT = 0x20; //
export const TYPE_FUSION = 0x40; //
export const TYPE_RITUAL = 0x80; //
export const TYPE_TRAPMONSTER = 0x100; //
export const TYPE_SPIRIT = 0x200; //
export const TYPE_UNION = 0x400; //
export const TYPE_DUAL = 0x800; //
export const TYPE_TUNER = 0x1000; //
export const TYPE_SYNCHRO = 0x2000; //
export const TYPE_TOKEN = 0x4000; //
const TYPE_QUICKPLAY = 0x10000; //
const TYPE_CONTINUOUS = 0x20000; //
const TYPE_EQUIP = 0x40000; //
const TYPE_FIELD = 0x80000; //
const TYPE_COUNTER = 0x100000; //
const TYPE_FLIP = 0x200000; //
const TYPE_TOON = 0x400000; //
const TYPE_XYZ = 0x800000; //
const TYPE_PENDULUM = 0x1000000; //
const TYPE_SPSUMMON = 0x2000000; //
export const TYPE_QUICKPLAY = 0x10000; //
export const TYPE_CONTINUOUS = 0x20000; //
export const TYPE_EQUIP = 0x40000; //
export const TYPE_FIELD = 0x80000; //
export const TYPE_COUNTER = 0x100000; //
export const TYPE_FLIP = 0x200000; //
export const TYPE_TOON = 0x400000; //
export const TYPE_XYZ = 0x800000; //
export const TYPE_PENDULUM = 0x1000000; //
export const TYPE_SPSUMMON = 0x2000000; //
export const TYPE_LINK = 0x4000000; //
/*
......@@ -147,13 +154,13 @@ export function isPendulumMonster(typeCode: number): boolean {
// 属性
// const ATTRIBUTE_ALL = 0x7f; //
const ATTRIBUTE_EARTH = 0x01; //
const ATTRIBUTE_WATER = 0x02; //
const ATTRIBUTE_FIRE = 0x04; //
const ATTRIBUTE_WIND = 0x08; //
const ATTRIBUTE_LIGHT = 0x10; //
const ATTRIBUTE_DARK = 0x20; //
const ATTRIBUTE_DEVINE = 0x40; //
export const ATTRIBUTE_EARTH = 0x01; //
export const ATTRIBUTE_WATER = 0x02; //
export const ATTRIBUTE_FIRE = 0x04; //
export const ATTRIBUTE_WIND = 0x08; //
export const ATTRIBUTE_LIGHT = 0x10; //
export const ATTRIBUTE_DARK = 0x20; //
export const ATTRIBUTE_DEVINE = 0x40; //
export const Attribute2StringCodeMap: Map<number, number> = new Map([
[ATTRIBUTE_EARTH, 1010],
......@@ -166,31 +173,31 @@ export const Attribute2StringCodeMap: Map<number, number> = new Map([
]);
// 种族
const RACE_WARRIOR = 0x1; //
const RACE_SPELLCASTER = 0x2; //
const RACE_FAIRY = 0x4; //
const RACE_FIEND = 0x8; //
const RACE_ZOMBIE = 0x10; //
const RACE_MACHINE = 0x20; //
const RACE_AQUA = 0x40; //
const RACE_PYRO = 0x80; //
const RACE_ROCK = 0x100; //
const RACE_WINDBEAST = 0x200; //
const RACE_PLANT = 0x400; //
const RACE_INSECT = 0x800; //
const RACE_THUNDER = 0x1000; //
const RACE_DRAGON = 0x2000; //
const RACE_BEAST = 0x4000; //
const RACE_BEASTWARRIOR = 0x8000; //
const RACE_DINOSAUR = 0x10000; //
const RACE_FISH = 0x20000; //
const RACE_SEASERPENT = 0x40000; //
const RACE_REPTILE = 0x80000; //
const RACE_PSYCHO = 0x100000; //
const RACE_DEVINE = 0x200000; //
const RACE_CREATORGOD = 0x400000; //
const RACE_WYRM = 0x800000; //
const RACE_CYBERSE = 0x1000000; //
export const RACE_WARRIOR = 0x1; //
export const RACE_SPELLCASTER = 0x2; //
export const RACE_FAIRY = 0x4; //
export const RACE_FIEND = 0x8; //
export const RACE_ZOMBIE = 0x10; //
export const RACE_MACHINE = 0x20; //
export const RACE_AQUA = 0x40; //
export const RACE_PYRO = 0x80; //
export const RACE_ROCK = 0x100; //
export const RACE_WINDBEAST = 0x200; //
export const RACE_PLANT = 0x400; //
export const RACE_INSECT = 0x800; //
export const RACE_THUNDER = 0x1000; //
export const RACE_DRAGON = 0x2000; //
export const RACE_BEAST = 0x4000; //
export const RACE_BEASTWARRIOR = 0x8000; //
export const RACE_DINOSAUR = 0x10000; //
export const RACE_FISH = 0x20000; //
export const RACE_SEASERPENT = 0x40000; //
export const RACE_REPTILE = 0x80000; //
export const RACE_PSYCHO = 0x100000; //
export const RACE_DEVINE = 0x200000; //
export const RACE_CREATORGOD = 0x400000; //
export const RACE_WYRM = 0x800000; //
export const RACE_CYBERSE = 0x1000000; //
export const Race2StringCodeMap: Map<number, number> = new Map([
[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 @@
* */
import { WebSocketStream } from "@/infra";
import handleSocketMessage from "../service/onSocketMessage";
import handleSocketOpen from "../service/onSocketOpen";
export enum socketCmd {
// 建立长连接
CONNECT,
// 断开长连接
DISCONNECT,
// 通过长连接发送数据
SEND,
// FIXME: 应该有个返回值,告诉业务方本次请求的结果。比如建立长连接失败。
export function initSocket(initInfo: {
ip: string;
player: string;
passWd: string;
}): WebSocketStream {
const { ip, player, passWd } = initInfo;
return new WebSocketStream(ip, (conn, _event) =>
handleSocketOpen(conn, ip, player, passWd),
);
}
export interface socketAction {
cmd: socketCmd;
// 创建长连接需要业务方传入的数据
initInfo?: {
ip: string;
player: string;
passWd: string;
};
isReplay?: boolean; // 是否是回放模式
replayInfo?: {
Url: string; // 提供回放服务的地址
data: ArrayBuffer; // 回放数据
};
// 通过长连接发送的数据
payload?: Uint8Array;
export function initReplaySocket(replayInfo: {
url: string; // 提供回放服务的地址
data: ArrayBuffer; // 回放数据
}): WebSocketStream {
const { url, data } = replayInfo;
return new WebSocketStream(url, (conn, _event) => {
console.info("replay websocket open.");
conn.binaryType = "arraybuffer";
conn.send(data);
});
}
let ws: WebSocketStream | null = null;
// 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);
export function sendSocketData(conn: WebSocketStream, payload: Uint8Array) {
conn.ws.send(payload);
}
break;
}
}
export function closeSocket(conn: WebSocketStream) {
conn.close();
}
// TODO: this middleware should be managed under `Container`, too.
import { isNil } from "lodash-es";
import { Database } from "sql.js";
......
This diff is collapsed.
import { ygopro } from "@/api";
import { Container } from "@/container";
import { replayStore } from "@/stores";
import { showWaiting } from "@/ui/Duel/Message";
import { YgoAgent } from "./agent";
import onAnnounce from "./announce";
import onMsgAttack from "./attack";
import onMsgAttackDisable from "./attackDisable";
......@@ -68,19 +70,6 @@ const ActiveList = [
"select_battle_cmd",
"select_unselect_card",
"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_counter",
"select_sum",
......@@ -90,20 +79,43 @@ const ReplayIgnoreMsg = [
];
export default async function handleGameMsg(
container: Container,
pb: ygopro.YgoStocMsg,
agent?: YgoAgent,
): Promise<void> {
const msg = pb.stoc_game_msg;
if (ActiveList.includes(msg.gameMsg)) {
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) {
case "start": {
await onMsgStart(msg.start);
// We should init agent when the MSG_START reached.
if (agent) await agent.init();
break;
}
case "draw": {
......@@ -132,7 +144,7 @@ export default async function handleGameMsg(
break;
}
case "select_place": {
onMsgSelectPlace(msg.select_place);
onMsgSelectPlace(container, msg.select_place);
break;
}
......@@ -141,12 +153,12 @@ export default async function handleGameMsg(
break;
}
case "select_card": {
onMsgSelectCard(msg.select_card);
onMsgSelectCard(container, msg.select_card);
break;
}
case "select_chain": {
onMsgSelectChain(msg.select_chain);
onMsgSelectChain(container, msg.select_chain);
break;
}
......@@ -161,7 +173,7 @@ export default async function handleGameMsg(
break;
}
case "select_option": {
await onMsgSelectOption(msg.select_option);
await onMsgSelectOption(container, msg.select_option);
break;
}
......
......@@ -6,4 +6,5 @@ export default (newTurn: ygopro.StocGameMessage.MsgNewTurn) => {
playEffect(AudioActionType.SOUND_NEXT_TURN);
const player = newTurn.player;
matStore.currentPlayer = player;
matStore.turnCount = matStore.turnCount + 1;
};
......@@ -8,11 +8,12 @@ import {
import MsgSelectBattleCmd = ygopro.StocGameMessage.MsgSelectBattleCmd;
export default (selectBattleCmd: MsgSelectBattleCmd) => {
export default async (selectBattleCmd: MsgSelectBattleCmd) => {
const player = selectBattleCmd.player;
const cmds = selectBattleCmd.battle_cmds;
// 先清掉之前的互动性
// TODO: 确认这里在AI托管的模式下是否需要
cardStore.inner.forEach((card) => {
card.idleInteractivities = [];
});
......
import { sendSelectMultiResponse, ygopro } from "@/api";
import MsgSelectCard = ygopro.StocGameMessage.MsgSelectCard;
import { Container } from "@/container";
import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
import { fetchCheckCardMeta } from "../utils";
export default async (selectCard: MsgSelectCard) => {
export default async (container: Container, selectCard: MsgSelectCard) => {
const { cancelable, min, max, cards } = selectCard;
const conn = container.conn;
// TODO: handle release_param
if (!cancelable && cards.length === 1) {
// auto send
sendSelectMultiResponse([cards[0].response]);
sendSelectMultiResponse(conn, [cards[0].response]);
return;
}
......
import { sendSelectSingleResponse, ygopro } from "@/api";
import { Container } from "@/container";
import { ChainSetting, fetchSelectHintMeta, matStore } from "@/stores";
import { displaySelectActionsModal } from "@/ui/Duel/Message/SelectActionsModal";
import { fetchCheckCardMeta } from "../utils";
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 forced = selectChain.forced;
const _hint0 = selectChain.hint0;
......@@ -15,7 +17,7 @@ export default async (selectChain: MsgSelectChain) => {
if (chainSetting === ChainSetting.CHAIN_IGNORE) {
// 如果玩家配置了忽略连锁,直接回应后端并返回
sendSelectSingleResponse(-1);
sendSelectSingleResponse(conn, -1);
return;
}
......@@ -60,7 +62,7 @@ export default async (selectChain: MsgSelectChain) => {
switch (handle_flag) {
case 0: {
// 直接回答
sendSelectSingleResponse(-1);
sendSelectSingleResponse(conn, -1);
break;
}
......@@ -86,7 +88,7 @@ export default async (selectChain: MsgSelectChain) => {
}
case 4: {
// 有一张强制发动的卡,直接回应
sendSelectSingleResponse(chains[0].response);
sendSelectSingleResponse(conn, chains[0].response);
break;
}
......
......@@ -8,11 +8,12 @@ import {
import MsgSelectIdleCmd = ygopro.StocGameMessage.MsgSelectIdleCmd;
export default (selectIdleCmd: MsgSelectIdleCmd) => {
export default async (selectIdleCmd: MsgSelectIdleCmd) => {
const player = selectIdleCmd.player;
const cmds = selectIdleCmd.idle_cmds;
// 先清掉之前的互动性
// TODO: 确认这里是否需要在AI托管的时候调用
cardStore.inner.forEach((card) => {
card.idleInteractivities = [];
});
......
......@@ -5,16 +5,22 @@ import {
sendSelectOptionResponse,
type ygopro,
} from "@/api";
import { Container } from "@/container";
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;
if (options.length === 0) {
sendSelectOptionResponse(0);
sendSelectOptionResponse(conn, 0);
return;
}
if (options.length === 1) {
sendSelectOptionResponse(options[0].response);
sendSelectOptionResponse(conn, options[0].response);
return;
}
......
import { sendSelectPlaceResponse, ygopro } from "@/api";
import { Container } from "@/container";
import { InteractType, placeStore } from "@/stores";
type MsgSelectPlace = ygopro.StocGameMessage.MsgSelectPlace;
export default (selectPlace: MsgSelectPlace) => {
export default async (container: Container, selectPlace: MsgSelectPlace) => {
const conn = container.conn;
if (selectPlace.count !== 1) {
console.warn(`Unhandled case: ${selectPlace}`);
return;
......@@ -11,7 +13,7 @@ export default (selectPlace: MsgSelectPlace) => {
if (selectPlace.places.length === 1) {
const place = selectPlace.places[0];
sendSelectPlaceResponse({
sendSelectPlaceResponse(conn, {
controller: place.controller,
zone: place.zone,
sequence: place.sequence,
......
......@@ -5,10 +5,10 @@ import { fetchCheckCardMeta } from "../utils";
type MsgSelectTribute = ygopro.StocGameMessage.MsgSelectTribute;
export default async (selectTribute: MsgSelectTribute) => {
// TODO: 当玩家选择卡数大于`max`时,是否也合法?
const { selecteds, mustSelects, selectables } = await fetchCheckCardMeta(
selectTribute.selectable_cards,
);
// TODO: 当玩家选择卡数大于`max`时,是否也合法?
await displaySelectActionsModal({
overflow: true,
totalLevels: 0,
......
......@@ -6,14 +6,15 @@ import { fetchCheckCardMeta } from "../utils";
import { isAllOnField } from "./util";
type MsgSelectUnselectCard = ygopro.StocGameMessage.MsgSelectUnselectCard;
export default async ({
finishable,
cancelable,
min,
max,
selectable_cards: selectableCards,
selected_cards: selectedCards,
}: MsgSelectUnselectCard) => {
export default async (selectUnselectCards: MsgSelectUnselectCard) => {
const {
finishable,
cancelable,
min,
max,
selectable_cards: selectableCards,
selected_cards: selectedCards,
} = selectUnselectCards;
if (
isAllOnField(
selectableCards.concat(selectedCards).map((info) => info.location),
......
import { sendTimeConfirm, ygopro } from "@/api";
import { Container } from "@/container";
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);
if (matStore.isMe(timeLimit.player)) {
sendTimeConfirm();
sendTimeConfirm(container.conn);
}
}
......@@ -8,3 +8,48 @@ export function isAllOnField(locations: ygopro.CardLocation[]): boolean {
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 @@
* */
import { adaptStoc } from "@/api/ocgcore/ocgAdapter/adapter";
import { YgoProPacket } from "@/api/ocgcore/ocgAdapter/packet";
import { Container } from "@/container";
import { replayStore } from "@/stores";
import { YgoAgent } from "./duel/agent";
import handleGameMsg from "./duel/gameMsg";
import handleTimeLimit from "./duel/timeLimit";
import handleDeckCount from "./mora/deckCount";
......@@ -32,12 +34,21 @@ import { handleWaitingSide } from "./side/waitingSide";
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);
for (const packet of packets) {
......@@ -97,12 +108,12 @@ async function _handle(e: MessageEvent) {
// 如果不是回放模式,则记录回放数据
replayStore.record(packet);
}
await handleGameMsg(pb);
await handleGameMsg(container, pb, agent);
break;
}
case "stoc_time_limit": {
handleTimeLimit(pb.stoc_time_limit);
handleTimeLimit(container, pb.stoc_time_limit);
break;
}
case "stoc_error_msg": {
......
......@@ -24,7 +24,7 @@ export interface CardType {
};
}
class CardStore implements NeosStore {
export class CardStore implements NeosStore {
inner: CardType[] = [];
at(zone: ygopro.CardZone, controller: number): CardType[];
at(
......
......@@ -2,15 +2,13 @@ import { proxy } from "valtio";
import { type NeosStore } from "./shared";
export interface ChatState extends NeosStore {
sender: number;
message: string;
export class ChatStore implements NeosStore {
sender: number = -1;
message: string = "";
reset(): void {
this.message = "";
}
}
export const chatStore = proxy<ChatState>({
sender: -1,
message: "",
reset() {
chatStore.message = "";
},
});
export const chatStore = proxy<ChatStore>(new ChatStore());
......@@ -18,6 +18,7 @@ const getWhom = (controller: number): "me" | "op" =>
* 原本名字叫judgeSelf
*/
export const isMe = (controller: number): boolean => {
// FIXME: all of the `matStore` need to access with container
switch (matStore.selfType) {
case 1:
// 自己是先攻
......@@ -93,9 +94,11 @@ const initialState: Omit<MatState, "reset"> = {
duelEnd: false,
// methods
isMe,
turnCount: 0,
error: "",
};
class MatStore implements MatState, NeosStore {
export class MatStore implements MatState, NeosStore {
chains = initialState.chains;
chainSetting = initialState.chainSetting;
timeLimits = initialState.timeLimits;
......@@ -109,6 +112,9 @@ class MatStore implements MatState, NeosStore {
tossResult = initialState.tossResult;
selectUnselectInfo = initialState.selectUnselectInfo;
duelEnd = initialState.duelEnd;
turnCount = initialState.turnCount;
error = initialState.error;
// methods
isMe = initialState.isMe;
reset(): void {
......@@ -137,6 +143,8 @@ class MatStore implements MatState, NeosStore {
selectedList: [],
};
this.duelEnd = false;
this.turnCount = 0;
this.error = initialState.error;
}
}
......
......@@ -49,6 +49,9 @@ export interface MatState {
/** 根据自己的先后手判断是否是自己 */
isMe: (player: number) => boolean;
turnCount: number;
error: string;
}
export interface InitInfo {
......
......@@ -58,7 +58,7 @@ const initialState = {
},
};
class PlaceStore implements NeosStore {
export class PlaceStore implements NeosStore {
inner: {
[zone: number]: {
me: BlockState[];
......@@ -70,14 +70,15 @@ class PlaceStore implements NeosStore {
controller: number;
sequence: number;
}): BlockState | undefined {
return placeStore.inner[location.zone][
return this.inner[location.zone][
// FIXME: inject `matStore`
matStore.isMe(location.controller) ? "me" : "op"
][location.sequence];
}
clearAllInteractivity() {
(["me", "op"] as const).forEach((who) => {
([MZONE, SZONE] as const).forEach((where) => {
placeStore.inner[where][who].forEach(
this.inner[where][who].forEach(
(block) => (block.interactivity = undefined),
);
});
......@@ -87,7 +88,7 @@ class PlaceStore implements NeosStore {
const resetObj = cloneDeep(initialState);
Object.keys(resetObj).forEach((key) => {
// @ts-ignore
placeStore.inner[key] = resetObj[key];
this.inner[key] = resetObj[key];
});
}
}
......
......@@ -33,7 +33,7 @@ export enum RoomStage {
DUEL_START = 6, // 决斗开始
}
class RoomStore implements NeosStore {
export class RoomStore implements NeosStore {
joined: boolean = false; // 是否已经加入房间
players: (Player | undefined)[] = Array.from({ length: 4 }).map(
(_) => undefined,
......
......@@ -15,7 +15,7 @@ export enum SideStage {
WAITING = 8, // 观战者等待双方玩家
}
class SideStore implements NeosStore {
export class SideStore implements NeosStore {
stage: SideStage = SideStage.NONE;
// 因为在上一局可能会出现断线重连,
......
......@@ -4,11 +4,13 @@ import { useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio";
import { sendSurrender } from "@/api";
import { getUIContainer } from "@/container/compat";
import { matStore } from "@/stores";
export const Alert = () => {
const matSnap = useSnapshot(matStore);
const unimplemented = matSnap.unimplemented;
const container = getUIContainer();
const navigate = useNavigate();
......@@ -24,7 +26,7 @@ export const Alert = () => {
banner
afterClose={() => {
// 发送投降信号
sendSurrender();
sendSurrender(container.conn);
navigate("/match");
}}
/>
......
......@@ -10,6 +10,7 @@ import {
sendSelectOptionResponse,
} from "@/api";
import { isDeclarable, isToken } from "@/common";
import { getUIContainer } from "@/container/compat";
import { emptySearchConditions } from "@/middleware/sqlite/fts";
import { NeosModal } from "../NeosModal";
......@@ -35,6 +36,7 @@ export const AnnounceModal: React.FC = () => {
const [searchWord, setSearchWord] = useState("");
const [cardList, setCardList] = useState<CardMeta[]>([]);
const [selected, setSelected] = useState<number | undefined>(undefined);
const container = getUIContainer();
const handleSearch = () => {
const result = searchCards({
......@@ -51,7 +53,7 @@ export const AnnounceModal: React.FC = () => {
};
const onSummit = () => {
if (selected !== undefined) {
sendSelectOptionResponse(selected);
sendSelectOptionResponse(container.conn, selected);
rs();
setSearchWord("");
setCardList([]);
......
......@@ -5,6 +5,7 @@ import React, { useEffect, useState } from "react";
import { proxy, useSnapshot } from "valtio";
import { fetchStrings, Region, sendSelectCounterResponse } from "@/api";
import { getUIContainer } from "@/container/compat";
import { YgoCard } from "@/ui/Shared";
import { NeosModal } from "../NeosModal";
......@@ -27,6 +28,7 @@ const defaultProps = {
const localStore = proxy<CheckCounterModalProps>(defaultProps);
export const CheckCounterModal = () => {
const container = getUIContainer();
const snapCheckCounterModal = useSnapshot(localStore);
const isOpen = snapCheckCounterModal.isOpen;
......@@ -46,7 +48,7 @@ export const CheckCounterModal = () => {
}, [options]);
const onFinish = () => {
sendSelectCounterResponse(selected);
sendSelectCounterResponse(container.conn, selected);
rs();
};
......
......@@ -18,6 +18,7 @@ export const HintNotification = () => {
const toss = snap.tossResult;
const handResults = snap.handResults;
const currentPhase = snap.phase.currentPhase;
const error = snap.error;
const [msgApi, msgContextHolder] = message.useMessage({
maxCount: NeosConfig.ui.hint.maxCount,
......@@ -61,6 +62,12 @@ export const HintNotification = () => {
}
}, [currentPhase]);
useEffect(() => {
if (error !== "") {
msgApi.error(error);
}
}, [error]);
return <>{msgContextHolder}</>;
};
......
......@@ -35,7 +35,7 @@ export const NeosModal: React.FC<ModalProps> = (props) => {
maskClosable={true}
onCancel={() => setMini(!mini)}
closeIcon={mini ? <UpOutlined /> : <MinusOutlined />}
bodyStyle={{ padding: "10px 0" }}
style={{ padding: "10px 0" }}
mask={!mini}
wrapClassName={classNames({ [styles.wrap]: mini })}
closable={true}
......
......@@ -12,6 +12,8 @@ import {
sendSelectIdleCmdResponse,
sendSelectOptionResponse,
} from "@/api";
import { Container } from "@/container";
import { getUIContainer } from "@/container/compat";
import { NeosModal } from "../NeosModal";
......@@ -29,6 +31,7 @@ const store = proxy(defaultStore);
const MAX_NUM_PER_PAGE = 4;
export const OptionModal = () => {
const container = getUIContainer();
const snap = useSnapshot(store);
const { title, isOpen, min, options } = snap;
// options可能太多,因此分页展示
......@@ -41,7 +44,7 @@ export const OptionModal = () => {
const responses = selecteds.flat();
if (responses.length > 0) {
const response = responses.reduce((res, current) => res | current, 0); // 多个选择求或
sendSelectOptionResponse(response);
sendSelectOptionResponse(container.conn, response);
rs();
}
};
......@@ -132,6 +135,7 @@ export const displayOptionModal = async (
};
export const handleEffectActivation = async (
container: Container,
meta: CardMeta,
effectInteractivies: {
desc: string;
......@@ -144,7 +148,7 @@ export const handleEffectActivation = async (
}
if (effectInteractivies.length === 1) {
// 如果只有一个效果,点击直接触发
sendSelectIdleCmdResponse(effectInteractivies[0].response);
sendSelectIdleCmdResponse(container.conn, effectInteractivies[0].response);
} else {
// optionsModal
const options = effectInteractivies.map((effect) => {
......
......@@ -5,6 +5,7 @@ import React, { useState } from "react";
import { proxy, useSnapshot } from "valtio";
import { sendSelectPositionResponse, ygopro } from "@/api";
import { getUIContainer } from "@/container/compat";
import { NeosModal } from "../NeosModal";
......@@ -91,6 +92,7 @@ const translations: Translations = {
};
export const PositionModal = () => {
const container = getUIContainer();
const { isOpen, positions } = useSnapshot(localStore);
const [selected, setSelected] = useState<ygopro.CardPosition | undefined>(
undefined,
......@@ -105,7 +107,7 @@ export const PositionModal = () => {
disabled={selected === undefined}
onClick={() => {
if (selected !== undefined) {
sendSelectPositionResponse(selected);
sendSelectPositionResponse(container.conn, selected);
rs();
}
}}
......
import { INTERNAL_Snapshot as Snapshot, proxy, useSnapshot } from "valtio";
import { sendSelectMultiResponse, sendSelectSingleResponse } from "@/api";
import { getUIContainer } from "@/container/compat";
import {
type Option,
......@@ -32,25 +33,26 @@ const defaultProps: Omit<
const localStore = proxy(defaultProps);
export const SelectActionsModal: React.FC = () => {
const container = getUIContainer();
const snap = useSnapshot(localStore);
const onSubmit = (options: Snapshot<Option[]>) => {
const values = options.map((option) => option.response!);
if (localStore.isChain) {
sendSelectSingleResponse(values[0]);
sendSelectSingleResponse(container.conn, values[0]);
} else {
sendSelectMultiResponse(values);
sendSelectMultiResponse(container.conn, values);
}
rs();
};
const onFinish = () => {
sendSelectSingleResponse(FINISH_RESPONSE);
sendSelectSingleResponse(container.conn, FINISH_RESPONSE);
rs();
};
const onCancel = () => {
sendSelectSingleResponse(CANCEL_RESPONSE);
sendSelectSingleResponse(container.conn, CANCEL_RESPONSE);
rs();
};
......
......@@ -22,6 +22,7 @@ import { proxy, useSnapshot } from "valtio";
import { sendSortCardResponse } from "@/api";
import { CardMeta, getCardImgUrl } from "@/api/cards";
import { getUIContainer } from "@/container/compat";
import { NeosModal } from "../NeosModal";
......@@ -41,6 +42,7 @@ const defaultProps = {
const localStore = proxy<SortCardModalProps>(defaultProps);
export const SortCardModal = () => {
const container = getUIContainer();
const { isOpen, options } = useSnapshot(localStore);
const [items, setItems] = useState(options);
const sensors = useSensors(
......@@ -51,7 +53,10 @@ export const SortCardModal = () => {
);
const onFinish = () => {
sendSortCardResponse(items.map((item) => item.response));
sendSortCardResponse(
container.conn,
items.map((item) => item.response),
);
rs();
};
const onDragEnd = (event: DragEndEvent) => {
......
......@@ -3,6 +3,7 @@ import React from "react";
import { proxy, useSnapshot } from "valtio";
import { sendSelectEffectYnResponse } from "@/api";
import { getUIContainer } from "@/container/compat";
import { matStore } from "@/stores";
import { NeosModal } from "../NeosModal";
......@@ -16,6 +17,7 @@ const defaultProps = { isOpen: false };
const localStore = proxy<YesNoModalProps>(defaultProps);
export const YesNoModal: React.FC = () => {
const container = getUIContainer();
const { isOpen, msg } = useSnapshot(localStore);
const hint = useSnapshot(matStore.hint);
......@@ -31,7 +33,7 @@ export const YesNoModal: React.FC = () => {
<>
<Button
onClick={() => {
sendSelectEffectYnResponse(false);
sendSelectEffectYnResponse(container.conn, false);
rs();
}}
>
......@@ -40,7 +42,7 @@ export const YesNoModal: React.FC = () => {
<Button
type="primary"
onClick={() => {
sendSelectEffectYnResponse(true);
sendSelectEffectYnResponse(container.conn, true);
rs();
}}
>
......
......@@ -2,6 +2,8 @@ import classnames from "classnames";
import { type INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
import { sendSelectPlaceResponse, ygopro } from "@/api";
import { Container } from "@/container";
import { getUIContainer } from "@/container/compat";
import {
type BlockState,
cardStore,
......@@ -46,6 +48,7 @@ const BgExtraRow: React.FC<{
meSnap: Snapshot<BlockState[]>;
opSnap: Snapshot<BlockState[]>;
}> = ({ meSnap, opSnap }) => {
const container = getUIContainer();
return (
<div className={classnames(styles.row)}>
{Array.from({ length: 2 }).map((_, i) => (
......@@ -53,8 +56,8 @@ const BgExtraRow: React.FC<{
key={i}
className={styles.extra}
onClick={() => {
onBlockClick(meSnap[i].interactivity);
onBlockClick(opSnap[1 - i].interactivity);
onBlockClick(container, meSnap[i].interactivity);
onBlockClick(container, opSnap[1 - i].interactivity);
}}
disabled={meSnap[i].disabled || opSnap[1 - i].disabled}
highlight={!!meSnap[i].interactivity || !!opSnap[1 - i].interactivity}
......@@ -71,23 +74,27 @@ const BgRow: React.FC<{
szone?: boolean;
opponent?: boolean;
snap: Snapshot<BlockState[]>;
}> = ({ szone = false, opponent = false, snap }) => (
<div className={classnames(styles.row, { [styles.opponent]: opponent })}>
{Array.from({ length: 5 }).map((_, i) => (
<BgBlock
key={i}
className={classnames({ [styles.szone]: szone })}
onClick={() => onBlockClick(snap[i].interactivity)}
disabled={snap[i].disabled}
highlight={!!snap[i].interactivity}
chains={{ chains: snap[i].chainIndex }}
/>
))}
</div>
);
}> = ({ szone = false, opponent = false, snap }) => {
const container = getUIContainer();
return (
<div className={classnames(styles.row, { [styles.opponent]: opponent })}>
{Array.from({ length: 5 }).map((_, i) => (
<BgBlock
key={i}
className={classnames({ [styles.szone]: szone })}
onClick={() => onBlockClick(container, snap[i].interactivity)}
disabled={snap[i].disabled}
highlight={!!snap[i].interactivity}
chains={{ chains: snap[i].chainIndex }}
/>
))}
</div>
);
};
const BgOtherBlocks: React.FC<{ op?: boolean }> = ({ op }) => {
useSnapshot(cardStore);
const container = getUIContainer();
const meController = isMe(0) ? 0 : 1;
const judgeGlowing = (zone: ygopro.CardZone) =>
!!cardStore
......@@ -134,7 +141,7 @@ const BgOtherBlocks: React.FC<{ op?: boolean }> = ({ op }) => {
/>
<BgBlock
className={styles.field}
onClick={() => onBlockClick(field.interactivity)}
onClick={() => onBlockClick(container, field.interactivity)}
disabled={field.disabled}
highlight={!!field.interactivity}
chains={{ chains: field.chainIndex, op }}
......@@ -171,9 +178,12 @@ export const Bg: React.FC = () => {
);
};
const onBlockClick = (placeInteractivity: PlaceInteractivity) => {
const onBlockClick = (
container: Container,
placeInteractivity: PlaceInteractivity,
) => {
if (placeInteractivity) {
sendSelectPlaceResponse(placeInteractivity.response);
sendSelectPlaceResponse(container.conn, placeInteractivity.response);
cardStore.inner.forEach((card) => (card.idleInteractivities = []));
placeStore.clearAllInteractivity();
}
......
......@@ -11,6 +11,8 @@ import {
sendSelectIdleCmdResponse,
ygopro,
} from "@/api";
import { Container } from "@/container";
import { getUIContainer } from "@/container/compat";
import { eventbus, Task } from "@/infra";
import { cardStore, CardType, Interactivity, InteractType } from "@/stores";
import { showCardModal as displayCardModal } from "@/ui/Duel/Message/CardModal";
......@@ -40,6 +42,7 @@ import type { SpringApiProps } from "./springs/types";
const { HAND, GRAVE, REMOVED, EXTRA, MZONE, SZONE, TZONE } = ygopro.CardZone;
export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
const container = getUIContainer();
const card = cardStore.inner[idx];
const snap = useSnapshot(card);
......@@ -161,7 +164,10 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
if (!isField) {
// 单卡: 直接召唤/特殊召唤/...
const card = cards[0];
sendSelectIdleCmdResponse(getNonEffectResponse(action, card));
sendSelectIdleCmdResponse(
container.conn,
getNonEffectResponse(action, card),
);
clearAllIdleInteractivities();
} else {
// 场地: 选择卡片
......@@ -174,7 +180,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
})),
});
if (option.length > 0) {
sendSelectIdleCmdResponse(option[0].response!);
sendSelectIdleCmdResponse(container.conn, option[0].response!);
clearAllIdleInteractivities();
}
}
......@@ -223,6 +229,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
}
// 选择发动哪个效果
handleEffectActivation(
container,
tmpCard.idleInteractivities
.filter(
({ interactType }) => interactType === InteractType.ACTIVATE,
......@@ -246,7 +253,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
const selectInfo = card.selectInfo;
if (selectInfo.selectable || selectInfo.selected) {
if (selectInfo.response !== undefined) {
sendSelectMultiResponse([selectInfo.response]);
sendSelectMultiResponse(container.conn, [selectInfo.response]);
clearSelectInfo();
} else {
console.error("card is selectable but the response is undefined!");
......@@ -358,13 +365,14 @@ type DropdownItem = NonNullable<MenuProps["items"]>[number] & {
};
const handleEffectActivation = (
container: Container,
effectInteractivies: Interactivy[],
meta?: CardMeta,
) => {
if (!effectInteractivies.length) return;
else if (effectInteractivies.length === 1) {
// 如果只有一个效果,点击直接触发
sendSelectIdleCmdResponse(effectInteractivies[0].response);
sendSelectIdleCmdResponse(container.conn, effectInteractivies[0].response);
} else {
// optionsModal
const options = effectInteractivies.map((effect) => {
......
......@@ -3,6 +3,8 @@ import {
CheckOutlined,
CloseCircleFilled,
MessageFilled,
RobotFilled,
RobotOutlined,
StepForwardFilled,
} from "@ant-design/icons";
import {
......@@ -33,6 +35,8 @@ import styles from "./index.module.scss";
import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType;
import { useTranslation } from "react-i18next";
import { getUIContainer } from "@/container/compat";
import { clearAllIdleInteractivities, clearSelectInfo } from "../../utils";
import { openChatBox } from "../ChatBox";
......@@ -215,6 +219,7 @@ const initialPhaseBind: [
];
export const Menu = () => {
const container = getUIContainer();
const { t: i18n } = useTranslation("Menu");
const {
currentPlayer,
......@@ -226,6 +231,10 @@ export const Menu = () => {
[],
);
const [enableKuriboh, setEnableKuriboh] = useState(
container.getEnableKuriboh(),
);
useEffect(() => {
const endResponse = [
PhaseType.BATTLE_START,
......@@ -266,8 +275,9 @@ export const Menu = () => {
label,
disabled: disabled,
onClick: () => {
if (response === 2) sendSelectIdleCmdResponse(response);
else sendSelectBattleCmdResponse(response);
if (response === 2)
sendSelectIdleCmdResponse(container.conn, response);
else sendSelectBattleCmdResponse(container.conn, response);
clearAllIdleInteractivities();
},
icon: disabled ? <CheckOutlined /> : <ArrowRightOutlined />,
......@@ -299,12 +309,20 @@ export const Menu = () => {
{
label: i18n("Confirm"),
danger: true,
onClick: sendSurrender,
onClick: () => {
sendSurrender(container.conn);
},
},
].map((item, i) => ({ key: i, ...item }));
const globalDisable = !matStore.isMe(currentPlayer);
const switchAutoSelect = () => {
const newValue = !enableKuriboh;
setEnableKuriboh(newValue);
container.setEnableKuriboh(newValue);
};
return (
<div className={styles["menu-container"]}>
<SelectManager />
......@@ -331,6 +349,13 @@ export const Menu = () => {
type="text"
></Button>
</DropdownWithTitle>
<Tooltip title="AI">
<Button
icon={enableKuriboh ? <RobotFilled /> : <RobotOutlined />}
onClick={switchAutoSelect}
type="text"
></Button>
</Tooltip>
<Tooltip title={i18n("ChatRoom")}>
<Button
icon={<MessageFilled />}
......@@ -401,10 +426,11 @@ const ChainIcon: React.FC<{ chainSetting: ChainSetting }> = ({
};
const SelectManager: React.FC = () => {
const container = getUIContainer();
const { t: i18n } = useTranslation("Menu");
const { finishable, cancelable } = useSnapshot(matStore.selectUnselectInfo);
const onFinishOrCancel = () => {
sendSelectSingleResponse(FINISH_CANCEL_RESPONSE);
sendSelectSingleResponse(container.conn, FINISH_CANCEL_RESPONSE);
clearSelectInfo();
};
return (
......
......@@ -81,8 +81,8 @@
"Select": "请选择",
"Minimum": "最小值",
"Maximum": "最大值",
"Confirm": "确 定",
"Cancel": "取 消"
"Confirm": "确定",
"Cancel": "取消"
},
"CardDetails": {
"Level": "等级",
......@@ -167,7 +167,9 @@
"UltraPreemptiveServer": "超先行服",
"PlayerNickname": "玩家昵称",
"RoomPasswordOptional": "房间密码(可选)",
"JoinRoom": "加入房间"
"JoinRoom": "加入房间",
"EnableAIAssist": "启用AI辅助功能",
"BetaTest": "Beta测试"
},
"ReplayModal": {
"SelectReplay": "选择回放",
......
......@@ -15,4 +15,16 @@
.select {
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 { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
......@@ -43,6 +43,7 @@ export const MatchModal: React.FC = ({}) => {
const [confirmLoading, setConfirmLoading] = useState(false);
const navigate = useNavigate();
const { t: i18n } = useTranslation("MatchModal");
const [enableKuriboh, setEnableKuriBoh] = useState(false);
const handlePlayerChange = (event: ChangeEvent<HTMLInputElement>) => {
setPlayer(event.target.value);
......@@ -60,6 +61,7 @@ export const MatchModal: React.FC = ({}) => {
player,
ip: genServerAddress(serverId),
passWd: passwd,
enableKuriboh,
});
};
......@@ -116,6 +118,14 @@ export const MatchModal: React.FC = ({}) => {
]}
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
className={styles.input}
type="text"
......
......@@ -101,7 +101,7 @@ export const WatchContent: React.FC = () => {
<Input
className={styles.input}
placeholder={i18n("SearchRoomByPlayerUsername")}
bordered={false}
variant="borderless"
suffix={<Button type="text" icon={<SearchOutlined />} />}
value={query}
onChange={(e) => setQuery(e.target.value)}
......
......@@ -2,8 +2,13 @@ import rustInit from "rust-src";
import { initStrings, initSuperPrerelease } from "@/api";
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 {
pollSocketLooper,
pollSocketLooperWithAgent,
} from "@/service/executor";
const NeosConfig = useConfig();
......@@ -12,6 +17,7 @@ export const connectSrvpro = async (params: {
ip: string;
player: string;
passWd: string;
enableKuriboh?: boolean;
replay?: boolean;
replayData?: ArrayBuffer;
}) => {
......@@ -38,20 +44,32 @@ export const connectSrvpro = async (params: {
await initSuperPrerelease();
if (params.replay && params.replayData) {
// 连接回放websocket服务
socketMiddleWare({
cmd: socketCmd.CONNECT,
isReplay: true,
replayInfo: {
Url: NeosConfig.replayUrl,
data: params.replayData,
},
// connect to replay Server
const conn = initReplaySocket({
url: NeosConfig.replayUrl,
data: params.replayData,
});
// initialize the UI Container
initUIContainer(conn);
// execute the event looper
pollSocketLooper(getUIContainer());
} else {
// 通过socket中间件向ygopro服务端请求建立长连接
socketMiddleWare({
cmd: socketCmd.CONNECT,
initInfo: params,
});
// connect to the ygopro Server
const conn = initSocket(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";
import { useSnapshot } from "valtio";
import { sendChat } from "@/api";
import { getUIContainer } from "@/container/compat";
import { chatStore, isMe, roomStore } from "@/stores";
interface ChatItem {
......@@ -14,6 +15,7 @@ interface ChatItem {
}
export const useChat = (isDuel: boolean = false) => {
const container = getUIContainer();
const [chatList, setChatList] = useState<ChatItem[]>([]);
const [input, setInput] = useState<string | undefined>(undefined);
const chat = useSnapshot(chatStore);
......@@ -31,7 +33,7 @@ export const useChat = (isDuel: boolean = false) => {
/** 发信息 */
const onSend = () => {
if (input !== undefined) {
sendChat(input);
sendChat(container.conn, input);
setInput("");
}
};
......
......@@ -3,11 +3,13 @@ import React from "react";
import { useSnapshot } from "valtio";
import { sendTpResult } from "@/api";
import { getUIContainer } from "@/container/compat";
import { SideStage, sideStore } from "@/stores";
import styles from "./TpModal.module.scss";
export const TpModal: React.FC = () => {
const container = getUIContainer();
const { stage } = useSnapshot(sideStore);
return (
......@@ -20,7 +22,7 @@ export const TpModal: React.FC = () => {
<div className={styles.container}>
<Button
onClick={() => {
sendTpResult(true);
sendTpResult(container.conn, true);
sideStore.stage = SideStage.TP_SELECTED;
}}
>
......@@ -28,7 +30,7 @@ export const TpModal: React.FC = () => {
</Button>
<Button
onClick={() => {
sendTpResult(false);
sendTpResult(container.conn, false);
sideStore.stage = SideStage.TP_SELECTED;
}}
>
......
......@@ -8,6 +8,7 @@ import { useSnapshot } from "valtio";
import { CardMeta, fetchCard, sendUpdateDeck } from "@/api";
import { isExtraDeckCard } from "@/common";
import { getUIContainer } from "@/container/compat";
import { AudioActionType, changeScene } from "@/infra/audio";
import { IDeck, roomStore, SideStage, sideStore } from "@/stores";
......@@ -24,6 +25,7 @@ export const loader: LoaderFunction = async () => {
};
export const Component: React.FC = () => {
const container = getUIContainer();
const { message } = App.useApp();
const initialDeck = sideStore.getSideDeck();
const { stage } = useSnapshot(sideStore);
......@@ -65,7 +67,7 @@ export const Component: React.FC = () => {
message.info("重置成功");
};
const onSummit = () => {
sendUpdateDeck(deck);
sendUpdateDeck(container.conn, deck);
sideStore.setSideDeck(deck);
};
......
......@@ -11,7 +11,6 @@ import {
sendUpdateDeck,
ygopro,
} from "@/api";
import socketMiddleWare, { socketCmd } from "@/middleware/socket";
import PlayerState = ygopro.StocHsPlayerChange.State;
import SelfType = ygopro.StocTypeChange.SelfType;
import { App, Avatar, Button, Skeleton, Space } from "antd";
......@@ -22,7 +21,9 @@ import { LoaderFunction, useNavigate } from "react-router-dom";
import { useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { getUIContainer } from "@/container/compat";
import { AudioActionType, changeScene } from "@/infra/audio";
import { closeSocket } from "@/middleware/socket";
import {
accountStore,
deckStore,
......@@ -48,6 +49,7 @@ export const loader: LoaderFunction = async () => {
};
export const Component: React.FC = () => {
const container = getUIContainer();
const { t: i18n } = useTranslation("WaitRoom");
const { message } = App.useApp();
const { user } = useSnapshot(accountStore);
......@@ -63,7 +65,7 @@ export const Component: React.FC = () => {
const navigate = useNavigate();
const updateDeck = (deck: IDeck) => {
sendUpdateDeck(deck);
sendUpdateDeck(container.conn, deck);
// 设置side里面的卡组
sideStore.setSideDeck(deck);
};
......@@ -71,7 +73,7 @@ export const Component: React.FC = () => {
const onDeckSelected = (deckName: string) => {
const newDeck = deckStore.get(deckName);
if (newDeck) {
sendHsNotReady();
sendHsNotReady(container.conn);
updateDeck(newDeck);
setDeck(newDeck);
} else {
......@@ -83,12 +85,12 @@ export const Component: React.FC = () => {
if (me?.state === PlayerState.NO_READY) {
if (deck) {
updateDeck(deck);
sendHsReady();
sendHsReady(container.conn);
} else {
message.error("请先选择卡组");
}
} else {
sendHsNotReady();
sendHsNotReady(container.conn);
}
};
......@@ -96,7 +98,7 @@ export const Component: React.FC = () => {
// 组件初始化时发一次更新卡组的包
//
// 否则娱乐匹配准备会有问题(原因不明)
if (deck) sendUpdateDeck(deck);
if (deck) sendUpdateDeck(container.conn, deck);
}, []);
useEffect(() => {
if (room.stage === RoomStage.DUEL_START) {
......@@ -184,11 +186,11 @@ export const Component: React.FC = () => {
</div>
<ActionButton
onMoraSelect={(mora) => {
sendHandResult(mora);
sendHandResult(container.conn, mora);
roomStore.stage = RoomStage.HAND_SELECTED;
}}
onTpSelect={(tp) => {
sendTpResult(tp === Tp.First);
sendTpResult(container.conn, tp === Tp.First);
roomStore.stage = RoomStage.TP_SELECTED;
}}
/>
......@@ -263,6 +265,7 @@ const MoraAvatar: React.FC<{ mora?: Mora }> = ({ mora }) => (
const Controller: React.FC<{ onDeckChange: (deckName: string) => void }> = ({
onDeckChange,
}) => {
const container = getUIContainer();
const { t: i18n } = useTranslation("WaitRoom");
const snapDeck = useSnapshot(deckStore);
const snapRoom = useSnapshot(roomStore);
......@@ -287,9 +290,9 @@ const Controller: React.FC<{ onDeckChange: (deckName: string) => void }> = ({
icon={<IconFont type="icon-record" size={18} />}
onClick={() => {
if (snapRoom.selfType !== SelfType.OBSERVER) {
sendHsToObserver();
sendHsToObserver(container.conn);
} else {
sendHsToDuelList();
sendHsToDuelList(container.conn);
}
}}
>
......@@ -326,8 +329,8 @@ const SideButtons: React.FC<{
</span>
}
onClick={() => {
// 断开websocket🔗
socketMiddleWare({ cmd: socketCmd.DISCONNECT });
// 断开websocket🔗
closeSocket(getUIContainer().conn);
// 重置stores
resetUniverse();
// 返回上一个路由
......@@ -355,6 +358,7 @@ const ActionButton: React.FC<{
onMoraSelect: (mora: Mora) => void;
onTpSelect: (tp: Tp) => void;
}> = ({ onMoraSelect, onTpSelect }) => {
const container = getUIContainer();
const room = useSnapshot(roomStore);
const { stage, isHost } = room;
const { t: i18n } = useTranslation("WaitRoom");
......@@ -371,7 +375,7 @@ const ActionButton: React.FC<{
room.getOpPlayer()?.state !== PlayerState.READY))
}
onClick={() => {
sendHsStart();
sendHsStart(container.conn);
}}
>
{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