/* Copyright (C) 1997-2001 Id Software, Inc. 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 the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "../g_local.h" #include "ai_local.h" // explosion fx void barrel_explode (edict_t *self) { vec3_t org; float spd; vec3_t save; T_RadiusDamage (self, self->activator, NULL, self->dmg, self->dmg, 0, NULL, self->dmg+40, MOD_BARREL); VectorCopy (self->s.origin, save); VectorMA (self->r.absmin, 0.5, self->r.size, self->s.origin); // a few big chunks spd = 1.5 * (float)self->dmg / 200.0; org[0] = self->s.origin[0] + crandom() * self->r.size[0]; org[1] = self->s.origin[1] + crandom() * self->r.size[1]; org[2] = self->s.origin[2] + crandom() * self->r.size[2]; ThrowDebris (self, "models/objects/debris1/tris.md2", spd, org); org[0] = self->s.origin[0] + crandom() * self->r.size[0]; org[1] = self->s.origin[1] + crandom() * self->r.size[1]; org[2] = self->s.origin[2] + crandom() * self->r.size[2]; ThrowDebris (self, "models/objects/debris1/tris.md2", spd, org); // bottom corners spd = 1.75 * (float)self->dmg / 200.0; VectorCopy (self->r.absmin, org); ThrowDebris (self, "models/objects/debris3/tris.md2", spd, org); VectorCopy (self->r.absmin, org); org[0] += self->r.size[0]; ThrowDebris (self, "models/objects/debris3/tris.md2", spd, org); VectorCopy (self->r.absmin, org); org[1] += self->r.size[1]; ThrowDebris (self, "models/objects/debris3/tris.md2", spd, org); VectorCopy (self->r.absmin, org); org[0] += self->r.size[0]; org[1] += self->r.size[1]; ThrowDebris (self, "models/objects/debris3/tris.md2", spd, org); // a bunch of little chunks spd = 2 * self->dmg / 200; org[0] = self->s.origin[0] + crandom() * self->r.size[0]; org[1] = self->s.origin[1] + crandom() * self->r.size[1]; org[2] = self->s.origin[2] + crandom() * self->r.size[2]; ThrowDebris (self, "models/objects/debris2/tris.md2", spd, org); org[0] = self->s.origin[0] + crandom() * self->r.size[0]; org[1] = self->s.origin[1] + crandom() * self->r.size[1]; org[2] = self->s.origin[2] + crandom() * self->r.size[2]; ThrowDebris (self, "models/objects/debris2/tris.md2", spd, org); org[0] = self->s.origin[0] + crandom() * self->r.size[0]; org[1] = self->s.origin[1] + crandom() * self->r.size[1]; org[2] = self->s.origin[2] + crandom() * self->r.size[2]; ThrowDebris (self, "models/objects/debris2/tris.md2", spd, org); org[0] = self->s.origin[0] + crandom() * self->r.size[0]; org[1] = self->s.origin[1] + crandom() * self->r.size[1]; org[2] = self->s.origin[2] + crandom() * self->r.size[2]; ThrowDebris (self, "models/objects/debris2/tris.md2", spd, org); org[0] = self->s.origin[0] + crandom() * self->r.size[0]; org[1] = self->s.origin[1] + crandom() * self->r.size[1]; org[2] = self->s.origin[2] + crandom() * self->r.size[2]; ThrowDebris (self, "models/objects/debris2/tris.md2", spd, org); org[0] = self->s.origin[0] + crandom() * self->r.size[0]; org[1] = self->s.origin[1] + crandom() * self->r.size[1]; org[2] = self->s.origin[2] + crandom() * self->r.size[2]; ThrowDebris (self, "models/objects/debris2/tris.md2", spd, org); org[0] = self->s.origin[0] + crandom() * self->r.size[0]; org[1] = self->s.origin[1] + crandom() * self->r.size[1]; org[2] = self->s.origin[2] + crandom() * self->r.size[2]; ThrowDebris (self, "models/objects/debris2/tris.md2", spd, org); org[0] = self->s.origin[0] + crandom() * self->r.size[0]; org[1] = self->s.origin[1] + crandom() * self->r.size[1]; org[2] = self->s.origin[2] + crandom() * self->r.size[2]; ThrowDebris (self, "models/objects/debris2/tris.md2", spd, org); VectorCopy (save, self->s.origin); if (self->groundentity) G_TurnEntityIntoEvent( self, EV_EXPLOSION2, 0 ); else BecomeExplosion1 (self); } void barrel_delay( edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point ) { self->takedamage = DAMAGE_NO; self->nextthink = level.timemsec + 2 * game.framemsec; self->think = barrel_explode; self->activator = attacker; } //ACE void SelectSpawnPoint( edict_t *ent, vec3_t origin, vec3_t angles); qboolean BOT_DMclass_FindEnemy( edict_t *self); void BOT_DMclass_CombatMovement( edict_t *self, usercmd_t *ucmd ); void BOT_DMclass_Wander( edict_t *self, usercmd_t *ucmd); void BOT_DMclass_Move( edict_t *self, usercmd_t *ucmd); //========================================== // M_default_Move // movement following paths code //========================================== void M_default_Move( edict_t *self, usercmd_t *ucmd ) { int current_node_flags = 0; int next_node_flags = 0; int current_link_type = 0; // int i; current_node_flags = nodes[self->ai.current_node].flags; next_node_flags = nodes[self->ai.next_node].flags; if( AI_PlinkExists( self->ai.current_node, self->ai.next_node )) { current_link_type = AI_PlinkMoveType( self->ai.current_node, self->ai.next_node ); //Com_Printf("%s\n", AI_LinkString( current_link_type )); } // Falling off ledge if( !self->groundentity && !self->is_step && !self->is_swim ) { AI_ChangeAngle(self); /* if (current_link_type == LINK_JUMPPAD ) { ucmd->forwardmove = 100; } else if( current_link_type == LINK_JUMP ) { self->velocity[0] = self->ai.move_vector[0] * 280; self->velocity[1] = self->ai.move_vector[1] * 280; } else { self->velocity[0] = self->ai.move_vector[0] * 160; self->velocity[1] = self->ai.move_vector[1] * 160; } */ return; } // jumping over (keep fall before this) if( current_link_type == LINK_JUMP && self->groundentity) { trace_t trace; vec3_t v1, v2; //check floor in front, if there's none... Jump! VectorCopy( self->s.origin, v1 ); VectorCopy( self->ai.move_vector, v2 ); VectorNormalize( v2 ); VectorMA( v1, 16, v2, v1 ); v1[2] += self->r.mins[2]; trap_Trace( &trace, v1, tv(-2, -2, -AI_JUMPABLE_HEIGHT), tv(2, 2, 0), v1, self, MASK_AISOLID ); //trace = gi.trace( v1, tv(-2, -2, -AI_JUMPABLE_HEIGHT), tv(2, 2, 0), v1, self, MASK_AISOLID ); if( !trace.startsolid && trace.fraction == 1.0 ) { //jump! ucmd->forwardmove = 400; //prevent double jumping on crates VectorCopy( self->s.origin, v1 ); v1[2] += self->r.mins[2]; trap_Trace( &trace, v1, tv(-12, -12, -8), tv(12, 12, 0), v1, self, MASK_AISOLID ); //trace = gi.trace( v1, tv(-12, -12, -8), tv(12, 12, 0), v1, self, MASK_AISOLID ); if( trace.startsolid ) ucmd->upmove = 400; return; } } // swimming if( self->is_swim ) { // We need to be pointed up/down AI_ChangeAngle(self); //if( !(gi.pointcontents(nodes[self->ai.next_node].origin) & MASK_WATER) ) // Exit water if( !(trap_PointContents(nodes[self->ai.next_node].origin) & MASK_WATER) ) // Exit water ucmd->upmove = 400; ucmd->forwardmove = 300; return; } // Check to see if stuck, and if so try to free us if( VectorCompare(self->s.old_origin, self->s.origin) ) { // Keep a random factor just in case.... if( random() > 0.1 && AI_SpecialMove(self, ucmd) ) //jumps, crouches, turns... return; self->s.angles[YAW] += random() * 180 - 90; AI_ChangeAngle(self); ucmd->forwardmove = 400; return; } AI_ChangeAngle(self); // Otherwise move as fast as we can... ucmd->forwardmove = 400; } //========================================== // M_default_CombatMovement // movement while in state combat //========================================== void M_default_CombatMovement( edict_t *self, usercmd_t *ucmd ) { BOT_DMclass_CombatMovement( self, ucmd ); } //========================================== // M_default_Wander // movement when couldn't find a goal //========================================== void M_default_Wander( edict_t *self, usercmd_t *ucmd ) { BOT_DMclass_Wander( self, ucmd ); } //========================================== // M_default_FindEnemy //========================================== qboolean M_default_FindEnemy( edict_t *self ) { return BOT_DMclass_FindEnemy( self ); } //========================================== // M_default_ChooseWeapon // Choose weapon based on range & weights //========================================== void M_default_ChooseWeapon( edict_t *self ) { } //========================================== // M_default_pain //========================================== void M_default_pain( edict_t *self, edict_t *other, float kick, int damage ) { } //========================================== // M_default_DeadFrame // AI passes through here instead of M_default_RunFrame // when self->deadflag is set up. The default monster // explodes and frees itself, so it's not happening now. //========================================== void M_default_DeadFrame( edict_t *self ) { } //========================================== // M_default_die //========================================== void M_default_die( edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point ) { if( AIDevel.debugMode && bot_debugmonster->integer ) G_PrintMsg( NULL, "monster: Die\n" ); //throw gibs G_Sound( self, CHAN_BODY, trap_SoundIndex( "sounds/misc/udeath.wav" ), 1, ATTN_NORM ); ThrowSmallPileOfGibs( self, 4, damage ); //ThrowHead( self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC ); self->deadflag = DEAD_DEAD; if( self->item ) { Drop_Item( self, self->item ); self->item = NULL; } AI_EnemyRemoved( self ); //explode barrel_delay( self, inflictor, attacker, damage, point ); } //========================================== // M_default_BloquedTimeout //========================================== void M_default_BloquedTimeout( edict_t *self ) { M_default_die( self, self, self, 10000, self->s.origin ); } //========================================== // M_default_CheckShot // Checks if shot is blocked or if too far to shoot //========================================== qboolean M_default_CheckShot( edict_t *self, vec3_t point ) { trace_t tr; //bloqued, don't shoot //tr = gi.trace( self->s.origin, vec3_origin, vec3_origin, point, self, MASK_AISOLID); trap_Trace( &tr, self->s.origin, vec3_origin, vec3_origin, point, self, MASK_AISOLID ); if( tr.fraction < 0.3 ) //just enough to prevent self damage (by now) return qfalse; return qtrue; } //========================================== // M_default_FireWeapon // Fire if needed //========================================== void W_Fire_Gunblade_Bullet( edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int mod); void M_default_FireWeapon( edict_t *self ) { float firedelay; vec3_t target; vec3_t angles; if( !self->enemy ) return; // Aim VectorCopy( self->enemy->s.origin,target ); // modify attack angles based on accuracy target[0] += (random()-0.5f) * 48 * (1.0f - self->ai.pers.skillLevel*0.5f); target[1] += (random()-0.5f) * 48 * (1.0f - self->ai.pers.skillLevel*0.5f); // Set direction VectorSubtract( target, self->s.origin, self->ai.move_vector ); VecToAngles( self->ai.move_vector, angles ); VectorCopy( angles,self->s.angles ); // Set the attack (fix this ramdomness) firedelay = ( 100.0f * (random()-0.25f) ) + ( 10.0f * self->ai.pers.skillLevel ); if( firedelay > 0.0f && M_default_CheckShot(self, target) ) { vec3_t start, forward, right; AngleVectors( self->s.angles, forward, right, NULL ); G_ProjectSource( self->s.origin, tv(15,15,0), forward, right, start ); W_Fire_Gunblade_Bullet( self, start, forward, 4, 4, MOD_UNKNOWN ); //monster_fire_bullet(self, start, forward, 4, 4, MZ2_INFANTRY_MACHINEGUN_2); //monster_fire_bullet(self, start, forward, 4, 4, rand(), rand(), MZ2_INFANTRY_MACHINEGUN_2); } if( AIDevel.debugMode && bot_debugmonster->integer ) G_PrintMsg( NULL, "monster: attacking\n" ); } //========================================== // M_default_WeightPlayers // weight players based on game state //========================================== void M_default_WeightPlayers(edict_t *self) { int i; //clear memset( self->ai.status.playersWeights, 0, sizeof (self->ai.status.playersWeights) ); for( i=0;iclassname, "monster") ) { self->ai.status.playersWeights[i] = 0.0f; continue; } //ignore spectators and dead players if( AIEnemies[i]->r.svflags & SVF_NOCLIENT || AIEnemies[i]->deadflag ) { self->ai.status.playersWeights[i] = 0.0f; continue; } //every player has some value self->ai.status.playersWeights[i] = 0.3f; } } //========================================== // M_default_WeightInventory // monsters can't pick up items, so zero all //========================================== void M_default_WeightInventory(edict_t *self) { //reset with persistant values memcpy(self->ai.status.inventoryWeights, self->ai.pers.inventoryWeights, sizeof(self->ai.pers.inventoryWeights)); } //========================================== // M_default_UpdateStatus // Set up status for ai. Happens before M_default_RunFrame //========================================== void M_default_UpdateStatus( edict_t *self ) { self->enemy = NULL; self->movetarget = NULL; //set up AI status for the upcoming AI_frame M_default_WeightInventory(self); M_default_WeightPlayers(self); } void M_Phys_Momentum_AddPush( vec3_t accel, vec3_t pushdir, float push, float mass, float timestep ); void M_Phys_Momentum_AddFriction( float classfriction, float class_stopspeed, vec3_t origin, vec3_t vel, float timestep, vec3_t mins, vec3_t maxs, edict_t *passent, int solidmask ); int AI_NPCPhysMove( edict_t *ent, float time, int mask, qboolean step); qboolean M_default_movestep( edict_t *self, usercmd_t *ucmd ) { int move; float forwardaccel = 0; float sideaccel = 0; float upaccel = 0; vec3_t forward, right, up; vec3_t vaccel; vec3_t xzvelocity; float speed; qboolean GroundEntity = qfalse; trace_t trace; vec3_t v1; //jalmoveme to defines or something float RUNaccel = 90;//26 float CLASS_MAX_velocity = 90.0f;//32 float CLASS_TRACTION = 1.0f; float CLASS_FRICTION = 3.0f; float stopspeed, CLASS_STOPDECCEL = 8; //float CLASS_BOUNCESCALE = 1.0; forwardaccel = 0.0f; sideaccel = 0.0f; //figure out what move do we want to do (forward) if( ucmd->forwardmove > 5 ) forwardaccel = RUNaccel; else if( ucmd->forwardmove < -5 ) forwardaccel = -RUNaccel; //figure out what move do we want to do (side) if( ucmd->sidemove > 5 ) sideaccel = RUNaccel; else if( ucmd->sidemove < -5 ) sideaccel = -RUNaccel; upaccel = 0.0f; VectorCopy( self->s.origin, self->s.old_origin ); //see if we are on floor (I don't trust ent->groundentity right now) VectorCopy( self->s.origin, v1 ); v1[2] -= 0.25f;//inside floor //trace = gi.trace( self->s.origin, self->r.mins, self->r.maxs, v1, self, MASK_AISOLID ); trap_Trace( &trace, self->s.origin, self->r.mins, self->r.maxs, v1, self, MASK_AISOLID ); if( trace.fraction < 1.0f && trace.plane.normal[2] >= 0.7 ) // We have ground entity { GroundEntity = qtrue; } stopspeed = 0.0f; if( GroundEntity == qtrue ) //don't accelerate if not at floor { //intended accel forwardaccel *= CLASS_TRACTION; sideaccel *= CLASS_TRACTION; // if the client is NOT pushing movement keys, it's assumed that the // player will stop moving, not only by friction, but by using their legs to stop if( !forwardaccel && !sideaccel ) { if( self->velocity[0] || self->velocity[1] || self->velocity[2] ) { stopspeed = CLASS_STOPDECCEL * CLASS_TRACTION; } //M_Phys_Momentum_AddFriction( CLASS_FRICTION, stopspeed, self->s.origin, self->velocity, FRAMETIME, self->r.mins, self->r.maxs, self->mass, self, MASK_AISOLID ); } } else { //air accel forwardaccel *= 0.05f; sideaccel *= 0.05f; } M_Phys_Momentum_AddFriction( CLASS_FRICTION, stopspeed, self->s.origin, self->velocity, FRAMETIME, self->r.mins, self->r.maxs, self, MASK_AISOLID ); //calculate accel VectorClear( vaccel ); AngleVectors( tv( 0, self->s.angles[YAW], 0), forward, right, up); M_Phys_Momentum_AddPush( vaccel, forward, forwardaccel, self->mass, FRAMETIME ); M_Phys_Momentum_AddPush( vaccel, right, sideaccel, self->mass, FRAMETIME ); //gravity if( GroundEntity == qtrue ) { if( self->velocity[2] < 0.0f ) self->velocity[2] = 0.0f; if( ucmd->upmove > 10 && self->velocity[2] == 0.0f ) //jump M_Phys_Momentum_AddPush( vaccel, up, 60.0f, self->mass, 1/*FRAMETIME*/ ); } else { if( self->velocity[2] > -g_maxvelocity->value ) self->velocity[2] -= self->gravity * g_gravity->value * FRAMETIME; } //add momentum to velocity VectorAdd( self->velocity, vaccel, self->velocity ); //speed limit xzvelocity[0] = self->velocity[0]; xzvelocity[1] = self->velocity[1]; xzvelocity[2] = 0.0f; speed = VectorLengthFast( xzvelocity ); if( speed > CLASS_MAX_velocity ) { speed = CLASS_MAX_velocity / speed; self->velocity[0] *= speed; self->velocity[1] *= speed; } //move move = AI_NPCPhysMove( self, FRAMETIME, MASK_AISOLID, qtrue ); if( move == 3 ) { //VectorCopy( oldorigin, self->s.old_origin ); VectorClear( self->velocity ); trap_LinkEntity(self); return qfalse; } //relink trap_LinkEntity(self); G_TouchTriggers(self); return qtrue; } void G_SetPModelFrame (edict_t *ent); //========================================== // M_default_RunFrame // // States Machine & call client movement //========================================== void M_default_RunFrame( edict_t *self ) { usercmd_t ucmd; memset( &ucmd, 0, sizeof(ucmd) ); // Look for enemies if( M_default_FindEnemy(self) ) { M_default_ChooseWeapon( self ); M_default_FireWeapon( self ); self->ai.state = BOT_STATE_ATTACK; self->ai.state_combat_timeout = level.time + 1.0; } else if( self->ai.state == BOT_STATE_ATTACK && level.time > self->ai.state_combat_timeout) { //Jalfixme: change to: AI_SetUpStateMove(self); self->ai.state = BOT_STATE_MOVE; } // Execute the move, or wander if( self->ai.state == BOT_STATE_MOVE ) M_default_Move( self, &ucmd ); else if(self->ai.state == BOT_STATE_ATTACK) M_default_CombatMovement( self, &ucmd ); else if ( self->ai.state == BOT_STATE_WANDER ) M_default_Wander( self, &ucmd ); //move a step if( M_default_movestep ( self, &ucmd ) ) self->ai.bloqued_timeout = level.time + 10.0; //update model animations AI_SetUpAnimMoveFlags( self, &ucmd ); G_SetPModelFrame( self ); self->nextthink = level.timemsec + game.framemsec; } //========================================== // M_default_InitPersistant // Persistant after respawns. //========================================== void M_default_InitPersistant( edict_t *self ) { float sv_skill; //jalfixme: this bools aren't the right way to go: ai.type might be better? self->ai.type = AI_ISMONSTER; //set 'class' functions self->ai.pers.RunFrame = M_default_RunFrame; self->ai.pers.UpdateStatus = M_default_UpdateStatus; self->ai.pers.bloquedTimeout = M_default_BloquedTimeout; self->ai.pers.deadFrame = M_default_DeadFrame; // set skill based on sv_skilllevel cvar sv_skill = trap_Cvar_VariableValue( "sv_skilllevel" ); // 0 = easy, 2 = hard sv_skill += (random() + 0.00001f);// so we have a float between 0 and 3 meaning the server skill self->ai.pers.skillLevel = 1.0f - (3.0f / sv_skill); // the same being a fraction of 1. if( self->ai.pers.skillLevel < 0.1f ) self->ai.pers.skillLevel = 0.1f; self->yaw_speed -= 20 * (1.0f - self->ai.pers.skillLevel); //available moveTypes for this class self->ai.pers.moveTypesMask = (LINK_MOVE|LINK_STAIRS|LINK_FALL/*|LINK_JUMP*/); //Persistant Inventory Weights (0 = can not pick) memset( self->ai.pers.inventoryWeights, 0, sizeof (self->ai.pers.inventoryWeights) ); } //========================================== // M_default_Spawn // //========================================== void M_default_Start( edict_t *self ) { self->health = 30; self->max_health = self->health; //self->item = GS_FindItemByClassname("ammo_bullets"); self->think = AI_Think; self->nextthink = level.timemsec + game.framemsec; self->yaw_speed = AI_DEFAULT_YAW_SPEED; M_default_InitPersistant(self); AI_ResetNavigation(self); //add as bot enemy AI_EnemyAdded( self ); if( AIDevel.debugMode && bot_debugmonster->integer ) G_PrintMsg (NULL, "monster: Spawn\n"); } void M_default_Spawn (void) { edict_t *ent; vec3_t spawn_origin, spawn_angles;//spawn at a spawnpoint ent = G_Spawn(); G_SpawnAI(ent); //jabot092(2) //spawn at a spawnpoint SelectSpawnPoint( ent, spawn_origin, spawn_angles ); spawn_origin[2] += 8; //------------------------------------------------- // clear entity values ent->groundentity = NULL; ent->takedamage = DAMAGE_AIM; ent->movetype = MOVETYPE_WALK; ent->viewheight = 22; ent->r.inuse = qtrue; ent->classname = "monster"; ent->mass = 200; ent->r.solid = SOLID_BBOX; ent->deadflag = DEAD_NO; ent->air_finished = level.time + 12; ent->r.clipmask = MASK_MONSTERSOLID; //ent->model = "models/monsters/infantry/tris.md2";//jalfixme ent->waterlevel = 0; ent->watertype = 0; ent->flags &= ~FL_NO_KNOCKBACK; ent->pain = M_default_pain; ent->die = M_default_die; VectorCopy( playerbox_stand_mins, ent->r.mins ); VectorCopy( playerbox_stand_maxs, ent->r.maxs ); VectorClear( ent->velocity ); ent->s.type = ET_PLAYER; ent->s.modelindex = trap_ModelIndex( "$models/players/monada" ); ent->s.skinnum = trap_SkinIndex( "models/players/monada/default" ); // clear entity state values ent->s.effects = 0; ent->s.frame = 0; ent->s.light = 0; VectorCopy( spawn_origin, ent->s.origin ); VectorCopy( ent->s.origin, ent->s.old_origin ); ent->s.angles[PITCH] = 0; ent->s.angles[YAW] = spawn_angles[YAW]; ent->s.angles[ROLL] = 0; if( !KillBox (ent) ) { // could't spawn in? } trap_LinkEntity(ent); //finish M_default_Start(ent); }