Files
Line_Project_Bot/src/UI/Lobby/SDGame.tsx
T

362 lines
11 KiB
TypeScript
Raw Normal View History

import MainControl from "@/Common/MainControl/MainControl";
import CSMessage from "@/Common/Message/CSMessage";
import { CoroutineV2 } from "@/Engine/CatanEngine/CoroutineV2/CoroutineV2";
import { INetResponse } from "@/Engine/CatanEngine/NetManagerV2/Core/INetResponse";
import { NetConnector } from "@/Engine/CatanEngine/NetManagerV2/NetConnector";
import { NetManagerSD } from "@/Engine/CatanEngine/NetManagerV2/NetManagerSD";
import CSSettingsSDV3 from "@/FormTableSD/CSSettingsSDV3";
import { SlotsetTableRow } from "@/FormTableSD/Tables/SlotsetTable";
import { gameObj, useGameItems } from "@/context/GameItemsContext";
import { SDAccountLoginRequest } from "@/define/Request/AccountRequest";
import { SlotInRequest } from "@/define/Request/SlotRequest";
import GameManager from "@/modules/GameManager";
import WheelSelect from "@/components/WheelSelect";
import { Button, InputNumber, Switch } from "antd";
import dayjs from "dayjs";
import React, { useEffect, useState } from "react";
const SDGame = (props: ISDGame) => {
const { gameUrl, onClickSlotOut } = props;
const { gameData } = useGameItems();
const { nowSlotId } = gameData;
const [isOK, setIsOK] = useState<boolean>(false);
const [isSpin, setIsSpin] = useState<boolean>(false);
const [betOptions, setBetOptions] = useState<number[]>([]);
const [bet, setBet] = useState<number>(0);
const [delay, setDelay] = useState<number>(0.1);
const [isRatioStop, setIsRatioStop] = useState<boolean>(false);
const [ratioStop, setRatioStop] = useState<number>(200);
const [isCountStop, setIsCountStop] = useState<boolean>(false);
const [countStop, setCountStop] = useState<number>(200);
const [log, setLog] = useState<string[]>([]);
let conn: NetConnector = undefined;
function* onLoad(): IterableIterator<any> {
reset();
const url: URL = new URL(gameUrl);
const queryParameters: URLSearchParams = new URLSearchParams(url.search);
const token: string = queryParameters.get("token");
const slotid: number = +queryParameters.get("slotid");
let host: string = queryParameters.get("host");
const port: number = 9005;
// NetConnector 對沒帶協定的 host 會用「目前頁面」的協定判斷 ws/wss,
// bot 頁面是 http 會誤判成 ws://;改依遊戲網址本身的協定,https 就強制 wss
if (url.protocol === "https:" && !/^https?:\/\//.test(host)) {
host = `https://${host}`;
}
GameManager.SlotData.SlotId = slotid;
GameManager.SlotData.IsRatioStop = isRatioStop;
GameManager.SlotData.RatioStop = ratioStop;
GameManager.SlotData.IsCountStop = isCountStop;
GameManager.SlotData.CountStop = countStop;
conn = new NetConnector(host, port);
conn.OnDataReceived.AddCallback(onNetDataReceived);
conn.OnDisconnected.AddCallback(onNetDisconnected);
loadJSON(slotid);
setSlotClass();
NetManagerSD.Initialize(conn);
console.log("[SDsocket] connecting...");
AddLog(`連線 SD socket: ${host}:${port}`);
yield NetManagerSD.ConnectAsync();
AddLog("SD socket 已連線");
yield* login(token);
yield* slotIn();
}
function onClose(): void {
CoroutineV2.StopCoroutinesBy("Spin");
try {
if (conn && conn.IsConnected) {
conn.Disconnect();
}
} catch (e) {
//
}
reset();
}
function reset(): void {
GameManager.SlotData.SlotId = undefined;
GameManager.SlotData.SlotClass = undefined;
GameManager.SlotData.IsSpin = undefined;
GameManager.SlotData.NowBet = undefined;
GameManager.SlotData.IsRatioStop = undefined;
GameManager.SlotData.RatioStop = undefined;
GameManager.SlotData.IsCountStop = undefined;
GameManager.SlotData.CountStop = undefined;
}
function* login(token: string): IterableIterator<any> {
const req = new SDAccountLoginRequest(token);
yield req.SendAsync(true);
const resp = req.Result;
if (!resp.IsValid) {
CSMessage.NetError(resp.Method, resp.Status, "SD Account Login Fail");
AddLog(`SD 登入失敗 [${resp.Method}] status:${resp.Status}`);
return;
}
AddLog("SD 登入成功");
}
function* slotIn(): IterableIterator<any> {
let req: SlotInRequest = new SlotInRequest(GameManager.SlotData.SlotId);
yield req.SendAsync(true);
let resp: INetResponse<JSON> = req.Result;
if (resp.IsValid) {
const br: number[] = resp.Data["br"];
const db: number = resp.Data["db"];
setBetOptions(br);
setBet(br[db]);
GameManager.SlotData.NowBet = br[db];
setIsOK(true);
AddLog(`機台就緒 押注選項: [${br.join(", ")}]`);
} else {
AddLog(`SlotIn 失敗 [${resp.Method}] status:${resp.Status}`);
}
}
async function loadJSON(slotid: number) {
let slotset: SlotsetTableRow = CSSettingsSDV3.Slotset[slotid];
let formName: string[] = slotset.FormName;
let parallel: Promise<void>[] = [];
for (let i: number = 0; i < formName.length; i++) {
await MainControl.DownloadFormSetting(formName[i]);
}
// set Form
await Promise.all(parallel);
}
async function setSlotClass() {
let slot: any;
const slotGroup: typeof import("../../define/Game/Base/Slot") = await import(/* @vite-ignore */`../../define/Game/Base/Slot`);
try {
slot = slotGroup[`Slot${GameManager.SlotData.SlotId}`];
} catch (error) {
//
}
if (!slot) {
slot = slotGroup.SlotBase;
}
GameManager.SlotData.SlotClass = new slot(GameManager.SlotData.SlotId, AddLog);
}
async function OnClickSpin() {
GameManager.SlotData.IsSpin = true;
setIsSpin(true);
CoroutineV2.Single(spin()).Start("Spin");
}
function OnClickStop() {
GameManager.SlotData.IsSpin = false;
setIsSpin(false);
}
function* spin(): IterableIterator<any> {
const { player } = gameObj;
const { m: money } = player;
if (money < GameManager.SlotData.NowBet) {
noMoney();
OnClickStop();
return;
}
if (isCountStop) {
if (GameManager.SlotData.CountStop <= 0) {
OnClickStop();
return;
}
GameManager.SlotData.CountStop--;
setCountStop((v) => v - 1);
}
yield* GameManager.SlotData.SlotClass.Spin(bet, OnClickStop);
yield CoroutineV2.WaitTime(delay);
if (GameManager.SlotData.IsSpin) {
yield* spin();
} else {
GameManager.SlotData.IsSpin = false;
setIsSpin(false);
}
}
function noMoney(): void {
const { player } = gameObj;
const { m: money } = player;
GameManager.SlotData.IsSpin = false;
AddLog(`金額不足: ${money}`);
}
function AddLog(s: string) {
const todayTimeStr: string = dayjs().format("YYYY/MM/DD HH:mm:ss");
setLog((v) => {
const next: string[] = [`${todayTimeStr} ${s}`, ...v];
return next.length > 100 ? next.slice(0, 100) : next;
});
}
/**RPC回傳.若協定錯誤斷線.原因也會在這裡收到 */
function onNetDataReceived(resp: INetResponse<any>) {
// MainControl.DataReceivedEvent.DispatchCallback([MainControl.DataType.ServerData, resp]);
}
/**只要連線中斷不管主被動都會走到這裡 */
function onNetDisconnected() {
console.warn("[socket] Disconnected");
conn.OnDataReceived.RemoveAllCallbacks();
// MainControl.DataReceivedEvent.DispatchCallback([MainControl.DataType.NetDisconnected]);
}
useEffect(() => {
CoroutineV2.Single(onLoad()).Start();
return onClose;
}, []);
return (
<div style={layoutStyle} className="lp-rise">
{/* 左側:控制面板 */}
<div className="lp-panel lp-corners" style={controlPanelStyle}>
<div style={{ marginBottom: 18 }}>
<div className="lp-kicker" style={{ marginBottom: 6 }}>SD Machine</div>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<h2 className="lp-title" style={{ fontSize: 24 }}>SLOT <span className="lp-mono" style={{ color: "var(--lp-amber)" }}>#{nowSlotId}</span></h2>
<span className={`lp-dot ${isSpin ? "lp-dot--red" : isOK ? "" : "lp-dot--amber"}`} />
<span className="lp-label">{isSpin ? "spinning" : isOK ? "ready" : "loading"}</span>
</div>
</div>
{isOK && <>
<div style={rowStyle}>
<span className="lp-label" style={rowLabelStyle}>押注</span>
<WheelSelect
wrapperStyle={{ flex: 1 }}
style={{ width: "100%" }}
value={bet}
disabled={isSpin}
options={betOptions.map((v: number) => ({ value: v, label: v.toLocaleString() }))}
onChange={(v: number) => { setBet(v); GameManager.SlotData.NowBet = v; }}
/>
</div>
<div style={rowStyle}>
<span className="lp-label" style={rowLabelStyle}>延遲</span>
<InputNumber
style={{ flex: 1 }}
min={0}
step={0.1}
value={delay}
onChange={(v: number) => setDelay(+v || 0)}
2026-08-03 11:15:03 +08:00
suffix="秒"
/>
</div>
<div style={rowStyle}>
<span className="lp-label" style={rowLabelStyle}>倍率停</span>
<Switch
checked={isRatioStop}
onChange={(checked: boolean) => { setIsRatioStop(checked); GameManager.SlotData.IsRatioStop = checked; }}
/>
{isRatioStop &&
<InputNumber
style={{ flex: 1 }}
min={0}
value={ratioStop}
onChange={(v: number) => { const n = +v || 0; setRatioStop(n); GameManager.SlotData.RatioStop = n; }}
2026-08-03 11:15:03 +08:00
suffix="倍"
/>
}
</div>
<div style={rowStyle}>
<span className="lp-label" style={rowLabelStyle}>轉數停</span>
<Switch
checked={isCountStop}
onChange={(checked: boolean) => { setIsCountStop(checked); GameManager.SlotData.IsCountStop = checked; }}
/>
{isCountStop &&
<InputNumber
style={{ flex: 1 }}
min={0}
value={countStop}
onChange={(v: number) => { const n = +v || 0; setCountStop(n); GameManager.SlotData.CountStop = n; }}
2026-08-03 11:15:03 +08:00
suffix="轉"
/>
}
</div>
<div style={{ marginTop: "auto", paddingTop: 20 }}>
{isSpin
? <button className="lp-spin-btn lp-spin-btn--stop" onClick={OnClickStop}>STOP</button>
: <button className="lp-spin-btn" onClick={OnClickSpin}>SPIN</button>
}
<Button danger block onClick={onClickSlotOut} style={{ marginTop: 12 }}>
離開機台
</Button>
</div>
</>}
{!isOK &&
<div style={{ marginTop: "auto" }}>
<Button danger block onClick={onClickSlotOut}>離開機台</Button>
</div>
}
</div>
{/* 右側:Log 終端 */}
<div style={logPanelStyle}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 8 }}>
<span className="lp-kicker">Log</span>
<span className="lp-label">{log.length} </span>
</div>
<div className="lp-log">
{log.length === 0 && <p className="lp-cursor" style={{ color: "var(--lp-text-dim)" }}>等待紀錄</p>}
{log.map((s: string, index: number) => <p key={index}>{s}</p>)}
</div>
</div>
</div>
);
};
export default SDGame;
interface ISDGame {
gameUrl: string;
onClickSlotOut: () => void;
}
const layoutStyle: React.CSSProperties = {
flex: 1,
minHeight: 0,
display: "flex",
gap: 16,
alignItems: "stretch",
};
const controlPanelStyle: React.CSSProperties = {
width: 340,
flexShrink: 0,
padding: "24px 24px 20px",
display: "flex",
flexDirection: "column",
overflowY: "auto",
};
const rowStyle: React.CSSProperties = {
display: "flex",
alignItems: "center",
gap: 12,
marginBottom: 16,
};
const rowLabelStyle: React.CSSProperties = {
width: 52,
flexShrink: 0,
};
const logPanelStyle: React.CSSProperties = {
flex: 1,
minWidth: 0,
display: "flex",
flexDirection: "column",
};