--- /dev/null
- it.ons_roundlost = true;
+#include "sv_controlpoint.qh"
+#include "sv_generator.qh"
+
+bool g_onslaught;
+
+float autocvar_g_onslaught_teleport_wait;
+bool autocvar_g_onslaught_spawn_at_controlpoints;
+bool autocvar_g_onslaught_spawn_at_generator;
+float autocvar_g_onslaught_cp_proxydecap;
+float autocvar_g_onslaught_cp_proxydecap_distance = 512;
+float autocvar_g_onslaught_cp_proxydecap_dps = 100;
+float autocvar_g_onslaught_spawn_at_controlpoints_chance = 0.5;
+float autocvar_g_onslaught_spawn_at_controlpoints_random;
+float autocvar_g_onslaught_spawn_at_generator_chance;
+float autocvar_g_onslaught_spawn_at_generator_random;
+float autocvar_g_onslaught_cp_buildhealth;
+float autocvar_g_onslaught_cp_buildtime;
+float autocvar_g_onslaught_cp_health;
+float autocvar_g_onslaught_cp_regen;
+float autocvar_g_onslaught_gen_health;
+float autocvar_g_onslaught_shield_force = 100;
+float autocvar_g_onslaught_allow_vehicle_touch;
+float autocvar_g_onslaught_round_timelimit;
+float autocvar_g_onslaught_warmup;
+float autocvar_g_onslaught_teleport_radius;
+float autocvar_g_onslaught_spawn_choose;
+float autocvar_g_onslaught_click_radius;
+
+void FixSize(entity e);
+
+// =======================
+// CaptureShield Functions
+// =======================
+
+bool ons_CaptureShield_Customize(entity this, entity client)
+{
+ entity e = WaypointSprite_getviewentity(client);
+
+ if(!this.enemy.isshielded && (ons_ControlPoint_Attackable(this.enemy, e.team) > 0 || this.enemy.classname != "onslaught_controlpoint")) { return false; }
+ if(SAME_TEAM(this, e)) { return false; }
+
+ return true;
+}
+
+void ons_CaptureShield_Touch(entity this, entity toucher)
+{
+ if(!this.enemy.isshielded && (ons_ControlPoint_Attackable(this.enemy, toucher.team) > 0 || this.enemy.classname != "onslaught_controlpoint")) { return; }
+ if(!IS_PLAYER(toucher)) { return; }
+ if(SAME_TEAM(toucher, this)) { return; }
+
+ vector mymid = (this.absmin + this.absmax) * 0.5;
+ vector theirmid = (toucher.absmin + toucher.absmax) * 0.5;
+
+ Damage(toucher, this, this, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(theirmid - mymid) * ons_captureshield_force);
+
+ if(IS_REAL_CLIENT(toucher))
+ {
+ play2(toucher, SND(ONS_DAMAGEBLOCKEDBYSHIELD));
+
+ if(this.enemy.classname == "onslaught_generator")
+ Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_GENERATOR_SHIELDED);
+ else
+ Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_CONTROLPOINT_SHIELDED);
+ }
+}
+
+void ons_CaptureShield_Reset(entity this)
+{
+ this.colormap = this.enemy.colormap;
+ this.team = this.enemy.team;
+}
+
+void ons_CaptureShield_Spawn(entity generator, bool is_generator)
+{
+ entity shield = new(ons_captureshield);
+
+ shield.enemy = generator;
+ shield.team = generator.team;
+ shield.colormap = generator.colormap;
+ shield.reset = ons_CaptureShield_Reset;
+ settouch(shield, ons_CaptureShield_Touch);
+ setcefc(shield, ons_CaptureShield_Customize);
+ shield.effects = EF_ADDITIVE;
+ set_movetype(shield, MOVETYPE_NOCLIP);
+ shield.solid = SOLID_TRIGGER;
+ shield.avelocity = '7 0 11';
+ shield.scale = 1;
+ shield.model = ((is_generator) ? "models/onslaught/generator_shield.md3" : "models/onslaught/controlpoint_shield.md3");
+
+ precache_model(shield.model);
+ setorigin(shield, generator.origin);
+ _setmodel(shield, shield.model);
+ setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
+}
+
+
+// ==========
+// Junk Pile
+// ==========
+
+void setmodel_fixsize(entity e, Model m)
+{
+ setmodel(e, m);
+ FixSize(e);
+}
+
+void onslaught_updatelinks()
+{
+ entity l;
+ // first check if the game has ended
+ LOG_DEBUG("--- updatelinks ---");
+ // mark generators as being shielded and networked
+ for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
+ {
+ if (l.iscaptured)
+ LOG_DEBUG(etos(l), " (generator) belongs to team ", ftos(l.team));
+ else
+ LOG_DEBUG(etos(l), " (generator) is destroyed");
+ l.islinked = l.iscaptured;
+ l.isshielded = l.iscaptured;
+ l.sprite.SendFlags |= 16;
+ }
+ // mark points as shielded and not networked
+ for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
+ {
+ l.islinked = false;
+ l.isshielded = true;
+ int i;
+ for(i = 0; i < 17; ++i) { l.isgenneighbor[i] = false; l.iscpneighbor[i] = false; }
+ LOG_DEBUG(etos(l), " (point) belongs to team ", ftos(l.team));
+ l.sprite.SendFlags |= 16;
+ }
+ // flow power outward from the generators through the network
+ bool stop = false;
+ while (!stop)
+ {
+ stop = true;
+ for(l = ons_worldlinklist; l; l = l.ons_worldlinknext)
+ {
+ // if both points are captured by the same team, and only one of
+ // them is powered, mark the other one as powered as well
+ if (l.enemy.iscaptured && l.goalentity.iscaptured)
+ if (l.enemy.islinked != l.goalentity.islinked)
+ if(SAME_TEAM(l.enemy, l.goalentity))
+ {
+ if (!l.goalentity.islinked)
+ {
+ stop = false;
+ l.goalentity.islinked = true;
+ LOG_DEBUG(etos(l), " (link) is marking ", etos(l.goalentity), " (point) because its team matches ", etos(l.enemy), " (point)");
+ }
+ else if (!l.enemy.islinked)
+ {
+ stop = false;
+ l.enemy.islinked = true;
+ LOG_DEBUG(etos(l), " (link) is marking ", etos(l.enemy), " (point) because its team matches ", etos(l.goalentity), " (point)");
+ }
+ }
+ }
+ }
+ // now that we know which points are powered we can mark their neighbors
+ // as unshielded if team differs
+ for(l = ons_worldlinklist; l; l = l.ons_worldlinknext)
+ {
+ if (l.goalentity.islinked)
+ {
+ if(DIFF_TEAM(l.goalentity, l.enemy))
+ {
+ LOG_DEBUG(etos(l), " (link) is unshielding ", etos(l.enemy), " (point) because its team does not match ", etos(l.goalentity), " (point)");
+ l.enemy.isshielded = false;
+ }
+ if(l.goalentity.classname == "onslaught_generator")
+ l.enemy.isgenneighbor[l.goalentity.team] = true;
+ else
+ l.enemy.iscpneighbor[l.goalentity.team] = true;
+ }
+ if (l.enemy.islinked)
+ {
+ if(DIFF_TEAM(l.goalentity, l.enemy))
+ {
+ LOG_DEBUG(etos(l), " (link) is unshielding ", etos(l.goalentity), " (point) because its team does not match ", etos(l.enemy), " (point)");
+ l.goalentity.isshielded = false;
+ }
+ if(l.enemy.classname == "onslaught_generator")
+ l.goalentity.isgenneighbor[l.enemy.team] = true;
+ else
+ l.goalentity.iscpneighbor[l.enemy.team] = true;
+ }
+ }
+ // now update the generators
+ for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
+ {
+ if (l.isshielded)
+ {
+ LOG_DEBUG(etos(l), " (generator) is shielded");
+ l.takedamage = DAMAGE_NO;
+ l.bot_attack = false;
+ }
+ else
+ {
+ LOG_DEBUG(etos(l), " (generator) is not shielded");
+ l.takedamage = DAMAGE_AIM;
+ l.bot_attack = true;
+ }
+
+ ons_Generator_UpdateSprite(l);
+ }
+ // now update the takedamage and alpha variables on control point icons
+ for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
+ {
+ if (l.isshielded)
+ {
+ LOG_DEBUG(etos(l), " (point) is shielded");
+ if (l.goalentity)
+ {
+ l.goalentity.takedamage = DAMAGE_NO;
+ l.goalentity.bot_attack = false;
+ }
+ }
+ else
+ {
+ LOG_DEBUG(etos(l), " (point) is not shielded");
+ if (l.goalentity)
+ {
+ l.goalentity.takedamage = DAMAGE_AIM;
+ l.goalentity.bot_attack = true;
+ }
+ }
+ ons_ControlPoint_UpdateSprite(l);
+ }
+ FOREACH_ENTITY_CLASS("ons_captureshield", true,
+ {
+ it.team = it.enemy.team;
+ it.colormap = it.enemy.colormap;
+ });
+}
+
+
+// ===================
+// Main Link Functions
+// ===================
+
+bool ons_Link_Send(entity this, entity to, int sendflags)
+{
+ WriteHeader(MSG_ENTITY, ENT_CLIENT_RADARLINK);
+ WriteByte(MSG_ENTITY, sendflags);
+ if(sendflags & 1)
+ {
+ WriteCoord(MSG_ENTITY, this.goalentity.origin_x);
+ WriteCoord(MSG_ENTITY, this.goalentity.origin_y);
+ WriteCoord(MSG_ENTITY, this.goalentity.origin_z);
+ }
+ if(sendflags & 2)
+ {
+ WriteCoord(MSG_ENTITY, this.enemy.origin_x);
+ WriteCoord(MSG_ENTITY, this.enemy.origin_y);
+ WriteCoord(MSG_ENTITY, this.enemy.origin_z);
+ }
+ if(sendflags & 4)
+ {
+ WriteByte(MSG_ENTITY, this.clientcolors); // which is goalentity's color + enemy's color * 16
+ }
+ return true;
+}
+
+void ons_Link_CheckUpdate(entity this)
+{
+ // TODO check if the two sides have moved (currently they won't move anyway)
+ float cc = 0, cc1 = 0, cc2 = 0;
+
+ if(this.goalentity.islinked || this.goalentity.iscaptured) { cc1 = (this.goalentity.team - 1) * 0x01; }
+ if(this.enemy.islinked || this.enemy.iscaptured) { cc2 = (this.enemy.team - 1) * 0x10; }
+
+ cc = cc1 + cc2;
+
+ if(cc != this.clientcolors)
+ {
+ this.clientcolors = cc;
+ this.SendFlags |= 4;
+ }
+
+ this.nextthink = time;
+}
+
+void ons_DelayedLinkSetup(entity this)
+{
+ this.goalentity = find(NULL, targetname, this.target);
+ this.enemy = find(NULL, targetname, this.target2);
+ if(!this.goalentity) { objerror(this, "can not find target\n"); }
+ if(!this.enemy) { objerror(this, "can not find target2\n"); }
+
+ LOG_DEBUG(etos(this.goalentity), " linked with ", etos(this.enemy));
+ this.SendFlags |= 3;
+ setthink(this, ons_Link_CheckUpdate);
+ this.nextthink = time;
+}
+
+
+// =============================
+// Main Control Point Functions
+// =============================
+
+int ons_ControlPoint_CanBeLinked(entity cp, int teamnumber)
+{
+ if(cp.isgenneighbor[teamnumber]) { return 2; }
+ if(cp.iscpneighbor[teamnumber]) { return 1; }
+
+ return 0;
+}
+
+int ons_ControlPoint_Attackable(entity cp, int teamnumber)
+ // -2: SAME TEAM, attackable by enemy!
+ // -1: SAME TEAM!
+ // 0: off limits
+ // 1: attack it
+ // 2: touch it
+ // 3: attack it (HIGH PRIO)
+ // 4: touch it (HIGH PRIO)
+{
+ int a;
+
+ if(cp.isshielded)
+ {
+ return 0;
+ }
+ else if(cp.goalentity)
+ {
+ // if there's already an icon built, nothing happens
+ if(cp.team == teamnumber)
+ {
+ a = ons_ControlPoint_CanBeLinked(cp, teamnumber);
+ if(a) // attackable by enemy?
+ return -2; // EMERGENCY!
+ return -1;
+ }
+ // we know it can be linked, so no need to check
+ // but...
+ a = ons_ControlPoint_CanBeLinked(cp, teamnumber);
+ if(a == 2) // near our generator?
+ return 3; // EMERGENCY!
+ return 1;
+ }
+ else
+ {
+ // free point
+ if(ons_ControlPoint_CanBeLinked(cp, teamnumber))
+ {
+ a = ons_ControlPoint_CanBeLinked(cp, teamnumber); // why was this here NUM_TEAM_1 + NUM_TEAM_2 - t
+ if(a == 2)
+ return 4; // GET THIS ONE NOW!
+ else
+ return 2; // TOUCH ME
+ }
+ }
+ return 0;
+}
+
+void ons_ControlPoint_Icon_Damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
+{
+ if(damage <= 0) { return; }
+
+ if (this.owner.isshielded)
+ {
+ // this is protected by a shield, so ignore the damage
+ if (time > this.pain_finished)
+ if (IS_PLAYER(attacker))
+ {
+ play2(attacker, SND(ONS_DAMAGEBLOCKEDBYSHIELD));
+ this.pain_finished = time + 1;
+ attacker.typehitsound += 1; // play both sounds (shield is way too quiet)
+ }
+
+ return;
+ }
+
+ if(IS_PLAYER(attacker))
+ if(time - ons_notification_time[this.team] > 10)
+ {
+ play2team(this.team, SND(ONS_CONTROLPOINT_UNDERATTACK));
+ ons_notification_time[this.team] = time;
+ }
+
+ this.health = this.health - damage;
+ if(this.owner.iscaptured)
+ WaypointSprite_UpdateHealth(this.owner.sprite, this.health);
+ else
+ WaypointSprite_UpdateBuildFinished(this.owner.sprite, time + (this.max_health - this.health) / (this.count / ONS_CP_THINKRATE));
+ this.pain_finished = time + 1;
+ // particles on every hit
+ pointparticles(EFFECT_SPARKS, hitloc, force*-1, 1);
+ //sound on every hit
+ if (random() < 0.5)
+ sound(this, CH_TRIGGER, SND_ONS_HIT1, VOL_BASE+0.3, ATTEN_NORM);
+ else
+ sound(this, CH_TRIGGER, SND_ONS_HIT2, VOL_BASE+0.3, ATTEN_NORM);
+
+ if (this.health < 0)
+ {
+ sound(this, CH_TRIGGER, SND_GRENADE_IMPACT, VOL_BASE, ATTEN_NORM);
+ pointparticles(EFFECT_ROCKET_EXPLODE, this.origin, '0 0 0', 1);
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_ONSLAUGHT_CPDESTROYED), this.owner.message, attacker.netname);
+
+ PlayerScore_Add(attacker, SP_ONS_TAKES, 1);
+ PlayerScore_Add(attacker, SP_SCORE, 10);
+
+ this.owner.goalentity = NULL;
+ this.owner.islinked = false;
+ this.owner.iscaptured = false;
+ this.owner.team = 0;
+ this.owner.colormap = 1024;
+
+ WaypointSprite_UpdateMaxHealth(this.owner.sprite, 0);
+
+ onslaught_updatelinks();
+
+ // Use targets now (somebody make sure this is in the right place..)
+ SUB_UseTargets(this.owner, this, NULL);
+
+ this.owner.waslinked = this.owner.islinked;
+ if(this.owner.model != "models/onslaught/controlpoint_pad.md3")
+ setmodel_fixsize(this.owner, MDL_ONS_CP_PAD1);
+ //setsize(this, '-32 -32 0', '32 32 8');
+
+ delete(this);
+ }
+
+ this.SendFlags |= CPSF_STATUS;
+}
+
+void ons_ControlPoint_Icon_Think(entity this)
+{
+ this.nextthink = time + ONS_CP_THINKRATE;
+
+ if(autocvar_g_onslaught_cp_proxydecap)
+ {
+ int _enemy_count = 0;
+ int _friendly_count = 0;
+
+ FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
+ if(vdist(it.origin - this.origin, <, autocvar_g_onslaught_cp_proxydecap_distance))
+ {
+ if(SAME_TEAM(it, this))
+ ++_friendly_count;
+ else
+ ++_enemy_count;
+ }
+ });
+
+ _friendly_count = _friendly_count * (autocvar_g_onslaught_cp_proxydecap_dps * ONS_CP_THINKRATE);
+ _enemy_count = _enemy_count * (autocvar_g_onslaught_cp_proxydecap_dps * ONS_CP_THINKRATE);
+
+ this.health = bound(0, this.health + (_friendly_count - _enemy_count), this.max_health);
+ this.SendFlags |= CPSF_STATUS;
+ if(this.health <= 0)
+ {
+ ons_ControlPoint_Icon_Damage(this, this, this, 1, 0, this.origin, '0 0 0');
+ return;
+ }
+ }
+
+ if (time > this.pain_finished + 5)
+ {
+ if(this.health < this.max_health)
+ {
+ this.health = this.health + this.count;
+ if (this.health >= this.max_health)
+ this.health = this.max_health;
+ WaypointSprite_UpdateHealth(this.owner.sprite, this.health);
+ }
+ }
+
+ if(this.owner.islinked != this.owner.waslinked)
+ {
+ // unteam the spawnpoint if needed
+ int t = this.owner.team;
+ if(!this.owner.islinked)
+ this.owner.team = 0;
+
+ SUB_UseTargets(this.owner, this, NULL);
+
+ this.owner.team = t;
+
+ this.owner.waslinked = this.owner.islinked;
+ }
+
+ // damaged fx
+ if(random() < 0.6 - this.health / this.max_health)
+ {
+ Send_Effect(EFFECT_ELECTRIC_SPARKS, this.origin + randompos('-10 -10 -20', '10 10 20'), '0 0 0', 1);
+
+ if(random() > 0.8)
+ sound(this, CH_PAIN, SND_ONS_SPARK1, VOL_BASE, ATTEN_NORM);
+ else if (random() > 0.5)
+ sound(this, CH_PAIN, SND_ONS_SPARK2, VOL_BASE, ATTEN_NORM);
+ }
+}
+
+void ons_ControlPoint_Icon_BuildThink(entity this)
+{
+ int a;
+
+ this.nextthink = time + ONS_CP_THINKRATE;
+
+ // only do this if there is power
+ a = ons_ControlPoint_CanBeLinked(this.owner, this.owner.team);
+ if(!a)
+ return;
+
+ this.health = this.health + this.count;
+
+ this.SendFlags |= CPSF_STATUS;
+
+ if (this.health >= this.max_health)
+ {
+ this.health = this.max_health;
+ this.count = autocvar_g_onslaught_cp_regen * ONS_CP_THINKRATE; // slow repair rate from now on
+ setthink(this, ons_ControlPoint_Icon_Think);
+ sound(this, CH_TRIGGER, SND_ONS_CONTROLPOINT_BUILT, VOL_BASE, ATTEN_NORM);
+ this.owner.iscaptured = true;
+ this.solid = SOLID_BBOX;
+
+ Send_Effect(EFFECT_CAP(this.owner.team), this.owner.origin, '0 0 0', 1);
+
+ WaypointSprite_UpdateMaxHealth(this.owner.sprite, this.max_health);
+ WaypointSprite_UpdateHealth(this.owner.sprite, this.health);
+
+ if(IS_PLAYER(this.owner.ons_toucher))
+ {
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ONSLAUGHT_CAPTURE, this.owner.ons_toucher.netname, this.owner.message);
+ Send_Notification(NOTIF_ALL_EXCEPT, this.owner.ons_toucher, MSG_CENTER, APP_TEAM_NUM(this.owner.ons_toucher.team, CENTER_ONS_CAPTURE), this.owner.message);
+ Send_Notification(NOTIF_ONE, this.owner.ons_toucher, MSG_CENTER, CENTER_ONS_CAPTURE, this.owner.message);
+ PlayerScore_Add(this.owner.ons_toucher, SP_ONS_CAPS, 1);
+ PlayerTeamScore_AddScore(this.owner.ons_toucher, 10);
+ }
+
+ this.owner.ons_toucher = NULL;
+
+ onslaught_updatelinks();
+
+ // Use targets now (somebody make sure this is in the right place..)
+ SUB_UseTargets(this.owner, this, NULL);
+
+ this.SendFlags |= CPSF_SETUP;
+ }
+ if(this.owner.model != MDL_ONS_CP_PAD2.model_str())
+ setmodel_fixsize(this.owner, MDL_ONS_CP_PAD2);
+
+ if(random() < 0.9 - this.health / this.max_health)
+ Send_Effect(EFFECT_RAGE, this.origin + 10 * randomvec(), '0 0 -1', 1);
+}
+
+void onslaught_controlpoint_icon_link(entity e, void(entity this) spawnproc);
+
+void ons_ControlPoint_Icon_Spawn(entity cp, entity player)
+{
+ entity e = new(onslaught_controlpoint_icon);
+
+ setsize(e, CPICON_MIN, CPICON_MAX);
+ setorigin(e, cp.origin + CPICON_OFFSET);
+
+ e.owner = cp;
+ e.max_health = autocvar_g_onslaught_cp_health;
+ e.health = autocvar_g_onslaught_cp_buildhealth;
+ e.solid = SOLID_NOT;
+ e.takedamage = DAMAGE_AIM;
+ e.bot_attack = true;
+ e.event_damage = ons_ControlPoint_Icon_Damage;
+ e.team = player.team;
+ e.colormap = 1024 + (e.team - 1) * 17;
+ e.count = (e.max_health - e.health) * ONS_CP_THINKRATE / autocvar_g_onslaught_cp_buildtime; // how long it takes to build
+
+ sound(e, CH_TRIGGER, SND_ONS_CONTROLPOINT_BUILD, VOL_BASE, ATTEN_NORM);
+
+ cp.goalentity = e;
+ cp.team = e.team;
+ cp.colormap = e.colormap;
+
+ Send_Effect(EFFECT_FLAG_TOUCH(player.team), e.origin, '0 0 0', 1);
+
+ WaypointSprite_UpdateBuildFinished(cp.sprite, time + (e.max_health - e.health) / (e.count / ONS_CP_THINKRATE));
+ WaypointSprite_UpdateRule(cp.sprite,cp.team,SPRITERULE_TEAMPLAY);
+ cp.sprite.SendFlags |= 16;
+
+ onslaught_controlpoint_icon_link(e, ons_ControlPoint_Icon_BuildThink);
+}
+
+entity ons_ControlPoint_Waypoint(entity e)
+{
+ if(e.team)
+ {
+ int a = ons_ControlPoint_Attackable(e, e.team);
+
+ if(a == -2) { return WP_OnsCPDefend; } // defend now
+ if(a == -1 || a == 1 || a == 2) { return WP_OnsCP; } // touch
+ if(a == 3 || a == 4) { return WP_OnsCPAttack; } // attack
+ }
+ else
+ return WP_OnsCP;
+
+ return WP_Null;
+}
+
+void ons_ControlPoint_UpdateSprite(entity e)
+{
+ entity s1 = ons_ControlPoint_Waypoint(e);
+ WaypointSprite_UpdateSprites(e.sprite, s1, s1, s1);
+
+ bool sh;
+ sh = !(ons_ControlPoint_CanBeLinked(e, NUM_TEAM_1) || ons_ControlPoint_CanBeLinked(e, NUM_TEAM_2) || ons_ControlPoint_CanBeLinked(e, NUM_TEAM_3) || ons_ControlPoint_CanBeLinked(e, NUM_TEAM_4));
+
+ if(e.lastteam != e.team + 2 || e.lastshielded != sh || e.iscaptured != e.lastcaptured)
+ {
+ if(e.iscaptured) // don't mess up build bars!
+ {
+ if(sh)
+ {
+ WaypointSprite_UpdateMaxHealth(e.sprite, 0);
+ }
+ else
+ {
+ WaypointSprite_UpdateMaxHealth(e.sprite, e.goalentity.max_health);
+ WaypointSprite_UpdateHealth(e.sprite, e.goalentity.health);
+ }
+ }
+ if(e.lastshielded)
+ {
+ if(e.team)
+ WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, 0.5 * colormapPaletteColor(e.team - 1, false));
+ else
+ WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, '0.5 0.5 0.5');
+ }
+ else
+ {
+ if(e.team)
+ WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, colormapPaletteColor(e.team - 1, false));
+ else
+ WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, '0.75 0.75 0.75');
+ }
+ WaypointSprite_Ping(e.sprite);
+
+ e.lastteam = e.team + 2;
+ e.lastshielded = sh;
+ e.lastcaptured = e.iscaptured;
+ }
+}
+
+void ons_ControlPoint_Touch(entity this, entity toucher)
+{
+ int attackable;
+
+ if(IS_VEHICLE(toucher) && toucher.owner)
+ if(autocvar_g_onslaught_allow_vehicle_touch)
+ toucher = toucher.owner;
+ else
+ return;
+
+ if(!IS_PLAYER(toucher)) { return; }
+ if(STAT(FROZEN, toucher)) { return; }
+ if(IS_DEAD(toucher)) { return; }
+
+ if ( SAME_TEAM(this,toucher) )
+ if ( this.iscaptured )
+ {
+ if(time <= toucher.teleport_antispam)
+ Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT_ANTISPAM, rint(toucher.teleport_antispam - time));
+ else
+ Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT);
+ }
+
+ attackable = ons_ControlPoint_Attackable(this, toucher.team);
+ if(attackable != 2 && attackable != 4)
+ return;
+ // we've verified that this player has a legitimate claim to this point,
+ // so start building the captured point icon (which only captures this
+ // point if it successfully builds without being destroyed first)
+ ons_ControlPoint_Icon_Spawn(this, toucher);
+
+ this.ons_toucher = toucher;
+
+ onslaught_updatelinks();
+}
+
+void ons_ControlPoint_Think(entity this)
+{
+ this.nextthink = time + ONS_CP_THINKRATE;
+ CSQCMODEL_AUTOUPDATE(this);
+}
+
+void ons_ControlPoint_Reset(entity this)
+{
+ if(this.goalentity)
+ delete(this.goalentity);
+
+ this.goalentity = NULL;
+ this.team = 0;
+ this.colormap = 1024;
+ this.iscaptured = false;
+ this.islinked = false;
+ this.isshielded = true;
+ setthink(this, ons_ControlPoint_Think);
+ this.ons_toucher = NULL;
+ this.nextthink = time + ONS_CP_THINKRATE;
+ setmodel_fixsize(this, MDL_ONS_CP_PAD1);
+
+ WaypointSprite_UpdateMaxHealth(this.sprite, 0);
+ WaypointSprite_UpdateRule(this.sprite,this.team,SPRITERULE_TEAMPLAY);
+
+ onslaught_updatelinks();
+
+ SUB_UseTargets(this, this, NULL); // to reset the structures, playerspawns etc.
+
+ CSQCMODEL_AUTOUPDATE(this);
+}
+
+void ons_DelayedControlPoint_Setup(entity this)
+{
+ onslaught_updatelinks();
+
+ // captureshield setup
+ ons_CaptureShield_Spawn(this, false);
+
+ CSQCMODEL_AUTOINIT(this);
+}
+
+void ons_ControlPoint_Setup(entity cp)
+{
+ // main setup
+ cp.ons_worldcpnext = ons_worldcplist; // link control point into ons_worldcplist
+ ons_worldcplist = cp;
+
+ cp.netname = "Control point";
+ cp.team = 0;
+ cp.solid = SOLID_BBOX;
+ set_movetype(cp, MOVETYPE_NONE);
+ settouch(cp, ons_ControlPoint_Touch);
+ setthink(cp, ons_ControlPoint_Think);
+ cp.nextthink = time + ONS_CP_THINKRATE;
+ cp.reset = ons_ControlPoint_Reset;
+ cp.colormap = 1024;
+ cp.iscaptured = false;
+ cp.islinked = false;
+ cp.isshielded = true;
+
+ if(cp.message == "") { cp.message = "a"; }
+
+ // appearence
+ setmodel_fixsize(cp, MDL_ONS_CP_PAD1);
+
+ // control point placement
+ if((cp.spawnflags & 1) || cp.noalign) // don't drop to floor, just stay at fixed location
+ {
+ cp.noalign = true;
+ set_movetype(cp, MOVETYPE_NONE);
+ }
+ else // drop to floor, automatically find a platform and set that as spawn origin
+ {
+ setorigin(cp, cp.origin + '0 0 20');
+ cp.noalign = false;
+ droptofloor(cp);
+ set_movetype(cp, MOVETYPE_TOSS);
+ }
+
+ // waypointsprites
+ WaypointSprite_SpawnFixed(WP_Null, cp.origin + CPGEN_WAYPOINT_OFFSET, cp, sprite, RADARICON_NONE);
+ WaypointSprite_UpdateRule(cp.sprite, cp.team, SPRITERULE_TEAMPLAY);
+
+ InitializeEntity(cp, ons_DelayedControlPoint_Setup, INITPRIO_SETLOCATION);
+}
+
+
+// =========================
+// Main Generator Functions
+// =========================
+
+entity ons_Generator_Waypoint(entity e)
+{
+ if (e.isshielded)
+ return WP_OnsGenShielded;
+ return WP_OnsGen;
+}
+
+void ons_Generator_UpdateSprite(entity e)
+{
+ entity s1 = ons_Generator_Waypoint(e);
+ WaypointSprite_UpdateSprites(e.sprite, s1, s1, s1);
+
+ if(e.lastteam != e.team + 2 || e.lastshielded != e.isshielded)
+ {
+ e.lastteam = e.team + 2;
+ e.lastshielded = e.isshielded;
+ if(e.lastshielded)
+ {
+ if(e.team)
+ WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, 0.5 * colormapPaletteColor(e.team - 1, false));
+ else
+ WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, '0.5 0.5 0.5');
+ }
+ else
+ {
+ if(e.team)
+ WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, colormapPaletteColor(e.team - 1, false));
+ else
+ WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, '0.75 0.75 0.75');
+ }
+ WaypointSprite_Ping(e.sprite);
+ }
+}
+
+void ons_GeneratorDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
+{
+ if(damage <= 0) { return; }
+ if(warmup_stage || gameover) { return; }
+ if(!round_handler_IsRoundStarted()) { return; }
+
+ if (attacker != this)
+ {
+ if (this.isshielded)
+ {
+ // this is protected by a shield, so ignore the damage
+ if (time > this.pain_finished)
+ if (IS_PLAYER(attacker))
+ {
+ play2(attacker, SND(ONS_DAMAGEBLOCKEDBYSHIELD));
+ attacker.typehitsound += 1;
+ this.pain_finished = time + 1;
+ }
+ return;
+ }
+ if (time > this.pain_finished)
+ {
+ this.pain_finished = time + 10;
+ FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it) && SAME_TEAM(it, this), Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_GENERATOR_UNDERATTACK));
+ play2team(this.team, SND(ONS_GENERATOR_UNDERATTACK));
+ }
+ }
+ this.health = this.health - damage;
+ WaypointSprite_UpdateHealth(this.sprite, this.health);
+ // choose an animation frame based on health
+ this.frame = 10 * bound(0, (1 - this.health / this.max_health), 1);
+ // see if the generator is still functional, or dying
+ if (this.health > 0)
+ {
+ this.lasthealth = this.health;
+ }
+ else
+ {
+ if (attacker == this)
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_ONSLAUGHT_GENDESTROYED_OVERTIME));
+ else
+ {
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_ONSLAUGHT_GENDESTROYED));
+ PlayerScore_Add(attacker, SP_SCORE, 100);
+ }
+ this.iscaptured = false;
+ this.islinked = false;
+ this.isshielded = false;
+ this.takedamage = DAMAGE_NO; // can't be hurt anymore
+ this.event_damage = func_null; // won't do anything if hurt
+ this.count = 0; // reset counter
+ setthink(this, func_null);
+ this.nextthink = 0;
+ //this.think(); // do the first explosion now
+
+ WaypointSprite_UpdateMaxHealth(this.sprite, 0);
+ WaypointSprite_Ping(this.sprite);
+ //WaypointSprite_Kill(this.sprite); // can't do this yet, code too poor
+
+ onslaught_updatelinks();
+ }
+
+ // Throw some flaming gibs on damage, more damage = more chance for gib
+ if(random() < damage/220)
+ {
+ sound(this, CH_TRIGGER, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_NORM);
+ }
+ else
+ {
+ // particles on every hit
+ Send_Effect(EFFECT_SPARKS, hitloc, force * -1, 1);
+
+ //sound on every hit
+ if (random() < 0.5)
+ sound(this, CH_TRIGGER, SND_ONS_HIT1, VOL_BASE, ATTEN_NORM);
+ else
+ sound(this, CH_TRIGGER, SND_ONS_HIT2, VOL_BASE, ATTEN_NORM);
+ }
+
+ this.SendFlags |= GSF_STATUS;
+}
+
+void ons_GeneratorThink(entity this)
+{
+ this.nextthink = time + GEN_THINKRATE;
+ if (!gameover)
+ {
+ if(!this.isshielded && this.wait < time)
+ {
+ this.wait = time + 5;
+ FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it), {
+ if(SAME_TEAM(it, this))
+ {
+ Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_ONS_NOTSHIELDED_TEAM);
+ soundto(MSG_ONE, it, CHAN_AUTO, SND(KH_ALARM), VOL_BASE, ATTEN_NONE); // FIXME: unique sound?
+ }
+ else
+ Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_TEAM_NUM(this.team, CENTER_ONS_NOTSHIELDED));
+ });
+ }
+ }
+}
+
+void ons_GeneratorReset(entity this)
+{
+ this.team = this.team_saved;
+ this.lasthealth = this.max_health = this.health = autocvar_g_onslaught_gen_health;
+ this.takedamage = DAMAGE_AIM;
+ this.bot_attack = true;
+ this.iscaptured = true;
+ this.islinked = true;
+ this.isshielded = true;
+ this.event_damage = ons_GeneratorDamage;
+ setthink(this, ons_GeneratorThink);
+ this.nextthink = time + GEN_THINKRATE;
+
+ Net_LinkEntity(this, false, 0, generator_send);
+
+ this.SendFlags = GSF_SETUP; // just incase
+ this.SendFlags |= GSF_STATUS;
+
+ WaypointSprite_UpdateMaxHealth(this.sprite, this.max_health);
+ WaypointSprite_UpdateHealth(this.sprite, this.health);
+ WaypointSprite_UpdateRule(this.sprite,this.team,SPRITERULE_TEAMPLAY);
+
+ onslaught_updatelinks();
+}
+
+void ons_DelayedGeneratorSetup(entity this)
+{
+ // bot waypoints
+ waypoint_spawnforitem_force(this, this.origin);
+ this.nearestwaypointtimeout = 0; // activate waypointing again
+ this.bot_basewaypoint = this.nearestwaypoint;
+
+ // captureshield setup
+ ons_CaptureShield_Spawn(this, true);
+
+ onslaught_updatelinks();
+
+ Net_LinkEntity(this, false, 0, generator_send);
+}
+
+
+void onslaught_generator_touch(entity this, entity toucher)
+{
+ if ( IS_PLAYER(toucher) )
+ if ( SAME_TEAM(this,toucher) )
+ if ( this.iscaptured )
+ {
+ Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT);
+ }
+}
+
+void ons_GeneratorSetup(entity gen) // called when spawning a generator entity on the map as a spawnfunc
+{
+ // declarations
+ int teamnumber = gen.team;
+
+ // main setup
+ gen.ons_worldgeneratornext = ons_worldgeneratorlist; // link generator into ons_worldgeneratorlist
+ ons_worldgeneratorlist = gen;
+
+ gen.netname = sprintf("%s generator", Team_ColoredFullName(teamnumber));
+ gen.classname = "onslaught_generator";
+ gen.solid = SOLID_BBOX;
+ gen.team_saved = teamnumber;
+ set_movetype(gen, MOVETYPE_NONE);
+ gen.lasthealth = gen.max_health = gen.health = autocvar_g_onslaught_gen_health;
+ gen.takedamage = DAMAGE_AIM;
+ gen.bot_attack = true;
+ gen.event_damage = ons_GeneratorDamage;
+ gen.reset = ons_GeneratorReset;
+ setthink(gen, ons_GeneratorThink);
+ gen.nextthink = time + GEN_THINKRATE;
+ gen.iscaptured = true;
+ gen.islinked = true;
+ gen.isshielded = true;
+ settouch(gen, onslaught_generator_touch);
+
+ // appearence
+ // model handled by CSQC
+ setsize(gen, GENERATOR_MIN, GENERATOR_MAX);
+ setorigin(gen, (gen.origin + CPGEN_SPAWN_OFFSET));
+ gen.colormap = 1024 + (teamnumber - 1) * 17;
+
+ // generator placement
+ droptofloor(gen);
+
+ // waypointsprites
+ WaypointSprite_SpawnFixed(WP_Null, gen.origin + CPGEN_WAYPOINT_OFFSET, gen, sprite, RADARICON_NONE);
+ WaypointSprite_UpdateRule(gen.sprite, gen.team, SPRITERULE_TEAMPLAY);
+ WaypointSprite_UpdateMaxHealth(gen.sprite, gen.max_health);
+ WaypointSprite_UpdateHealth(gen.sprite, gen.health);
+
+ InitializeEntity(gen, ons_DelayedGeneratorSetup, INITPRIO_SETLOCATION);
+}
+
+
+// ===============
+// Round Handler
+// ===============
+
+int total_generators;
+void Onslaught_count_generators()
+{
+ entity e;
+ total_generators = redowned = blueowned = yellowowned = pinkowned = 0;
+ for(e = ons_worldgeneratorlist; e; e = e.ons_worldgeneratornext)
+ {
+ ++total_generators;
+ redowned += (e.team == NUM_TEAM_1 && e.health > 0);
+ blueowned += (e.team == NUM_TEAM_2 && e.health > 0);
+ yellowowned += (e.team == NUM_TEAM_3 && e.health > 0);
+ pinkowned += (e.team == NUM_TEAM_4 && e.health > 0);
+ }
+}
+
+int Onslaught_GetWinnerTeam()
+{
+ int winner_team = 0;
+ if(redowned > 0)
+ winner_team = NUM_TEAM_1;
+ if(blueowned > 0)
+ {
+ if(winner_team) return 0;
+ winner_team = NUM_TEAM_2;
+ }
+ if(yellowowned > 0)
+ {
+ if(winner_team) return 0;
+ winner_team = NUM_TEAM_3;
+ }
+ if(pinkowned > 0)
+ {
+ if(winner_team) return 0;
+ winner_team = NUM_TEAM_4;
+ }
+ if(winner_team)
+ return winner_team;
+ return -1; // no generators left?
+}
+
+void nades_Clear(entity e);
+
+#define ONS_OWNED_GENERATORS() ((redowned > 0) + (blueowned > 0) + (yellowowned > 0) + (pinkowned > 0))
+#define ONS_OWNED_GENERATORS_OK() (ONS_OWNED_GENERATORS() > 1)
+bool Onslaught_CheckWinner()
+{
+ if ((autocvar_timelimit && time > game_starttime + autocvar_timelimit * 60) || (round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0))
+ {
+ ons_stalemate = true;
+
+ if (!wpforenemy_announced)
+ {
+ Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_OVERTIME_CONTROLPOINT);
+ sound(NULL, CH_INFO, SND_ONS_GENERATOR_DECAY, VOL_BASE, ATTEN_NONE);
+
+ wpforenemy_announced = true;
+ }
+
+ entity tmp_entity; // temporary entity
+ float d;
+ for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext) if(time >= tmp_entity.ons_overtime_damagedelay)
+ {
+ // tmp_entity.max_health / 300 gives 5 minutes of overtime.
+ // control points reduce the overtime duration.
+ d = 1;
+ entity e;
+ for(e = ons_worldcplist; e; e = e.ons_worldcpnext)
+ {
+ if(DIFF_TEAM(e, tmp_entity))
+ if(e.islinked)
+ d = d + 1;
+ }
+
+ if(autocvar_g_campaign && autocvar__campaign_testrun)
+ d = d * tmp_entity.max_health;
+ else
+ d = d * tmp_entity.max_health / max(30, 60 * autocvar_timelimit_suddendeath);
+
+ Damage(tmp_entity, tmp_entity, tmp_entity, d, DEATH_HURTTRIGGER.m_id, tmp_entity.origin, '0 0 0');
+
+ tmp_entity.sprite.SendFlags |= 16;
+
+ tmp_entity.ons_overtime_damagedelay = time + 1;
+ }
+ }
+ else { wpforenemy_announced = false; ons_stalemate = false; }
+
+ Onslaught_count_generators();
+
+ if(ONS_OWNED_GENERATORS_OK())
+ return 0;
+
+ int winner_team = Onslaught_GetWinnerTeam();
+
+ if(winner_team > 0)
+ {
+ Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, APP_TEAM_NUM(winner_team, CENTER_ROUND_TEAM_WIN));
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(winner_team, INFO_ROUND_TEAM_WIN));
+ TeamScore_AddToTeam(winner_team, ST_ONS_CAPS, +1);
+ }
+ else if(winner_team == -1)
+ {
+ Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED);
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED);
+ }
+
+ ons_stalemate = false;
+
+ play2all(SND(CTF_CAPTURE(winner_team)));
+
+ round_handler_Init(7, autocvar_g_onslaught_warmup, autocvar_g_onslaught_round_timelimit);
+
+ FOREACH_CLIENT(IS_PLAYER(it), {
- it.ons_roundlost = false;
++ STAT(ROUNDLOST, it) = true;
+ it.player_blocked = true;
+
+ nades_Clear(it);
+ });
+
+ return 1;
+}
+
+bool Onslaught_CheckPlayers()
+{
+ return 1;
+}
+
+void Onslaught_RoundStart()
+{
+ entity tmp_entity;
+ FOREACH_CLIENT(IS_PLAYER(it), it.player_blocked = false);
+
+ for(tmp_entity = ons_worldcplist; tmp_entity; tmp_entity = tmp_entity.ons_worldcpnext)
+ tmp_entity.sprite.SendFlags |= 16;
+
+ for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
+ tmp_entity.sprite.SendFlags |= 16;
+}
+
+
+// ================
+// Bot player logic
+// ================
+
+// NOTE: LEGACY CODE, needs to be re-written!
+
+void havocbot_goalrating_ons_offenseitems(entity this, float ratingscale, vector org, float sradius)
+{
+ bool needarmor = false, needweapons = false;
+
+ // Needs armor/health?
+ if(this.health<100)
+ needarmor = true;
+
+ // Needs weapons?
+ int c = 0;
+ FOREACH(Weapons, it != WEP_Null, {
+ if(this.weapons & (it.m_wepset))
+ if(++c >= 4)
+ break;
+ });
+
+ if(c<4)
+ needweapons = true;
+
+ if(!needweapons && !needarmor)
+ return;
+
+ LOG_DEBUG(this.netname, " needs weapons ", ftos(needweapons));
+ LOG_DEBUG(this.netname, " needs armor ", ftos(needarmor));
+
+ // See what is around
+ FOREACH_ENTITY_FLOAT(bot_pickup, true,
+ {
+ // gather health and armor only
+ if (it.solid)
+ if ( ((it.health || it.armorvalue) && needarmor) || (it.weapons && needweapons ) )
+ if (vdist(it.origin - org, <, sradius))
+ {
+ int t = it.bot_pickupevalfunc(this, it);
+ if (t > 0)
+ navigation_routerating(this, it, t * ratingscale, 500);
+ }
+ });
+}
+
+void havocbot_role_ons_setrole(entity this, int role)
+{
+ LOG_DEBUG(this.netname," switched to ");
+ switch(role)
+ {
+ case HAVOCBOT_ONS_ROLE_DEFENSE:
+ LOG_DEBUG("defense");
+ this.havocbot_role = havocbot_role_ons_defense;
+ this.havocbot_role_flags = HAVOCBOT_ONS_ROLE_DEFENSE;
+ this.havocbot_role_timeout = 0;
+ break;
+ case HAVOCBOT_ONS_ROLE_ASSISTANT:
+ LOG_DEBUG("assistant");
+ this.havocbot_role = havocbot_role_ons_assistant;
+ this.havocbot_role_flags = HAVOCBOT_ONS_ROLE_ASSISTANT;
+ this.havocbot_role_timeout = 0;
+ break;
+ case HAVOCBOT_ONS_ROLE_OFFENSE:
+ LOG_DEBUG("offense");
+ this.havocbot_role = havocbot_role_ons_offense;
+ this.havocbot_role_flags = HAVOCBOT_ONS_ROLE_OFFENSE;
+ this.havocbot_role_timeout = 0;
+ break;
+ }
+ LOG_DEBUG("");
+}
+
+void havocbot_goalrating_ons_controlpoints_attack(entity this, float ratingscale)
+{
+ entity cp, cp1, cp2, best, wp;
+ float radius, bestvalue;
+ int c;
+ bool found;
+
+ // Filter control points
+ for(cp2 = ons_worldcplist; cp2; cp2 = cp2.ons_worldcpnext)
+ {
+ cp2.wpcost = c = 0;
+ cp2.wpconsidered = false;
+
+ if(cp2.isshielded)
+ continue;
+
+ // Ignore owned controlpoints
+ if(!(cp2.isgenneighbor[this.team] || cp2.iscpneighbor[this.team]))
+ continue;
+
+ // Count team mates interested in this control point
+ // (easier and cleaner than keeping counters per cp and teams)
+ FOREACH_CLIENT(IS_PLAYER(it), {
+ if(SAME_TEAM(it, this))
+ if(it.havocbot_role_flags & HAVOCBOT_ONS_ROLE_OFFENSE)
+ if(it.havocbot_ons_target == cp2)
+ ++c;
+ });
+
+ // NOTE: probably decrease the cost of attackable control points
+ cp2.wpcost = c;
+ cp2.wpconsidered = true;
+ }
+
+ // We'll consider only the best case
+ bestvalue = 99999999999;
+ cp = NULL;
+ for(cp1 = ons_worldcplist; cp1; cp1 = cp1.ons_worldcpnext)
+ {
+ if (!cp1.wpconsidered)
+ continue;
+
+ if(cp1.wpcost<bestvalue)
+ {
+ bestvalue = cp1.wpcost;
+ cp = cp1;
+ this.havocbot_ons_target = cp1;
+ }
+ }
+
+ if (!cp)
+ return;
+
+ LOG_DEBUG(this.netname, " chose cp ranked ", ftos(bestvalue));
+
+ if(cp.goalentity)
+ {
+ // Should be attacked
+ // Rate waypoints near it
+ found = false;
+ best = NULL;
+ bestvalue = 99999999999;
+ for(radius=0; radius<1000 && !found; radius+=500)
+ {
+ for(wp=findradius(cp.origin,radius); wp; wp=wp.chain)
+ {
+ if(!(wp.wpflags & WAYPOINTFLAG_GENERATED))
+ if(wp.classname=="waypoint")
+ if(checkpvs(wp.origin,cp))
+ {
+ found = true;
+ if(wp.cnt<bestvalue)
+ {
+ best = wp;
+ bestvalue = wp.cnt;
+ }
+ }
+ }
+ }
+
+ if(best)
+ {
+ navigation_routerating(this, best, ratingscale, 10000);
+ best.cnt += 1;
+
+ this.havocbot_attack_time = 0;
+ if(checkpvs(this.view_ofs,cp))
+ if(checkpvs(this.view_ofs,best))
+ this.havocbot_attack_time = time + 2;
+ }
+ else
+ {
+ navigation_routerating(this, cp, ratingscale, 10000);
+ }
+ LOG_DEBUG(this.netname, " found an attackable controlpoint at ", vtos(cp.origin));
+ }
+ else
+ {
+ // Should be touched
+ LOG_DEBUG(this.netname, " found a touchable controlpoint at ", vtos(cp.origin));
+ found = false;
+
+ // Look for auto generated waypoint
+ if (!bot_waypoints_for_items)
+ for (wp = findradius(cp.origin,100); wp; wp = wp.chain)
+ {
+ if(wp.classname=="waypoint")
+ {
+ navigation_routerating(this, wp, ratingscale, 10000);
+ found = true;
+ }
+ }
+
+ // Nothing found, rate the controlpoint itself
+ if (!found)
+ navigation_routerating(this, cp, ratingscale, 10000);
+ }
+}
+
+bool havocbot_goalrating_ons_generator_attack(entity this, float ratingscale)
+{
+ entity g, wp, bestwp;
+ bool found;
+ int best;
+
+ for(g = ons_worldgeneratorlist; g; g = g.ons_worldgeneratornext)
+ {
+ if(SAME_TEAM(g, this) || g.isshielded)
+ continue;
+
+ // Should be attacked
+ // Rate waypoints near it
+ found = false;
+ bestwp = NULL;
+ best = 99999999999;
+
+ for(wp=findradius(g.origin,400); wp; wp=wp.chain)
+ {
+ if(wp.classname=="waypoint")
+ if(checkpvs(wp.origin,g))
+ {
+ found = true;
+ if(wp.cnt<best)
+ {
+ bestwp = wp;
+ best = wp.cnt;
+ }
+ }
+ }
+
+ if(bestwp)
+ {
+ LOG_DEBUG("waypoints found around generator");
+ navigation_routerating(this, bestwp, ratingscale, 10000);
+ bestwp.cnt += 1;
+
+ this.havocbot_attack_time = 0;
+ if(checkpvs(this.view_ofs,g))
+ if(checkpvs(this.view_ofs,bestwp))
+ this.havocbot_attack_time = time + 5;
+
+ return true;
+ }
+ else
+ {
+ LOG_DEBUG("generator found without waypoints around");
+ // if there aren't waypoints near the generator go straight to it
+ navigation_routerating(this, g, ratingscale, 10000);
+ this.havocbot_attack_time = 0;
+ return true;
+ }
+ }
+ return false;
+}
+
+void havocbot_role_ons_offense(entity this)
+{
+ if(IS_DEAD(this))
+ {
+ this.havocbot_attack_time = 0;
+ havocbot_ons_reset_role(this);
+ return;
+ }
+
+ // Set the role timeout if necessary
+ if (!this.havocbot_role_timeout)
+ this.havocbot_role_timeout = time + 120;
+
+ if (time > this.havocbot_role_timeout)
+ {
+ havocbot_ons_reset_role(this);
+ return;
+ }
+
+ if(this.havocbot_attack_time>time)
+ return;
+
+ if (this.bot_strategytime < time)
+ {
+ navigation_goalrating_start(this);
+ havocbot_goalrating_enemyplayers(this, 20000, this.origin, 650);
+ if(!havocbot_goalrating_ons_generator_attack(this, 20000))
+ havocbot_goalrating_ons_controlpoints_attack(this, 20000);
+ havocbot_goalrating_ons_offenseitems(this, 10000, this.origin, 10000);
+ navigation_goalrating_end(this);
+
+ this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
+ }
+}
+
+void havocbot_role_ons_assistant(entity this)
+{
+ havocbot_ons_reset_role(this);
+}
+
+void havocbot_role_ons_defense(entity this)
+{
+ havocbot_ons_reset_role(this);
+}
+
+void havocbot_ons_reset_role(entity this)
+{
+ if(IS_DEAD(this))
+ return;
+
+ this.havocbot_ons_target = NULL;
+
+ // TODO: Defend control points or generator if necessary
+
+ havocbot_role_ons_setrole(this, HAVOCBOT_ONS_ROLE_OFFENSE);
+}
+
+
+/*
+ * Find control point or generator owned by the same team self which is nearest to pos
+ * if max_dist is positive, only control points within this range will be considered
+ */
+entity ons_Nearest_ControlPoint(entity this, vector pos, float max_dist)
+{
+ entity closest_target = NULL;
+ FOREACH_ENTITY_CLASS("onslaught_controlpoint", true,
+ {
+ if(SAME_TEAM(it, this))
+ if(it.iscaptured)
+ if(max_dist <= 0 || vdist(it.origin - pos, <=, max_dist))
+ if(vlen2(it.origin - pos) <= vlen2(closest_target.origin - pos) || closest_target == NULL)
+ closest_target = it;
+ });
+ FOREACH_ENTITY_CLASS("onslaught_generator", true,
+ {
+ if(SAME_TEAM(it, this))
+ if(max_dist <= 0 || vdist(it.origin - pos, <, max_dist))
+ if(vlen2(it.origin - pos) <= vlen2(closest_target.origin - pos) || closest_target == NULL)
+ closest_target = it;
+ });
+
+ return closest_target;
+}
+
+/*
+ * Find control point or generator owned by the same team self which is nearest to pos
+ * if max_dist is positive, only control points within this range will be considered
+ * This function only check distances on the XY plane, disregarding Z
+ */
+entity ons_Nearest_ControlPoint_2D(entity this, vector pos, float max_dist)
+{
+ entity closest_target = NULL;
+ vector delta;
+ float smallest_distance = 0, distance;
+
+ FOREACH_ENTITY_CLASS("onslaught_controlpoint", true,
+ {
+ delta = it.origin - pos;
+ delta_z = 0;
+ distance = vlen(delta);
+
+ if(SAME_TEAM(it, this))
+ if(it.iscaptured)
+ if(max_dist <= 0 || distance <= max_dist)
+ if(closest_target == NULL || distance <= smallest_distance )
+ {
+ closest_target = it;
+ smallest_distance = distance;
+ }
+ });
+ FOREACH_ENTITY_CLASS("onslaught_generator", true,
+ {
+ delta = it.origin - pos;
+ delta_z = 0;
+ distance = vlen(delta);
+
+ if(SAME_TEAM(it, this))
+ if(max_dist <= 0 || distance <= max_dist)
+ if(closest_target == NULL || distance <= smallest_distance )
+ {
+ closest_target = it;
+ smallest_distance = distance;
+ }
+ });
+
+ return closest_target;
+}
+/**
+ * find the number of control points and generators in the same team as this
+ */
+int ons_Count_SelfControlPoints(entity this)
+{
+ int n = 0;
+ FOREACH_ENTITY_CLASS("onslaught_controlpoint", true,
+ {
+ if(SAME_TEAM(it, this))
+ if(it.iscaptured)
+ n++;
+ });
+ FOREACH_ENTITY_CLASS("onslaught_generator", true,
+ {
+ if(SAME_TEAM(it, this))
+ n++;
+ });
+ return n;
+}
+
+/**
+ * Teleport player to a random position near tele_target
+ * if tele_effects is true, teleport sound+particles are created
+ * return false on failure
+ */
+bool ons_Teleport(entity player, entity tele_target, float range, bool tele_effects)
+{
+ if ( !tele_target )
+ return false;
+
+ int i;
+ vector loc;
+ float theta;
+ // narrow the range for each iteration to increase chances that a spawnpoint
+ // can be found even if there's little room around the control point
+ float iteration_scale = 1;
+ for(i = 0; i < 16; ++i)
+ {
+ iteration_scale -= i / 16;
+ theta = random() * 2 * M_PI;
+ loc_y = sin(theta);
+ loc_x = cos(theta);
+ loc_z = 0;
+ loc *= random() * range * iteration_scale;
+
+ loc += tele_target.origin + '0 0 128' * iteration_scale;
+
+ tracebox(loc, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), loc, MOVE_NORMAL, player);
+ if(trace_fraction == 1.0 && !trace_startsolid)
+ {
+ traceline(tele_target.origin, loc, MOVE_NOMONSTERS, tele_target); // double check to make sure we're not spawning outside the NULL
+ if(trace_fraction == 1.0 && !trace_startsolid)
+ {
+ if ( tele_effects )
+ {
+ Send_Effect(EFFECT_TELEPORT, player.origin, '0 0 0', 1);
+ sound (player, CH_TRIGGER, SND_TELEPORT, VOL_BASE, ATTEN_NORM);
+ }
+ setorigin(player, loc);
+ player.angles = '0 1 0' * ( theta * RAD2DEG + 180 );
+ makevectors(player.angles);
+ player.fixangle = true;
+ player.teleport_antispam = time + autocvar_g_onslaught_teleport_wait;
+
+ if ( tele_effects )
+ Send_Effect(EFFECT_TELEPORT, player.origin + v_forward * 32, '0 0 0', 1);
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+// ==============
+// Hook Functions
+// ==============
+
+MUTATOR_HOOKFUNCTION(ons, reset_map_global)
+{
+ FOREACH_CLIENT(IS_PLAYER(it), {
- client.ons_roundlost = spectatee.ons_roundlost; // make spectators see it too
++ STAT(ROUNDLOST, it) = false;
+ it.ons_deathloc = '0 0 0';
+ PutClientInServer(it);
+ });
+ return false;
+}
+
+MUTATOR_HOOKFUNCTION(ons, ClientDisconnect)
+{
+ entity player = M_ARGV(0, entity);
+
+ player.ons_deathloc = '0 0 0';
+}
+
+MUTATOR_HOOKFUNCTION(ons, MakePlayerObserver)
+{
+ entity player = M_ARGV(0, entity);
+
+ player.ons_deathloc = '0 0 0';
+}
+
+MUTATOR_HOOKFUNCTION(ons, PlayerSpawn)
+{
+ entity player = M_ARGV(0, entity);
+
+ if(!round_handler_IsRoundStarted())
+ {
+ player.player_blocked = true;
+ return false;
+ }
+
+ entity l;
+ for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
+ {
+ l.sprite.SendFlags |= 16;
+ }
+ for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
+ {
+ l.sprite.SendFlags |= 16;
+ }
+
+ if(ons_stalemate) { Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_OVERTIME_CONTROLPOINT); }
+
+ if ( autocvar_g_onslaught_spawn_choose )
+ if ( player.ons_spawn_by )
+ if ( ons_Teleport(player,player.ons_spawn_by,autocvar_g_onslaught_teleport_radius,false) )
+ {
+ player.ons_spawn_by = NULL;
+ return false;
+ }
+
+ if(autocvar_g_onslaught_spawn_at_controlpoints)
+ if(random() <= autocvar_g_onslaught_spawn_at_controlpoints_chance)
+ {
+ float random_target = autocvar_g_onslaught_spawn_at_controlpoints_random;
+ entity tmp_entity, closest_target = NULL;
+ vector spawn_loc = player.ons_deathloc;
+
+ // new joining player or round reset, don't bother checking
+ if(spawn_loc == '0 0 0') { return false; }
+
+ if(random_target) { RandomSelection_Init(); }
+
+ for(tmp_entity = ons_worldcplist; tmp_entity; tmp_entity = tmp_entity.ons_worldcpnext)
+ {
+ if(SAME_TEAM(tmp_entity, player))
+ if(random_target)
+ RandomSelection_Add(tmp_entity, 0, string_null, 1, 1);
+ else if(vlen2(tmp_entity.origin - spawn_loc) <= vlen2(closest_target.origin - spawn_loc) || closest_target == NULL)
+ closest_target = tmp_entity;
+ }
+
+ if(random_target) { closest_target = RandomSelection_chosen_ent; }
+
+ if(closest_target)
+ {
+ float i;
+ vector loc;
+ float iteration_scale = 1;
+ for(i = 0; i < 10; ++i)
+ {
+ iteration_scale -= i / 10;
+ loc = closest_target.origin + '0 0 96' * iteration_scale;
+ loc += ('0 1 0' * random()) * 128 * iteration_scale;
+ tracebox(loc, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), loc, MOVE_NORMAL, player);
+ if(trace_fraction == 1.0 && !trace_startsolid)
+ {
+ traceline(closest_target.origin, loc, MOVE_NOMONSTERS, closest_target); // double check to make sure we're not spawning outside the NULL
+ if(trace_fraction == 1.0 && !trace_startsolid)
+ {
+ setorigin(player, loc);
+ player.angles = normalize(loc - closest_target.origin) * RAD2DEG;
+ return false;
+ }
+ }
+ }
+ }
+ }
+
+ if(autocvar_g_onslaught_spawn_at_generator)
+ if(random() <= autocvar_g_onslaught_spawn_at_generator_chance)
+ {
+ float random_target = autocvar_g_onslaught_spawn_at_generator_random;
+ entity tmp_entity, closest_target = NULL;
+ vector spawn_loc = player.ons_deathloc;
+
+ // new joining player or round reset, don't bother checking
+ if(spawn_loc == '0 0 0') { return false; }
+
+ if(random_target) { RandomSelection_Init(); }
+
+ for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
+ {
+ if(random_target)
+ RandomSelection_Add(tmp_entity, 0, string_null, 1, 1);
+ else
+ {
+ if(SAME_TEAM(tmp_entity, player))
+ if(vlen2(tmp_entity.origin - spawn_loc) <= vlen2(closest_target.origin - spawn_loc) || closest_target == NULL)
+ closest_target = tmp_entity;
+ }
+ }
+
+ if(random_target) { closest_target = RandomSelection_chosen_ent; }
+
+ if(closest_target)
+ {
+ float i;
+ vector loc;
+ float iteration_scale = 1;
+ for(i = 0; i < 10; ++i)
+ {
+ iteration_scale -= i / 10;
+ loc = closest_target.origin + '0 0 128' * iteration_scale;
+ loc += ('0 1 0' * random()) * 256 * iteration_scale;
+ tracebox(loc, STAT(PL_MIN, NULL), STAT(PL_MAX, NULL), loc, MOVE_NORMAL, player);
+ if(trace_fraction == 1.0 && !trace_startsolid)
+ {
+ traceline(closest_target.origin, loc, MOVE_NOMONSTERS, closest_target); // double check to make sure we're not spawning outside the NULL
+ if(trace_fraction == 1.0 && !trace_startsolid)
+ {
+ setorigin(player, loc);
+ player.angles = normalize(loc - closest_target.origin) * RAD2DEG;
+ return false;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+MUTATOR_HOOKFUNCTION(ons, PlayerDies)
+{
+ entity frag_target = M_ARGV(2, entity);
+
+ frag_target.ons_deathloc = frag_target.origin;
+ entity l;
+ for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
+ {
+ l.sprite.SendFlags |= 16;
+ }
+ for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
+ {
+ l.sprite.SendFlags |= 16;
+ }
+
+ if ( autocvar_g_onslaught_spawn_choose )
+ if ( ons_Count_SelfControlPoints(frag_target) > 1 )
+ stuffcmd(frag_target, "qc_cmd_cl hud clickradar\n");
+
+ return false;
+}
+
+MUTATOR_HOOKFUNCTION(ons, MonsterMove)
+{
+ entity mon = M_ARGV(0, entity);
+
+ entity e = find(NULL, targetname, mon.target);
+ if (e != NULL)
+ mon.team = e.team;
+}
+
+void ons_MonsterSpawn_Delayed(entity this)
+{
+ entity own = this.owner;
+
+ if(!own) { delete(this); return; }
+
+ if(own.targetname)
+ {
+ entity e = find(NULL, target, own.targetname);
+ if(e != NULL)
+ {
+ own.team = e.team;
+
+ own.use(own, e, NULL);
+ }
+ }
+
+ delete(this);
+}
+
+MUTATOR_HOOKFUNCTION(ons, MonsterSpawn)
+{
+ entity mon = M_ARGV(0, entity);
+
+ entity e = spawn();
+ e.owner = mon;
+ InitializeEntity(e, ons_MonsterSpawn_Delayed, INITPRIO_FINDTARGET);
+}
+
+void ons_TurretSpawn_Delayed(entity this)
+{
+ entity own = this.owner;
+
+ if(!own) { delete(this); return; }
+
+ if(own.targetname)
+ {
+ entity e = find(NULL, target, own.targetname);
+ if(e != NULL)
+ {
+ own.team = e.team;
+ own.active = ACTIVE_NOT;
+
+ own.use(own, e, NULL);
+ }
+ }
+
+ delete(this);
+}
+
+MUTATOR_HOOKFUNCTION(ons, TurretSpawn)
+{
+ entity turret = M_ARGV(0, entity);
+
+ entity e = spawn();
+ e.owner = turret;
+ InitializeEntity(e, ons_TurretSpawn_Delayed, INITPRIO_FINDTARGET);
+
+ return false;
+}
+
+MUTATOR_HOOKFUNCTION(ons, HavocBot_ChooseRole)
+{
+ entity bot = M_ARGV(0, entity);
+
+ havocbot_ons_reset_role(bot);
+ return true;
+}
+
+MUTATOR_HOOKFUNCTION(ons, GetTeamCount)
+{
+ // onslaught is special
+ for(entity tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
+ {
+ switch(tmp_entity.team)
+ {
+ case NUM_TEAM_1: c1 = 0; break;
+ case NUM_TEAM_2: c2 = 0; break;
+ case NUM_TEAM_3: c3 = 0; break;
+ case NUM_TEAM_4: c4 = 0; break;
+ }
+ }
+
+ return true;
+}
+
+MUTATOR_HOOKFUNCTION(ons, SpectateCopy)
+{
+ entity spectatee = M_ARGV(0, entity);
+ entity client = M_ARGV(1, entity);
+
++ STAT(ROUNDLOST, client) = STAT(ROUNDLOST, spectatee); // make spectators see it too
+}
+
+MUTATOR_HOOKFUNCTION(ons, SV_ParseClientCommand)
+{
+ if(MUTATOR_RETURNVALUE) // command was already handled?
+ return false;
+
+ entity player = M_ARGV(0, entity);
+ string cmd_name = M_ARGV(1, string);
+ int cmd_argc = M_ARGV(2, int);
+
+ if ( cmd_name == "ons_spawn" )
+ {
+ vector pos = player.origin;
+ if(cmd_argc > 1)
+ pos_x = stof(argv(1));
+ if(cmd_argc > 2)
+ pos_y = stof(argv(2));
+ if(cmd_argc > 3)
+ pos_z = stof(argv(3));
+
+ if ( IS_PLAYER(player) )
+ {
+ if ( !STAT(FROZEN, player) )
+ {
+ entity source_point = ons_Nearest_ControlPoint(player, player.origin, autocvar_g_onslaught_teleport_radius);
+
+ if ( !source_point && player.health > 0 )
+ {
+ sprint(player, "\nYou need to be next to a control point\n");
+ return true;
+ }
+
+
+ entity closest_target = ons_Nearest_ControlPoint_2D(player, pos, autocvar_g_onslaught_click_radius);
+
+ if ( closest_target == NULL )
+ {
+ sprint(player, "\nNo control point found\n");
+ return true;
+ }
+
+ if ( player.health <= 0 )
+ {
+ player.ons_spawn_by = closest_target;
+ player.respawn_flags = player.respawn_flags | RESPAWN_FORCE;
+ }
+ else
+ {
+ if ( source_point == closest_target )
+ {
+ sprint(player, "\nTeleporting to the same point\n");
+ return true;
+ }
+
+ if ( !ons_Teleport(player,closest_target,autocvar_g_onslaught_teleport_radius,true) )
+ sprint(player, "\nUnable to teleport there\n");
+ }
+
+ return true;
+ }
+
+ sprint(player, "\nNo teleportation for you\n");
+ }
+
+ return true;
+ }
+ return false;
+}
+
+MUTATOR_HOOKFUNCTION(ons, PlayerUseKey)
+{
+ if(MUTATOR_RETURNVALUE || gameover) { return false; }
+
+ entity player = M_ARGV(0, entity);
+
+ if((time > player.teleport_antispam) && (!IS_DEAD(player)) && !player.vehicle)
+ {
+ entity source_point = ons_Nearest_ControlPoint(player, player.origin, autocvar_g_onslaught_teleport_radius);
+ if ( source_point )
+ {
+ stuffcmd(player, "qc_cmd_cl hud clickradar\n");
+ return true;
+ }
+ }
+}
+
+MUTATOR_HOOKFUNCTION(ons, PlayHitsound)
+{
+ entity frag_victim = M_ARGV(0, entity);
+
+ return (frag_victim.classname == "onslaught_generator" && !frag_victim.isshielded)
+ || (frag_victim.classname == "onslaught_controlpoint_icon" && !frag_victim.owner.isshielded);
+}
+
+MUTATOR_HOOKFUNCTION(ons, SendWaypoint)
+{
+ entity wp = M_ARGV(0, entity);
+ entity to = M_ARGV(1, entity);
+ int sf = M_ARGV(2, int);
+ int wp_flag = M_ARGV(3, int);
+
+ if(sf & 16)
+ {
+ if(wp.owner.classname == "onslaught_controlpoint")
+ {
+ entity wp_owner = wp.owner;
+ entity e = WaypointSprite_getviewentity(to);
+ if(SAME_TEAM(e, wp_owner) && wp_owner.goalentity.health >= wp_owner.goalentity.max_health) { wp_flag |= 2; }
+ if(!ons_ControlPoint_Attackable(wp_owner, e.team)) { wp_flag |= 2; }
+ }
+ if(wp.owner.classname == "onslaught_generator")
+ {
+ entity wp_owner = wp.owner;
+ if(wp_owner.isshielded && wp_owner.health >= wp_owner.max_health) { wp_flag |= 2; }
+ if(wp_owner.health <= 0) { wp_flag |= 2; }
+ }
+ }
+
+ M_ARGV(3, int) = wp_flag;
+}
+
+MUTATOR_HOOKFUNCTION(ons, TurretValidateTarget)
+{
+ entity turret_target = M_ARGV(1, entity);
+
+ if(substring(turret_target.classname, 0, 10) == "onslaught_") // don't attack onslaught targets, that's the player's job!
+ {
+ M_ARGV(3, float) = -3;
+ return true;
+ }
+
+ return false;
+}
+
+MUTATOR_HOOKFUNCTION(ons, TurretThink)
+{
+ entity turret = M_ARGV(0, entity);
+
+ // ONS uses somewhat backwards linking.
+ if(turret.target)
+ {
+ entity e = find(NULL, targetname, turret.target);
+ if (e != NULL)
+ turret.team = e.team;
+ }
+
+ if(turret.team != turret.tur_head.team)
+ turret_respawn(turret);
+}
+
+
+// ==========
+// Spawnfuncs
+// ==========
+
+/*QUAKED spawnfunc_onslaught_link (0 .5 .8) (-16 -16 -16) (16 16 16)
+ Link between control points.
+
+ This entity targets two different spawnfunc_onslaught_controlpoint or spawnfunc_onslaught_generator entities, and suppresses shielding on both if they are owned by different teams.
+
+keys:
+"target" - first control point.
+"target2" - second control point.
+ */
+spawnfunc(onslaught_link)
+{
+ if(!g_onslaught) { delete(this); return; }
+
+ if (this.target == "" || this.target2 == "")
+ objerror(this, "target and target2 must be set\n");
+
+ this.ons_worldlinknext = ons_worldlinklist; // link into ons_worldlinklist
+ ons_worldlinklist = this;
+
+ InitializeEntity(this, ons_DelayedLinkSetup, INITPRIO_FINDTARGET);
+ Net_LinkEntity(this, false, 0, ons_Link_Send);
+}
+
+/*QUAKED spawnfunc_onslaught_controlpoint (0 .5 .8) (-32 -32 0) (32 32 128)
+ Control point. Be sure to give this enough clearance so that the shootable part has room to exist
+
+ This should link to an spawnfunc_onslaught_controlpoint entity or spawnfunc_onslaught_generator entity.
+
+keys:
+"targetname" - name that spawnfunc_onslaught_link entities will use to target this.
+"target" - target any entities that are tied to this control point, such as vehicles and buildable structure entities.
+"message" - name of this control point (should reflect the location in the map, such as "center bridge", "north tower", etc)
+ */
+
+spawnfunc(onslaught_controlpoint)
+{
+ if(!g_onslaught) { delete(this); return; }
+
+ ons_ControlPoint_Setup(this);
+}
+
+/*QUAKED spawnfunc_onslaught_generator (0 .5 .8) (-32 -32 -24) (32 32 64)
+ Base generator.
+
+ spawnfunc_onslaught_link entities can target this.
+
+keys:
+"team" - team that owns this generator (5 = red, 14 = blue, etc), MUST BE SET.
+"targetname" - name that spawnfunc_onslaught_link entities will use to target this.
+ */
+spawnfunc(onslaught_generator)
+{
+ if(!g_onslaught) { delete(this); return; }
+ if(!this.team) { objerror(this, "team must be set"); }
+
+ ons_GeneratorSetup(this);
+}
+
+// scoreboard setup
+void ons_ScoreRules()
+{
+ CheckAllowedTeams(NULL);
+ int teams = 0;
+ if(c1 >= 0) teams |= BIT(0);
+ if(c2 >= 0) teams |= BIT(1);
+ if(c3 >= 0) teams |= BIT(2);
+ if(c4 >= 0) teams |= BIT(3);
+ ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
+ ScoreInfo_SetLabel_TeamScore (ST_ONS_CAPS, "destroyed", SFL_SORT_PRIO_PRIMARY);
+ ScoreInfo_SetLabel_PlayerScore(SP_ONS_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
+ ScoreInfo_SetLabel_PlayerScore(SP_ONS_TAKES, "takes", 0);
+ ScoreRules_basics_end();
+}
+
+void ons_DelayedInit(entity this) // Do this check with a delay so we can wait for teams to be set up
+{
+ ons_ScoreRules();
+
+ round_handler_Spawn(Onslaught_CheckPlayers, Onslaught_CheckWinner, Onslaught_RoundStart);
+ round_handler_Init(5, autocvar_g_onslaught_warmup, autocvar_g_onslaught_round_timelimit);
+}
+
+void ons_Initialize()
+{
+ g_onslaught = true;
+ ons_captureshield_force = autocvar_g_onslaught_shield_force;
+
+ InitializeEntity(NULL, ons_DelayedInit, INITPRIO_GAMETYPE);
+}
--- /dev/null
+#include "player.qh"
+
+#include "bot/api.qh"
+#include "cheats.qh"
+#include "g_damage.qh"
+#include "g_subs.qh"
+#include "miscfunctions.qh"
+#include "portals.qh"
+#include "teamplay.qh"
+#include "weapons/throwing.qh"
+#include "command/common.qh"
+#include "../common/state.qh"
+#include "../common/anim.qh"
+#include "../common/animdecide.qh"
+#include "../common/csqcmodel_settings.qh"
+#include "../common/deathtypes/all.qh"
+#include "../common/triggers/subs.qh"
+#include "../common/playerstats.qh"
+#include "../lib/csqcmodel/sv_model.qh"
+
+#include "../common/minigames/sv_minigames.qh"
+
+#include "../common/physics/player.qh"
+#include "../common/effects/qc/all.qh"
+#include "../common/mutators/mutator/waypoints/waypointsprites.qh"
+#include "../common/triggers/include.qh"
+
+#include "weapons/weaponstats.qh"
+
+#include "../common/animdecide.qh"
+
+void Drop_Special_Items(entity player)
+{
+ // called when the player has become stuck or frozen
+ // so objective items aren't stuck with the player
+
+ MUTATOR_CALLHOOK(DropSpecialItems, player);
+}
+
+void CopyBody_Think(entity this)
+{
+ if(this.CopyBody_nextthink && time > this.CopyBody_nextthink)
+ {
+ this.CopyBody_think(this);
+ if(wasfreed(this))
+ return;
+ this.CopyBody_nextthink = this.nextthink;
+ this.CopyBody_think = getthink(this);
+ setthink(this, CopyBody_Think);
+ }
+ CSQCMODEL_AUTOUPDATE(this);
+ this.nextthink = time;
+}
+void CopyBody(entity this, float keepvelocity)
+{
+ if (this.effects & EF_NODRAW)
+ return;
+ entity clone = new(body);
+ clone.enemy = this;
+ clone.lip = this.lip;
+ clone.colormap = this.colormap;
+ clone.iscreature = this.iscreature;
+ clone.teleportable = this.teleportable;
+ clone.damagedbycontents = this.damagedbycontents;
+ clone.angles = this.angles;
+ clone.v_angle = this.v_angle;
+ clone.avelocity = this.avelocity;
+ clone.damageforcescale = this.damageforcescale;
+ clone.effects = this.effects;
+ clone.glowmod = this.glowmod;
+ clone.event_damage = this.event_damage;
+ clone.anim_state = this.anim_state;
+ clone.anim_time = this.anim_time;
+ clone.anim_lower_action = this.anim_lower_action;
+ clone.anim_lower_time = this.anim_lower_time;
+ clone.anim_upper_action = this.anim_upper_action;
+ clone.anim_upper_time = this.anim_upper_time;
+ clone.anim_implicit_state = this.anim_implicit_state;
+ clone.anim_implicit_time = this.anim_implicit_time;
+ clone.anim_lower_implicit_action = this.anim_lower_implicit_action;
+ clone.anim_lower_implicit_time = this.anim_lower_implicit_time;
+ clone.anim_upper_implicit_action = this.anim_upper_implicit_action;
+ clone.anim_upper_implicit_time = this.anim_upper_implicit_time;
+ clone.dphitcontentsmask = this.dphitcontentsmask;
+ clone.death_time = this.death_time;
+ clone.pain_finished = this.pain_finished;
+ clone.health = this.health;
+ clone.armorvalue = this.armorvalue;
+ clone.armortype = this.armortype;
+ clone.model = this.model;
+ clone.modelindex = this.modelindex;
+ clone.skin = this.skin;
+ clone.species = this.species;
+ clone.move_qcphysics = false; // don't run gamecode logic on clones, too many
+ set_movetype(clone, this.move_movetype);
+ clone.solid = this.solid;
+ clone.ballistics_density = this.ballistics_density;
+ clone.takedamage = this.takedamage;
+ setcefc(clone, getcefc(this));
+ clone.uncustomizeentityforclient = this.uncustomizeentityforclient;
+ clone.uncustomizeentityforclient_set = this.uncustomizeentityforclient_set;
+ if (keepvelocity == 1)
+ clone.velocity = this.velocity;
+ clone.oldvelocity = clone.velocity;
+ clone.alpha = this.alpha;
+ clone.fade_time = this.fade_time;
+ clone.fade_rate = this.fade_rate;
+ //clone.weapon = this.weapon;
+ setorigin(clone, this.origin);
+ setsize(clone, this.mins, this.maxs);
+ clone.prevorigin = this.origin;
+ clone.reset = SUB_Remove;
+ clone._ps = this._ps;
+
+ Drag_MoveDrag(this, clone);
+
+ if(clone.colormap <= maxclients && clone.colormap > 0)
+ clone.colormap = 1024 + this.clientcolors;
+
+ CSQCMODEL_AUTOINIT(clone);
+ clone.CopyBody_nextthink = this.nextthink;
+ clone.CopyBody_think = getthink(this);
+ clone.nextthink = time;
+ setthink(clone, CopyBody_Think);
+ // "bake" the current animation frame for clones (they don't get clientside animation)
+ animdecide_load_if_needed(clone);
+ animdecide_setframes(clone, false, frame, frame1time, frame2, frame2time);
++
++ MUTATOR_CALLHOOK(CopyBody, this, clone, keepvelocity);
+}
+
+void player_setupanimsformodel(entity this)
+{
+ // load animation info
+ animdecide_load_if_needed(this);
+ animdecide_setstate(this, 0, false);
+}
+
+void player_anim(entity this)
+{
+ int deadbits = (this.anim_state & (ANIMSTATE_DEAD1 | ANIMSTATE_DEAD2));
+ if(IS_DEAD(this)) {
+ if (!deadbits) {
+ // Decide on which death animation to use.
+ if(random() < 0.5)
+ deadbits = ANIMSTATE_DEAD1;
+ else
+ deadbits = ANIMSTATE_DEAD2;
+ }
+ } else {
+ // Clear a previous death animation.
+ deadbits = 0;
+ }
+ int animbits = deadbits;
+ if(STAT(FROZEN, this))
+ animbits |= ANIMSTATE_FROZEN;
+ if(this.move_movetype == MOVETYPE_FOLLOW)
+ animbits |= ANIMSTATE_FOLLOW;
+ if(this.crouch)
+ animbits |= ANIMSTATE_DUCK;
+ animdecide_setstate(this, animbits, false);
+ animdecide_setimplicitstate(this, IS_ONGROUND(this));
+}
+
+void PlayerCorpseDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
+{
+ float take, save;
+ vector v;
+ Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, this, attacker);
+
+ // damage resistance (ignore most of the damage from a bullet or similar)
+ damage = max(damage - 5, 1);
+
+ v = healtharmor_applydamage(this.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, damage);
+ take = v.x;
+ save = v.y;
+
+ if(sound_allowed(MSG_BROADCAST, attacker))
+ {
+ if (save > 10)
+ sound (this, CH_SHOTS, SND_ARMORIMPACT, VOL_BASE, ATTEN_NORM);
+ else if (take > 30)
+ sound (this, CH_SHOTS, SND_BODYIMPACT2, VOL_BASE, ATTEN_NORM);
+ else if (take > 10)
+ sound (this, CH_SHOTS, SND_BODYIMPACT1, VOL_BASE, ATTEN_NORM);
+ }
+
+ if (take > 50)
+ Violence_GibSplash_At(hitloc, force * -0.1, 3, 1, this, attacker);
+ if (take > 100)
+ Violence_GibSplash_At(hitloc, force * -0.2, 3, 1, this, attacker);
+
+ this.armorvalue = this.armorvalue - save;
+ this.health = this.health - take;
+ // pause regeneration for 5 seconds
+ this.pauseregen_finished = max(this.pauseregen_finished, time + autocvar_g_balance_pause_health_regen);
+
+ this.dmg_save = this.dmg_save + save;//max(save - 10, 0);
+ this.dmg_take = this.dmg_take + take;//max(take - 10, 0);
+ this.dmg_inflictor = inflictor;
+
+ if (this.health <= -autocvar_sv_gibhealth && this.alpha >= 0)
+ {
+ // don't use any animations as a gib
+ this.frame = 0;
+ // view just above the floor
+ this.view_ofs = '0 0 4';
+
+ Violence_GibSplash(this, 1, 1, attacker);
+ this.alpha = -1;
+ this.solid = SOLID_NOT; // restore later
+ this.takedamage = DAMAGE_NO; // restore later
+ this.damagedbycontents = false;
+ }
+}
+
+void calculate_player_respawn_time(entity this)
+{
+ if(g_ca)
+ return;
+
+ float gametype_setting_tmp;
+ float sdelay_max = GAMETYPE_DEFAULTED_SETTING(respawn_delay_max);
+ float sdelay_small = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small);
+ float sdelay_large = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large);
+ float sdelay_small_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small_count);
+ float sdelay_large_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large_count);
+ float waves = GAMETYPE_DEFAULTED_SETTING(respawn_waves);
+
+ float pcount = 1; // Include myself whether or not team is already set right and I'm a "player".
+ if (teamplay)
+ {
+ FOREACH_CLIENT(IS_PLAYER(it) && it != this, LAMBDA(
+ if(it.team == this.team)
+ ++pcount;
+ ));
+ if (sdelay_small_count == 0)
+ sdelay_small_count = 1;
+ if (sdelay_large_count == 0)
+ sdelay_large_count = 1;
+ }
+ else
+ {
+ FOREACH_CLIENT(IS_PLAYER(it) && it != this, LAMBDA(
+ ++pcount;
+ ));
+ if (sdelay_small_count == 0)
+ {
+ if (g_cts)
+ {
+ // Players play independently. No point in requiring enemies.
+ sdelay_small_count = 1;
+ }
+ else
+ {
+ // Players play AGAINST each other. Enemies required.
+ sdelay_small_count = 2;
+ }
+ }
+ if (sdelay_large_count == 0)
+ {
+ if (g_cts)
+ {
+ // Players play independently. No point in requiring enemies.
+ sdelay_large_count = 1;
+ }
+ else
+ {
+ // Players play AGAINST each other. Enemies required.
+ sdelay_large_count = 2;
+ }
+ }
+ }
+
+ float sdelay;
+
+ if (pcount <= sdelay_small_count)
+ sdelay = sdelay_small;
+ else if (pcount >= sdelay_large_count)
+ sdelay = sdelay_large;
+ else // NOTE: this case implies sdelay_large_count > sdelay_small_count.
+ sdelay = sdelay_small + (sdelay_large - sdelay_small) * (pcount - sdelay_small_count) / (sdelay_large_count - sdelay_small_count);
+
+ if(waves)
+ this.respawn_time = ceil((time + sdelay) / waves) * waves;
+ else
+ this.respawn_time = time + sdelay;
+
+ if(sdelay < sdelay_max)
+ this.respawn_time_max = time + sdelay_max;
+ else
+ this.respawn_time_max = this.respawn_time;
+
+ if((sdelay + waves >= 5.0) && (this.respawn_time - time > 1.75))
+ this.respawn_countdown = 10; // first number to count down from is 10
+ else
+ this.respawn_countdown = -1; // do not count down
+
+ if(autocvar_g_forced_respawn)
+ this.respawn_flags = this.respawn_flags | RESPAWN_FORCE;
+}
+
+void PlayerDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
+{
+ float take, save, dh, da;
+ vector v;
+ float valid_damage_for_weaponstats;
+ float excess;
+
+ dh = max(this.health, 0);
+ da = max(this.armorvalue, 0);
+
+ if(!DEATH_ISSPECIAL(deathtype))
+ {
+ damage *= sqrt(bound(1.0, this.cvar_cl_handicap, 100.0));
+ if(this != attacker)
+ damage /= sqrt(bound(1.0, attacker.cvar_cl_handicap, 100.0));
+ }
+
+ if(DEATH_ISWEAPON(deathtype, WEP_TUBA))
+ {
+ // tuba causes blood to come out of the ears
+ vector ear1, ear2;
+ vector d;
+ float f;
+ ear1 = this.origin;
+ ear1_z += 0.125 * this.view_ofs.z + 0.875 * this.maxs.z; // 7/8
+ ear2 = ear1;
+ makevectors(this.angles);
+ ear1 += v_right * -10;
+ ear2 += v_right * +10;
+ d = inflictor.origin - this.origin;
+ if (d)
+ f = (d * v_right) / vlen(d); // this is cos of angle of d and v_right!
+ else
+ f = 0; // Assum ecenter.
+ force = v_right * vlen(force);
+ Violence_GibSplash_At(ear1, force * -1, 2, bound(0, damage, 25) / 2 * (0.5 - 0.5 * f), this, attacker);
+ Violence_GibSplash_At(ear2, force, 2, bound(0, damage, 25) / 2 * (0.5 + 0.5 * f), this, attacker);
+ if(f > 0)
+ {
+ hitloc = ear1;
+ force = force * -1;
+ }
+ else
+ {
+ hitloc = ear2;
+ // force is already good
+ }
+ }
+ else
+ Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, this, attacker);
+
+
+ v = healtharmor_applydamage(this.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, damage);
+ take = v.x;
+ save = v.y;
+
+ if(attacker == this)
+ {
+ // don't reset pushltime for this damage as it may be an attempt to
+ // escape a lava pit or similar
+ //this.pushltime = 0;
+ this.istypefrag = 0;
+ }
+ else if(IS_PLAYER(attacker))
+ {
+ this.pusher = attacker;
+ this.pushltime = time + autocvar_g_maxpushtime;
+ this.istypefrag = PHYS_INPUT_BUTTON_CHAT(this);
+ }
+ else if(time < this.pushltime)
+ {
+ attacker = this.pusher;
+ this.pushltime = max(this.pushltime, time + 0.6);
+ }
+ else
+ {
+ this.pushltime = 0;
+ this.istypefrag = 0;
+ }
+
+ if (time < this.spawnshieldtime && autocvar_g_spawnshield_blockdamage < 1)
+ {
+ vector v = healtharmor_applydamage(this.armorvalue, max(0, autocvar_g_spawnshield_blockdamage), deathtype, damage);
+ take = v.x;
+ save = v.y;
+ }
+
+ MUTATOR_CALLHOOK(PlayerDamage_SplitHealthArmor, inflictor, attacker, this, force, take, save, deathtype, damage);
+ take = bound(0, M_ARGV(4, float), this.health);
+ save = bound(0, M_ARGV(5, float), this.armorvalue);
+ excess = max(0, damage - take - save);
+
+ if(sound_allowed(MSG_BROADCAST, attacker))
+ {
+ if (save > 10)
+ sound (this, CH_SHOTS, SND_ARMORIMPACT, VOL_BASE, ATTEN_NORM);
+ else if (take > 30)
+ sound (this, CH_SHOTS, SND_BODYIMPACT2, VOL_BASE, ATTEN_NORM);
+ else if (take > 10)
+ sound (this, CH_SHOTS, SND_BODYIMPACT1, VOL_BASE, ATTEN_NORM); // FIXME possibly remove them?
+ }
+
+ if (take > 50)
+ Violence_GibSplash_At(hitloc, force * -0.1, 3, 1, this, attacker);
+ if (take > 100)
+ Violence_GibSplash_At(hitloc, force * -0.2, 3, 1, this, attacker);
+
+ if (time >= this.spawnshieldtime || autocvar_g_spawnshield_blockdamage < 1)
+ {
+ if (!(this.flags & FL_GODMODE))
+ {
+ this.armorvalue = this.armorvalue - save;
+ this.health = this.health - take;
+ // pause regeneration for 5 seconds
+ if(take)
+ this.pauseregen_finished = max(this.pauseregen_finished, time + autocvar_g_balance_pause_health_regen);
+
+ if (time > this.pain_finished) //Don't switch pain sequences like crazy
+ {
+ this.pain_finished = time + 0.5; //Supajoe
+
+ if(autocvar_sv_gentle < 1) {
+ if(this.classname != "body") // pain anim is BORKED on our ZYMs, FIXME remove this once we have good models
+ {
+ if (!this.animstate_override)
+ {
+ if (random() > 0.5)
+ animdecide_setaction(this, ANIMACTION_PAIN1, true);
+ else
+ animdecide_setaction(this, ANIMACTION_PAIN2, true);
+ }
+ }
+
+ if(sound_allowed(MSG_BROADCAST, attacker))
+ if((this.health < 2 * WEP_CVAR_PRI(blaster, damage) * autocvar_g_balance_selfdamagepercent + 1) || !(DEATH_WEAPONOF(deathtype).spawnflags & WEP_FLAG_CANCLIMB) || attacker != this) // WEAPONTODO: create separate limit for pain notification with laser
+ if(this.health > 1)
+ // exclude pain sounds for laserjumps as long as you aren't REALLY low on health and would die of the next two
+ {
+ if(deathtype == DEATH_FALL.m_id)
+ PlayerSound(this, playersound_fall, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
+ else if(this.health > 75)
+ PlayerSound(this, playersound_pain100, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
+ else if(this.health > 50)
+ PlayerSound(this, playersound_pain75, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
+ else if(this.health > 25)
+ PlayerSound(this, playersound_pain50, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
+ else
+ PlayerSound(this, playersound_pain25, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
+ }
+ }
+ }
+
+ // throw off bot aim temporarily
+ float shake;
+ if(IS_BOT_CLIENT(this) && this.health >= 1)
+ {
+ shake = damage * 5 / (bound(0,skill,100) + 1);
+ this.v_angle_x = this.v_angle.x + (random() * 2 - 1) * shake;
+ this.v_angle_y = this.v_angle.y + (random() * 2 - 1) * shake;
+ this.v_angle_x = bound(-90, this.v_angle.x, 90);
+ }
+ }
+ else
+ this.max_armorvalue += (save + take);
+ }
+ this.dmg_save = this.dmg_save + save;//max(save - 10, 0);
+ this.dmg_take = this.dmg_take + take;//max(take - 10, 0);
+ this.dmg_inflictor = inflictor;
+
+ if (this != attacker) {
+ float realdmg = damage - excess;
+ if (IS_PLAYER(attacker)) {
+ PlayerScore_Add(attacker, SP_DMG, realdmg);
+ }
+ if (IS_PLAYER(this)) {
+ PlayerScore_Add(this, SP_DMGTAKEN, realdmg);
+ }
+ }
+
+ bool abot = (IS_BOT_CLIENT(attacker));
+ bool vbot = (IS_BOT_CLIENT(this));
+
+ valid_damage_for_weaponstats = 0;
+ Weapon awep = WEP_Null;
+
+ if(vbot || IS_REAL_CLIENT(this))
+ if(abot || IS_REAL_CLIENT(attacker))
+ if(attacker && this != attacker)
+ if(DIFF_TEAM(this, attacker))
+ {
+ if(DEATH_ISSPECIAL(deathtype))
+ awep = PS(attacker).m_weapon;
+ else
+ awep = DEATH_WEAPONOF(deathtype);
+ valid_damage_for_weaponstats = 1;
+ }
+
+ dh = dh - max(this.health, 0);
+ da = da - max(this.armorvalue, 0);
+ if(valid_damage_for_weaponstats)
+ {
+ WeaponStats_LogDamage(awep.m_id, abot, PS(this).m_weapon.m_id, vbot, dh + da);
+ }
+ if (dh + da)
+ {
+ MUTATOR_CALLHOOK(PlayerDamaged, attacker, this, dh, da, hitloc, deathtype);
+ }
+
+ if (this.health < 1)
+ {
+ float defer_ClientKill_Now_TeamChange;
+ defer_ClientKill_Now_TeamChange = false;
+
+ if(this.alivetime)
+ {
+ PS_GR_P_ADDVAL(this, PLAYERSTATS_ALIVETIME, time - this.alivetime);
+ this.alivetime = 0;
+ }
+
+ if(valid_damage_for_weaponstats)
+ WeaponStats_LogKill(awep.m_id, abot, PS(this).m_weapon.m_id, vbot);
+
+ if(autocvar_sv_gentle < 1)
+ if(sound_allowed(MSG_BROADCAST, attacker))
+ {
+ if(deathtype == DEATH_DROWN.m_id)
+ PlayerSound(this, playersound_drown, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
+ else
+ PlayerSound(this, playersound_death, CH_PAIN, VOL_BASE, VOICETYPE_PLAYERSOUND);
+ }
+
+ // get rid of kill indicator
+ if(this.killindicator)
+ {
+ delete(this.killindicator);
+ this.killindicator = NULL;
+ if(this.killindicator_teamchange)
+ defer_ClientKill_Now_TeamChange = true;
+
+ if(this.classname == "body")
+ if(deathtype == DEATH_KILL.m_id)
+ {
+ // for the lemmings fans, a small harmless explosion
+ Send_Effect(EFFECT_ROCKET_EXPLODE, this.origin, '0 0 0', 1);
+ }
+ }
+
+ // print an obituary message
+ if(this.classname != "body")
+ Obituary (attacker, inflictor, this, deathtype);
+
+ // increment frag counter for used weapon type
+ Weapon w = DEATH_WEAPONOF(deathtype);
+ if(w != WEP_Null)
+ if(accuracy_isgooddamage(attacker, this))
+ attacker.accuracy.(accuracy_frags[w.m_id-1]) += 1;
+
+ MUTATOR_CALLHOOK(PlayerDies, inflictor, attacker, this, deathtype, damage);
+ excess = M_ARGV(4, float);
+
+ Weapon wep = PS(this).m_weapon;
+ /*for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
+ {
+ .entity weaponentity = weaponentities[slot];
+ wep.wr_playerdeath(wep, this, weaponentity);
+ }*/
+ .entity weaponentity = weaponentities[0]; // TODO: unhardcode
+ wep.wr_playerdeath(wep, this, weaponentity);
+
+ RemoveGrapplingHook(this);
+
+ Portal_ClearAllLater(this);
+
+ this.fixangle = true;
+
+ if(defer_ClientKill_Now_TeamChange)
+ ClientKill_Now_TeamChange(this); // can turn player into spectator
+
+ // player could have been miraculously resuscitated ;)
+ // e.g. players in freezetag get frozen, they don't really die
+ if(this.health >= 1 || !(IS_PLAYER(this) || this.classname == "body"))
+ return;
+
+ // when we get here, player actually dies
+
+ Unfreeze(this); // remove any icy remains
+ this.health = 0; // Unfreeze resets health, so we need to set it back
+
+ // clear waypoints
+ WaypointSprite_PlayerDead(this);
+ // throw a weapon
+ SpawnThrownWeapon(this, this.origin + (this.mins + this.maxs) * 0.5, PS(this).m_switchweapon.m_id);
+
+ // become fully visible
+ this.alpha = default_player_alpha;
+ // make the corpse upright (not tilted)
+ this.angles_x = 0;
+ this.angles_z = 0;
+ // don't spin
+ this.avelocity = '0 0 0';
+ // view from the floor
+ this.view_ofs = '0 0 -8';
+ // toss the corpse
+ set_movetype(this, MOVETYPE_TOSS);
+ // shootable corpse
+ this.solid = SOLID_CORPSE;
+ this.ballistics_density = autocvar_g_ballistics_density_corpse;
+ // don't stick to the floor
+ UNSET_ONGROUND(this);
+ // dying animation
+ this.deadflag = DEAD_DYING;
+
+ // when to allow respawn
+ calculate_player_respawn_time(this);
+
+ this.death_time = time;
+ if (random() < 0.5)
+ animdecide_setstate(this, this.anim_state | ANIMSTATE_DEAD1, true);
+ else
+ animdecide_setstate(this, this.anim_state | ANIMSTATE_DEAD2, true);
+ if (this.maxs.z > 5)
+ {
+ this.maxs_z = 5;
+ setsize(this, this.mins, this.maxs);
+ }
+ // set damage function to corpse damage
+ this.event_damage = PlayerCorpseDamage;
+ // call the corpse damage function just in case it wants to gib
+ this.event_damage(this, inflictor, attacker, excess, deathtype, hitloc, force);
+
+ // set up to fade out later
+ SUB_SetFade (this, time + 6 + random (), 1);
+ // reset body think wrapper broken by SUB_SetFade
+ if(this.classname == "body" && getthink(this) != CopyBody_Think) {
+ this.CopyBody_think = getthink(this);
+ this.CopyBody_nextthink = this.nextthink;
+ setthink(this, CopyBody_Think);
+ this.nextthink = time;
+ }
+
+ if(autocvar_sv_gentle > 0 || autocvar_ekg || this.classname == "body") {
+ // remove corpse
+ // clones don't run any animation code any more, so we must gib them when they die :(
+ PlayerCorpseDamage(this, inflictor, attacker, autocvar_sv_gibhealth+1.0, deathtype, hitloc, force);
+ }
+
+ // reset fields the weapons may use just in case
+ FOREACH(Weapons, it != WEP_Null, LAMBDA(
+ it.wr_resetplayer(it, this);
+ for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
+ {
+ ATTACK_FINISHED_FOR(this, it.m_id, slot) = 0;
+ }
+ ));
+ }
+}
+
+void MoveToTeam(entity client, int team_colour, int type)
+{
+ int lockteams_backup = lockteams; // backup any team lock
+ lockteams = 0; // disable locked teams
+ TeamchangeFrags(client); // move the players frags
+ SetPlayerColors(client, team_colour - 1); // set the players colour
+ Damage(client, client, client, 100000, DEATH_AUTOTEAMCHANGE.m_id, client.origin, '0 0 0'); // kill the player
+ lockteams = lockteams_backup; // restore the team lock
+ LogTeamchange(client.playerid, client.team, type);
+}
+
+/** print(), but only print if the server is not local */
+void dedicated_print(string input)
+{
+ if (server_is_dedicated) print(input);
+}
+
+/**
+ * message "": do not say, just test flood control
+ * return value:
+ * 1 = accept
+ * 0 = reject
+ * -1 = fake accept
+ */
+int Say(entity source, int teamsay, entity privatesay, string msgin, bool floodcontrol)
+{
+ if (!teamsay && !privatesay) if (substring(msgin, 0, 1) == " ")
+ msgin = substring(msgin, 1, -1); // work around DP say bug (say_team does not have this!)
+
+ msgin = formatmessage(source, msgin);
+
+ string colorstr;
+ if (!IS_PLAYER(source))
+ colorstr = "^0"; // black for spectators
+ else if(teamplay)
+ colorstr = Team_ColorCode(source.team);
+ else
+ {
+ colorstr = "";
+ teamsay = false;
+ }
+
+ if(intermission_running)
+ teamsay = false;
+
+ if (!source) {
+ colorstr = "";
+ teamsay = false;
+ }
+
+ if(msgin != "")
+ msgin = trigger_magicear_processmessage_forallears(source, teamsay, privatesay, msgin);
+
+ /*
+ * using bprint solves this... me stupid
+ // how can we prevent the message from appearing in a listen server?
+ // for now, just give "say" back and only handle say_team
+ if(!teamsay)
+ {
+ clientcommand(source, strcat("say ", msgin));
+ return;
+ }
+ */
+
+ string namestr = "";
+ if (source)
+ namestr = autocvar_g_chat_teamcolors ? playername(source) : source.netname;
+
+ string colorprefix = (strdecolorize(namestr) == namestr) ? "^3" : "^7";
+
+ string msgstr, cmsgstr;
+ string privatemsgprefix = string_null;
+ int privatemsgprefixlen = 0;
+ if (msgin == "") {
+ msgstr = cmsgstr = "";
+ } else {
+ if(privatesay)
+ {
+ msgstr = strcat("\{1}\{13}* ", colorprefix, namestr, "^3 tells you: ^7");
+ privatemsgprefixlen = strlen(msgstr);
+ msgstr = strcat(msgstr, msgin);
+ cmsgstr = strcat(colorstr, colorprefix, namestr, "^3 tells you:\n^7", msgin);
+ if(autocvar_g_chat_teamcolors)
+ privatemsgprefix = strcat("\{1}\{13}* ^3You tell ", playername(privatesay), ": ^7");
+ else
+ privatemsgprefix = strcat("\{1}\{13}* ^3You tell ", privatesay.netname, ": ^7");
+ }
+ else if(teamsay)
+ {
+ if(strstrofs(msgin, "/me", 0) >= 0)
+ {
+ //msgin = strreplace("/me", "", msgin);
+ //msgin = substring(msgin, 3, strlen(msgin));
+ msgin = strreplace("/me", strcat(colorstr, "(", colorprefix, namestr, colorstr, ")^7"), msgin);
+ msgstr = strcat("\{1}\{13}^4* ", "^7", msgin);
+ }
+ else
+ msgstr = strcat("\{1}\{13}", colorstr, "(", colorprefix, namestr, colorstr, ") ^7", msgin);
+ cmsgstr = strcat(colorstr, "(", colorprefix, namestr, colorstr, ")\n^7", msgin);
+ }
+ else
+ {
+ if(strstrofs(msgin, "/me", 0) >= 0)
+ {
+ //msgin = strreplace("/me", "", msgin);
+ //msgin = substring(msgin, 3, strlen(msgin));
+ msgin = strreplace("/me", strcat(colorprefix, namestr), msgin);
+ msgstr = strcat("\{1}^4* ", "^7", msgin);
+ }
+ else {
+ msgstr = "\{1}";
+ msgstr = strcat(msgstr, (namestr != "") ? strcat(colorprefix, namestr, "^7: ") : "^7");
+ msgstr = strcat(msgstr, msgin);
+ }
+ cmsgstr = "";
+ }
+ msgstr = strcat(strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint
+ }
+
+ string fullmsgstr = msgstr;
+ string fullcmsgstr = cmsgstr;
+
+ // FLOOD CONTROL
+ int flood = 0;
+ var .float flood_field = floodcontrol_chat;
+ if(floodcontrol)
+ {
+ float flood_spl;
+ float flood_burst;
+ float flood_lmax;
+ float lines;
+ if(privatesay)
+ {
+ flood_spl = autocvar_g_chat_flood_spl_tell;
+ flood_burst = autocvar_g_chat_flood_burst_tell;
+ flood_lmax = autocvar_g_chat_flood_lmax_tell;
+ flood_field = floodcontrol_chattell;
+ }
+ else if(teamsay)
+ {
+ flood_spl = autocvar_g_chat_flood_spl_team;
+ flood_burst = autocvar_g_chat_flood_burst_team;
+ flood_lmax = autocvar_g_chat_flood_lmax_team;
+ flood_field = floodcontrol_chatteam;
+ }
+ else
+ {
+ flood_spl = autocvar_g_chat_flood_spl;
+ flood_burst = autocvar_g_chat_flood_burst;
+ flood_lmax = autocvar_g_chat_flood_lmax;
+ flood_field = floodcontrol_chat;
+ }
+ flood_burst = max(0, flood_burst - 1);
+ // to match explanation in default.cfg, a value of 3 must allow three-line bursts and not four!
+
+ // do flood control for the default line size
+ if(msgstr != "")
+ {
+ getWrappedLine_remaining = msgstr;
+ msgstr = "";
+ lines = 0;
+ while(getWrappedLine_remaining && (!flood_lmax || lines <= flood_lmax))
+ {
+ msgstr = strcat(msgstr, " ", getWrappedLineLen(82.4289758859709, strlennocol)); // perl averagewidth.pl < gfx/vera-sans.width
+ ++lines;
+ }
+ msgstr = substring(msgstr, 1, strlen(msgstr) - 1);
+
+ if(getWrappedLine_remaining != "")
+ {
+ msgstr = strcat(msgstr, "\n");
+ flood = 2;
+ }
+
+ if (time >= source.(flood_field))
+ {
+ source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + lines * flood_spl;
+ }
+ else
+ {
+ flood = 1;
+ msgstr = fullmsgstr;
+ }
+ }
+ else
+ {
+ if (time >= source.(flood_field))
+ source.(flood_field) = max(time - flood_burst * flood_spl, source.(flood_field)) + flood_spl;
+ else
+ flood = 1;
+ }
+
+ if (timeout_status == TIMEOUT_ACTIVE) // when game is paused, no flood protection
+ source.(flood_field) = flood = 0;
+ }
+
+ string sourcemsgstr, sourcecmsgstr;
+ if(flood == 2) // cannot happen for empty msgstr
+ {
+ if(autocvar_g_chat_flood_notify_flooder)
+ {
+ sourcemsgstr = strcat(msgstr, "\n^3FLOOD CONTROL: ^7message too long, trimmed\n");
+ sourcecmsgstr = "";
+ }
+ else
+ {
+ sourcemsgstr = fullmsgstr;
+ sourcecmsgstr = fullcmsgstr;
+ }
+ cmsgstr = "";
+ }
+ else
+ {
+ sourcemsgstr = msgstr;
+ sourcecmsgstr = cmsgstr;
+ }
+
+ if (!privatesay && source && !IS_PLAYER(source))
+ {
+ if (!intermission_running)
+ if(teamsay || (autocvar_g_chat_nospectators == 1) || (autocvar_g_chat_nospectators == 2 && !(warmup_stage || gameover)))
+ teamsay = -1; // spectators
+ }
+
+ if(flood)
+ LOG_INFO("NOTE: ", playername(source), "^7 is flooding.\n");
+
+ // build sourcemsgstr by cutting off a prefix and replacing it by the other one
+ if(privatesay)
+ sourcemsgstr = strcat(privatemsgprefix, substring(sourcemsgstr, privatemsgprefixlen, -1));
+
+ int ret;
+ if(source.muted)
+ {
+ // always fake the message
+ ret = -1;
+ }
+ else if(flood == 1)
+ {
+ if (autocvar_g_chat_flood_notify_flooder)
+ {
+ sprint(source, strcat("^3FLOOD CONTROL: ^7wait ^1", ftos(source.(flood_field) - time), "^3 seconds\n"));
+ ret = 0;
+ }
+ else
+ ret = -1;
+ }
+ else
+ {
+ ret = 1;
+ }
+
+ if(sourcemsgstr != "" && ret != 0)
+ {
+ if(ret < 0) // faked message, because the player is muted
+ {
+ sprint(source, sourcemsgstr);
+ if(sourcecmsgstr != "" && !privatesay)
+ centerprint(source, sourcecmsgstr);
+ }
+ else if(privatesay) // private message, between 2 people only
+ {
+ sprint(source, sourcemsgstr);
+ sprint(privatesay, msgstr);
+ if (!autocvar_g_chat_tellprivacy) { dedicated_print(msgstr); } // send to server console too if "tellprivacy" is disabled
+ if(cmsgstr != "")
+ centerprint(privatesay, cmsgstr);
+ }
+ else if ( teamsay && source.active_minigame )
+ {
+ sprint(source, sourcemsgstr);
+ dedicated_print(msgstr); // send to server console too
+ FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source && it.active_minigame == source.active_minigame, sprint(it, msgstr));
+ }
+ else if(teamsay > 0) // team message, only sent to team mates
+ {
+ sprint(source, sourcemsgstr);
+ dedicated_print(msgstr); // send to server console too
+ if(sourcecmsgstr != "")
+ centerprint(source, sourcecmsgstr);
+ FOREACH_CLIENT(IS_PLAYER(it) && IS_REAL_CLIENT(it) && it != source && it.team == source.team, {
+ sprint(it, msgstr);
+ if(cmsgstr != "")
+ centerprint(it, cmsgstr);
+ });
+ }
+ else if(teamsay < 0) // spectator message, only sent to spectators
+ {
+ sprint(source, sourcemsgstr);
+ dedicated_print(msgstr); // send to server console too
+ FOREACH_CLIENT(!IS_PLAYER(it) && IS_REAL_CLIENT(it) && it != source, sprint(it, msgstr));
+ }
+ else
+ {
+ if (source) {
+ sprint(source, sourcemsgstr);
+ dedicated_print(msgstr); // send to server console too
+ MX_Say(strcat(playername(source), "^7: ", msgin));
+ }
+ FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != source, sprint(it, msgstr));
+ }
+ }
+
+ return ret;
+}