diff --git a/index.html b/index.html
index fea89f1..85b3fa1 100644
--- a/index.html
+++ b/index.html
@@ -27,6 +27,7 @@
@@ -308,6 +309,139 @@
+
+
diff --git a/src/main.ts b/src/main.ts
index c95c69f..551c33c 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -713,6 +713,18 @@ router.on('/settings', () => {
}
});
+router.on('/controls', () => {
+ showView('controls');
+ // Dynamically import and initialize controls screen
+ if (!(window as any).__controlsInitialized) {
+ import('./ui/screens/controlsScreen').then((module) => {
+ const controlsScreen = new module.ControlsScreen();
+ controlsScreen.initialize();
+ (window as any).__controlsInitialized = true;
+ });
+ }
+});
+
// Initialize registry and start router
// This must happen BEFORE router.start() so levels are available
async function initializeApp() {
diff --git a/src/ship/input/controllerInput.ts b/src/ship/input/controllerInput.ts
index 4762c30..decbbab 100644
--- a/src/ship/input/controllerInput.ts
+++ b/src/ship/input/controllerInput.ts
@@ -6,6 +6,7 @@ import {
WebXRInputSource,
} from "@babylonjs/core";
import debugLog from "../../core/debug";
+import { ControllerMappingConfig, StickAction } from "./controllerMapping";
const controllerComponents = [
"a-button",
@@ -38,8 +39,16 @@ interface CameraAdjustment {
* Maps controller thumbsticks and buttons to ship controls
*/
export class ControllerInput {
+ // Raw stick values (before mapping)
+ private _rawLeftStickX: number = 0;
+ private _rawLeftStickY: number = 0;
+ private _rawRightStickX: number = 0;
+ private _rawRightStickY: number = 0;
+
+ // Legacy stick vectors (for compatibility)
private _leftStick: Vector2 = Vector2.Zero();
private _rightStick: Vector2 = Vector2.Zero();
+
private _shooting: boolean = false;
private _leftInputSource: WebXRInputSource;
private _rightInputSource: WebXRInputSource;
@@ -50,9 +59,11 @@ export class ControllerInput {
new Observable();
private _onStatusScreenToggleObservable: Observable = new Observable();
private _enabled: boolean = true;
+ private _mappingConfig: ControllerMappingConfig;
constructor() {
this._controllerObservable.add(this.handleControllerEvent.bind(this));
+ this._mappingConfig = ControllerMappingConfig.getInstance();
}
/**
@@ -78,6 +89,7 @@ export class ControllerInput {
/**
* Get current input state (stick positions)
+ * Applies controller mapping configuration to translate raw input to actions
*/
public getInputState() {
if (!this._enabled) {
@@ -86,9 +98,41 @@ export class ControllerInput {
rightStick: Vector2.Zero(),
};
}
+
+ const mapping = this._mappingConfig.getMapping();
+
+ // Create action values map
+ const actions = new Map();
+ actions.set('yaw', 0);
+ actions.set('pitch', 0);
+ actions.set('roll', 0);
+ actions.set('forward', 0);
+
+ // Apply raw stick values to configured actions (with inversion)
+ const leftX = mapping.invertLeftStickX ? -this._rawLeftStickX : this._rawLeftStickX;
+ const leftY = mapping.invertLeftStickY ? -this._rawLeftStickY : this._rawLeftStickY;
+ const rightX = mapping.invertRightStickX ? -this._rawRightStickX : this._rawRightStickX;
+ const rightY = mapping.invertRightStickY ? -this._rawRightStickY : this._rawRightStickY;
+
+ if (mapping.leftStickX !== 'none') {
+ actions.set(mapping.leftStickX, leftX);
+ }
+ if (mapping.leftStickY !== 'none') {
+ actions.set(mapping.leftStickY, leftY);
+ }
+ if (mapping.rightStickX !== 'none') {
+ actions.set(mapping.rightStickX, rightX);
+ }
+ if (mapping.rightStickY !== 'none') {
+ actions.set(mapping.rightStickY, rightY);
+ }
+
+ // Map actions back to virtual stick positions for ShipPhysics
+ // leftStick.x = yaw, leftStick.y = forward
+ // rightStick.x = roll, rightStick.y = pitch
return {
- leftStick: this._leftStick.clone(),
- rightStick: this._rightStick.clone(),
+ leftStick: new Vector2(actions.get('yaw') || 0, actions.get('forward') || 0),
+ rightStick: new Vector2(actions.get('roll') || 0, actions.get('pitch') || 0),
};
}
@@ -98,7 +142,12 @@ export class ControllerInput {
public setEnabled(enabled: boolean): void {
this._enabled = enabled;
if (!enabled) {
- // Reset stick values when disabled
+ // Reset raw stick values when disabled
+ this._rawLeftStickX = 0;
+ this._rawLeftStickY = 0;
+ this._rawRightStickX = 0;
+ this._rawRightStickY = 0;
+ // Also reset legacy values
this._leftStick.x = 0;
this._leftStick.y = 0;
this._rightStick.x = 0;
@@ -232,14 +281,15 @@ export class ControllerInput {
}
if (controllerEvent.type === "thumbstick") {
+ // Store raw stick values (mapping will be applied in getInputState())
if (controllerEvent.hand === "left") {
- this._leftStick.x = controllerEvent.axisData.x;
- this._leftStick.y = controllerEvent.axisData.y;
+ this._rawLeftStickX = controllerEvent.axisData.x;
+ this._rawLeftStickY = controllerEvent.axisData.y;
}
if (controllerEvent.hand === "right") {
- this._rightStick.x = controllerEvent.axisData.x;
- this._rightStick.y = controllerEvent.axisData.y;
+ this._rawRightStickX = controllerEvent.axisData.x;
+ this._rawRightStickY = controllerEvent.axisData.y;
}
}
diff --git a/src/ship/input/controllerMapping.ts b/src/ship/input/controllerMapping.ts
new file mode 100644
index 0000000..8edbbf4
--- /dev/null
+++ b/src/ship/input/controllerMapping.ts
@@ -0,0 +1,268 @@
+import debugLog from '../../core/debug';
+
+const STORAGE_KEY = 'space-game-controller-mapping';
+
+/**
+ * Available stick actions
+ */
+export type StickAction =
+ | 'yaw' // Rotation around Y-axis (left/right turn)
+ | 'pitch' // Rotation around X-axis (nose up/down)
+ | 'roll' // Rotation around Z-axis (barrel roll)
+ | 'forward' // Forward/backward thrust
+ | 'none'; // No action
+
+/**
+ * Available button actions
+ */
+export type ButtonAction =
+ | 'fire' // Fire weapon
+ | 'cameraUp' // Adjust camera up
+ | 'cameraDown' // Adjust camera down
+ | 'statusScreen' // Toggle status screen
+ | 'none'; // No action
+
+/**
+ * Complete controller mapping configuration
+ */
+export interface ControllerMapping {
+ // Stick axis mappings
+ leftStickX: StickAction;
+ leftStickY: StickAction;
+ rightStickX: StickAction;
+ rightStickY: StickAction;
+
+ // Inversion flags for each axis
+ invertLeftStickX: boolean;
+ invertLeftStickY: boolean;
+ invertRightStickX: boolean;
+ invertRightStickY: boolean;
+
+ // Button mappings
+ trigger: ButtonAction;
+ aButton: ButtonAction;
+ bButton: ButtonAction;
+ xButton: ButtonAction;
+ yButton: ButtonAction;
+ squeeze: ButtonAction;
+}
+
+/**
+ * Singleton configuration manager for controller mappings
+ * Handles loading, saving, and validation of controller configurations
+ */
+export class ControllerMappingConfig {
+ private static _instance: ControllerMappingConfig | null = null;
+ private _mapping: ControllerMapping;
+
+ /**
+ * Default controller mapping (matches original game behavior)
+ */
+ private static readonly DEFAULT_MAPPING: ControllerMapping = {
+ // Stick mappings (original behavior)
+ leftStickX: 'yaw',
+ leftStickY: 'forward',
+ rightStickX: 'roll',
+ rightStickY: 'pitch',
+
+ // No inversions by default
+ invertLeftStickX: false,
+ invertLeftStickY: false,
+ invertRightStickX: false,
+ invertRightStickY: false,
+
+ // Button mappings (original behavior)
+ trigger: 'fire',
+ aButton: 'cameraDown',
+ bButton: 'cameraUp',
+ xButton: 'statusScreen',
+ yButton: 'none',
+ squeeze: 'none',
+ };
+
+ private constructor() {
+ this._mapping = { ...ControllerMappingConfig.DEFAULT_MAPPING };
+ this.loadFromStorage();
+ }
+
+ /**
+ * Get singleton instance
+ */
+ public static getInstance(): ControllerMappingConfig {
+ if (!ControllerMappingConfig._instance) {
+ ControllerMappingConfig._instance = new ControllerMappingConfig();
+ }
+ return ControllerMappingConfig._instance;
+ }
+
+ /**
+ * Get current mapping configuration
+ */
+ public getMapping(): Readonly {
+ return { ...this._mapping };
+ }
+
+ /**
+ * Update mapping configuration
+ */
+ public setMapping(mapping: ControllerMapping): void {
+ this._mapping = { ...mapping };
+ debugLog('[ControllerMapping] Configuration updated:', this._mapping);
+ }
+
+ /**
+ * Reset to default mapping
+ */
+ public resetToDefault(): void {
+ this._mapping = { ...ControllerMappingConfig.DEFAULT_MAPPING };
+ debugLog('[ControllerMapping] Reset to default configuration');
+ }
+
+ /**
+ * Save current mapping to localStorage
+ */
+ public save(): void {
+ try {
+ const json = JSON.stringify(this._mapping);
+ localStorage.setItem(STORAGE_KEY, json);
+ debugLog('[ControllerMapping] Saved to localStorage');
+ } catch (error) {
+ console.error('[ControllerMapping] Failed to save to localStorage:', error);
+ }
+ }
+
+ /**
+ * Load mapping from localStorage
+ */
+ private loadFromStorage(): void {
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY);
+ if (stored) {
+ const parsed = JSON.parse(stored) as Partial;
+
+ // Merge with defaults to handle missing properties (backward compatibility)
+ this._mapping = {
+ ...ControllerMappingConfig.DEFAULT_MAPPING,
+ ...parsed,
+ };
+
+ debugLog('[ControllerMapping] Loaded from localStorage:', this._mapping);
+ } else {
+ debugLog('[ControllerMapping] No saved configuration, using defaults');
+ }
+ } catch (error) {
+ console.warn('[ControllerMapping] Failed to load from localStorage, using defaults:', error);
+ this._mapping = { ...ControllerMappingConfig.DEFAULT_MAPPING };
+ }
+ }
+
+ /**
+ * Validate mapping configuration
+ * Returns array of warning messages (empty if valid)
+ */
+ public validate(): string[] {
+ const warnings: string[] = [];
+
+ // Check if fire action is mapped
+ const hasFireAction = this._mapping.trigger === 'fire' ||
+ this._mapping.aButton === 'fire' ||
+ this._mapping.bButton === 'fire' ||
+ this._mapping.xButton === 'fire' ||
+ this._mapping.yButton === 'fire' ||
+ this._mapping.squeeze === 'fire';
+
+ if (!hasFireAction) {
+ warnings.push('Warning: No button is mapped to "Fire Weapon"');
+ }
+
+ // Check if forward thrust is mapped
+ const hasForwardAction = this._mapping.leftStickX === 'forward' ||
+ this._mapping.leftStickY === 'forward' ||
+ this._mapping.rightStickX === 'forward' ||
+ this._mapping.rightStickY === 'forward';
+
+ if (!hasForwardAction) {
+ warnings.push('Warning: No stick is mapped to "Forward Thrust"');
+ }
+
+ // Check for duplicate stick actions (excluding 'none')
+ const stickActions = [
+ this._mapping.leftStickX,
+ this._mapping.leftStickY,
+ this._mapping.rightStickX,
+ this._mapping.rightStickY,
+ ].filter(action => action !== 'none');
+
+ const duplicateStickActions = stickActions.filter((action, index) =>
+ stickActions.indexOf(action) !== index
+ );
+
+ if (duplicateStickActions.length > 0) {
+ const unique = Array.from(new Set(duplicateStickActions));
+ warnings.push(`Warning: Multiple sticks mapped to same action: ${unique.join(', ')}`);
+ }
+
+ // Check for duplicate button actions (excluding 'none')
+ const buttonActions = [
+ this._mapping.trigger,
+ this._mapping.aButton,
+ this._mapping.bButton,
+ this._mapping.xButton,
+ this._mapping.yButton,
+ this._mapping.squeeze,
+ ].filter(action => action !== 'none');
+
+ const duplicateButtonActions = buttonActions.filter((action, index) =>
+ buttonActions.indexOf(action) !== index
+ );
+
+ if (duplicateButtonActions.length > 0) {
+ const unique = Array.from(new Set(duplicateButtonActions));
+ warnings.push(`Warning: Multiple buttons mapped to same action: ${unique.join(', ')}`);
+ }
+
+ return warnings;
+ }
+
+ /**
+ * Get human-readable label for a stick action
+ */
+ public static getStickActionLabel(action: StickAction): string {
+ switch (action) {
+ case 'yaw': return 'Yaw (Turn Left/Right)';
+ case 'pitch': return 'Pitch (Nose Up/Down)';
+ case 'roll': return 'Roll (Barrel Roll)';
+ case 'forward': return 'Forward/Backward Thrust';
+ case 'none': return 'None';
+ default: return action;
+ }
+ }
+
+ /**
+ * Get human-readable label for a button action
+ */
+ public static getButtonActionLabel(action: ButtonAction): string {
+ switch (action) {
+ case 'fire': return 'Fire Weapon';
+ case 'cameraUp': return 'Camera Adjust Up';
+ case 'cameraDown': return 'Camera Adjust Down';
+ case 'statusScreen': return 'Toggle Status Screen';
+ case 'none': return 'None';
+ default: return action;
+ }
+ }
+
+ /**
+ * Get all available stick actions
+ */
+ public static getAvailableStickActions(): StickAction[] {
+ return ['yaw', 'pitch', 'roll', 'forward', 'none'];
+ }
+
+ /**
+ * Get all available button actions
+ */
+ public static getAvailableButtonActions(): ButtonAction[] {
+ return ['fire', 'cameraUp', 'cameraDown', 'statusScreen', 'none'];
+ }
+}
diff --git a/src/ui/screens/controlsScreen.ts b/src/ui/screens/controlsScreen.ts
new file mode 100644
index 0000000..bd37195
--- /dev/null
+++ b/src/ui/screens/controlsScreen.ts
@@ -0,0 +1,294 @@
+import {
+ ControllerMappingConfig,
+ ControllerMapping,
+ StickAction,
+ ButtonAction
+} from '../../ship/input/controllerMapping';
+
+/**
+ * Controller remapping screen
+ * Allows users to customize VR controller button and stick mappings
+ */
+export class ControlsScreen {
+ private config: ControllerMappingConfig;
+ private messageDiv: HTMLElement | null = null;
+
+ constructor() {
+ this.config = ControllerMappingConfig.getInstance();
+ }
+
+ /**
+ * Initialize the controls screen
+ * Set up event listeners and populate form with current configuration
+ */
+ public initialize(): void {
+ console.log('[ControlsScreen] Initializing');
+
+ // Get form elements
+ this.messageDiv = document.getElementById('controlsMessage');
+
+ // Populate dropdowns
+ this.populateDropdowns();
+
+ // Load current configuration into form
+ this.loadCurrentMapping();
+
+ // Set up event listeners
+ this.setupEventListeners();
+
+ console.log('[ControlsScreen] Initialized');
+ }
+
+ /**
+ * Populate all dropdown select elements with available actions
+ */
+ private populateDropdowns(): void {
+ // Stick action dropdowns
+ const stickSelects = [
+ 'leftStickX', 'leftStickY',
+ 'rightStickX', 'rightStickY'
+ ];
+
+ const stickActions = ControllerMappingConfig.getAvailableStickActions();
+
+ stickSelects.forEach(id => {
+ const select = document.getElementById(id) as HTMLSelectElement;
+ if (select) {
+ select.innerHTML = '';
+ stickActions.forEach(action => {
+ const option = document.createElement('option');
+ option.value = action;
+ option.textContent = ControllerMappingConfig.getStickActionLabel(action);
+ select.appendChild(option);
+ });
+ }
+ });
+
+ // Button action dropdowns
+ const buttonSelects = [
+ 'trigger', 'aButton', 'bButton',
+ 'xButton', 'yButton', 'squeeze'
+ ];
+
+ const buttonActions = ControllerMappingConfig.getAvailableButtonActions();
+
+ buttonSelects.forEach(id => {
+ const select = document.getElementById(id) as HTMLSelectElement;
+ if (select) {
+ select.innerHTML = '';
+ buttonActions.forEach(action => {
+ const option = document.createElement('option');
+ option.value = action;
+ option.textContent = ControllerMappingConfig.getButtonActionLabel(action);
+ select.appendChild(option);
+ });
+ }
+ });
+ }
+
+ /**
+ * Load current mapping configuration into form elements
+ */
+ private loadCurrentMapping(): void {
+ const mapping = this.config.getMapping();
+
+ // Stick mappings
+ this.setSelectValue('leftStickX', mapping.leftStickX);
+ this.setSelectValue('leftStickY', mapping.leftStickY);
+ this.setSelectValue('rightStickX', mapping.rightStickX);
+ this.setSelectValue('rightStickY', mapping.rightStickY);
+
+ // Inversion checkboxes
+ this.setCheckboxValue('invertLeftStickX', mapping.invertLeftStickX);
+ this.setCheckboxValue('invertLeftStickY', mapping.invertLeftStickY);
+ this.setCheckboxValue('invertRightStickX', mapping.invertRightStickX);
+ this.setCheckboxValue('invertRightStickY', mapping.invertRightStickY);
+
+ // Button mappings
+ this.setSelectValue('trigger', mapping.trigger);
+ this.setSelectValue('aButton', mapping.aButton);
+ this.setSelectValue('bButton', mapping.bButton);
+ this.setSelectValue('xButton', mapping.xButton);
+ this.setSelectValue('yButton', mapping.yButton);
+ this.setSelectValue('squeeze', mapping.squeeze);
+
+ console.log('[ControlsScreen] Loaded current mapping into form');
+ }
+
+ /**
+ * Set up event listeners for buttons
+ */
+ private setupEventListeners(): void {
+ // Save button
+ const saveBtn = document.getElementById('saveControlsBtn');
+ if (saveBtn) {
+ saveBtn.addEventListener('click', () => this.saveMapping());
+ }
+
+ // Reset button
+ const resetBtn = document.getElementById('resetControlsBtn');
+ if (resetBtn) {
+ resetBtn.addEventListener('click', () => this.resetToDefault());
+ }
+
+ // Test button (shows current mapping preview)
+ const testBtn = document.getElementById('testControlsBtn');
+ if (testBtn) {
+ testBtn.addEventListener('click', () => this.showTestPreview());
+ }
+ }
+
+ /**
+ * Save current form values to configuration
+ */
+ private saveMapping(): void {
+ // Read all form values
+ const mapping: ControllerMapping = {
+ // Stick mappings
+ leftStickX: this.getSelectValue('leftStickX') as StickAction,
+ leftStickY: this.getSelectValue('leftStickY') as StickAction,
+ rightStickX: this.getSelectValue('rightStickX') as StickAction,
+ rightStickY: this.getSelectValue('rightStickY') as StickAction,
+
+ // Inversions
+ invertLeftStickX: this.getCheckboxValue('invertLeftStickX'),
+ invertLeftStickY: this.getCheckboxValue('invertLeftStickY'),
+ invertRightStickX: this.getCheckboxValue('invertRightStickX'),
+ invertRightStickY: this.getCheckboxValue('invertRightStickY'),
+
+ // Button mappings
+ trigger: this.getSelectValue('trigger') as ButtonAction,
+ aButton: this.getSelectValue('aButton') as ButtonAction,
+ bButton: this.getSelectValue('bButton') as ButtonAction,
+ xButton: this.getSelectValue('xButton') as ButtonAction,
+ yButton: this.getSelectValue('yButton') as ButtonAction,
+ squeeze: this.getSelectValue('squeeze') as ButtonAction,
+ };
+
+ // Validate
+ this.config.setMapping(mapping);
+ const warnings = this.config.validate();
+
+ if (warnings.length > 0) {
+ // Show warnings but still save
+ this.showMessage(
+ 'Configuration saved with warnings:\n' + warnings.join('\n'),
+ 'warning'
+ );
+ } else {
+ this.showMessage('Configuration saved successfully!', 'success');
+ }
+
+ // Save to localStorage
+ this.config.save();
+
+ console.log('[ControlsScreen] Saved mapping:', mapping);
+ }
+
+ /**
+ * Reset form to default mapping
+ */
+ private resetToDefault(): void {
+ if (confirm('Reset all controller mappings to default? This cannot be undone.')) {
+ this.config.resetToDefault();
+ this.config.save();
+ this.loadCurrentMapping();
+ this.showMessage('Reset to default configuration', 'success');
+ console.log('[ControlsScreen] Reset to defaults');
+ }
+ }
+
+ /**
+ * Show test preview of current mapping
+ */
+ private showTestPreview(): void {
+ const mapping = this.readCurrentFormValues();
+
+ let preview = 'Current Controller Mapping:\n\n';
+
+ preview += 'š STICK MAPPINGS:\n';
+ preview += ` Left Stick X: ${ControllerMappingConfig.getStickActionLabel(mapping.leftStickX)}`;
+ preview += mapping.invertLeftStickX ? ' (Inverted)\n' : '\n';
+ preview += ` Left Stick Y: ${ControllerMappingConfig.getStickActionLabel(mapping.leftStickY)}`;
+ preview += mapping.invertLeftStickY ? ' (Inverted)\n' : '\n';
+ preview += ` Right Stick X: ${ControllerMappingConfig.getStickActionLabel(mapping.rightStickX)}`;
+ preview += mapping.invertRightStickX ? ' (Inverted)\n' : '\n';
+ preview += ` Right Stick Y: ${ControllerMappingConfig.getStickActionLabel(mapping.rightStickY)}`;
+ preview += mapping.invertRightStickY ? ' (Inverted)\n' : '\n';
+
+ preview += '\nš® BUTTON MAPPINGS:\n';
+ preview += ` Trigger: ${ControllerMappingConfig.getButtonActionLabel(mapping.trigger)}\n`;
+ preview += ` A Button: ${ControllerMappingConfig.getButtonActionLabel(mapping.aButton)}\n`;
+ preview += ` B Button: ${ControllerMappingConfig.getButtonActionLabel(mapping.bButton)}\n`;
+ preview += ` X Button: ${ControllerMappingConfig.getButtonActionLabel(mapping.xButton)}\n`;
+ preview += ` Y Button: ${ControllerMappingConfig.getButtonActionLabel(mapping.yButton)}\n`;
+ preview += ` Squeeze/Grip: ${ControllerMappingConfig.getButtonActionLabel(mapping.squeeze)}\n`;
+
+ alert(preview);
+ }
+
+ /**
+ * Read current form values into a mapping object
+ */
+ private readCurrentFormValues(): ControllerMapping {
+ return {
+ leftStickX: this.getSelectValue('leftStickX') as StickAction,
+ leftStickY: this.getSelectValue('leftStickY') as StickAction,
+ rightStickX: this.getSelectValue('rightStickX') as StickAction,
+ rightStickY: this.getSelectValue('rightStickY') as StickAction,
+ invertLeftStickX: this.getCheckboxValue('invertLeftStickX'),
+ invertLeftStickY: this.getCheckboxValue('invertLeftStickY'),
+ invertRightStickX: this.getCheckboxValue('invertRightStickX'),
+ invertRightStickY: this.getCheckboxValue('invertRightStickY'),
+ trigger: this.getSelectValue('trigger') as ButtonAction,
+ aButton: this.getSelectValue('aButton') as ButtonAction,
+ bButton: this.getSelectValue('bButton') as ButtonAction,
+ xButton: this.getSelectValue('xButton') as ButtonAction,
+ yButton: this.getSelectValue('yButton') as ButtonAction,
+ squeeze: this.getSelectValue('squeeze') as ButtonAction,
+ };
+ }
+
+ /**
+ * Show a message to the user
+ */
+ private showMessage(message: string, type: 'success' | 'error' | 'warning' = 'success'): void {
+ if (this.messageDiv) {
+ this.messageDiv.textContent = message;
+ this.messageDiv.className = `controls-message ${type}`;
+ this.messageDiv.style.display = 'block';
+
+ // Hide after 5 seconds
+ setTimeout(() => {
+ if (this.messageDiv) {
+ this.messageDiv.style.display = 'none';
+ }
+ }, 5000);
+ }
+ }
+
+ // Helper methods for form manipulation
+ private setSelectValue(id: string, value: string): void {
+ const select = document.getElementById(id) as HTMLSelectElement;
+ if (select) {
+ select.value = value;
+ }
+ }
+
+ private getSelectValue(id: string): string {
+ const select = document.getElementById(id) as HTMLSelectElement;
+ return select ? select.value : '';
+ }
+
+ private setCheckboxValue(id: string, checked: boolean): void {
+ const checkbox = document.getElementById(id) as HTMLInputElement;
+ if (checkbox) {
+ checkbox.checked = checked;
+ }
+ }
+
+ private getCheckboxValue(id: string): boolean {
+ const checkbox = document.getElementById(id) as HTMLInputElement;
+ return checkbox ? checkbox.checked : false;
+ }
+}