925 lines
35 KiB
Plaintext
925 lines
35 KiB
Plaintext
/*==============================================================================
|
|
SKULL WIZARD (Hexen2 model by Raven Software)
|
|
==============================================================================*/
|
|
|
|
// Fall backwards, vanish and robes fall down
|
|
$frame death1 death2 death3 death4 death5 death6 death7 death8
|
|
$frame death9 death10 death11 death12 death13 death14 death15
|
|
|
|
// Summon minion
|
|
$frame summon1 summon2 summon3 summon4 summon5 summon6 summon7 summon8
|
|
$frame summon9 summon10 summon11 summon12 summon13 summon14 summon15 summon16
|
|
$frame summon17 summon18 summon19 summon20 summon21 summon22 summon23 summon24
|
|
$frame summon25 summon26 summon27 summon28 summon29 summon30
|
|
|
|
// Pain A
|
|
$frame painA1 painA2 painA3 painA4 painA5 painA6 painA7 painA8
|
|
$frame painA9 painA10 painA11 painA12
|
|
|
|
// Transition - Move from upright to crouch
|
|
$frame transA1 transA2 transA3 transA4 transA5 transA6 transA7 transA8
|
|
$frame transA9 transA10 transA11 transA12
|
|
|
|
// Cast fireball spell (frame 12 - fire)
|
|
$frame spellA1 spellA2 spellA3 spellA4 spellA5 spellA6 spellA7 spellA8
|
|
$frame spellA9 spellA10 spellA11 spellA12 spellA13 spellA14 spellA15
|
|
|
|
// Cast teleport spell (touching book)
|
|
$frame spellB1 spellB2 spellB3 spellB4 spellB5 spellB6 spellB7 spellB8
|
|
$frame spellB9 spellB10 spellB11 spellB12 spellB13 spellB14 spellB15
|
|
|
|
// Transition - Move from crouch to upright
|
|
$frame transB1 transB2 transB3 transB4 transB5 transB6 transB7
|
|
|
|
// Default stand - breathing
|
|
$frame stand1 stand2 stand3 stand4 stand5 stand6 stand7 stand8
|
|
$frame stand9 stand10 stand11 stand12 stand13 stand14 stand15 stand16
|
|
$frame stand17 stand18 stand19
|
|
|
|
// Default walk - very slow
|
|
$frame walk1 walk2 walk3 walk4 walk5 walk6 walk7 walk8
|
|
$frame walk9 walk10 walk11 walk12 walk13 walk14 walk15 walk16
|
|
$frame walk17 walk18 walk19 walk20 walk21 walk22 walk23 walk24
|
|
|
|
// Teleport
|
|
$frame tele1 tele2 tele3 tele4 tele5 tele6 tele7 tele8 tele9
|
|
|
|
float SKULLW_STAND = 1;
|
|
float SKULLW_WALK = 2;
|
|
float SKULLW_HEIGHT = 96;
|
|
float SKULLW_DISTMIN = 256;
|
|
float SKULLW_DISTMAX = 512;
|
|
|
|
//============================================================================
|
|
void() skullw_stand1 =[ $stand1, skullw_stand2 ] {
|
|
self.idlebusy=SKULLW_STAND; monster_idle_sound();ai_stand();};
|
|
void() skullw_stand2 =[ $stand18, skullw_stand3 ] {ai_stand();};
|
|
void() skullw_stand3 =[ $stand16, skullw_stand4 ] {ai_stand();};
|
|
void() skullw_stand4 =[ $stand14, skullw_stand5 ] {ai_stand();};
|
|
void() skullw_stand5 =[ $stand12, skullw_stand6 ] {ai_stand();};
|
|
void() skullw_stand6 =[ $stand3, skullw_stand7 ] {ai_stand();};
|
|
void() skullw_stand7 =[ $stand5, skullw_stand8 ] {ai_stand();};
|
|
void() skullw_stand8 =[ $stand7, skullw_stand9 ] {ai_stand();};
|
|
void() skullw_stand9 =[ $stand9, skullw_stand10 ] {ai_stand();};
|
|
void() skullw_stand10 =[ $stand11, skullw_stand11 ] {ai_stand();};
|
|
void() skullw_stand11 =[ $stand13, skullw_stand12 ] {ai_stand();};
|
|
void() skullw_stand12 =[ $stand15, skullw_stand13 ] {ai_stand();};
|
|
void() skullw_stand13 =[ $stand17, skullw_stand14 ] {ai_stand();};
|
|
void() skullw_stand14 =[ $stand19, skullw_stand1 ] {ai_stand();};
|
|
|
|
//============================================================================
|
|
void() skullw_walk1 =[ $walk1, skullw_walk2 ] {monster_footstep(FALSE);
|
|
self.idlebusy=SKULLW_WALK; monster_idle_sound();ai_walk(2);};
|
|
void() skullw_walk2 =[ $walk3, skullw_walk3 ] {ai_walk(5);};
|
|
void() skullw_walk3 =[ $walk4, skullw_walk4 ] {ai_walk(5);};
|
|
void() skullw_walk4 =[ $walk5, skullw_walk5 ] {ai_walk(4);};
|
|
void() skullw_walk5 =[ $walk7, skullw_walk6 ] {ai_walk(4);};
|
|
void() skullw_walk6 =[ $walk8, skullw_walk7 ] {ai_walk(2);};
|
|
void() skullw_walk7 =[ $walk9, skullw_walk8 ] {ai_walk(2);};
|
|
void() skullw_walk8 =[ $walk11, skullw_walk9 ] {ai_walk(2);};
|
|
void() skullw_walk9 =[ $walk12, skullw_walk10 ] {monster_footstep(FALSE); ai_walk(2);};
|
|
void() skullw_walk10 =[ $walk13, skullw_walk11 ] {ai_walk(5);};
|
|
void() skullw_walk11 =[ $walk15, skullw_walk12 ] {ai_walk(5);};
|
|
void() skullw_walk12 =[ $walk16, skullw_walk13 ] {ai_walk(4);};
|
|
void() skullw_walk13 =[ $walk17, skullw_walk14 ] {ai_walk(4);};
|
|
void() skullw_walk14 =[ $walk19, skullw_walk15 ] {ai_walk(2);};
|
|
void() skullw_walk15 =[ $walk20, skullw_walk16 ] {ai_walk(2);};
|
|
void() skullw_walk16 =[ $walk22, skullw_walk1 ] {ai_walk(3);};
|
|
|
|
//============================================================================
|
|
// WALK -> STAND
|
|
void() skullw_transA1 =[ $transA5, skullw_transA2 ] {};
|
|
void() skullw_transA2 =[ $transA7, skullw_transA3 ] {};
|
|
void() skullw_transA3 =[ $transA9, skullw_transA4 ] {};
|
|
void() skullw_transA4 =[ $transA11, skullw_stand1 ] {};
|
|
|
|
// STAND -> WALK
|
|
void() skullw_transB1 =[ $transB3, skullw_transB2 ] {};
|
|
void() skullw_transB2 =[ $transB5, skullw_transB3 ] {};
|
|
void() skullw_transB3 =[ $transB7, skullw_walk1 ] {};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() skullw_stand =
|
|
{
|
|
if (self.idlebusy == SKULLW_WALK) skullw_transA1();
|
|
else skullw_stand1();
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() skullw_walk =
|
|
{
|
|
if (self.idlebusy == SKULLW_STAND) skullw_transB1();
|
|
else skullw_walk1();
|
|
};
|
|
|
|
//============================================================================
|
|
void() skullw_run1 =[ $stand1, skullw_run2 ] {ai_face();
|
|
monster_idle_sound();ai_run(0);};
|
|
void() skullw_run2 =[ $stand18, skullw_run3 ] {ai_face();ai_run(0);};
|
|
void() skullw_run3 =[ $stand16, skullw_run4 ] {ai_run(0);};
|
|
void() skullw_run4 =[ $stand14, skullw_run5 ] {ai_run(0);};
|
|
void() skullw_run5 =[ $stand12, skullw_run6 ] {ai_face();ai_run(0);};
|
|
void() skullw_run6 =[ $stand3, skullw_run7 ] {ai_face();ai_run(0);};
|
|
void() skullw_run7 =[ $stand5, skullw_run8 ] {ai_run(0);};
|
|
void() skullw_run8 =[ $stand7, skullw_run9 ] {ai_run(0);};
|
|
void() skullw_run9 =[ $stand9, skullw_run10] {ai_face();ai_run(0);};
|
|
void() skullw_run10=[ $stand11, skullw_run11] {ai_face();ai_run(0);};
|
|
void() skullw_run11=[ $stand13, skullw_run12] {ai_run(0);};
|
|
void() skullw_run12=[ $stand15, skullw_run13] {ai_run(0);};
|
|
void() skullw_run13=[ $stand17, skullw_run14] {ai_run(0);};
|
|
void() skullw_run14=[ $stand19, skullw_run1 ] {ai_run(0);};
|
|
|
|
//============================================================================
|
|
// MELEE - Teleportation
|
|
//============================================================================
|
|
void() skullw_tfin1 =[ $tele9, skullw_tfin2 ] {
|
|
self.velocity = '0 0 0';
|
|
// Always phase in facing the enemy
|
|
self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin);
|
|
self.angles_y = self.ideal_yaw;
|
|
sound (self, CHAN_AUTO, "skullwiz/blinkin.wav", 1, ATTN_NORM);
|
|
};
|
|
void() skullw_tfin2 =[ $tele7, skullw_tfin3 ] {ai_face();};
|
|
void() skullw_tfin3 =[ $tele5, skullw_tfin4 ] {ai_face();};
|
|
void() skullw_tfin4 =[ $tele3, skullw_tfin5 ] {ai_face();};
|
|
void() skullw_tfin5 =[ $tele1, skullw_run1 ] {ai_face();
|
|
self.bodyphased = MONAI_SKULLWSOLID;
|
|
self.takedamage = DAMAGE_AIM;
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Find new teleport location
|
|
//----------------------------------------------------------------------
|
|
void() skullw_blink = {
|
|
local float dist, edist, blinkfinished, blinkcount;
|
|
local vector org, org_ofs, angle_vec;
|
|
|
|
blinkfinished = TRUE; blinkcount = 0;
|
|
org = angle_vec = org_ofs = '0 0 0';
|
|
org_ofs_z = self.height;
|
|
self.oldorigin = self.origin;
|
|
|
|
// Trace upwards to find ceiling or maximum teleport height
|
|
traceline(self.origin, self.origin+org_ofs, TRUE, self);
|
|
org_ofs = trace_endpos;
|
|
|
|
// Loop around for possible locations
|
|
while(blinkfinished) {
|
|
blinkcount = blinkcount + 1; // next loop
|
|
angle_vec_y = random() * 360; // Random direction
|
|
makevectors(angle_vec); // make vector
|
|
|
|
// Generate random angle and trace teleport direction
|
|
dist = self.distmin + (random() * self.distmax);
|
|
org = org_ofs + (v_forward * dist);
|
|
traceline(org_ofs, org, TRUE, self);
|
|
|
|
// Check distance is far enough away from enemy
|
|
// Move away from any walls (traceline is surface contact)
|
|
dist = vlen(trace_endpos - org_ofs) - 32;
|
|
org = org_ofs + (v_forward * dist);
|
|
edist = vlen(self.enemy.origin - org);
|
|
|
|
// Is the enemy far enough away?
|
|
if (edist > MONAI_MELEESKULLW) {
|
|
// Trace down to floor below new point
|
|
traceline(org, org - '0 0 512', TRUE, self);
|
|
org = trace_endpos + self.view_ofs;
|
|
|
|
// The teleport fuction is working with a radius command
|
|
// and this will ignore walls and just find any position
|
|
// in all directions. This can lead to the skull wizard
|
|
// teleporting behind walls or other parts of the map!
|
|
// This can be prevent in two ways:
|
|
// 1) linesight between skull wizard and player
|
|
// - can leads to less interesting locations (always insight)
|
|
// 2) linesight between new location and old
|
|
// - This allows skull wizard to teleport around corner
|
|
// and still be within the same area!
|
|
//
|
|
// Trace through monsters and check trace fraction
|
|
traceline(org, self.origin, TRUE, self);
|
|
if (trace_fraction == 1) blinkfinished = FALSE;
|
|
}
|
|
|
|
// Been looping too long, back to original location
|
|
if (blinkcount > 10) {
|
|
blinkfinished = FALSE;
|
|
org = self.origin;
|
|
}
|
|
}
|
|
|
|
// Move to new location
|
|
self.solid = SOLID_SLIDEBOX; // Standard monster movement
|
|
setmodel(self, self.mdl); // Setup model
|
|
setsize (self, self.bbmins, self.bbmaxs); // Restore BB size
|
|
setorigin(self, org); // move to new location
|
|
|
|
// Test if new location is solid?
|
|
if (!walkmove (0, 0)) {
|
|
self.solid = SOLID_NOT; // No world interaction yet
|
|
setmodel(self,""); // Turn off model
|
|
setorigin(self, self.oldorigin); // restore original location
|
|
self.nextthink = time + 1; // Keep checking
|
|
self.think = skullw_blink;
|
|
}
|
|
else skullw_tfin1();
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Cycle around teleport chain lists instead
|
|
//----------------------------------------------------------------------
|
|
void() skullw_waitforlink =
|
|
{
|
|
self.nextthink = time + 0.5 + random();
|
|
self.think = skullw_waitforlink;
|
|
setorigin(self, self.movelast.origin); // move to new location
|
|
self.enemydist = range_distance(self.enemy, FALSE);
|
|
|
|
// Is the enemy too close or dead?
|
|
if (self.enemydist > self.movelast.distance || self.enemy.health < 1) {
|
|
// Move to new location
|
|
self.solid = SOLID_SLIDEBOX; // Standard monster movement
|
|
setmodel(self, self.mdl); // Setup model
|
|
setsize (self, self.bbmins, self.bbmaxs); // Restore BB size
|
|
|
|
// Test if new location is solid?
|
|
if (!walkmove (0, 0)) {
|
|
self.solid = SOLID_NOT; // No world interaction yet
|
|
setmodel(self,""); // Turn off model
|
|
}
|
|
else skullw_tfin1();
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() skullw_checkblinklist = {
|
|
// Does the blink list exist already?
|
|
if (self.movelast.classtype != CT_SKULLTELEPORT) {
|
|
self.movelast = find(world,targetname,self.target2);
|
|
// Does the entity chain exist?
|
|
if (self.movelast.classtype == CT_SKULLTELEPORT)
|
|
skullw_waitforlink();
|
|
else {
|
|
// target2 is not valid, do regular teleport
|
|
self.target2 = "";
|
|
skullw_blink();
|
|
}
|
|
}
|
|
else {
|
|
// Move forward in chain and try next spawn location
|
|
self.movelast = self.movelast.enemy;
|
|
skullw_waitforlink();
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// touch book and phase out
|
|
//----------------------------------------------------------------------
|
|
void() skullw_tele1 =[ $spellB3, skullw_tele2 ] {
|
|
self.pain_finished = time + 1; // no pain during teleportation
|
|
if (random() < 0.5) sound (self, CHAN_VOICE, "skullwiz/blinkspk1.wav", 1, ATTN_NORM);
|
|
else sound (self, CHAN_VOICE, "skullwiz/blinkspk2.wav", 1, ATTN_NORM);
|
|
};
|
|
void() skullw_tele2 =[ $spellB5, skullw_tele3 ] {};
|
|
void() skullw_tele3 =[ $spellB7, skullw_tele4 ] {};
|
|
void() skullw_tele4 =[ $spellB9, skullw_tele6 ] {};
|
|
void() skullw_tele6 =[ $spellB13, skullw_tele8 ] {};
|
|
void() skullw_tele8 =[ $tele1, skullw_tele9 ] {
|
|
self.takedamage = DAMAGE_NO; // No damage or impact, phasing out
|
|
self.solid = SOLID_NOT;
|
|
self.bodyphased = MONAI_SKULLWINVIS;
|
|
particle_explode(self.origin, 100, 1, self.part_style, PARTICLE_BURST_SKULLUP);
|
|
sound (self, CHAN_VOICE, "skullwiz/blinkout.wav", 1, ATTN_NORM);
|
|
};
|
|
void() skullw_tele9 =[ $tele3, skullw_tele10 ] {};
|
|
void() skullw_tele10=[ $tele5, skullw_tele11 ] {};
|
|
void() skullw_tele11=[ $tele7, skullw_tele12 ] {};
|
|
void() skullw_tele12=[ $tele9, skullw_tele13 ] {};
|
|
void() skullw_tele13 = {
|
|
setmodel(self,"");
|
|
// Check if a chain list has been defined
|
|
if (self.target2 != "") skullw_checkblinklist();
|
|
else skullw_blink();
|
|
};
|
|
|
|
//============================================================================
|
|
// Rocket Skull Attack (Regular)
|
|
//============================================================================
|
|
void(vector orgofs) skullw_fire =
|
|
{
|
|
local vector org, dir;
|
|
|
|
ai_face();
|
|
self.effects = self.effects | EF_MUZZLEFLASH;
|
|
if (random() < 0.5) sound (self, CHAN_WEAPON, "skullwiz/skull1.wav", 1, ATTN_NORM);
|
|
else sound (self, CHAN_WEAPON, "skullwiz/skull2.wav", 1, ATTN_NORM);
|
|
|
|
makevectors (self.angles);
|
|
org = self.origin + attack_vector(orgofs);
|
|
dir = normalize(self.enemy.origin - org);
|
|
|
|
self.attack_speed = SPEED_SWIZMISSILE + (skill * SPEED_SWIZSKILL);
|
|
Launch_Missile (org, dir, '0 0 0', CT_PROJ_SKULLW, self.attack_speed);
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() skullw_attack1 =[ $spellA3, skullw_attack2 ] {
|
|
sound (self, CHAN_VOICE, "skullwiz/attack.wav", 1, ATTN_NORM);};
|
|
void() skullw_attack2 =[ $spellA5, skullw_attack3 ] {ai_face();};
|
|
void() skullw_attack3 =[ $spellA7, skullw_attack4 ] {ai_face();};
|
|
void() skullw_attack4 =[ $spellA10, skullw_attack5 ] {ai_face();};
|
|
void() skullw_attack5 =[ $spellA11, skullw_attack6 ] {ai_face();};
|
|
void() skullw_attack6 =[ $spellA12, skullw_attack7 ] {skullw_fire('30 0 16');};
|
|
void() skullw_attack7 =[ $spellA13, skullw_attack8 ] {};
|
|
void() skullw_attack8 =[ $spellA15, skullw_run1 ] {};
|
|
|
|
//============================================================================
|
|
// Poison Skull Attack (Guardians)
|
|
//============================================================================
|
|
void() skullw_poison_touch =
|
|
{
|
|
if (other.solid == SOLID_TRIGGER) return;
|
|
if (other.classtype == CT_TEMPSTREAM) return;
|
|
if (other.health > 0) return;
|
|
if (self.attack_finished > time) return;
|
|
|
|
self.attack_finished = time + 1;
|
|
self.modelindex = 0; // Make sure no model
|
|
self.model = "";
|
|
self.think = SUB_Remove;
|
|
self.nextthink = time + 0.1;
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() skullw_poison_think =
|
|
{
|
|
// Randomly drift upwards and slow down forward movement
|
|
self.velocity_z = self.velocity_z + 10 + (random() * 20);
|
|
self.velocity = self.velocity - (self.velocity * 0.125);
|
|
|
|
self.frame = self.frame + 1;
|
|
if (self.frame > self.count) SUB_Remove();
|
|
else self.nextthink = time + 0.1;
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() skullw_guardian =
|
|
{
|
|
local float poison_loop;
|
|
local vector poison_angles, org, dir;
|
|
|
|
// Double check the skull wizard has poisonous flag set
|
|
if (!self.poisonous) return;
|
|
|
|
// Remove projectile
|
|
entity_remove(self, 1);
|
|
// Play explosion sound
|
|
sound (self, CHAN_WEAPON, "skullwiz/poison_hit.wav", 1, ATTN_NORM);
|
|
|
|
// Apply any direct damage and poison effect
|
|
if (other.takedamage) {
|
|
T_Damage (other, self, self.owner, DAMAGE_SWIZPOISON, DAMARMOR);
|
|
if (other.flags & FL_CLIENT) PoisonDeBuff(other);
|
|
}
|
|
|
|
poison_loop = 0;
|
|
poison_angles = '0 0 0';
|
|
|
|
while (poison_loop < 360) {
|
|
newmis = spawn();
|
|
newmis.classtype = CT_TEMPSTREAM;
|
|
newmis.movetype = MOVETYPE_FLY; // Fly, no gravity
|
|
newmis.solid = SOLID_TRIGGER;
|
|
setmodel(newmis, SBURST_POISON);
|
|
|
|
org = self.origin;
|
|
org_z = org_z + (crandom() * 24);
|
|
setorigin(newmis, org);
|
|
setsize (newmis, VEC_ORIGIN, VEC_ORIGIN);
|
|
|
|
poison_angles_y = poison_loop;
|
|
makevectors(poison_angles);
|
|
dir = vecrand(0,50,FALSE);
|
|
dir = dir + (v_forward * (400 + random() * 50));
|
|
newmis.velocity = dir;
|
|
newmis.count = 6;
|
|
newmis.frame = rint(random()*2);
|
|
|
|
// If DP engine active remove particle shadow
|
|
if (engine == ENG_DPEXT) newmis.effects = newmis.effects + EF_NOSHADOW;
|
|
|
|
newmis.think = skullw_poison_think;
|
|
newmis.touch = skullw_poison_touch;
|
|
newmis.nextthink = time + 0.1;
|
|
|
|
poison_loop = poison_loop + rint(random()*15);
|
|
}
|
|
};
|
|
|
|
//============================================================================
|
|
// SUMMON MINION - Purple magic ball
|
|
//============================================================================
|
|
void() skullw_create_attachment =
|
|
{
|
|
// Are the attachments setup yet?
|
|
if (!self.attachment) {
|
|
self.attachment = spawn();
|
|
self.attachment.owner = self;
|
|
self.attachment.classtype = CT_ATTACHMENT;
|
|
self.attachment.mdl = MODEL_PROJ_SWBALL;
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() skullw_finish_attachment =
|
|
{
|
|
if (self.attachment) {
|
|
setmodel(self.attachment, "");
|
|
self.attachment.state = STATE_OFF;
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() skullw_remove_attachment =
|
|
{
|
|
if (self.attachment) {
|
|
self.attachment.think = SUB_Remove;
|
|
self.attachment.nextthink = time + 0.1;
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Display a purple ball in the hand of the skull wizard
|
|
//----------------------------------------------------------------------
|
|
void(vector orgofs, float dbframe) skullw_sumball =
|
|
{
|
|
local vector org;
|
|
|
|
// Frame 0 is start of the sequence (move everything into place)
|
|
if (dbframe == 0) {
|
|
self.attachment.state = STATE_ON;
|
|
setorigin(self.attachment, self.pos2);
|
|
setmodel(self.attachment, self.attachment.mdl);
|
|
setsize (self.attachment, VEC_ORIGIN, VEC_ORIGIN);
|
|
self.attachment.movetype = MOVETYPE_NONE;
|
|
self.attachment.solid = SOLID_NOT;
|
|
self.attachment.skin = 0;
|
|
self.attachment.alpha = 0.85;
|
|
}
|
|
|
|
// Generate attachment in hand (left)
|
|
makevectors(self.angles);
|
|
org = self.origin + attack_vector(orgofs);
|
|
setorigin(self.attachment, org);
|
|
self.attachment.angles_y = rint(random()*359);
|
|
self.attachment.frame = dbframe;
|
|
|
|
// Create particles inside of skull wizard robes floating up
|
|
// once lost soul is spawning, then switch particle location
|
|
if (self.aflag == TRUE) particle_explode(self.pos2, 3, 1, self.part_style, PARTICLE_BURST_MINOTAUR);
|
|
// Need to cut down on particles, too much visual noise
|
|
// else particle_debuff(self.origin, 16, rint(4+random()*4), self.part_style);
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Raise hand up to the sky and spawn purple particles from body
|
|
// Always check if there is space to spawn a lost soul first
|
|
// Block monster pain function so that the summons is completed
|
|
//----------------------------------------------------------------------
|
|
void() skullw_summon1 =[ $summon1, skullw_summon2 ] {
|
|
ai_face();self.pain_finished = time + 2;};
|
|
void() skullw_summon2 =[ $summon3, skullw_summon3 ] {
|
|
ai_face();skullw_sumball('22 -2 6',0);};
|
|
void() skullw_summon3 =[ $summon5, skullw_summon4 ] {
|
|
ai_face(); skullw_sumball('23 -3 7',1);
|
|
sound (self, CHAN_BODY, "skullwiz/summon.wav", 1, ATTN_NORM);
|
|
/* The touch trigger is created early (4 frames) so its gets
|
|
// a chance for a client to touch its bounding box
|
|
// This is a giant trigger area to catch speedy players
|
|
makevectors(self.angles);
|
|
self.pos1 = self.origin + self.view_ofs + v_forward*64 + v_up*16;
|
|
setup_minionspace(self.pos1, '-32 -32 -32', '32 32 32'); */
|
|
};
|
|
void() skullw_summon4 =[ $summon7, skullw_summon5 ] {skullw_sumball('24 -4 9',2);};
|
|
void() skullw_summon5 =[ $summon9, skullw_summon6 ] {skullw_sumball('26 -4 10',3);};
|
|
void() skullw_summon6 =[ $summon11, skullw_summon7 ] {skullw_sumball('26 -4 12',3);};
|
|
void() skullw_summon7 =[ $summon13, skullw_summon8 ] {
|
|
// Find out if there is enough space to spawn a lost soul
|
|
// This is really a tricky thing to test for ...
|
|
// * pointcontents only checks against world space
|
|
// * walkmove does not work properly on the lower plane face
|
|
// * touch triggers only work when something touches not inside
|
|
// * findradius will find a lot of entities depending on radius
|
|
|
|
// Touch trigger version
|
|
//self.aflag = test_minionspace();
|
|
|
|
// Findradius version
|
|
makevectors(self.angles);
|
|
self.pos1 = self.origin + self.view_ofs + v_forward*64 + v_up*16;
|
|
self.aflag = find_minionspace(self.pos1);
|
|
|
|
// If the spawn locaiton all clear, spawn something!
|
|
if (self.aflag == TRUE) {
|
|
minion_lostsoul(self.pos1, self.enemy);
|
|
self.pos2 = self.pos1 - '0 0 16'; // Where to spawn particles
|
|
skullw_sumball('26 -4 14',4);
|
|
}
|
|
};
|
|
void() skullw_summon8 =[ $summon15, skullw_summon9 ] {skullw_sumball('26 -4 16',4);};
|
|
void() skullw_summon9 =[ $summon17, skullw_summon10 ] {skullw_sumball('26 -4 19',4);};
|
|
void() skullw_summon10 =[ $summon19, skullw_summon11 ] {skullw_sumball('26 -5 23',4);};
|
|
void() skullw_summon11 =[ $summon21, skullw_summon12 ] {skullw_sumball('26 -6 30',3);};
|
|
void() skullw_summon12 =[ $summon23, skullw_summon13 ] {skullw_sumball('19 -7 32',1);};
|
|
void() skullw_summon13 =[ $summon25, skullw_summon14 ] {skullw_finish_attachment();
|
|
if (self.aflag == TRUE) particle_explode(self.pos1, 15, 2, self.part_style, PARTICLE_BURST_MINOTAUR);};
|
|
void() skullw_summon14 =[ $summon27, skullw_summon15 ] {ai_face();};
|
|
void() skullw_summon15 =[ $summon29, skullw_run1 ] {
|
|
ai_face();SUB_AttackFinished (1 + 2*random());};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Both type of skull wizards go through the same magic function
|
|
//----------------------------------------------------------------------
|
|
void() skullw_magic =
|
|
{
|
|
local entity miniondef;
|
|
|
|
// Make sure the attachments are setup ready
|
|
skullw_create_attachment();
|
|
|
|
if (self.spawnflags & MON_SKULLWIZ_MINIONS && !self.minion_active) {
|
|
// Check for minion template first
|
|
miniondef = find(world,classname,"monster_skullwizminion");
|
|
if (miniondef.classtype == CT_CACHELOSTSOUL) setup_minionsupport();
|
|
else {
|
|
// If template no available, warn and remove feature
|
|
dprint("\b[SKULLWIZ]\b Cannot find minion template!\n");
|
|
self.spawnflags = self.spawnflags - MON_SKULLWIZ_MINIONS;
|
|
}
|
|
}
|
|
|
|
// Is the target the player or monster?
|
|
if (self.enemy.flags & FL_CLIENT) {
|
|
if (self.spawnflags & MON_SKULLWIZ_MINIONS) {
|
|
// has the lost soul limit been reached?
|
|
if (query_minionactive(self) == TRUE) skullw_summon1();
|
|
else {
|
|
if (self.minion_baseattack > 0 ) skullw_attack1();
|
|
else skullw_run1();
|
|
}
|
|
}
|
|
// Red Skull wizards attacks with skull rockets
|
|
else skullw_attack1();
|
|
|
|
}
|
|
// Always attack monsters with skull rockets
|
|
else skullw_attack1();
|
|
};
|
|
|
|
//============================================================================
|
|
void() skullw_phased = {
|
|
self.nextthink = time + 0.1; // Keep looking for player
|
|
self.think = skullw_phased; // Keep looping
|
|
if (random() < 0.02)
|
|
particle_explode(self.origin, rint(10+random()*10), 1, self.part_style, PARTICLE_BURST_SKULLUP);
|
|
monster_idle_sound(); // High chance of idle sound
|
|
ai_stand(); // Keep looking
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() skullw_wakeup =
|
|
{
|
|
// Reset all monster state functions back to default
|
|
self.th_stand = skullw_stand;
|
|
self.th_walk = skullw_walk;
|
|
self.th_run = skullw_run1;
|
|
self.th_missile = skullw_magic;
|
|
self.th_melee = skullw_tele1;
|
|
self.th_updmissile = skullw_guardian;
|
|
|
|
// Temporarily restore model/bound box
|
|
self.solid = SOLID_SLIDEBOX;
|
|
self.movetype = MOVETYPE_STEP;
|
|
setmodel(self, self.mdl);
|
|
setsize (self, self.bbmins, self.bbmaxs);
|
|
|
|
// Can the skull wizard move?
|
|
if (!walkmove (0, 0)) {
|
|
// Something blocking, teleport elsewhere
|
|
self.solid = SOLID_NOT;
|
|
setmodel(self,"");
|
|
self.nextthink = time + 0.1;
|
|
self.think = skullw_blink;
|
|
}
|
|
else skullw_tfin1();
|
|
};
|
|
|
|
//============================================================================
|
|
// Short body recoil (6 frames)
|
|
void() skullw_painA1 =[ $painA2, skullw_painA2 ] {};
|
|
void() skullw_painA2 =[ $painA4, skullw_painA3 ] {};
|
|
void() skullw_painA3 =[ $painA6, skullw_painA4 ] {};
|
|
void() skullw_painA4 =[ $painA8, skullw_painA5 ] {};
|
|
void() skullw_painA5 =[ $painA10, skullw_painA6 ] {};
|
|
void() skullw_painA6 =[ $painA12, skullw_run1 ] {};
|
|
|
|
// Long expanded out version (10 frames)
|
|
void() skullw_painB1 =[ $painA2, skullw_painB3 ] {};
|
|
void() skullw_painB3 =[ $painA3, skullw_painB4 ] {};
|
|
void() skullw_painB4 =[ $painA4, skullw_painB5 ] {};
|
|
void() skullw_painB5 =[ $painA5, skullw_painB6 ] {};
|
|
void() skullw_painB6 =[ $painA6, skullw_painB7 ] {};
|
|
void() skullw_painB7 =[ $painA7, skullw_painB8 ] {};
|
|
void() skullw_painB8 =[ $painA8, skullw_painB9 ] {};
|
|
void() skullw_painB9 =[ $painA9, skullw_painB10 ] {};
|
|
void() skullw_painB10 =[ $painA10, skullw_painB11 ] {};
|
|
void() skullw_painB11 =[ $painA11, skullw_run1 ] {};
|
|
|
|
//----------------------------------------------------------------------
|
|
void(entity inflictor, entity attacker, float damage) skullw_pain =
|
|
{
|
|
// Cannot feel pain when phased out
|
|
if (self.bodyphased == MONAI_SKULLWINVIS) return;
|
|
|
|
// Check all pain conditions and set up what to do next
|
|
monster_pain_check(attacker, damage);
|
|
skullw_finish_attachment();
|
|
|
|
// Any pain animation/sound required?
|
|
if (self.pain_check > 0) {
|
|
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);
|
|
if (self.pain_check == 1 || self.pain_check == 2) {
|
|
if (random() < 0.85) skullw_painA1(); // classic, body recoil
|
|
else skullw_painB1(); // Really long version
|
|
}
|
|
}
|
|
};
|
|
|
|
//============================================================================
|
|
void() skullw_fadeaway =
|
|
{
|
|
// sound (self, CHAN_BODY, "skullwiz/fadeaway.wav", 1, ATTN_NORM);
|
|
// Particle burst upwards as robes fade away
|
|
particle_explode(self.origin, 100, 3, self.part_style, PARTICLE_BURST_SKULLUP);
|
|
self.think = model_fade;
|
|
self.nextthink = time + 0.1;
|
|
self.ltime = self.nextthink;
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() skullw_explode =
|
|
{
|
|
self.health = MON_NOGIBVELOCITY; // Low gib directional modifier
|
|
particle_explode(self.origin, 100, 3, PARTICLE_BURST_WHITE, PARTICLE_BURST_SKULLUP);
|
|
self.gib2skin = self.exactskin; // Update book skin
|
|
ThrowGib(11,1); ThrowGib(12,1); // Throw head and book
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() skullw_death1 =[ $death2, skullw_death2 ] {ai_face();};
|
|
void() skullw_death2 =[ $death3, skullw_death3 ] {ai_face();};
|
|
void() skullw_death3 =[ $death4, skullw_death4 ] {ai_face();self.solid = SOLID_NOT;};
|
|
void() skullw_death4 =[ $death5, skullw_death5 ] {ai_face();};
|
|
void() skullw_death5 =[ $death6, skullw_death6 ] {skullw_explode();};
|
|
void() skullw_death6 =[ $death7, skullw_death7 ] {};
|
|
void() skullw_death7 =[ $death8, skullw_death8 ] {};
|
|
void() skullw_death8 =[ $death9, skullw_death9 ] {};
|
|
void() skullw_death9 =[ $death10, skullw_death10 ] {};
|
|
void() skullw_death10 =[ $death11, skullw_death11 ] {};
|
|
void() skullw_death11 =[ $death12, skullw_death12 ] {};
|
|
void() skullw_death12 =[ $death13, skullw_death13 ] {};
|
|
void() skullw_death13 =[ $death14, skullw_death14 ] {};
|
|
void() skullw_death14 =[ $death15, skullw_death14] {
|
|
// Random timer for robes to fade away
|
|
self.think = skullw_fadeaway;
|
|
self.nextthink = time + 8 + random() * 8;
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() skullw_die =
|
|
{
|
|
// Play fade away/death sound
|
|
sound (self, CHAN_VOICE, "skullwiz/death.wav", 1, ATTN_NORM);
|
|
|
|
// Pre-check routine to tidy up extra entities
|
|
monster_death_precheck();
|
|
skullw_remove_attachment();
|
|
|
|
// Special death, Instantly vaporize cloak
|
|
// This is really a gib death without the gibs!
|
|
if (self.health < -40) {
|
|
// Typical Grenade explosion
|
|
WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
|
|
WriteByte (MSG_BROADCAST, TE_EXPLOSION);
|
|
WriteCoord (MSG_BROADCAST, self.origin_x);
|
|
WriteCoord (MSG_BROADCAST, self.origin_y);
|
|
WriteCoord (MSG_BROADCAST, self.origin_z);
|
|
SpawnExplosion(EXPLODE_MED, self.origin, SOUND_REXP3);
|
|
|
|
self.health = MON_NOGIBVELOCITY; // Low gib directional modifier
|
|
self.gib2skin = self.exactskin; // Update book skin
|
|
ThrowGib(11,1); ThrowGib(12,1); // Throw head and book
|
|
entity_hide(self);
|
|
}
|
|
// Regular death, collapse and fade away
|
|
else {
|
|
// Large particle explosion upwards
|
|
particle_explode(self.origin, 100, 3, self.part_style, PARTICLE_BURST_SKULLUP);
|
|
skullw_death1();
|
|
}
|
|
};
|
|
|
|
/*======================================================================
|
|
QUAKED monster_skullwiz (0.5 0 1) (-16 -16 -24) (16 16 40)
|
|
======================================================================*/
|
|
void() monster_skullwiz =
|
|
{
|
|
if (deathmatch) { remove(self); return; }
|
|
|
|
self.mdl = "progs/mon_skullwiz.mdl";
|
|
precache_model (self.mdl);
|
|
precache_model (MODEL_PROJ_SWSKULL); // explosive flaming skull
|
|
precache_model (MODEL_PROJ_SWSKULLP); // explosive poison skull
|
|
precache_model (MODEL_PROJ_SWBALL); // Purple minion ball
|
|
precache_model (SBURST_POISON); // Poison explosion
|
|
|
|
self.gib1mdl = "progs/h_skullwiz.mdl"; // Skull head
|
|
self.gib2mdl = "progs/w_skullbook.mdl"; // Unique book item
|
|
precache_model (self.gib1mdl);
|
|
precache_model (self.gib2mdl);
|
|
|
|
self.idle_sound = "skullwiz/idle1.wav";
|
|
self.idle_sound2 = "skullwiz/idle2.wav";
|
|
precache_sound (self.idle_sound);
|
|
precache_sound (self.idle_sound2);
|
|
|
|
precache_sound ("skullwiz/death.wav");
|
|
self.pain_sound = "skullwiz/pain1.wav";
|
|
self.pain_sound2 = "skullwiz/pain2.wav";
|
|
precache_sound (self.pain_sound);
|
|
precache_sound (self.pain_sound2);
|
|
|
|
precache_sound ("skullwiz/attack.wav"); // fire spell
|
|
precache_sound ("skullwiz/skull1.wav"); // Screaming skull attack
|
|
precache_sound ("skullwiz/skull2.wav");
|
|
precache_sound ("skullwiz/summon.wav");
|
|
precache_sound ("skullwiz/poison_hit.wav"); // Guardian Explosion
|
|
|
|
precache_sound ("skullwiz/blinkspk1.wav"); // Talk casting blink spell
|
|
precache_sound ("skullwiz/blinkspk2.wav");
|
|
precache_sound ("skullwiz/blinkout.wav"); // Sound of blink animation
|
|
precache_sound ("skullwiz/blinkin.wav");
|
|
|
|
self.sight_sound = "skullwiz/sight.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_TALL;
|
|
if (self.health < 1) self.health = 120;
|
|
self.gibhealth = MON_NEVERGIB; // Cannot be gibbed by weapons
|
|
self.gibbed = FALSE; // Still in one piece
|
|
self.pain_flinch = 50; // Armour strength robes!
|
|
self.steptype = FS_TYPEMEDIUM; // Humanoid footsteps
|
|
self.pain_longanim = FALSE; // No long pain animation
|
|
self.poisonous = FALSE; // Only Guardians can be poisonous
|
|
self.deathstring = " was blown apart by a Skull Wizard\n";
|
|
|
|
// Always reset Ammo Resistance to be consistent
|
|
self.resist_shells = self.resist_nails = 0;
|
|
self.resist_rockets = self.resist_cells = 0;
|
|
|
|
// Skull wizards have special animation movement sets
|
|
// Need extra parameters for their teleporting ability
|
|
// Can be setup to start phased out and surprise the player
|
|
self.idlebusy = SKULLW_STAND;
|
|
if (self.height == 0) self.height = SKULLW_HEIGHT;
|
|
if (self.distmin == 0) self.distmin = SKULLW_DISTMIN;
|
|
if (self.distmax == 0) self.distmax = SKULLW_DISTMAX;
|
|
if (!self.bodyphased) self.bodyphased = MONAI_SKULLWSOLID;
|
|
else self.bodyphased = MONAI_SKULLWINVIS;
|
|
|
|
self.th_checkattack = SkullWizCheckAttack;
|
|
self.th_updmissile = skullw_guardian;
|
|
self.th_pain = skullw_pain;
|
|
self.th_die = skullw_die;
|
|
|
|
// Does the skull wizard start phased out?
|
|
// Need to intercept idle/combat functions
|
|
if (self.bodyphased == MONAI_SKULLWINVIS) {
|
|
self.th_stand = skullw_phased;
|
|
self.th_walk = skullw_phased;
|
|
self.th_run = skullw_wakeup;
|
|
self.th_missile = skullw_wakeup;
|
|
self.th_melee = skullw_wakeup;
|
|
}
|
|
else {
|
|
self.th_stand = skullw_stand;
|
|
self.th_walk = skullw_walk;
|
|
self.th_run = skullw_run1;
|
|
self.th_missile = skullw_magic;
|
|
self.th_melee = skullw_tele1;
|
|
}
|
|
|
|
self.classtype = CT_MONSKULLW;
|
|
self.classgroup = CG_WIZARD;
|
|
self.classmove = MON_MOVEWALK;
|
|
|
|
// Special black robes for skull spawner
|
|
if (self.spawnflags & MON_SKULLWIZ_MINIONS) {
|
|
self.health = self.health * 1.5; // 150% HP
|
|
if (!self.exactskin) self.exactskin = 2; // Black robes
|
|
}
|
|
// Special green robes for rune guardians
|
|
else if (self.spawnflags & MON_SKULLWIZ_GUARDIAN) {
|
|
self.health = self.health * 2; // 200% HP
|
|
if (!self.exactskin) self.exactskin = 3; // Green robes
|
|
self.poisonous = TRUE; // Explosive Poison
|
|
}
|
|
|
|
// Make sure the particles match the skin
|
|
if (self.exactskin == 1) self.part_style = PARTICLE_BURST_GREEN;
|
|
else if (self.exactskin == 2) self.part_style = PARTICLE_BURST_PURPLE;
|
|
else if (self.exactskin == 3) self.part_style = PARTICLE_BURST_GREEN;
|
|
else if (self.exactskin == 4) self.part_style = PARTICLE_BURST_WHITE;
|
|
else self.part_style = PARTICLE_BURST_RED;
|
|
|
|
monster_start();
|
|
};
|
|
|
|
/*======================================================================
|
|
/*QUAKED info_skullwiz_destination (0.6 0 0.8) (-16 -16 -24) (16 16 40) x
|
|
Teleport destination for skull wizard ONLY
|
|
-------- KEYS --------
|
|
targetname : part of a chain group (required)
|
|
target : next destination in the chain
|
|
distance : Minimum distance enemy needs to be before spawning
|
|
-------- SPAWNFLAGS --------
|
|
-------- NOTES --------
|
|
Teleport destination for skull wizard ONLY
|
|
|
|
======================================================================*/
|
|
void() info_skullwiz_link =
|
|
{
|
|
local entity currlink, master;
|
|
local float loopcondition;
|
|
|
|
// Teleport destination already been linked
|
|
if (self.enemy) return;
|
|
|
|
// The master entity is the start of the chain
|
|
// the currlink is a link in the chain
|
|
master = self;
|
|
loopcondition = TRUE;
|
|
|
|
while (loopcondition)
|
|
{
|
|
// Setup next link to master and find next link
|
|
self.enemy = master;
|
|
currlink = find(world, targetname, self.target);
|
|
// reached end of list?
|
|
if (!currlink || currlink == master)
|
|
loopcondition = FALSE;
|
|
else {
|
|
// Move forward in the chain
|
|
self.enemy = currlink;
|
|
self = currlink;
|
|
}
|
|
}
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() info_skullwiz_destination =
|
|
{
|
|
// All teleportation points need to be part of a chain
|
|
// Otherwise the skull Wizard would have to use findradius
|
|
// which is expensive if lots of entities are around
|
|
if (self.targetname == "" || self.target == "") {
|
|
dprint("\b[SKULLWIZ_DEST]\b missing target or targetname\n");
|
|
spawn_marker(self.origin, SPNMARK_YELLOW);
|
|
remove(self);
|
|
return;
|
|
}
|
|
|
|
self.classtype = CT_SKULLTELEPORT;
|
|
self.mangle = self.angles;
|
|
// Setup minimum player spawn distance
|
|
if (self.distance <=0) self.distance = MONAI_MELEESKULLW;
|
|
// Setup if the destination is active or not
|
|
self.state = FALSE;
|
|
|
|
// Check for targetname->target loops
|
|
if (self.targetname == self.target) {
|
|
self.enemy = self;
|
|
self.owner = self;
|
|
}
|
|
else {
|
|
// Wait for all targets to spawn
|
|
self.think = info_skullwiz_link;
|
|
self.nextthink = time + 0.2 + random();
|
|
}
|
|
};
|
|
|