/* grapple.qc This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to: Free Software Foundation, Inc. 59 Temple Place - Suite 330 Boston, MA 02111-1307, USA $Id$ */ /* =========================================================================== grapple.qc 12/29/96 Quakeworld-friendly grapple hook code by Wedge (Steve Bond) visit Quake Command http://www.nuc.net/quake Original 'Morning Star' (Grapple Hook) by "Mike" I took care to preserve the speed and damage values of the original Morning Star. Depending on latency, performance should be near exact. =========================================================================== */ // prototypes for WEAPONS.QC functions float () crandom; void (vector org, float damage) SpawnBlood; // Reset_Grapple - Removes the hook and resets its owner's state. // expects a pointer to the hook void (entity rhook) Reset_Grapple = { if (rhook.owner == world) return; sound (rhook.owner, CHAN_NO_PHS_ADD + CHAN_WEAPON, "weapons/bounce2.wav", 1, ATTN_NORM); rhook.owner.on_hook = FALSE; rhook.owner.hook_out = FALSE; rhook.owner.weaponframe = 0; rhook.owner.attack_finished = time + 0.25; rhook.think = SUB_Remove; rhook.nextthink = time; }; // Grapple_Track - Constantly updates the hook's position relative to // what it's hooked to. Inflicts damage if attached to // a player that is not on the same team as the hook's // owner. void () Grapple_Track = { // Release dead targets if (self.enemy.classname == "player" && self.enemy.health <= 0) { self.owner.on_hook = FALSE; self.owner.attack_finished = time +0.75; } // drop the hook if owner is dead or has released the button if (!self.owner.on_hook || self.owner.health <= 0) { Reset_Grapple (self); return; } // bring the pAiN! if (self.enemy.classname == "player") { // 4.1, if we can't see our enemy, unlock if (!CanDamage(self.enemy, self.owner)) { Reset_Grapple(self); return; } // move the hook along with the player. It's invisible, but // we need this to make the sound come from the right spot setorigin (self, self.enemy.origin); sound (self, CHAN_WEAPON, "blob/land1.wav", 1, ATTN_NORM); T_Damage (self.enemy, self, self.owner, 1); makevectors (self.v_angle); SpawnBlood (self.enemy.origin, 1); } // If the hook is not attached to the player, constantly copy the target's // velocity. Velocity copying DOES NOT work properly for a hooked client. if (self.enemy.classname != "player") self.velocity = self.enemy.velocity; self.nextthink = time + 0.1; }; // MakeLink - spawns the chain link entities entity () MakeLink = { newmis = spawn (); newmis.movetype = MOVETYPE_FLYMISSILE; newmis.solid = SOLID_NOT; newmis.owner = self; // SELF is the hook! newmis.avelocity = '200 200 200'; setmodel (newmis, "progs/bit.mdl"); setorigin (newmis, self.origin); setsize (newmis, '0 0 0' , '0 0 0'); return newmis; }; // Remove_Chain - Removes all chain link entities; this is a separate // function because CLIENT also needs to be able // to remove the chain. Only one function required to // remove all links. void () Remove_Chain = { self.think = SUB_Remove; self.nextthink = time; if (self.goalentity) { self.goalentity.think = SUB_Remove; self.goalentity.nextthink = time; if (self.goalentity.goalentity) { self.goalentity.goalentity.think = SUB_Remove; self.goalentity.goalentity.nextthink = time; } } }; // Update_Chain - Repositions the chain links each frame. This single function // maintains the positions of all of the links. Only one link // is thinking every frame. void () Update_Chain = { local vector temp; if (!self.owner.hook_out) { self.think = Remove_Chain; self.nextthink = time; return; } temp = (self.owner.hook.origin - self.owner.origin); // These numbers are correct assuming 3 links. // 4 links would be *20 *40 *60 and *80 setorigin (self, self.owner.origin + temp * 0.25); setorigin (self.goalentity, self.owner.origin + temp * 0.5); setorigin (self.goalentity.goalentity, self.owner.origin + temp * 0.75); self.nextthink = time + 0.1; }; // Build_Chain - Builds the chain (linked list) void () Build_Chain = { self.goalentity = MakeLink(); self.goalentity.think = Update_Chain; self.goalentity.nextthink = time + 0.1; self.goalentity.owner = self.owner; self.goalentity.goalentity = MakeLink (); self.goalentity.goalentity.goalentity = MakeLink (); }; // Check_Overhead - Makes sure there is sufficient headroom above the player // so that setorigin doesn't stick them into a wall. I tried // to compare pointcontents, but that was too flaky. float () Check_Overhead = { makevectors (self.owner.angles); // quick check right above head traceline (self.owner.origin - '0 0 24', self.owner.origin - '0 0 24', FALSE, self.owner); if (trace_fraction != 1.0) return FALSE; traceline (self.owner.origin - '0 0 24' - v_forward * 16, self.owner.origin - '0 0 24' - v_forward * 16 + '0 0 58', FALSE, self.owner); if (trace_fraction != 1.0) return FALSE; traceline (self.owner.origin - '0 0 24' + v_forward * 16, self.owner.origin - '0 0 24' + v_forward * 16 + '0 0 58', FALSE, self.owner); if (trace_fraction != 1.0) return FALSE; traceline (self.owner.origin - '0 0 24' - v_right * 16, self.owner.origin - '0 0 24' - v_right * 16 + '0 0 58', FALSE, self.owner); if (trace_fraction != 1.0) return FALSE; traceline (self.owner.origin - '0 0 24' + v_right * 16, self.owner.origin - '0 0 24' + v_right * 16 + '0 0 58', FALSE, self.owner); if (trace_fraction != 1.0) return FALSE; return TRUE; }; // Anchor_Grapple - Tries to anchor the grapple to whatever it touches void () Anchor_Grapple = { if (other == self.owner) return; // DO NOT allow the grapple to hook to any projectiles, no matter WHAT! // if you create new types of projectiles, make sure you use one of the // classnames below or write code to exclude your new classname so // grapples will not stick to them. if (other.classname == "missile" || other.classname == "grenade" || other.classname == "spike" || other.classname == "hook") return; // Don't stick the the sky. if (pointcontents (self.origin) == CONTENT_SKY) { Reset_Grapple (self); return; } if (other.classname == "player") { // glance off of teammates if (other.steam == self.owner.steam) return; sound (self, CHAN_WEAPON, "player/axhit1.wav", 1, ATTN_NORM); T_Damage (other, self, self.owner, 10); // make hook invisible since we will be pulling directly towards the // player the hook hit. Quakeworld makes it too quirky to try to match // hook's velocity with that of the client that it hit. setmodel (self, ""); } else { sound (self, CHAN_WEAPON, "player/axhit2.wav", 1, ATTN_NORM); // One point of damage inflicted upon impact. Subsequent damage will // only be done to PLAYERS... this way secret doors and triggers will // only be damaged once. if (other.takedamage) T_Damage (other, self, self.owner, 1); self.velocity = '0 0 0'; self.avelocity = '0 0 0'; } // conveniently clears the sound channel of the CHAIN1 sound, which is a // looping sample and would continue to play. Tink1 is the least offensive // choice, ass NULL.WAV loops and clogs the channel with silence sound (self.owner, CHAN_NO_PHS_ADD + CHAN_WEAPON, "weapons/tink1.wav", 1, ATTN_NORM); if (!self.owner.button0) { Reset_Grapple (self); return; } /* // our last chance to avoid being picked up off of the ground. check over // the client's head to make sure there is one unit clearance so we can // lift him off of the ground. test = Check_Overhead (); if (!test) { Reset_Grapple (self); return; } */ if (self.owner.flags & FL_ONGROUND) { self.owner.flags &= ~FL_ONGROUND; // setorigin (self.owner,self.owner.origin + '0 0 1'); } self.owner.on_hook = TRUE; sound (self.owner, CHAN_WEAPON, "weapons/chain2.wav", 1, ATTN_NORM); // CHAIN2 is a looping sample. Use LEFTY as a flag so that client. qc will // know to only play the tink sound ONCE to clear the weapons sound // channel. (Lefty is a leftover from AI.QC, so I reused it to avoid // adding a field) self.owner.lefty = TRUE; self.enemy = other; // remember this guy! self.think = Grapple_Track; self.nextthink = time; self.solid = SOLID_NOT; self.touch = SUB_Null; }; // Throw_Grapple - called from WEAPONS.QC, 'fires' the grapple void () Throw_Grapple = { if (self.hook_out) // reject subsequent calls from player.qc return; msg_entity = self; WriteByte (MSG_ONE, SVC_SMALLKICK); // chain out sound (loops) sound (self, CHAN_WEAPON, "weapons/chain1.wav", 1, ATTN_NORM); newmis = spawn (); newmis.movetype = MOVETYPE_FLYMISSILE; newmis.solid = SOLID_BBOX; newmis.owner = self;// newmis belongs to me self.hook = newmis;// This is my newmis newmis.classname = "hook"; makevectors (self.v_angle); newmis.velocity = v_forward * 800; newmis.avelocity = '0 0 -500'; newmis.touch = Anchor_Grapple; newmis.think = Build_Chain; // don't jam newmis & links into same packet newmis.nextthink = time + 0.1; setmodel (newmis,"progs/star.mdl"); setorigin (newmis, self.origin + v_forward * 16 + '0 0 16'); setsize(newmis, '0 0 0' , '0 0 0 '); self.hook_out = TRUE; }; // Service_Grapple - called each frame by CLIENT.QC if client is ON_HOOK void () Service_Grapple = { local vector hook_dir = '0 0 0'; // drop the hook if player lets go of button if (!self.button0) { if (self.weapon == IT_GRAPPLE) { Reset_Grapple (self.hook); return; } } // If hooked to a player, track them directly! if (self.hook.enemy.classname == "player") hook_dir = (self.hook.enemy.origin - self.origin); else if (self.hook.enemy.classname != "player") // track to hook hook_dir = (self.hook.origin - self.origin); self.velocity = normalize (hook_dir) * 750; if (vlen(hook_dir) <= 100 && self.lefty) { // cancel chain sound // If there is a chain, ditch it now. We're close enough. Having extra // entities lying around is never a good idea. if (self.hook.goalentity) { self.hook.goalentity.think = Remove_Chain; self.hook.goalentity.nextthink = time; } sound (self, CHAN_NO_PHS_ADD + CHAN_WEAPON, "weapons/chain3.wav", 1, ATTN_NORM); self.lefty = FALSE; // we've reset the sound channel. } };