/****************************************************************************/ /* */ /* project : CGF (c) 1999 William van der Sterren */ /* parts (c) 1998 id software */ /* */ /* file : cgf_sfx_glass.cpp "special effects for glass entities" */ /* author(s): William van der Sterren */ /* version : 0.5 */ /* */ /* date (last revision): Jun 12, 99 */ /* date (creation) : Jun 04, 99 */ /* */ /* */ /* revision history */ /* -- date ---- | -- revision ---------------------- | -- revisor -- */ /* Jun 12, 1999 | fixed knife slash breaks glass | William */ /* Jun 08, 1999 | improved fragment limit | William */ /* */ /******* http://www.botepidemic.com/aid/cgf for CGF for Action Quake2 *******/ #ifdef __cplusplus // VC++, for CGF #include // prevent problems between C and STL extern "C" { #include "g_local.h" #include "cgf_sfx_glass.h" } #else // C, for other AQ2 variants #include "g_local.h" #include "cgf_sfx_glass.h" #endif // cvar for breaking glass static cvar_t *breakableglass = 0; // cvar for max glass fragment count static cvar_t *glassfragmentlimit = 0; static int glassfragmentcount = 0; // additional functions - Q2 expects C calling convention #ifdef __cplusplus extern "C" { #endif void CGF_SFX_TouchGlass(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf); // called whenever an entity hits the trigger spawned for the glass void CGF_SFX_EmitGlass (edict_t* aGlassPane, edict_t* anInflictor, vec3_t aPoint); // emits glass fragments from aPoint, to show effects of firing thru window void CGF_SFX_BreakGlass(edict_t* aGlassPane, edict_t* anOther, edict_t* anAttacker, int aDamage, vec3_t aPoint, vec_t aPaneDestructDelay ); // breaks glass void CGF_SFX_InstallBreakableGlass(edict_t* aGlassPane); // when working on a glass pane for the first time, just install trigger // when working on a glass pane again (after a game ended), move // glass back to original location void CGF_SFX_HideBreakableGlass(edict_t* aGlassPane); // after being broken, the pane cannot be removed as it is needed in // subsequent missions/games, so hide it at about z = -1000 void CGF_SFX_ApplyGlassFragmentLimit(const char* aClassName); // updates glassfragmentcount and removes oldest glass fragement if // necessary to meet limit void CGF_SFX_MiscGlassUse(edict_t *self, edict_t *other, edict_t *activator); // catches use from unforeseen objects (weapons, debris, // etc. touching the window) void CGF_SFX_MiscGlassDie(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point); // catches die calls caused by unforeseen objects (weapons, debris, // etc. damaging the window) void CGF_SFX_GlassThrowDebris (edict_t *self, char *modelname, float speed, vec3_t origin); // variant of id software's ThrowDebris, now numbering the entity (for later removal) extern // from a_game.c edict_t *FindEdictByClassnum (char *classname, int classnum); // declaration from g_misc.c extern // from g_misc.c void debris_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point); #ifdef __cplusplus } #endif void CGF_SFX_InstallGlassSupport() { breakableglass = gi.cvar("breakableglass", "0", 0); glassfragmentlimit = gi.cvar("glassfragmentlimit", "30", 0); } int CGF_SFX_IsBreakableGlassEnabled() { // returns whether breakable glass is enabled (cvar) and allowed (dm mode) return breakableglass->value; } void CGF_SFX_TestBreakableGlassAndRemoveIfNot_Think(edict_t* aPossibleGlassEntity) { // at level.time == 0.1 the entity has been introduced in the game, // and we can use gi.pointcontents and gi.trace to check the entity vec3_t origin; int breakingglass; trace_t trace; // test for cvar if (!CGF_SFX_IsBreakableGlassEnabled()) { G_FreeEdict(aPossibleGlassEntity); return; } VectorAdd(aPossibleGlassEntity->absmax, aPossibleGlassEntity->absmin, origin); VectorScale(origin, 0.5, origin); // detect glass (does not work for complex shapes, // for example, the glass window near the satellite // dish at Q2 base3 breakingglass = (gi.pointcontents(origin) & CONTENTS_TRANSLUCENT); if (!breakingglass) { // test for complex brushes that happen to be // hollow in their origin (for instance, the // window at Q2 base3, near the satellite dish trace = gi.trace(origin, vec3_origin, vec3_origin, aPossibleGlassEntity->absmax, 0, MASK_PLAYERSOLID ); breakingglass = ((trace.ent == aPossibleGlassEntity) && (trace.contents & CONTENTS_TRANSLUCENT) ); trace = gi.trace(origin, vec3_origin, vec3_origin, aPossibleGlassEntity->absmin, 0, MASK_PLAYERSOLID ); breakingglass = ((breakingglass) || ((trace.ent == aPossibleGlassEntity) && (trace.contents & CONTENTS_TRANSLUCENT) ) ); } if (!breakingglass) { // do remove other func_explosives G_FreeEdict (aPossibleGlassEntity); return; } // discovered some glass - now make store the origin // we need that after hiding the glass VectorCopy(aPossibleGlassEntity->s.origin, aPossibleGlassEntity->pos1); // IMPORTANT! // make a backup of the health in light_level aPossibleGlassEntity->light_level = aPossibleGlassEntity->health; // install the glass CGF_SFX_InstallBreakableGlass(aPossibleGlassEntity); } void CGF_SFX_InstallBreakableGlass(edict_t* aGlassPane) { // when working on a glass pane for the first time, just install trigger // when working on a glass pane again (after a game ended), move // glass back to original location edict_t* trigger; vec3_t maxs; vec3_t mins; // reset origin based on aGlassPane->pos1 VectorCopy(aGlassPane->pos1, aGlassPane->s.origin); // reset health based on aGlassPane->light_level aGlassPane->health = aGlassPane->light_level; // replace die and use functions by glass specific ones aGlassPane->die = CGF_SFX_MiscGlassDie; aGlassPane->use = CGF_SFX_MiscGlassUse; // reset some pane attributes aGlassPane->takedamage = DAMAGE_YES; aGlassPane->solid = SOLID_BSP; aGlassPane->movetype = MOVETYPE_FLYMISSILE; // for other movetypes, cannot move pane to hidden location and back // try to establish size VectorCopy(aGlassPane->maxs, maxs); VectorCopy(aGlassPane->mins, mins); // set up trigger, similar to triggers for doors // but with a smaller box mins[0] -= 24; mins[1] -= 24; mins[2] -= 24; maxs[0] += 24; maxs[1] += 24; maxs[2] += 24; // adjust some settings trigger = G_Spawn (); trigger->classname = "breakableglass_trigger"; VectorCopy (mins, trigger->mins); VectorCopy (maxs, trigger->maxs); trigger->owner = aGlassPane; trigger->solid = SOLID_TRIGGER; trigger->movetype = MOVETYPE_NONE; trigger->touch = CGF_SFX_TouchGlass; gi.linkentity (trigger); } void CGF_SFX_ShootBreakableGlass(edict_t* aGlassPane, edict_t* anAttacker, /*trace_t**/ void* tr, int mod ) { // process gunshots thru glass edict_t* trigger; int destruct; // depending on mod, destroy window or emit fragments switch (mod) { // break for ap, shotgun, handcannon, and kick, destory window case MOD_M3 : case MOD_HC : case MOD_SNIPER : case MOD_KICK : case MOD_GRENADE : case MOD_G_SPLASH: case MOD_HANDGRENADE: case MOD_HG_SPLASH: case MOD_KNIFE : // slash damage destruct = true; break; default : destruct = (rand() % 3 == 0); break; }; if (destruct) { // break glass (and hurt if doing kick) CGF_SFX_BreakGlass(aGlassPane, anAttacker, 0, aGlassPane->health, vec3_origin, FRAMETIME); if (mod == MOD_KICK) { vec3_t bloodorigin; vec3_t dir; vec3_t normal; VectorAdd(aGlassPane->absmax, aGlassPane->absmin, bloodorigin); VectorScale(bloodorigin, 0.5, bloodorigin); VectorSubtract(bloodorigin, anAttacker->s.origin, dir); VectorNormalize(dir); VectorMA(anAttacker->s.origin, 32.0, dir, bloodorigin); VectorSet(normal, 0, 0, -1); T_Damage(anAttacker, aGlassPane, anAttacker, dir, bloodorigin, normal, 15.0, 0, 0, MOD_BREAKINGGLASS); } // remove corresponding trigger trigger = 0; while (trigger = G_Find(trigger, FOFS(classname), "breakableglass_trigger")) { if (trigger->owner == aGlassPane) { // remove it G_FreeEdict(trigger); // only one to be found break; } } } else { // add decal (if not grenade) if ( (mod != MOD_HANDGRENADE) && (mod != MOD_HG_SPLASH) && (mod != MOD_GRENADE) && (mod != MOD_G_SPLASH) ) { AddDecal(anAttacker, (trace_t*) tr); } // and emit glass CGF_SFX_EmitGlass(aGlassPane, anAttacker, ((trace_t*) tr)->endpos); } } void CGF_SFX_TouchGlass(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf) { // called whenever an entity hits the trigger spawned for the glass vec3_t origin; vec3_t normal; vec3_t spot; trace_t trace; edict_t* glass; vec3_t velocity; vec_t speed; vec_t projected_speed; int is_hgrenade; int is_knife; is_hgrenade = is_knife = false; // ignore non-clients-non-grenade-non-knife if (!other->client) { is_knife = (0 == Q_stricmp("weapon_knife", other->classname)); if (!is_knife) { is_hgrenade = (0 == Q_stricmp("hgrenade", other->classname)); } if ((!is_knife) && (!is_hgrenade)) return; if (is_knife) goto knife_and_grenade_handling; } // test whether other really hits the glass - deal with // the special case that other hits some boundary close to the border of the glass pane // // // ....trigger....... // +++++++++ +++++++++++ // .+---glass------+. // wall .+--------------+.wall // +++++++++ +++++++++++ // ----->.................. // wrong ^ ^ // | | // wrong ok // glass = self->owner; // hack - set glass' movetype to MOVETYPE_PUSH as it is not // moving as long as the trigger is active glass->movetype = MOVETYPE_PUSH; VectorAdd(glass->absmax, glass->absmin, origin); VectorScale(origin, 0.5, origin); // other needs to be able to trace to glass origin trace = gi.trace(other->s.origin, vec3_origin, vec3_origin, origin, other, MASK_PLAYERSOLID ); if (trace.ent != glass) return; // we can reach the glass origin, so we have the normal of // the glass plane VectorCopy(trace.plane.normal, normal); // we need to check if client is not running into wall next // to the glass (the trigger stretches into the wall) VectorScale(normal, -1000.0, spot); VectorAdd(spot, other->s.origin, spot); // line between other->s.origin and spot (perpendicular to glass // surface should not hit wall but glass instead trace = gi.trace(other->s.origin, vec3_origin, vec3_origin, spot, other, MASK_PLAYERSOLID ); if (trace.ent != glass) return; // now, we check if the client's speed perpendicular to // the glass plane, exceeds the required 175 // (speed should be < -200, as the plane's normal // points towards the client VectorCopy(other->velocity, velocity); speed = VectorNormalize(velocity); projected_speed = speed * DotProduct(velocity, normal); // bump projected speed for grenades - they should break // the window more easily if (is_hgrenade) projected_speed *= 1.5; // if hitting the glass with sufficient speed (project < -175), // being jumpkicked (speed > 700, project < -5) break the window if (!((projected_speed < -175.0) || ((projected_speed < -5) && (speed > 700)) ) ) goto knife_and_grenade_handling; // break glass CGF_SFX_BreakGlass(glass, other, other, glass->health, vec3_origin, 3.0 * FRAMETIME); // glass can take care of itself, but the trigger isn't needed anymore G_FreeEdict (self); /* not needed // reduce momentum of the client (he just broke the window // so he should lose speed. in addition, it doesn't feel // right if he overtakes the glass fragments // VectorScale(normal, 200.0, velocity); // VectorAdd(other->velocity, velocity, other->velocity); */ // make sure client takes damage T_Damage(other, glass, other, normal, other->s.origin, normal, 15.0, 0, 0, MOD_BREAKINGGLASS); return; // goto label knife_and_grenade_handling: // if knife or grenade, bounce them if ((is_knife) || (is_hgrenade)) { // change clipmask to bounce of glass other->clipmask = MASK_SOLID; } } void CGF_SFX_BreakGlass(edict_t* aGlassPane, edict_t* anInflictor, edict_t* anAttacker, int aDamage, vec3_t aPoint, vec_t aPaneDestructDelay ) { // based on func_explode, but with lotsa subtle differences vec3_t origin; vec3_t old_origin; vec3_t chunkorigin; vec3_t size; int count; int mass; // bmodel origins are (0 0 0), we need to adjust that here VectorCopy(aGlassPane->s.origin, old_origin); VectorScale (aGlassPane->size, 0.5, size); VectorAdd (aGlassPane->absmin, size, origin); VectorCopy (origin, aGlassPane->s.origin); aGlassPane->takedamage = DAMAGE_NO; VectorSubtract (aGlassPane->s.origin, anInflictor->s.origin, aGlassPane->velocity); VectorNormalize (aGlassPane->velocity); // use speed 250 instead of 150 for funkier glass spray VectorScale (aGlassPane->velocity, 250.0, aGlassPane->velocity); // start chunks towards the center VectorScale (size, 0.75, size); mass = aGlassPane->mass; if (!mass) mass = 75; // big chunks if (mass >= 100) { count = mass / 100; if (count > 8) count = 8; while(count--) { CGF_SFX_ApplyGlassFragmentLimit("debris"); chunkorigin[0] = origin[0] + crandom() * size[0]; chunkorigin[1] = origin[1] + crandom() * size[1]; chunkorigin[2] = origin[2] + crandom() * size[2]; CGF_SFX_GlassThrowDebris (aGlassPane, "models/objects/debris1/tris.md2", 1, chunkorigin); } } // small chunks count = mass / 25; if (count > 16) count = 16; while(count--) { CGF_SFX_ApplyGlassFragmentLimit("debris"); chunkorigin[0] = origin[0] + crandom() * size[0]; chunkorigin[1] = origin[1] + crandom() * size[1]; chunkorigin[2] = origin[2] + crandom() * size[2]; CGF_SFX_GlassThrowDebris (aGlassPane, "models/objects/debris2/tris.md2", 2, chunkorigin); } // clear velocity, reset origin (that has been abused in ThrowDebris) VectorClear(aGlassPane->velocity); VectorCopy (old_origin, aGlassPane->s.origin); if (anAttacker) { // jumping thru G_UseTargets (aGlassPane, anAttacker); } else { // firing thru - the pane has no direct attacker to hurt, // but G_UseTargets expects one. So make it a DIY G_UseTargets (aGlassPane, aGlassPane); } // have glass plane be visible for two more frames, // and have it self-destruct then // meanwhile, make sure the player can move thru aGlassPane->solid = SOLID_NOT; aGlassPane->think = CGF_SFX_HideBreakableGlass; aGlassPane->nextthink = level.time + aPaneDestructDelay; } void CGF_SFX_EmitGlass (edict_t* aGlassPane, edict_t* anInflictor, vec3_t aPoint) { // based on func_explode, but with lotsa subtle differences vec3_t old_origin; vec3_t chunkorigin; vec3_t size; int count; // bmodel origins are (0 0 0), we need to adjust that here VectorCopy(aGlassPane->s.origin, old_origin); VectorCopy (aPoint, aGlassPane->s.origin); VectorSubtract (aGlassPane->s.origin, anInflictor->s.origin, aGlassPane->velocity); VectorNormalize (aGlassPane->velocity); // use speed 250 instead of 150 for funkier glass spray VectorScale (aGlassPane->velocity, 250.0, aGlassPane->velocity); // start chunks towards the center VectorScale (aGlassPane->size, 0.25, size); count = 4; while(count--) { CGF_SFX_ApplyGlassFragmentLimit("debris"); chunkorigin[0] = aPoint[0] + crandom() * size[0]; chunkorigin[1] = aPoint[1] + crandom() * size[1]; chunkorigin[2] = aPoint[2] + crandom() * size[2]; CGF_SFX_GlassThrowDebris (aGlassPane, "models/objects/debris2/tris.md2", 2, chunkorigin); } // clear velocity, reset origin (that has been abused in ThrowDebris) VectorClear(aGlassPane->velocity); VectorCopy (old_origin, aGlassPane->s.origin); // firing thru - the pane has no direct attacker to hurt, // but G_UseTargets expects one. So make it a DIY G_UseTargets (aGlassPane, aGlassPane); } void CGF_SFX_HideBreakableGlass(edict_t* aGlassPane) { // remove all attached decals edict_t* decal; decal = 0; while (decal = G_Find(decal, FOFS(classname), "decal")) { if (decal->owner == aGlassPane) { // make it goaway in the next frame decal->nextthink = level.time + FRAMETIME; } } while (decal = G_Find(decal, FOFS(classname), "splat")) { if (decal->owner == aGlassPane) { // make it goaway in the next frame decal->nextthink = level.time + FRAMETIME; } } // after being broken, the pane cannot be freed as it is needed in // subsequent missions/games, so hide it at about z = -1000 lower aGlassPane->movetype = MOVETYPE_FLYMISSILE; VectorCopy(aGlassPane->s.origin, aGlassPane->pos1); aGlassPane->s.origin[2] -=1000.0; } void CGF_SFX_AttachDecalToGlass(edict_t* aGlassPane, edict_t* aDecal) { // just set aDecal's owner to be the glass pane aDecal->owner = aGlassPane; } void CGF_SFX_RebuildAllBrokenGlass() { // iterate over all func_explosives edict_t* glass; glass = 0; while (glass = G_Find(glass, FOFS(classname), "func_explosive")) { // glass is broken if solid != SOLID_BSP if (glass->solid != SOLID_BSP) { CGF_SFX_InstallBreakableGlass(glass); } } } void CGF_SFX_ApplyGlassFragmentLimit(const char* aClassName) { edict_t* oldfragment; glassfragmentcount++; if (glassfragmentcount > glassfragmentlimit->value) glassfragmentcount = 1; // remove fragment with corresponding number if any oldfragment = FindEdictByClassnum((char*) aClassName, glassfragmentcount); if (oldfragment) { // oldfragment->nextthink = level.time + FRAMETIME; G_FreeEdict(oldfragment); } } void CGF_SFX_MiscGlassUse(edict_t *self, edict_t *other, edict_t *activator) { #ifdef _DEBUG const char* classname; classname = other->classname; #endif } void CGF_SFX_MiscGlassDie(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { #ifdef _DEBUG const char* classname; classname = inflictor->classname; #endif } static vec_t previous_throw_time = 0; static int this_throw_count = 0; void CGF_SFX_GlassThrowDebris (edict_t *self, char *modelname, float speed, vec3_t origin) { // based on ThrowDebris from id software - now returns debris created edict_t *chunk; vec3_t v; if (level.time != previous_throw_time) { previous_throw_time = level.time; this_throw_count = 0; } else { this_throw_count++; if (this_throw_count > glassfragmentlimit->value) return; } chunk = G_Spawn(); VectorCopy (origin, chunk->s.origin); gi.setmodel (chunk, modelname); v[0] = 100 * crandom(); v[1] = 100 * crandom(); v[2] = 100 + 100 * crandom(); VectorMA (self->velocity, speed, v, chunk->velocity); chunk->movetype = MOVETYPE_BOUNCE; chunk->solid = SOLID_NOT; chunk->avelocity[0] = random()*600; chunk->avelocity[1] = random()*600; chunk->avelocity[2] = random()*600; chunk->think = G_FreeEdict; chunk->nextthink = level.time + 5 + random()*5; chunk->s.frame = 0; chunk->flags = 0; chunk->classname = "debris"; chunk->takedamage = DAMAGE_YES; chunk->die = debris_die; gi.linkentity (chunk); // number chunk chunk->classnum = glassfragmentcount; }