Files
gcsim-translater/lib/translator/rotation.test.ts
T
gunshiz 3fe17a2ff7
CI / verify (push) Successful in 2m10s
CI / build-and-deploy (push) Successful in 2m14s
feat: add traveler rotation shortcut
2026-09-20 00:00:30 +07:00

117 lines
8.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { generateTeam } from "./generate";
import { translateRotation } from "./rotation";
import { ROTATION_DELAY_HELPER, ROTATION_ENVIRONMENT } from "./rotation-defaults";
import { teamSchema, type Team } from "./types";
const build = {
id: "mav", characterId: "mav", characterKey: "mavuika", characterName: "Mavuika", characterImageUrl: "", level: 90, maxLevel: 90, constellation: 0, talents: [9, 9, 9], weaponId: "weapon", weaponKey: "favoniusgreatsword", weaponName: "Weapon", weaponLevel: 90, weaponMaxLevel: 90, refinement: 1, artifactMode: "4pc", artifactOneId: "set", artifactOneKey: "noblesseoblige", artifactOneName: "Set", artifactTwoId: "", artifactTwoKey: "", artifactTwoName: "", sands: "atk%", goblet: "pyro%", circlet: "cr", rolls: { hp: 0, atk: 0, def: 0, em: 0, er: 0, "hp%": 0, "atk%": 0, "def%": 0, cr: 0, cd: 0 },
} satisfies Team["builds"][number];
const team: Team = { id: "team", name: "Team", builds: [build, { ...build, id: "xil", characterId: "xil", characterKey: "xilonen", characterName: "Xilonen" }] };
const withRotation = (text: string, repeat = false): Team => ({ ...team, rotation: { enabled: true, text, repeat } });
describe("rotation translator", () => {
it("translates the requested example", () => {
expect(translateRotation(withRotation("Mavuika E > Xilonen E N2"))).toEqual({ code: "active mavuika;\n\nmavuika skill;\nxilonen skill, attack:2;" });
});
it("preserves combined actions and repeated group order", () => {
expect(translateRotation(withRotation("mavuika Q 3(N2CD) J")).code).toContain("mavuika burst, attack:2, charge, dash, attack:2, charge, dash, attack:2, charge, dash, jump;");
});
it("accepts Skirk's compact repeat in the fourth rotation step", () => {
const skirk = { ...build, id: "skirk", characterId: "skirk", characterKey: "skirk", characterName: "Skirk" };
const rotation = { ...withRotation("Mavuika E > Xilonen E N2 > Mavuika Q > Skirk E 2N2D Q"), builds: [...team.builds, skirk] };
expect(translateRotation(rotation).error).toBeUndefined();
expect(translateRotation(rotation).code).toContain("skirk skill, attack:2, dash, attack:2, dash, burst;");
});
it.each([
["2N2D", "2(N2D)"], ["3N2CD Q", "3(N2CD) Q"],
["2n2d", "2(N2D)"], ["2hE J", "2(hE) J"],
["2(3N1CJ D)", "2(3(N1CJ) D)"],
])("interprets compact combo %s as %s", (compact, grouped) => {
expect(translateRotation(withRotation(`Mavuika ${compact}`))).toEqual(translateRotation(withRotation(`Mavuika ${grouped}`)));
});
it.each(["0N2D", "2000N2D", "2N0D", "2.5N2D", "2N2DX", "2N2001D"])("rejects invalid compact combo %s", (combo) => {
expect(translateRotation(withRotation(`Mavuika ${combo}`)).error).toBeTruthy();
});
it("supports tap/hold, waits, case insensitive keys and newlines", () => {
expect(translateRotation(withRotation("MAVUIKA tE W(60) hE\nxilonen n" )).code).toBe("active mavuika;\n\nmavuika skill[hold=0];\nwait(60);\nmavuika skill[hold=1];\nxilonen attack;");
});
it("supports nested repetitions and loops", () => {
expect(translateRotation(withRotation("Mavuika 2(2(N)D)", true)).code).toBe("active mavuika;\n\nwhile 1 {\n mavuika attack, attack, dash, attack, attack, dash;\n}");
});
it("matches multiword names before shorter names", () => {
const named: Team = { ...withRotation("Kaedehara Kazuha E"), builds: [{ ...build, characterKey: "kazuha", characterName: "Kaedehara Kazuha" }] };
expect(translateRotation(named).code).toContain("kazuha skill;");
});
it.each([
["traveler_anemo", "aetheranemo", "mc"],
["traveler_dendro", "aetherdendro", "MC"],
["traveler_cryo", "aethercryo", "Mc"],
])("resolves %s from the %s shortcut", (characterId, characterKey, shortcut) => {
const traveler = { ...build, id: characterId, characterId, characterKey, characterName: `${characterKey.slice(6)} Traveler` };
const result = translateRotation({ ...withRotation(`${shortcut} E`), builds: [traveler] });
expect(result).toEqual({ code: `active ${characterKey};\n\n${characterKey} skill;` });
});
it("only accepts mc when exactly one Traveler is in the shorthand team", () => {
expect(translateRotation(withRotation("mc E")).error).toContain("Unknown character");
const anemo = { ...build, id: "anemo", characterId: "traveler_anemo", characterKey: "aetheranemo", characterName: "Anemo Traveler" };
const dendro = { ...build, id: "dendro", characterId: "traveler_dendro", characterKey: "aetherdendro", characterName: "Dendro Traveler" };
expect(translateRotation({ ...withRotation("mc E"), builds: [anemo, dendro] }).error).toContain("ambiguous");
});
it("does not apply the mc shortcut to imported gcsim rotations", () => {
const traveler = { ...build, characterId: "traveler_dendro", characterKey: "aetherdendro", characterName: "Dendro Traveler" };
const imported: Team = { ...withRotation("mc skill;"), builds: [traveler], rotation: { enabled: true, text: "mc skill;", repeat: false, format: "gcsim" } };
expect(translateRotation(imported).error).toContain("not in this team");
});
it.each(["", "Mavuika", "Mavuika N0", "Mavuika 0(N)", "Mavuika 2()", "Mavuika 2(N", "Mavuika N)", "Mavuika W(0)", "Mavuika W(-1)", "Mavuika Z", "Mavuika E >", "Nahida E", "MavuikaExtra E", "Mavuika 2000(N2)", "Mavuika N2000 > Xilonen E"])("rejects invalid rotation %s", (text) => {
expect(translateRotation(withRotation(text)).error).toBeTruthy();
expect(translateRotation(withRotation(text)).code).toBe("");
});
it("enforces text and nesting limits", () => {
expect(translateRotation(withRotation("Mavuika " + "N ".repeat(5000))).error).toContain("10,000");
expect(translateRotation(withRotation("Mavuika " + "1(".repeat(40) + "N" + ")".repeat(40))).error).toContain("nested");
});
it("accepts exactly 2000 expanded actions", () => {
expect(translateRotation(withRotation("Mavuika N2000")).error).toBeUndefined();
});
it("excludes disabled rotation without validating its text", () => {
const disabled: Team = { ...withRotation("invalid"), rotation: { enabled: false, text: "invalid", repeat: true } };
expect(translateRotation(disabled)).toEqual({ code: "" });
expect(generateTeam(disabled)).toBe(generateTeam(team));
});
it("appends valid rotation and keeps build preview for invalid input", () => {
expect(generateTeam(withRotation("Mavuika E"))).toBe(ROTATION_DELAY_HELPER + "\n\n" + generateTeam(team) + "\n\n" + ROTATION_ENVIRONMENT + "\n\n# Rotation\nactive mavuika;\n\nmavuika skill;");
expect(generateTeam(withRotation("Mavuika Z"))).toBe(ROTATION_DELAY_HELPER + "\n\n" + generateTeam(team) + "\n\n" + ROTATION_ENVIRONMENT);
});
it("shows the fixed defaults immediately when enabled before entering actions", () => {
expect(generateTeam(withRotation(""))).toBe(ROTATION_DELAY_HELPER + "\n\n" + generateTeam(team) + "\n\n" + ROTATION_ENVIRONMENT);
expect(translateRotation(withRotation("")).error).toBeTruthy();
});
it("excludes the fixed helper, target, and energy with rotation disabled", () => {
const output = generateTeam(team);
expect(output).not.toContain("fn rand_delay");
expect(output).not.toContain("target lvl=");
expect(output).not.toContain("energy every");
});
it("keeps fixed defaults out of imported script and prevents duplicate overrides", () => {
const raw: Team = { ...team, rotation: { enabled: true, format: "gcsim", repeat: false, text: ROTATION_DELAY_HELPER.replace("rand_delay(12, 3)", "rand_delay(99, 99)") + "\n" + ROTATION_ENVIRONMENT + "\nmavuika skill;" } };
const output = generateTeam(raw);
expect(output.startsWith(ROTATION_DELAY_HELPER)).toBe(true);
expect(output.match(/fn rand_delay\(/g)).toHaveLength(1);
expect(output.match(/fn execute_action\(/g)).toHaveLength(1);
expect(output.match(/target lvl=/g)).toHaveLength(1);
expect(output.match(/energy every/g)).toHaveLength(1);
expect(output).not.toContain("rand_delay(99, 99)");
});
it("rejects removed characters", () => {
expect(translateRotation({ ...withRotation("Xilonen E"), builds: [build] }).error).toContain("Unknown character");
});
it("loads old drafts and round trips independent settings", () => {
expect(teamSchema.parse(team).rotation).toBeUndefined();
const saved = teamSchema.parse(JSON.parse(JSON.stringify(withRotation("Mavuika E", true))));
expect(saved.rotation).toEqual({ enabled: true, text: "Mavuika E", repeat: true });
expect(teamSchema.parse({ ...team, rotation: {} }).rotation).toEqual({ enabled: false, text: "", repeat: false });
expect(team.rotation).toBeUndefined();
});
});