Files
quakemapping/mod_xj19/my_progs/mon_sentinel.qc
2020-01-01 23:35:17 +01:00

293 lines
9.9 KiB
Plaintext

/*==============================================================================
SENTINEL (Originally from Quoth - Kell/Necros/Preach)
* QC was created for AD mappers to play with in their own projects
* Original Quoth model/sounds not included with main MOD
Interesting QC traits
* All stand/walk/run go through single function, easier setup
* Has no front section to model, wakeup event is 360 FOV
* Does not move when attacking so can maintain original height
* Will move very fast when out of sight of enemy
* No head model or body on death, just a pile of gibs
QUOTH assets required to get this monster working in AD
(model's in 'progs' and wav's in 'sound' sub directories)
* sentinel.mdl -> mon_sentinel.mdl (rename file)
* sentinel/widle1.wav, sentinel/widle2.wav, sentinel/wsight.wav
* sentinel/laser.wav, sentinel/nail.wav, sentinel/wpain.wav
==============================================================================*/
// Writhe (move tentacles) animation
$frame idle1 idle2 idle3 idle4 idle5 idle6 idle7 idle8 idle9 idle10
// Pain (move tentacles) animation
$frame pain1 pain2 pain3 pain4 pain5 pain6
// Export frames (ignored)
$frame base1
float SENT_STAND = 0; // Default state
float SENT_WALK = 1; // Patrolling
float SENT_RUN = 2; // Attacking - Sentry mode
//======================================================================
// Update Sentinel every frame
// Its a great shame the quoth model has no idle animations
// - could have been body attachments or different tentacle movements
//======================================================================
void() sent_update =
{
// If Sentinel is dead, no more updates
if (self.health < 1) return;
// Time for an idle sound?
if (self.idletimer < time) monster_idle_sound();
// Update animation frame
self.frame = $idle1 + self.walkframe;
// Move frame forward, check for conditions
self.walkframe = self.walkframe + 1;
if (self.walkframe > 9) self.walkframe = 0;
self.nextthink = time + 0.1;
self.think = sent_update;
// Check sentinel states
if (self.attack_timer == SENT_STAND) {
self.solid = SOLID_SLIDEBOX;
self.movetype = MOVETYPE_FLY;
// Change the movement type so that can easily move up/down
// using velocity, forced origin movement is really jerky!
if (self.velocity_x == 0 && self.velocity_y == 0) {
if (self.attack_finished < time) {
self.attack_finished = time + 2;
if (self.lip < 1) self.lip = 1;
else self.lip = -1;
self.velocity_z = 2 * self.lip;
}
}
ai_stand();
}
else if (self.attack_timer == SENT_WALK) {
self.solid = SOLID_SLIDEBOX;
self.movetype = MOVETYPE_STEP;
ai_walk(8);
}
else if (self.attack_timer == SENT_RUN) {
self.solid = SOLID_SLIDEBOX;
self.movetype = MOVETYPE_STEP;
// If can see enemy, don't move, just fire
if (visible(self.enemy)) {
self.movespeed = -1;
ai_run(0);
}
// Cannot see enemy, track them down
else {
self.movespeed = 1;
ai_run(8);
}
}
};
//======================================================================
// All stand, walk and run functions are condensed down to one entry
// Might as well be one loop as there is only one animation set
//
void() sent_stand = { self.attack_timer = SENT_STAND; sent_update(); };
void() sent_walk = { self.attack_timer = SENT_WALK; sent_update(); };
void() sent_run = { self.attack_timer = SENT_RUN; sent_update(); };
//===========================================================================
// RANGE ATTACK - Fires spikes or laser bolts
//===========================================================================
void() sent_attack =
{
local vector org, dir, vec;
// Keep cycling the pain animation
self.think = sent_run;
self.nextthink = time + 0.1;
if (self.enemy && self.health > 0) {
// Always make sure there is no monster or obstacle in the way
// Cannot use enemy entity direct, enemytarget will be active
if ( !visxray(SUB_entEnemyTarget(), self.attack_offset, '0 0 12', FALSE) ) return;
self.effects = self.effects | EF_MUZZLEFLASH;
makevectors (self.angles);
org = self.origin + attack_vector(self.attack_offset);
// Aim high to catch jumping players
dir = SUB_orgEnemyTarget() + '0 0 12';
vec = normalize(dir - org);
// Laser/nail speed : 575=easy, 650=normal, 725=hard, nm=800
self.attack_speed = SPEED_SENTPROJ + (skill * SPEED_SENTPROJSKILL);
// Switch projectile type
if (self.spawnflags & MON_SENTINEL_NAIL) {
if (random() < 0.2) sound (self, CHAN_WEAPON, "sentinel/nail.wav", 1, ATTN_NORM);
else sound (self, CHAN_WEAPON, "weapons/rocket1i.wav", 1, ATTN_NORM);
launch_projectile(org, vec, CT_PROJ_MONNG, self.attack_speed);
}
else {
if (random() < 0.2) sound (self, CHAN_WEAPON, "sentinel/laser.wav", 1, ATTN_NORM);
else sound (self, CHAN_WEAPON, "enforcer/enfire.wav", 1, ATTN_NORM);
launch_projectile(org, vec, CT_PROJ_LASER, self.attack_speed);
}
}
};
//============================================================================
// ROBOT PAIN!?!
//============================================================================
void() sent_inpain =
{
// Update animation frame
self.frame = $pain1 + self.inpain;
// Move backwards away from damage
ai_backface(5-self.inpain);
// Keep cycling the pain animation
self.think = sent_inpain;
self.nextthink = time + 0.1;
// Start of pain cycle
if (self.inpain == 0) {
// Spawn a pile of sparks and dust falling down
particle_dust(self.origin, 10+random()*10, PARTICLE_BURST_YELLOW);
}
// Finished, back to combat
else if (self.inpain >= 5) {
self.think = self.th_run;
}
// Next pain animation
self.inpain = self.inpain + 1;
};
//----------------------------------------------------------------------
void(entity inflictor, entity attacker, float damage) sent_pain =
{
// Check all pain conditions and set up what to do next
monster_pain_check(attacker, damage);
// Stop any ai_run velocity and reset movetype
self.velocity = '0 0 0';
self.solid = SOLID_SLIDEBOX;
self.movetype = MOVETYPE_STEP;
sound (self, CHAN_VOICE, self.pain_sound, 1, ATTN_NORM);
self.inpain = 0;
sent_inpain();
};
//============================================================================
void() sent_die =
{
// Pre-check routine to tidy up extra entities
monster_death_precheck();
// Final fireworks!
particle_dust(self.origin, 10+random()*10, PARTICLE_BURST_YELLOW);
SpawnProjectileSmoke(self.origin, 150, 50, 150);
SpawnProjectileSmoke(self.origin, 150, 50, 150);
SpawnExplosion(EXPLODE_BIG, self.origin, self.death_sound);
// no more sentinel
entity_hide (self);
// Make sure gibs go flying up
self.max_health = MON_GIBFOUNTAIN;
self.health = -100;
// Regular blood like gibs
ThrowGib(4, 2 + rint(random()*4));
ThrowGib(5, 1);
// Metal and custom body parts
self.gibtype = GIBTYPE_METAL;
ThrowGib(11, 2 + rint(random()*2));
ThrowGib(12, 2 + rint(random()*2));
};
/*======================================================================
QUAKED monster_sentinel (1 0 0) (-16 -16 -24) (16 16 24) Ambush
======================================================================*/
void() monster_sentinel =
{
if (deathmatch) { remove(self); return; }
self.mdl = "progs/mon_sentinel.mdl"; // AD naming
self.gib1mdl = "progs/gib_metal1.mdl"; // Breakable metal
self.gib2mdl = "progs/gib_metal3.mdl"; // Breakable metal
precache_model (self.mdl);
precache_model (self.gib1mdl); // Generic metal1_2
precache_model (self.gib2mdl); // Generic metal1_2
self.idle_sound = "sentinel/widle1.wav";
self.idle_sound2 = "sentinel/widle2.wav";
precache_sound (self.idle_sound);
precache_sound (self.idle_sound2);
// Default attack - lasers!
precache_model (MODEL_PROJ_LASER); // Copy of enforcer laser
precache_sound("sentinel/laser.wav"); // Unique laser fire
precache_sound ("enforcer/enfire.wav");
precache_sound ("enforcer/enfstop.wav");
// Alternative attack - red hot nails!?!
if (self.spawnflags & MON_SENTINEL_NAIL) {
self.exactskin = 1; // Nail version
precache_model (MODEL_PROJ_NGRED); // Copy of freddie nails
precache_sound("weapons/rocket1i.wav");
precache_sound("sentinel/nail.wav"); // Unique nail fire
}
self.pain_sound = "sentinel/wpain.wav";
precache_sound (self.pain_sound);
self.death_sound = "jim/explode_major.wav";
precache_sound (self.death_sound);
self.sight_sound = "sentinel/wsight.wav";
precache_sound (self.sight_sound);
self.solid = SOLID_NOT; // No interaction with world
self.movetype = MOVETYPE_NONE; // Static item, no movement
if (self.bboxtype < 1) self.bboxtype = BBOX_SHORT;
if (self.health < 1) self.health = 75;
self.gibhealth = MON_NEVERGIB; // Cannot be gibbed by weapons
self.gibbed = FALSE;
self.pain_flinch = 30; // Sometimes flinch
self.steptype = FS_FLYING; // Silent feet
self.pain_longanim = FALSE; // No long pain animation
self.blockudeath = TRUE; // No humanoid death sound
self.idlemoreoften = TRUE; // More creepy idle sounds
self.poisonous = FALSE; // Robots are not poisonous
if (self.height == 0) self.height = 32; // Custom height
self.walkframe = 0; // Reset frame counter
self.attack_offset = '0 0 14'; // front/middle of body
self.sight_nofront = TRUE; // Has no front facing
self.deathstring = " was scorched by a Sentinel\n";
// Always reset Ammo Resistance to be consistent
self.resist_shells = self.resist_nails = 0;
self.resist_rockets = self.resist_cells = 0;
self.th_checkattack = SentinelCheckAttack;
self.th_stand = sent_stand;
self.th_walk = sent_walk;
self.th_run = sent_run;
self.th_missile = sent_attack;
self.th_pain = sent_pain;
self.th_die = sent_die;
self.classtype = CT_MONSENTINEL;
self.classgroup = CG_ROBOT;
self.classmove = MON_MOVEFLY;
monster_start();
};