diff --git a/src/js/actions/index.ts b/src/js/actions/index.ts index 3c41608..6d054b2 100644 --- a/src/js/actions/index.ts +++ b/src/js/actions/index.ts @@ -128,7 +128,7 @@ export const setPlayer = (playerId: PlayerId, charName: PlayerData["name"]) => : "normal" const playerData: PlayerData = { name: charName, - frameData: helpCreateFrameDataJSON(frameDataState[charName].moves, dataDisplaySettingsState.moveNameType, dataDisplaySettingsState.inputNotationType, dataDisplaySettingsState.normalNotationType, stateToSet), + frameData: helpCreateFrameDataJSON(frameDataState[charName].moves, dataDisplaySettingsState, stateToSet, activeGameState), stats: frameDataState[charName].stats, vtState: stateToSet, } diff --git a/src/js/pages/Settings.tsx b/src/js/pages/Settings.tsx index 31960a6..a03765d 100644 --- a/src/js/pages/Settings.tsx +++ b/src/js/pages/Settings.tsx @@ -111,8 +111,7 @@ const Settings = () => { - {/* @Jon Uncomment this! */} - {/* +

Normal Notation

Choose long or short normal names

@@ -133,8 +132,7 @@ const Settings = () => { Full Word Shorthand -
*/} - +
{/* 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