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 map
main
imspace 2022-12-18 14:48:05 +08:00 committed by GitHub
parent 4386a75c99
commit 770458f3b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 124 additions and 21 deletions

View File

@ -1,3 +1,7 @@
## 0.2.6
feat: add tri-color support
## 0.2.5 ## 0.2.5
feat: add crown feat: add crown

View File

@ -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({

View File

@ -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",

View File

@ -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,

View File

@ -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 =

View File

@ -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;