/* =========================================================================== Copyright (C) 2000 - 2013, Raven Software, Inc. Copyright (C) 2001 - 2013, Activision, Inc. Copyright (C) 2013 - 2015, OpenJK contributors This file is part of the OpenJK source code. OpenJK is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. 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, see . =========================================================================== */ #include "g_headers.h" #include "g_local.h" #include "g_functions.h" #include "b_local.h" extern team_t TranslateTeamName( const char *name ); extern cvar_t *g_spskill; void G_SetEnemy( gentity_t *self, gentity_t *enemy ); void finish_spawning_turret( gentity_t *base ); void ObjectDie (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath ); extern void G_SoundOnEnt( gentity_t *ent, soundChannel_t channel, const char *soundPath ); #define ARM_ANGLE_RANGE 60 #define HEAD_ANGLE_RANGE 90 //------------------------------------------------------------------------------------------------------------ void TurretPain( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, vec3_t point, int damage, int mod, int hitLoc ) //------------------------------------------------------------------------------------------------------------ { vec3_t dir; VectorSubtract( point, self->currentOrigin, dir ); VectorNormalize( dir ); if ( mod == MOD_DEMP2 || mod == MOD_DEMP2_ALT ) { // DEMP2 makes the turret stop shooting for a bit..and does extra feedback self->attackDebounceTime = level.time + 800 + Q_flrand(0.0f, 1.0f) * 500; G_PlayEffect( "spark_exp_nosnd", point, dir ); } G_PlayEffect( "spark_exp_nosnd", point, dir ); } //------------------------------------------------------------------------------------------------------------ void turret_die ( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath,int dFlags,int hitLoc ) //------------------------------------------------------------------------------------------------------------ { vec3_t forward = { 0,0,-1 }, pos; // Turn off the thinking of the base & use it's targets self->e_ThinkFunc = thinkF_NULL; self->e_UseFunc = useF_NULL; // clear my data self->e_DieFunc = dieF_NULL; self->takedamage = qfalse; self->health = 0; self->s.loopSound = 0; // hack the effect angle so that explode death can orient the effect properly if ( self->spawnflags & 2 ) { VectorSet( forward, 0, 0, 1 ); } // VectorCopy( self->currentOrigin, self->s.pos.trBase ); if ( self->fxID > 0 ) { VectorMA( self->currentOrigin, 12, forward, pos ); G_PlayEffect( self->fxID, pos, forward ); } if ( self->splashDamage > 0 && self->splashRadius > 0 ) { G_RadiusDamage( self->currentOrigin, attacker, self->splashDamage, self->splashRadius, attacker, MOD_UNKNOWN ); } if ( self->s.eFlags & EF_SHADER_ANIM ) { self->s.frame = 1; // black } self->s.weapon = 0; // crosshair code uses this to mark crosshair red if ( self->s.modelindex2 ) { // switch to damage model if we should self->s.modelindex = self->s.modelindex2; VectorCopy( self->currentAngles, self->s.apos.trBase ); VectorClear( self->s.apos.trDelta ); if ( self->target ) { G_UseTargets( self, attacker ); } } else { ObjectDie( self, inflictor, attacker, damage, meansOfDeath ); } } #define START_DIS 15 //---------------------------------------------------------------- static void turret_fire ( gentity_t *ent, vec3_t start, vec3_t dir ) //---------------------------------------------------------------- { vec3_t org; gentity_t *bolt; if ( gi.pointcontents( start, MASK_SHOT )) { return; } VectorMA( start, -START_DIS, dir, org ); // dumb.... G_PlayEffect( "blaster/muzzle_flash", org, dir ); bolt = G_Spawn(); bolt->classname = "turret_proj"; bolt->nextthink = level.time + 10000; bolt->e_ThinkFunc = thinkF_G_FreeEntity; bolt->s.eType = ET_MISSILE; bolt->s.weapon = WP_BLASTER; bolt->owner = ent; bolt->damage = ent->damage; bolt->dflags = DAMAGE_NO_KNOCKBACK | DAMAGE_HEAVY_WEAP_CLASS; // Don't push them around, or else we are constantly re-aiming bolt->splashDamage = 0; bolt->splashRadius = 0; bolt->methodOfDeath = MOD_ENERGY; bolt->clipmask = MASK_SHOT | CONTENTS_LIGHTSABER; bolt->trigger_formation = qfalse; // don't draw tail on first frame VectorSet( bolt->maxs, 1.5, 1.5, 1.5 ); VectorScale( bolt->maxs, -1, bolt->mins ); bolt->s.pos.trType = TR_LINEAR; bolt->s.pos.trTime = level.time; VectorCopy( start, bolt->s.pos.trBase ); VectorScale( dir, 1100, bolt->s.pos.trDelta ); SnapVector( bolt->s.pos.trDelta ); // save net bandwidth VectorCopy( start, bolt->currentOrigin); } //----------------------------------------------------- void turret_head_think( gentity_t *self ) //----------------------------------------------------- { // if it's time to fire and we have an enemy, then gun 'em down! pushDebounce time controls next fire time if ( self->enemy && self->pushDebounceTime < level.time && self->attackDebounceTime < level.time ) { // set up our next fire time self->pushDebounceTime = level.time + self->wait; vec3_t fwd, org; mdxaBone_t boltMatrix; // Getting the flash bolt here gi.G2API_GetBoltMatrix( self->ghoul2, self->playerModel, self->torsoBolt, &boltMatrix, self->currentAngles, self->currentOrigin, (cg.time?cg.time:level.time), NULL, self->s.modelScale ); gi.G2API_GiveMeVectorFromMatrix( boltMatrix, ORIGIN, org ); gi.G2API_GiveMeVectorFromMatrix( boltMatrix, POSITIVE_Y, fwd ); VectorMA( org, START_DIS, fwd, org ); turret_fire( self, org, fwd ); self->fly_sound_debounce_time = level.time;//used as lastShotTime } } //----------------------------------------------------- static void turret_aim( gentity_t *self ) //----------------------------------------------------- { vec3_t enemyDir, org, org2; vec3_t desiredAngles, setAngle; float diffYaw = 0.0f, diffPitch = 0.0f; // move our gun base yaw to where we should be at this time.... EvaluateTrajectory( &self->s.apos, level.time, self->currentAngles ); self->currentAngles[YAW] = AngleNormalize360( self->currentAngles[YAW] ); self->speed = AngleNormalize360( self->speed ); if ( self->enemy ) { // ...then we'll calculate what new aim adjustments we should attempt to make this frame // Aim at enemy if ( self->enemy->client ) { VectorCopy( self->enemy->client->renderInfo.eyePoint, org ); } else { VectorCopy( self->enemy->currentOrigin, org ); } if ( self->spawnflags & 2 ) { org[2] -= 15; } else { org[2] -= 5; } mdxaBone_t boltMatrix; // Getting the "eye" here gi.G2API_GetBoltMatrix( self->ghoul2, self->playerModel, self->torsoBolt, &boltMatrix, self->currentAngles, self->s.origin, (cg.time?cg.time:level.time), NULL, self->s.modelScale ); gi.G2API_GiveMeVectorFromMatrix( boltMatrix, ORIGIN, org2 ); VectorSubtract( org, org2, enemyDir ); vectoangles( enemyDir, desiredAngles ); diffYaw = AngleSubtract( self->currentAngles[YAW], desiredAngles[YAW] ); diffPitch = AngleSubtract( self->speed, desiredAngles[PITCH] ); } else { // no enemy, so make us slowly sweep back and forth as if searching for a new one // diffYaw = sin( level.time * 0.0001f + self->count ) * 5.0f; // don't do this for now since it can make it go into walls. } if ( diffYaw ) { // cap max speed.... if ( fabs(diffYaw) > 14.0f ) { diffYaw = ( diffYaw >= 0 ? 14.0f : -14.0f ); } // ...then set up our desired yaw VectorSet( setAngle, 0.0f, diffYaw, 0.0f ); VectorCopy( self->currentAngles, self->s.apos.trBase ); VectorScale( setAngle,- 5, self->s.apos.trDelta ); self->s.apos.trTime = level.time; self->s.apos.trType = TR_LINEAR; } if ( diffPitch ) { if ( fabs(diffPitch) > 3.0f ) { // cap max speed self->speed += (diffPitch > 0.0f) ? -3.0f : 3.0f; } else { // small enough, so just add half the diff so we smooth out the stopping self->speed -= ( diffPitch );//desiredAngles[PITCH]; } // Note that this is NOT interpolated, so it will be less smooth...On the other hand, it does use Ghoul2 to blend, so it may smooth it out a bit? if ( self->spawnflags & 2 ) { VectorSet( desiredAngles, self->speed, 0.0f, 0.0f ); } else { VectorSet( desiredAngles, -self->speed, 0.0f, 0.0f ); } gi.G2API_SetBoneAngles( &self->ghoul2[0], "Bone_body", desiredAngles, BONE_ANGLES_POSTMULT, POSITIVE_Y, POSITIVE_Z, POSITIVE_X, NULL, 100, cg.time ); } if ( diffYaw || diffPitch ) { self->s.loopSound = G_SoundIndex( "sound/chars/turret/move.wav" ); } else { self->s.loopSound = 0; } } //----------------------------------------------------- static void turret_turnoff( gentity_t *self ) //----------------------------------------------------- { if ( self->enemy == NULL ) { // we don't need to turnoff return; } // shut-down sound G_Sound( self, G_SoundIndex( "sound/chars/turret/shutdown.wav" )); // make turret play ping sound for 5 seconds self->aimDebounceTime = level.time + 5000; // Clear enemy self->enemy = NULL; } //----------------------------------------------------- static qboolean turret_find_enemies( gentity_t *self ) //----------------------------------------------------- { qboolean found = qfalse; int count; float bestDist = self->radius * self->radius; float enemyDist; vec3_t enemyDir, org, org2; gentity_t *entity_list[MAX_GENTITIES], *target, *bestTarget = NULL; if ( self->aimDebounceTime > level.time ) // time since we've been shut off { // We were active and alert, i.e. had an enemy in the last 3 secs if ( self->painDebounceTime < level.time ) { G_Sound(self, G_SoundIndex( "sound/chars/turret/ping.wav" )); self->painDebounceTime = level.time + 1000; } } VectorCopy( self->currentOrigin, org2 ); if ( self->spawnflags & 2 ) { org2[2] += 20; } else { org2[2] -= 20; } count = G_RadiusList( org2, self->radius, self, qtrue, entity_list ); for ( int i = 0; i < count; i++ ) { target = entity_list[i]; if ( !target->client ) { // only attack clients continue; } if ( target == self || !target->takedamage || target->health <= 0 || ( target->flags & FL_NOTARGET )) { continue; } if ( target->client->playerTeam == self->noDamageTeam ) { // A bot we don't want to shoot continue; } if ( !gi.inPVS( org2, target->currentOrigin )) { continue; } VectorCopy( target->client->renderInfo.eyePoint, org ); if ( self->spawnflags & 2 ) { org[2] -= 15; } else { org[2] += 5; } trace_t tr; gi.trace( &tr, org2, NULL, NULL, org, self->s.number, MASK_SHOT, G2_NOCOLLIDE, 0 ); if ( !tr.allsolid && !tr.startsolid && ( tr.fraction == 1.0 || tr.entityNum == target->s.number )) { // Only acquire if have a clear shot, Is it in range and closer than our best? VectorSubtract( target->currentOrigin, self->currentOrigin, enemyDir ); enemyDist = VectorLengthSquared( enemyDir ); if ( enemyDist < bestDist )// all things equal, keep current { if ( self->attackDebounceTime < level.time ) { // We haven't fired or acquired an enemy in the last 2 seconds-start-up sound G_Sound( self, G_SoundIndex( "sound/chars/turret/startup.wav" )); // Wind up turrets for a bit self->attackDebounceTime = level.time + 1400; } bestTarget = target; bestDist = enemyDist; found = qtrue; } } } if ( found ) { if ( !self->enemy ) {//just aquired one AddSoundEvent( bestTarget, self->currentOrigin, 256, AEL_DISCOVERED ); AddSightEvent( bestTarget, self->currentOrigin, 512, AEL_DISCOVERED, 20 ); } G_SetEnemy( self, bestTarget ); if ( VALIDSTRING( self->target2 )) { G_UseTargets2( self, self, self->target2 ); } } return found; } //----------------------------------------------------- void turret_base_think( gentity_t *self ) //----------------------------------------------------- { qboolean turnOff = qtrue; float enemyDist; vec3_t enemyDir, org, org2; self->nextthink = level.time + FRAMETIME; if ( self->spawnflags & 1 ) { // not turned on turret_turnoff( self ); turret_aim( self ); // No target self->flags |= FL_NOTARGET; return; } else { // I'm all hot and bothered self->flags &= ~FL_NOTARGET; } if ( !self->enemy ) { if ( turret_find_enemies( self )) { turnOff = qfalse; } } else { if ( self->enemy->health > 0 ) { // enemy is alive VectorSubtract( self->enemy->currentOrigin, self->currentOrigin, enemyDir ); enemyDist = VectorLengthSquared( enemyDir ); if ( enemyDist < self->radius * self->radius ) { // was in valid radius if ( gi.inPVS( self->currentOrigin, self->enemy->currentOrigin ) ) { // Every now and again, check to see if we can even trace to the enemy trace_t tr; if ( self->enemy->client ) { VectorCopy( self->enemy->client->renderInfo.eyePoint, org ); } else { VectorCopy( self->enemy->currentOrigin, org ); } VectorCopy( self->currentOrigin, org2 ); if ( self->spawnflags & 2 ) { org2[2] += 10; } else { org2[2] -= 10; } gi.trace( &tr, org2, NULL, NULL, org, self->s.number, MASK_SHOT, G2_NOCOLLIDE, 0 ); if ( !tr.allsolid && !tr.startsolid && tr.entityNum == self->enemy->s.number ) { turnOff = qfalse; // Can see our enemy } } } } turret_head_think( self ); } if ( turnOff ) { if ( self->bounceCount < level.time ) // bounceCount is used to keep the thing from ping-ponging from on to off { turret_turnoff( self ); } } else { // keep our enemy for a minimum of 2 seconds from now self->bounceCount = level.time + 2000 + Q_flrand(0.0f, 1.0f) * 150; } turret_aim( self ); } //----------------------------------------------------------------------------- void turret_base_use( gentity_t *self, gentity_t *other, gentity_t *activator ) //----------------------------------------------------------------------------- { // Toggle on and off self->spawnflags = (self->spawnflags ^ 1); if (( self->s.eFlags & EF_SHADER_ANIM ) && ( self->spawnflags & 1 )) // Start_Off { self->s.frame = 1; // black } else { self->s.frame = 0; // glow } } /*QUAKED misc_turret (1 0 0) (-8 -8 -22) (8 8 0) START_OFF UPSIDE_DOWN Turret that hangs from the ceiling, will aim and shoot at enemies START_OFF - Starts off UPSIDE_DOWN - make it rest on a surface/floor instead of hanging from the ceiling radius - How far away an enemy can be for it to pick it up (default 512) wait - Time between shots (default 150 ms) dmg - How much damage each shot does (default 5) health - How much damage it can take before exploding (default 100) splashDamage - How much damage the explosion does splashRadius - The radius of the explosion NOTE: If either of the above two are 0, it will not make an explosion targetname - Toggles it on/off target - What to use when destroyed target2 - What to use when it decides to start shooting at an enemy team - team that is not targeted by and does not take damage from this turret "player", "enemy", (default) "neutral" */ //----------------------------------------------------- void SP_misc_turret( gentity_t *base ) //----------------------------------------------------- { base->s.modelindex = G_ModelIndex( "models/map_objects/imp_mine/turret_canon.glm" ); base->s.modelindex2 = G_ModelIndex( "models/map_objects/imp_mine/turret_damage.md3" ); base->playerModel = gi.G2API_InitGhoul2Model( base->ghoul2, "models/map_objects/imp_mine/turret_canon.glm", base->s.modelindex, NULL_HANDLE, NULL_HANDLE, 0, 0 ); base->s.radius = 80.0f; gi.G2API_SetBoneAngles( &base->ghoul2[base->playerModel], "Bone_body", vec3_origin, BONE_ANGLES_POSTMULT, POSITIVE_Y, POSITIVE_Z, POSITIVE_X, NULL, 0, 0 ); base->torsoBolt = gi.G2API_AddBolt( &base->ghoul2[base->playerModel], "*flash03" ); finish_spawning_turret( base ); if (( base->spawnflags & 1 )) // Start_Off { base->s.frame = 1; // black } else { base->s.frame = 0; // glow } base->s.eFlags |= EF_SHADER_ANIM; } //----------------------------------------------------- void finish_spawning_turret( gentity_t *base ) { vec3_t fwd; if ( base->spawnflags & 2 ) { base->s.angles[ROLL] += 180; base->s.origin[2] -= 22.0f; } G_SetAngles( base, base->s.angles ); AngleVectors( base->currentAngles, fwd, NULL, NULL ); G_SetOrigin(base, base->s.origin); base->noDamageTeam = TEAM_ENEMY; base->s.eType = ET_GENERAL; if ( base->team && base->team[0] ) { base->noDamageTeam = TranslateTeamName( base->team ); base->team = NULL; } // Set up our explosion effect for the ExplodeDeath code.... base->fxID = G_EffectIndex( "turret/explode" ); G_EffectIndex( "spark_exp_nosnd" ); base->e_UseFunc = useF_turret_base_use; base->e_PainFunc = painF_TurretPain; // don't start working right away base->e_ThinkFunc = thinkF_turret_base_think; base->nextthink = level.time + FRAMETIME * 5; // this is really the pitch angle..... base->speed = 0; // this is a random time offset for the no-enemy-search-around-mode base->count = Q_flrand(0.0f, 1.0f) * 9000; if ( !base->health ) { base->health = 100; } // search radius if ( !base->radius ) { base->radius = 512; } // How quickly to fire if ( !base->wait ) { base->wait = 150 + Q_flrand(0.0f, 1.0f) * 55; } if ( !base->splashDamage ) { base->splashDamage = 10; } if ( !base->splashRadius ) { base->splashRadius = 25; } // how much damage each shot does if ( !base->damage ) { base->damage = 5; } if ( base->spawnflags & 2 ) {//upside-down, invert mins and maxe VectorSet( base->maxs, 10.0f, 10.0f, 30.0f ); VectorSet( base->mins, -10.0f, -10.0f, 0.0f ); } else { VectorSet( base->maxs, 10.0f, 10.0f, 0.0f ); VectorSet( base->mins, -10.0f, -10.0f, -30.0f ); } // Precache moving sounds G_SoundIndex( "sound/chars/turret/startup.wav" ); G_SoundIndex( "sound/chars/turret/shutdown.wav" ); G_SoundIndex( "sound/chars/turret/ping.wav" ); G_SoundIndex( "sound/chars/turret/move.wav" ); base->contents = CONTENTS_BODY|CONTENTS_PLAYERCLIP|CONTENTS_MONSTERCLIP|CONTENTS_SHOTCLIP; base->max_health = base->health; base->takedamage = qtrue; base->e_DieFunc = dieF_turret_die; base->material = MAT_METAL; base->svFlags |= SVF_NO_TELEPORT|SVF_NONNPC_ENEMY|SVF_SELF_ANIMATING; // Register this so that we can use it for the missile effect RegisterItem( FindItemForWeapon( WP_BLASTER )); // But set us as a turret so that we can be identified as a turret base->s.weapon = WP_TURRET; gi.linkentity( base ); } /*QUAKED misc_ns_turret (1 0 0) (-8 -8 -32) (8 8 29) START_OFF NS turret that only hangs from the ceiling, will aim and shoot at enemies START_OFF - Starts off radius - How far away an enemy can be for it to pick it up (default 512) wait - Time between shots (default 150 ms) dmg - How much damage each shot does (default 5) health - How much damage it can take before exploding (default 100) splashDamage - How much damage the explosion does splashRadius - The radius of the explosion NOTE: If either of the above two are 0, it will not make an explosion targetname - Toggles it on/off target - What to use when destroyed team - team that is not targeted by and does not take damage from this turret "player", "enemy", (default) "neutral" */ //----------------------------------------------------- void SP_misc_ns_turret( gentity_t *base ) //----------------------------------------------------- { base->s.modelindex = G_ModelIndex( "models/map_objects/nar_shaddar/turret/turret.glm" ); base->s.modelindex2 = G_ModelIndex( "models/map_objects/imp_mine/turret_damage.md3" ); // FIXME! base->playerModel = gi.G2API_InitGhoul2Model( base->ghoul2, "models/map_objects/nar_shaddar/turret/turret.glm", base->s.modelindex, NULL_HANDLE, NULL_HANDLE, 0, 0 ); base->s.radius = 80.0f; gi.G2API_SetBoneAngles( &base->ghoul2[base->playerModel], "Bone_body", vec3_origin, BONE_ANGLES_POSTMULT, POSITIVE_Y, POSITIVE_Z, POSITIVE_X, NULL, 0, 0 ); base->torsoBolt = gi.G2API_AddBolt( &base->ghoul2[base->playerModel], "*flash02" ); finish_spawning_turret( base ); } //-------------------------------------- void laser_arm_fire (gentity_t *ent) { vec3_t start, end, fwd, rt, up; trace_t trace; if ( ent->attackDebounceTime < level.time && ent->alt_fire ) { // If I'm firing the laser and it's time to quit....then quit! ent->alt_fire = qfalse; // ent->e_ThinkFunc = thinkF_NULL; // return; } ent->nextthink = level.time + FRAMETIME; // If a fool gets in the laser path, fry 'em AngleVectors( ent->currentAngles, fwd, rt, up ); VectorMA( ent->currentOrigin, 20, fwd, start ); //VectorMA( start, -6, rt, start ); //VectorMA( start, -3, up, start ); VectorMA( start, 4096, fwd, end ); gi.trace( &trace, start, NULL, NULL, end, ENTITYNUM_NONE, MASK_SHOT, G2_NOCOLLIDE, 0 );//ignore ent->fly_sound_debounce_time = level.time;//used as lastShotTime // Only deal damage when in alt-fire mode if ( trace.fraction < 1.0 && ent->alt_fire ) { if ( trace.entityNum < ENTITYNUM_WORLD ) { gentity_t *hapless_victim = &g_entities[trace.entityNum]; if ( hapless_victim && hapless_victim->takedamage && ent->damage ) { G_Damage( hapless_victim, ent, ent->nextTrain->activator, fwd, trace.endpos, ent->damage, DAMAGE_IGNORE_TEAM, MOD_UNKNOWN ); } } } if ( ent->alt_fire ) { // CG_FireLaser( start, trace.endpos, trace.plane.normal, ent->nextTrain->startRGBA, qfalse ); } else { // CG_AimLaser( start, trace.endpos, trace.plane.normal ); } } void laser_arm_use (gentity_t *self, gentity_t *other, gentity_t *activator) { vec3_t newAngles; self->activator = activator; switch( self->count ) { case 0: default: //Fire //gi.Printf("FIRE!\n"); // self->lastEnemy->lastEnemy->e_ThinkFunc = thinkF_laser_arm_fire; // self->lastEnemy->lastEnemy->nextthink = level.time + FRAMETIME; //For 3 seconds self->lastEnemy->lastEnemy->alt_fire = qtrue; // Let 'er rip! self->lastEnemy->lastEnemy->attackDebounceTime = level.time + self->lastEnemy->lastEnemy->wait; G_Sound(self->lastEnemy->lastEnemy, G_SoundIndex("sound/chars/l_arm/fire.wav")); break; case 1: //Yaw left //gi.Printf("LEFT...\n"); VectorCopy( self->lastEnemy->currentAngles, newAngles ); newAngles[1] += self->speed; G_SetAngles( self->lastEnemy, newAngles ); // bolt_head_to_arm( self->lastEnemy, self->lastEnemy->lastEnemy, LARM_FOFS, LARM_ROFS, LARM_UOFS ); G_Sound( self->lastEnemy, G_SoundIndex( "sound/chars/l_arm/move.wav" ) ); break; case 2: //Yaw right //gi.Printf("RIGHT...\n"); VectorCopy( self->lastEnemy->currentAngles, newAngles ); newAngles[1] -= self->speed; G_SetAngles( self->lastEnemy, newAngles ); // bolt_head_to_arm( self->lastEnemy, self->lastEnemy->lastEnemy, LARM_FOFS, LARM_ROFS, LARM_UOFS ); G_Sound( self->lastEnemy, G_SoundIndex( "sound/chars/l_arm/move.wav" ) ); break; case 3: //pitch up //gi.Printf("UP...\n"); //FIXME: Clamp VectorCopy( self->lastEnemy->lastEnemy->currentAngles, newAngles ); newAngles[0] -= self->speed; if ( newAngles[0] < -45 ) { newAngles[0] = -45; } G_SetAngles( self->lastEnemy->lastEnemy, newAngles ); G_Sound( self->lastEnemy->lastEnemy, G_SoundIndex( "sound/chars/l_arm/move.wav" ) ); break; case 4: //pitch down //gi.Printf("DOWN...\n"); //FIXME: Clamp VectorCopy( self->lastEnemy->lastEnemy->currentAngles, newAngles ); newAngles[0] += self->speed; if ( newAngles[0] > 90 ) { newAngles[0] = 90; } G_SetAngles( self->lastEnemy->lastEnemy, newAngles ); G_Sound( self->lastEnemy->lastEnemy, G_SoundIndex( "sound/chars/l_arm/move.wav" ) ); break; } } /*QUAKED misc_laser_arm (1 0 0) (-8 -8 -8) (8 8 8) What it does when used depends on it's "count" (can be set by a script) count: 0 (default) - Fire in direction facing 1 turn left 2 turn right 3 aim up 4 aim down speed - How fast it turns (degrees per second, default 30) dmg - How much damage the laser does 10 times a second (default 5 = 50 points per second) wait - How long the beam lasts, in seconds (default is 3) targetname - to use it target - What thing for it to be pointing at to start with "startRGBA" - laser color, Red Green Blue Alpha, range 0 to 1 (default 1.0 0.85 0.15 0.75 = Yellow-Orange) */ void laser_arm_start (gentity_t *base) { vec3_t armAngles; vec3_t headAngles; base->e_ThinkFunc = thinkF_NULL; //We're the base, spawn the arm and head gentity_t *arm = G_Spawn(); gentity_t *head = G_Spawn(); VectorCopy( base->s.angles, armAngles ); VectorCopy( base->s.angles, headAngles ); if ( base->target && base->target[0] ) {//Start out pointing at something gentity_t *targ = G_Find( NULL, FOFS(targetname), base->target ); if ( !targ ) {//couldn't find it! Com_Printf(S_COLOR_RED "ERROR : laser_arm can't find target %s!\n", base->target); } else {//point at it vec3_t dir, angles; VectorSubtract(targ->currentOrigin, base->s.origin, dir ); vectoangles( dir, angles ); armAngles[1] = angles[1]; headAngles[0] = angles[0]; headAngles[1] = angles[1]; } } //Base //Base does the looking for enemies and pointing the arm and head G_SetAngles( base, base->s.angles ); //base->s.origin[2] += 4; G_SetOrigin(base, base->s.origin); gi.linkentity(base); //FIXME: need an actual model base->s.modelindex = G_ModelIndex("models/mapobjects/dn/laser_base.md3"); base->s.eType = ET_GENERAL; G_SpawnVector4( "startRGBA", "1.0 0.85 0.15 0.75", (float *)&base->startRGBA ); //anglespeed - how fast it can track the player, entered in degrees per second, so we divide by FRAMETIME/1000 if ( !base->speed ) { base->speed = 3.0f; } else { base->speed *= FRAMETIME/1000.0f; } base->e_UseFunc = useF_laser_arm_use; base->nextthink = level.time + FRAMETIME; //Arm //Does nothing, not solid, gets removed when head explodes G_SetOrigin( arm, base->s.origin ); gi.linkentity(arm); G_SetAngles( arm, armAngles ); // bolt_head_to_arm( arm, head, LARM_FOFS, LARM_ROFS, LARM_UOFS ); arm->s.modelindex = G_ModelIndex("models/mapobjects/dn/laser_arm.md3"); //Head //Fires when enemy detected, animates, can be blown up //Need to normalize the headAngles pitch for the clamping later if ( headAngles[0] < -180 ) { headAngles[0] += 360; } else if ( headAngles[0] > 180 ) { headAngles[0] -= 360; } G_SetAngles( head, headAngles ); head->s.modelindex = G_ModelIndex("models/mapobjects/dn/laser_head.md3"); head->s.eType = ET_GENERAL; // head->svFlags |= SVF_BROADCAST;// Broadcast to all clients VectorSet( head->mins, -8, -8, -8 ); VectorSet( head->maxs, 8, 8, 8 ); head->contents = CONTENTS_BODY; //FIXME: make an index into an external string table for localization head->fullName = "Surgical Laser"; gi.linkentity(head); //dmg if ( !base->damage ) { head->damage = 5; } else { head->damage = base->damage; } base->damage = 0; //lifespan of beam if ( !base->wait ) { head->wait = 3000; } else { head->wait = base->wait * 1000; } base->wait = 0; //Precache firing and explode sounds G_SoundIndex("sound/weapons/explosions/cargoexplode.wav"); G_SoundIndex("sound/chars/l_arm/fire.wav"); G_SoundIndex("sound/chars/l_arm/move.wav"); //Link them up base->lastEnemy = arm; arm->lastEnemy = head; head->owner = arm; arm->nextTrain = head->nextTrain = base; // The head should always think, since it will be either firing a damage laser or just a target laser head->e_ThinkFunc = thinkF_laser_arm_fire; head->nextthink = level.time + FRAMETIME; head->alt_fire = qfalse; // Don't do damage until told to } void SP_laser_arm (gentity_t *base) { base->e_ThinkFunc = thinkF_laser_arm_start; base->nextthink = level.time + START_TIME_LINK_ENTS; } //-------------------------- // PERSONAL ASSAULT SENTRY //-------------------------- #define PAS_DAMAGE 2 //----------------------------------------------------------------------------- void pas_use( gentity_t *self, gentity_t *other, gentity_t *activator ) //----------------------------------------------------------------------------- { // Toggle on and off self->spawnflags = (self->spawnflags ^ 1); if ( self->spawnflags & 1 ) { self->nextthink = 0; // turn off and do nothing self->e_ThinkFunc = thinkF_NULL; } else { self->nextthink = level.time + 50; self->e_ThinkFunc = thinkF_pas_think; } } //---------------------------------------------------------------- void pas_fire( gentity_t *ent ) //---------------------------------------------------------------- { vec3_t fwd, org; mdxaBone_t boltMatrix; // Getting the flash bolt here gi.G2API_GetBoltMatrix( ent->ghoul2, ent->playerModel, ent->torsoBolt, &boltMatrix, ent->currentAngles, ent->s.origin, (cg.time?cg.time:level.time), NULL, ent->s.modelScale ); gi.G2API_GiveMeVectorFromMatrix( boltMatrix, ORIGIN, org ); gi.G2API_GiveMeVectorFromMatrix( boltMatrix, POSITIVE_Y, fwd ); G_PlayEffect( "turret/muzzle_flash", org, fwd ); gentity_t *bolt; bolt = G_Spawn(); bolt->classname = "turret_proj"; bolt->nextthink = level.time + 10000; bolt->e_ThinkFunc = thinkF_G_FreeEntity; bolt->s.eType = ET_MISSILE; bolt->s.weapon = WP_TURRET; bolt->owner = ent; bolt->damage = PAS_DAMAGE; bolt->dflags = DAMAGE_NO_KNOCKBACK; // Don't push them around, or else we are constantly re-aiming bolt->splashDamage = 0; bolt->splashRadius = 0; bolt->methodOfDeath = MOD_ENERGY; bolt->clipmask = MASK_SHOT | CONTENTS_LIGHTSABER; VectorSet( bolt->maxs, 1, 1, 1 ); VectorScale( bolt->maxs, -1, bolt->mins ); bolt->s.pos.trType = TR_LINEAR; bolt->s.pos.trTime = level.time; // move a bit on the very first frame VectorCopy( org, bolt->s.pos.trBase ); VectorScale( fwd, 900, bolt->s.pos.trDelta ); SnapVector( bolt->s.pos.trDelta ); // save net bandwidth VectorCopy( org, bolt->currentOrigin); } //----------------------------------------------------- static qboolean pas_find_enemies( gentity_t *self ) //----------------------------------------------------- { qboolean found = qfalse; int count; float bestDist = self->radius * self->radius; float enemyDist; vec3_t enemyDir, org, org2; gentity_t *entity_list[MAX_GENTITIES], *target; if ( self->aimDebounceTime > level.time ) // time since we've been shut off { // We were active and alert, i.e. had an enemy in the last 3 secs if ( self->painDebounceTime < level.time ) { G_Sound(self, G_SoundIndex( "sound/chars/turret/ping.wav" )); self->painDebounceTime = level.time + 1000; } } mdxaBone_t boltMatrix; // Getting the "eye" here gi.G2API_GetBoltMatrix( self->ghoul2, self->playerModel, self->torsoBolt, &boltMatrix, self->currentAngles, self->s.origin, (cg.time?cg.time:level.time), NULL, self->s.modelScale ); gi.G2API_GiveMeVectorFromMatrix( boltMatrix, ORIGIN, org2 ); count = G_RadiusList( org2, self->radius, self, qtrue, entity_list ); for ( int i = 0; i < count; i++ ) { target = entity_list[i]; if ( !target->client ) { continue; } if ( target == self || !target->takedamage || target->health <= 0 || ( target->flags & FL_NOTARGET )) { continue; } if ( target->client->playerTeam == self->noDamageTeam ) { // A bot we don't want to shoot continue; } if ( !gi.inPVS( org2, target->currentOrigin )) { continue; } if ( target->client ) { VectorCopy( target->client->renderInfo.eyePoint, org ); org[2] -= 15; } else { VectorCopy( target->currentOrigin, org ); } trace_t tr; gi.trace( &tr, org2, NULL, NULL, org, self->s.number, MASK_SHOT, G2_NOCOLLIDE, 0 ); if ( !tr.allsolid && !tr.startsolid && ( tr.fraction == 1.0 || tr.entityNum == target->s.number )) { // Only acquire if have a clear shot, Is it in range and closer than our best? VectorSubtract( target->currentOrigin, self->currentOrigin, enemyDir ); enemyDist = VectorLengthSquared( enemyDir ); if ( target->s.number ) // don't do this for the player { G_StartFlee( target, self, self->currentOrigin, AEL_DANGER, 3000, 5000 ); } if ( enemyDist < bestDist )// all things equal, keep current { if ( self->attackDebounceTime + 2000 < level.time ) { // We haven't fired or acquired an enemy in the last 2 seconds-start-up sound G_Sound( self, G_SoundIndex( "sound/chars/turret/startup.wav" )); // Wind up turrets for a bit self->attackDebounceTime = level.time + 900 + Q_flrand(0.0f, 1.0f) * 200; } G_SetEnemy( self, target ); bestDist = enemyDist; found = qtrue; } } } if ( found && VALIDSTRING( self->target2 )) { G_UseTargets2( self, self, self->target2 ); } return found; } //--------------------------------- void pas_adjust_enemy( gentity_t *ent ) //--------------------------------- { qboolean keep = qtrue; if ( ent->enemy->health <= 0 ) { keep = qfalse; } else// if ( Q_flrand(0.0f, 1.0f) > 0.5f ) { // do a trace every now and then. mdxaBone_t boltMatrix; vec3_t org, org2; // Getting the "eye" here gi.G2API_GetBoltMatrix( ent->ghoul2, ent->playerModel, ent->torsoBolt, &boltMatrix, ent->currentAngles, ent->s.origin, (cg.time?cg.time:level.time), NULL, ent->s.modelScale ); gi.G2API_GiveMeVectorFromMatrix( boltMatrix, ORIGIN, org2 ); if ( ent->enemy->client ) { VectorCopy( ent->enemy->client->renderInfo.eyePoint, org ); org[2] -= 15; } else { VectorCopy( ent->enemy->currentOrigin, org ); } trace_t tr; gi.trace( &tr, org2, NULL, NULL, org, ent->s.number, MASK_SHOT, G2_NOCOLLIDE, 0 ); if ( tr.allsolid || tr.startsolid || tr.entityNum != ent->enemy->s.number ) { // trace failed keep = qfalse; } } if ( keep ) { ent->bounceCount = level.time + 500 + Q_flrand(0.0f, 1.0f) * 150; } else if ( ent->bounceCount < level.time ) // don't ping pong on and off { ent->enemy = NULL; // shut-down sound G_Sound( ent, G_SoundIndex( "sound/chars/turret/shutdown.wav" )); // make turret play ping sound for 5 seconds ent->aimDebounceTime = level.time + 5000; } } //--------------------------------- void pas_think( gentity_t *ent ) //--------------------------------- { if ( !ent->damage ) { // let us do our animation, then we are good to go in terms of pounding the crap out of enemies. ent->damage = 1; gi.G2API_SetBoneAnimIndex( &ent->ghoul2[ent->playerModel], ent->rootBone, 0, 11, BONE_ANIM_OVERRIDE_FREEZE, 0.8f, cg.time, -1, -1 ); ent->nextthink = level.time + 1200; return; } if ( !ent->count ) { // turrets that have no ammo may as well do nothing return; } ent->nextthink = level.time + FRAMETIME; if ( ent->enemy ) { // make sure that the enemy is still valid pas_adjust_enemy( ent ); } if ( !ent->enemy ) { pas_find_enemies( ent ); } qboolean moved = qfalse; float diffYaw = 0.0f, diffPitch = 0.0f; vec3_t enemyDir, org; vec3_t frontAngles, backAngles; vec3_t desiredAngles; ent->speed = AngleNormalize360( ent->speed ); ent->random = AngleNormalize360( ent->random ); if ( ent->enemy ) { // ...then we'll calculate what new aim adjustments we should attempt to make this frame // Aim at enemy if ( ent->enemy->client ) { VectorCopy( ent->enemy->client->renderInfo.eyePoint, org ); org[2] -= 40; } else { VectorCopy( ent->enemy->currentOrigin, org ); } VectorSubtract( org, ent->currentOrigin, enemyDir ); vectoangles( enemyDir, desiredAngles ); diffYaw = AngleSubtract( ent->speed, desiredAngles[YAW] ); diffPitch = AngleSubtract( ent->random, desiredAngles[PITCH] ); } else { // no enemy, so make us slowly sweep back and forth as if searching for a new one diffYaw = sin( level.time * 0.0001f + ent->count ) * 2.0f; } if ( fabs(diffYaw) > 0.25f ) { moved = qtrue; if ( fabs(diffYaw) > 10.0f ) { // cap max speed ent->speed += (diffYaw > 0.0f) ? -10.0f : 10.0f; } else { // small enough ent->speed -= diffYaw; } } if ( fabs(diffPitch) > 0.25f ) { moved = qtrue; if ( fabs(diffPitch) > 4.0f ) { // cap max speed ent->random += (diffPitch > 0.0f) ? -4.0f : 4.0f; } else { // small enough ent->random -= diffPitch; } } // the bone axes are messed up, so hence some dumbness here VectorSet( frontAngles, -ent->random, 0.0f, 0.0f ); VectorSet( backAngles, 0.0f, 0.0f, ent->speed - ent->s.angles[YAW] ); gi.G2API_SetBoneAngles( &ent->ghoul2[ent->playerModel], "bone_barrel", frontAngles, BONE_ANGLES_POSTMULT, POSITIVE_Y, POSITIVE_Z, NEGATIVE_X, NULL,100,cg.time); gi.G2API_SetBoneAngles( &ent->ghoul2[ent->playerModel], "bone_gback", frontAngles, BONE_ANGLES_POSTMULT, POSITIVE_Y, POSITIVE_Z, NEGATIVE_X, NULL,100,cg.time); gi.G2API_SetBoneAngles( &ent->ghoul2[ent->playerModel], "bone_hinge", backAngles, BONE_ANGLES_POSTMULT, POSITIVE_Y, POSITIVE_Z, POSITIVE_X, NULL,100,cg.time); if ( moved ) { //ent->s.loopSound = G_SoundIndex( "sound/chars/turret/move.wav" ); } else { ent->s.loopSound = 0; } if ( ent->enemy && ent->attackDebounceTime < level.time && Q_flrand(0.0f, 1.0f) > 0.3f ) { ent->count--; if ( ent->count ) { pas_fire( ent ); ent->fly_sound_debounce_time = level.time;//used as lastShotTime } else { ent->nextthink = 0; G_Sound( ent, G_SoundIndex( "sound/chars/turret/shutdown.wav" )); } } } /*QUAKED misc_sentry_turret (1 0 0) (-16 -16 0) (16 16 24) START_OFF RESERVED personal assault sentry, like the ones you can carry in your inventory RESERVED - do no use this flag for anything, does nothing..etc. radius - How far away an enemy can be for it to pick it up (default 512) count - number of shots before thing deactivates. -1 = infinite, default 150 health - How much damage it can take before exploding (default 50) splashDamage - How much damage the explosion does splashRadius - The radius of the explosion NOTE: If either of the above two are 0, it will not make an explosion target - What to use when destroyed target2 - What to use when it decides to fire at an enemy team - team that does not take damage from this item "player", "enemy", "neutral" */ //--------------------------------- void SP_PAS( gentity_t *base ) //--------------------------------- { base->classname = "PAS"; G_SetOrigin( base, base->s.origin ); G_SetAngles( base, base->s.angles ); base->speed = base->s.angles[YAW]; base->s.modelindex = G_ModelIndex( "models/items/psgun.glm" ); base->playerModel = gi.G2API_InitGhoul2Model( base->ghoul2, "models/items/psgun.glm", base->s.modelindex, NULL_HANDLE, NULL_HANDLE, 0, 0 ); base->s.radius = 30.0f; VectorSet( base->s.modelScale, 1.0f, 1.0f, 1.0f ); base->rootBone = gi.G2API_GetBoneIndex( &base->ghoul2[base->playerModel], "model_root", qtrue ); gi.G2API_SetBoneAngles( &base->ghoul2[base->playerModel], "bone_hinge", vec3_origin, BONE_ANGLES_POSTMULT, POSITIVE_Y, POSITIVE_Z, POSITIVE_X, NULL, 0, 0 ); gi.G2API_SetBoneAngles( &base->ghoul2[base->playerModel], "bone_gback", vec3_origin, BONE_ANGLES_POSTMULT, POSITIVE_Y, POSITIVE_Z, POSITIVE_X, NULL, 0, 0 ); gi.G2API_SetBoneAngles( &base->ghoul2[base->playerModel], "bone_barrel", vec3_origin, BONE_ANGLES_POSTMULT, POSITIVE_Y, POSITIVE_Z, POSITIVE_X, NULL, 0, 0 ); base->torsoBolt = gi.G2API_AddBolt( &base->ghoul2[base->playerModel], "*flash02" ); base->s.eType = ET_GENERAL; if ( !base->radius ) { base->radius = 512; } if ( base->count == 0 ) { // give ammo base->count = 150; } base->e_UseFunc = useF_pas_use; base->damage = 0; // start animation flag base->contents = CONTENTS_SHOTCLIP|CONTENTS_CORPSE;//for certain traces VectorSet( base->mins, -8, -8, 0 ); VectorSet( base->maxs, 8, 8, 18 ); if ( !(base->spawnflags & 1 )) // START_OFF { base->nextthink = level.time + 1000; // we aren't starting off, so start working right away base->e_ThinkFunc = thinkF_pas_think; } // Set up our explosion effect for the ExplodeDeath code.... base->fxID = G_EffectIndex( "turret/explode" ); G_EffectIndex( "spark_exp_nosnd" ); if ( !base->health ) { base->health = 50; } base->max_health = base->health; base->takedamage = qtrue; base->e_PainFunc = painF_TurretPain; base->e_DieFunc = dieF_turret_die; // hack this flag on so that when it calls the turret die code, it will orient the effect up // HACK //-------------------------------------- base->spawnflags |= 2; // Use this for our missile effect RegisterItem( FindItemForWeapon( WP_TURRET )); base->s.weapon = WP_TURRET; base->svFlags |= SVF_NONNPC_ENEMY; base->noDamageTeam = TEAM_NEUTRAL; if ( base->team && base->team[0] ) { base->noDamageTeam = TranslateTeamName( base->team ); base->team = NULL; } gi.linkentity( base ); } //------------------------------------------------------------------------ qboolean place_portable_assault_sentry( gentity_t *self, vec3_t origin, vec3_t angs ) //------------------------------------------------------------------------ { vec3_t fwd, pos; vec3_t mins, maxs; trace_t tr; gentity_t *pas; VectorSet( maxs, 9, 9, 0 ); VectorScale( maxs, -1, mins ); angs[PITCH] = 0; angs[ROLL] = 0; AngleVectors( angs, fwd, NULL, NULL ); // and move a consistent distance away from us so we don't have the dumb thing spawning inside of us. VectorMA( origin, 30, fwd, pos ); gi.trace( &tr, origin, NULL, NULL, pos, self->s.number, MASK_SHOT, G2_NOCOLLIDE, 0 ); // find the ground tr.endpos[2] += 20; VectorCopy( tr.endpos, pos ); pos[2] -= 64; gi.trace( &tr, tr.endpos, mins, maxs, pos, self->s.number, MASK_SHOT, G2_NOCOLLIDE, 0 ); // check for a decent surface, meaning mostly flat...should probably also check surface parms so we don't set us down on lava or something. if ( !tr.startsolid && !tr.allsolid && tr.fraction < 1.0f && tr.plane.normal[2] > 0.9f && tr.entityNum >= ENTITYNUM_WORLD ) { // Then spawn us if it seems cool. pas = G_Spawn(); if ( pas ) { VectorCopy( tr.endpos, pas->s.origin ); SP_PAS( pas ); pas->contents |= CONTENTS_PLAYERCLIP; // player placed ones can block players but not npcs pas->e_UseFunc = useF_NULL; // placeable ones never need to be used // we don't hurt us or anyone who belongs to the same team as us. if ( self->client ) { pas->noDamageTeam = self->client->playerTeam; } G_Sound( self, G_SoundIndex( "sound/player/use_sentry" )); pas->activator = self; return qtrue; } } return qfalse; } //------------- // ION CANNON //------------- //---------------------------------------- void ion_cannon_think( gentity_t *self ) //---------------------------------------- { if ( self->spawnflags & 2 ) { if ( self->count ) { // still have bursts left, so keep going self->count--; } else { // done with burst, so wait delay amount, plus a random bit self->nextthink = level.time + ( self->delay + Q_flrand(-1.0f, 1.0f) * self->random ); self->count = Q_irand(0,5); // 0-5 bursts // Not firing this time return; } } if ( self->fxID ) { vec3_t fwd, org; mdxaBone_t boltMatrix; // Getting the flash bolt here gi.G2API_GetBoltMatrix( self->ghoul2, self->playerModel, self->torsoBolt, &boltMatrix, self->s.angles, self->s.origin, (cg.time?cg.time:level.time), NULL, self->s.modelScale ); gi.G2API_GiveMeVectorFromMatrix( boltMatrix, ORIGIN, org ); gi.G2API_GiveMeVectorFromMatrix( boltMatrix, POSITIVE_Y, fwd ); G_PlayEffect( self->fxID, org, fwd ); } if ( self->target2 ) { // If we have a target2 fire it off in sync with our gun firing G_UseTargets2( self, self, self->target2 ); } gi.G2API_SetBoneAnimIndex( &self->ghoul2[self->playerModel], self->rootBone, 0, 8, BONE_ANIM_OVERRIDE_FREEZE, 0.6f, cg.time, -1, -1 ); self->nextthink = level.time + self->wait + Q_flrand(-1.0f, 1.0f) * self->random; } //---------------------------------------------------------------------------------------------- void ion_cannon_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod,int dFlags,int hitLoc ) //---------------------------------------------------------------------------------------------- { vec3_t org; // dead, so nuke the ghoul model and put in the damage md3 version if ( self->playerModel >= 0 ) { gi.G2API_RemoveGhoul2Model( self->ghoul2, self->playerModel ); } self->s.modelindex = self->s.modelindex2; self->s.modelindex2 = 0; // Turn off the thinking of the base & use it's targets self->e_ThinkFunc = thinkF_NULL; self->e_UseFunc = useF_NULL; if ( self->target ) { G_UseTargets( self, attacker ); } // clear my data self->e_DieFunc = dieF_NULL; self->takedamage = qfalse; self->health = 0; self->takedamage = qfalse;//stop chain reaction runaway loops self->s.loopSound = 0; // not solid anymore self->contents = 0; VectorCopy( self->currentOrigin, self->s.pos.trBase ); VectorCopy( self->currentOrigin, org ); org[2] += 20; G_PlayEffect( "env/ion_cannon_explosion", org ); if ( self->splashDamage > 0 && self->splashRadius > 0 ) { G_RadiusDamage( self->currentOrigin, attacker, self->splashDamage, self->splashRadius, attacker, MOD_UNKNOWN ); } gi.linkentity( self ); } //---------------------------------------------------------------------------- void ion_cannon_use( gentity_t *self, gentity_t *other, gentity_t *activator ) //---------------------------------------------------------------------------- { // toggle if ( self->e_ThinkFunc == thinkF_NULL ) { // start thinking now self->e_ThinkFunc = thinkF_ion_cannon_think; self->nextthink = level.time + FRAMETIME; // fires right on being used } else { self->e_ThinkFunc = thinkF_NULL; } } /*QUAKED misc_ion_cannon (1 0 0) (-140 -140 0) (140 140 320) START_OFF BURSTS SHIELDED Huge ion cannon, like the ones at the rebel base on Hoth. START_OFF - Starts off BURSTS - adds more variation, shots come out in bursts SHIELDED - cannon is shielded, any kind of shot bounces off. wait - How fast it shoots (default 1500 ms between shots, can't be less than 500 ms) random - milliseconds wait variation (default 400 ms...up to plus or minus .4 seconds) delay - Number of milliseconds between bursts (default 6000 ms, can't be less than 1000 ms, only works when BURSTS checked) health - default 2000 splashDamage - how much damage to do when it dies, must be greater than 0 to actually work splashRadius - damage radius, must be greater than 0 to actually work targetname - Toggles it on/off target - What to use when destroyed target2 - What to use when it fires a shot. */ //----------------------------------------------------- void SP_misc_ion_cannon( gentity_t *base ) //----------------------------------------------------- { G_SetAngles( base, base->s.angles ); G_SetOrigin(base, base->s.origin); base->s.modelindex = G_ModelIndex( "models/map_objects/imp_mine/ion_cannon.glm" ); base->playerModel = gi.G2API_InitGhoul2Model( base->ghoul2, "models/map_objects/imp_mine/ion_cannon.glm", base->s.modelindex, NULL_HANDLE, NULL_HANDLE, 0, 0 ); base->s.radius = 320.0f; VectorSet( base->s.modelScale, 1.0f, 1.0f, 1.0f ); base->rootBone = gi.G2API_GetBoneIndex( &base->ghoul2[base->playerModel], "model_root", qtrue ); base->torsoBolt = gi.G2API_AddBolt( &base->ghoul2[base->playerModel], "*flash02" ); // register damage model base->s.modelindex2 = G_ModelIndex( "models/map_objects/imp_mine/ion_cannon_damage.md3" ); base->e_UseFunc = useF_ion_cannon_use; // How quickly to fire if ( base->wait == 0.0f ) { base->wait = 1500.0f; } else if ( base->wait < 500.0f ) { base->wait = 500.0f; } if ( base->random == 0.0f ) { base->random = 400.0f; } if ( base->delay == 0 ) { base->delay = 6000; } else if ( base->delay < 1000 ) { base->delay = 1000; } // we only take damage from a heavy weapon class missile base->flags |= FL_DMG_BY_HEAVY_WEAP_ONLY; if ( base->spawnflags & 4 )//shielded { base->flags |= FL_SHIELDED; //technically, this would only take damage from a lightsaber, but the other flag just above would cancel that out too. } G_SpawnInt( "health", "2000", &base->health ); base->e_DieFunc = dieF_ion_cannon_die; base->takedamage = qtrue; // Start Off? if ( base->spawnflags & 1 ) { base->e_ThinkFunc = thinkF_NULL; } else { // start thinking now, otherwise, we'll wait until we are used base->e_ThinkFunc = thinkF_ion_cannon_think; base->nextthink = level.time + base->wait + Q_flrand(-1.0f, 1.0f) * base->random; } // Bursts? if ( base->spawnflags & 2 ) { base->count = Q_irand(0,5); // 0-5 bursts } // precache base->fxID = G_EffectIndex( "env/ion_cannon" ); // Set up our explosion effect for the ExplodeDeath code.... G_EffectIndex( "env/ion_cannon_explosion" ); base->contents = CONTENTS_BODY; VectorSet( base->mins, -141.0f, -148.0f, 0.0f ); VectorSet( base->maxs, 142.0f, 135.0f, 245.0f ); gi.linkentity( base ); } //----------------------------------------------------- void spotlight_use( gentity_t *self, gentity_t *other, gentity_t *activator ) { if ( self->e_ThinkFunc == thinkF_NULL ) { // start thinking now, otherwise, we'll wait until we are used self->e_ThinkFunc = thinkF_spotlight_think; self->nextthink = level.time + FRAMETIME; } else { self->e_ThinkFunc = thinkF_NULL; self->s.eFlags &= ~EF_ALT_FIRING; } } //----------------------------------------------------- void spotlight_think( gentity_t *ent ) { vec3_t dir, end; trace_t tr; // dumb hack flag so that we can draw an interpolated light cone cgame side. ent->s.eFlags |= EF_ALT_FIRING; VectorSubtract( ent->enemy->currentOrigin, ent->currentOrigin, dir ); VectorNormalize( dir ); vectoangles( dir, ent->s.apos.trBase ); ent->s.apos.trType = TR_INTERPOLATE; VectorMA( ent->currentOrigin, 2048, dir, end ); // just pick some max trace distance gi.trace( &tr, ent->currentOrigin, vec3_origin, vec3_origin, end, ent->s.number, CONTENTS_SOLID, G2_NOCOLLIDE, 0 ); ent->radius = tr.fraction * 2048.0f; if ( tr.fraction < 1 ) { if ( DistanceSquared( tr.endpos, g_entities[0].currentOrigin ) < 140 * 140 ) { // hit player--use target2 G_UseTargets2( ent, &g_entities[0], ent->target2 ); #ifndef FINAL_BUILD if ( g_developer->integer == PRINT_DEVELOPER ) { Com_Printf( S_COLOR_MAGENTA "Spotlight hit player at time: %d!!!\n", level.time ); } #endif } } ent->nextthink = level.time + 50; } //----------------------------------------------------- void spotlight_link( gentity_t *ent ) { gentity_t *target = 0; target = G_Find( target, FOFS(targetname), ent->target ); if ( !target ) { Com_Printf( S_COLOR_RED "ERROR: spotlight_link: bogus target %s\n", ent->target ); G_FreeEntity( ent ); return; } ent->enemy = target; // Start Off? if ( ent->spawnflags & 1 ) { ent->e_ThinkFunc = thinkF_NULL; ent->s.eFlags &= ~EF_ALT_FIRING; } else { // start thinking now, otherwise, we'll wait until we are used ent->e_ThinkFunc = thinkF_spotlight_think; ent->nextthink = level.time + FRAMETIME; } } /*QUAKED misc_spotlight (1 0 0) (-10 -10 0) (10 10 10) START_OFF #MODELNAME="models/map_objects/imp_mine/spotlight.md3" Search spotlight that must be targeted at a func_train or other entity Uses its target2 when it detects the player START_OFF - Starts off targetname - Toggles it on/off target - What to point at target2 - What to use when detects player */ //----------------------------------------------------- void SP_misc_spotlight( gentity_t *base ) //----------------------------------------------------- { if ( !base->target ) { Com_Printf( S_COLOR_RED "ERROR: misc_spotlight must have a target\n" ); G_FreeEntity( base ); return; } G_SetAngles( base, base->s.angles ); G_SetOrigin( base, base->s.origin ); base->s.modelindex = G_ModelIndex( "models/map_objects/imp_mine/spotlight.md3" ); G_SpawnInt( "health", "300", &base->health ); // Set up our lightcone effect, though we will have it draw cgame side so it looks better G_EffectIndex( "env/light_cone" ); base->contents = CONTENTS_BODY; base->e_UseFunc = useF_spotlight_use; // the thing we need to target may not have spawned yet, so try back in a bit base->e_ThinkFunc = thinkF_spotlight_link; base->nextthink = level.time + 100; gi.linkentity( base ); } /*QUAKED misc_panel_turret (0 0 1) (-8 -8 -12) (8 8 16) HEALTH Creates a turret that, when the player uses a panel, takes control of this turret and adopts the turret view HEALTH - gun turret has health and displays a hud with its current health "target" - thing to use when player enters the turret view "target2" - thing to use when player leaves the turret view "target3" - thing to use when it dies. radius - the max yaw range in degrees, (default 90) which means you can move 90 degrees on either side of the start angles. random - the max pitch range in degrees, (default 60) which means you can move 60 degrees above or below the start angles. delay - time between shots, in milliseconds (default 200). damage - amount of damage shots do, (default 50). speed - missile speed, (default 3000) heatlh - how much heatlh the thing has, (default 200) only works if HEALTH is checked, otherwise it can't be destroyed. */ extern gentity_t *player; extern qboolean G_ClearViewEntity( gentity_t *ent ); extern void G_SetViewEntity( gentity_t *self, gentity_t *viewEntity ); extern gentity_t *CreateMissile( vec3_t org, vec3_t dir, float vel, int life, gentity_t *owner, qboolean altFire = qfalse ); void panel_turret_shoot( gentity_t *self, vec3_t org, vec3_t dir) { gentity_t *missile = CreateMissile( org, dir, self->speed, 10000, self ); missile->classname = "b_proj"; missile->s.weapon = WP_EMPLACED_GUN; VectorSet( missile->maxs, 7, 7, 7 ); VectorScale( missile->maxs, -1, missile->mins ); missile->bounceCount = 0; missile->damage = self->damage; missile->dflags = DAMAGE_DEATH_KNOCKBACK; missile->methodOfDeath = MOD_ENERGY; missile->clipmask = MASK_SHOT | CONTENTS_LIGHTSABER; G_SoundOnEnt( self, CHAN_AUTO, "sound/movers/objects/ladygun_fire" ); VectorMA( org, 32, dir, org ); org[2] -= 5; G_PlayEffect( "emplaced/muzzle_flash", org, dir ); } //----------------------------------------- void misc_panel_turret_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod, int dFlags, int hitLoc ) { if ( self->target3 ) { G_UseTargets2( self, player, self->target3 ); } // FIXME: might need some other kind of logic or functionality in here?? G_UseTargets2( self, player, self->target2 ); G_ClearViewEntity( player ); cg.overrides.active &= ~CG_OVERRIDE_FOV; cg.overrides.fov = 0; } //----------------------------------------- void panel_turret_think( gentity_t *self ) { // Ensure that I am the viewEntity if ( player && player->client && player->client->ps.viewEntity == self->s.number ) { usercmd_t *ucmd = &player->client->usercmd; // We are the viewEnt so update our new viewangles based on the sum of our start angles and the ucmd angles for ( int i = 0; i < 3; i++ ) { // convert our base angle to a short, add with the usercmd.angle ( a short ), then switch use back to a real angle self->s.apos.trBase[i] = AngleNormalize180( SHORT2ANGLE( ucmd->angles[i] + ANGLE2SHORT( self->s.angles[i] ) + self->pos3[i] )); } // Only clamp if we have a PITCH clamp if ( self->random != 0.0f ) // Angle clamping -- PITCH { if ( self->s.apos.trBase[PITCH] > self->random ) // random is PITCH { self->pos3[PITCH] += ANGLE2SHORT( AngleNormalize180( self->random - self->s.apos.trBase[PITCH])); self->s.apos.trBase[PITCH] = self->random; } else if ( self->s.apos.trBase[PITCH] < -self->random ) { self->pos3[PITCH] -= ANGLE2SHORT( AngleNormalize180( self->random + self->s.apos.trBase[PITCH])); self->s.apos.trBase[PITCH] = -self->random; } } // Only clamp if we have a YAW clamp if ( self->radius != 0.0f ) { float yawDif = AngleSubtract( self->s.apos.trBase[YAW], self->s.angles[YAW] ); // Angle clamping -- YAW if ( yawDif > self->radius ) // radius is YAW { self->pos3[YAW] += ANGLE2SHORT( self->radius - yawDif ); self->s.apos.trBase[YAW] = AngleNormalize180( self->s.angles[YAW] + self->radius ); } else if ( yawDif < -self->radius ) // radius is YAW { self->pos3[YAW] -= ANGLE2SHORT( self->radius + yawDif ); self->s.apos.trBase[YAW] = AngleNormalize180( self->s.angles[YAW] - self->radius ); } } // Let cgame interpolation smooth out the angle changes self->s.apos.trType = TR_INTERPOLATE; self->s.pos.trType = TR_INTERPOLATE; // not really moving, but this fixes an interpolation bug in cg_ents. // Check for backing out of turret if ( ( self->useDebounceTime < level.time ) && ((ucmd->buttons & BUTTON_BLOCKING) || (ucmd->buttons & BUTTON_USE) || ucmd->upmove) ) // || ucmd->forwardmove || ucmd->rightmove) ) { self->useDebounceTime = level.time + 200; G_UseTargets2( self, player, self->target2 ); G_ClearViewEntity( player ); G_Sound( player, self->soundPos2 ); cg.overrides.active &= ~CG_OVERRIDE_FOV; cg.overrides.fov = 0; if ( ucmd->upmove > 0 ) {//stop player from doing anything for a half second after player->aimDebounceTime = level.time + 500; } // can be drawn // self->s.eFlags &= ~EF_NODRAW; } else { // don't draw me when being looked through // self->s.eFlags |= EF_NODRAW; // self->s.modelindex = 0; // we only need to think when we are being used self->nextthink = level.time + 50; cg.overrides.active |= CG_OVERRIDE_FOV; cg.overrides.fov = 50; } if ( ucmd->buttons & BUTTON_ATTACK || ucmd->buttons & BUTTON_ALT_ATTACK ) { if ( self->attackDebounceTime < level.time ) { vec3_t dir, pt; AngleVectors( self->s.apos.trBase, dir, NULL, NULL ); VectorCopy( self->currentOrigin, pt ); pt[2] -= 4; panel_turret_shoot( self, pt, dir ); self->attackDebounceTime = level.time + self->delay; } } } } //----------------------------------------- void panel_turret_use( gentity_t *self, gentity_t *other, gentity_t *activator ) { // really only usable by the player if ( !activator || !activator->client || activator->s.number ) { return; } if ( self->useDebounceTime > level.time ) { // can't use it again right away. return; } if ( self->spawnflags & 1 ) // health...presumably the lady luck gun { G_Sound( self, G_SoundIndex( "sound/movers/objects/ladygun_on" )); } self->useDebounceTime = level.time + 200; // Compensating for the difference between the players view at the time of use and the start angles that the gun object has self->pos3[PITCH] = -activator->client->usercmd.angles[PITCH]; self->pos3[YAW] = -activator->client->usercmd.angles[YAW]; self->pos3[ROLL] = 0; // set me as view entity G_UseTargets2( self, activator, self->target ); G_SetViewEntity( activator, self ); G_Sound( activator, self->soundPos1 ); self->e_ThinkFunc = thinkF_panel_turret_think; // panel_turret_think( self ); self->nextthink = level.time + 150; } //----------------------------------------- void SP_misc_panel_turret( gentity_t *self ) { G_SpawnFloat( "radius", "90", &self->radius ); // yaw G_SpawnFloat( "random", "60", &self->random ); // pitch G_SpawnFloat( "speed" , "3000", &self->speed ); G_SpawnInt( "delay", "200", &self->delay ); G_SpawnInt( "damage", "50", &self->damage ); VectorSet( self->pos3, 0.0f, 0.0f, 0.0f ); if ( self->spawnflags & 1 ) // heatlh { self->takedamage = qtrue; self->contents = CONTENTS_SHOTCLIP; G_SpawnInt( "health", "200", &self->health ); self->max_health = self->health; self->dflags |= DAMAGE_CUSTOM_HUD; // dumb, but we draw a custom hud G_SoundIndex( "sound/movers/objects/ladygun_on" ); } self->s.modelindex = G_ModelIndex( "models/map_objects/imp_mine/ladyluck_gun.md3" ); self->soundPos1 = G_SoundIndex( "sound/movers/camera_on.mp3" ); self->soundPos2 = G_SoundIndex( "sound/movers/camera_off.mp3" ); G_SoundIndex( "sound/movers/objects/ladygun_fire" ); G_SetOrigin( self, self->s.origin ); G_SetAngles( self, self->s.angles ); VectorSet( self->mins, -8, -8, -12 ); VectorSet( self->maxs, 8, 8, 0 ); self->contents = CONTENTS_SOLID; self->s.weapon = WP_TURRET; RegisterItem( FindItemForWeapon( WP_EMPLACED_GUN )); gi.linkentity( self ); self->e_UseFunc = useF_panel_turret_use; self->e_DieFunc = dieF_misc_panel_turret_die; }