@@ -133,8 +132,7 @@ const Settings = () => {
Full WordShorthand
- */}
-
+
{/* APP OPTIONS */}
App Settings
diff --git a/src/js/utils/MoveFormatter.ts b/src/js/utils/MoveFormatter.ts
new file mode 100644
index 0000000..83841f4
--- /dev/null
+++ b/src/js/utils/MoveFormatter.ts
@@ -0,0 +1,98 @@
+import BaseFormatRule from "./format_rules/BaseFormatRule";
+import CodyUSF4FormatRule from "./format_rules/CodyUsf4FormatRule";
+import DefaultFormatRule from "./format_rules/DefaultFormatRule";
+import MenatSF5FormatRule from "./format_rules/MenatSf5FormatRule";
+import YoungZekuSF5FormatRule from "./format_rules/YoungZekuSf5FormatRule";
+
+export default class MoveFormatter {
+ private rules: BaseFormatRule[];
+
+ constructor() {
+ this.rules = [
+ new MenatSF5FormatRule(),
+ new YoungZekuSF5FormatRule(),
+ new CodyUSF4FormatRule(),
+ new DefaultFormatRule()
+ ];
+ }
+
+ formatToShorthand(moveData): string {
+ let shorthand: string;
+
+ for (const rule of this.rules) {
+ if (this.skipFormattingMove(moveData)) {
+ return "";
+ }
+
+ shorthand = rule.formatMove(moveData);
+
+ if (shorthand) {
+ return shorthand;
+ }
+ }
+
+ return "";
+ }
+
+ /**
+ * Skips character moves that meet various criteria in order to focus on applying
+ * formatting to normals.
+ * @remarks Some move types like command normals and others will sometimes get caught by the
+ * formatter engine because of their attributes in their JSON object.
+ * @param moveData The current move and its attributes as a JSON object
+ * @returns true if the move should not have formatting applied to it, false otherwise
+ */
+ private skipFormattingMove(moveData): boolean {
+ const TARGET_COMBO: string[] = ["(TC)", "Target Combo"];
+ const COMMAND_NORMAL: string[] = ["3", "6"];
+ const SYMBOLIC_CMD_NORMAL: string[] = [">", "(air)", "(run)", "(lvl"];
+ const RASHID_WIND: string = "(wind)";
+ const IGNORED_THIRD_STRIKE_MOVES: string[] = [
+ "Kakushuu Rakukyaku" /* Chun-li b.MK (Hold) */,
+ "Inazuma Kakato Wari (Long)" /* Ken b.MK (Hold) */,
+ "Elbow Cannon" /* Necro db.HP */
+ ];
+ const MOVE_NAME: string = moveData.moveName;
+
+ if (!moveData.numCmd) {
+ if (!moveData.plnCmd) {
+ return true;
+ }
+ }
+
+ // Do not attempt to apply formatting to target combos
+ if (TARGET_COMBO.some(indicator => MOVE_NAME.includes(indicator))) {
+ return true;
+ }
+
+ // Do not attempt to apply formatting to command normals
+ if (COMMAND_NORMAL.some(indicator => moveData.numCmd.includes(indicator))) {
+ return true;
+ }
+
+ // If the above check doesn't find anything, check for some other common indicators; if
+ // nothing comes back here, we're good and don't need to skip formatting
+ if (SYMBOLIC_CMD_NORMAL.some(indicator => moveData.plnCmd.includes(indicator))) {
+ return true;
+ }
+
+ // Rashid should be the only one (for now) to trigger this condition for his mixers
+ if (MOVE_NAME.includes(RASHID_WIND)) {
+ return true;
+ }
+
+ // For USF4, if the move motion is "back" but the move name doesn't include back, skip it
+ if (moveData.moveMotion === "B" && !MOVE_NAME.includes("Back")) {
+ return true;
+ }
+
+ // There are three awkward moves that get caught by the formatting engine
+ // for the 3S data. If the 3S data is ever cleaned up, this could be removed
+ // or refactored
+ if (IGNORED_THIRD_STRIKE_MOVES.some(indicator => MOVE_NAME === indicator)) {
+ return true;
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/js/utils/format_rules/BaseFormatRule.ts b/src/js/utils/format_rules/BaseFormatRule.ts
new file mode 100644
index 0000000..a71f09d
--- /dev/null
+++ b/src/js/utils/format_rules/BaseFormatRule.ts
@@ -0,0 +1,101 @@
+export default abstract class BaseFormatRule {
+ protected characterMoveRule: string;
+ protected strengths: string[] = ["lp", "mp", "hp", "lk", "mk", "hk"];
+ protected stanceToAbbreviationMap: Map = new Map([
+ ["stand", "st."],
+ ["crouch", "cr."],
+ ["jump", "j."],
+ ["neutral", "nj."],
+ ["close", "cl."],
+ ["far", "f."],
+ ["downback", "db."],
+ ["back", "b."]
+ ]);
+
+ /**
+ * @param rule A word, digit, or character to use as criteria for formatting
+ */
+ constructor(rule: string) {
+ this.characterMoveRule = rule;
+ }
+
+ /**
+ * Given a move in its full-length form (i.e., Stand HP), returns the
+ * move in the common shorthand form of "abbreviated stance.abbreviated input",
+ * i.e. st.HP
+ * @param move The text representing the move as a string
+ * @returns A string containing the abbreviated move
+ */
+ formatMove(moveData): string {
+ let formattedMoveName: string = "";
+
+ // If the move doesn't match the rule, we should break out of the method
+ // so the next rule can take over
+ if (!moveData.moveName.toLowerCase().includes(this.characterMoveRule.toLowerCase()) && this.characterMoveRule !== "") {
+ return formattedMoveName;
+ }
+
+ // If the move contains something like (Hold), use the regex format method
+ if (moveData.moveName.includes('(')) {
+ return this.formatMoveWithParenthesis(moveData.moveName);
+ }
+
+ let stanceAbbreviation: string = this.getStanceToAbbreviation(moveData.moveName);
+ let moveInput: string[] = this.extractInput(moveData);
+
+ if (moveInput.length > 1) {
+ formattedMoveName = `${stanceAbbreviation}${moveInput[0]} ${moveInput[1]}`;
+ } else {
+ formattedMoveName = `${stanceAbbreviation}${moveInput[0]}`;
+ }
+
+ return formattedMoveName;
+ }
+
+ /**
+ * Given a stance in full form (i.e., stand), returns the abbreviated version
+ * @param move The move provided to the call to formatMove
+ * @returns The stance of the move in its abbreviated form
+ */
+ private getStanceToAbbreviation(move: string): string {
+ let splitMove: string[] = move.trim().toLowerCase().split(' ');
+ let stance: string = splitMove[0];
+
+ return this.stanceToAbbreviationMap.get(stance);
+ }
+
+ /**
+ * Given a move in its full-length form but with a trailing word
+ * surrounded by parenthesis (i.e., Stand HP (Hold)), returns the
+ * move in the common shorthand form of "abbr stance.abbr input (parenContent)"
+ * i.e., st.HP (Hold)
+ * @param move The move provided to the call to formatMove
+ * @returns A string containing the abbreviated move
+ */
+ private formatMoveWithParenthesis(move: string) {
+ /*
+ Regex documentation:
+ Lead with \s to account for the leading space, i.e, " (Hold)", but we don't want to include it in the captured result
+ The outermost parentheses start the capture group of characters we DO want to capture
+ The character combo of \( means that we want to find an actual opening parenthesis
+ [a-z\s]* = Within the parenthesis, we want to find any combination of letters and spaces to account for cases like "(crouch large)"
+ Then we want to find the closing parenthesis with \)
+ The capture group is closed, and the "i" at the end sets a "case insensitive" flag for the regex expression
+ */
+ let splitMoveFromExtraParens: string[] = move.split(/\s(\([a-z\s]*\))/i).filter((x: string) => x !== "");
+ let splitMove: string[] = splitMoveFromExtraParens[0].split(' ');
+ let modifierParens: string[] = splitMoveFromExtraParens.slice(1);
+ let stanceAbbreviation: string = this.stanceToAbbreviationMap.get(splitMove[0].toLowerCase());
+ let input: string = splitMove[splitMove.length - 1].toUpperCase();
+
+ return `${stanceAbbreviation}${input} ${modifierParens.join(' ')}`;
+ }
+
+ /**
+ * Given a move, extract the input from it. This method's logic will vary
+ * for some characters, but the default case simply retrieves the currently
+ * used input from the end of the string.
+ * @param move The move provided to the call to formatMove
+ */
+ protected abstract extractInput(moveData): string[];
+}
\ No newline at end of file
diff --git a/src/js/utils/format_rules/MenatSF5FormatRule.ts b/src/js/utils/format_rules/MenatSF5FormatRule.ts
new file mode 100644
index 0000000..445ac85
--- /dev/null
+++ b/src/js/utils/format_rules/MenatSF5FormatRule.ts
@@ -0,0 +1,20 @@
+import BaseFormatRule from "./BaseFormatRule";
+
+export default class MenatSF5FormatRule extends BaseFormatRule {
+ // Sentence-casing for the "Orb" label
+ private orbLabel = this.characterMoveRule.charAt(0).toUpperCase() + this.characterMoveRule.slice(1);
+
+ constructor() {
+ super("orb");
+ }
+
+ protected extractInput(moveData): string[] {
+ let input: string[] = [];
+ let moveInput: string = moveData.moveName.split(' ').find(x => this.strengths.some(y => y === x.toLowerCase()));
+
+ input.push(moveInput.toUpperCase());
+ input.push(this.orbLabel);
+
+ return input;
+ }
+}
\ No newline at end of file
diff --git a/src/js/utils/format_rules/YoungZekuSF5FormatRule.ts b/src/js/utils/format_rules/YoungZekuSF5FormatRule.ts
new file mode 100644
index 0000000..7f99c5e
--- /dev/null
+++ b/src/js/utils/format_rules/YoungZekuSF5FormatRule.ts
@@ -0,0 +1,19 @@
+import BaseFormatRule from "./BaseFormatRule";
+
+export default class YoungZekuSF5FormatRule extends BaseFormatRule {
+ constructor() {
+ super("late");
+ }
+
+ protected extractInput(moveData): string[] {
+ let input: string[] = [];
+ let splitMove = moveData.moveName.toLowerCase().split(' ');
+ let lateHit: string = `${splitMove[2]} ${splitMove[3]}`;
+
+ input.push(splitMove[1].toUpperCase())
+ input.push(lateHit);
+
+ return input;
+ }
+
+}
\ No newline at end of file
diff --git a/src/js/utils/format_rules/codyusf4formatrule.ts b/src/js/utils/format_rules/codyusf4formatrule.ts
new file mode 100644
index 0000000..5a91838
--- /dev/null
+++ b/src/js/utils/format_rules/codyusf4formatrule.ts
@@ -0,0 +1,21 @@
+import BaseFormatRule from "./BaseFormatRule";
+
+export default class CodyUSF4FormatRule extends BaseFormatRule {
+ // Sentence-casing for the "Knife" label
+ private knifeLabel: string = this.characterMoveRule.charAt(0).toUpperCase() + this.characterMoveRule.slice(1);
+
+ constructor() {
+ super("knife");
+ }
+
+ protected extractInput(moveData): string[] {
+ let input: string[] = [];
+ let moveInput: string = moveData.moveName.split(' ').find(x => this.strengths.some(y => y === x.toLowerCase()));
+
+ input.push(moveInput.toUpperCase());
+ input.push(this.knifeLabel);
+
+ return input;
+ }
+
+}
\ No newline at end of file
diff --git a/src/js/utils/format_rules/defaultformatrule.ts b/src/js/utils/format_rules/defaultformatrule.ts
new file mode 100644
index 0000000..208edbe
--- /dev/null
+++ b/src/js/utils/format_rules/defaultformatrule.ts
@@ -0,0 +1,17 @@
+import BaseFormatRule from "./BaseFormatRule";
+
+export default class DefaultFormatRule extends BaseFormatRule {
+ constructor() {
+ super("");
+ }
+
+ protected extractInput(moveData): string[] {
+ let input: string[] = [];
+ let splitMove = moveData.moveName.trim().split(' ');
+
+ input.push(splitMove[splitMove.length - 1].toUpperCase());
+
+ return input;
+ }
+
+}
\ No newline at end of file
diff --git a/src/js/utils/index.ts b/src/js/utils/index.ts
index 017fd87..f224707 100644
--- a/src/js/utils/index.ts
+++ b/src/js/utils/index.ts
@@ -1,22 +1,79 @@
import { mapKeys, isEqual } from 'lodash';
+import { DataDisplaySettingsReducerState } from '../reducers/datadisplaysettings';
+import { VtState } from '../types';
+import MoveFormatter from './MoveFormatter';
-export function renameData(rawFrameData, moveNameType, inputNotationType) {
+/**
+ * Renames the moves in the character frame data to reflect the user's desired naming convention
+ * @param {string} rawFrameData The frame data for the current character
+ * @param {DataDisplaySettingsReducerState} dataDisplayState The Redux state containing various move text render settings
+ * @returns The frame data JSON object with renamed moves
+ */
+export function renameData(rawFrameData, dataDisplayState: DataDisplaySettingsReducerState, activeGame: string) {
+ const renameFrameData = (rawData, renameKey, notationDisplay) => {
+ switch (notationDisplay) {
+ case "fullWord":
+ return mapKeys(rawData, (moveValue, moveKey) => moveValue[renameKey] ? moveValue[renameKey] : moveKey);
+ case "shorthand":
+ return renameFrameDataToShorthand(rawData, renameKey, activeGame);
+ default:
+ break;
+ }
+ }
+
+ const renameFrameDataToShorthand = (rawData: string, nameTypeKey: string, activeGame: string) => {
+ const MOVE_TYPE_HELD_NORMAL: string = "held normal";
+ const MOVE_TYPE_NORMAL: string = "normal";
+ const GUILTY_GEAR_STRIVE: string = "ggst";
+
+ let rename = mapKeys(rawData, (moveValue, moveKey) => {
+ let activeGameIsGuiltyGearStrive: boolean = activeGame.toLowerCase() === GUILTY_GEAR_STRIVE;
+ let defaultMoveName: string = moveValue[nameTypeKey] ? moveValue[nameTypeKey] : moveKey;
+ let moveDataHasMovesList: boolean = Boolean(moveValue.movesList);
+
+ // Conditional expressions are used here because checking moveValue.moveType for a truthy value
+ // is how JavaScript/TypeScript can simultaneously check if a string is empty, null, or undefined
+ let moveIsNormal: boolean = moveValue.moveType ? moveValue.moveType.toLowerCase() === MOVE_TYPE_NORMAL : false;
+ let moveIsHeldNormal: boolean = moveValue.moveType ? moveValue.moveType.toLowerCase() === MOVE_TYPE_HELD_NORMAL : false;
+
+ if (activeGameIsGuiltyGearStrive) {
+ return defaultMoveName;
+ }
+
+ if (moveIsNormal) {
+ if (moveDataHasMovesList) {
+ return moveIsHeldNormal ? formatMoveName(moveValue) : defaultMoveName;
+ } else {
+ let formattedMove: string = formatMoveName(moveValue);
+ return formattedMove !== "" ? formattedMove : defaultMoveName;
+ }
+ } else {
+ return defaultMoveName;
+ }
+ });
- let renameKey = "";
- if (moveNameType === "official") {
- renameKey = "moveName"
- } else if (moveNameType === "common") {
- renameKey = "cmnName";
- } else if (moveNameType === "inputs" && inputNotationType) {
- renameKey = inputNotationType;
+ return rename;
}
- const renamedFrameData = mapKeys(rawFrameData,
- (moveData, moveKey) =>
- moveData[renameKey] ? moveData[renameKey] : moveKey
- );
+ const formatMoveName = (moveData) => {
+ let truncatedMoveName: string = "";
+ let moveFormatter = new MoveFormatter();
+
+ truncatedMoveName = moveFormatter.formatToShorthand(moveData);
+
+ return truncatedMoveName;
+ }
- return renamedFrameData;
+ switch (dataDisplayState.moveNameType) {
+ case "official":
+ return renameFrameData(rawFrameData, "moveName", dataDisplayState.normalNotationType);
+ case "common":
+ return renameFrameData(rawFrameData, "cmnName", dataDisplayState.normalNotationType);
+ case "inputs":
+ return renameFrameData(rawFrameData, dataDisplayState.inputNotationType, "fullWord");
+ default:
+ return rawFrameData;
+ }
}
@@ -27,18 +84,18 @@ function vTriggerMerge(rawFrameData, vtState) {
const vtMergedData = {
...rawFrameData.normal, ...rawFrameData[vtState]
}
-
+
Object.keys(rawFrameData[vtState]).forEach(vtMove => {
- let changedValues = [];
- Object.keys(rawFrameData[vtState][vtMove]).forEach(detail => {
- if (!rawFrameData.normal[vtMove]) {
- vtMergedData[vtMove]["uniqueInVt"] = true;
- } else if (rawFrameData.normal[vtMove] && !isEqual(rawFrameData.normal[vtMove][detail], rawFrameData[vtState][vtMove][detail])) {
- changedValues = [ ...changedValues, detail ]
- }
- })
- vtMergedData[vtMove] = { ...vtMergedData[vtMove], changedValues }
+ let changedValues = [];
+ Object.keys(rawFrameData[vtState][vtMove]).forEach(detail => {
+ if (!rawFrameData.normal[vtMove]) {
+ vtMergedData[vtMove]["uniqueInVt"] = true;
+ } else if (rawFrameData.normal[vtMove] && !isEqual(rawFrameData.normal[vtMove][detail], rawFrameData[vtState][vtMove][detail])) {
+ changedValues = [...changedValues, detail]
}
+ })
+ vtMergedData[vtMove] = { ...vtMergedData[vtMove], changedValues }
+ }
)
// based on https://stackoverflow.com/a/39442287
@@ -47,7 +104,7 @@ function vTriggerMerge(rawFrameData, vtState) {
.sort((moveOne: any, moveTwo: any) => {
return moveOne[1].i - moveTwo[1].i
})
- .reduce((_sortedObj, [k,v]) => ({
+ .reduce((_sortedObj, [k, v]) => ({
..._sortedObj,
[k]: v
}), {})
@@ -57,12 +114,9 @@ function vTriggerMerge(rawFrameData, vtState) {
}
// this allow me to build the JSON for the setPlayer action creator in selectCharacter, SegmentSwitcher and ____ componenet
-export function helpCreateFrameDataJSON(rawFrameData, moveNameType, inputNotationType, normalNotationType, vtState) {
-
- const dataToRename = vtState === "normal"
- ? rawFrameData.normal
- : vTriggerMerge(rawFrameData, vtState);
+export function helpCreateFrameDataJSON(rawFrameData, dataDisplayState: DataDisplaySettingsReducerState, vtState: VtState, activeGame: string) {
- return moveNameType === "official" ? dataToRename : renameData(dataToRename, moveNameType, inputNotationType);
+ const dataToRename = (vtState === "normal") ? rawFrameData.normal : vTriggerMerge(rawFrameData, vtState);
+ return renameData(dataToRename, dataDisplayState, activeGame);
}
\ No newline at end of file