#include "g_local.h" void angleToward(edict_t *self, vec3_t point, float speed); // spawnflags #define AC_SF_START_OFF 1 #define AC_SF_BERSERK 2 #define AC_SF_BERSERK_TOGGLE 4 // variables #define AC_RANGE 2048 #define AC_TIMEOUT 2.0 #define AC_EXPLODE_DMG 150 #define AC_EXPLODE_RADIUS 384 #define AC_TURN_SPEED 6.0 #define AC_TURN_DELAY 1.0 // states #define AC_S_IDLE 0 #define AC_S_ACTIVATING 1 #define AC_S_ACTIVE 2 #define AC_S_DEACTIVATING 3 // models char* models[] = { NULL, "models/objects/acannon/chain/tris.md2", "models/objects/acannon/rocket/tris.md2", "models/objects/acannon/laser/tris.md2", "models/objects/acannon/laser/tris.md2" }; char* floorModels[] = { NULL, "", "models/objects/acannon/rocket2/tris.md2", "models/objects/acannon/laser2/tris.md2", "models/objects/acannon/laser2/tris.md2" }; // pitch extents const int acPitchExtents[2][2] = { {0,60}, // max, min {-60,0} }; // frames filler/chain/rocket/laser const int acIdleStart[] = { 0, 0, 0, 0, 0 }; const int acIdleEnd[] = { 0, 0, 0, 0, 0 }; const int acActStart[] = { 0, 1, 1, 1, 1 }; const int acActEnd[] = { 0, 9, 9, 9, 9 }; const int acActiveStart[] = { 0, 10, 10, 10, 10 }; const int acActiveEnd[] = { 0, 10, 10, 10, 10 }; typedef struct ac_anim_frame_s { qboolean last; qboolean fire; int frame; } ac_anim_frame_t; typedef struct ac_anim_s { int firstNonPause; ac_anim_frame_t frames[32]; } ac_anim_t; ac_anim_t acFiringFrames[5] = { // dummy { 0, { true, false, -1 } }, // chaingun { 6, { { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, // start of firing sequence { false, true, 11 }, { false, false, 12 }, { false, true, 13 }, { false, false, 14 }, { false, true, 15 }, { false, false, 16 }, { false, true, 17 }, { false, false, 18 }, { false, true, 19 }, { false, false, 20 }, { false, true, 21 }, { true, false, 2 }, } }, // rockets { 6, { { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, // start of firing sequence { false, true, 11 }, { false, false, 11 }, { false, false, 12 }, { false, false, 12 }, { false, false, 13 }, { false, false, 13 }, { false, false, 14 }, { false, false, 14 }, { false, false, 15 }, { false, false, 15 }, { false, false, 16 }, { true, false, 16 }, } }, // laser { 6, { { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, // start of firing sequence { false, true, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { true, false, 11 }, } }, // slow laser { 6, { { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, // start of firing sequence { false, true, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { false, false, 11 }, { true, false, 11 }, } } }; vec3_t fireOffset[5] = { {0,0,0}, {24,-4,0}, {0,-4,0}, {24,-5,0}, {24,-5,0} }; const int acDeactStart[] = { 0, 23, 23, 23, 23 }; const int acDeactEnd[] = { 0, 31, 31, 31, 31 }; const qboolean turretIdle[] = { false, false, true, true }; // collapse when idle? // turret animations const int turretIdleStart = 0; const int turretIdleEnd = 0; const int turretActStart = 1; const int turretActEnd = 9; const int turretActiveStart = 10; const int turretActiveEnd = 10; const int turretDeactStart = 23; const int turretDeactEnd = 31; // bullet params #define AC_BULLET_DMG 4.0 #define AC_BULLET_KICK 2.0 // rocket params #define AC_ROCKET_DMG 100 #define AC_ROCKET_SPEED 650 #define AC_ROCKET_RADIUS_DMG 120 #define AC_ROCKET_DMG_RADIUS 120 // blaster params #define AC_BLASTER_DMG 20 #define AC_BLASTER_SPEED 1000 void monster_autocannon_fire(edict_t *self) { vec3_t forward, right, start; // fire straight ahead AngleVectors (self->s.angles, forward, right, NULL); if (self->onFloor) VectorNegate(right, right); VectorMA(self->s.origin, 24, forward, start); G_ProjectSource (self->s.origin, fireOffset[self->style], forward, right, start); if(EMPNukeCheck(self, start)) { gi.sound (self, CHAN_AUTO, gi.soundindex("items/empnuke/emp_missfire.wav"), 1, ATTN_NORM, 0); return; } // what to fire? switch(self->style) { case 1: default: fire_bullet(self, start, forward, AC_BULLET_DMG, AC_BULLET_KICK, DEFAULT_BULLET_HSPREAD, DEFAULT_BULLET_VSPREAD, MOD_AUTOCANNON); gi.WriteByte (svc_muzzleflash); gi.WriteShort (self - g_edicts); gi.WriteByte (MZ_CHAINGUN2); gi.multicast (self->s.origin, MULTICAST_PVS); break; case 2: fire_rocket(self, start, forward, AC_ROCKET_DMG, AC_ROCKET_SPEED, AC_ROCKET_RADIUS_DMG, AC_ROCKET_DMG_RADIUS); gi.WriteByte (svc_muzzleflash); gi.WriteShort (self - g_edicts); gi.WriteByte (MZ_ROCKET); gi.multicast (self->s.origin, MULTICAST_PVS); break; case 3: case 4: fire_blaster (self, start, forward, AC_BLASTER_DMG, AC_BLASTER_SPEED, EF_HYPERBLASTER, true); gi.WriteByte (svc_muzzleflash); gi.WriteShort (self - g_edicts); gi.WriteByte (MZ_HYPERBLASTER); gi.multicast (self->s.origin, MULTICAST_PVS); break; } } qboolean angleBetween(float *ang, float *min, float *max) { // directly between? if (*ang > *min && *ang < *max) return true; // make positive while(*min < 0) *min += 360.0; while(*ang < *min) *ang += 360.0; while(*max < *min) *max += 360.0; if (*ang > *min && *ang < *max) return true; else return false; } float mod180(float val) { while(val > 180) val -= 360.0; while(val < -180) val += 360.0; return val; } qboolean canShoot(edict_t *self, edict_t *e) { vec3_t delta; vec3_t dangles; VectorSubtract(e->s.origin, self->s.origin, delta); vectoangles(delta, dangles); dangles[PITCH] = mod180(dangles[PITCH]); if ((!self->onFloor && dangles[PITCH] < 0) || (self->onFloor && dangles[PITCH] > 0)) // facing up or down return false; if (self->monsterinfo.linkcount > 0) { float ideal_yaw = self->monsterinfo.attack_state; float max_yaw = anglemod(ideal_yaw + self->monsterinfo.linkcount); float min_yaw = anglemod(ideal_yaw - self->monsterinfo.linkcount); if (!angleBetween(&dangles[YAW], &min_yaw, &max_yaw)) return false; } return true; } qboolean autocannonInfront (edict_t *self, edict_t *other) { vec3_t vec; vec3_t angle; float dot; float min = -30.0; float max = 30.0; // what's the yaw distance between the 2? VectorSubtract (other->s.origin, self->s.origin, vec); vectoangles(vec, angle); dot = angle[YAW] - self->s.angles[YAW]; if (angleBetween(&dot, &min, &max)) return true; return false; } void monster_autocannon_findenemy(edict_t *self) { edict_t *e = NULL; // can we still use our enemy? if (self->enemy) { if (!canShoot(self, self->enemy)) { self->oldenemy = NULL; self->enemy = NULL; } else if (!visible(self, self->enemy)) { self->oldenemy = self->enemy; self->enemy = NULL; } else if (self->enemy->flags & FL_NOTARGET) { self->oldenemy = NULL; self->enemy = NULL; } else if (self->enemy->health <= 0) { self->oldenemy = NULL; self->enemy = NULL; } } while(self->enemy == NULL) { e = findradius(e, self->s.origin, AC_RANGE); if (e == NULL) { if (self->oldenemy == NULL) return; if (level.time > self->timeout) { self->oldenemy = NULL; return; } self->enemy = self->oldenemy; break; } if (self->spawnflags & AC_SF_BERSERK) { // attack clients and monsters if (!e->client && !(e->svflags & SVF_MONSTER)) continue; } else { // only attack clients if (!e->client) continue; } // don't target dead stuff if (e->health <= 0) continue; // don't target notarget stuff if (e->flags & FL_NOTARGET) continue; // don't target other autocannons if (Q_stricmp(e->classname, "monster_autocannon") == 0) continue; // don't target self if (e == self) continue; // can it be seen? if (!visible(self, e)) continue; if (!autocannonInfront(self, e)) continue; if (canShoot(self, e)) self->enemy = e; } } void monster_autocannon_turn(edict_t *self) { vec3_t old_angles; VectorCopy(self->s.angles, old_angles); if (!self->enemy) { if (self->monsterinfo.linkcount > 0) { int ideal_yaw = self->monsterinfo.attack_state; int max_yaw = anglemod(ideal_yaw + self->monsterinfo.linkcount); int min_yaw = anglemod(ideal_yaw - self->monsterinfo.linkcount); while (max_yaw < min_yaw) max_yaw += 360.0; self->s.angles[YAW] += (self->monsterinfo.lefty ? -AC_TURN_SPEED : AC_TURN_SPEED); // back and forth if (self->s.angles[YAW] > max_yaw) { self->monsterinfo.lefty = 1; self->s.angles[YAW] = max_yaw; } else if (self->s.angles[YAW] < min_yaw) { self->monsterinfo.lefty = 0; self->s.angles[YAW] = min_yaw; } } else { self->s.angles[YAW] = anglemod(self->s.angles[YAW] + AC_TURN_SPEED); } // angle pitch towards 5 to 10... if (!self->onFloor) { if (self->s.angles[PITCH] > 10) self->s.angles[PITCH] -= 4; else if (self->s.angles[PITCH] < 5) self->s.angles[PITCH] += 4; } else { if (self->s.angles[PITCH] < -10) self->s.angles[PITCH] += 4; else if (self->s.angles[PITCH] > -5) self->s.angles[PITCH] -= 4; } } else { // look toward enemy mid point if (visible(self, self->enemy)) { vec3_t offset, dest; VectorCopy(self->enemy->mins, offset); VectorAdd(offset, self->enemy->maxs, offset); VectorScale(offset, 0.65, offset); VectorAdd(self->enemy->s.origin, offset, dest); angleToward(self, dest, AC_TURN_SPEED); VectorCopy(dest, self->monsterinfo.last_sighting); self->timeout = level.time + AC_TIMEOUT; // restrict our range of movement if need be if (self->monsterinfo.linkcount > 0) { float amax = anglemod(self->monsterinfo.attack_state + self->monsterinfo.linkcount); float amin = anglemod(self->monsterinfo.attack_state - self->monsterinfo.linkcount); self->s.angles[YAW] = anglemod(self->s.angles[YAW]); if (!angleBetween(&self->s.angles[YAW], &amin, &amax)) { // which is closer? if (self->s.angles[YAW] - amax < amin - self->s.angles[YAW]) self->s.angles[YAW] = amin; else self->s.angles[YAW] = amax; } } } else // not visible now, so head toward last known spot angleToward(self, self->monsterinfo.last_sighting, AC_TURN_SPEED); } // get our angles between 180 and -180 while(self->s.angles[PITCH] > 180) self->s.angles[PITCH] -= 360.0; while(self->s.angles[PITCH] < -180) self->s.angles[PITCH] += 360; // outside of the pitch extents? if (self->s.angles[PITCH] > acPitchExtents[self->onFloor][1]) self->s.angles[PITCH] = acPitchExtents[self->onFloor][1]; else if (self->s.angles[PITCH] < acPitchExtents[self->onFloor][0]) self->s.angles[PITCH] = acPitchExtents[self->onFloor][0]; // make sure the turret's angles match the gun's self->chain->s.angles[YAW] = self->s.angles[YAW]; self->chain->s.angles[PITCH] = 0; // setup the sound if (VectorCompare(self->s.angles, old_angles)) self->chain->s.sound = 0; else self->chain->s.sound = gi.soundindex("objects/acannon/ac_idle.wav"); } void monster_autocannon_think(edict_t *self) { ac_anim_frame_t frame; ac_anim_t anim; int lefty = 0; edict_t *old_enemy; self->nextthink = level.time + FRAMETIME; // get an enemy old_enemy = self->enemy; monster_autocannon_findenemy(self); if (self->enemy != NULL && old_enemy != self->enemy) gi.sound(self, CHAN_VOICE, gi.soundindex("objects/acannon/ac_act.wav"), 1, ATTN_NORM, 0); // turn whereever lefty = self->monsterinfo.lefty; if (level.time > self->delay) { monster_autocannon_turn(self); if (self->monsterinfo.lefty != lefty) self->delay = level.time + AC_TURN_DELAY; } anim = acFiringFrames[self->style]; frame = anim.frames[self->seq]; // ok, we don't have an enemy if (self->enemy == NULL) { if (self->seq == 0) { // get into idle animation self->s.frame++; if (self->s.frame > acActiveEnd[self->style] || self->s.frame < acActiveStart[self->style]) self->s.frame = acActiveStart[self->style]; return; // done, we want to wait here } // set the frame self->s.frame = frame.frame; // fire if (frame.fire) monster_autocannon_fire(self); // if we're not done with the firing sequence, we need to finish it off if (frame.last) // end of the loop or firing frame? self->seq = 0; else self->seq++; return; } // we have an enemy but he's not infront, go to the beginning of the firing sequence if (!autocannonInfront(self, self->enemy)) { self->s.frame = frame.frame; if (self->seq == anim.firstNonPause) return; // done, we want to wait here if (frame.last) // end of the loop or firing frame? self->seq = anim.firstNonPause; else self->seq++; return; } // we have an enemy, AND he's visible // let's kick his ass self->s.frame = frame.frame; if (frame.fire) monster_autocannon_fire(self); if (frame.last) // end of the loop? self->seq = anim.firstNonPause; else self->seq++; } void monster_autocannon_explode (edict_t *ent) { vec3_t origin; T_RadiusDamage(ent, ent, AC_EXPLODE_DMG, ent->enemy, AC_EXPLODE_RADIUS, MOD_TRIPBOMB); VectorMA (ent->s.origin, -0.02, ent->velocity, origin); gi.WriteByte (svc_temp_entity); if (ent->waterlevel) { if (ent->groundentity) gi.WriteByte (TE_GRENADE_EXPLOSION_WATER); else gi.WriteByte (TE_ROCKET_EXPLOSION_WATER); } else { if (ent->groundentity) gi.WriteByte (TE_GRENADE_EXPLOSION); else gi.WriteByte (TE_ROCKET_EXPLOSION); } gi.WritePosition (origin); gi.multicast (ent->s.origin, MULTICAST_PHS); // set the pain skin ent->chain->chain->s.skinnum = 1; // pain ent->chain->chain->rideWith[0] = NULL; ent->chain->chain->rideWith[1] = NULL; G_FreeEdict(ent->chain); G_FreeEdict(ent); } void monster_autocannon_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { // explode self->takedamage = DAMAGE_NO; self->think = monster_autocannon_explode; self->nextthink = level.time + FRAMETIME; } void monster_autocannon_pain (edict_t *self, edict_t *other, float kick, int damage) { // keep the enemy if (other->client || other->svflags & SVF_MONSTER) self->enemy = other; } void monster_autocannon_activate(edict_t *self) { self->active = AC_S_ACTIVATING; self->nextthink = level.time + FRAMETIME; // go thru the activation frames if (self->s.frame >= acActStart[self->style] && self->s.frame < acActEnd[self->style]) { if (self->s.frame == acActStart[self->style]) { //gi.sound(self, CHAN_VOICE, gi.soundindex("objects/acannon/ac_out.wav"), 1, ATTN_NORM, 0); } // continue self->s.frame++; self->chain->s.frame++; } else if (self->s.frame == acActEnd[self->style]) { self->s.frame = acActiveStart[self->style]; self->chain->s.frame = turretActiveStart; self->think = monster_autocannon_think; self->active = AC_S_ACTIVE; } else { self->s.frame = acActStart[self->style]; self->chain->s.frame = turretActStart; } } void monster_autocannon_deactivate(edict_t *self) { self->active = AC_S_DEACTIVATING; self->nextthink = level.time + FRAMETIME; // go thru the deactivation frames if (self->s.angles[PITCH] != 0) { if (self->s.angles[PITCH] > 0) { self->s.angles[PITCH] -= 5; if (self->s.angles[PITCH] < 0) self->s.angles[PITCH] = 0; } else { self->s.angles[PITCH] += 5; if (self->s.angles[PITCH] > 0) self->s.angles[PITCH] = 0; } } else if (self->s.frame >= acDeactStart[self->style] && self->s.frame < acDeactEnd[self->style]) { self->chain->s.sound = 0; if (self->s.frame == acDeactStart[self->style]) { //gi.sound(self, CHAN_VOICE, gi.soundindex("objects/acannon/ac_away.wav"), 1, ATTN_NORM, 0); } // continue self->s.frame++; self->chain->s.frame++; } else if (self->s.frame == acDeactEnd[self->style]) { self->s.frame = acIdleStart[self->style]; self->chain->s.frame = turretIdleStart; self->think = NULL; self->nextthink = 0; self->chain->s.sound = 0; self->active = AC_S_IDLE; } else { self->s.frame = acDeactStart[self->style]; self->chain->s.frame = turretDeactStart; } } void monster_autocannon_act(edict_t *self) { if (self->active == AC_S_IDLE) { if (acActStart[self->style] != -1) self->think = monster_autocannon_activate; else { self->s.frame = acActiveStart[self->style]; self->chain->s.frame = turretActiveStart; self->think = monster_autocannon_think; self->active = AC_S_ACTIVE; } self->nextthink = level.time + FRAMETIME; } else if (self->active == AC_S_ACTIVE) { if (acDeactStart[self->style] != -1) { self->nextthink = level.time + FRAMETIME; self->think = monster_autocannon_deactivate; } else { if (turretIdle[self->style]) self->chain->s.frame = turretIdleStart; else self->chain->s.frame = turretActiveStart; self->s.frame = acActiveStart[self->style]; self->think = NULL; self->active = AC_S_IDLE; self->nextthink = 0; } } } void monster_autocannon_use(edict_t *self, edict_t *other, edict_t *activator) { // on/off or berserk toggle? if (self->spawnflags & AC_SF_BERSERK_TOGGLE) { if (self->spawnflags & AC_SF_BERSERK) self->spawnflags &= ~AC_SF_BERSERK; else self->spawnflags |= AC_SF_BERSERK; } else monster_autocannon_act(self); } void monster_autocannon_usestub(edict_t *self) { // stub monster_autocannon_act(self); } void SP_monster_autocannon(edict_t *self) { edict_t *base, *turret; vec3_t offset; if (deathmatch->value) { G_FreeEdict(self); return; } if (self->style > 4 || self->style < 1) self->style = 1; // if we're on hard or nightmare, use fast lasers if (skill->value >= 2 && self->style == 4) self->style = 3; // precache some sounds and models gi.soundindex("objects/acannon/ac_idle.wav"); gi.soundindex("objects/acannon/ac_act.wav"); //gi.soundindex("objects/acannon/ac_out.wav"); //gi.soundindex("objects/acannon/ac_away.wav"); gi.modelindex("models/objects/rocket/tris.md2"); gi.modelindex("models/objects/laser/tris.md2"); // create the base base = G_Spawn(); base->classname = "autocannon base"; base->solid = SOLID_BBOX; VectorCopy(self->s.origin, base->s.origin); if (!self->onFloor) base->movetype = MOVETYPE_NONE; else base->movetype = MOVETYPE_RIDE; // make the base MOVETYPE_RIDE so that it can ride on trains if (!self->onFloor) base->s.modelindex = gi.modelindex("models/objects/acannon/base/tris.md2"); else base->s.modelindex = gi.modelindex("models/objects/acannon/base2/tris.md2"); gi.linkentity(base); // create the turret turret = G_Spawn(); turret->classname = "autocannon turret"; turret->solid = SOLID_BBOX; turret->movetype = MOVETYPE_NONE; turret->chain = base; VectorCopy(self->s.origin, turret->s.origin); if (!self->onFloor) turret->s.modelindex = gi.modelindex("models/objects/acannon/turret/tris.md2"); else turret->s.modelindex = gi.modelindex("models/objects/acannon/turret2/tris.md2"); if (turretIdle[self->style]) turret->s.frame = turretIdleStart; else turret->s.frame = turretActiveStart; turret->s.angles[YAW] = self->s.angles[YAW]; turret->s.angles[PITCH] = 0; gi.linkentity(turret); // fill in the details about ourself self->solid = SOLID_BBOX; self->movetype = MOVETYPE_NONE; if (!self->onFloor) VectorSet(offset, 0, 0, -20); // offset down a bit else VectorSet(offset, 0, 0, 20); // offset up a bit VectorAdd(self->s.origin, offset, self->s.origin); // set the bounding box if (!self->onFloor) { VectorSet(self->mins, -12, -12, -28); VectorSet(self->maxs, 12, 12, 16); } else { VectorSet(self->mins, -12, -12, -16); VectorSet(self->maxs, 12, 12, 28); } self->chain = turret; if (!self->onFloor) self->s.modelindex = gi.modelindex(models[self->style]); else self->s.modelindex = gi.modelindex(floorModels[self->style]); self->s.frame = acIdleStart[self->style]; self->active = AC_S_IDLE; self->monsterinfo.lefty = 0; self->monsterinfo.attack_state = self->s.angles[YAW]; // used for centre of back-and-forth "search" self->seq = 0; if (st.lip) self->monsterinfo.linkcount = (st.lip > 0 ? st.lip : 0); //self->svflags = SVF_MONSTER; // default health if (!self->health) self->health = 100; // enable/disable? ... berserk/not if (self->targetname) self->use = monster_autocannon_use; if (self->spawnflags & AC_SF_BERSERK_TOGGLE || !(self->spawnflags & AC_SF_START_OFF)) { self->think = monster_autocannon_usestub; self->nextthink = level.time + FRAMETIME; } self->takedamage = DAMAGE_AIM; self->die = monster_autocannon_die; self->pain = monster_autocannon_pain; // last but not least, setup the "rideWith" information base->rideWith[0] = turret; VectorSubtract(turret->s.origin, base->s.origin, base->rideWithOffset[0]); base->rideWith[1] = self; VectorSubtract(self->s.origin, base->s.origin, base->rideWithOffset[1]); gi.linkentity(self); } void SP_monster_autocannon_floor(edict_t *self) { if (self->style == 1) { gi.error("monster_autocannon_floor does not permit bullet style"); G_FreeEdict(self); return; } if (self->style < 1 || self->style > 4) self->style = 2; self->onFloor = 1; // signify floor mounted // call the other one SP_monster_autocannon(self); }