Skip to content

Commit 7228827

Browse files
committed
CLIENT: Add support for gamepad Aim Snapping
1 parent dfcae80 commit 7228827

5 files changed

Lines changed: 259 additions & 26 deletions

File tree

progs/csqc.src

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@ chat.qc
3232
view_model.qc
3333
particles.qc
3434
chasecam.qc
35+
zombie.qc
3536
main.qc
3637
#endlist

source/client/defs/custom.qc

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -172,21 +172,4 @@ float hide_viewmodel;
172172
// csqc player prediction by eukara
173173
vector playerOrigin;
174174
vector playerOriginOld;
175-
vector playerVelocity;
176-
177-
//
178-
// List of Zombie limb meshes for custom skinning
179-
//
180-
string zombie_skins[] =
181-
{
182-
"models/ai/zb%.mdl",
183-
"models/ai/zbc%.mdl",
184-
"models/ai/zcfull.mdl",
185-
"models/ai/zhc^.mdl",
186-
"models/ai/zalc(.mdl",
187-
"models/ai/zarc(.mdl",
188-
"models/ai/zfull.mdl",
189-
"models/ai/zh^.mdl",
190-
"models/ai/zal(.mdl",
191-
"models/ai/zar(.mdl"
192-
};
175+
vector playerVelocity;

source/client/main.qc

Lines changed: 181 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,6 @@ void() ToggleMenu =
4747
}
4848
}
4949

50-
float(float isnew) SetZombieSkinning =
51-
{
52-
self.drawmask = MASK_ENGINE;
53-
setcustomskin(self, __NULL__, sprintf("replace \"\" \"models/ai/zfull.mdl_%d.pcx\"\n", self.skin));
54-
return PREDRAW_NEXT;
55-
};
56-
5750
//
5851
// GenerateAlphaTransparencyQ3Shaders()
5952
// What a mouth-full! Anyway, NZ:P supports
@@ -720,6 +713,180 @@ void() Camera_SniperSway =
720713

721714
float gamepad_enabled;
722715

716+
vector cam_snap_difference;
717+
float pressed_zoom_in;
718+
719+
#define CAMERA_AIM_SNAP_MAX_DIST 1024 // hard cutoff (quake units)
720+
721+
// score weighting (higher DOT_WEIGHT = more center preference)
722+
#define CAMERA_AIM_SNAP_DOT_WEIGHT 2.0
723+
#define CAMERA_AIM_SNAP_DIST_WEIGHT 1.0
724+
725+
#define CAMERA_AIM_SNAP_NEAR_DIST 128
726+
#define CAMERA_AIM_SNAP_FAR_DIST 1024
727+
728+
#define CAMERA_AIM_SNAP_NEAR_DOT 0.980 // yaw cone, near (looser)
729+
#define CAMERA_AIM_SNAP_FAR_DOT 0.9998 // yaw cone, far (tighter)
730+
731+
#define CAMERA_AIM_SNAP_NEAR_PITCH 13 // near = allow more vertical slop
732+
#define CAMERA_AIM_SNAP_FAR_PITCH 8 // far = be stricter
733+
734+
float Camera_InFront(vector ent1_origin, vector ent2_origin, vector client_view_angles)
735+
{
736+
vector to3d = ent2_origin - ent1_origin;
737+
738+
// Distance for scaling (use horizontal distance so stairs/height don't distort t)
739+
vector to2d = to3d;
740+
to2d[2] = 0;
741+
742+
float dist = vlen(to2d);
743+
if (dist < 1)
744+
return true;
745+
746+
// normalize distance t in [0..1]
747+
float t = (dist - CAMERA_AIM_SNAP_NEAR_DIST)
748+
/ (CAMERA_AIM_SNAP_FAR_DIST - CAMERA_AIM_SNAP_NEAR_DIST);
749+
if (t < 0) t = 0;
750+
if (t > 1) t = 1;
751+
752+
vector yaw_angles = client_view_angles;
753+
yaw_angles[0] = 0;
754+
yaw_angles[2] = 0;
755+
makevectors(yaw_angles);
756+
757+
vector dir2d = normalize(to2d);
758+
float dotp = dotproduct(dir2d, v_forward);
759+
760+
float yaw_threshold = CAMERA_AIM_SNAP_NEAR_DOT
761+
+ t * (CAMERA_AIM_SNAP_FAR_DOT - CAMERA_AIM_SNAP_NEAR_DOT);
762+
763+
if (dotp <= yaw_threshold)
764+
return false;
765+
766+
vector dir3d = normalize(to3d);
767+
vector targ_ang = vectoangles(dir3d);
768+
targ_ang[0] *= -1;
769+
770+
float pitch_tol = CAMERA_AIM_SNAP_NEAR_PITCH
771+
+ t * (CAMERA_AIM_SNAP_FAR_PITCH - CAMERA_AIM_SNAP_NEAR_PITCH);
772+
773+
float dpitch = fabs(angledelta(targ_ang[0] - client_view_angles[0]));
774+
if (dpitch > pitch_tol)
775+
return false;
776+
777+
return true;
778+
}
779+
780+
float Camera_YawDot(vector from_origin, vector to_origin, vector client_view_angles)
781+
{
782+
vector to = to_origin - from_origin;
783+
to[2] = 0;
784+
785+
if (vlen(to) < 1)
786+
return 1;
787+
788+
vector dir = normalize(to);
789+
790+
vector yaw_angles = client_view_angles;
791+
yaw_angles[0] = 0;
792+
yaw_angles[2] = 0;
793+
makevectors(yaw_angles);
794+
795+
return dotproduct(dir, v_forward);
796+
}
797+
798+
void(vector client_view_angles) Camera_AimSnap =
799+
{
800+
float zoom_value = getstatf(STAT_WEAPONZOOM);
801+
802+
if (!(gamepad_enabled && cvar("in_aimassist") == 1))
803+
return;
804+
805+
if (zoom_value == 1 || zoom_value == 2) {
806+
if (pressed_zoom_in == false) {
807+
pressed_zoom_in = true;
808+
} else {
809+
return;
810+
}
811+
} else {
812+
pressed_zoom_in = false;
813+
return;
814+
}
815+
816+
//print("snap start\n");
817+
818+
entity best_ai = world;
819+
float best_score = -999999;
820+
vector best_target_origin = [0, 0, 0];
821+
822+
vector client_origin = getproperty(VF_ORIGIN);
823+
824+
// Iterate over all of our AI meshes.
825+
for (int i = 0; i < zombie_snap_struct.length; i++) {
826+
float model_index = getmodelindex(zombie_snap_struct[i].model_path);
827+
828+
entity snap_ai = findfloat(world, modelindex, model_index);
829+
while (snap_ai != world) {
830+
831+
// use the same target point for gating + snapping
832+
vector snap_offset = (getstatf(STAT_PERKS) & P_DEAD) ? zombie_snap_struct[i].deadshot_snap_offset : zombie_snap_struct[i].snap_offset;
833+
vector target_origin = snap_ai.origin + snap_offset;
834+
835+
// Hard max distance cutoff
836+
float dist = vlen(target_origin - client_origin);
837+
if (dist > CAMERA_AIM_SNAP_MAX_DIST) {
838+
snap_ai = findfloat(snap_ai, modelindex, model_index);
839+
continue;
840+
}
841+
842+
// Gate: in-front + pitch sanity
843+
if (Camera_InFront(client_origin, target_origin, client_view_angles)) {
844+
845+
// LOS must be clear
846+
traceline(target_origin, client_origin, true, world);
847+
if (trace_fraction >= 1) {
848+
float dotp = Camera_YawDot(client_origin, target_origin, client_view_angles);
849+
850+
float dist01 = 1 - (dist / CAMERA_AIM_SNAP_MAX_DIST);
851+
if (dist01 < 0) dist01 = 0;
852+
853+
// prefer centered, then closeness
854+
float score = (dotp * CAMERA_AIM_SNAP_DOT_WEIGHT)
855+
+ (dist01 * CAMERA_AIM_SNAP_DIST_WEIGHT);
856+
857+
// Pick best
858+
if (score > best_score) {
859+
best_score = score;
860+
best_ai = snap_ai;
861+
best_target_origin = target_origin;
862+
//print(sprintf("best score=%f dot=%f dist=%f org=[%v]\n", score, dotp, dist, target_origin));
863+
}
864+
}
865+
}
866+
867+
snap_ai = findfloat(snap_ai, modelindex, model_index);
868+
}
869+
}
870+
871+
if (best_ai != world) {
872+
vector distance_vector = best_target_origin - client_origin;
873+
distance_vector = normalize(distance_vector);
874+
875+
vector distance_angles = vectoangles(distance_vector);
876+
distance_angles[0] += (distance_angles[0] > 180) ? -360 : 0;
877+
distance_angles[0] *= -1;
878+
879+
if (distance_angles[0] < -70 || distance_angles[0] > 80)
880+
return;
881+
882+
cam_snap_difference = distance_angles - client_view_angles;
883+
//print(sprintf("cam_snap_difference: [%v]\n", cam_snap_difference));
884+
}
885+
886+
//print("snap end\n");
887+
};
888+
889+
723890
// CALLED EVERY CLIENT RENDER FRAME
724891
float pap_flash_alternate;
725892
noref void(float width, float height, float menushown) CSQC_UpdateView =
@@ -799,6 +966,12 @@ noref void(float width, float height, float menushown) CSQC_UpdateView =
799966
camang[1] += sniper_sway[1];
800967
camang[2] += sniper_sway[2];
801968

969+
Camera_AimSnap(camang);
970+
971+
camang[0] += cam_snap_difference[0];
972+
camang[1] += cam_snap_difference[1];
973+
camang[2] += cam_snap_difference[2];
974+
802975
setviewprop(VF_ANGLES, camang);
803976

804977
if (cvar("chase_active")) {
@@ -1020,6 +1193,7 @@ noref void() CSQC_Input_Frame =
10201193
{
10211194
input_angles += gun_kick;
10221195
input_angles += sniper_sway;
1196+
input_angles += cam_snap_difference;
10231197
}
10241198

10251199
#define DEG2RAD(x) (x * M_PI / 180.f)

source/client/zombie.qc

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
client/zombie.qc
3+
4+
Client-side functions for AI (Zombies).
5+
6+
Copyright (C) 2021-2025 NZ:P Team
7+
8+
This program is free software; you can redistribute it and/or
9+
modify it under the terms of the GNU General Public License
10+
as published by the Free Software Foundation; either version 2
11+
of the License, or (at your option) any later version.
12+
13+
This program is distributed in the hope that it will be useful,
14+
but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16+
17+
See the GNU General Public License for more details.
18+
19+
You should have received a copy of the GNU General Public License
20+
along with this program; if not, write to:
21+
22+
Free Software Foundation, Inc.
23+
59 Temple Place - Suite 330
24+
Boston, MA 02111-1307, USA
25+
26+
*/
27+
28+
//
29+
// List of Zombie limb meshes for custom skinning
30+
//
31+
string zombie_skins[] =
32+
{
33+
"models/ai/zb%.mdl",
34+
"models/ai/zbc%.mdl",
35+
"models/ai/zcfull.mdl",
36+
"models/ai/zhc^.mdl",
37+
"models/ai/zalc(.mdl",
38+
"models/ai/zarc(.mdl",
39+
"models/ai/zfull.mdl",
40+
"models/ai/zh^.mdl",
41+
"models/ai/zal(.mdl",
42+
"models/ai/zar(.mdl"
43+
};
44+
45+
//
46+
// List of AI Models to snap to for Aim Snapping
47+
// and the offset distance from ent origin (if necessary).
48+
//
49+
var struct
50+
{
51+
string model_path;
52+
vector snap_offset;
53+
vector deadshot_snap_offset;
54+
} zombie_snap_struct[] =
55+
{
56+
{"models/ai/zfull.mdl", [0, 0, 18], [0, 0, 35]},
57+
{"models/ai/zb%.mdl", [0, 0, 18], [0, 0, 35]},
58+
{"models/ai/zcfull.mdl", [0, 0, 0], [0, 0, 0]},
59+
{"models/ai/zbc%.mdl", [0, 0, 0], [0, 0, 0]},
60+
{"models/ai/dog.mdl", [0, 0, 0], [0, 0, 0]},
61+
};
62+
63+
float(float isnew) SetZombieSkinning =
64+
{
65+
string model_path = modelnameforindex(self.modelindex);
66+
self.drawmask = MASK_ENGINE;
67+
setcustomskin(self, __NULL__, sprintf("replace \"\" \"models/ai/zfull.mdl_%d.pcx\"\n", self.skin));
68+
69+
if (isnew) {
70+
//print(sprintf("adding a new zombie ent: [%s] [%f]\n", model_path, time));
71+
addentity(self);
72+
}
73+
74+
return PREDRAW_NEXT;
75+
};

source/menu/menu_gpad.qc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ void() Menu_Gamepad =
111111
}
112112
Menu_DrawOptionValue(2, rumble_string);
113113

114-
Menu_Button(3, "gp_aima", "AIM ASSIST", "Toggle Camera Aim Assist.") ? Menu_Gamepad_ApplyAimAssist() : 0;
114+
Menu_Button(3, "gp_aima", "AIM ASSIST", "Toggle Aim Sensitivity+Snapping Assist.") ? Menu_Gamepad_ApplyAimAssist() : 0;
115115
string aa_string = "";
116116
switch(cvar("in_aimassist")) {
117117
case 0: aa_string = "DISABLED"; break;

0 commit comments

Comments
 (0)