#include <lib/warpzone/common.qh>
.vector item_glowmod;
-.bool item_simple; // probably not really needed, but better safe than sorry
+.int item_simple;
.float alpha;
.bool pushable;
+.float anim_start_time; // reusing for bob waveform synchronisation
+.vector angles_held; // reusing for (re)storing original angles
+.float wait, delay, pointtime; // reusing for despawn effects
+.vector m_mins, m_maxs; // reusing for storing standard bbox (same purpose as in SVQC itemdef)
-void ItemDraw(entity this)
+HashMap ENT_CLIENT_ITEM_simple;
+STATIC_INIT(ENT_CLIENT_ITEM_simple)
+{
+ HM_NEW(ENT_CLIENT_ITEM_simple);
+}
+SHUTDOWN(ENT_CLIENT_ITEM_simple)
+{
+ HM_DELETE(ENT_CLIENT_ITEM_simple);
+}
+
+void ItemSetModel(entity this, bool wantsimple)
{
- if(this.gravity)
+ if(wantsimple)
{
- Movetype_Physics_MatchServer(this, false);
- if(IS_ONGROUND(this))
- { // For some reason avelocity gets set to '0 0 0' here ...
- this.oldorigin = this.origin;
- this.gravity = 0;
-
- if(autocvar_cl_animate_items)
- { // ... so reset it if animations are requested.
- if(this.ItemStatus & ITS_ANIMATE1)
- this.avelocity = '0 180 0';
+ string _fn2 = substring(this.mdl, 0 , strlen(this.mdl) -4);
+ #define extensions(x) \
+ x(iqm) \
+ x(dpm) \
+ x(md3) \
+ x(mdl) \
+ /**/
+ #define tryext(ext) { \
+ string s = strcat(_fn2, autocvar_cl_simpleitems_postfix, "." #ext); \
+ string cached = HM_gets(ENT_CLIENT_ITEM_simple, s); \
+ if (cached == "") { \
+ HM_sets(ENT_CLIENT_ITEM_simple, s, cached = fexists(s) ? "1" : "0"); \
+ } \
+ if (cached != "0") { \
+ this.model = s; \
+ this.item_simple = 1; \
+ break; \
+ } \
+ }
+ do {
+ extensions(tryext);
+ this.model = this.mdl; // fall back to 3d model
+ this.item_simple = -1; // don't retry every frame
+ LOG_TRACEF("Simple item requested for %s but no model exists for it", this.mdl);
+ } while (0);
+ #undef tryext
+ #undef extensions
+ }
+ else
+ {
+ this.model = this.mdl;
+ this.item_simple = 0;
+ }
- if(this.ItemStatus & ITS_ANIMATE2)
- this.avelocity = '0 -90 0';
- }
+ // this.model is an engine string so it doesn't need to be zoned and can't be unzoned
+ if(this.model == "")
+ LOG_WARNF("this.model is unset for item %s", this.classname);
+ precache_model(this.model);
+ _setmodel(this, this.model);
+ setsize(this, this.m_mins, this.m_maxs);
+}
- // delay is for blocking item's position for a while;
- // it's a workaround for dropped weapons that receive the position
- // another time right after they spawn overriding animation position
- this.onground_time = time + 0.5;
- }
+void ItemDraw(entity this)
+{
+ bool wantsimple = autocvar_cl_simple_items && (this.ItemStatus & ITS_ALLOWSI);
+ if(wantsimple != this.item_simple && this.item_simple != -1)
+ ItemSetModel(this, wantsimple);
+
+ // no bobbing applied to simple items, for consistency's sake (no visual difference between ammo and weapons)
+ bool animate = (autocvar_cl_items_animate & 1) && this.item_simple <= 0 && ((this.ItemStatus & ITS_ANIMATE1) || (this.ItemStatus & ITS_ANIMATE2));
+
+ // rotation must be set before running physics
+ if(!animate)
+ {
+ this.avelocity_y = 0;
+ this.angles = this.angles_held; // restore angles sent from server
}
- else if (autocvar_cl_animate_items && !this.item_simple) // no bobbing applied to simple items, for consistency's sake (no visual difference between ammo and weapons)
+ else if(!this.avelocity_y) // unset by MOVETYPE_TOSS or animation was disabled previously
{
if(this.ItemStatus & ITS_ANIMATE1)
- {
- this.angles += this.avelocity * frametime;
- float fade_in = bound(0, time - this.onground_time, 1);
- setorigin(this, this.oldorigin + fade_in * ('0 0 10' + '0 0 8' * sin((time - this.onground_time) * 2)));
- }
+ this.avelocity_y = 180;
+ else if(this.ItemStatus & ITS_ANIMATE2)
+ this.avelocity_y = -90;
+ }
- if(this.ItemStatus & ITS_ANIMATE2)
+ // CSQC physics OR bobbing (both would look weird)
+ float bobheight = 0; // reset bob offset if animations are disabled
+ if(this.move_movetype && (!IS_ONGROUND(this) || this.velocity != '0 0 0'))
+ {
+ // this isn't equivalent to player prediction but allows smooth motion with very low ISF_LOCATION rate
+ // which requires running this even if the item is just outside visible range (it could be moving into range)
+ if(animate)
+ bobheight = this.origin_z - this.oldorigin_z;
+ Movetype_Physics_NoMatchTicrate(this, frametime, true);
+ this.oldorigin = this.origin; // update real (SVQC equivalent) origin
+ if(animate)
{
- this.angles += this.avelocity * frametime;
- float fade_in = bound(0, time - this.onground_time, 1);
- setorigin(this, this.oldorigin + fade_in * ('0 0 8' + '0 0 4' * sin((time - this.onground_time) * 3)));
+ if(bobheight)
+ {
+ this.anim_start_time += frametime; // bobbing is paused this frame
+ this.oldorigin_z -= bobheight; // restore bob offset (CSQC physics uses the offset bbox)
+ }
+ else
+ {
+ this.anim_start_time = time; // starting our bob animation from NOW
+ if(this.ItemStatus & ITS_ANIMATE1)
+ bobheight = 10; // height of wave at 0 time
+ else if(this.ItemStatus & ITS_ANIMATE2)
+ bobheight = 8; // height of wave at 0 time
+ }
}
}
+ else if(animate)
+ {
+ this.angles += this.avelocity * frametime; // MOVETYPE_TOSS does this while it's moving
+
+ if(this.ItemStatus & ITS_ANIMATE1)
+ bobheight = 10 + 8 * sin((time - this.anim_start_time) * 2);
+ else if(this.ItemStatus & ITS_ANIMATE2)
+ bobheight = 8 + 4 * sin((time - this.anim_start_time) * 3);
+ }
+
+ // apply new bob offset
+ if (bobheight != this.origin_z - this.oldorigin_z)
+ {
+ this.origin_z = this.oldorigin_z + bobheight;
+ this.mins_z = this.m_mins.z - bobheight; // don't want the absmin and absmax to bob
+ this.maxs_z = this.m_maxs.z - bobheight;
+ }
// set alpha based on distance
this.alpha = 1;
this.colormod = this.glowmod = autocvar_cl_ghost_items_color;
}
+ if(!this.alpha)
+ return;
+
+ // loot item despawn effects
+ if(this.ItemStatus & ITS_EXPIRING)
+ {
+ if(!this.wait) // when receiving the first message with ITS_EXPIRING set
+ {
+ this.wait = time + IT_DESPAWNFX_TIME; // it will despawn then
+ this.delay = 0.25;
+ }
+
+ if(autocvar_cl_items_animate & 2)
+ this.alpha *= (this.wait - time) / IT_DESPAWNFX_TIME;
+
+ if((autocvar_cl_items_animate & 4) && time >= this.pointtime)
+ {
+ pointparticles(EFFECT_ITEM_DESPAWN, this.origin + '0 0 16', '0 0 0', 1);
+ if (this.delay > 0.0625)
+ this.delay *= 0.5;
+ this.pointtime = time + this.delay;
+ }
+ }
+
this.drawmask = this.alpha <= 0 ? 0 : MASK_NORMAL;
}
strfree(this.mdl);
}
-HashMap ENT_CLIENT_ITEM_simple;
-STATIC_INIT(ENT_CLIENT_ITEM_simple)
-{
- HM_NEW(ENT_CLIENT_ITEM_simple);
-}
-SHUTDOWN(ENT_CLIENT_ITEM_simple)
-{
- HM_DELETE(ENT_CLIENT_ITEM_simple);
-}
-
NET_HANDLE(ENT_CLIENT_ITEM, bool isnew)
{
int sf = ReadByte();
if(sf & ISF_LOCATION)
{
- vector org = ReadVector();
- setorigin(this, org);
- this.oldorigin = org;
+ float bobheight = this.origin_z - this.oldorigin_z;
+ this.origin = this.oldorigin = ReadVector();
+ this.origin_z += bobheight; // restore animation offset (SVQC physics is unaware of CSQC bbox offset)
+ setorigin(this, this.origin); // link
}
if(sf & ISF_ANGLES)
{
- this.angles = ReadAngleVector();
- }
-
- if(sf & ISF_SIZE)
- {
- setsize(this, '-16 -16 0', '16 16 48');
+ this.angles = this.angles_held = ReadAngleVector();
}
if(sf & ISF_STATUS) // need to read/write status first so model can handle simple, fb etc.
{
+ int prevItemStatus = this.ItemStatus;
this.ItemStatus = ReadByte();
if(this.ItemStatus & ITS_ALLOWFB)
else
this.effects &= ~EF_FULLBRIGHT;
- if(this.ItemStatus & ITS_GLOW)
+ if(this.ItemStatus & ITS_AVAILABLE)
{
- if(this.ItemStatus & ITS_AVAILABLE)
+ if(this.solid != SOLID_TRIGGER)
+ {
+ this.solid = SOLID_TRIGGER;
+ setorigin(this, this.origin); // link it to the area grid
+ }
+
+ if(this.ItemStatus & ITS_GLOW)
this.effects |= (EF_ADDITIVE | EF_FULLBRIGHT);
- else
+ if(!(prevItemStatus & ITS_AVAILABLE) && this.alpha && !isnew)
+ pointparticles(EFFECT_ITEM_RESPAWN, (this.absmin + this.absmax) * 0.5, '0 0 0', 1);
+ }
+ else
+ {
+ if(this.solid != SOLID_NOT)
+ {
+ this.solid = SOLID_NOT;
+ setorigin(this, this.origin); // optimisation: unlink it from the area grid
+ }
+
+ if(this.ItemStatus & ITS_GLOW)
this.effects &= ~(EF_ADDITIVE | EF_FULLBRIGHT);
+ if((prevItemStatus & ITS_AVAILABLE) && this.alpha)
+ pointparticles(EFFECT_ITEM_PICKUP, (this.absmin + this.absmax) * 0.5, '0 0 0', 1);
}
}
- if(sf & ISF_MODEL)
+ if(sf & ISF_SIZE || sf & ISF_SIZE2) // always true when it's spawned (in CSQC's perspective)
{
- set_movetype(this, MOVETYPE_TOSS);
- if (isnew) IL_PUSH(g_drawables, this);
- this.draw = ItemDraw;
- this.solid = SOLID_TRIGGER;
- //this.flags |= FL_ITEM;
-
- this.fade_end = ReadShort();
-
- strfree(this.mdl);
-
- string _fn = ReadString();
- this.item_simple = false; // reset it!
-
- if(autocvar_cl_simple_items && (this.ItemStatus & ITS_ALLOWSI))
+ if(isnew)
{
- string _fn2 = substring(_fn, 0 , strlen(_fn) -4);
- this.item_simple = true;
-
- #define extensions(x) \
- x(md3) \
- x(dpm) \
- x(iqm) \
- x(mdl) \
- /**/
- #define tryext(ext) { \
- string s = strcat(_fn2, autocvar_cl_simpleitems_postfix, "." #ext); \
- string cached = HM_gets(ENT_CLIENT_ITEM_simple, s); \
- if (cached == "") { \
- HM_sets(ENT_CLIENT_ITEM_simple, s, cached = fexists(s) ? "1" : "0"); \
- } \
- if (cached != "0") { \
- strcpy(this.mdl, s); \
- break; \
- } \
- }
- do {
- extensions(tryext);
- this.item_simple = false;
- LOG_TRACEF("Simple item requested for %s but no model exists for it", _fn);
- } while (0);
- #undef tryext
- #undef extensions
+ IL_PUSH(g_drawables, this);
+ this.draw = ItemDraw;
+ this.flags |= FL_ITEM;
+ this.entremove = ItemRemove;
}
- if(!this.item_simple)
- strcpy(this.mdl, _fn);
+ if(sf & ISF_SIZE && !(sf & ISF_SIZE2)) // Small
+ {
+ this.m_mins = ITEM_S_MINS;
+ this.m_maxs = ITEM_S_MAXS;
+ }
+ else if(!(sf & ISF_SIZE) && sf & ISF_SIZE2) // Large
+ {
+ this.m_mins = ITEM_D_MINS;
+ this.m_maxs = ITEM_L_MAXS;
+ }
+ else // Default
+ {
+ this.m_mins = ITEM_D_MINS;
+ this.m_maxs = ITEM_D_MAXS;
+ }
- if(this.mdl == "")
- LOG_WARNF("this.mdl is unset for item %s", this.classname);
+ this.fade_end = ReadShort();
- precache_model(this.mdl);
- _setmodel(this, this.mdl);
+ strcpy(this.mdl, ReadString());
+ this.item_simple = -2;
this.skin = ReadByte();
-
- setsize(this, '-16 -16 0', '16 16 48');
}
if(sf & ISF_COLORMAP)
{
this.gravity = 1;
this.pushable = true;
- //this.angles = '0 0 0';
set_movetype(this, MOVETYPE_TOSS);
this.velocity = ReadVector();
- setorigin(this, this.oldorigin);
-
- if(!this.move_time)
- {
- this.move_time = time;
- this.spawntime = time;
- }
- else
- this.move_time = max(this.move_time, time);
}
-
- if(autocvar_cl_animate_items)
+ else if (this.gravity) // caution: kludge FIXME (with sv_legacy_bbox_expand)
{
- if(this.ItemStatus & ITS_ANIMATE1)
- this.avelocity = '0 180 0';
-
- if(this.ItemStatus & ITS_ANIMATE2)
- this.avelocity = '0 -90 0';
+ // workaround for prediction errors caused by bbox discrepancy between SVQC and CSQC
+ this.gravity = 0; // don't do this kludge again
+ this.pushable = false; // no fun allowed
+ set_movetype(this, MOVETYPE_NONE); // disable physics
+ this.velocity = '0 0 0'; // disable it more
+ SET_ONGROUND(this); // extra overkill
}
- this.entremove = ItemRemove;
+ if(sf & ISF_REMOVEFX && !(sf & ISF_SIZE) && !(sf & ISF_SIZE2)) // TODO !isnew isn't reliable for this... are we double sending initialisations?
+ {
+ // no longer available to pick up, about to be removed
+ if (this.drawmask) // this.alpha > 0
+ pointparticles(EFFECT_ITEM_PICKUP, (this.absmin + this.absmax) * 0.5, '0 0 0', 1);
+ // removing now causes CSQC_Ent_Remove() to spam
+ this.drawmask = 0;
+ IL_REMOVE(g_drawables, this);
+ this.solid = SOLID_NOT;
+ }
return true;
}