diff --git a/assets/pakQ3VR/ui/controls3.menu b/assets/pakQ3VR/ui/controls3.menu index 5d178d6413..09c4f6defd 100644 --- a/assets/pakQ3VR/ui/controls3.menu +++ b/assets/pakQ3VR/ui/controls3.menu @@ -209,6 +209,36 @@ itemDef { action { uiScript update "vr_controlSchema" } } + itemDef { + name controls3 + group grpControls3 + type ITEM_TYPE_YESNO + text "Weapon Adjustment:" + cvar "vr_weaponAdjust" + rect 99 350 256 20 + textalign ITEM_ALIGN_RIGHT + textalignx 128 + textaligny 20 + textscale .333 + forecolor 1 1 1 1 + visible 1 + mouseEnter { show message_weaponadjust } + mouseExit { hide message_weaponadjust } + } + + itemDef { + name message_weaponadjust + text "Hold both grips for 1s to enter weapon adjustment mode" + rect 99 375 256 20 + textalign ITEM_ALIGN_CENTER + textalignx 128 + textaligny 14 + textscale .25 + forecolor 1 .75 0 1 + visible 0 + decoration + } + itemDef { name fadebox style WINDOW_STYLE_FILLED diff --git a/assets/pakQ3VR/ui/ingame_controls.menu b/assets/pakQ3VR/ui/ingame_controls.menu index 97a314d508..a67014f938 100644 --- a/assets/pakQ3VR/ui/ingame_controls.menu +++ b/assets/pakQ3VR/ui/ingame_controls.menu @@ -8,7 +8,7 @@ menuDef { visible 0 fullscreen 0 outOfBoundsClick // this closes the window if it gets a click out of the rectangle - rect 125 30 370 375 + rect 125 30 370 407 focusColor 1 .75 0 1 style 1 border 1 @@ -16,7 +16,7 @@ menuDef { itemDef { name window - rect 10 15 360 360 + rect 10 15 360 392 style 1 backcolor 0 .1 0 1 visible 1 @@ -69,7 +69,7 @@ itemDef { } itemDef { name window - rect 0 314 64 64 + rect 0 346 64 64 style 3 background "ui/assets/ingameleftcornerb.tga" visible 1 @@ -77,7 +77,7 @@ itemDef { } itemDef { name window - rect 317 314 64 64 + rect 317 346 64 64 style 3 background "ui/assets/ingamerightcornerb.tga" visible 1 @@ -105,7 +105,7 @@ itemDef { itemDef { name window - rect 0 232 16 84 + rect 0 232 16 116 style 3 background "ui/assets/ingameleft.tga" visible 1 @@ -132,7 +132,7 @@ itemDef { } itemDef { name window - rect 365 232 16 84 + rect 365 232 16 116 style 3 background "ui/assets/ingameright.tga" visible 1 @@ -142,7 +142,7 @@ itemDef { itemDef { name window - rect 64 370 144 8 + rect 64 402 144 8 style 3 background "ui/assets/ingamebottom.tga" visible 1 @@ -150,7 +150,7 @@ itemDef { } itemDef { name window - rect 208 370 144 8 + rect 208 402 144 8 style 3 background "ui/assets/ingamebottom.tga" visible 1 @@ -325,18 +325,48 @@ itemDef { textalign ITEM_ALIGN_RIGHT textalignx 143 textaligny 17 - textscale .25 + textscale .25 forecolor 1 1 1 1 visible 1 action { uiScript update "vr_controlSchema" } } + itemDef { + name controls + group grpControls + type ITEM_TYPE_YESNO + text "Weapon Adjustment:" + cvar "vr_weaponAdjust" + rect 30 218 200 20 + textalign ITEM_ALIGN_RIGHT + textalignx 143 + textaligny 17 + textscale .25 + forecolor 1 1 1 1 + visible 1 + mouseEnter { show message_weaponadjust } + mouseExit { hide message_weaponadjust } + } + + itemDef { + name message_weaponadjust + text "Hold both grips for 1s to adjust" + rect 10 235 360 14 + textalign ITEM_ALIGN_CENTER + textalignx 180 + textaligny 12 + textscale .2 + forecolor 1 .75 0 1 + visible 0 + decoration + } + itemDef { name controls group grpControls style 1 text "Comfort Options" - rect 100 231 100 20 + rect 100 247 100 20 textalign ITEM_ALIGN_RIGHT textalignx 143 textaligny 17 @@ -351,7 +381,7 @@ itemDef { type ITEM_TYPE_SLIDER text "Comfort Vignette:" cvarfloat "vr_comfortVignette" 0.2 0 1 - rect 30 251 200 20 + rect 30 267 200 20 textalign ITEM_ALIGN_RIGHT textalignx 143 textaligny 17 @@ -365,7 +395,7 @@ itemDef { type ITEM_TYPE_SLIDER text "Height Adjust:" cvarfloat "vr_heightAdjust" 0.2 0 1 - rect 30 271 200 20 + rect 30 287 200 20 textalign ITEM_ALIGN_RIGHT textalignx 143 textaligny 17 @@ -379,7 +409,7 @@ itemDef { type ITEM_TYPE_YESNO text "Roll When Hit:" cvar "vr_rollWhenHit" - rect 30 291 200 20 + rect 30 307 200 20 textalign ITEM_ALIGN_RIGHT textalignx 143 textaligny 17 @@ -393,7 +423,7 @@ itemDef { type ITEM_TYPE_SLIDER text "Haptic Intensity:" cvarfloat "vr_hapticIntensity" 0.2 0 1 - rect 30 311 200 20 + rect 30 327 200 20 textalign ITEM_ALIGN_RIGHT textalignx 143 textaligny 17 @@ -407,7 +437,7 @@ itemDef { type ITEM_TYPE_SLIDER text "HUD Depth:" cvarfloat "vr_hudDepth" 1 0 5 - rect 30 331 200 20 + rect 30 347 200 20 textalign ITEM_ALIGN_RIGHT textalignx 143 textaligny 17 @@ -421,7 +451,7 @@ itemDef { type ITEM_TYPE_SLIDER text "HUD Vertical Position:" cvarfloat "vr_hudYOffset" 20 -200 200 - rect 30 351 200 20 + rect 30 367 200 20 textalign ITEM_ALIGN_RIGHT textalignx 143 textaligny 17 diff --git a/code/cgame/cg_consolecmds.c b/code/cgame/cg_consolecmds.c index 4ee4d62f04..983fadc0f7 100644 --- a/code/cgame/cg_consolecmds.c +++ b/code/cgame/cg_consolecmds.c @@ -478,6 +478,9 @@ static consoleCommand_t commands[] = { { "tell_target", CG_TellTarget_f }, { "tell_attacker", CG_TellAttacker_f }, { "weapon_select", CG_WeaponSelectorSelect_f }, + { "weapon_adjust", CG_WeaponAdjust_f }, + { "weapon_adjust_reset", CG_WeaponAdjustReset_f }, + { "weapon_adjust_reset_all", CG_WeaponAdjustResetAll_f }, #ifdef MISSIONPACK { "vtell_target", CG_VoiceTellTarget_f }, { "vtell_attacker", CG_VoiceTellAttacker_f }, diff --git a/code/cgame/cg_draw.c b/code/cgame/cg_draw.c index c9dee92c5b..420d3ba948 100644 --- a/code/cgame/cg_draw.c +++ b/code/cgame/cg_draw.c @@ -3136,6 +3136,9 @@ static void CG_DrawScreen2D(void) CG_DrawWeapReticle(); } } + + // Weapon adjustment overlay (drawn regardless of team/health since we auto-exit on those) + CG_WeaponAdjustDraw(); } diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index 6bb5946861..cce38b6968 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -1343,6 +1343,7 @@ void CG_RemoveHUDFlags(int flags); void CG_AdjustFrom640( float *x, float *y, float *w, float *h ); void CG_FillRect( float x, float y, float width, float height, const float *color ); void CG_DrawPic( float x, float y, float width, float height, qhandle_t hShader ); +void CG_DrawChar( int x, int y, int width, int height, int ch ); #define USE_NEW_FONT_RENDERER @@ -1505,6 +1506,15 @@ void CG_LaserSight( vec3_t start, vec3_t end, byte colour[4], float width ); void CG_OutOfAmmoChange( void ); // should this be in pmove? +// weapon adjustment mode +void CG_WeaponAdjust_f( void ); +void CG_WeaponAdjust_Enter( void ); +void CG_WeaponAdjust_Exit( void ); +void CG_WeaponAdjustReset_f( void ); +void CG_WeaponAdjustResetAll_f( void ); +void CG_WeaponAdjustFrame( void ); +void CG_WeaponAdjustDraw( void ); + // // cg_marks.c // diff --git a/code/cgame/cg_view.c b/code/cgame/cg_view.c index 85c9670bf4..ffb0d649e1 100644 --- a/code/cgame/cg_view.c +++ b/code/cgame/cg_view.c @@ -1458,6 +1458,9 @@ void CG_DrawActiveFrame( int serverTime, stereoFrame_t stereoView, qboolean demo CG_AddParticles (); CG_AddLocalEntities(); } + // Process weapon adjustment mode (reads thumbstick, updates cvars) + CG_WeaponAdjustFrame(); + CG_AddViewWeapon( &cg.predictedPlayerState ); // add buffered sounds diff --git a/code/cgame/cg_weapons.c b/code/cgame/cg_weapons.c index 9301fcb070..2bb2e51416 100644 --- a/code/cgame/cg_weapons.c +++ b/code/cgame/cg_weapons.c @@ -1735,7 +1735,7 @@ void CG_AddViewWeapon( playerState_t *ps ) { } #endif - if (vr->weapon_select) + if (vr->weapon_select && !vr->weapon_adjust) { CG_DrawWeaponSelector(); return; @@ -1861,6 +1861,304 @@ void CG_AddViewWeapon( playerState_t *ps ) { /* ============================================================================== +WEAPON ADJUSTMENT MODE + +============================================================================== +*/ + +#define WEAPADJUST_NUM_PARAMS 7 + +static const char *weaponAdjustParamNames[WEAPADJUST_NUM_PARAMS] = { + "scale", "right", "up", "fwd", "pitch", "yaw", "roll" +}; + +// Defaults matching vr_cvars.c registration +static const char *weaponAdjustDefaultStrings[] = { + "", // 0: WP_NONE + "1,-4.0,7,-10,-20,-15,0", // 1: Gauntlet + "0.8,-3.0,5.5,0,0,0,0", // 2: Machinegun + "0.8,-3.3,8,3.7,0,0,0", // 3: Shotgun + "0.75,-5.4,6.5,-4,0,0,0", // 4: Grenade Launcher + "0.8,-5.2,6,7.5,0,0,0", // 5: Rocket Launcher + "0.8,-3.3,6,7,0,0,0", // 6: Lightning Gun + "0.8,-5.5,6,0,0,0,0", // 7: Railgun + "0.8,-4.5,6,1.5,0,0,0", // 8: Plasma Gun + "0.8,-5.5,6,0,0,0,0", // 9: BFG + "", // 10: Grappling Hook + "0.8,-5.5,6,0,0,0,0", // 11: Nailgun (TA) + "0.8,-5.5,6,0,0,0,0", // 12: Prox Launcher (TA) + "0.8,-5.5,6,0,0,0,0", // 13: Chaingun (TA) +}; +#define WEAPADJUST_NUM_DEFAULTS (sizeof(weaponAdjustDefaultStrings) / sizeof(weaponAdjustDefaultStrings[0])) + +// Adjustment state (file-scoped statics — reset on level load since cgame reloads) +static int weaponAdjustParam = 0; // 0-6: which parameter is selected +static float weaponAdjustValues[WEAPADJUST_NUM_PARAMS]; +static float weaponAdjustDefaults[WEAPADJUST_NUM_PARAMS]; +static int weaponAdjustWeaponId = 0; // weapon we're currently adjusting +static qboolean weaponAdjustParamCycled = qfalse; // debounce for thumbstick param cycling +static int weaponAdjustResetHoldStart = 0; // time A button was first pressed (for hold-to-reset-all) + +static void CG_WeaponAdjust_ParseValues( const char *str, float *out ) { + out[0] = out[1] = out[2] = out[3] = out[4] = out[5] = out[6] = 0.0f; + if ( str && strlen(str) > 0 ) { + sscanf( str, "%f,%f,%f,%f,%f,%f,%f", + &out[0], &out[1], &out[2], &out[3], + &out[4], &out[5], &out[6] ); + } +} + +static void CG_WeaponAdjust_WriteCvar( int weaponId, float *vals ) { + char cvar_name[64]; + char value[256]; + Com_sprintf( cvar_name, sizeof(cvar_name), "vr_weapon_adjustment_%i", weaponId ); + Com_sprintf( value, sizeof(value), "%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f", + vals[0], vals[1], vals[2], vals[3], vals[4], vals[5], vals[6] ); + trap_Cvar_Set( cvar_name, value ); +} + +static void CG_WeaponAdjust_LoadWeapon( int weaponId ) { + char cvar_name[64]; + char weapon_adjustment[256]; + + weaponAdjustWeaponId = weaponId; + + // Load current values from cvar + Com_sprintf( cvar_name, sizeof(cvar_name), "vr_weapon_adjustment_%i", weaponId ); + trap_Cvar_VariableStringBuffer( cvar_name, weapon_adjustment, sizeof(weapon_adjustment) ); + CG_WeaponAdjust_ParseValues( weapon_adjustment, weaponAdjustValues ); + + // Load defaults + if ( weaponId >= 0 && weaponId < (int)WEAPADJUST_NUM_DEFAULTS ) { + CG_WeaponAdjust_ParseValues( weaponAdjustDefaultStrings[weaponId], weaponAdjustDefaults ); + } else { + memset( weaponAdjustDefaults, 0, sizeof(weaponAdjustDefaults) ); + } +} + +void CG_WeaponAdjust_Enter( void ) { + playerState_t *ps = &cg.snap->ps; + + // Dismiss weapon wheel and weapon stabilisation from the grip hold that got us here + vr->weapon_select = qfalse; + vr->weapon_stabilised = qfalse; + cg.weaponSelectorTime = 0; + + vr->weapon_adjust = qtrue; + weaponAdjustParam = 0; + weaponAdjustParamCycled = qfalse; + weaponAdjustResetHoldStart = 0; + + CG_WeaponAdjust_LoadWeapon( ps->weapon ); + CG_Printf( "Weapon adjustment mode: ON\n" ); +} + +void CG_WeaponAdjust_Exit( void ) { + vr->weapon_adjust = qfalse; + weaponAdjustResetHoldStart = 0; + CG_Printf( "Weapon adjustment mode: OFF\n" ); +} + +void CG_WeaponAdjust_f( void ) { + if ( vr->weapon_adjust ) { + CG_WeaponAdjust_Exit(); + } else { + if ( !trap_Cvar_VariableValue( "vr_weaponAdjust" ) ) { + return; + } + if ( !cg.snap ) return; + // Don't enter if in virtual screen, zoomed, spectator, or dead + if ( vr->virtual_screen || vr->weapon_zoomed || + cg.snap->ps.stats[STAT_HEALTH] <= 0 || + cg.snap->ps.persistant[PERS_TEAM] == TEAM_SPECTATOR ) { + CG_Printf( "Cannot enter weapon adjustment mode right now.\n" ); + return; + } + CG_WeaponAdjust_Enter(); + } +} + +void CG_WeaponAdjustReset_f( void ) { + if ( !vr->weapon_adjust ) return; + weaponAdjustValues[weaponAdjustParam] = weaponAdjustDefaults[weaponAdjustParam]; + CG_WeaponAdjust_WriteCvar( weaponAdjustWeaponId, weaponAdjustValues ); +} + +void CG_WeaponAdjustResetAll_f( void ) { + if ( !vr->weapon_adjust ) return; + for ( int i = 0; i < WEAPADJUST_NUM_PARAMS; i++ ) { + weaponAdjustValues[i] = weaponAdjustDefaults[i]; + } + CG_WeaponAdjust_WriteCvar( weaponAdjustWeaponId, weaponAdjustValues ); + const char *weaponName = "Unknown"; + if ( cg_weapons[weaponAdjustWeaponId].item ) { + weaponName = cg_weapons[weaponAdjustWeaponId].item->pickup_name; + } + CG_Printf( "Reset %s adjustments to defaults.\n", weaponName ); +} + +void CG_WeaponAdjustFrame( void ) { + if ( !vr->weapon_adjust || !cg.snap ) return; + + playerState_t *ps = &cg.snap->ps; + + // Auto-exit conditions + if ( vr->virtual_screen || vr->weapon_zoomed || + ps->stats[STAT_HEALTH] <= 0 || + ps->persistant[PERS_TEAM] == TEAM_SPECTATOR || + (ps->pm_flags & PMF_FOLLOW) ) { + CG_WeaponAdjust_Exit(); + return; + } + + // If weapon changed, reload values for new weapon + if ( ps->weapon != weaponAdjustWeaponId ) { + CG_WeaponAdjust_LoadWeapon( ps->weapon ); + } + + // Off-hand thumbstick left/right cycles params, weapon-hand thumbstick up/down adjusts value + int primaryThumb = vr->right_handed ? THUMB_RIGHT : THUMB_LEFT; + int offhandThumb = vr->right_handed ? THUMB_LEFT : THUMB_RIGHT; + + // --- Parameter cycling (off-hand thumbstick left/right) --- + float offhandX = vr->thumbstick_location[offhandThumb][0]; + if ( offhandX > 0.5f && !weaponAdjustParamCycled ) { + weaponAdjustParam = ( weaponAdjustParam + 1 ) % WEAPADJUST_NUM_PARAMS; + weaponAdjustParamCycled = qtrue; + } else if ( offhandX < -0.5f && !weaponAdjustParamCycled ) { + weaponAdjustParam = ( weaponAdjustParam + WEAPADJUST_NUM_PARAMS - 1 ) % WEAPADJUST_NUM_PARAMS; + weaponAdjustParamCycled = qtrue; + } else if ( offhandX > -0.3f && offhandX < 0.3f ) { + weaponAdjustParamCycled = qfalse; + } + + // --- Value adjustment (weapon-hand thumbstick up/down) --- + float primaryY = vr->thumbstick_location[primaryThumb][1]; + float deadzone = 0.15f; + float absY = fabs( primaryY ); + + if ( absY > deadzone ) { + float normalized = ( absY - deadzone ) / ( 1.0f - deadzone ); + // Cubic curve for fine control at small deflections + float curved = normalized * normalized * normalized; + float direction = primaryY > 0 ? 1.0f : -1.0f; + + // Per-parameter scale factors (per second, at full deflection) + float paramScale; + switch ( weaponAdjustParam ) { + case 0: paramScale = 0.25f; break; // scale + case 1: paramScale = 5.0f; break; // right + case 2: paramScale = 5.0f; break; // up + case 3: paramScale = 5.0f; break; // forward + case 4: paramScale = 22.0f; break; // pitch (degrees) + case 5: paramScale = 22.0f; break; // yaw (degrees) + case 6: paramScale = 22.0f; break; // roll (degrees) + default: paramScale = 1.0f; break; + } + + float deltaTime = cg.frametime / 1000.0f; + if ( deltaTime > 0.1f ) deltaTime = 0.1f; // clamp to prevent huge jumps + + float delta = direction * curved * paramScale * deltaTime; + weaponAdjustValues[weaponAdjustParam] += delta; + + // Write updated values to cvar immediately + CG_WeaponAdjust_WriteCvar( weaponAdjustWeaponId, weaponAdjustValues ); + } +} + +void CG_WeaponAdjustDraw( void ) { + if ( !vr->weapon_adjust ) return; + + // Colors + vec4_t bgColor = { 0.0f, 0.0f, 0.0f, 0.65f }; + vec4_t titleColor = { 1.0f, 1.0f, 1.0f, 1.0f }; + vec4_t activeColor = { 1.0f, 1.0f, 0.0f, 1.0f }; + vec4_t normalColor = { 0.7f, 0.7f, 0.7f, 1.0f }; + vec4_t changedColor = { 0.0f, 1.0f, 1.0f, 1.0f }; + vec4_t helpColor = { 0.5f, 0.5f, 0.5f, 1.0f }; + vec4_t activeBgColor = { 1.0f, 1.0f, 0.0f, 0.15f }; + + int charW = TINYCHAR_WIDTH; + int charH = TINYCHAR_HEIGHT; + int lineH = charH + 1; + + // Horizontal layout across the top — keeps the bottom clear for weapon view + // Rows: title | arrow-up | name | value | arrow-down + int colChars = 6; // each column is 6 characters wide + int colW = colChars * charW; // pixel width per column + int colPad = 2; // pixels between columns + float boxW = (float)(colW * WEAPADJUST_NUM_PARAMS + colPad * (WEAPADJUST_NUM_PARAMS - 1) + 8); + float boxX = (640 - boxW) / 2; // center horizontally + float boxY = 110; + float boxH = (float)(lineH * 4 + 14); // title + arrow + name + value + arrow + int textY = (int)boxY + 4; + + // Background + CG_FillRect( boxX, boxY, boxW, boxH, bgColor ); + + // Title: weapon name + const char *weaponName = "Unknown"; + if ( weaponAdjustWeaponId > 0 && weaponAdjustWeaponId < WP_NUM_WEAPONS ) { + CG_RegisterWeapon( weaponAdjustWeaponId ); + if ( cg_weapons[weaponAdjustWeaponId].item ) { + weaponName = cg_weapons[weaponAdjustWeaponId].item->pickup_name; + } + } + CG_DrawStringExt( (int)boxX + 4, textY, va("ADJUST: %s", weaponName), titleColor, + qtrue, qfalse, charW, charH, 0 ); + + // Help text on the right side of the title row + CG_DrawStringExt( (int)(boxX + boxW) - 18 * charW, textY, "A:Accept B:Default", + helpColor, qtrue, qfalse, charW, charH, 0 ); + textY += lineH + 2; + + // Parameter rows: up-arrow | name | value | down-arrow + int paramStartY = textY; + int nameY = paramStartY + lineH; // name row below up-arrow + int valY = nameY + lineH; // value row below name + + for ( int i = 0; i < WEAPADJUST_NUM_PARAMS; i++ ) { + qboolean isActive = ( weaponAdjustParam == i ); + qboolean isChanged = ( weaponAdjustValues[i] != weaponAdjustDefaults[i] ); + vec4_t *color = isActive ? &activeColor : ( isChanged ? &changedColor : &normalColor ); + int colX = (int)boxX + 4 + i * ( colW + colPad ); + + // Highlight background for active parameter — spans from arrow row to bottom of box + if ( isActive ) { + float highlightTop = (float)paramStartY - 1; + float highlightBot = boxY + boxH; + CG_FillRect( (float)colX - 1, highlightTop, + (float)colW + 2, highlightBot - highlightTop, activeBgColor ); + } + + // Up/down triangle arrows on active column (charset row 8: col 7=▲, col 6=▼) + if ( isActive ) { + int arrowX = colX + ( colW - charW ) / 2; + trap_R_SetColor( activeColor ); + CG_DrawChar( arrowX, paramStartY, charW, charH, 135 ); + CG_DrawChar( arrowX, valY + lineH, charW, charH, 134 ); + trap_R_SetColor( NULL ); + } + + // Parameter name — right-align within column + int nameLen = (int)strlen( weaponAdjustParamNames[i] ); + int nameX = colX + colW - nameLen * charW; + CG_DrawStringExt( nameX, nameY, weaponAdjustParamNames[i], + *color, qtrue, qfalse, charW, charH, 0 ); + + // Parameter value — right-align within column + const char *valStr = va( "%.2f", weaponAdjustValues[i] ); + int valLen = (int)strlen( valStr ); + int valX = colX + colW - valLen * charW; + CG_DrawStringExt( valX, valY, valStr, + *color, qtrue, qfalse, charW, charH, 0 ); + } +} + +/* +============================================================================== + WEAPON SELECTION ============================================================================== diff --git a/code/q3_ui/ui_vr.c b/code/q3_ui/ui_vr.c index a54f4481e2..ce10d4f24b 100644 --- a/code/q3_ui/ui_vr.c +++ b/code/q3_ui/ui_vr.c @@ -44,7 +44,8 @@ VR OPTIONS MENU #define ID_SHOWOFFHAND 152 #define ID_DRAWHUD 153 #define ID_SELECTORWITHHUD 154 -#define ID_BACK 155 +#define ID_WEAPONADJUST 155 +#define ID_BACK 156 typedef struct { @@ -59,6 +60,7 @@ typedef struct { menuradiobutton_s showoffhand; menulist_s drawhud; menuradiobutton_s selectorwithhud; + menuradiobutton_s weaponadjust; menubitmap_s back; } vr_t; @@ -72,9 +74,14 @@ static void VR_SetMenuItems( void ) { s_vr.showoffhand.curvalue = trap_Cvar_VariableValue( "vr_showOffhand" ) != 0; s_vr.drawhud.curvalue = trap_Cvar_VariableValue( "vr_hudDrawStatus" ); s_vr.selectorwithhud.curvalue = trap_Cvar_VariableValue( "vr_weaponSelectorWithHud" ) != 0; + s_vr.weaponadjust.curvalue = trap_Cvar_VariableValue( "vr_weaponAdjust" ) != 0; } +static void VR_WeaponAdjustStatusBar( void *self ) { + UI_DrawString( SCREEN_WIDTH * 0.50, SCREEN_HEIGHT * 0.80, "Hold both grips for 1s to enter weapon adjustment mode", UI_SMALLFONT|UI_CENTER, colorWhite ); +} + static void VR_MenuEvent( void* ptr, int notification ) { if( notification != QM_ACTIVATED ) { return; @@ -102,6 +109,10 @@ static void VR_MenuEvent( void* ptr, int notification ) { trap_Cvar_SetValue( "vr_weaponSelectorWithHud", s_vr.selectorwithhud.curvalue); break; + case ID_WEAPONADJUST: + trap_Cvar_SetValue( "vr_weaponAdjust", s_vr.weaponadjust.curvalue); + break; + case ID_BACK: UI_PopMenu(); break; @@ -162,7 +173,7 @@ static void VR_MenuInit( void ) { s_vr.framer.width = 256; s_vr.framer.height = 334; - y = 198; + y = 176; s_vr.virtualscreenmode.generic.type = MTYPE_SPINCONTROL; s_vr.virtualscreenmode.generic.x = VR_X_POS; s_vr.virtualscreenmode.generic.y = y; @@ -212,6 +223,16 @@ static void VR_MenuInit( void ) { s_vr.selectorwithhud.generic.x = VR_X_POS; s_vr.selectorwithhud.generic.y = y; + y += BIGCHAR_HEIGHT+2; + s_vr.weaponadjust.generic.type = MTYPE_RADIOBUTTON; + s_vr.weaponadjust.generic.name = "Weapon Adjustment:"; + s_vr.weaponadjust.generic.flags = QMF_PULSEIFFOCUS|QMF_SMALLFONT; + s_vr.weaponadjust.generic.callback = VR_MenuEvent; + s_vr.weaponadjust.generic.id = ID_WEAPONADJUST; + s_vr.weaponadjust.generic.x = VR_X_POS; + s_vr.weaponadjust.generic.y = y; + s_vr.weaponadjust.generic.statusbar = VR_WeaponAdjustStatusBar; + s_vr.back.generic.type = MTYPE_BITMAP; s_vr.back.generic.name = ART_BACK0; s_vr.back.generic.flags = QMF_LEFT_JUSTIFY|QMF_PULSEIFFOCUS; @@ -232,6 +253,7 @@ static void VR_MenuInit( void ) { Menu_AddItem( &s_vr.menu, &s_vr.showoffhand ); Menu_AddItem( &s_vr.menu, &s_vr.drawhud ); Menu_AddItem( &s_vr.menu, &s_vr.selectorwithhud ); + Menu_AddItem( &s_vr.menu, &s_vr.weaponadjust ); Menu_AddItem( &s_vr.menu, &s_vr.back ); diff --git a/code/vr/vr_clientinfo.h b/code/vr/vr_clientinfo.h index f52de7c7ab..39912e750e 100644 --- a/code/vr/vr_clientinfo.h +++ b/code/vr/vr_clientinfo.h @@ -56,6 +56,7 @@ typedef struct { qboolean weapon_select; qboolean weapon_select_autoclose; qboolean weapon_select_using_thumbstick; + qboolean weapon_adjust; qboolean no_crosshair; int realign; // used to realign the fake 6DoF playspace in a multiplayer game diff --git a/code/vr/vr_cvars.c b/code/vr/vr_cvars.c index 705960d152..9c002781dc 100644 --- a/code/vr/vr_cvars.c +++ b/code/vr/vr_cvars.c @@ -41,6 +41,7 @@ cvar_t *vr_desktopMenuStyle = NULL; cvar_t *vr_desktopMode = NULL; cvar_t *vr_virtualScreenMode = NULL; cvar_t *vr_virtualScreenShape = NULL; +cvar_t *vr_weaponAdjust = NULL; cvar_t *vr_thumbstickDeadzone = NULL; cvar_t *vr_thumbstickFullDeflection = NULL; @@ -88,6 +89,7 @@ void VR_InitCvars( void ) vr_desktopMode = Cvar_Get ("vr_desktopMode", "1", CVAR_ARCHIVE | CVAR_LATCH); // 0 - off, 1 - on vr_virtualScreenMode = Cvar_Get ("vr_virtualScreenMode", "0", CVAR_ARCHIVE); // 0 - fixed, 1 - follow vr_virtualScreenShape = Cvar_Get ("vr_virtualScreenShape", "0", CVAR_ARCHIVE); // 0 - curved, 1 - flat + vr_weaponAdjust = Cvar_Get ("vr_weaponAdjust", "0", CVAR_ARCHIVE); // 0 - disabled, 1 - enabled vr_thumbstickDeadzone = Cvar_Get ("vr_thumbstickDeadzone", "0.15", CVAR_ARCHIVE); vr_thumbstickFullDeflection = Cvar_Get ("vr_thumbstickFullDeflection", "0.85", CVAR_ARCHIVE); diff --git a/code/vr/vr_input.c b/code/vr/vr_input.c index 106d782202..a697c43f92 100644 --- a/code/vr/vr_input.c +++ b/code/vr/vr_input.c @@ -81,6 +81,11 @@ static qboolean wasInMenuMode = qfalse; static float triggerPressedThreshold = 0.75f; static float triggerReleasedThreshold = 0.5f; +// Weapon adjustment mode: dual-grip hold detection +static int dualGripHoldStartTime = 0; +static qboolean dualGripWasActive = qfalse; +static int weaponAdjustBHoldStart = 0; // B button hold tracking for reset-all + // Apply analog curve to thumbstick input // Smoothly ramps from dead zone to maximum value (same approach as SDL gamepad) static float IN_ApplyThumbstickCurve(float value, float threshold) @@ -118,6 +123,7 @@ extern cvar_t *vr_weaponScope; extern cvar_t *vr_hapticIntensity; extern cvar_t *vr_thumbstickDeadzone; extern cvar_t *vr_thumbstickFullDeflection; +extern cvar_t *vr_weaponAdjust; extern cvar_t *vr_weaponSelectorMode; qboolean alt_key_mode_active = qfalse; @@ -1180,6 +1186,13 @@ static void IN_VRJoystick( qboolean isRightController, float joystickX, float jo vr.thumbstick_location[isRightController][0] = joystickX; vr.thumbstick_location[isRightController][1] = joystickY; + // Weapon adjustment mode: suppress normal joystick processing. + // Thumbstick values are already stored above for cgame to read. + if (vr.weapon_adjust) + { + return; + } + // Apply analog curve to joystick input for smoother control float curvedX = IN_ApplyThumbstickCurve(joystickX, vr_thumbstickDeadzone->value); float curvedY = IN_ApplyThumbstickCurve(joystickY, vr_thumbstickDeadzone->value); @@ -1339,6 +1352,12 @@ static void IN_VRTriggers( qboolean isRightController, float triggerValue ) { vrController_t* controller = isRightController == qtrue ? &rightController : &leftController; + // Weapon adjustment mode: suppress all trigger actions + if (vr.weapon_adjust) + { + return; + } + // Detect if we're in menu mode (virtual screen, intermission, or scoreboard) qboolean inMenuMode = (vr.virtual_screen && (!vr.first_person_following || vr.in_menu)) || cl.snap.ps.pm_type == PM_INTERMISSION || @@ -1417,6 +1436,72 @@ static void IN_VRButtons( qboolean isRightController, uint32_t buttons ) { vrController_t* controller = isRightController == qtrue ? &rightController : &leftController; + // Weapon adjustment mode: suppress most buttons, handle A (reset) and B (exit) + if (vr.weapon_adjust) + { + // Still allow menu button + if ((buttons & VR_Button_Enter) && !IN_InputActivated(&controller->buttons, VR_Button_Enter)) + { + IN_ActivateInput(&controller->buttons, VR_Button_Enter); + Com_QueueEvent(in_vrEventTime, SE_KEY, K_ESCAPE, qtrue, 0, NULL); + } + else if (!(buttons & VR_Button_Enter) && IN_InputActivated(&controller->buttons, VR_Button_Enter)) + { + IN_DeactivateInput(&controller->buttons, VR_Button_Enter); + Com_QueueEvent(in_vrEventTime, SE_KEY, K_ESCAPE, qfalse, 0, NULL); + } + + // A button: accept and exit adjustment mode + if ((buttons & VR_Button_A) && !IN_InputActivated(&controller->buttons, VR_Button_A)) + { + IN_ActivateInput(&controller->buttons, VR_Button_A); + Cbuf_AddText("weapon_adjust\n"); + } + else if (!(buttons & VR_Button_A)) + { + IN_DeactivateInput(&controller->buttons, VR_Button_A); + } + + // B button: reset to default (tap = single param, hold 2s = all params) + if (buttons & VR_Button_B) + { + if (!IN_InputActivated(&controller->buttons, VR_Button_B)) + { + IN_ActivateInput(&controller->buttons, VR_Button_B); + weaponAdjustBHoldStart = in_vrEventTime; + } + else if (weaponAdjustBHoldStart > 0 && + (in_vrEventTime - weaponAdjustBHoldStart) > 2000) + { + Cbuf_AddText("weapon_adjust_reset_all\n"); + VR_Vibrate(200, 3, 0.8f); + weaponAdjustBHoldStart = 0; // prevent repeated triggers + } + } + else + { + if (IN_InputActivated(&controller->buttons, VR_Button_B)) + { + IN_DeactivateInput(&controller->buttons, VR_Button_B); + // If released before 2s, treat as single-param default reset + if (weaponAdjustBHoldStart > 0) + { + Cbuf_AddText("weapon_adjust_reset\n"); + VR_Vibrate(50, 3, 0.4f); + } + weaponAdjustBHoldStart = 0; + } + } + + // Release any held grip/other button states to prevent stuck actions + if (!(buttons & VR_Button_GripTrigger)) + { + IN_DeactivateInput(&controller->buttons, VR_Button_GripTrigger); + } + + return; + } + // Menu button if ((buttons & VR_Button_Enter) && !IN_InputActivated(&controller->buttons, VR_Button_Enter)) { @@ -1662,6 +1747,33 @@ void VR_ProcessInputActions( void ) if (GetActionStateBoolean(thumbrestRightTouchAction).currentState) rButtons |= VR_Button_Thumbrest; IN_VRButtons(qtrue, rButtons); + // Dual-grip hold detection for weapon adjustment mode activation + if (vr_weaponAdjust->integer) + { + qboolean bothGripsHeld = (lButtons & VR_Button_GripTrigger) && (rButtons & VR_Button_GripTrigger); + if (bothGripsHeld) + { + if (!dualGripWasActive) + { + dualGripHoldStartTime = in_vrEventTime; + dualGripWasActive = qtrue; + } + else if (dualGripHoldStartTime > 0 && + (in_vrEventTime - dualGripHoldStartTime) > 1000) + { + // Toggle weapon adjustment mode + Cbuf_AddText("weapon_adjust\n"); + VR_Vibrate(150, 3, 0.5f); + dualGripHoldStartTime = 0; // prevent repeated triggers while still held + } + } + else + { + dualGripWasActive = qfalse; + dualGripHoldStartTime = 0; + } + } + //index finger click XrActionStateBoolean indexState; indexState = GetActionStateBoolean(indexLeftAction);