feat : add fix delay helper and target
This commit is contained in:
@@ -27,7 +27,14 @@ Mavuika E > Xilonen E N2
|
||||
|
||||
Use character names or keys from that team, separated by `>` or newlines. Supported shorthand: `E` (skill), `Q` (burst), `N2` (two normals), `C` (charged attack), `D` (dash), `J` (jump), `tE` / `hE` (tap / hold skill), and `W(60)` (wait 60 frames). Combine actions with `N2CD` or repeat groups with `3(N2CD)`; nested groups are supported. Compact repeats such as `2N2D` mean `2(N2D)`: perform two normals and a dash twice. A following spaced action runs once, so `2N2D Q` repeats `N2D` twice, then uses burst. **Repeat rotation** loops the actions until the simulation ends.
|
||||
|
||||
Both toggles default to off. Rotation text and settings save with each team in this browser; turning rotation off preserves the text and outputs only builds. Invalid enabled rotations show an error and disable copying. The translator checks shorthand, not combat legality or energy requirements. Output contains build blocks and optional actions; add simulation settings and targets in gcsim.
|
||||
Both toggles default to off. Rotation text and settings save with each team in this browser; turning rotation off preserves the text and outputs only builds. Invalid enabled rotations show an error and disable copying. The translator checks shorthand, not combat legality or energy requirements. Enabling rotation automatically includes the fixed action-delay helper above the character builds, then these fixed environment settings below all character stats:
|
||||
|
||||
```text
|
||||
target lvl=100 resist=0.1 radius=2 pos=0,2.4 hp=999999999 freeze_resist=1;
|
||||
energy every interval=480,720 amount=1;
|
||||
```
|
||||
|
||||
The helper and environment settings are not editable in the app. Rotation actions follow these settings. Add simulation options in gcsim.
|
||||
|
||||
## Import from gcsim
|
||||
|
||||
@@ -35,7 +42,7 @@ Click **Import from gcsim**, paste a config, review the preview, and click **Imp
|
||||
|
||||
Imported rotations remain editable gcsim script, retaining loops, conditionals, waits, and action parameters. **Include rotation** controls whether that script is copied. Use **Clear script and use shorthand** to start a new shorthand rotation instead.
|
||||
|
||||
Imports support up to four characters from the current catalog, common character aliases, and Favonius aliases such as `favsword`, `favgs`, `favbow`, `favspear`, and `favcodex`. Unknown catalog entries, malformed build settings, and invalid values show errors before any team is added. Simulation options, targets, and energy settings are omitted with a notice in the import preview; full script validation happens in gcsim.
|
||||
Imports support up to four characters from the current catalog, common character aliases, and Favonius aliases such as `favsword`, `favgs`, `favbow`, `favspear`, and `favcodex`. Unknown catalog entries, malformed build settings, and invalid values show errors before any team is added. Imported simulation options, targets, and energy settings are omitted with a notice in the import preview. Enabled rotations use the fixed defaults, and reimported delay helpers are removed to prevent duplicate definitions. Full script validation happens in gcsim.
|
||||
|
||||
## Production
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ function RotationEditor({ team, onChange }: { team: Team; onChange: (team: Team)
|
||||
return <Card>
|
||||
<CardHeader><CardTitle>Rotation translator</CardTitle><CardDescription>{rotation.format === "gcsim" ? "Imported script with its original loops and action parameters." : "Write your combo using character names and shorthand."}</CardDescription></CardHeader>
|
||||
<CardContent><FieldGroup>
|
||||
<Field><Toggle variant="outline" pressed={rotation.enabled} onPressedChange={(enabled) => update({ enabled })} aria-controls={`${inputId}-controls`}>Include rotation</Toggle><FieldDescription>{rotation.enabled ? "Rotation actions will be included in the copied config." : "Off: only character builds are included."}</FieldDescription></Field>
|
||||
<Field><Toggle variant="outline" pressed={rotation.enabled} onPressedChange={(enabled) => update({ enabled })} aria-controls={`${inputId}-controls`}>Include rotation</Toggle><FieldDescription>{rotation.enabled ? "Includes rotation, the fixed delay helper, target, and energy settings." : "Off: only character builds are included."}</FieldDescription></Field>
|
||||
{rotation.enabled && <FieldGroup id={`${inputId}-controls`}>
|
||||
<Field data-invalid={Boolean(error)}><FieldLabel htmlFor={inputId}>{rotation.format === "gcsim" ? "Imported gcsim rotation" : "Rotation"}</FieldLabel><Textarea id={inputId} value={rotation.text} maxLength={10000} onChange={(event) => update({ text: event.target.value })} placeholder={rotation.format === "gcsim" ? "mavuika skill;" : "Mavuika E > Xilonen E N2"} aria-invalid={Boolean(error)} aria-describedby={`${inputId}-help${error ? ` ${inputId}-error` : ""}`} /><FieldDescription id={`${inputId}-help`}>{rotation.format === "gcsim" ? "Edit the imported gcsim script directly. Brackets are checked here; full script validation happens in gcsim." : <>Use names or keys from this team: {team.builds.map((build) => build.characterName).join(", ")}. Separate character steps with > or a new line.</>}</FieldDescription>{error && <FieldError id={`${inputId}-error`}>{error}</FieldError>}</Field>
|
||||
{rotation.format === "gcsim" ? <Field><Button variant="outline" onClick={() => update({ format: "shorthand", text: "", repeat: false })}>Clear script and use shorthand</Button></Field> : <><Field><FieldDescription>E = skill · Q = burst · N2 = 2 normals · C = charged attack · D = dash · J = jump. tE / hE = tap / hold skill. W(60) = wait 60 frames. Combine actions: N2CD. Repeat a combo: 3(N2CD).</FieldDescription></Field>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Build, MainStatKey, RollKey, Team } from "./types";
|
||||
import { translateRotation } from "./rotation";
|
||||
import { ROTATION_DELAY_HELPER, ROTATION_ENVIRONMENT } from "./rotation-defaults";
|
||||
|
||||
export const SUBSTAT_SCALARS: Record<RollKey, number> = {
|
||||
hp: 253.94, atk: 16.54, def: 19.68, em: 19.82,
|
||||
@@ -48,5 +49,7 @@ export function generateBuild(build: Build): string {
|
||||
export function generateTeam(team: Team): string {
|
||||
const builds = team.builds.map(generateBuild).join("\n\n");
|
||||
const rotation = translateRotation(team);
|
||||
return rotation.code ? `${builds}\n\n# Rotation\n${rotation.code}` : builds;
|
||||
if (!team.rotation?.enabled) return builds;
|
||||
const config = `${ROTATION_DELAY_HELPER}\n\n${builds}\n\n${ROTATION_ENVIRONMENT}`;
|
||||
return rotation.code ? `${config}\n\n# Rotation\n${rotation.code}` : config;
|
||||
}
|
||||
|
||||
@@ -141,6 +141,8 @@ describe("gcsim import", () => {
|
||||
const again = importGcsimConfig(generateTeam(imported), catalog).team!;
|
||||
expect(again.builds).toEqual(imported.builds);
|
||||
expect(again.rotation).toEqual(imported.rotation);
|
||||
expect(again.rotation!.text).not.toContain("fn rand_delay");
|
||||
expect(generateTeam(again).match(/fn execute_action\(/g)).toHaveLength(1);
|
||||
});
|
||||
it("imports app generated configs without adding more artifact stats", () => {
|
||||
const original = createBuild(catalog, catalog.characters[0], "build");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createBuild } from "./build";
|
||||
import { splitGcsimStatements } from "./gcsim-text";
|
||||
import { translateRotation } from "./rotation";
|
||||
import { isRotationDefaultStatement } from "./rotation-defaults";
|
||||
import { canonicalArtifactKey, canonicalCharacterKey, canonicalWeaponKey } from "./keys";
|
||||
import { importedStatKeys, translationSchema, type Build, type Catalog, type CatalogItem, type Team } from "./types";
|
||||
|
||||
@@ -123,9 +124,10 @@ export function importGcsimConfig(input: string, catalog: Catalog, name = "Impor
|
||||
continue;
|
||||
}
|
||||
if (/^(options|target|energy)\b/i.test(statement)) {
|
||||
warnings.add("Simulation options, targets, and energy settings are not imported; add them in gcsim when running the config.");
|
||||
warnings.add("Simulation options, targets, and energy settings are not imported. With rotation enabled, output uses the fixed target and energy defaults.");
|
||||
continue;
|
||||
}
|
||||
if (isRotationDefaultStatement(statement)) continue;
|
||||
script.push(statement);
|
||||
}
|
||||
for (const build of builds.values()) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
export const ROTATION_DELAY_HELPER = `fn rand_delay(mean, stddev) {
|
||||
let del = randnorm() * stddev + mean;
|
||||
if del > (mean + mean) {
|
||||
del = mean + mean;
|
||||
}
|
||||
delay(del);
|
||||
}
|
||||
|
||||
let prev_char_id = -1;
|
||||
let prev_action_id = -1;
|
||||
|
||||
let _execute_action = execute_action;
|
||||
fn execute_action(char_id number, action_id number, p map) {
|
||||
|
||||
if prev_char_id != char_id && action_id != .action.swap {
|
||||
execute_action(char_id, .action.swap, []);
|
||||
}
|
||||
|
||||
if action_id == .action.swap {
|
||||
rand_delay(12, 3);
|
||||
} else if prev_action_id == .action.attack && action_id != .action.attack && action_id != .action.charge {
|
||||
rand_delay(6, 3);
|
||||
} else if prev_action_id != .action.attack {
|
||||
rand_delay(6, 3);
|
||||
}
|
||||
|
||||
prev_char_id = char_id;
|
||||
prev_action_id = action_id;
|
||||
return _execute_action(char_id, action_id, p);
|
||||
}`;
|
||||
|
||||
export const ROTATION_ENVIRONMENT = `target lvl=100 resist=0.1 radius=2 pos=0,2.4 hp=999999999 freeze_resist=1;
|
||||
energy every interval=480,720 amount=1;`;
|
||||
|
||||
// These names belong to the fixed generated helper, including when reimported.
|
||||
export function isRotationDefaultStatement(statement: string): boolean {
|
||||
return /^fn\s+(?:rand_delay|execute_action)\s*\(/.test(statement)
|
||||
|| /^let\s+(?:prev_char_id|prev_action_id|_execute_action)\s*=/.test(statement)
|
||||
|| /^(?:target|energy)\b/i.test(statement);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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 = {
|
||||
@@ -59,8 +60,28 @@ describe("rotation translator", () => {
|
||||
expect(generateTeam(disabled)).toBe(generateTeam(team));
|
||||
});
|
||||
it("appends valid rotation and keeps build preview for invalid input", () => {
|
||||
expect(generateTeam(withRotation("Mavuika E"))).toBe(generateTeam(team) + "\n\n# Rotation\nactive mavuika;\n\nmavuika skill;");
|
||||
expect(generateTeam(withRotation("Mavuika Z"))).toBe(generateTeam(team));
|
||||
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");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Team } from "./types";
|
||||
import { splitGcsimStatements } from "./gcsim-text";
|
||||
import { canonicalCharacterKey } from "./keys";
|
||||
import { isRotationDefaultStatement } from "./rotation-defaults";
|
||||
|
||||
export type RotationResult = { code: string; error?: never } | { code: ""; error: string };
|
||||
const MAX_ACTIONS = 2000;
|
||||
@@ -92,7 +93,8 @@ export function translateRotation(team: Team): RotationResult {
|
||||
if (text.length > 10000) return { code: "", error: "Rotation exceeds 10,000 characters." };
|
||||
if (team.rotation.format === "gcsim") {
|
||||
try {
|
||||
const statements = splitGcsimStatements(text);
|
||||
const statements = splitGcsimStatements(text).filter((statement) => !isRotationDefaultStatement(statement));
|
||||
if (!statements.length) return { code: "", error: "Enter rotation actions or turn Include rotation off." };
|
||||
if (statements.some((statement) => /^active\b/i.test(statement) && !/^active\s+[a-z]+\s*;$/i.test(statement))) return { code: "", error: "Active character must use 'active character;' with a semicolon." };
|
||||
const script = statements.join("\n").replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
|
||||
const references = [...script.matchAll(/\bactive\s+([a-z]+)\s*;|\b([a-z]+)\s+(?:attack|skill|burst|charge|aim|low_plunge|high_plunge|dash|jump|swap|walk)\b/gi)];
|
||||
@@ -101,7 +103,8 @@ export function translateRotation(team: Team): RotationResult {
|
||||
if (!team.builds.some((build) => canonicalCharacterKey(build.characterKey) === canonicalCharacterKey(key))) return { code: "", error: `Rotation character '${key}' is not in this team.` };
|
||||
}
|
||||
const active = references.some((reference) => reference[1]);
|
||||
return { code: active ? text.trim() : `active ${team.builds[0].characterKey};\n\n${text.trim()}` };
|
||||
const body = statements.join("\n");
|
||||
return { code: active ? body : `active ${team.builds[0].characterKey};\n\n${body}` };
|
||||
} catch (error) { return { code: "", error: (error as Error).message }; }
|
||||
}
|
||||
const names = team.builds.flatMap((build) => [build.characterName, build.characterKey].map((name) => ({ name, key: build.characterKey }))).sort((a, b) => b.name.length - a.name.length);
|
||||
|
||||
Reference in New Issue
Block a user