531 lines
18 KiB
Plaintext
531 lines
18 KiB
Plaintext
/*==============================================================================
|
|
OLD ONE / Shub-Niggurath (ID software Version)
|
|
==============================================================================*/
|
|
$frame idle1 idle2 idle3 idle4 idle5 idle6 idle7 idle8
|
|
$frame idle9 idle10 idle11 idle12 idle13 idle14 idle15 idle16
|
|
$frame idle17 idle18 idle19 idle20 idle21 idle22 idle23 idle24
|
|
$frame idle25 idle26 idle27 idle28 idle29 idle30 idle31 idle32
|
|
$frame idle33 idle34 idle35 idle36 idle37 idle38 idle39 idle40
|
|
$frame idle41 idle42 idle43 idle44 idle45 idle46
|
|
|
|
$frame shake1 shake2 shake3 shake4 shake5 shake6 shake7 shake8
|
|
$frame shake9 shake10 shake11 shake12 shake13 shake14 shake15
|
|
$frame shake16 shake17 shake18 shake19 shake20
|
|
|
|
float SHUB_PHASE1 = 1; // Waiting for a trigger event
|
|
float SHUB_PHASE2 = 2; // Fighting
|
|
float SHUB_PHASE3 = 3; // Frenzy mode
|
|
float SHUB_PHASE4 = 4; // Death
|
|
|
|
//======================================================================
|
|
// Global functions
|
|
//======================================================================
|
|
// Special streamlined player find function
|
|
//----------------------------------------------------------------------
|
|
float() xxshub_FindTarget =
|
|
{
|
|
local entity client;
|
|
|
|
// Get the obvious exception(s) done first
|
|
if (self.health < 1) return FALSE;
|
|
if (intermission_running) return FALSE;
|
|
|
|
// Find a client in current PVS
|
|
client = checkclient ();
|
|
|
|
// Go through all the exception(s)
|
|
if (!client) return FALSE;
|
|
if (!(client.flags & FL_CLIENT)) return FALSE;
|
|
if (client.flags & FL_NOTARGET) return FALSE;
|
|
if (client.items & IT_INVISIBILITY) return FALSE;
|
|
|
|
// Check range and visibility of player
|
|
enemy_vis = visible(client);
|
|
if (!enemy_vis) return FALSE;
|
|
|
|
// Finally found something
|
|
self.enemy = client;
|
|
self.goalentity = self.enemy;
|
|
|
|
// We have a winner!
|
|
return TRUE;
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Setup wave HP and trigger boundaries (one function to be consistent)
|
|
//----------------------------------------------------------------------
|
|
void() xxshub_WaveSetupHP =
|
|
{
|
|
// Is there anymore boss waves left?
|
|
if (self.bosswave >= self.bosswavetotal) {
|
|
// Only one wave left (death is final trigger)
|
|
self.health = self.bosswaveqty;
|
|
self.bosswavetrig = -1000;
|
|
}
|
|
else {
|
|
// Multiple waves are still left (reset hp+trigger)
|
|
// Always reset HP to stop high DPS weapons trashing waves boundaries
|
|
self.health = ((self.bosswavetotal - self.bosswave) + 1) * self.bosswaveqty;
|
|
// The wave trigger is always one wave lower
|
|
self.bosswavetrig = self.health - self.bosswaveqty;
|
|
}
|
|
// Debug messages for wave and health
|
|
dprint("\b[BOSS]\b Wave ("); dprint(ftos(self.bosswave));
|
|
dprint(" / "); dprint(ftos(self.bosswavetotal));
|
|
dprint(") HP ("); dprint(ftos(self.health));
|
|
dprint(") Trig ("); dprint(ftos(self.bosswavetrig));
|
|
dprint(")\n");
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Check if HP has reached next boss wave trigger event
|
|
//----------------------------------------------------------------------
|
|
float() xxshub_WaveCheck =
|
|
{
|
|
// Check for boss wave boundary event
|
|
if (self.health > 1 && self.health < self.bosswavetrig) {
|
|
// Check for wave boundary triggers
|
|
self.noise = "";
|
|
if (self.bosswave == 1) self.noise = self.noise1;
|
|
else if(self.bosswave == 2) self.noise = self.noise2;
|
|
else if(self.bosswave == 3) self.noise = self.noise3;
|
|
else if(self.bosswave == 4) self.noise = self.noise4;
|
|
|
|
// Is there any trigger for the wave boundary?
|
|
if (self.noise != "") trigger_strs(self.noise, self);
|
|
|
|
// Update Boss wave parameters (next wave!)
|
|
self.bosswave = self.bosswave + 1;
|
|
xxshub_WaveSetupHP(); // Reset trigger/hp
|
|
self.style = SHUB_PHASE3; // Frenzy mode
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
};
|
|
|
|
//===========================================================================
|
|
// IDLE state - flailing arms (only state really)
|
|
//===========================================================================
|
|
void() xxshub_idleframe =
|
|
{
|
|
// Get the obvious exception(s) done first
|
|
if (self.health < 1) return;
|
|
|
|
// Beginning of animation block
|
|
if (self.walkframe == 0) monster_idle_sound();
|
|
else if (self.walkframe == 12) monster_idle_sound();
|
|
else if (self.walkframe == 24) monster_idle_sound();
|
|
else if (self.walkframe == 36) monster_idle_sound();
|
|
|
|
// Move frame forward, check for conditions
|
|
self.walkframe = self.walkframe + 1;
|
|
if (self.walkframe > 45) self.walkframe = 0;
|
|
self.nextthink = time + 0.1;
|
|
self.think = xxshub_idleframe;
|
|
|
|
// Setup current animation frame
|
|
self.frame = $idle1 + self.walkframe;
|
|
|
|
// Check for HP trigger event
|
|
if (xxshub_WaveCheck() == TRUE) self.th_missile();
|
|
|
|
// Check for combat event
|
|
if (self.style == SHUB_PHASE2) {
|
|
// Check for any no combat conditions
|
|
if (self.enemy.health < 1) self.enemy = world;
|
|
else if (intermission_running > 0) self.enemy = world;
|
|
else if (self.enemy.flags & FL_NOTARGET) self.enemy = world;
|
|
else if (self.enemy.items & IT_INVISIBILITY) self.enemy = world;
|
|
|
|
// If no enemy, keep looking
|
|
if (!self.enemy) xxshub_FindTarget();
|
|
// Check for attack 1 - Homing Missile
|
|
if (self.enemy) self.th_melee();
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() xxshub_idle ={ self.walkframe = 0; xxshub_idleframe(); };
|
|
|
|
//============================================================================
|
|
// Attack 1 : Fire Shub Homing Missile
|
|
//============================================================================
|
|
void() xxshub_attack1 =
|
|
{
|
|
// Check sightline to player
|
|
if (visblocked(self.enemy)) return;
|
|
if (time < self.attack_finished) return;
|
|
|
|
SUB_AttackFinished (2 + 2*random());
|
|
self.effects = self.effects | EF_MUZZLEFLASH;
|
|
sound (self, CHAN_WEAPON, "shub/attack1.wav", 1, ATTN_NORM);
|
|
|
|
// Skill level adjustment
|
|
self.attack_speed = self.pos1_x + (skill*self.pos1_y);
|
|
// Spawn from front of Shub
|
|
Launch_HomingMissile (self.dest1, '0 0 10', CT_PROJ_SHUB1, self.attack_speed);
|
|
};
|
|
|
|
//============================================================================
|
|
// Attack 2 : shake and spam grenades (end of wave event)
|
|
//============================================================================
|
|
void() xxshub_shakeattack =
|
|
{
|
|
local vector org, dir, ang, avel;
|
|
|
|
// Custom grenade firing speed
|
|
self.attack_speed = self.pos3_x;
|
|
|
|
// Shoot grenades from top of shub
|
|
makevectors(self.angles);
|
|
org = self.origin + attack_vector(self.dest2);
|
|
|
|
// Randomly shoot grenades in any directions
|
|
ang = '0 0 0';
|
|
ang_y = rint(random()*360);
|
|
makevectors(ang);
|
|
dir = v_forward * self.attack_speed;
|
|
dir_z = self.pos3_y;
|
|
|
|
// Bit of random spin and fire grenade
|
|
avel = vecrand(100,200,FALSE);
|
|
Launch_Grenade(org, dir, avel, CT_PROJ_SHUB2);
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() xxshub_shakeframe =
|
|
{
|
|
// Randomnly fire grenades outward
|
|
if (random() < 0.5) xxshub_shakeattack();
|
|
|
|
if (self.walkframe == 0) {
|
|
sound (self, CHAN_WEAPON, "shub/attack2.wav", 1, ATTN_NORM);
|
|
self.effects = self.effects | EF_MUZZLEFLASH;
|
|
}
|
|
|
|
// Keep on looping around
|
|
self.nextthink = time + 0.1;
|
|
self.think = xxshub_shakeframe;
|
|
|
|
// Move frame forward, check for conditions
|
|
self.walkframe = self.walkframe + 1;
|
|
if (self.walkframe > 14) {
|
|
self.walkframe = 0;
|
|
self.count = self.count + 1;
|
|
|
|
// has shub looped around 3 times?
|
|
if (self.count > 2) {
|
|
// Slight attack pause after the shake
|
|
self.attack_finished = time + 2 + 2*random();
|
|
Resist_ChangeType(self, FALSE);
|
|
self.style = SHUB_PHASE2;
|
|
self.think = self.th_stand;
|
|
}
|
|
}
|
|
|
|
// Update frame and keep on shaking!
|
|
else self.frame = $shake1 + self.walkframe;
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() xxshub_attack2 =
|
|
{
|
|
Resist_ChangeType(self, TRUE);
|
|
self.walkframe = self.count = 0;
|
|
xxshub_shakeframe();
|
|
};
|
|
|
|
//============================================================================
|
|
// PAIN (ignored, just keeps on firing)
|
|
//============================================================================
|
|
void(entity inflictor, entity attacker, float damage) xxshub_pain =
|
|
{
|
|
// Shub been hit, wakeup and start attacking
|
|
if (self.style == SHUB_PHASE1) {self.use(); return;}
|
|
self.pain_finished = time + 2 + random()*2;
|
|
|
|
// Check for boss wave trigger events
|
|
if (xxshub_WaveCheck() == TRUE) {self.th_missile(); return;}
|
|
|
|
// Check all pain conditions and set up what to do next
|
|
monster_pain_check(attacker, damage);
|
|
|
|
// Any pain animation/sound required?
|
|
if (self.pain_check > 0) {
|
|
// Spawn particle/smoke damage
|
|
SpawnProjectileSmoke(inflictor.origin, 200, 50, 150);
|
|
if (damage > 30) SpawnProjectileSmoke(inflictor.origin, 200, 50, 250);
|
|
// random pick sound
|
|
if (random() < 0.5)
|
|
sound (self, CHAN_VOICE, self.pain_sound, 1, ATTN_NORM);
|
|
else sound (self, CHAN_VOICE, self.pain_sound2, 1, ATTN_NORM);
|
|
}
|
|
};
|
|
|
|
//===========================================================================
|
|
// FINAL DEATH - shake it baby and explode
|
|
//===========================================================================
|
|
void() xxshub_explode =
|
|
{
|
|
// No more Boss!
|
|
entity_hide(self);
|
|
|
|
// Blue ID particle explosion
|
|
WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
|
|
WriteByte (MSG_BROADCAST, TE_EXPLOSION2);
|
|
WriteCoord (MSG_BROADCAST, self.origin_x);
|
|
WriteCoord (MSG_BROADCAST, self.origin_y);
|
|
WriteCoord (MSG_BROADCAST, self.origin_z);
|
|
WriteByte (MSG_BROADCAST, 35);
|
|
WriteByte (MSG_BROADCAST, 8);
|
|
|
|
// Classic sprite/DP explosion
|
|
SpawnExplosion(EXPLODE_BIG, self.origin, "shub/explode_death.wav");
|
|
|
|
// Gib explosive fountain!?!
|
|
ThrowGib(11, 10 + rint(random()*10)); // large blobs
|
|
ThrowGib(4, 5 + rint(random()*3)); // Some flesh bits
|
|
ThrowGib(5, 10 + rint(random()*3));
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() xxshub_deathframe =
|
|
{
|
|
// Throw debris out from top of shub
|
|
self.pos3 = vecrand(100,100,TRUE);
|
|
self.pos3_z = 128 + random()*128;
|
|
SpawnProjectileSmoke(self.origin+self.pos3, 200, 50, 150);
|
|
// Random chance of gib + explosion
|
|
if (random() < 0.25)
|
|
SpawnExplosion(EXPLODE_MED, self.origin+self.pos3, SOUND_REXP3);
|
|
|
|
// Move frame forward, check for conditions
|
|
self.walkframe = self.walkframe + 1;
|
|
if (self.walkframe > 15 && self.count < 3) {
|
|
self.walkframe = 0;
|
|
self.count = self.count + 1;
|
|
}
|
|
|
|
if (self.walkframe > 19) xxshub_explode();
|
|
// Update frame and keep on shaking!
|
|
else {
|
|
self.frame = $shake1 + self.walkframe;
|
|
// Keep on looping around
|
|
self.nextthink = time + 0.1;
|
|
self.think = xxshub_deathframe;
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() xxshub_die =
|
|
{
|
|
self.style = SHUB_PHASE4; // Game Over for Shub!
|
|
self.deadflag = DEAD_DEAD; // the rock finally crashed
|
|
self.effects = 0; // Remove effects on death
|
|
self.solid = SOLID_NOT; // no longer need to block
|
|
self.max_health = MON_GIBEXPLOSION;
|
|
|
|
sound (self, CHAN_VOICE, "shub/death1.wav", 1, ATTN_NORM);
|
|
self.walkframe = self.count = 0;
|
|
xxshub_deathframe();
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Switch Shub into a fighting stance
|
|
//----------------------------------------------------------------------
|
|
void() xxshub_wakeup =
|
|
{
|
|
// Time to fight!
|
|
self.use = SUB_Null; // No more triggers
|
|
self.style = SHUB_PHASE2; // Start fighting
|
|
|
|
Resist_ChangeType(self, FALSE); // Default resistance
|
|
|
|
// Wakeup sound
|
|
sound (self, CHAN_VOICE, self.sight_sound, 1, ATTN_NORM);
|
|
|
|
// Restore pain thresholds and timers
|
|
self.pain_flinch = 400; // Shambler level
|
|
self.pain_timeout = 2; // Stop constant pain
|
|
|
|
// Wait for wakeup sound to finish before combat
|
|
self.pain_finished = time + 2 + random()*2;
|
|
self.attack_finished = time + 2;
|
|
|
|
// Check for player (use activator)
|
|
if (activator.flags & FL_CLIENT) self.enemy = activator;
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Setup Shub after trigger event
|
|
//----------------------------------------------------------------------
|
|
void() xxshub_start =
|
|
{
|
|
self.use = SUB_Null; // Suppress trigger events
|
|
self.style = SHUB_PHASE1; // Waiting for combat trigger
|
|
self.flags = FL_MONSTER; // Reset flag (no user settings)
|
|
if (self.spawnflags & MON_SHUB_UPSIDE) self.flags = self.flags | FL_FLY;
|
|
else self.flags = self.flags | FL_ONGROUND;
|
|
|
|
self.solid = SOLID_SLIDEBOX;
|
|
self.movetype = MOVETYPE_STEP;
|
|
setmodel(self, self.mdl); // Setup model
|
|
setsize (self, self.bbmins, self.bbmaxs); // Restore BB size
|
|
|
|
self.yaw_speed = 0; // No turning speed
|
|
self.velocity = '0 0 0'; // Make sure stationary
|
|
self.deadflag = DEAD_NO; // used to stop death re-triggering
|
|
self.liquidbase = self.liquidcheck = 0; // Used for liquid content damage
|
|
self.dmgcombined = self.dmgtimeframe = 0; // combined damage over 0.1s
|
|
self.pain_longanim = FALSE; // No axe advantage
|
|
self.takedamage = DAMAGE_AIM; // Can receive damage
|
|
self.pain_flinch = 1; // Pain is a start trigger
|
|
|
|
self.movespeed = 1; // Shub does not move!
|
|
self.noinfighting = TRUE; // No infighting
|
|
self.gibhealth = -1000; // Special death sequence
|
|
self.pain_finished = self.attack_finished = 0;
|
|
|
|
Resist_CheckRange(self); // Double check values
|
|
Resist_Save(self); // Save for Later
|
|
// Allow for a damage trigger (spawnflag option only)
|
|
if (!(self.spawnflags & MON_SHUB_DMGTRIG))
|
|
Resist_ChangeType(self, TRUE);
|
|
|
|
// Setup boss waves and overall health
|
|
if (self.bosswave < 1) self.bosswavetotal = 5;
|
|
else self.bosswavetotal = self.bosswave;
|
|
|
|
// Always start bosswave at 1
|
|
self.bosswave = 1;
|
|
if (self.bosswaveqty < 1) self.bosswaveqty = 500;
|
|
self.max_health = self.bosswavetotal * self.bosswaveqty;
|
|
// Setup boss wave HP + trigger event
|
|
xxshub_WaveSetupHP();
|
|
|
|
// Restore all think functions
|
|
self.th_stand = xxshub_idle;
|
|
self.th_walk = xxshub_idle;
|
|
self.th_run = xxshub_idle;
|
|
self.th_melee = xxshub_attack1;
|
|
self.th_missile = xxshub_attack2;
|
|
self.th_pain = xxshub_pain;
|
|
self.th_die = xxshub_die;
|
|
|
|
// Wait for trigger to start attacking
|
|
self.use = xxshub_wakeup;
|
|
self.th_stand();
|
|
};
|
|
|
|
/*============================================================================
|
|
QUAKED monster_shub (1 0 0) (-128 -128 -24) (128 128 512)
|
|
============================================================================*/
|
|
void() monster_shub =
|
|
{
|
|
if (deathmatch) { remove(self); return; }
|
|
|
|
self.mdl = "progs/oldone.mdl";
|
|
precache_model (self.mdl);
|
|
precache_model (MODEL_PROJ_SHUB1); // Large Homing Missile
|
|
precache_model (MODEL_PROJ_SHUB2); // Large blob of blood
|
|
|
|
// Final explosionm and grenades for attack 2
|
|
self.gib1mdl = MODEL_PROJ_SHUB2;
|
|
self.gib1frame = 9;
|
|
|
|
self.idle_sound = "shub/idle1.wav";
|
|
self.idle_sound2 = "shub/idle2.wav";
|
|
precache_sound (self.idle_sound);
|
|
precache_sound (self.idle_sound2);
|
|
|
|
// Att1 : Homing missile
|
|
// Att2 : Grenade spam
|
|
precache_sound ("shub/attack1.wav");
|
|
precache_sound ("shub/attack2.wav");
|
|
precache_sound ("shub/bounce.wav");
|
|
|
|
self.pain_sound = "shub/pain1.wav";
|
|
self.pain_sound2 = "shub/pain2.wav";
|
|
precache_sound (self.pain_sound);
|
|
precache_sound (self.pain_sound2);
|
|
precache_sound ("shub/death1.wav");
|
|
precache_sound ("shub/explode_death.wav");
|
|
|
|
self.sight_sound = "shub/sight1.wav";
|
|
precache_sound (self.sight_sound);
|
|
|
|
self.solid = SOLID_NOT; // No interaction with world
|
|
self.movetype = MOVETYPE_NONE; // Static item, no movement
|
|
// Use to be 128 square, reduced size to help with
|
|
// radius/splash damage being more effective
|
|
self.bbmins = '-96 -96 -24'; // has own entity setup
|
|
self.bbmaxs = '96 96 192';
|
|
self.bboxtype = BBOX_CUSTOM; // Custom BBox size
|
|
self.bossflag = TRUE; // Boss flag (like FL_MONSTER)
|
|
self.health = self.max_health = MEGADEATH;
|
|
self.pain_finished = LARGE_TIMER;
|
|
self.no_liquiddmg = TRUE; // no slime/lava damage
|
|
self.idlemoreoften = TRUE; // More frequent idle sounds
|
|
self.gibbed = FALSE; // Still in one piece
|
|
self.poisonous = FALSE; // Cannot be poisonous
|
|
self.deathstring = " became one with Shub-Niggurath\n";
|
|
|
|
self.deadflag = DEAD_NO; // used to stop death re-triggering
|
|
self.liquidbase = self.liquidcheck = 0; // Used for liquid content damage
|
|
self.dmgcombined = self.dmgtimeframe = 0; // combined damage over 0.1s
|
|
self.takedamage = DAMAGE_NO; // Immune to damage
|
|
|
|
// Attack 1 - Setup projectile speed (200 + 20*skill)
|
|
if (CheckZeroVector(self.pos1)) self.pos1 = '200 20 0';
|
|
// Attack 1 - Setup projectile damage (Base + Random, Splash)
|
|
if (CheckZeroVector(self.pos2)) self.pos2 = '0 0 40';
|
|
// Attack 2 - Setup projectile speed, splash damage
|
|
if (CheckZeroVector(self.pos3)) self.pos3 = '300 500 40';
|
|
|
|
// Attack 1 - Spawn location (offset from origin)
|
|
if (CheckZeroVector(self.dest1)) self.dest1 = '0 0 128';
|
|
// Attack 2 - Spawn location (offset from origin)
|
|
if (CheckZeroVector(self.dest2)) self.dest2 = '0 0 128';
|
|
|
|
if (self.spawnflags & MON_SHUB_UPSIDE) {
|
|
self.bbmins = '-96 -96 -192'; // up side down version
|
|
self.bbmaxs = '96 96 24';
|
|
}
|
|
|
|
self.classtype = CT_MONXXSHUB;
|
|
self.classgroup = CG_BOSS;
|
|
self.style = 0;
|
|
|
|
// No targetname = no trigger!
|
|
if (self.targetname == "") {
|
|
dprint("\b[SHUB]\b Missing trigger name!\n");
|
|
spawn_marker(self.origin, SPNMARK_YELLOW);
|
|
return;
|
|
}
|
|
|
|
// Does not go through monster spawn functions
|
|
total_monsters = total_monsters + 1;
|
|
|
|
// Default is no spawn delay, wait for trigger
|
|
if (!(self.spawnflags & MON_SPAWN_DELAY)) {
|
|
self.nextthink = time + 0.1 + random()*0.4;
|
|
self.think = xxshub_start;
|
|
}
|
|
else self.use = xxshub_start;
|
|
};
|
|
|
|
/*============================================================================
|
|
QUAKED monster_shubupsd (1 0 0) (-128 -128 -24) (128 128 512)
|
|
============================================================================*/
|
|
void() monster_shubupsd =
|
|
{
|
|
if (deathmatch) { remove(self); return; }
|
|
|
|
// Attack 2 - Setup projectile speed, splash damage
|
|
if (CheckZeroVector(self.pos3)) self.pos3 = '600 0 40';
|
|
self.angles_x = 180;
|
|
self.angles_y = anglemod(self.angles_y + 180);
|
|
self.spawnflags = self.spawnflags | MON_SHUB_UPSIDE;
|
|
|
|
monster_shub();
|
|
}; |