CLIENT: Add FTE skel defs

SERVER: Add test IQM ledge-climb traversal
This commit is contained in:
blubs 2023-08-29 22:43:54 -07:00
parent e3dcb2aa13
commit 11004c96b5
3 changed files with 688 additions and 629 deletions

View file

@ -1179,10 +1179,28 @@ float(float modlindex, optional float useabstransforms) skel_create = #263; /* P
Allocates a new uninitiaised skeletal object, with enough bone info to animate the given model. Allocates a new uninitiaised skeletal object, with enough bone info to animate the given model.
eg: self.skeletonobject = skel_create(self.modelindex); */ eg: self.skeletonobject = skel_create(self.modelindex); */
typedef struct {
int sourcemodelindex; /*frame data will be imported from this model, bones must be compatible*/
int reserved;
int firstbone;
int lastbone;
float prescale; /*0 destroys existing data, 1 retains it*/
float scale[4]; /*you'll need to do lerpfrac manually*/
int animation[4];
float animationtime[4];
/*halflife models*/
float subblend[2];
float controllers[5];
} skelblend_t;
float(float skel, entity ent, float modelindex, float retainfrac, float firstbone, float lastbone, optional float addfrac) skel_build = #264; /* Part of FTE_CSQC_SKELETONOBJECTS float(float skel, entity ent, float modelindex, float retainfrac, float firstbone, float lastbone, optional float addfrac) skel_build = #264; /* Part of FTE_CSQC_SKELETONOBJECTS
Animation data (according to the entity's frame info) is pulled from the specified model and blended into the specified skeletal object. Animation data (according to the entity's frame info) is pulled from the specified model and blended into the specified skeletal object.
If retainfrac is set to 0 on the first call and 1 on the others, you can blend multiple animations together according to the addfrac value. The final weight should be 1. Other values will result in scaling and/or other weirdness. You can use firstbone and lastbone to update only part of the skeletal object, to allow legs to animate separately from torso, use 0 for both arguments to specify all, as bones are 1-based. */ If retainfrac is set to 0 on the first call and 1 on the others, you can blend multiple animations together according to the addfrac value. The final weight should be 1. Other values will result in scaling and/or other weirdness. You can use firstbone and lastbone to update only part of the skeletal object, to allow legs to animate separately from torso, use 0 for both arguments to specify all, as bones are 1-based. */
float(float skel, int numblends, skelblend_t *weights, int structsize) skel_build_ptr = #0:skel_build_ptr; /*
Like skel_build, but slightly simpler. */
float(float skel) skel_get_numbones = #265; /* Part of FTE_CSQC_SKELETONOBJECTS float(float skel) skel_get_numbones = #265; /* Part of FTE_CSQC_SKELETONOBJECTS
Retrives the number of bones in the model. The valid range is 1<=bone<=numbones. */ Retrives the number of bones in the model. The valid range is 1<=bone<=numbones. */
@ -1627,6 +1645,10 @@ void(entity e, string skinfilename, optional string skindata) setcustomskin = #3
The texture is determined to be sufficient to hold the first named image, additional images can be named as extra tokens on the same line. The texture is determined to be sufficient to hold the first named image, additional images can be named as extra tokens on the same line.
Use a + at the end of the line to continue reading image tokens from the next line also, the named shader must use 'map $diffuse' to read the composed texture (compatible with the defaultskin shader). */ Use a + at the end of the line to continue reading image tokens from the next line also, the named shader must use 'map $diffuse' to read the composed texture (compatible with the defaultskin shader). */
float(string skinfilename, optional string skindata) loadcustomskin = #377;
void(entity e, float skinobj) applycustomskin = #378;
void(float skinobj) releasecustomskin = #379;
__variant*(int size) memalloc = #384; /* Part of FTE_MEMALLOC __variant*(int size) memalloc = #384; /* Part of FTE_MEMALLOC
Allocate an arbitary block of memory */ Allocate an arbitary block of memory */

View file

@ -509,10 +509,6 @@ float() zombie_predraw = {
// Due to QC's limit of 8-args per vararg func, split this up into // Due to QC's limit of 8-args per vararg func, split this up into
// a few different sprintf calls. // a few different sprintf calls.
string skin_str = "";
skin_str = sprintf("%sgeomset %d %d\n", skin_str, ZOMBIE_IQM_GEOMSET_BODY, )
string skin_str = ""; string skin_str = "";
// Add in the geomset defs // Add in the geomset defs
skin_str = sprintf("%sgeomset %d %d\n", skin_str, ZOMBIE_IQM_GEOMSET_BODY, 0); skin_str = sprintf("%sgeomset %d %d\n", skin_str, ZOMBIE_IQM_GEOMSET_BODY, 0);
@ -522,7 +518,7 @@ float() zombie_predraw = {
skin_str = sprintf("%sgeomset %d %d\n", skin_str, ZOMBIE_IQM_GEOMSET_LEG_L, self.limbs_state & ZOMBIE_LIMB_STATE_LEG_L == 0); skin_str = sprintf("%sgeomset %d %d\n", skin_str, ZOMBIE_IQM_GEOMSET_LEG_L, self.limbs_state & ZOMBIE_LIMB_STATE_LEG_L == 0);
skin_str = sprintf("%sgeomset %d %d\n", skin_str, ZOMBIE_IQM_GEOMSET_LEG_R, self.limbs_state & ZOMBIE_LIMB_STATE_LEG_R == 0); skin_str = sprintf("%sgeomset %d %d\n", skin_str, ZOMBIE_IQM_GEOMSET_LEG_R, self.limbs_state & ZOMBIE_LIMB_STATE_LEG_R == 0);
skin_str = sprintf("%sgeomset %d %d\n", skin_str, ZOMBIE_IQM_GEOMSET_ARM_R, self.limbs_state & ZOMBIE_LIMB_STATE_ARM_R == 0); skin_str = sprintf("%sgeomset %d %d\n", skin_str, ZOMBIE_IQM_GEOMSET_ARM_R, self.limbs_state & ZOMBIE_LIMB_STATE_ARM_R == 0);
// Add in the shader defs // Add in the per-mesh shader defs
skin_str = sprintf("%sreplace ZombieBody %S\n", skin_str, shader_name); skin_str = sprintf("%sreplace ZombieBody %S\n", skin_str, shader_name);
skin_str = sprintf("%sreplace ZombieHead %S\n", skin_str, shader_name); skin_str = sprintf("%sreplace ZombieHead %S\n", skin_str, shader_name);
skin_str = sprintf("%sreplace ZombieArmL %S\n", skin_str, shader_name); skin_str = sprintf("%sreplace ZombieArmL %S\n", skin_str, shader_name);

View file

@ -13,6 +13,8 @@ float zombie_anim_walk2_modelindex;
float zombie_anim_walk3_modelindex; float zombie_anim_walk3_modelindex;
float zombie_anim_jog1_modelindex; float zombie_anim_jog1_modelindex;
float zombie_anim_run1_modelindex; float zombie_anim_run1_modelindex;
float zombie_anim_climbledge_modelindex;
#define AI_STATE_PATHING 0 #define AI_STATE_PATHING 0
#define AI_STATE_TRAVERSING 1 #define AI_STATE_TRAVERSING 1
@ -59,7 +61,6 @@ class AI_Chase : entity {
float substate; float substate;
// ----------------------------- // -----------------------------
// ----------------------------- // -----------------------------
// Traversal state vars // Traversal state vars
// ----------------------------- // -----------------------------
@ -69,12 +70,10 @@ class AI_Chase : entity {
// Think callback executed every logic tick, independent of current animation // Think callback executed every logic tick, independent of current animation
virtual void() think_callback = SUB_Null; virtual void() think_callback = SUB_Null;
// We also have a frame callback that's called each time a new animation frame is reached // // We also have a frame callback that's called each time a new animation frame is reached
virtual void() frame_callback = SUB_Null; // virtual void() frame_callback = SUB_Null;
float think_delta_time; // Time (in seconds) between entity's ".think()" invocations float think_delta_time; // Time (in seconds) between entity's ".think()" invocations
// ------------------------------ // ------------------------------
// Animation variables // Animation variables
// ------------------------------ // ------------------------------
@ -85,27 +84,21 @@ class AI_Chase : entity {
float cur_anim_last_time; // Time into the animation at which the last `think` was invoked float cur_anim_last_time; // Time into the animation at which the last `think` was invoked
float cur_anim_playback_speed; // 1.0 = Normal speed, 2.0 = Twice as fast, 0.5 = Half-speed, etc. float cur_anim_playback_speed; // 1.0 = Normal speed, 2.0 = Twice as fast, 0.5 = Half-speed, etc.
// float cur_anim_move_dist; // float cur_anim_move_dist;
// Cached derived vars:
float cur_anim_duration; // Length (in seconds) of current animation at 1.0 playback speed
// --- Next / Queued animation vars --- // --- Next / Queued animation vars ---
float next_anim_model_idx; float next_anim_model_idx;
float next_anim_framegroup; float next_anim_framegroup;
float next_anim_playback_speed; float next_anim_playback_speed;
float pathfind_result_idx; // TODO - Need to increment this on instantiation float pathfind_result_idx; // TODO - Need to increment this on instantiation
float pathfind_cur_point_idx; float pathfind_cur_point_idx;
// TODO - Move this to AI_Zombie subclass? // TODO - Move this to AI_Zombie subclass?
float limbs_state; float limbs_state;
// Constructor. Called when calling `spawn(AI_Chase);`
// virtual void() AI_Chase;
// void() AI_Chase::think = {
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Animation movement integration fields / code // Animation movement integration fields / code
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -118,7 +111,357 @@ class AI_Chase : entity {
// Returns the fraction of an event that overlaps with the saerch region // Returns the fraction of an event that overlaps with the saerch region
// The search region is defined over: [this.cur_anim_move_region_start_time, this.cur_anim_move_region_end_time] // The search region is defined over: [this.cur_anim_move_region_start_time, this.cur_anim_move_region_end_time]
virtual float(float event_start_time, float event_end_time) anim_move_get_event_intersection = { virtual float(float event_start_time, float event_end_time) anim_move_get_event_intersection;
// Callback for built-in `processmodelevents` to integrate all animation movement between two timestamps
virtual void(float event_timestamp, int event_code, string event_data) anim_move_accumulate_iqm_event_movement;
virtual float() calc_anim_movement_speed;
// ------------------------------------------------------------------------
virtual void (float move_speed) do_walk_to_goal;
virtual float(entity to, float sendflags) send_entity_func;
// Constructor. Called when calling `spawn(AI_Chase);`
virtual void() AI_Chase;
// void() take_damage = {
// // TODO - Update limb states, mark as needing networking
// }
virtual void(float timestamp, int event_code, string event_data) handle_iqm_event;
// Stop what you're doing and play this animation immediately.
virtual void(float anim_model_idx, float anim_framegroup) play_anim;
// Queue up another animation to play when the current one finishes
virtual void(float anim_model_idx, float anim_framegroup) queue_anim;
// The "logic tick" of the entity.
virtual void() think;
// virtual void () fg_die = {};
// virtual void () fg_walk = {};
// virtual void () fg_attack = {};
// virtual void () fg_idle = {};
// virtual void (float dist) do_walk_to_goal;
};
void zombie_traversal_logic() {
// AI_Zombie zombie_ent = (AI_Zombie) self;
AI_Chase zombie_ent = (AI_Chase) self;
float traversal_idx = zombie_ent.cur_traversal_idx;
vector start_pos = sv_navmesh_traversals[traversal_idx].start_pos;
vector midpoint_pos;
vector end_pos = sv_navmesh_get_traversal_end_pos(traversal_idx);
string traversal_type;
traversal_type = "ledge";
// traversal_type = "leap";
// traversal_type = "jump_gap";
// traversal_type = "hop_barricade";
// traversal_type = "hop_fence";
// traversal_type = "window";
// traversal_type = "teleport";
// Jump up logic
if(traversal_type == "ledge") {
// Adjust zombie angle to its smallest representation
zombie_ent.angles.y = ((zombie_ent.angles.y + 180) % 360) - 180;
// Apply smallest delta angle
float delta_angle = sv_navmesh_traversals[traversal_idx].angle - zombie_ent.angles.y;
delta_angle = ((delta_angle + 180) % 360) - 180;
zombie_ent.angles.y += 0.5 * delta_angle;
// Jump up traversal consists of the following substates:
// 0: Start traversal, play jump up anim
// 1: Wait for jump up anim to complete
// 2: Move zombie up to ledge
// 3: Play zombie get up animation
//
// Check height of ledge we're climbing:
float traversal_height = end_pos.z - start_pos.z;
float traversal_time;
float lerp_frac;
float anim_time;
vector ledge_pos;
// Fall down
if(traversal_height < 0 ) {
if(zombie_ent.substate == 0) {
zombie_ent.movetype = MOVETYPE_STEP;
// zombie_ent.play_anim(get_anim_frame_zombie_fall, get_anim_length_zombie_fall(), ANIM_STOP_TYPE_STOP);
// zombie_ent.queue_anim(get_anim_frame_zombie_fall_loop, get_anim_length_zombie_fall_loop(), ANIM_STOP_TYPE_LOOP);
traversal_time = min(-traversal_height * (0.35 / 100.0), 2.0);
zombie_ent.cur_traversal_start_time = time;
zombie_ent.cur_traversal_end_time = time + traversal_time;
zombie_ent.substate = 1;
}
else if(zombie_ent.substate == 1) {
lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
zombie_ent.origin = lerpVector(start_pos, end_pos, lerp_frac * lerp_frac);
if(lerp_frac >= 1.0) {
// zombie_ent.play_anim(get_anim_frame_zombie_land, get_anim_length_zombie_land(), ANIM_STOP_TYPE_NEXT_ANIM);
// zombie_ent.cur_anim_frametime = 0.05;
// zombie_ent.queue_anim(get_anim_frame_zombie_walk1, get_anim_length_zombie_walk1(), ANIM_STOP_TYPE_LOOP);
zombie_ent.state = AI_STATE_PATHING;
zombie_ent.movetype = MOVETYPE_WALK;
}
}
}
// Short ledge
// else if(traversal_height < 98) {
// makevectors([0, sv_navmesh_traversals[traversal_idx].angle, 0]);
// ledge_pos = end_pos - '0 0 72' - v_forward * 21;
// if(zombie_ent.substate == 0) {
// zombie_ent.movetype = MOVETYPE_STEP;
// // If short jump up, play short jump / short climb anim
// // zombie_ent.play_anim(get_anim_frame_zombie_jump_low, get_anim_length_zombie_jump_low(), ANIM_STOP_TYPE_STOP);
// anim_time = 1;
// // anim_time = (zombie_ent.cur_anim_length - 1) * zombie_ent.cur_anim_frametime;
// // zombie_ent.cur_traversal_end_time = time + anim_time - (2 * zombie_ent.cur_anim_frametime);
// zombie_ent.cur_traversal_end_time = time + anim_time;
// // zombie_ent.cur_traversal_end_time = 0;
// // Stash anim stop-time in this variable so we can tell when to proceed: (minus three frames)
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + 0.3;
// zombie_ent.substate = 1;
// }
// if(zombie_ent.substate == 1) {
// lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
// zombie_ent.origin = lerpVector(start_pos, ledge_pos, lerp_frac);
// if(lerp_frac >= 1) {
// zombie_ent.substate = 2;
// // If short jump up, play short climb anim
// // zombie_ent.play_anim(get_anim_frame_zombie_climb_low, get_anim_length_zombie_climb_low(), ANIM_STOP_TYPE_STOP);
// // anim_time = (zombie_ent.cur_anim_length - 1) * zombie_ent.cur_anim_frametime;
// anim_time = 1;
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + anim_time;
// }
// }
// else if(zombie_ent.substate == 2) {
// lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
// start_pos = end_pos - '0 0 72' - v_forward * 21;
// zombie_ent.origin = lerpVector(start_pos, end_pos, lerp_frac);
// if(lerp_frac >= 1.0) {
// zombie_ent.state = AI_STATE_PATHING;
// zombie_ent.movetype = MOVETYPE_WALK;
// // FIXME - Need a better way to revert to walking
// // zombie_ent.play_anim(get_anim_frame_zombie_walk1, get_anim_length_zombie_walk1(), ANIM_STOP_TYPE_LOOP);
// }
// }
// }
// Tall ledge
else {
makevectors([0, sv_navmesh_traversals[traversal_idx].angle, 0]);
// ledge_pos = end_pos - '0 0 98' - v_forward * 28;
ledge_pos = end_pos - '0 0 94' - v_forward * 28;
if(zombie_ent.substate == 0) {
zombie_ent.movetype = MOVETYPE_STEP;
zombie_ent.play_anim(zombie_anim_climbledge_modelindex, 0);
// Wait until jump animation is near completion to start moving zombie
zombie_ent.cur_traversal_end_time = time + zombie_ent.cur_anim_duration - 0.3;
zombie_ent.substate = 1;
// zombie_ent.cur_anim_get_frame_func = (float(float)) SUB_Null;
// Figure out how fast to move the zombie
// float traversal_length = vlen(end_pos - start_pos);
// zombie_ent.cur_traversal_start_time = time;
}
else if(zombie_ent.substate == 1) {
if(zombie_ent.cur_traversal_end_time <= time) {
// Zombie moves from bottom to top of ledge in fixed time:
traversal_time = 0.15; // seconds
// TODO - Should we determine how fast the zombie moves based on traversal distance?
// TODO Otherwise zombie will always jump up in 0.5 seconds regardless of ledge height
zombie_ent.cur_traversal_start_time = time;
zombie_ent.cur_traversal_end_time = time + traversal_time;
zombie_ent.substate = 2;
}
}
else if(zombie_ent.substate == 2) {
// Move zombie up to ledge
lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
setorigin(zombie_ent, lerpVector(start_pos, ledge_pos, lerp_frac));
// Once at ledge, play climb animation:
if(lerp_frac >= 1) {
zombie_ent.substate = 3;
zombie_ent.play_anim(zombie_anim_climbledge_modelindex, 1);
zombie_ent.cur_traversal_start_time = time;
// Wait until animation near completion to advance
zombie_ent.cur_traversal_end_time = time + (zombie_ent.cur_anim_duration - 0.2);
}
}
else if(zombie_ent.substate == 3) {
lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
setorigin(zombie_ent, lerpVector(ledge_pos, end_pos, lerp_frac));
if(lerp_frac >= 1.0) {
zombie_ent.state = AI_STATE_PATHING;
zombie_ent.movetype = MOVETYPE_WALK;
// FIXME - Need a better way to revert to walking
zombie_ent.play_anim(zombie_anim_walk1_modelindex, 0);
}
}
}
// Mark to send origin / angles to clients
zombie_ent.SendFlags |= 1;
return;
}
// TODO - Implement leap
// else if(traversal_type == "leap") {
// midpoint_pos = sv_navmesh_get_traversal_midpoint_pos(traversal_idx);
// // Adjust zombie angle to its smallest representation
// zombie_ent.angles.y = ((zombie_ent.angles.y + 180) % 360) - 180;
// // Apply smallest delta angle
// float delta_angle = sv_navmesh_traversals[traversal_idx].angle - zombie_ent.angles.y;
// delta_angle = ((delta_angle + 180) % 360) - 180;
// zombie_ent.angles.y += 0.5 * delta_angle;
// // Leap traversal consists of the following substates:
// // 0: Play leap animation (frames 218-233)
// // 1: Wait for frame 233
// // 2: Move zombie across arc to end pos, ends at frame 241
// // 3: Wait at endpos for frame 247
// // TODO - Adjust traversal speed?
// // TODO - Break up leap start / leap-mid-air / leap land anims?
// // float traversal_length =
// float lerp_frac;
// if(zombie_ent.substate == 0) {
// zombie_ent.movetype = MOVETYPE_STEP;
// // zombie_ent.play_anim(get_anim_frame_zombie_leap_jump, get_anim_length_zombie_leap_jump(), ANIM_STOP_TYPE_STOP);
// zombie_ent.substate = 1;
// // Advance to next substate (1->2) after 5 frames
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + (5 * zombie_ent.cur_anim_frametime);
// }
// else if(zombie_ent.substate == 1) {
// if(zombie_ent.cur_traversal_end_time <= time) {
// zombie_ent.substate = 2;
// // Advance to next substate (2->3) after 8 frames
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + (8 * zombie_ent.cur_anim_frametime);
// }
// }
// else if(zombie_ent.substate == 2) {
// lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
// zombie_ent.origin = lerp_vector_bezier(start_pos, midpoint_pos, end_pos, lerp_frac);
// if(lerp_frac >= 1) {
// zombie_ent.substate = 3;
// // zombie_ent.play_anim(get_anim_frame_zombie_leap_land, get_anim_length_zombie_leap_land(), ANIM_STOP_TYPE_STOP);
// float anim_time = (zombie_ent.cur_anim_length - 1) * zombie_ent.cur_anim_frametime;
// // Finish traversal at the end of the land animation
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + anim_time;
// }
// }
// else if(zombie_ent.substate == 3) {
// if(zombie_ent.cur_traversal_end_time <= time) {
// zombie_ent.state = AI_STATE_PATHING;
// zombie_ent.movetype = MOVETYPE_WALK;
// // FIXME - Need a better way to revert to walking
// // zombie_ent.play_anim(get_anim_frame_zombie_walk1, get_anim_length_zombie_walk1(), ANIM_STOP_TYPE_LOOP);
// }
// }
// return;
// }
// // Starting traversal
// if(zombie_ent.substate == 0) {
// zombie_ent.velocity = '0 0 0';
// // zombie_ent.cur_anim_get_frame_func = (float(float)) SUB_Null;
// // Start zombie animation:
// // zombie_ent.play_anim(get_anim_frame_zombie_window_hop, get_anim_length_zombie_window_hop(), ANIM_STOP_TYPE_STOP);
// // zombie_ent.play_anim(get_anim_frame_zombie_jump_climb, get_anim_length_zombie_jump_climb(), ANIM_STOP_TYPE_STOP);
// // zombie_ent.cur_anim_get_frame_func = (float(float)) SUB_Null;
// // Figure out how fast to move the zombie
// float traversal_length = vlen(end_pos - start_pos);
// float anim_time = (zombie_ent.cur_anim_length - 1) * zombie_ent.cur_anim_frametime;
// zombie_ent.cur_traversal_start_time = time;
// // FIXME - Some traversals will have a different way of getting speed...
// zombie_ent.cur_traversal_end_time = time + anim_time;
// zombie_ent.substate = 1;
// zombie_ent.movetype = MOVETYPE_STEP;
// zombie_ent.angles.y = sv_navmesh_traversals[traversal_idx].angle;
// }
// // Moving zombie across traversal
// if(zombie_ent.substate == 1) {
// float lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
// if(lerp_frac > 1.0) {
// zombie_ent.state = AI_STATE_PATHING;
// zombie_ent.movetype = MOVETYPE_WALK;
// // TODO - How to tell zombie to play walk anim again?
// // FIXME - This ain't right
// zombie_ent.play_anim(get_anim_frame_zombie_walk1, get_anim_length_zombie_walk1(), ANIM_STOP_TYPE_LOOP);
// }
// // If the traversal uses the midpoint, lerp across midpoint
// if(sv_navmesh_traversals[zombie_ent.cur_traversal_idx].use_midpoint) {
// print("Current lerpfrac: ", ftos(lerp_frac), "\n");
// midpoint_pos = sv_navmesh_get_traversal_midpoint_pos(traversal_idx);
// // Lerp from start to midpoint
// if(lerp_frac < 0.5) {
// zombie_ent.origin = lerpVector(start_pos, midpoint_pos, lerp_frac * 2.0);
// }
// // Lerp from midpoint to end
// else {
// zombie_ent.origin = lerpVector(midpoint_pos, end_pos, (lerp_frac - 0.5) * 2.0);
// }
// }
// // Otherwise, lerp from start to end
// else {
// zombie_ent.origin = lerpVector(start_pos, end_pos, lerp_frac);
// }
// // TODO - Moving
// // vector start_pos = sv_navmesh_traversals[traversal_idx].start_pos;
// // vector end_pos = sv_navmesh_get_traversal_end_pos(traversal_idx);
// }
}
// Returns the fraction of an event that overlaps with the saerch region
// The search region is defined over: [this.cur_anim_move_region_start_time, this.cur_anim_move_region_end_time]
float(float event_start_time, float event_end_time) AI_Chase::anim_move_get_event_intersection {
if(event_end_time < this.cur_anim_move_region_start_time) { if(event_end_time < this.cur_anim_move_region_start_time) {
return 0; return 0;
} }
@ -134,13 +477,12 @@ class AI_Chase : entity {
float overlap_end_lerp_frac = (overlap_end - event_start_time) / (event_end_time - event_start_time); float overlap_end_lerp_frac = (overlap_end - event_start_time) / (event_end_time - event_start_time);
float overlap = overlap_end_lerp_frac - overlap_start_lerp_frac; float overlap = overlap_end_lerp_frac - overlap_start_lerp_frac;
return overlap; return overlap;
}; };
// Callback for built-in `processmodelevents` to integrate all animation movement between two timestamps // Callback for built-in `processmodelevents` to integrate all animation movement between two timestamps
virtual void(float event_timestamp, int event_code, string event_data) anim_move_accumulate_iqm_event_movement = { void(float event_timestamp, int event_code, string event_data) AI_Chase::anim_move_accumulate_iqm_event_movement {
if(event_code == IQM_EVENT_MOVE_SPEED) { if(event_code == IQM_EVENT_MOVE_SPEED) {
float event_dist = stof(event_data); float event_dist = stof(event_data);
float anim_duration = frameduration(this.cur_anim_model_idx, this.cur_anim_framegroup);
float anim_looped = true; // FIXME - Assumes all anims are looped. Is there any way to tell whether an animation is looped? float anim_looped = true; // FIXME - Assumes all anims are looped. Is there any way to tell whether an animation is looped?
float last_event_start_time = this.cur_anim_move_last_event_start; float last_event_start_time = this.cur_anim_move_last_event_start;
@ -158,25 +500,24 @@ class AI_Chase : entity {
if(anim_looped == false) { if(anim_looped == false) {
break; break;
} }
last_event_start_time += anim_duration; last_event_start_time += this.cur_anim_duration;
last_event_end_time += anim_duration; last_event_end_time += this.cur_anim_duration;
} }
// Store current event as last event // Store current event as last event
this.cur_anim_move_last_event_dist = event_dist; this.cur_anim_move_last_event_dist = event_dist;
this.cur_anim_move_last_event_start = event_timestamp; this.cur_anim_move_last_event_start = event_timestamp;
} }
}; };
virtual float() calc_anim_movement_speed { float() AI_Chase::calc_anim_movement_speed {
float walk_dist = 0; float walk_dist = 0;
// Given current animation state, read through the IQM events to determine exactly how much to move at this logic tick // Given current animation state, read through the IQM events to determine exactly how much to move at this logic tick
// float prev_time = time - ent.think_delta_time; // float prev_time = time - ent.think_delta_time;
float anim_duration = frameduration(this.cur_anim_model_idx, this.cur_anim_framegroup); // float anim_last_frametime = this.cur_anim_last_time % this.cur_anim_duration;
// float anim_last_frametime = this.cur_anim_last_time % anim_duration; // float anim_cur_frametime = ((time - this.cur_anim_start_time) * this.cur_anim_playback_speed) % this.cur_anim_duration;
// float anim_cur_frametime = ((time - this.cur_anim_start_time) * this.cur_anim_playback_speed) % anim_duration;
float anim_last_frametime = this.cur_anim_last_time; float anim_last_frametime = this.cur_anim_last_time;
float anim_cur_frametime = ((time - this.cur_anim_start_time) * this.cur_anim_playback_speed); float anim_cur_frametime = ((time - this.cur_anim_start_time) * this.cur_anim_playback_speed);
@ -190,17 +531,17 @@ class AI_Chase : entity {
// Wrap anim_last_frametime so it falls within [0, anim_duration] // Wrap anim_last_frametime so it falls within [0, anim_duration]
// Then make anim_cur_frametime relative to that (not wrapped) // Then make anim_cur_frametime relative to that (not wrapped)
this.cur_anim_move_region_end_time = anim_cur_frametime - anim_last_frametime; // First set it to delta this.cur_anim_move_region_end_time = anim_cur_frametime - anim_last_frametime; // First set it to delta
this.cur_anim_move_region_start_time = anim_last_frametime % anim_duration; // Wrap `last_frametime` this.cur_anim_move_region_start_time = anim_last_frametime % this.cur_anim_duration; // Wrap `last_frametime`
this.cur_anim_move_region_end_time += this.cur_anim_move_region_start_time; // Add `start_time` back in to get region end relative to region start this.cur_anim_move_region_end_time += this.cur_anim_move_region_start_time; // Add `start_time` back in to get region end relative to region start
// Search through the entire animation: // Search through the entire animation:
float region_start = 0; float region_start = 0;
float region_end = anim_duration + 1; float region_end = this.cur_anim_duration + 1;
// print("Accumulating moves over: (", ftos(region_start), ",", ftos(region_end), "), Search range: (", ftos(this.cur_anim_move_region_start_time)); // print("Accumulating moves over: (", ftos(region_start), ",", ftos(region_end), "), Search range: (", ftos(this.cur_anim_move_region_start_time));
// print(",", ftos(this.cur_anim_move_region_end_time),").\n"); // print(",", ftos(this.cur_anim_move_region_end_time),").\n");
processmodelevents(this.cur_anim_model_idx, this.cur_anim_framegroup, region_start, region_end, this.anim_move_accumulate_iqm_event_movement); processmodelevents(this.cur_anim_model_idx, this.cur_anim_framegroup, region_start, region_end, this.anim_move_accumulate_iqm_event_movement);
// Call one more time so the final event has a chance to be deposited // Call one more time so the final event has a chance to be deposited
this.anim_move_accumulate_iqm_event_movement(anim_duration, IQM_EVENT_MOVE_SPEED, "0"); this.anim_move_accumulate_iqm_event_movement(this.cur_anim_duration, IQM_EVENT_MOVE_SPEED, "0");
// print("\tFinal Result: ",ftos(this.cur_anim_move_dist),"\n"); // print("\tFinal Result: ",ftos(this.cur_anim_move_dist),"\n");
walk_dist = this.cur_anim_move_dist; walk_dist = this.cur_anim_move_dist;
@ -209,13 +550,13 @@ class AI_Chase : entity {
// To get speed, we simply divide by think_delta_time, no? // To get speed, we simply divide by think_delta_time, no?
float movement_speed = walk_dist / this.think_delta_time; float movement_speed = walk_dist / this.think_delta_time;
return movement_speed; return movement_speed;
}; };
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
virtual void (float move_speed) do_walk_to_goal = { void (float move_speed) AI_Chase::do_walk_to_goal {
if(move_speed == 0) { if(move_speed == 0) {
return; return;
} }
@ -248,10 +589,6 @@ class AI_Chase : entity {
this.state = AI_STATE_TRAVERSING; this.state = AI_STATE_TRAVERSING;
this.substate = 0; this.substate = 0;
this.cur_traversal_idx = res->point_path_traversals[this.pathfind_cur_point_idx]; this.cur_traversal_idx = res->point_path_traversals[this.pathfind_cur_point_idx];
print("Setting state to traversing.\n");
vector traversal_end_pos = sv_navmesh_get_traversal_end_pos(res->point_path_traversals[this.pathfind_cur_point_idx]);
print("Setting origin to end position: ", vtos(traversal_end_pos), "\n");
this.origin = traversal_end_pos;
} }
this.pathfind_cur_point_idx += 1; this.pathfind_cur_point_idx += 1;
goal_pos = res->point_path_points[this.pathfind_cur_point_idx]; goal_pos = res->point_path_points[this.pathfind_cur_point_idx];
@ -286,10 +623,10 @@ class AI_Chase : entity {
new_velocity_z = this.velocity_z; new_velocity_z = this.velocity_z;
this.velocity = new_velocity; this.velocity = new_velocity;
this.SendFlags |= 1; this.SendFlags |= 1;
}; };
virtual float(entity to, float sendflags) send_entity_func = { float(entity to, float sendflags) AI_Chase::send_entity_func {
WriteByte(MSG_ENTITY, ENT_TYPE_ZOMBIE); WriteByte(MSG_ENTITY, ENT_TYPE_ZOMBIE);
// Write bytflags indicating payload contents // Write bytflags indicating payload contents
WriteByte(MSG_ENTITY, sendflags); WriteByte(MSG_ENTITY, sendflags);
@ -332,12 +669,12 @@ class AI_Chase : entity {
} }
// TODO - Send limb state / texture info // TODO - Send limb state / texture info
return TRUE; return TRUE;
}; };
// Constructor. Called when calling `spawn(AI_Chase);` // Constructor. Called when calling `spawn(AI_Chase);`
virtual void() AI_Chase = { void() AI_Chase::AI_Chase {
this.path_target = world; this.path_target = world;
// this.path_pos = world.origin; // this.path_pos = world.origin;
this.cur_anim_model_idx = -1; this.cur_anim_model_idx = -1;
@ -348,13 +685,13 @@ class AI_Chase : entity {
// Should this be done in constructor? // Should this be done in constructor?
this.SendEntity = this.send_entity_func; this.SendEntity = this.send_entity_func;
this.cur_anim_playback_speed = 1; this.cur_anim_playback_speed = 1;
}; };
// void() take_damage = { // void() take_damage = {
// // TODO - Update limb states, mark as needing networking // // TODO - Update limb states, mark as needing networking
// } // }
virtual void(float timestamp, int event_code, string event_data) handle_iqm_event = { void(float timestamp, int event_code, string event_data) AI_Chase::handle_iqm_event {
switch(event_code) { switch(event_code) {
// case IQM_EVENT_MOVE_SPEED: // FRAME MOVEMENT DIST // case IQM_EVENT_MOVE_SPEED: // FRAME MOVEMENT DIST
// this.cur_anim_move_dist += stof(event_data); // this.cur_anim_move_dist += stof(event_data);
@ -366,29 +703,30 @@ class AI_Chase : entity {
default: default:
break; break;
} }
}; };
// Stop what you're doing and play this animation immediately. // Stop what you're doing and play this animation immediately.
virtual void(float anim_model_idx, float anim_framegroup) play_anim { void(float anim_model_idx, float anim_framegroup) AI_Chase::play_anim {
this.cur_anim_model_idx = anim_model_idx; this.cur_anim_model_idx = anim_model_idx;
this.cur_anim_framegroup = 0; this.cur_anim_framegroup = anim_framegroup;
this.cur_anim_start_time = time; this.cur_anim_start_time = time;
this.cur_anim_last_time = 0; this.cur_anim_last_time = 0;
this.cur_anim_playback_speed = 1; // Reset value this.cur_anim_playback_speed = 1; // Reset value
this.cur_anim_duration = frameduration(this.cur_anim_model_idx, this.cur_anim_framegroup);
// Indicate that we need to broadcast the current anim frame info: // Indicate that we need to broadcast the current anim frame info:
self.SendFlags |= 2; // Indicate that we should update client-side animations self.SendFlags |= 2; // Indicate that we should update client-side animations
}; };
// Queue up another animation to play when the current one finishes // Queue up another animation to play when the current one finishes
virtual void(float anim_model_idx, float anim_framegroup) queue_anim { void(float anim_model_idx, float anim_framegroup) AI_Chase::queue_anim {
this.next_anim_model_idx = anim_model_idx; this.next_anim_model_idx = anim_model_idx;
this.next_anim_framegroup = anim_framegroup; this.next_anim_framegroup = anim_framegroup;
this.next_anim_playback_speed = 1.0; this.next_anim_playback_speed = 1.0;
}; };
// The "logic tick" of the entity. // The "logic tick" of the entity.
virtual void() think = { void() AI_Chase::think {
if(this.cur_anim_model_idx >= 0) { if(this.cur_anim_model_idx >= 0) {
// print("Think!, ", ftos(cur_anim_time)); // print("Think!, ", ftos(cur_anim_time));
float cur_anim_time = (time - this.cur_anim_start_time) * this.cur_anim_playback_speed; float cur_anim_time = (time - this.cur_anim_start_time) * this.cur_anim_playback_speed;
@ -402,12 +740,9 @@ class AI_Chase : entity {
skel_blend_data.animationtime[0] = cur_anim_time; skel_blend_data.animationtime[0] = cur_anim_time;
skel_build_ptr(this.skeletonindex, 1, &skel_blend_data, sizeof(skel_blend_data)); skel_build_ptr(this.skeletonindex, 1, &skel_blend_data, sizeof(skel_blend_data));
// Zero out for this frame, we're going to accumulate total distance to move here via `processmodelevents`
// this.cur_anim_move_dist = 0;
// Process all animation frame events that elapsed since the last `think` function was called // Process all animation frame events that elapsed since the last `think` function was called
float anim_duration = frameduration(this.cur_anim_model_idx, this.cur_anim_framegroup); float events_start_time = this.cur_anim_last_time % this.cur_anim_duration;
float events_start_time = this.cur_anim_last_time % anim_duration; float events_end_time = cur_anim_time % this.cur_anim_duration;
float events_end_time = cur_anim_time % anim_duration;
// If the animation was looped, and start time is near the anim end, while end time is near the anim start // If the animation was looped, and start time is near the anim end, while end time is near the anim start
@ -415,7 +750,7 @@ class AI_Chase : entity {
if(events_end_time < events_start_time) { if(events_end_time < events_start_time) {
// Process elapsed events at the end of the animation // Process elapsed events at the end of the animation
float segment_start = events_start_time; float segment_start = events_start_time;
float segment_end = anim_duration; float segment_end = this.cur_anim_duration;
processmodelevents(this.cur_anim_model_idx, this.cur_anim_framegroup, segment_start, segment_end, this.handle_iqm_event); processmodelevents(this.cur_anim_model_idx, this.cur_anim_framegroup, segment_start, segment_end, this.handle_iqm_event);
// Process elapsed events at the start of the animation // Process elapsed events at the start of the animation
segment_start = 0; segment_start = 0;
@ -428,7 +763,7 @@ class AI_Chase : entity {
} }
// If we have a queued animation, check if this animation is over (or is very close to being over) // If we have a queued animation, check if this animation is over (or is very close to being over)
if(this.next_anim_model_idx >= 0 && (cur_anim_time + this.think_delta_time * this.cur_anim_playback_speed) >= anim_duration) { if(this.next_anim_model_idx >= 0 && (cur_anim_time + this.think_delta_time * this.cur_anim_playback_speed) >= this.cur_anim_duration) {
this.play_anim( this.next_anim_model_idx, this.next_anim_framegroup); this.play_anim( this.next_anim_model_idx, this.next_anim_framegroup);
this.cur_anim_playback_speed = this.next_anim_playback_speed; this.cur_anim_playback_speed = this.next_anim_playback_speed;
this.SendFlags |= 2; // Indicate that we should update client-side animations this.SendFlags |= 2; // Indicate that we should update client-side animations
@ -447,20 +782,13 @@ class AI_Chase : entity {
// Recalculate cur_anim_time, in case another animation was triggered // Recalculate cur_anim_time, in case another animation was triggered
this.cur_anim_last_time = (time - this.cur_anim_start_time) * this.cur_anim_playback_speed; this.cur_anim_last_time = (time - this.cur_anim_start_time) * this.cur_anim_playback_speed;
} }
};
// virtual void () fg_die = {};
// virtual void () fg_walk = {};
// virtual void () fg_attack = {};
// virtual void () fg_idle = {};
// virtual void (float dist) do_walk_to_goal;
}; };
// class AI_Zombie : AI_Chase { // class AI_Zombie : AI_Chase {
// entity enemy; // If near, attack // entity enemy; // If near, attack
@ -518,15 +846,13 @@ void zombie_think_callback() {
} }
else if(zombie.state == AI_STATE_TRAVERSING) { else if(zombie.state == AI_STATE_TRAVERSING) {
// TODO - traversal logic needs to be redone... // TODO - traversal logic needs to be redone...
// zombie_traversal_logic(); zombie_traversal_logic();
// FIXME - For now, teleport to end of traversal:
float traversal_idx = zombie.cur_traversal_idx;
vector end_pos = sv_navmesh_get_traversal_end_pos(traversal_idx);
setorigin(zombie, end_pos);
zombie.state = AI_STATE_PATHING;
// // FIXME - For now, teleport to end of traversal:
// float traversal_idx = zombie.cur_traversal_idx;
// vector end_pos = sv_navmesh_get_traversal_end_pos(traversal_idx);
// setorigin(zombie, end_pos);
// zombie.state = AI_STATE_PATHING;
} }
}; };
@ -546,6 +872,7 @@ void() test_new_ent = {
precache_model("models/ai/zombie_anim_walk3.iqm"); precache_model("models/ai/zombie_anim_walk3.iqm");
precache_model("models/ai/zombie_anim_jog1.iqm"); precache_model("models/ai/zombie_anim_jog1.iqm");
precache_model("models/ai/zombie_anim_run1.iqm"); precache_model("models/ai/zombie_anim_run1.iqm");
precache_model("models/ai/zombie_anim_climbledge.iqm");
zombie_anim_idle_modelindex = getmodelindex("models/ai/zombie_anim_idle.iqm"); zombie_anim_idle_modelindex = getmodelindex("models/ai/zombie_anim_idle.iqm");
zombie_anim_rise_modelindex = getmodelindex("models/ai/zombie_anim_rise.iqm"); zombie_anim_rise_modelindex = getmodelindex("models/ai/zombie_anim_rise.iqm");
zombie_anim_walk1_modelindex = getmodelindex("models/ai/zombie_anim_walk1.iqm"); zombie_anim_walk1_modelindex = getmodelindex("models/ai/zombie_anim_walk1.iqm");
@ -553,6 +880,7 @@ void() test_new_ent = {
zombie_anim_walk3_modelindex = getmodelindex("models/ai/zombie_anim_walk3.iqm"); zombie_anim_walk3_modelindex = getmodelindex("models/ai/zombie_anim_walk3.iqm");
zombie_anim_jog1_modelindex = getmodelindex("models/ai/zombie_anim_jog1.iqm"); zombie_anim_jog1_modelindex = getmodelindex("models/ai/zombie_anim_jog1.iqm");
zombie_anim_run1_modelindex = getmodelindex("models/ai/zombie_anim_run1.iqm"); zombie_anim_run1_modelindex = getmodelindex("models/ai/zombie_anim_run1.iqm");
zombie_anim_climbledge_modelindex = getmodelindex("models/ai/zombie_anim_climbledge.iqm");
} }
AI_Chase zombie = spawn(AI_Chase); AI_Chase zombie = spawn(AI_Chase);
@ -589,8 +917,6 @@ void() test_new_ent = {
setmodel(zombie, "models/ai/nazi_zombie.iqm"); setmodel(zombie, "models/ai/nazi_zombie.iqm");
skelblend_t skel_blend_data; skelblend_t skel_blend_data;
zombie.skeletonindex = skel_create(zombie.modelindex); zombie.skeletonindex = skel_create(zombie.modelindex);
// TODO - Do an initial skeleton build with the idle animation...
skel_blend_data.sourcemodelindex = zombie.skeletonindex; // FIXME - replace this with idle animation...
skel_blend_data.sourcemodelindex = zombie_anim_idle_modelindex; skel_blend_data.sourcemodelindex = zombie_anim_idle_modelindex;
skel_blend_data.firstbone = -1; skel_blend_data.firstbone = -1;
skel_blend_data.lastbone = -1; skel_blend_data.lastbone = -1;
@ -691,291 +1017,6 @@ void() test_new_ent = {
// void zombie_traversal_logic() {
// AI_Zombie zombie_ent = (AI_Zombie) self;
// float traversal_idx = zombie_ent.cur_traversal_idx;
// vector start_pos = sv_navmesh_traversals[traversal_idx].start_pos;
// vector midpoint_pos;
// vector end_pos = sv_navmesh_get_traversal_end_pos(traversal_idx);
// string traversal_type;
// // traversal_type = "ledge";
// traversal_type = "leap";
// // traversal_type = "jump_gap";
// // traversal_type = "hop_barricade";
// // traversal_type = "hop_fence";
// // traversal_type = "window";
// // traversal_type = "teleport";
// // Jump up logic
// if(traversal_type == "ledge") {
// // Adjust zombie angle to its smallest representation
// zombie_ent.angles.y = ((zombie_ent.angles.y + 180) % 360) - 180;
// // Apply smallest delta angle
// float delta_angle = sv_navmesh_traversals[traversal_idx].angle - zombie_ent.angles.y;
// delta_angle = ((delta_angle + 180) % 360) - 180;
// zombie_ent.angles.y += 0.5 * delta_angle;
// // Jump up traversal consists of the following substates:
// // 0: Start traversal, play jump up anim
// // 1: Wait for jump up anim to complete
// // 2: Move zombie up to ledge
// // 3: Play zombie get up animation
// //
// // Check height of ledge we're climbing:
// float traversal_height = end_pos.z - start_pos.z;
// float traversal_time;
// float lerp_frac;
// float anim_time;
// vector ledge_pos;
// // Fall down
// if(traversal_height < 0 ) {
// if(zombie_ent.substate == 0) {
// zombie_ent.movetype = MOVETYPE_STEP;
// zombie_ent.play_anim(get_anim_frame_zombie_fall, get_anim_length_zombie_fall(), ANIM_STOP_TYPE_STOP);
// zombie_ent.queue_anim(get_anim_frame_zombie_fall_loop, get_anim_length_zombie_fall_loop(), ANIM_STOP_TYPE_LOOP);
// traversal_time = min(-traversal_height * (0.35 / 100.0), 2.0);
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + traversal_time;
// zombie_ent.substate = 1;
// }
// else if(zombie_ent.substate == 1) {
// lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
// zombie_ent.origin = lerpVector(start_pos, end_pos, lerp_frac * lerp_frac);
// if(lerp_frac >= 1.0) {
// zombie_ent.play_anim(get_anim_frame_zombie_land, get_anim_length_zombie_land(), ANIM_STOP_TYPE_NEXT_ANIM);
// zombie_ent.cur_anim_frametime = 0.05;
// zombie_ent.queue_anim(get_anim_frame_zombie_walk1, get_anim_length_zombie_walk1(), ANIM_STOP_TYPE_LOOP);
// zombie_ent.state = AI_STATE_PATHING;
// zombie_ent.movetype = MOVETYPE_WALK;
// }
// }
// }
// // Short ledge
// else if(traversal_height < 98) {
// makevectors([0, sv_navmesh_traversals[traversal_idx].angle, 0]);
// ledge_pos = end_pos - '0 0 72' - v_forward * 21;
// if(zombie_ent.substate == 0) {
// zombie_ent.movetype = MOVETYPE_STEP;
// // If short jump up, play short jump / short climb anim
// zombie_ent.play_anim(get_anim_frame_zombie_jump_low, get_anim_length_zombie_jump_low(), ANIM_STOP_TYPE_STOP);
// anim_time = (zombie_ent.cur_anim_length - 1) * zombie_ent.cur_anim_frametime;
// // zombie_ent.cur_traversal_end_time = time + anim_time - (2 * zombie_ent.cur_anim_frametime);
// zombie_ent.cur_traversal_end_time = 0;
// // Stash anim stop-time in this variable so we can tell when to proceed: (minus three frames)
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + 0.3;
// zombie_ent.substate = 1;
// }
// if(zombie_ent.substate == 1) {
// lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
// zombie_ent.origin = lerpVector(start_pos, ledge_pos, lerp_frac);
// if(lerp_frac >= 1) {
// zombie_ent.substate = 2;
// // If short jump up, play short climb anim
// zombie_ent.play_anim(get_anim_frame_zombie_climb_low, get_anim_length_zombie_climb_low(), ANIM_STOP_TYPE_STOP);
// anim_time = (zombie_ent.cur_anim_length - 1) * zombie_ent.cur_anim_frametime;
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + anim_time;
// }
// }
// else if(zombie_ent.substate == 2) {
// lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
// start_pos = end_pos - '0 0 72' - v_forward * 21;
// zombie_ent.origin = lerpVector(start_pos, end_pos, lerp_frac);
// if(lerp_frac >= 1.0) {
// zombie_ent.state = AI_STATE_PATHING;
// zombie_ent.movetype = MOVETYPE_WALK;
// // FIXME - Need a better way to revert to walking
// zombie_ent.play_anim(get_anim_frame_zombie_walk1, get_anim_length_zombie_walk1(), ANIM_STOP_TYPE_LOOP);
// }
// }
// }
// // Tall ledge
// else {
// makevectors([0, sv_navmesh_traversals[traversal_idx].angle, 0]);
// ledge_pos = end_pos - '0 0 98' - v_forward * 28;
// if(zombie_ent.substate == 0) {
// zombie_ent.movetype = MOVETYPE_STEP;
// zombie_ent.play_anim(get_anim_frame_zombie_jump, get_anim_length_zombie_jump(), ANIM_STOP_TYPE_STOP);
// // zombie_ent.cur_anim_frametime = 0.08;
// anim_time = (zombie_ent.cur_anim_length - 1) * zombie_ent.cur_anim_frametime;
// zombie_ent.cur_traversal_end_time = time + anim_time - (1 * zombie_ent.cur_anim_frametime);
// // Stash anim stop-time in this variable so we can tell when to proceed: (minus three frames)
// zombie_ent.substate = 1;
// // zombie_ent.cur_anim_get_frame_func = (float(float)) SUB_Null;
// // Figure out how fast to move the zombie
// // float traversal_length = vlen(end_pos - start_pos);
// // zombie_ent.cur_traversal_start_time = time;
// // FIXME - Some traversals will have a different way of getting speed...
// }
// else if(zombie_ent.substate == 1) {
// if(zombie_ent.cur_traversal_end_time <= time) {
// // Zombie jumping should be real fast,
// traversal_time = 0.12; // seconds
// // TODO - Should we determine how fast the zombie moves based on traversal distance?
// // TODO Otherwise zombie will always jump up in 0.5 seconds regardless of ledge height
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + traversal_time;
// zombie_ent.substate = 2;
// }
// }
// else if(zombie_ent.substate == 2) {
// lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
// zombie_ent.origin = lerpVector(start_pos, ledge_pos, lerp_frac);
// if(lerp_frac >= 1) {
// zombie_ent.substate = 3;
// zombie_ent.play_anim(get_anim_frame_zombie_climb, get_anim_length_zombie_climb(), ANIM_STOP_TYPE_STOP);
// anim_time = (zombie_ent.cur_anim_length - 1) * zombie_ent.cur_anim_frametime;
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + anim_time;
// }
// }
// else if(zombie_ent.substate == 3) {
// lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
// zombie_ent.origin = lerpVector(ledge_pos, end_pos, lerp_frac);
// if(lerp_frac >= 1.0) {
// zombie_ent.state = AI_STATE_PATHING;
// zombie_ent.movetype = MOVETYPE_WALK;
// // FIXME - Need a better way to revert to walking
// zombie_ent.play_anim(get_anim_frame_zombie_walk1, get_anim_length_zombie_walk1(), ANIM_STOP_TYPE_LOOP);
// }
// }
// }
// return;
// }
// else if(traversal_type == "leap") {
// midpoint_pos = sv_navmesh_get_traversal_midpoint_pos(traversal_idx);
// // Adjust zombie angle to its smallest representation
// zombie_ent.angles.y = ((zombie_ent.angles.y + 180) % 360) - 180;
// // Apply smallest delta angle
// float delta_angle = sv_navmesh_traversals[traversal_idx].angle - zombie_ent.angles.y;
// delta_angle = ((delta_angle + 180) % 360) - 180;
// zombie_ent.angles.y += 0.5 * delta_angle;
// // Leap traversal consists of the following substates:
// // 0: Play leap animation (frames 218-233)
// // 1: Wait for frame 233
// // 2: Move zombie across arc to end pos, ends at frame 241
// // 3: Wait at endpos for frame 247
// // TODO - Adjust traversal speed?
// // TODO - Break up leap start / leap-mid-air / leap land anims?
// // float traversal_length =
// float lerp_frac;
// if(zombie_ent.substate == 0) {
// zombie_ent.movetype = MOVETYPE_STEP;
// zombie_ent.play_anim(get_anim_frame_zombie_leap_jump, get_anim_length_zombie_leap_jump(), ANIM_STOP_TYPE_STOP);
// zombie_ent.substate = 1;
// // Advance to next substate (1->2) after 5 frames
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + (5 * zombie_ent.cur_anim_frametime);
// }
// else if(zombie_ent.substate == 1) {
// if(zombie_ent.cur_traversal_end_time <= time) {
// zombie_ent.substate = 2;
// // Advance to next substate (2->3) after 8 frames
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + (8 * zombie_ent.cur_anim_frametime);
// }
// }
// else if(zombie_ent.substate == 2) {
// lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
// zombie_ent.origin = lerp_vector_bezier(start_pos, midpoint_pos, end_pos, lerp_frac);
// if(lerp_frac >= 1) {
// zombie_ent.substate = 3;
// zombie_ent.play_anim(get_anim_frame_zombie_leap_land, get_anim_length_zombie_leap_land(), ANIM_STOP_TYPE_STOP);
// float anim_time = (zombie_ent.cur_anim_length - 1) * zombie_ent.cur_anim_frametime;
// // Finish traversal at the end of the land animation
// zombie_ent.cur_traversal_start_time = time;
// zombie_ent.cur_traversal_end_time = time + anim_time;
// }
// }
// else if(zombie_ent.substate == 3) {
// if(zombie_ent.cur_traversal_end_time <= time) {
// zombie_ent.state = AI_STATE_PATHING;
// zombie_ent.movetype = MOVETYPE_WALK;
// // FIXME - Need a better way to revert to walking
// zombie_ent.play_anim(get_anim_frame_zombie_walk1, get_anim_length_zombie_walk1(), ANIM_STOP_TYPE_LOOP);
// }
// }
// return;
// }
// // // Starting traversal
// // if(zombie_ent.substate == 0) {
// // zombie_ent.velocity = '0 0 0';
// // // zombie_ent.cur_anim_get_frame_func = (float(float)) SUB_Null;
// // // Start zombie animation:
// // // zombie_ent.play_anim(get_anim_frame_zombie_window_hop, get_anim_length_zombie_window_hop(), ANIM_STOP_TYPE_STOP);
// // // zombie_ent.play_anim(get_anim_frame_zombie_jump_climb, get_anim_length_zombie_jump_climb(), ANIM_STOP_TYPE_STOP);
// // // zombie_ent.cur_anim_get_frame_func = (float(float)) SUB_Null;
// // // Figure out how fast to move the zombie
// // float traversal_length = vlen(end_pos - start_pos);
// // float anim_time = (zombie_ent.cur_anim_length - 1) * zombie_ent.cur_anim_frametime;
// // zombie_ent.cur_traversal_start_time = time;
// // // FIXME - Some traversals will have a different way of getting speed...
// // zombie_ent.cur_traversal_end_time = time + anim_time;
// // zombie_ent.substate = 1;
// // zombie_ent.movetype = MOVETYPE_STEP;
// // zombie_ent.angles.y = sv_navmesh_traversals[traversal_idx].angle;
// // }
// // // Moving zombie across traversal
// // if(zombie_ent.substate == 1) {
// // float lerp_frac = (time - zombie_ent.cur_traversal_start_time) / (zombie_ent.cur_traversal_end_time - zombie_ent.cur_traversal_start_time);
// // if(lerp_frac > 1.0) {
// // zombie_ent.state = AI_STATE_PATHING;
// // zombie_ent.movetype = MOVETYPE_WALK;
// // // TODO - How to tell zombie to play walk anim again?
// // // FIXME - This ain't right
// // zombie_ent.play_anim(get_anim_frame_zombie_walk1, get_anim_length_zombie_walk1(), ANIM_STOP_TYPE_LOOP);
// // }
// // // If the traversal uses the midpoint, lerp across midpoint
// // if(sv_navmesh_traversals[zombie_ent.cur_traversal_idx].use_midpoint) {
// // print("Current lerpfrac: ", ftos(lerp_frac), "\n");
// // midpoint_pos = sv_navmesh_get_traversal_midpoint_pos(traversal_idx);
// // // Lerp from start to midpoint
// // if(lerp_frac < 0.5) {
// // zombie_ent.origin = lerpVector(start_pos, midpoint_pos, lerp_frac * 2.0);
// // }
// // // Lerp from midpoint to end
// // else {
// // zombie_ent.origin = lerpVector(midpoint_pos, end_pos, (lerp_frac - 0.5) * 2.0);
// // }
// // }
// // // Otherwise, lerp from start to end
// // else {
// // zombie_ent.origin = lerpVector(start_pos, end_pos, lerp_frac);
// // }
// // // TODO - Moving
// // // vector start_pos = sv_navmesh_traversals[traversal_idx].start_pos;
// // // vector end_pos = sv_navmesh_get_traversal_end_pos(traversal_idx);
// // }
// }