293 lines
9.9 KiB
Plaintext
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();
|
|
};
|