/* =========================================================================== Copyright (C) 2023 the OpenMoHAA team This file is part of OpenMoHAA source code. OpenMoHAA source code 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. OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ // DESCRIPTION: // Temporary models effects #include "cg_local.h" #include "cg_commands.h" #include "tiki.h" cvar_t *cg_showtempmodels; cvar_t *cg_max_tempmodels; cvar_t *cg_reserve_tempmodels; cvar_t *cg_detail; cvar_t *cg_effectdetail; cvar_t *cg_effect_physicsrate; extern refEntity_t *current_entity; extern int current_entity_number; extern centity_t *current_centity; extern float current_scale; extern dtiki_t *current_tiki; extern Event EV_Client_Swipe; extern Event EV_Client_SwipeOn; extern Event EV_Client_SwipeOff; //============= // AllocateTempModel //============= ctempmodel_t *ClientGameCommandManager::AllocateTempModel(void) { ctempmodel_t *p; p = m_free_tempmodels; if (!p) { // no free entities return NULL; } m_free_tempmodels = m_free_tempmodels->next; // link into the active list p->next = m_active_tempmodels.next; p->prev = &m_active_tempmodels; m_active_tempmodels.next->prev = p; m_active_tempmodels.next = p; return p; } //=============== // FreeTempModel //=============== void ClientGameCommandManager::FreeTempModel(ctempmodel_t *p) { if (!p->prev) { cgi.Error(ERR_DROP, "CCM::FreeTempModel: not active"); } RemoveClientEntity(p->number, p->cgd.tiki, NULL, p); if (m_active_tempmodels.next == p) { // use the next active temp model m_active_tempmodels.next = p->next; } // remove from the doubly linked active list p->prev->next = p->next; p->next->prev = p->prev; // the free list is only singly linked p->next = m_free_tempmodels; m_free_tempmodels = p; if (p->m_spawnthing) { p->m_spawnthing->numtempmodels--; // delete unused spawnthings if (!p->m_spawnthing->numtempmodels) { m_emitters.RemoveObject(p->m_spawnthing); if (p->m_spawnthing == m_spawnthing) { m_spawnthing = NULL; } delete p->m_spawnthing; } p->m_spawnthing = NULL; } } //=============== // FreeAllTempModels //=============== void ClientGameCommandManager::FreeAllTempModels(void) { ctempmodel_t *p, *next; // Go through all the temp models and run the physics if necessary, // then add them to the ref p = m_active_tempmodels.prev; for (; p != &m_active_tempmodels; p = next) { // grab next now, so if the local entity is freed we still have it next = p->prev; FreeTempModel(p); } } //=============== // FreeSomeTempModels //=============== void ClientGameCommandManager::FreeSomeTempModels(void) { ctempmodel_t *model; int count = 0; unsigned int i; unsigned int numToFree; if (!m_free_tempmodels) { return; } for (model = m_active_tempmodels.prev; model != &m_active_tempmodels; model = model->prev) { count++; } if (cg_reserve_tempmodels->integer <= (cg_max_tempmodels->integer - count)) { // nothing to free return; } numToFree = cg_reserve_tempmodels->integer - (cg_max_tempmodels->integer - count); for (i = 0; i < numToFree; i++) { FreeTempModel(m_active_tempmodels.prev); } } //=============== // FreeSpawnthing //=============== void ClientGameCommandManager::FreeSpawnthing(spawnthing_t *sp) { ctempmodel_t *model; ctempmodel_t *prev; if (sp->numtempmodels) { for (model = m_active_tempmodels.prev; model != &m_active_tempmodels; model = prev) { prev = model->prev; if (model->m_spawnthing == sp) { FreeTempModel(model); } } } else { m_emitters.RemoveObject(sp); if (sp == m_spawnthing) { m_spawnthing = NULL; } delete sp; } } //=============== // ResetTempModels //=============== void ClientGameCommandManager::ResetTempModels(void) { // Go through all the active tempmodels and free them ctempmodel_t *p, *next; p = m_active_tempmodels.prev; for (; p != &m_active_tempmodels; p = next) { next = p->prev; FreeTempModel(p); } } static int lastTempModelFrameTime = 0; int next_tempmodel_warning = 0; void CG_ResetTempModels(void) { commandManager.ResetTempModels(); lastTempModelFrameTime = cg.time; } //============= // InitializeTempModels //============= void ClientGameCommandManager::InitializeTempModels(void) { int i; int numtempmodels = MAX_TEMPMODELS; m_active_tempmodels.next = &m_active_tempmodels; m_active_tempmodels.prev = &m_active_tempmodels; m_free_tempmodels = &m_tempmodels[0]; for (i = 0; i < numtempmodels - 1; i++) { m_tempmodels[i].next = &m_tempmodels[i + 1]; } m_tempmodels[numtempmodels - 1].next = NULL; } void ClientGameCommandManager::InitializeTempModelCvars(void) { cg_showtempmodels = cgi.Cvar_Get("cg_showtempmodels", "0", 0); cg_detail = cgi.Cvar_Get("detail", "1", CVAR_ARCHIVE); cg_effectdetail = cgi.Cvar_Get("cg_effectdetail", "0.2", CVAR_ARCHIVE); cgi.Cvar_CheckRange(cg_effectdetail, 0.2, 1.0, qfalse); cg_effect_physicsrate = cgi.Cvar_Get("cg_effect_physicsrate", "10", CVAR_ARCHIVE); cg_max_tempmodels = cgi.Cvar_Get("cg_max_tempmodels", "1100", CVAR_ARCHIVE); cgi.Cvar_CheckRange(cg_max_tempmodels, 200, MAX_TEMPMODELS, qtrue); cg_reserve_tempmodels = cgi.Cvar_Get("cg_reserve_tempmodels", "200", CVAR_ARCHIVE); if (cg_max_tempmodels->integer > MAX_TEMPMODELS) { // 2.40 sets the integer value directly rather than calling Cvar_Set() //cg_max_tempmodels->integer = MAX_TEMPMODELS cgi.Cvar_Set("cg_max_tempmodels", va("%i", MAX_TEMPMODELS)); } if (cg_reserve_tempmodels->integer * 5 > cg_max_tempmodels->integer) { // 2.40 sets the integer value directly rather than calling Cvar_Set() //cg_reserve_tempmodels->integer = cg_max_tempmodels->integer / 5; cgi.Cvar_Set("cg_reserve_tempmodels", va("%i", cg_max_tempmodels->integer / 5)); } } //=============== // AnimateTempModel - animate temp models //=============== void ClientGameCommandManager::AnimateTempModel(ctempmodel_t *p, Vector origin, refEntity_t *newEnt) { int numframes; int deltatime; int frametime; float prev; // This code is for animating tempmodels that are spawned from the client // side if (!p->cgd.tiki) { return; } // Calc frame stuff frametime = 1000.0f * cgi.Anim_Frametime(p->cgd.tiki, p->ent.frameInfo[0].index); deltatime = cg.time - p->lastAnimTime; numframes = cgi.Anim_NumFrames(p->cgd.tiki, p->ent.frameInfo[0].index); if (!p->addedOnce) { // Process entry commands CG_ProcessEntityCommands(TIKI_FRAME_ENTRY, p->ent.frameInfo[0].index, -1, &p->ent, NULL); } if (numframes < 2) { return; } // Go through all the frames, and process any commands associated with the // tempmodel as well prev = deltatime; while (deltatime >= frametime) { deltatime -= frametime; p->lastAnimTime += frametime; p->ent.wasframe = (p->ent.wasframe + 1) % numframes; CG_ProcessEntityCommands(p->ent.wasframe, p->ent.frameInfo[0].index, -1, &p->ent, NULL); if (deltatime == prev) { break; } prev = deltatime; } } //=============== // UpdateSwarm //=============== void ClientGameCommandManager::UpdateSwarm(ctempmodel_t *p) { if (p->cgd.swarmfreq == 0) { return; } // If the frequency is hit, set a new velocity if (!(rand() % p->cgd.swarmfreq)) { p->cgd.velocity.x = crandom() * p->cgd.swarmmaxspeed; p->cgd.velocity.y = crandom() * p->cgd.swarmmaxspeed; p->cgd.velocity.z = crandom() * p->cgd.swarmmaxspeed; } // Try to move toward the origin by the specified delta if (p->cgd.origin.x < p->cgd.parentOrigin.x) { p->cgd.velocity.x += p->cgd.swarmdelta; } else { p->cgd.velocity.x -= p->cgd.swarmdelta; } if (p->cgd.origin.y < p->cgd.parentOrigin.y) { p->cgd.velocity.y += p->cgd.swarmdelta; } else { p->cgd.velocity.y -= p->cgd.swarmdelta; } if (p->cgd.origin.z < p->cgd.parentOrigin.z) { p->cgd.velocity.z += p->cgd.swarmdelta; } else { p->cgd.velocity.z -= p->cgd.swarmdelta; } } qboolean ClientGameCommandManager::TempModelRealtimeEffects(ctempmodel_t *p, float ftime, float scale) { float fade, fadein; float dtime; byte tempColor[4]; if (p->cgd.flags & (T_FADE | T_SCALEUPDOWN)) { fade = 1.0f - (float)(p->aliveTime - p->cgd.fadedelay) / (float)(p->cgd.life - p->cgd.fadedelay); // Clamp the fade if (fade > 1) { fade = 1; } if (fade < 0) { fade = 0; } } else { fade = 1.0f; } dtime = (cg.time - p->cgd.createTime); // Calculate fade in value if (p->cgd.flags & T_FADEIN) { fadein = dtime / (float)p->cgd.fadeintime; } else { fadein = 0; } // Convert dtime to seconds dtime *= 0.001f; // Do the scale animation if (ftime && p->cgd.scaleRate) { p->ent.scale += p->cgd.scale * (p->cgd.scaleRate * ftime); } else if (p->cgd.flags & T_DLIGHT) { p->cgd.lightIntensity += p->cgd.scaleRate * ftime * p->cgd.lightIntensity; if (p->cgd.lightIntensity < 0.0f) { return qfalse; } } if (p->cgd.flags & T_SCALEUPDOWN) { p->ent.scale = p->cgd.scale * sin((fade)*M_PI); if (p->ent.scale < p->cgd.scalemin) { p->ent.scale = p->cgd.scalemin; } if (p->ent.scale > p->cgd.scalemax) { p->ent.scale = p->cgd.scalemax; } } if (p->cgd.lightstyle >= 0) { int i; float color[4]; CG_LightStyleColor(p->cgd.lightstyle, dtime * 1000, color); for (i = 0; i < 4; i++) { tempColor[i] = (byte)(color[i] * 255.0f); } } else { if (p->cgd.flags2 & T2_COLOR_AVEL) { p->cgd.color[0] += p->cgd.avelocity.x * ftime; p->cgd.color[1] += p->cgd.avelocity.y * ftime; p->cgd.color[2] += p->cgd.avelocity.z * ftime; if (p->cgd.color[0] < 0.0f) { p->cgd.color[0] = 0.0f; } if (p->cgd.color[1] < 0.0f) { p->cgd.color[1] = 0.0f; } if (p->cgd.color[2] < 0.0f) { p->cgd.color[2] = 0.0f; } } tempColor[0] = (int)(p->cgd.color[0] * 255.0f); tempColor[1] = (int)(p->cgd.color[1] * 255.0f); tempColor[2] = (int)(p->cgd.color[2] * 255.0f); tempColor[3] = (int)(p->cgd.color[3] * 255.0f); } if (p->cgd.flags & T_TWINKLE) { // See if we should toggle the twinkle if (cg.time > p->twinkleTime) { // If off, turn it on if (p->cgd.flags & T_TWINKLE_OFF) { p->cgd.flags &= ~T_TWINKLE_OFF; p->twinkleTime = cg.time + p->cgd.min_twinkletimeon + random() * p->cgd.max_twinkletimeon; } else { p->cgd.flags |= T_TWINKLE_OFF; p->twinkleTime = cg.time + p->cgd.min_twinkletimeoff + random() * p->cgd.max_twinkletimeoff; } } if (p->cgd.flags & T_TWINKLE_OFF) { memset(tempColor, 0, sizeof(tempColor)); } } if (p->cgd.flags & T_COLLISION) { vec3_t vLighting; cgi.R_GetLightingForSmoke(vLighting, p->ent.origin); p->ent.shaderRGBA[0] = (int)((float)tempColor[0] * vLighting[0]); p->ent.shaderRGBA[1] = (int)((float)tempColor[1] * vLighting[1]); p->ent.shaderRGBA[2] = (int)((float)tempColor[2] * vLighting[2]); } else { p->ent.shaderRGBA[0] = tempColor[0]; p->ent.shaderRGBA[1] = tempColor[1]; p->ent.shaderRGBA[2] = tempColor[2]; } if (p->cgd.flags & T_FADEIN && (fadein < 1)) // Do the fadein effect { p->ent.shaderRGBA[3] = (int)((float)tempColor[3] * (fadein * p->cgd.alpha)); } else if (p->cgd.flags & T_FADE) // Do a fadeout effect { p->ent.shaderRGBA[3] = (int)((float)tempColor[3] * (fade * p->cgd.alpha)); } else { p->ent.shaderRGBA[3] = (int)((float)tempColor[3] * p->cgd.alpha); } if (p->cgd.flags & T_FLICKERALPHA) { float random = random(); if (p->cgd.flags & (T_FADE | T_FADEIN)) { p->ent.shaderRGBA[3] *= random; } else { p->ent.shaderRGBA[3] = p->cgd.color[3] * random; } } // Check for completely faded out model if (fade <= 0 && p->addedOnce) { return false; } // Check for completely scaled out model if ((p->ent.scale <= 0 && p->addedOnce) && !(p->cgd.flags & T_SCALEUPDOWN)) { return false; } // Do swarming flies effects if (p->cgd.flags & T_SWARM) { UpdateSwarm(p); } return true; } void ClientGameCommandManager::OtherTempModelEffects(ctempmodel_t *p, Vector origin, refEntity_t *newEnt) { vec3_t axis[3]; if (p->number != -1) { refEntity_t *old_entity; dtiki_t *old_tiki; int oldnum; float oldscale; // Set the axis AnglesToAxis(p->cgd.angles, axis); old_entity = current_entity; old_tiki = current_tiki; oldnum = current_entity_number; oldscale = current_scale; current_scale = newEnt->scale; current_entity = newEnt; current_tiki = p->cgd.tiki; current_entity_number = p->number; // Update any emitters that are active on this tempmodel UpdateEmitter(p->cgd.tiki, axis, p->number, p->cgd.parent, origin); // Add in trails for this tempmodel if (p->cgd.flags2 & T2_TRAIL) { Event *ev = new Event(EV_Client_Swipe); ev->AddVector(origin); commandManager.ProcessEvent(ev); } current_entity_number = oldnum; current_tiki = old_tiki; current_entity = old_entity; current_scale = oldscale; } if (p->cgd.flags2 & T2_ALIGNSTRETCH) { Vector vDelta; float fScale; vDelta = p->cgd.origin - p->cgd.oldorigin; fScale = vDelta.length() * p->cgd.scale2; VectorScale(newEnt->axis[0], fScale, newEnt->axis[0]); } } qboolean ClientGameCommandManager::TempModelPhysics(ctempmodel_t *p, float ftime, float scale) { int dtime; Vector parentOrigin(0, 0, 0); Vector parentAngles(0, 0, 0); Vector tempangles; trace_t trace; float dot; int i; VectorCopy(p->ent.origin, p->lastEnt.origin); AxisCopy(p->ent.axis, p->lastEnt.axis); dtime = (cg.time - p->cgd.createTime); // Save oldorigin p->cgd.oldorigin = p->cgd.origin; // Update based on swarm if (p->cgd.flags & T_SWARM) { p->cgd.origin += p->cgd.velocity * ftime * scale; } // Update the orign and the angles based on velocities first else if (p->cgd.flags2 & (T2_MOVE | T2_ACCEL)) { p->cgd.origin += p->cgd.velocity * ftime * scale; } // If linked to the parent or hardlinked, get the parent's origin if ((p->cgd.flags & (T_PARENTLINK | T_HARDLINK)) && (p->cgd.parent != ENTITYNUM_NONE)) { centity_t *pc; refEntity_t *e; pc = &cg_entities[p->cgd.parent]; if (!pc->currentValid) { return false; } e = cgi.R_GetRenderEntity(p->cgd.parent); if (!e) { return false; } parentOrigin = e->origin; vectoangles(e->axis[0], parentAngles); } else if (p->cgd.flags & T_SWARM) { p->cgd.parentOrigin = p->cgd.velocity + p->cgd.accel * ftime * scale; } if (p->cgd.flags2 & T2_WATERONLY) { if (!(cgi.CM_PointContents(p->cgd.origin, 0) & (CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA))) { return false; } } // Align the object along it's traveling vector if (p->cgd.flags & T_ALIGN) { p->cgd.angles = p->cgd.velocity.toAngles(); parentAngles = vec_zero; } if (p->cgd.flags & T_RANDOMROLL) { p->cgd.angles[ROLL] = random() * 360; } // Update the angles based on angular velocity if (p->cgd.flags2 & T2_AMOVE) { p->cgd.angles = p->cgd.angles + (ftime * p->cgd.avelocity); } // Mod the angles if needed p->cgd.angles[0] = AngleMod(p->cgd.angles[0]); p->cgd.angles[1] = AngleMod(p->cgd.angles[1]); p->cgd.angles[2] = AngleMod(p->cgd.angles[2]); // Convert to axis if ((p->cgd.flags & (T_ALIGN | T_RANDOMROLL | T_PARENTLINK | T_HARDLINK | T_ANGLES)) || (p->cgd.flags2 & T2_AMOVE)) { // Add in parent angles tempangles = p->cgd.angles + parentAngles; AnglesToAxis(tempangles, p->ent.axis); } // Only do real collision if necessary if (p->cgd.flags & T_COLLISION) { // trace a line from previous position to new position CG_Trace( &trace, p->cgd.oldorigin, vec3_origin, vec3_origin, p->cgd.origin, -1, p->cgd.collisionmask, qfalse, qfalse, "Collision" ); } else { // Fake it out so it never collides trace.fraction = 1.0; } // Check for collision if (trace.fraction == 1.0) { // Acceleration of velocity if (p->cgd.flags2 & T2_ACCEL) { p->cgd.velocity = p->cgd.velocity + ftime * p->cgd.accel; } if (p->cgd.flags2 & T2_FRICTION) { float fFriction = 1.0f - ftime * p->cgd.friction; if (fFriction > 0.0f) { p->cgd.velocity *= fFriction; } else { p->cgd.velocity = vec_zero; } } if (p->cgd.flags2 & T2_CLAMP_VEL) { p->cgd.velocity.x = Q_clamp_float(p->cgd.velocity.x, p->cgd.minVel.x, p->cgd.maxVel.x); p->cgd.velocity.y = Q_clamp_float(p->cgd.velocity.y, p->cgd.minVel.y, p->cgd.maxVel.y); p->cgd.velocity.z = Q_clamp_float(p->cgd.velocity.z, p->cgd.minVel.z, p->cgd.maxVel.z); } else if (p->cgd.flags2 & T2_CLAMP_VEL_AXIS) { Vector localVelocity; localVelocity.x = DotProduct(p->cgd.velocity, p->ent.axis[0]); localVelocity.y = DotProduct(p->cgd.velocity, p->ent.axis[1]); localVelocity.z = DotProduct(p->cgd.velocity, p->ent.axis[2]); localVelocity.x = Q_clamp_float(localVelocity.x, p->cgd.minVel.x, p->cgd.maxVel.x); localVelocity.y = Q_clamp_float(localVelocity.y, p->cgd.minVel.y, p->cgd.maxVel.y); localVelocity.z = Q_clamp_float(localVelocity.z, p->cgd.minVel.z, p->cgd.maxVel.z); p->cgd.velocity.x = DotProduct(localVelocity, p->ent.axis[0]); p->cgd.velocity.y = DotProduct(localVelocity, p->ent.axis[1]); p->cgd.velocity.z = DotProduct(localVelocity, p->ent.axis[2]); } if (p->cgd.flags2 & T2_WIND_AFFECT) { for (i = 0; i < 3; i++) { float fWind; switch (i) { case 0: fWind = vss_wind_x->value; break; case 1: fWind = vss_wind_y->value; break; case 2: fWind = vss_wind_z->value; break; } if (fWind < 0) { if (p->cgd.velocity[i] > fWind) { p->cgd.velocity[i] -= ftime * vss_wind_strength->value; if (p->cgd.velocity[i] > fWind) { p->cgd.velocity[i] = fWind; } } else if (p->cgd.velocity[i] < fWind) { p->cgd.velocity[i] += ftime * vss_movement_dampen->value; if (p->cgd.velocity[i] < fWind) { p->cgd.velocity[i] = fWind; } } } else { if (p->cgd.velocity[i] < fWind) { p->cgd.velocity[i] += ftime * vss_wind_strength->value; if (p->cgd.velocity[i] > fWind) { p->cgd.velocity[i] = fWind; } } else if (p->cgd.velocity[i] > fWind) { p->cgd.velocity[i] -= ftime * vss_wind_strength->value; if (p->cgd.velocity[i] < fWind) { p->cgd.velocity[i] = fWind; } } } } } } else { Vector normal; // Set the origin p->cgd.origin = trace.endpos; if ((p->cgd.flags2 & T2_BOUNCE_DECAL) && (p->cgd.bouncecount < p->cgd.maxbouncecount)) { // Put down a bounce decal qhandle_t shader = cgi.R_RegisterShader(p->cgd.shadername); CG_ImpactMarkSimple( shader, trace.endpos, trace.plane.normal, p->cgd.decal_orientation, p->cgd.decal_radius, p->cgd.color[0], p->cgd.color[1], p->cgd.color[2], p->cgd.alpha, p->cgd.flags & T_FADE, p->cgd.flags2 & T2_TEMPORARY_DECAL, qtrue, p->cgd.flags & T_FADEIN ); p->cgd.bouncecount++; } if (p->cgd.flags & T_DIETOUCH) { return false; } // calculate the bounce normal = trace.plane.normal; // reflect the velocity on the trace plane if (p->cgd.flags2 & T2_ACCEL) { p->cgd.velocity = p->cgd.velocity + ftime * trace.fraction * p->cgd.accel; } dot = p->cgd.velocity * normal; p->cgd.velocity = p->cgd.velocity + ((-2 * dot) * normal); p->cgd.velocity *= p->cgd.bouncefactor; p->cgd.avelocity *= -p->cgd.bouncefactor; // check for stop if (trace.plane.normal[2] > 0 && p->cgd.velocity[2] < 45) { p->cgd.velocity = Vector(0, 0, 0); p->cgd.avelocity = Vector(0, 0, 0); p->cgd.flags &= ~T_WAVE; } else { if (p->cgd.flags & T_BOUNCESOUNDONCE) { vec3_t org; VectorCopy(p->cgd.origin, org); PlaySound(p->cgd.bouncesound, org); p->cgd.flags &= ~(T_BOUNCESOUNDONCE | T_BOUNCESOUND); } else if ((p->cgd.flags & T_BOUNCESOUND) && (p->next_bouncesound_time < cg.time)) { vec3_t org; VectorCopy(p->cgd.origin, org); PlaySound(p->cgd.bouncesound, org); p->next_bouncesound_time = cg.time + p->cgd.bouncesound_delay; } } } // copy over origin VectorCopy(p->cgd.origin, p->ent.origin); // Add in parent's origin if linked if (p->cgd.flags & (T_PARENTLINK | T_HARDLINK)) { VectorAdd(p->ent.origin, parentOrigin, p->ent.origin); } if (!p->lastEntValid) { // Make the lastEnt valid, by setting it to p->ent and setting the // origin to the tempmodel's oldorigin p->lastEnt = p->ent; VectorCopy(p->cgd.oldorigin, p->lastEnt.origin); p->lastEntValid = true; } return true; } qboolean ClientGameCommandManager::LerpTempModel(refEntity_t *newEnt, ctempmodel_t *p, float frac) { int i, j; // If the tempmodel is parentlinked, then we need to get the origin of the // parent and add it to the tempmodel's origin if (p->cgd.flags & (T_PARENTLINK | T_HARDLINK)) { centity_t *pc; Vector parentOrigin; // Lerp the tempmodel's local origin for (i = 0; i < 3; i++) { newEnt->origin[i] = p->cgd.oldorigin[i] + frac * (p->cgd.origin[i] - p->cgd.oldorigin[i]); } // Find the parent entity pc = &cg_entities[p->cgd.parent]; if (pc->currentValid) { refEntity_t *e; e = cgi.R_GetRenderEntity(p->cgd.parent); if (!e) { return false; } parentOrigin = e->origin; } else { return false; } // Add the parent ent's origin to the local origin VectorAdd(newEnt->origin, parentOrigin, newEnt->origin); } else { if (p->cgd.flags2 & (T2_MOVE | T2_ACCEL) || (p->cgd.flags & T_SWARM)) { // Lerp the ent's origin for (i = 0; i < 3; i++) { newEnt->origin[i] = p->lastEnt.origin[i] + frac * (p->ent.origin[i] - p->lastEnt.origin[i]); } } } if (p->cgd.flags2 & T2_PARALLEL) { Vector v1 = p->cgd.origin - cg.refdef.vieworg; vectoangles(v1, p->cgd.angles); AnglesToAxis(p->cgd.angles, newEnt->axis); } else if ((p->cgd.flags & (T_ALIGN | T_RANDOMROLL | T_PARENTLINK | T_HARDLINK)) || (p->cgd.flags2 & T2_AMOVE)) { // Lerp axis for (i = 0; i < 3; i++) { for (j = 0; j < 3; j++) { newEnt->axis[i][j] = p->lastEnt.axis[i][j] + frac * (p->ent.axis[i][j] - p->lastEnt.axis[i][j]); } } } return true; } //=============== // CG_AddTempModels //=============== void CG_AddTempModels(void) { commandManager.AddTempModels(); } //=============== // AddTempModels - Update and add tempmodels to the ref //=============== #define TOO_MUCH_TIME_PASSED 500 void ClientGameCommandManager::AddTempModels(void) { ctempmodel_t *p, *next; int count = 0; // Tempmodel count int frameTime; float effectTime, effectTime2; int mstime = 0; float ftime = 0; float time2 = 0; float scale = 1.0f; float lerpfrac = 0; int physics_rate = 0; qboolean ret; refEntity_t newEnt; dtiki_t *old_tiki; int old_num; refEntity_t *old_ent; // To counteract cg.time going backwards if (lastTempModelFrameTime && ((cg.time < lastTempModelFrameTime) || (cg.time - lastTempModelFrameTime > TOO_MUCH_TIME_PASSED))) { p = m_active_tempmodels.prev; for (; p != &m_active_tempmodels; p = next) { next = p->prev; p->lastPhysicsTime = cg.time; } lastTempModelFrameTime = cg.time; return; } if (lastTempModelFrameTime) { frameTime = cg.time - lastTempModelFrameTime; } else { frameTime = 0; } if (paused->integer) { lastTempModelFrameTime = 0; } else { lastTempModelFrameTime = cg.time; } memset(&newEnt, 0, sizeof(newEnt)); newEnt.parentEntity = ENTITYNUM_NONE; // Set this frame time for the next one effectTime = (float)frameTime / 1000.0f; effectTime2 = effectTime * effectTime; // If there is a current entity, it's scale is used as a factor if (current_entity) { scale = current_entity->scale; } // Go through all the temp models and run the physics if necessary, // then add them to the ref old_ent = current_entity; old_tiki = current_tiki; old_num = current_entity_number; p = m_active_tempmodels.prev; for (; p != &m_active_tempmodels; p = next) { // grab next now, so if the local entity is freed we still have it next = p->prev; if ((p->cgd.flags & T_DETAIL) && !cg_detail->integer) { FreeTempModel(p); continue; } p->ent.tiki = p->cgd.tiki; current_entity = &p->ent; current_tiki = p->cgd.tiki; current_entity_number = p->number; TempModelRealtimeEffects(p, effectTime, scale); if (p->lastPhysicsTime) { mstime = cg.time - p->lastPhysicsTime; // Check for physics physics_rate = 1000 / p->cgd.physicsRate; // Physics rate in milliseconds // Avoid large jumps in time if (mstime > physics_rate * 2) { mstime = physics_rate; } if ((mstime >= physics_rate) || (p->cgd.flags2 & T2_PHYSICS_EVERYFRAME)) { ftime = mstime / 1000.0f; ret = TempModelPhysics(p, ftime, scale); if (!ret) { FreeTempModel(p); continue; } p->lastPhysicsTime = cg.time; } } // Calculate the lerp value based on the time passed since last physics // time of this tempmodel lerpfrac = (float)(cg.time - p->lastPhysicsTime) / (float)physics_rate; // Clamp if (lerpfrac > 1 || (p->cgd.flags2 & T2_PHYSICS_EVERYFRAME)) { lerpfrac = 1; } if (lerpfrac < 0) { lerpfrac = 0; } // Increment the time this tempmodel has been alive p->aliveTime += frameTime; // Dead, and free up the tempmodel if (p->aliveTime >= p->cgd.life && p->addedOnce) { FreeTempModel(p); continue; } // Run physics if the lastEnt is not valid to get a valid lerp if (!p->lastEntValid) { float t; t = physics_rate / 1000.0f; ret = TempModelPhysics(p, t, scale); if (!ret) { FreeTempModel(p); continue; } lerpfrac = 0; p->lastPhysicsTime = cg.time; } // clear out the new entity and initialize it // this will become the current_entity if anything is spawned off it newEnt.scale = p->ent.scale; memcpy(newEnt.shaderRGBA, p->ent.shaderRGBA, 4); AxisCopy(p->ent.axis, newEnt.axis); VectorCopy(p->ent.origin, newEnt.origin); // Lerp the tempmodel if (!LerpTempModel(&newEnt, p, lerpfrac)) { FreeTempModel(p); continue; } if (p->cgd.flags & T_WAVE) { vec3_t origin; float axis[3][3]; VectorMA(p->m_spawnthing->linked_origin, newEnt.origin[0], p->m_spawnthing->linked_axis[0], origin); VectorMA(origin, newEnt.origin[1], p->m_spawnthing->linked_axis[1], origin); VectorMA(origin, newEnt.origin[2], p->m_spawnthing->linked_axis[2], origin); VectorCopy(origin, newEnt.origin); MatrixMultiply(newEnt.axis, p->m_spawnthing->linked_axis, axis); AxisCopy(axis, newEnt.axis); } // Animate and do trails (swipes) newEnt.renderfx = p->ent.renderfx; newEnt.hModel = p->ent.hModel; newEnt.reType = p->ent.reType; newEnt.shaderTime = p->ent.shaderTime; newEnt.frameInfo[0].index = p->ent.frameInfo[0].index; newEnt.frameInfo[0].weight = 1.0; newEnt.frameInfo[0].time = 0.0; newEnt.wasframe = p->ent.wasframe; newEnt.actionWeight = 1.0; newEnt.entityNumber = ENTITYNUM_NONE; newEnt.tiki = p->ent.tiki; AnimateTempModel(p, newEnt.origin, &newEnt); OtherTempModelEffects(p, newEnt.origin, &newEnt); VectorCopy(newEnt.origin, newEnt.lightingOrigin); newEnt.radius = 4.0; // Add to the ref if (p->cgd.flags & T_DLIGHT) { // Tempmodel is a Dynamic Light cgi.R_AddLightToScene( p->cgd.origin, p->cgd.lightIntensity * scale, p->cgd.color[0], p->cgd.color[1], p->cgd.color[2], p->cgd.lightType ); } else if (p->ent.reType == RT_SPRITE) { vec3_t vTestAngles; cgi.R_AddRefSpriteToScene(&newEnt); // Sprite MatrixToEulerAngles(newEnt.axis, vTestAngles); } else { cgi.R_AddRefEntityToScene(&newEnt, ENTITYNUM_NONE); // Model } // Set the added once flag so we can delete it later p->addedOnce = qtrue; // Local tempmodel count stat count++; } current_entity = old_ent; current_tiki = old_tiki; current_entity_number = old_num; // stats if (cg_showtempmodels->integer) { cgi.DPrintf("TC:%i\n", count); } } //================= // SpawnTempModel //================= void ClientGameCommandManager::SpawnTempModel(int count, spawnthing_t *sp) { m_spawnthing = sp; SpawnTempModel(count); } //================= // SpawnTempModel //================= void ClientGameCommandManager::SpawnTempModel(int mcount) { int i; ctempmodel_t *p; refEntity_t ent; int count; float current_entity_scale = 1.0f; Vector newForward; Vector delta; Vector start; Vector vForward, vLeft, vUp; float fDist; delta = m_spawnthing->cgd.origin - cg.refdef.vieworg; fDist = delta * delta * (cg.refdef.fov_x * cg.refdef.fov_x / 6400.0); if (fDist >= m_spawnthing->fMaxRangeSquared || fDist < m_spawnthing->fMinRangeSquared) { // don't draw above the distance return; } if (current_entity) { current_entity_scale = current_entity->scale; } else { current_entity_scale = 1.0f; } if (current_scale > 0) { current_entity_scale *= current_scale; } if (mcount > 1) { mcount *= cg_effectdetail->value; } if (mcount < 1) { mcount = 1; } if (m_spawnthing->cgd.flags2 & T2_SPIN) { float cosa, sina; float fAngle; fAngle = (cg.time - cgs.levelStartTime) * m_spawnthing->cgd.spin_rotation / 160.0f; cosa = cos(fAngle); sina = sin(fAngle); vForward = cosa * Vector(m_spawnthing->axis[0]); VectorMA(vForward, -sina, m_spawnthing->axis[1], vForward); vLeft = sina * Vector(m_spawnthing->axis[0]); VectorMA(vLeft, cosa, m_spawnthing->axis[1], vLeft); } else { vForward = m_spawnthing->axis[0]; vLeft = m_spawnthing->axis[1]; } vUp = m_spawnthing->axis[2]; for (count = 0; count < mcount; count++) { p = AllocateTempModel(); if (!p) { if (cgi.Milliseconds() >= next_tempmodel_warning) { // make sure to not spam the console with this message cgi.DPrintf("Out of tempmodels\n"); next_tempmodel_warning = cgi.Milliseconds() + 1000; } return; } memset(&ent, 0, sizeof(refEntity_t)); memset(&p->lastEnt, 0, sizeof(refEntity_t)); if (m_spawnthing->cgd.flags & T_WAVE) { p->m_spawnthing = m_spawnthing; m_spawnthing->numtempmodels++; start = Vector(0, 0, 0); } else { p->m_spawnthing = NULL; start = m_spawnthing->cgd.origin; } // Copy over the common data block p->cgd = m_spawnthing->cgd; // newForward may be changed by spehere or circle newForward = vForward; // Set up the origin of the tempmodel if (m_spawnthing->cgd.flags & T_SPHERE) { do { // Create a random forward vector so the particles burst out in a // sphere newForward = Vector(crandom(), crandom(), crandom()); } while (newForward * newForward > 1.0); } else if (m_spawnthing->cgd.flags & T_CIRCLE) { if (m_spawnthing->sphereRadius != 0) // Offset by the radius { Vector dst; // Create a circular shaped burst around the up vector float angle; if (mcount == 1) { angle = random() * 360; } else { angle = ((float)count / (float)m_spawnthing->count) * 360; // * M_PI * 2; } Vector end = Vector(vLeft) * m_spawnthing->sphereRadius * current_entity_scale; RotatePointAroundVector(dst, Vector(vForward), end, angle); VectorAdd(dst, m_spawnthing->cgd.origin, p->cgd.origin); newForward = p->cgd.origin - m_spawnthing->cgd.origin; newForward.normalize(); } } else if (m_spawnthing->cgd.flags & T_INWARDSPHERE) { // Project the origin along a random ray, and set the forward // vector pointing back to the origin Vector dir, end; do { dir = Vector(crandom(), crandom(), crandom()); } while (dir * dir > 1.0); end = m_spawnthing->cgd.origin + dir * m_spawnthing->sphereRadius * current_entity_scale; VectorCopy(end, p->cgd.origin); newForward = dir * -1; } else if (m_spawnthing->cgd.flags2 & T2_CONE) { float fHeight; float fRadius; float fAngle; float sina; float cosa; fHeight = random(); fRadius = random(); if (fHeight < fRadius) { float fTemp = fHeight; fHeight = fRadius; fRadius = fTemp; } fHeight *= m_spawnthing->coneHeight; fRadius = m_spawnthing->sphereRadius; fAngle = random() * M_PI * 2; sina = sin(fAngle); cosa = cos(fAngle); p->cgd.origin = start + vForward * fHeight; p->cgd.origin += vLeft * (cosa * fRadius); p->cgd.origin += vUp * (sina * fRadius); } else if (m_spawnthing->sphereRadius != 0) { // Offset in a spherical pattern Vector dir, end; dir = Vector(crandom(), crandom(), crandom()); dir.normalize(); end = m_spawnthing->cgd.origin + dir * m_spawnthing->sphereRadius * current_entity_scale; VectorCopy(end, p->cgd.origin); newForward = dir; } else { VectorCopy(start, p->cgd.origin); } if (m_spawnthing->cgd.flags & T_SWARM && !(m_spawnthing->cgd.flags & (T_HARDLINK | T_PARENTLINK))) { p->cgd.parentOrigin = p->cgd.origin; } // Randomize the origin based on offsets for (i = 0; i < 3; i++) { p->cgd.origin[i] += (random() * m_spawnthing->origin_offset_amplitude[i] + m_spawnthing->origin_offset_base[i]) * current_entity_scale; } p->cgd.oldorigin = p->cgd.origin; p->modelname = m_spawnthing->GetModel(); if (!(p->cgd.flags & T_DLIGHT) && !p->modelname.length()) { FreeTempModel(p); continue; } p->addedOnce = qfalse; p->lastEntValid = qfalse; if (p->modelname.length()) { ent.hModel = cgi.R_RegisterModel(p->modelname.c_str()); } // Initialize the refEntity ent.shaderTime = cg.time / 1000.0f; // Get the tikihandle p->cgd.tiki = cgi.R_Model_GetHandle(ent.hModel); // Set the reftype based on the modelname if (p->modelname == "*beam") { ent.reType = RT_BEAM; ent.customShader = cgi.R_RegisterShader("beamshader"); } else if (strstr(p->modelname, ".spr")) { ent.reType = RT_SPRITE; } else { ent.reType = RT_MODEL; } // Set the animation if (m_spawnthing->animName.length() && p->cgd.tiki) { ent.frameInfo[0].index = cgi.Anim_NumForName(p->cgd.tiki, m_spawnthing->animName); if (ent.frameInfo[0].index < 0) { ent.frameInfo[0].index = 0; } ent.frameInfo[0].weight = 1.0f; ent.frameInfo[0].time = 0.0f; ent.actionWeight = 1.0f; } else if (ent.reType == RT_MODEL && p->cgd.tiki) { ent.frameInfo[0].index = cgi.Anim_NumForName(p->cgd.tiki, "idle"); if (ent.frameInfo[0].index < 0) { ent.frameInfo[0].index = 0; } ent.frameInfo[0].weight = 1.0f; ent.frameInfo[0].time = 0.0f; ent.actionWeight = 1.0f; } // Randomize the scale if (m_spawnthing->cgd.flags & T_RANDSCALE) // Check for random scale { ent.scale = RandomizeRange(m_spawnthing->cgd.scalemin, m_spawnthing->cgd.scalemax); p->cgd.scale = ent.scale; } else { ent.scale = m_spawnthing->cgd.scale; } ent.scale *= current_entity_scale; // CURRENT ENTITY INFLUENCES ON THE TEMPMODELS HAPPEN HERE // copy over the renderfx from the current_entity, but only the flags we // want if (current_entity) { // explicitly add RF_LIGHTING ORIGIN and RF_SHADOWPLANE because we // don't want those on dynamic objects ent.renderfx |= (current_entity->renderfx & ~(RF_FLAGS_NOT_INHERITED | RF_LIGHTING_ORIGIN)); } // Set up modulation for constant color for (i = 0; i < 4; i++) { p->cgd.color[i] = m_spawnthing->cgd.color[i]; ent.shaderRGBA[i] = (byte)(p->cgd.color[i] * 255.0); } if (p->cgd.flags2 & T2_VARYCOLOR) { for (i = 0; i < 3; i++) { p->cgd.color[i] *= 0.8f + random() * 0.2f; } } // Apply the alpha from the current_entity to the tempmodel if (current_entity) { if (current_entity->shaderRGBA[3] != 255) { // pre-multiply the alpha from the entity for (i = 0; i < 4; i++) { p->cgd.color[3] *= current_entity->shaderRGBA[3] / 255.0; ent.shaderRGBA[3] = current_entity->shaderRGBA[3]; } } if (m_spawnthing->cgd.color[3] < 1.0f) { p->cgd.color[3] *= m_spawnthing->cgd.color[3]; ent.shaderRGBA[3] = (int)(p->cgd.color[3] * 255.0f); } } p->ent = ent; p->lastEnt = ent; p->number = -1; // If createTime is specified, the use it. Otherwise use the createTime // from the spawnthing. p->aliveTime = 0; // If animateonce is set, set the life = to the length of the anim if ((m_spawnthing->cgd.flags & T_ANIMATEONCE) && (p->ent.frameInfo[0].index > 0)) { p->cgd.life = cgi.Anim_Time(p->cgd.tiki, p->ent.frameInfo[0].index) * 1000.0f; } else { p->cgd.life = m_spawnthing->cgd.life; if (m_spawnthing->life_random) { p->cgd.life += m_spawnthing->life_random * random(); } } p->lastAnimTime = p->cgd.createTime; p->lastPhysicsTime = p->cgd.createTime; p->killTime = cg.time + p->cgd.life; // The time the tempmodel will die p->seed = m_seed++; // For use with randomizations p->cgd.velocity = Vector(0, 0, 0); // Zero out the velocity p->next_bouncesound_time = 0; // Init the next bouncesound time if (p->cgd.flags & T_TWINKLE) { if (random() > 0.5f) { p->cgd.flags |= T_TWINKLE_OFF; } } if (p->cgd.flags2 & T2_TRAIL) { // Assign a global number to this entity from the // command_time_manager Tempmodels with trails need their own unique // number. p->number = m_command_time_manager.AssignNumber(); p->cgd.flags |= T_ASSIGNED_NUMBER; int oldnum = current_entity_number; centity_t *oldcent = current_centity; current_centity = NULL; current_entity_number = p->number; Event *swipeEv = new Event(EV_Client_SwipeOn); swipeEv->AddString(p->cgd.swipe_shader); swipeEv->AddString(p->cgd.swipe_tag_start); swipeEv->AddString(p->cgd.swipe_tag_end); swipeEv->AddFloat(p->cgd.swipe_life); commandManager.ProcessEvent(swipeEv); current_centity = oldcent; current_entity_number = oldnum; } // Check to see if this tiki has any emitters bound to it and update // it's number. This is used for updating emitters that are attached to // tempmodels. if (p->cgd.tiki) { for (i = 1; i <= m_emitters.NumObjects(); i++) { spawnthing_t *st = m_emitters.ObjectAt(i); if (st->cgd.tiki == p->cgd.tiki) { // Assign this tempmodel a number if he doesn't already have // one if (p->number == -1) { p->number = st->AssignNumber(); } st->GetEmitTime(p->number); } } } for (i = 0; i < 3; i++) { // Randomize avelocity or set absolute p->cgd.avelocity[i] = m_spawnthing->avelocity_amplitude[i] * random() + m_spawnthing->avelocity_base[i]; // Randomize angles or set absolute p->cgd.angles[i] = m_spawnthing->angles_amplitude[i] * random() + m_spawnthing->cgd.angles[i]; } // If forward velocity is set, just use that otherwise use random // variation of the velocity if (m_spawnthing->forwardVelocity != 0) { for (i = 0; i < 3; i++) { p->cgd.velocity[i] = newForward[i] * m_spawnthing->forwardVelocity * current_entity_scale; } } if (p->cgd.flags2 & T2_PARALLEL) { Vector v1 = p->cgd.origin - cg.refdef.vieworg; vectoangles(v1, p->cgd.angles); } AnglesToAxis(p->cgd.angles, m_spawnthing->axis); AxisCopy(m_spawnthing->axis, p->ent.axis); // Random offsets along axis for (i = 0; i < 3; i++) { if (p->cgd.flags2 & T2_PARALLEL) { p->cgd.origin += Vector(m_spawnthing->axis[i]) * ((m_spawnthing->axis_offset_amplitude[i] * random() + m_spawnthing->axis_offset_base[i]) * current_entity_scale); } else { p->cgd.origin += Vector(m_spawnthing->tag_axis[i]) * ((m_spawnthing->axis_offset_amplitude[i] * random() + m_spawnthing->axis_offset_base[i]) * current_entity_scale); } } if (m_spawnthing->cgd.flags2 & T2_RADIALVELOCITY) { float fLength; p->cgd.velocity = p->cgd.origin - start; fLength = p->cgd.velocity.length(); if (fLength) { float fVel = m_spawnthing->cgd.velocity[0] + (m_spawnthing->cgd.velocity[1] + m_spawnthing->cgd.velocity[2] * random()) / fLength; p->cgd.velocity *= fVel; } } for (i = 0; i < 3; i++) { float fVel = (m_spawnthing->randvel_base[i] + m_spawnthing->randvel_amplitude[i] * random()) * current_entity_scale; if (m_spawnthing->cgd.flags & T_RANDVELAXIS) { if (p->cgd.flags2 & T2_NOTAGAXIS) { p->cgd.velocity += fVel * Vector(m_spawnthing->axis[i]); } else { p->cgd.velocity += fVel * Vector(m_spawnthing->tag_axis[i]); } } else { p->cgd.velocity[i] += fVel; } } if (p->cgd.flags & (T_ALIGN | T_DETAIL)) { p->cgd.angles = p->cgd.velocity.toAngles(); } p->cgd.origin += p->cgd.velocity * (p->aliveTime / 1000.0) * current_entity_scale; if (p->cgd.flags & T_AUTOCALCLIFE) { Vector end, delta; float length, speed; vec3_t vForward; trace_t trace; AngleVectorsLeft(p->cgd.angles, vForward, NULL, NULL); end = p->cgd.origin + Vector(vForward) * MAP_SIZE; CG_Trace( &trace, p->cgd.origin, vec_zero, vec_zero, end, ENTITYNUM_NONE, CONTENTS_SOLID | CONTENTS_WATER, qfalse, qfalse, "AutoCalcLife" ); delta = trace.endpos - p->cgd.origin; length = delta.length(); speed = p->cgd.velocity.length(); p->cgd.life = (length / speed) * 1000.0f; } // global fading is based on the number of animations in the // current_entity's animation if (current_entity) { if (m_spawnthing->cgd.flags & (T_GLOBALFADEIN | T_GLOBALFADEOUT)) { int numframes = cgi.Anim_NumFrames(current_tiki, current_entity->frameInfo[0].index); p->cgd.alpha = (float)current_entity->wasframe / (float)numframes; if (m_spawnthing->cgd.flags & T_GLOBALFADEOUT) { p->cgd.alpha = 1.0f - p->cgd.alpha; } } } // Make sure to not spawn the world model as a tempmodel // Of course it would become extremely confusing... if (!(p->cgd.flags & T_DLIGHT) && p->ent.reType == RT_MODEL && !p->ent.hModel) { Com_Printf("^~^~^ not spawning tempmodel because it is using the world as a brush model\n"); FreeTempModel(p); break; } if (m_spawnthing->cgd.flags2 & T2_RELATIVEANGLES) { float mat[3][3]; MatrixMultiply(m_spawnthing->axis, p->ent.axis, mat); VectorCopy(mat[0], p->ent.axis[0]); VectorCopy(mat[1], p->ent.axis[1]); VectorCopy(mat[2], p->ent.axis[2]); MatrixToEulerAngles(p->ent.axis, p->cgd.angles); } } }