feat: add tri-color support (#57)
* feat: add tri-color support * feat: add signal * feat: add tri-color filter to export script * fix: tri-color mapmain
parent
4386a75c99
commit
770458f3b6
|
|
@ -1,3 +1,7 @@
|
||||||
|
## 0.2.6
|
||||||
|
|
||||||
|
feat: add tri-color support
|
||||||
|
|
||||||
## 0.2.5
|
## 0.2.5
|
||||||
|
|
||||||
feat: add crown
|
feat: add crown
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,15 @@ import { Game } from "../src/types.ts";
|
||||||
import { parseHistoryDetailId } from "../src/utils.ts";
|
import { parseHistoryDetailId } from "../src/utils.ts";
|
||||||
|
|
||||||
async function exportType(
|
async function exportType(
|
||||||
{ statInkExporter, fileExporter, type, gameFetcher }: {
|
{ statInkExporter, fileExporter, type, gameFetcher, filter }: {
|
||||||
statInkExporter: StatInkExporter;
|
statInkExporter: StatInkExporter;
|
||||||
fileExporter: FileExporter;
|
fileExporter: FileExporter;
|
||||||
gameFetcher: GameFetcher;
|
gameFetcher: GameFetcher;
|
||||||
type: Game["type"];
|
type: Game["type"];
|
||||||
|
filter?: (game: Game) => boolean;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const gameList = await fileExporter.exportedGames({ uid, type });
|
const gameList = await fileExporter.exportedGames({ uid, type, filter });
|
||||||
|
|
||||||
const workQueue = [
|
const workQueue = [
|
||||||
...await statInkExporter.notExported({
|
...await statInkExporter.notExported({
|
||||||
|
|
@ -32,7 +33,10 @@ async function exportType(
|
||||||
]
|
]
|
||||||
.reverse().map((id) => gameList.find((i) => i.id === id)!);
|
.reverse().map((id) => gameList.find((i) => i.id === id)!);
|
||||||
|
|
||||||
console.log(`Exporting ${workQueue.length} ${type} games`);
|
console.log(
|
||||||
|
`Exporting ${workQueue.length} ${type} games` +
|
||||||
|
(filter ? " (filtered)" : ""),
|
||||||
|
);
|
||||||
|
|
||||||
let exported = 0;
|
let exported = 0;
|
||||||
for (const { getContent } of workQueue) {
|
for (const { getContent } of workQueue) {
|
||||||
|
|
@ -79,7 +83,7 @@ if (opts.help) {
|
||||||
`Usage: deno run -A ${Deno.mainModule} [options]
|
`Usage: deno run -A ${Deno.mainModule} [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--type Type of game to export. Can be vs, coop, or all. (default: coop)
|
--type Type of game to export. Can be vs, tri-color, coop, or all. (default: coop)
|
||||||
--profile-path <path>, -p Path to config file (default: ./profile.json)
|
--profile-path <path>, -p Path to config file (default: ./profile.json)
|
||||||
--help Show this help message and exit`,
|
--help Show this help message and exit`,
|
||||||
);
|
);
|
||||||
|
|
@ -130,7 +134,24 @@ const statInkExporter = new StatInkExporter({
|
||||||
uploadMode: "Manual",
|
uploadMode: "Manual",
|
||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
const type = (opts.type ?? "coop").replace("all", "vs,coop");
|
const type = (opts.type ?? "coop").replace("all", "vs,coop,tri-color");
|
||||||
|
|
||||||
|
if (type.includes("tri-color")) {
|
||||||
|
[
|
||||||
|
await exportType({
|
||||||
|
type: "VsInfo",
|
||||||
|
fileExporter,
|
||||||
|
statInkExporter,
|
||||||
|
gameFetcher,
|
||||||
|
filter: (game) => {
|
||||||
|
if (game.type === "CoopInfo") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return game.detail.vsRule.rule === "TRI_COLOR";
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if (type.includes("vs")) {
|
if (type.includes("vs")) {
|
||||||
await exportType({
|
await exportType({
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
|
import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
|
||||||
|
|
||||||
export const AGENT_NAME = "s3si.ts";
|
export const AGENT_NAME = "s3si.ts";
|
||||||
export const S3SI_VERSION = "0.2.5";
|
export const S3SI_VERSION = "0.2.6";
|
||||||
export const NSOAPP_VERSION = "2.4.0";
|
export const NSOAPP_VERSION = "2.4.0";
|
||||||
export const WEB_VIEW_VERSION = "2.0.0-bd36a652";
|
export const WEB_VIEW_VERSION = "2.0.0-bd36a652";
|
||||||
export const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts";
|
export const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts";
|
||||||
|
|
@ -46,8 +46,7 @@ export const SPLATNET3_STATINK_MAP: {
|
||||||
LOFT: "yagura",
|
LOFT: "yagura",
|
||||||
GOAL: "hoko",
|
GOAL: "hoko",
|
||||||
CLAM: "asari",
|
CLAM: "asari",
|
||||||
// TODO: support tri-color
|
TRI_COLOR: "tricolor",
|
||||||
TRI_COLOR: "nawabari",
|
|
||||||
},
|
},
|
||||||
RESULT: {
|
RESULT: {
|
||||||
WIN: "win",
|
WIN: "win",
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,11 @@ export class FileExporter implements GameExporter {
|
||||||
* Get all exported files
|
* Get all exported files
|
||||||
*/
|
*/
|
||||||
async exportedGames(
|
async exportedGames(
|
||||||
{ uid, type }: { uid: string; type: Game["type"] },
|
{ uid, type, filter }: {
|
||||||
|
uid: string;
|
||||||
|
type: Game["type"];
|
||||||
|
filter?: (game: Game) => boolean;
|
||||||
|
},
|
||||||
): Promise<{ id: string; getContent: () => Promise<Game> }[]> {
|
): Promise<{ id: string; getContent: () => Promise<Game> }[]> {
|
||||||
const out: { id: string; filepath: string; timestamp: string }[] = [];
|
const out: { id: string; filepath: string; timestamp: string }[] = [];
|
||||||
|
|
||||||
|
|
@ -78,12 +82,18 @@ export class FileExporter implements GameExporter {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (body.type === "VS" && type === "VsInfo") {
|
if (body.type === "VS" && type === "VsInfo") {
|
||||||
|
if (filter && !filter(body.data)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
out.push({
|
out.push({
|
||||||
id: body.data.detail.id,
|
id: body.data.detail.id,
|
||||||
filepath,
|
filepath,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
} else if (body.type === "COOP" && type === "CoopInfo") {
|
} else if (body.type === "COOP" && type === "CoopInfo") {
|
||||||
|
if (filter && !filter(body.data)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
out.push({
|
out.push({
|
||||||
id: body.data.detail.id,
|
id: body.data.detail.id,
|
||||||
filepath,
|
filepath,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
USERAGENT,
|
USERAGENT,
|
||||||
} from "../constant.ts";
|
} from "../constant.ts";
|
||||||
import {
|
import {
|
||||||
|
Color,
|
||||||
CoopHistoryDetail,
|
CoopHistoryDetail,
|
||||||
CoopHistoryPlayerResult,
|
CoopHistoryPlayerResult,
|
||||||
CoopInfo,
|
CoopInfo,
|
||||||
|
|
@ -261,14 +262,6 @@ export class StatInkExporter implements GameExporter {
|
||||||
return vsMode.mode === "FEST" && b64Number(vsMode.id) === 8;
|
return vsMode.mode === "FEST" && b64Number(vsMode.id) === 8;
|
||||||
}
|
}
|
||||||
async exportGame(game: Game): Promise<ExportResult> {
|
async exportGame(game: Game): Promise<ExportResult> {
|
||||||
if (game.type === "VsInfo" && this.isTriColor(game.detail)) {
|
|
||||||
// TODO: support tri-color fest
|
|
||||||
return {
|
|
||||||
status: "skip",
|
|
||||||
reason: "Tri-color fest is not supported",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game.type === "VsInfo") {
|
if (game.type === "VsInfo") {
|
||||||
const body = await this.mapBattle(game);
|
const body = await this.mapBattle(game);
|
||||||
const { url } = await this.api.postBattle(body);
|
const { url } = await this.api.postBattle(body);
|
||||||
|
|
@ -333,7 +326,7 @@ export class StatInkExporter implements GameExporter {
|
||||||
} else if (modeId === 7) {
|
} else if (modeId === 7) {
|
||||||
return "splatfest_challenge";
|
return "splatfest_challenge";
|
||||||
} else if (modeId === 8) {
|
} else if (modeId === 8) {
|
||||||
throw new Error("Tri-color battle is not supported");
|
return "splatfest_open";
|
||||||
}
|
}
|
||||||
} else if (vsMode === "X_MATCH") {
|
} else if (vsMode === "X_MATCH") {
|
||||||
return "xmatch";
|
return "xmatch";
|
||||||
|
|
@ -406,6 +399,7 @@ export class StatInkExporter implements GameExporter {
|
||||||
result.assist = player.result.assist;
|
result.assist = player.result.assist;
|
||||||
result.kill = result.kill_or_assist - result.assist;
|
result.kill = result.kill_or_assist - result.assist;
|
||||||
result.death = player.result.death;
|
result.death = player.result.death;
|
||||||
|
result.signal = player.result.noroshiTry ?? undefined;
|
||||||
result.special = player.result.special;
|
result.special = player.result.special;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -437,6 +431,10 @@ export class StatInkExporter implements GameExporter {
|
||||||
}
|
}
|
||||||
const startedAt = Math.floor(new Date(playedTime).getTime() / 1000);
|
const startedAt = Math.floor(new Date(playedTime).getTime() / 1000);
|
||||||
|
|
||||||
|
if (otherTeams.length === 0) {
|
||||||
|
throw new Error(`Other teams is empty`);
|
||||||
|
}
|
||||||
|
|
||||||
const result: StatInkPostBody = {
|
const result: StatInkPostBody = {
|
||||||
uuid: await gameId(vsDetail.id),
|
uuid: await gameId(vsDetail.id),
|
||||||
lobby: this.mapLobby(vsDetail),
|
lobby: this.mapLobby(vsDetail),
|
||||||
|
|
@ -452,7 +450,7 @@ export class StatInkExporter implements GameExporter {
|
||||||
|
|
||||||
our_team_players: await Promise.all(myTeam.players.map(this.mapPlayer)),
|
our_team_players: await Promise.all(myTeam.players.map(this.mapPlayer)),
|
||||||
their_team_players: await Promise.all(
|
their_team_players: await Promise.all(
|
||||||
otherTeams.flatMap((i) => i.players).map(
|
otherTeams[0].players.map(
|
||||||
this.mapPlayer,
|
this.mapPlayer,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -472,16 +470,23 @@ export class StatInkExporter implements GameExporter {
|
||||||
result.assist = self.result.assist;
|
result.assist = self.result.assist;
|
||||||
result.kill = result.kill_or_assist - result.assist;
|
result.kill = result.kill_or_assist - result.assist;
|
||||||
result.death = self.result.death;
|
result.death = self.result.death;
|
||||||
|
result.signal = self.result.noroshiTry ?? undefined;
|
||||||
result.special = self.result.special;
|
result.special = self.result.special;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.our_team_color = this.mapColor(myTeam.color);
|
||||||
|
result.their_team_color = this.mapColor(otherTeams[0].color);
|
||||||
|
if (otherTeams.length === 2) {
|
||||||
|
result.third_team_color = this.mapColor(otherTeams[1].color);
|
||||||
|
}
|
||||||
|
|
||||||
if (festMatch) {
|
if (festMatch) {
|
||||||
result.fest_dragon =
|
result.fest_dragon =
|
||||||
SPLATNET3_STATINK_MAP.DRAGON[festMatch.dragonMatchType];
|
SPLATNET3_STATINK_MAP.DRAGON[festMatch.dragonMatchType];
|
||||||
result.clout_change = festMatch.contribution;
|
result.clout_change = festMatch.contribution;
|
||||||
result.fest_power = festMatch.myFestPower ?? undefined;
|
result.fest_power = festMatch.myFestPower ?? undefined;
|
||||||
}
|
}
|
||||||
if (rule === "TURF_WAR") {
|
if (rule === "TURF_WAR" || rule === "TRI_COLOR") {
|
||||||
result.our_team_percent = (myTeam?.result?.paintRatio ?? 0) * 100;
|
result.our_team_percent = (myTeam?.result?.paintRatio ?? 0) * 100;
|
||||||
result.their_team_percent = (otherTeams?.[0]?.result?.paintRatio ?? 0) *
|
result.their_team_percent = (otherTeams?.[0]?.result?.paintRatio ?? 0) *
|
||||||
100;
|
100;
|
||||||
|
|
@ -493,6 +498,39 @@ export class StatInkExporter implements GameExporter {
|
||||||
(acc, i) => acc + i.paint,
|
(acc, i) => acc + i.paint,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (myTeam.tricolorRole && myTeam.festTeamName) {
|
||||||
|
result.our_team_role = myTeam.tricolorRole === "DEFENSE"
|
||||||
|
? "defender"
|
||||||
|
: "attacker";
|
||||||
|
result.our_team_theme = myTeam.festTeamName;
|
||||||
|
}
|
||||||
|
if (otherTeams[0].tricolorRole && otherTeams[0].festTeamName) {
|
||||||
|
result.their_team_role = otherTeams[0].tricolorRole === "DEFENSE"
|
||||||
|
? "defender"
|
||||||
|
: "attacker";
|
||||||
|
result.their_team_theme = otherTeams[0].festTeamName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherTeams.length === 2) {
|
||||||
|
result.third_team_players = await Promise.all(
|
||||||
|
otherTeams[0].players.map(
|
||||||
|
this.mapPlayer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
result.third_team_percent = (otherTeams[1]?.result?.paintRatio ?? 0) *
|
||||||
|
100;
|
||||||
|
result.third_team_inked = otherTeams[1].players.reduce(
|
||||||
|
(acc, i) => acc + i.paint,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
if (otherTeams[1].tricolorRole && otherTeams[1].festTeamName) {
|
||||||
|
result.third_team_role = otherTeams[1].tricolorRole === "DEFENSE"
|
||||||
|
? "defender"
|
||||||
|
: "attacker";
|
||||||
|
result.third_team_theme = otherTeams[1].festTeamName;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (knockout) {
|
if (knockout) {
|
||||||
result.knockout = knockout === "NEITHER" ? "no" : "yes";
|
result.knockout = knockout === "NEITHER" ? "no" : "yes";
|
||||||
|
|
@ -569,6 +607,13 @@ export class StatInkExporter implements GameExporter {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
mapColor(color: Color): string | undefined {
|
||||||
|
const float2hex = (i: number) =>
|
||||||
|
Math.round(i * 255).toString(16).padStart(2, "0");
|
||||||
|
// rgba
|
||||||
|
const nums = [color.r, color.g, color.b, color.a];
|
||||||
|
return nums.map(float2hex).join("");
|
||||||
|
}
|
||||||
isRandom(image: Image | null): boolean {
|
isRandom(image: Image | null): boolean {
|
||||||
// question mark
|
// question mark
|
||||||
const RANDOM_FILENAME =
|
const RANDOM_FILENAME =
|
||||||
|
|
|
||||||
26
src/types.ts
26
src/types.ts
|
|
@ -154,6 +154,7 @@ export type VsPlayer = {
|
||||||
death: number;
|
death: number;
|
||||||
assist: number;
|
assist: number;
|
||||||
special: number;
|
special: number;
|
||||||
|
noroshiTry: null | number;
|
||||||
} | null;
|
} | null;
|
||||||
paint: number;
|
paint: number;
|
||||||
crown: boolean;
|
crown: boolean;
|
||||||
|
|
@ -162,8 +163,17 @@ export type VsPlayer = {
|
||||||
clothingGear: PlayerGear;
|
clothingGear: PlayerGear;
|
||||||
shoesGear: PlayerGear;
|
shoesGear: PlayerGear;
|
||||||
};
|
};
|
||||||
|
export type Color = {
|
||||||
|
a: number;
|
||||||
|
b: number;
|
||||||
|
g: number;
|
||||||
|
r: number;
|
||||||
|
};
|
||||||
export type VsTeam = {
|
export type VsTeam = {
|
||||||
players: VsPlayer[];
|
players: VsPlayer[];
|
||||||
|
color: Color;
|
||||||
|
tricolorRole: null | "DEFENSE" | "ATTACK1" | "ATTACK2";
|
||||||
|
festTeamName: null | string;
|
||||||
result: null | {
|
result: null | {
|
||||||
paintRatio: null | number;
|
paintRatio: null | number;
|
||||||
score: null | number;
|
score: null | number;
|
||||||
|
|
@ -637,6 +647,7 @@ export type StatInkPlayer = {
|
||||||
assist?: number;
|
assist?: number;
|
||||||
kill_or_assist?: number;
|
kill_or_assist?: number;
|
||||||
death?: number;
|
death?: number;
|
||||||
|
signal?: number;
|
||||||
special?: number;
|
special?: number;
|
||||||
gears?: StatInkGears;
|
gears?: StatInkGears;
|
||||||
crown?: "yes" | "no";
|
crown?: "yes" | "no";
|
||||||
|
|
@ -742,7 +753,7 @@ export type StatInkPostBody = {
|
||||||
| "splatfest_challenge"
|
| "splatfest_challenge"
|
||||||
| "splatfest_open"
|
| "splatfest_open"
|
||||||
| "private";
|
| "private";
|
||||||
rule: "nawabari" | "area" | "hoko" | "yagura" | "asari";
|
rule: "nawabari" | "area" | "hoko" | "yagura" | "asari" | "tricolor";
|
||||||
stage: string;
|
stage: string;
|
||||||
weapon: string;
|
weapon: string;
|
||||||
result: "win" | "lose" | "draw" | "exempted_lose";
|
result: "win" | "lose" | "draw" | "exempted_lose";
|
||||||
|
|
@ -752,15 +763,27 @@ export type StatInkPostBody = {
|
||||||
assist?: number;
|
assist?: number;
|
||||||
kill_or_assist?: number; // equals to kill + assist if you know them
|
kill_or_assist?: number; // equals to kill + assist if you know them
|
||||||
death?: number;
|
death?: number;
|
||||||
|
signal?: number;
|
||||||
special?: number; // use count
|
special?: number; // use count
|
||||||
inked: number; // not including bonus
|
inked: number; // not including bonus
|
||||||
medals: string[]; // 0-3 elements
|
medals: string[]; // 0-3 elements
|
||||||
our_team_inked?: number; // TW, not including bonus
|
our_team_inked?: number; // TW, not including bonus
|
||||||
their_team_inked?: number; // TW, not including bonus
|
their_team_inked?: number; // TW, not including bonus
|
||||||
|
third_team_inked?: number; // Tricolor Turf War
|
||||||
our_team_percent?: number; // TW
|
our_team_percent?: number; // TW
|
||||||
their_team_percent?: number; // TW
|
their_team_percent?: number; // TW
|
||||||
|
third_team_percent?: number; // Tricolor Turf War
|
||||||
our_team_count?: number; // Anarchy
|
our_team_count?: number; // Anarchy
|
||||||
their_team_count?: number; // Anarchy
|
their_team_count?: number; // Anarchy
|
||||||
|
our_team_color?: string;
|
||||||
|
their_team_color?: string;
|
||||||
|
third_team_color?: string;
|
||||||
|
our_team_role?: "attacker" | "defender";
|
||||||
|
their_team_role?: "attacker" | "defender";
|
||||||
|
third_team_role?: "attacker" | "defender";
|
||||||
|
our_team_theme?: string;
|
||||||
|
their_team_theme?: string;
|
||||||
|
third_team_theme?: string;
|
||||||
level_before?: number;
|
level_before?: number;
|
||||||
level_after?: number;
|
level_after?: number;
|
||||||
rank_before?: string; // one of c- ... s+, lowercase only /^[abcs][+-]?$/ except s-
|
rank_before?: string; // one of c- ... s+, lowercase only /^[abcs][+-]?$/ except s-
|
||||||
|
|
@ -790,6 +813,7 @@ export type StatInkPostBody = {
|
||||||
cash_after?: number;
|
cash_after?: number;
|
||||||
our_team_players: StatInkPlayer[];
|
our_team_players: StatInkPlayer[];
|
||||||
their_team_players: StatInkPlayer[];
|
their_team_players: StatInkPlayer[];
|
||||||
|
third_team_players?: StatInkPlayer[]; // Tricolor Turf War
|
||||||
|
|
||||||
agent: string;
|
agent: string;
|
||||||
agent_version: string;
|
agent_version: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue