Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions progs/csqc.src
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ chat.qc
view_model.qc
particles.qc
chasecam.qc
zombie.qc
main.qc
#endlist
19 changes: 1 addition & 18 deletions source/client/defs/custom.qc
Original file line number Diff line number Diff line change
Expand Up @@ -172,21 +172,4 @@ float hide_viewmodel;
// csqc player prediction by eukara
vector playerOrigin;
vector playerOriginOld;
vector playerVelocity;

//
// List of Zombie limb meshes for custom skinning
//
string zombie_skins[] =
{
"models/ai/zb%.mdl",
"models/ai/zbc%.mdl",
"models/ai/zcfull.mdl",
"models/ai/zhc^.mdl",
"models/ai/zalc(.mdl",
"models/ai/zarc(.mdl",
"models/ai/zfull.mdl",
"models/ai/zh^.mdl",
"models/ai/zal(.mdl",
"models/ai/zar(.mdl"
};
vector playerVelocity;
188 changes: 181 additions & 7 deletions source/client/main.qc
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,6 @@ void() ToggleMenu =
}
}

float(float isnew) SetZombieSkinning =
{
self.drawmask = MASK_ENGINE;
setcustomskin(self, __NULL__, sprintf("replace \"\" \"models/ai/zfull.mdl_%d.pcx\"\n", self.skin));
return PREDRAW_NEXT;
};

//
// GenerateAlphaTransparencyQ3Shaders()
// What a mouth-full! Anyway, NZ:P supports
Expand Down Expand Up @@ -720,6 +713,180 @@ void() Camera_SniperSway =

float gamepad_enabled;

vector cam_snap_difference;
float pressed_zoom_in;

#define CAMERA_AIM_SNAP_MAX_DIST 1024 // hard cutoff (quake units)

// score weighting (higher DOT_WEIGHT = more center preference)
#define CAMERA_AIM_SNAP_DOT_WEIGHT 2.0
#define CAMERA_AIM_SNAP_DIST_WEIGHT 1.0

#define CAMERA_AIM_SNAP_NEAR_DIST 128
#define CAMERA_AIM_SNAP_FAR_DIST 1024

#define CAMERA_AIM_SNAP_NEAR_DOT 0.980 // yaw cone, near (looser)
#define CAMERA_AIM_SNAP_FAR_DOT 0.9998 // yaw cone, far (tighter)

#define CAMERA_AIM_SNAP_NEAR_PITCH 13 // near = allow more vertical slop
#define CAMERA_AIM_SNAP_FAR_PITCH 8 // far = be stricter

float Camera_InFront(vector ent1_origin, vector ent2_origin, vector client_view_angles)
{
vector to3d = ent2_origin - ent1_origin;

// Distance for scaling (use horizontal distance so stairs/height don't distort t)
vector to2d = to3d;
to2d[2] = 0;

float dist = vlen(to2d);
if (dist < 1)
return true;

// normalize distance t in [0..1]
float t = (dist - CAMERA_AIM_SNAP_NEAR_DIST)
/ (CAMERA_AIM_SNAP_FAR_DIST - CAMERA_AIM_SNAP_NEAR_DIST);
if (t < 0) t = 0;
if (t > 1) t = 1;

vector yaw_angles = client_view_angles;
yaw_angles[0] = 0;
yaw_angles[2] = 0;
makevectors(yaw_angles);

vector dir2d = normalize(to2d);
float dotp = dotproduct(dir2d, v_forward);

float yaw_threshold = CAMERA_AIM_SNAP_NEAR_DOT
+ t * (CAMERA_AIM_SNAP_FAR_DOT - CAMERA_AIM_SNAP_NEAR_DOT);

if (dotp <= yaw_threshold)
return false;

vector dir3d = normalize(to3d);
vector targ_ang = vectoangles(dir3d);
targ_ang[0] *= -1;

float pitch_tol = CAMERA_AIM_SNAP_NEAR_PITCH
+ t * (CAMERA_AIM_SNAP_FAR_PITCH - CAMERA_AIM_SNAP_NEAR_PITCH);

float dpitch = fabs(angledelta(targ_ang[0] - client_view_angles[0]));
if (dpitch > pitch_tol)
return false;

return true;
}

float Camera_YawDot(vector from_origin, vector to_origin, vector client_view_angles)
{
vector to = to_origin - from_origin;
to[2] = 0;

if (vlen(to) < 1)
return 1;

vector dir = normalize(to);

vector yaw_angles = client_view_angles;
yaw_angles[0] = 0;
yaw_angles[2] = 0;
makevectors(yaw_angles);

return dotproduct(dir, v_forward);
}

void(vector client_view_angles) Camera_AimSnap =
{
float zoom_value = getstatf(STAT_WEAPONZOOM);

if (!(gamepad_enabled && cvar("in_aimassist") == 1))
return;

if (zoom_value == 1 || zoom_value == 2) {
if (pressed_zoom_in == false) {
pressed_zoom_in = true;
} else {
return;
}
} else {
pressed_zoom_in = false;
return;
}

//print("snap start\n");

entity best_ai = world;
float best_score = -999999;
vector best_target_origin = [0, 0, 0];

vector client_origin = getproperty(VF_ORIGIN);

// Iterate over all of our AI meshes.
for (int i = 0; i < zombie_snap_struct.length; i++) {
float model_index = getmodelindex(zombie_snap_struct[i].model_path);

entity snap_ai = findfloat(world, modelindex, model_index);
while (snap_ai != world) {

// use the same target point for gating + snapping
vector snap_offset = (getstatf(STAT_PERKS) & P_DEAD) ? zombie_snap_struct[i].deadshot_snap_offset : zombie_snap_struct[i].snap_offset;
vector target_origin = snap_ai.origin + snap_offset;

// Hard max distance cutoff
float dist = vlen(target_origin - client_origin);
if (dist > CAMERA_AIM_SNAP_MAX_DIST) {
snap_ai = findfloat(snap_ai, modelindex, model_index);
continue;
}

// Gate: in-front + pitch sanity
if (Camera_InFront(client_origin, target_origin, client_view_angles)) {

// LOS must be clear
traceline(target_origin, client_origin, true, world);
if (trace_fraction >= 1) {
float dotp = Camera_YawDot(client_origin, target_origin, client_view_angles);

float dist01 = 1 - (dist / CAMERA_AIM_SNAP_MAX_DIST);
if (dist01 < 0) dist01 = 0;

// prefer centered, then closeness
float score = (dotp * CAMERA_AIM_SNAP_DOT_WEIGHT)
+ (dist01 * CAMERA_AIM_SNAP_DIST_WEIGHT);

// Pick best
if (score > best_score) {
best_score = score;
best_ai = snap_ai;
best_target_origin = target_origin;
//print(sprintf("best score=%f dot=%f dist=%f org=[%v]\n", score, dotp, dist, target_origin));
}
}
}

snap_ai = findfloat(snap_ai, modelindex, model_index);
}
}

if (best_ai != world) {
vector distance_vector = best_target_origin - client_origin;
distance_vector = normalize(distance_vector);

vector distance_angles = vectoangles(distance_vector);
distance_angles[0] += (distance_angles[0] > 180) ? -360 : 0;
distance_angles[0] *= -1;

if (distance_angles[0] < -70 || distance_angles[0] > 80)
return;

cam_snap_difference = distance_angles - client_view_angles;
//print(sprintf("cam_snap_difference: [%v]\n", cam_snap_difference));
}

//print("snap end\n");
};


// CALLED EVERY CLIENT RENDER FRAME
float pap_flash_alternate;
noref void(float width, float height, float menushown) CSQC_UpdateView =
Expand Down Expand Up @@ -799,6 +966,12 @@ noref void(float width, float height, float menushown) CSQC_UpdateView =
camang[1] += sniper_sway[1];
camang[2] += sniper_sway[2];

Camera_AimSnap(camang);

camang[0] += cam_snap_difference[0];
camang[1] += cam_snap_difference[1];
camang[2] += cam_snap_difference[2];

setviewprop(VF_ANGLES, camang);

if (cvar("chase_active")) {
Expand Down Expand Up @@ -1020,6 +1193,7 @@ noref void() CSQC_Input_Frame =
{
input_angles += gun_kick;
input_angles += sniper_sway;
input_angles += cam_snap_difference;
}

#define DEG2RAD(x) (x * M_PI / 180.f)
Expand Down
75 changes: 75 additions & 0 deletions source/client/zombie.qc
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
client/zombie.qc

Client-side functions for AI (Zombies).

Copyright (C) 2021-2025 NZ:P Team

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to:

Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA

*/

//
// List of Zombie limb meshes for custom skinning
//
string zombie_skins[] =
{
"models/ai/zb%.mdl",
"models/ai/zbc%.mdl",
"models/ai/zcfull.mdl",
"models/ai/zhc^.mdl",
"models/ai/zalc(.mdl",
"models/ai/zarc(.mdl",
"models/ai/zfull.mdl",
"models/ai/zh^.mdl",
"models/ai/zal(.mdl",
"models/ai/zar(.mdl"
};

//
// List of AI Models to snap to for Aim Snapping
// and the offset distance from ent origin (if necessary).
//
var struct
{
string model_path;
vector snap_offset;
vector deadshot_snap_offset;
} zombie_snap_struct[] =
{
{"models/ai/zfull.mdl", [0, 0, 18], [0, 0, 35]},
{"models/ai/zb%.mdl", [0, 0, 18], [0, 0, 35]},
{"models/ai/zcfull.mdl", [0, 0, 0], [0, 0, 0]},
{"models/ai/zbc%.mdl", [0, 0, 0], [0, 0, 0]},
{"models/ai/dog.mdl", [0, 0, 0], [0, 0, 0]},
};

float(float isnew) SetZombieSkinning =
{
string model_path = modelnameforindex(self.modelindex);
self.drawmask = MASK_ENGINE;
setcustomskin(self, __NULL__, sprintf("replace \"\" \"models/ai/zfull.mdl_%d.pcx\"\n", self.skin));

if (isnew) {
//print(sprintf("adding a new zombie ent: [%s] [%f]\n", model_path, time));
addentity(self);
}

return PREDRAW_NEXT;
};
2 changes: 1 addition & 1 deletion source/menu/menu_gpad.qc
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ void() Menu_Gamepad =
}
Menu_DrawOptionValue(2, rumble_string);

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