diff --git a/dev_deps.ts b/dev_deps.ts new file mode 100644 index 0000000..d68d3be --- /dev/null +++ b/dev_deps.ts @@ -0,0 +1 @@ +export { assertEquals } from "https://deno.land/std@0.160.0/testing/asserts.ts"; diff --git a/initRank.ts b/initRank.ts new file mode 100644 index 0000000..daf315a --- /dev/null +++ b/initRank.ts @@ -0,0 +1,112 @@ +/** + * If rankState in profile.json is not defined, it will be initialized. + */ +import { flags } from "./deps.ts"; +import { getBulletToken, getGToken } from "./src/iksm.ts"; +import { checkToken, getBattleDetail, getBattleList } from "./src/splatnet3.ts"; +import { gameId, readline } from "./src/utils.ts"; +import { FileStateBackend } from "./src/state.ts"; +import { BattleListType } from "./src/types.ts"; +import { RANK_PARAMS } from "./src/RankTracker.ts"; + +const parseArgs = (args: string[]) => { + const parsed = flags.parse(args, { + string: ["profilePath"], + alias: { + "help": "h", + "profilePath": ["p", "profile-path"], + }, + }); + return parsed; +}; + +const opts = parseArgs(Deno.args); +if (opts.help) { + console.log( + `Usage: deno run -A ${Deno.mainModule} [options] + + Options: + --profile-path , -p Path to config file (default: ./profile.json) + --help Show this help message and exit`, + ); + Deno.exit(0); +} + +const stateBackend = new FileStateBackend(opts.profilePath ?? "./profile.json"); +let state = await stateBackend.read(); + +if (state.rankState) { + console.log("rankState is already initialized."); + Deno.exit(0); +} + +if (!await checkToken(state)) { + const sessionToken = state.loginState?.sessionToken; + + if (!sessionToken) { + throw new Error("Session token is not set."); + } + + const { webServiceToken, userCountry, userLang } = await getGToken({ + fApi: state.fGen, + sessionToken, + }); + + const bulletToken = await getBulletToken({ + webServiceToken, + userLang, + userCountry, + appUserAgent: state.appUserAgent, + }); + + state = { + ...state, + loginState: { + ...state.loginState, + gToken: webServiceToken, + bulletToken, + }, + userLang: state.userLang ?? userLang, + userCountry: state.userCountry ?? userCountry, + }; + await stateBackend.write(state); +} + +const battleList = await getBattleList(state, BattleListType.Bankara); +if (battleList.length === 0) { + console.log("No anarchy battle found. Did you play anarchy?"); + Deno.exit(0); +} +const { vsHistoryDetail: detail } = await getBattleDetail(state, battleList[0]); + +console.log( + `Your latest battle is played at ${ + new Date(detail.playedTime).toLocaleString() + }. Please enter your rank after this battle(format: RANK,POINT. S+0,300):`, +); + +while (true) { + const userInput = await readline(); + const [rank, point] = userInput.split(","); + const pointNumber = parseInt(point); + + if (!RANK_PARAMS.find((i) => i.rank === rank)) { + console.log("Invalid rank. Please enter again:"); + } else if (isNaN(pointNumber)) { + console.log("Invalid point. Please enter again:"); + } else { + state = { + ...state, + rankState: { + gameId: await gameId(detail.id), + rank, + rankPoint: pointNumber, + }, + }; + + break; + } +} + +await stateBackend.write(state); +console.log("rankState is initialized."); diff --git a/src/RankTracker.test.ts b/src/RankTracker.test.ts new file mode 100644 index 0000000..4d99fa0 --- /dev/null +++ b/src/RankTracker.test.ts @@ -0,0 +1,7 @@ +import { RankTracker } from "./RankTracker.ts"; +import { assertEquals } from "../dev_deps.ts"; + +Deno.test("RankTracker", () => { + const tracker = new RankTracker(); + assertEquals(tracker, new RankTracker()); +}); diff --git a/src/RankTracker.ts b/src/RankTracker.ts new file mode 100644 index 0000000..e2b14c0 --- /dev/null +++ b/src/RankTracker.ts @@ -0,0 +1,113 @@ +import { RankState } from "./state.ts"; +import { GameFetcher } from "./GameFetcher.ts"; + +type RankParam = { + rank: string; + pointRange: [number, number]; + entrance: number; + openWin: number; + openLose: number; + rankUp?: boolean; +}; + +const splusParams = () => { + const out: RankParam[] = []; + + for (let i = 0; i < 50; i++) { + const level = i % 10; + const item: RankParam = { + rank: `S+${i}`, + pointRange: [300 + level * 350, 300 + (level + 1) * 350], + entrance: 160, + openWin: 8, + openLose: 5, + }; + if (level === 9) { + item.rankUp = true; + } + out.push(item); + } + + out.push({ + rank: "S+50", + pointRange: [0, 9999], + entrance: 160, + openWin: 8, + openLose: 5, + }); + + return out; +}; + +export const RANK_PARAMS: RankParam[] = [{ + rank: "C-", + pointRange: [0, 200], + entrance: 0, + openWin: 8, + openLose: 1, +}, { + rank: "C", + pointRange: [200, 400], + entrance: 20, + openWin: 8, + openLose: 1, +}, { + rank: "C+", + pointRange: [400, 600], + entrance: 40, + openWin: 8, + openLose: 1, + rankUp: true, +}, { + rank: "B-", + pointRange: [100, 350], + entrance: 55, + openWin: 8, + openLose: 2, +}, { + rank: "B", + pointRange: [350, 600], + entrance: 70, + openWin: 8, + openLose: 2, +}, { + rank: "B+", + pointRange: [600, 850], + entrance: 85, + openWin: 8, + openLose: 2, + rankUp: true, +}, { + rank: "A-", + pointRange: [200, 500], + entrance: 100, + openWin: 8, + openLose: 3, +}, { + rank: "A", + pointRange: [500, 800], + entrance: 110, + openWin: 8, + openLose: 3, +}, { + rank: "A+", + pointRange: [800, 1100], + entrance: 120, + openWin: 8, + openLose: 3, + rankUp: true, +}, { + rank: "S", + pointRange: [300, 1000], + entrance: 150, + openWin: 8, + openLose: 4, + rankUp: true, +}, ...splusParams()]; + +/** + * if state is empty, it will not track rank. + */ +export class RankTracker { + constructor(private state?: RankState) {} +} diff --git a/src/state.ts b/src/state.ts index fd611de..b03794a 100644 --- a/src/state.ts +++ b/src/state.ts @@ -3,6 +3,15 @@ export type LoginState = { gToken?: string; bulletToken?: string; }; +export type RankState = { + // generated by gameId(battle.id) + // If the gameId does not exist, the tracker will assume that the user has + // not played a bankara match. And it will start tracking from the first match + gameId?: string; + // C-, B, A+, S, S+0, S+12 + rank: string; + rankPoint: number; +}; export type State = { loginState?: LoginState; fGen: string; @@ -10,6 +19,8 @@ export type State = { userLang?: string; userCountry?: string; + rankState?: RankState; + cacheDir: string; // Exporter config diff --git a/src/utils.ts b/src/utils.ts index cb4bf72..de5619d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -19,9 +19,11 @@ export function urlBase64Decode(data: string) { ); } -export async function readline() { +export async function readline( + { skipEmpty = true }: { skipEmpty?: boolean } = {}, +) { for await (const line of stdinLines) { - if (line !== "") { + if (!skipEmpty || line !== "") { return line; } }