Skip to content

Commit

Permalink
Unit Tests & Documentation (#1095)
Browse files Browse the repository at this point in the history
  • Loading branch information
BrandonPacewic authored Aug 21, 2024
2 parents ca0f6d0 + 21f318d commit b813e13
Show file tree
Hide file tree
Showing 19 changed files with 445 additions and 132 deletions.
3 changes: 3 additions & 0 deletions fission/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.15.6",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^20.4.4",
"@types/pako": "^2.0.3",
"@types/react": "^18.2.47",
Expand Down
2 changes: 0 additions & 2 deletions fission/src/Synthesis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ import NewInputSchemeModal from "./ui/modals/configuring/theme-editor/NewInputSc
import AssignNewSchemeModal from "./ui/modals/configuring/theme-editor/AssignNewSchemeModal.tsx"
import AnalyticsConsent from "./ui/components/AnalyticsConsent.tsx"
import PreferencesSystem from "./systems/preferences/PreferencesSystem.ts"
import ResetAllInputsModal from "./ui/modals/configuring/inputs/ResetAllInputsModal.tsx"
import APSManagementModal from "./ui/modals/APSManagementModal.tsx"
import ConfigurePanel from "./ui/panels/configuring/assembly-config/ConfigurePanel.tsx"

Expand Down Expand Up @@ -217,7 +216,6 @@ const initialModals = [
<MatchModeModal key="match-mode" modalId="match-mode" />,
<ConfigMotorModal key="config-motor" modalId="config-motor" />,
<ImportLocalMirabufModal key="import-local-mirabuf" modalId="import-local-mirabuf" />,
<ResetAllInputsModal key="reset-inputs" modalId="reset-inputs" />,
<APSManagementModal key="aps-management" modalId="aps-management" />,
]

Expand Down
6 changes: 5 additions & 1 deletion fission/src/systems/input/DefaultInputs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { InputScheme } from "./InputSchemeManager"
import { AxisInput, ButtonInput, EmptyModifierState } from "./InputSystem"

/** The purpose of this class is to store any defaults related to the input system. */
class DefaultInputs {
static ernie = () => {
return {
Expand Down Expand Up @@ -133,6 +134,7 @@ class DefaultInputs {
}
}

/** We like this guy */
public static hunter = () => {
return {
schemeName: "Hunter",
Expand Down Expand Up @@ -187,7 +189,8 @@ class DefaultInputs {
}
}

public static get defaultInputCopies() {
/** @returns {InputScheme[]} New copies of the default input schemes without reference to any others. */
public static get defaultInputCopies(): InputScheme[] {
return [
DefaultInputs.ernie(),
DefaultInputs.luna(),
Expand All @@ -197,6 +200,7 @@ class DefaultInputs {
]
}

/** @returns {InputScheme} A new blank input scheme with no control bound. */
public static get newBlankScheme(): InputScheme {
return {
schemeName: "",
Expand Down
14 changes: 0 additions & 14 deletions fission/src/systems/input/InputSchemeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,20 +129,6 @@ class InputSchemeManager {
PreferencesSystem.setGlobalPreference("InputSchemes", customizedSchemes)
PreferencesSystem.savePreferences()
}

/** Returns a copy of a scheme without references to the original in any way */
public static copyScheme(scheme: InputScheme): InputScheme {
const copiedInputs: Input[] = []
scheme.inputs.forEach(i => copiedInputs.push(i.getCopy()))

return {
schemeName: scheme.schemeName,
descriptiveName: scheme.descriptiveName,
customized: scheme.customized,
usesGamepad: scheme.usesGamepad,
inputs: copiedInputs,
}
}
}

export default InputSchemeManager
120 changes: 76 additions & 44 deletions fission/src/systems/input/InputSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,45 @@ export type ModifierState = {
}
export const EmptyModifierState: ModifierState = { ctrl: false, alt: false, shift: false, meta: false }

// Represents any input
/** Represents any user input */
abstract class Input {
public inputName: string

/** @param {string} inputName - The name given to this input to identify it's function. */
constructor(inputName: string) {
this.inputName = inputName
}

// Returns the current value of the input. Range depends on input type
/** @returns {number} a number between -1 and 1 for this input. */
abstract getValue(useGamepad: boolean): number

// Creates a copy to avoid modifying the default inputs by reference
abstract getCopy(): Input
}

// A single button
/** Represents any user input that is a single true/false button. */
class ButtonInput extends Input {
public keyCode: string
public keyModifiers: ModifierState

public gamepadButton: number

/**
* All optional params will remain unassigned if not value is given. This can be assigned later by the user through the configuration panel.
*
* @param {string} inputName - The name given to this input to identify it's function.
* @param {string} [keyCode] - The keyboard button for this input if a gamepad is not used.
* @param {number} [gamepadButton] - The gamepad button for this input if a gamepad is used.
* @param {ModifierState} [keyModifiers] - The key modifier state for the keyboard input.
*/
public constructor(inputName: string, keyCode?: string, gamepadButton?: number, keyModifiers?: ModifierState) {
super(inputName)
this.keyCode = keyCode ?? ""
this.keyModifiers = keyModifiers ?? EmptyModifierState
this.gamepadButton = gamepadButton ?? -1
}

// Returns 1 if pressed and 0 if not pressed
/**
* @param useGamepad Looks at the gamepad if true and the keyboard if false.
* @returns 1 if pressed, 0 if not pressed or not found.
*/
getValue(useGamepad: boolean): number {
// Gamepad button input
if (useGamepad) {
Expand All @@ -48,13 +57,9 @@ class ButtonInput extends Input {
// Keyboard button input
return InputSystem.isKeyPressed(this.keyCode, this.keyModifiers) ? 1 : 0
}

getCopy(): Input {
return new ButtonInput(this.inputName, this.keyCode, this.gamepadButton, this.keyModifiers)
}
}

// An axis between two buttons (-1 to 1)
/** Represents any user input that is an axis between -1 and 1. Can be a gamepad axis, two gamepad buttons, or two keyboard buttons. */
class AxisInput extends Input {
public posKeyCode: string
public posKeyModifiers: ModifierState
Expand All @@ -67,6 +72,20 @@ class AxisInput extends Input {
public posGamepadButton: number
public negGamepadButton: number

/**
* All optional params will remain unassigned if not value is given. This can be assigned later by the user through the configuration panel.
*
* @param {string} inputName - The name given to this input to identify it's function.
* @param {string} [posKeyCode] - The keyboard input that corresponds to a positive input value (1).
* @param {string} [negKeyCode] - The keyboard input that corresponds to a negative input value (-1).
* @param {number} [gamepadAxisNumber] - The gamepad axis that this input looks at if the scheme is set to use a gamepad.
* @param {boolean} [joystickInverted] - Inverts the input if a gamepad axis is used.
* @param {boolean} [useGamepadButtons] - If this is true and the scheme is set to use a gamepad, this axis will be between two buttons on the controller.
* @param {number} [posGamepadButton] - The gamepad button that corresponds to a positive input value (1).
* @param {number} [negGamepadButton] - The gamepad button that corresponds to a negative input value (-1).
* @param {ModifierState} [posKeyModifiers] - The key modifier state for the positive keyboard input.
* @param {ModifierState} [negKeyModifiers] - The key modifier state for the negative keyboard input.
*/
public constructor(
inputName: string,
posKeyCode?: string,
Expand Down Expand Up @@ -94,11 +113,14 @@ class AxisInput extends Input {
this.negGamepadButton = negGamepadButton ?? -1
}

// For keyboard: returns 1 if positive pressed, -1 if negative pressed, or 0 if none or both are pressed
// For gamepad axis: returns a range between -1 and 1 with a deadband in the middle
/**
* @param useGamepad Looks at the gamepad if true and the keyboard if false.
* @returns {number} KEYBOARD: 1 if positive pressed, -1 if negative pressed, or 0 if none or both are pressed.
* @returns {number} GAMEPAD: a number between -1 and 1 with a deadband in the middle.
*/
getValue(useGamepad: boolean): number {
// Gamepad joystick axis
if (useGamepad) {
// Gamepad joystick axis
if (!this.useGamepadButtons)
return InputSystem.getGamepadAxis(this.gamepadAxisNumber) * (this.joystickInverted ? -1 : 1)

Expand All @@ -115,38 +137,28 @@ class AxisInput extends Input {
(InputSystem.isKeyPressed(this.negKeyCode, this.negKeyModifiers) ? 1 : 0)
)
}

getCopy(): Input {
return new AxisInput(
this.inputName,
this.posKeyCode,
this.negKeyCode,
this.gamepadAxisNumber,
this.joystickInverted,
this.useGamepadButtons,
this.posGamepadButton,
this.negGamepadButton,
this.posKeyModifiers,
this.negKeyModifiers
)
}
}

/**
* The input system listens for and records key presses and joystick positions to be used by robots.
* It also maps robot behaviors (such as an arcade drivetrain or an arm) to specific keys through customizable input schemes.
*/
class InputSystem extends WorldSystem {
public static currentModifierState: ModifierState

// A list of keys currently being pressed
/** The keys currently being pressed. */
private static _keysPressed: { [key: string]: boolean } = {}

private static _gpIndex: number | null
public static gamepad: Gamepad | null

// Maps a brain index to a certain input scheme
/** Maps a brain index to an input scheme. */
public static brainIndexSchemeMap: Map<number, InputScheme> = new Map()

constructor() {
super()

// Initialize input events
this.handleKeyDown = this.handleKeyDown.bind(this)
document.addEventListener("keydown", this.handleKeyDown)

Expand All @@ -159,7 +171,7 @@ class InputSystem extends WorldSystem {
this.gamepadDisconnected = this.gamepadDisconnected.bind(this)
window.addEventListener("gamepaddisconnected", this.gamepadDisconnected)

// Detect when the user leaves the page to clear inputs
// Initialize an event that's triggered when the user exits/enters the page
document.addEventListener("visibilitychange", () => {
if (document.hidden) this.clearKeyData()
})
Expand All @@ -177,13 +189,13 @@ class InputSystem extends WorldSystem {
}

public Update(_: number): void {
InputSystem
// Fetch current gamepad information
if (InputSystem._gpIndex == null) InputSystem.gamepad = null
else InputSystem.gamepad = navigator.getGamepads()[InputSystem._gpIndex]

if (!document.hasFocus()) this.clearKeyData()

// Update the current modifier state to be checked against target stats when getting input values
InputSystem.currentModifierState = {
ctrl: InputSystem.isKeyPressed("ControlLeft") || InputSystem.isKeyPressed("ControlRight"),
alt: InputSystem.isKeyPressed("AltLeft") || InputSystem.isKeyPressed("AltRight"),
Expand All @@ -199,21 +211,22 @@ class InputSystem extends WorldSystem {
window.removeEventListener("gamepaddisconnected", this.gamepadDisconnected)
}

// Called when any key is first pressed
/** Called when any key is first pressed */
private handleKeyDown(event: KeyboardEvent) {
InputSystem._keysPressed[event.code] = true
}

// Called when any key is released
/* Called when any key is released */
private handleKeyUp(event: KeyboardEvent) {
InputSystem._keysPressed[event.code] = false
}

/** Clears all stored key data when the user leaves the page. */
private clearKeyData() {
for (const keyCode in InputSystem._keysPressed) delete InputSystem._keysPressed[keyCode]
}

// Called once when a gamepad is first connected
/* Called once when a gamepad is first connected */
private gamepadConnected(event: GamepadEvent) {
console.log(
"Gamepad connected at index %d: %s. %d buttons, %d axes.",
Expand All @@ -226,22 +239,30 @@ class InputSystem extends WorldSystem {
InputSystem._gpIndex = event.gamepad.index
}

// Called once when a gamepad is first disconnected
/* Called once when a gamepad is first disconnected */
private gamepadDisconnected(event: GamepadEvent) {
console.log("Gamepad disconnected from index %d: %s", event.gamepad.index, event.gamepad.id)

InputSystem._gpIndex = null
}

// Returns true if the given key is currently down
/**
* @param {string} key - The keycode of the target key.
* @param {ModifierState} modifiers - The target modifier state. Assumed to be no modifiers if undefined.
* @returns {boolean} True if the key is pressed or false otherwise.
*/
public static isKeyPressed(key: string, modifiers?: ModifierState): boolean {
if (modifiers != null && !InputSystem.compareModifiers(InputSystem.currentModifierState, modifiers))
return false

return !!InputSystem._keysPressed[key]
}

// If an input exists, return it's value
/**
* @param {string} inputName The name of the function of the input.
* @param {number} brainIndex The robot brain index for this input. Used to map to a control scheme.
* @returns {number} A number between -1 and 1 based on the current state of the input.
*/
public static getInput(inputName: string, brainIndex: number): number {
const targetScheme = InputSystem.brainIndexSchemeMap.get(brainIndex)

Expand All @@ -252,8 +273,12 @@ class InputSystem extends WorldSystem {
return targetInput.getValue(targetScheme.usesGamepad)
}

// Returns true if two modifier states are identical
private static compareModifiers(state1: ModifierState, state2: ModifierState): boolean {
/**
* @param {ModifierState} state1 Any key modifier state.
* @param {ModifierState} state2 Any key modifier state.
* @returns {boolean} True if the modifier states are identical and false otherwise.
*/
public static compareModifiers(state1: ModifierState, state2: ModifierState): boolean {
if (!state1 || !state2) return false

return (
Expand All @@ -264,7 +289,10 @@ class InputSystem extends WorldSystem {
)
}

// Returns a number between -1 and 1 with a deadband
/**
* @param {number} axisNumber The joystick axis index. Must be an integer.
* @returns {number} A number between -1 and 1 based on the position of this axis or 0 if no gamepad is connected or the axis is not found.
*/
public static getGamepadAxis(axisNumber: number): number {
if (InputSystem.gamepad == null) return 0

Expand All @@ -276,7 +304,11 @@ class InputSystem extends WorldSystem {
return Math.abs(value) < 0.15 ? 0 : value
}

// Returns true if a gamepad is connected and a certain button is pressed
/**
*
* @param {number} buttonNumber - The gamepad button index. Must be an integer.
* @returns {boolean} True if the button is pressed, false if not, a gamepad isn't connected, or the button can't be found.
*/
public static isGamepadButtonPressed(buttonNumber: number): boolean {
if (InputSystem.gamepad == null) return false

Expand Down
Loading

0 comments on commit b813e13

Please sign in to comment.