1 #include "sv_assault.qh"
3 #include <common/mapobjects/func/breakable.qh>
6 #define AS_ROUND_DELAY 5
9 void assault_objective_use(entity this, entity actor, entity trigger)
12 SetResourceExplicit(this, RES_HEALTH, 100);
13 //print("^2Activated objective ", this.targetname, "=", etos(this), "\n");
14 //print("Activator is ", actor.classname, "\n");
16 IL_EACH(g_assault_objectivedecreasers, it.target == this.targetname,
18 target_objective_decrease_activate(it);
22 vector target_objective_spawn_evalfunc(entity this, entity player, entity spot, vector current)
24 float hlth = GetResource(this, RES_HEALTH);
25 if (hlth < 0 || hlth >= ASSAULT_VALUE_INACTIVE)
30 // reset this objective. Used when spawning an objective
31 // and when a new round starts
32 void assault_objective_reset(entity this)
34 SetResourceExplicit(this, RES_HEALTH, ASSAULT_VALUE_INACTIVE);
37 // decrease the health of targeted objectives
38 void assault_objective_decrease_use(entity this, entity actor, entity trigger)
40 if(actor.team != assault_attacker_team)
42 // wrong team triggered decrease
46 if(trigger.assault_sprite)
48 WaypointSprite_Disown(trigger.assault_sprite, waypointsprite_deadlifetime);
49 if(trigger.classname == "func_assault_destructible")
50 trigger.sprite = NULL; // TODO: just unsetting it?!
53 return; // already activated! cannot activate again!
55 float hlth = GetResource(this.enemy, RES_HEALTH);
56 if (hlth < ASSAULT_VALUE_INACTIVE)
58 if (hlth - this.dmg > 0.5)
60 GameRules_scoring_add_team(actor, SCORE, this.dmg);
61 TakeResource(this.enemy, RES_HEALTH, this.dmg);
65 GameRules_scoring_add_team(actor, SCORE, hlth);
66 GameRules_scoring_add_team(actor, ASSAULT_OBJECTIVES, 1);
67 SetResourceExplicit(this.enemy, RES_HEALTH, -1);
69 if(this.enemy.message)
70 FOREACH_CLIENT(IS_PLAYER(it), { centerprint(it, this.enemy.message); });
72 SUB_UseTargets(this.enemy, this, trigger);
77 void assault_setenemytoobjective(entity this)
79 IL_EACH(g_assault_objectives, it.targetname == this.target,
81 if(this.enemy == NULL)
84 objerror(this, "more than one objective as target - fix the map!");
88 if(this.enemy == NULL)
89 objerror(this, "no objective as target - fix the map!");
92 bool assault_decreaser_sprite_visible(entity this, entity player, entity view)
94 if(GetResource(this.assault_decreaser.enemy, RES_HEALTH) >= ASSAULT_VALUE_INACTIVE)
100 void target_objective_decrease_activate(entity this)
104 FOREACH_ENTITY_STRING(target, this.targetname,
106 if(it.assault_sprite != NULL)
108 WaypointSprite_Disown(it.assault_sprite, waypointsprite_deadlifetime);
109 if(it.classname == "func_assault_destructible")
110 it.sprite = NULL; // TODO: just unsetting it?!
113 spr = WaypointSprite_SpawnFixed(WP_AssaultDefend, 0.5 * (it.absmin + it.absmax), it, assault_sprite, RADARICON_OBJECTIVE);
114 spr.assault_decreaser = this;
115 spr.waypointsprite_visible_for_player = assault_decreaser_sprite_visible;
116 spr.classname = "sprite_waypoint";
117 WaypointSprite_UpdateRule(spr, assault_attacker_team, SPRITERULE_TEAMPLAY);
118 if(it.classname == "func_assault_destructible")
120 WaypointSprite_UpdateSprites(spr, WP_AssaultDefend, WP_AssaultDestroy, WP_AssaultDestroy);
121 WaypointSprite_UpdateMaxHealth(spr, it.max_health);
122 WaypointSprite_UpdateHealth(spr, GetResource(it, RES_HEALTH));
126 WaypointSprite_UpdateSprites(spr, WP_AssaultDefend, WP_AssaultPush, WP_AssaultPush);
130 void target_objective_decrease_findtarget(entity this)
132 assault_setenemytoobjective(this);
135 void target_assault_roundend_reset(entity this)
137 //print("round end reset\n");
138 ++this.cnt; // up round counter
139 this.winning = false; // up round
142 void target_assault_roundend_use(entity this, entity actor, entity trigger)
144 this.winning = 1; // round has been won by attackers
147 void assault_roundstart_use(entity this, entity actor, entity trigger)
149 SUB_UseTargets(this, this, trigger);
151 //(Re)spawn all turrets
152 IL_EACH(g_turrets, true,
155 if(it.team == NUM_TEAM_1)
156 it.team = NUM_TEAM_2;
158 it.team = NUM_TEAM_1;
160 // Doubles as teamchange
164 void assault_roundstart_use_this(entity this)
166 assault_roundstart_use(this, NULL, NULL);
169 void assault_wall_think(entity this)
171 if(GetResource(this.enemy, RES_HEALTH) < 0)
174 this.solid = SOLID_NOT;
178 this.model = this.mdl;
179 this.solid = SOLID_BSP;
182 this.nextthink = time + 0.2;
186 // reset objectives, toggle spawnpoints, reset triggers, ...
187 void assault_new_round(entity this)
189 //bprint("ASSAULT: new round\n");
192 this.winning = this.winning + 1;
194 // swap attacker/defender roles
195 if(assault_attacker_team == NUM_TEAM_1)
196 assault_attacker_team = NUM_TEAM_2;
198 assault_attacker_team = NUM_TEAM_1;
200 IL_EACH(g_saved_team, !IS_CLIENT(it),
202 if(it.team_saved == NUM_TEAM_1)
203 it.team_saved = NUM_TEAM_2;
204 else if(it.team_saved == NUM_TEAM_2)
205 it.team_saved = NUM_TEAM_1;
208 // reset the level with a countdown
209 cvar_set("timelimit", ftos(ceil(time - AS_ROUND_DELAY - game_starttime) / 60));
210 ReadyRestart_force(); // sets game_starttime
215 void as_round_think()
217 game_stopped = false;
218 assault_new_round(as_round.ent_winning);
223 // Assault winning condition: If the attackers triggered a round end (by fulfilling all objectives)
224 // they win. Otherwise the defending team wins once the timelimit passes.
225 int WinningCondition_Assault()
230 WinningConditionHelper(NULL); // set worldstatus
232 int status = WINNING_NO;
233 // as the timelimit has not yet passed just assume the defending team will win
234 if(assault_attacker_team == NUM_TEAM_1)
236 SetWinners(team, NUM_TEAM_2);
240 SetWinners(team, NUM_TEAM_1);
244 ent = find(NULL, classname, "target_assault_roundend");
247 if(ent.winning) // round end has been triggered by attacking team
249 bprint("Assault: round completed.\n");
250 SetWinners(team, assault_attacker_team);
252 TeamScore_AddToTeam(assault_attacker_team, ST_ASSAULT_OBJECTIVES, 666 - TeamScore_AddToTeam(assault_attacker_team, ST_ASSAULT_OBJECTIVES, 0));
254 if(ent.cnt == 1 || autocvar_g_campaign) // this was the second round
256 status = WINNING_YES;
260 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ASSAULT_OBJ_DESTROYED, ceil(time - game_starttime));
261 as_round = new(as_round);
262 as_round.think = as_round_think;
263 as_round.ent_winning = ent;
264 as_round.nextthink = time + AS_ROUND_DELAY;
267 // make sure timelimit isn't hit while the game is blocked
268 if(autocvar_timelimit > 0)
269 if(time + AS_ROUND_DELAY >= game_starttime + autocvar_timelimit * 60)
270 cvar_set("timelimit", ftos(autocvar_timelimit + AS_ROUND_DELAY / 60));
279 spawnfunc(info_player_attacker)
281 if (!g_assault) { delete(this); return; }
283 this.team = NUM_TEAM_1; // red, gets swapped every round
284 spawnfunc_info_player_deathmatch(this);
287 spawnfunc(info_player_defender)
289 if (!g_assault) { delete(this); return; }
291 this.team = NUM_TEAM_2; // blue, gets swapped every round
292 spawnfunc_info_player_deathmatch(this);
295 spawnfunc(target_objective)
297 if (!g_assault) { delete(this); return; }
299 this.classname = "target_objective";
300 IL_PUSH(g_assault_objectives, this);
301 this.use = assault_objective_use;
302 this.reset = assault_objective_reset;
304 this.spawn_evalfunc = target_objective_spawn_evalfunc;
307 spawnfunc(target_objective_decrease)
309 if (!g_assault) { delete(this); return; }
311 this.classname = "target_objective_decrease";
312 IL_PUSH(g_assault_objectivedecreasers, this);
317 this.use = assault_objective_decrease_use;
318 SetResourceExplicit(this, RES_HEALTH, ASSAULT_VALUE_INACTIVE);
319 this.max_health = ASSAULT_VALUE_INACTIVE;
322 InitializeEntity(this, target_objective_decrease_findtarget, INITPRIO_FINDTARGET);
325 // destructible walls that can be used to trigger target_objective_decrease
326 bool destructible_heal(entity targ, entity inflictor, float amount, float limit)
328 float true_limit = ((limit != RES_LIMIT_NONE) ? limit : targ.max_health);
329 float hlth = GetResource(targ, RES_HEALTH);
330 if (hlth <= 0 || hlth >= true_limit)
333 GiveResourceWithLimit(targ, RES_HEALTH, amount, true_limit);
336 WaypointSprite_UpdateHealth(targ.sprite, GetResource(targ, RES_HEALTH));
338 func_breakable_colormod(targ);
342 spawnfunc(func_assault_destructible)
344 if (!g_assault) { delete(this); return; }
347 this.classname = "func_assault_destructible";
348 this.event_heal = destructible_heal;
349 IL_PUSH(g_assault_destructibles, this);
351 if(assault_attacker_team == NUM_TEAM_1)
352 this.team = NUM_TEAM_2;
354 this.team = NUM_TEAM_1;
356 spawnfunc_func_breakable(this);
359 spawnfunc(func_assault_wall)
361 if (!g_assault) { delete(this); return; }
363 this.classname = "func_assault_wall";
364 this.mdl = this.model;
365 _setmodel(this, this.mdl);
366 this.solid = SOLID_BSP;
367 setthink(this, assault_wall_think);
368 this.nextthink = time;
369 InitializeEntity(this, assault_setenemytoobjective, INITPRIO_FINDTARGET);
372 spawnfunc(target_assault_roundend)
374 if (!g_assault) { delete(this); return; }
376 this.winning = 0; // round not yet won by attackers
377 this.classname = "target_assault_roundend";
378 this.use = target_assault_roundend_use;
379 this.cnt = 0; // first round
380 this.reset = target_assault_roundend_reset;
383 spawnfunc(target_assault_roundstart)
385 if (!g_assault) { delete(this); return; }
387 assault_attacker_team = NUM_TEAM_1;
388 this.classname = "target_assault_roundstart";
389 this.use = assault_roundstart_use;
390 this.reset2 = assault_roundstart_use_this;
391 InitializeEntity(this, assault_roundstart_use_this, INITPRIO_FINDTARGET);
395 void havocbot_goalrating_ast_targets(entity this, float ratingscale)
397 IL_EACH(g_assault_destructibles, it.bot_attack,
404 IL_EACH(g_assault_objectivedecreasers, it.targetname == destr.target,
406 float hlth = GetResource(it.enemy, RES_HEALTH);
407 if (hlth > 0 && hlth < ASSAULT_VALUE_INACTIVE)
417 vector p = 0.5 * (it.absmin + it.absmax);
419 // Find and rate waypoints around it
422 float bestvalue = FLOAT_MAX;
424 for (float radius = 500; radius <= 1500 && !found; radius += 500)
426 FOREACH_ENTITY_RADIUS(p, radius, it.classname == "waypoint" && !(it.wpflags & WAYPOINTFLAG_GENERATED),
428 if(checkpvs(it.origin, des))
431 if(it.cnt < bestvalue)
442 /// dprint("waypoints around target were found\n");
443 // te_lightning2(NULL, '0 0 0', best.origin);
444 // te_knightspike(best.origin);
446 navigation_routerating(this, best, ratingscale, 4000);
449 this.havocbot_attack_time = 0;
451 if(checkpvs(this.origin + this.view_ofs, it))
452 if(checkpvs(this.origin + this.view_ofs, best))
454 // dprint("increasing attack time for this target\n");
455 this.havocbot_attack_time = time + 2;
461 void havocbot_role_ast_offense(entity this)
465 this.havocbot_attack_time = 0;
466 havocbot_ast_reset_role(this);
470 // Set the role timeout if necessary
471 if (!this.havocbot_role_timeout)
472 this.havocbot_role_timeout = time + 120;
474 if (time > this.havocbot_role_timeout)
476 havocbot_ast_reset_role(this);
480 if(this.havocbot_attack_time>time)
483 if (navigation_goalrating_timeout(this))
486 navigation_goalrating_start(this);
487 havocbot_goalrating_enemyplayers(this, 10000, this.origin, 650);
488 havocbot_goalrating_ast_targets(this, 20000);
489 havocbot_goalrating_items(this, 30000, this.origin, 10000);
490 navigation_goalrating_end(this);
492 navigation_goalrating_timeout_set(this);
496 void havocbot_role_ast_defense(entity this)
500 this.havocbot_attack_time = 0;
501 havocbot_ast_reset_role(this);
505 // Set the role timeout if necessary
506 if (!this.havocbot_role_timeout)
507 this.havocbot_role_timeout = time + 120;
509 if (time > this.havocbot_role_timeout)
511 havocbot_ast_reset_role(this);
515 if(this.havocbot_attack_time>time)
518 if (navigation_goalrating_timeout(this))
521 navigation_goalrating_start(this);
522 havocbot_goalrating_enemyplayers(this, 10000, this.origin, 3000);
523 havocbot_goalrating_ast_targets(this, 20000);
524 havocbot_goalrating_items(this, 30000, this.origin, 10000);
525 navigation_goalrating_end(this);
527 navigation_goalrating_timeout_set(this);
531 void havocbot_role_ast_setrole(entity this, float role)
535 case HAVOCBOT_AST_ROLE_DEFENSE:
536 this.havocbot_role = havocbot_role_ast_defense;
537 this.havocbot_role_timeout = 0;
539 case HAVOCBOT_AST_ROLE_OFFENSE:
540 this.havocbot_role = havocbot_role_ast_offense;
541 this.havocbot_role_timeout = 0;
546 void havocbot_ast_reset_role(entity this)
551 if(this.team == assault_attacker_team)
552 havocbot_role_ast_setrole(this, HAVOCBOT_AST_ROLE_OFFENSE);
554 havocbot_role_ast_setrole(this, HAVOCBOT_AST_ROLE_DEFENSE);
558 MUTATOR_HOOKFUNCTION(as, PlayerSpawn)
560 entity player = M_ARGV(0, entity);
562 if(player.team == assault_attacker_team)
563 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_ASSAULT_ATTACKING);
565 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_ASSAULT_DEFENDING);
568 MUTATOR_HOOKFUNCTION(as, TurretSpawn)
570 entity turret = M_ARGV(0, entity);
572 if(!turret.team || turret.team == FLOAT_MAX)
573 turret.team = assault_attacker_team; // this gets reversed when match starts (assault_roundstart_use)
576 MUTATOR_HOOKFUNCTION(as, VehicleInit)
578 entity veh = M_ARGV(0, entity);
580 veh.nextthink = time + 0.5;
583 MUTATOR_HOOKFUNCTION(as, HavocBot_ChooseRole)
585 entity bot = M_ARGV(0, entity);
587 havocbot_ast_reset_role(bot);
591 MUTATOR_HOOKFUNCTION(as, PlayHitsound)
593 entity frag_victim = M_ARGV(0, entity);
595 return (frag_victim.classname == "func_assault_destructible");
598 MUTATOR_HOOKFUNCTION(as, TeamBalance_CheckAllowedTeams)
600 // assault always has 2 teams
601 M_ARGV(0, float) = BIT(0) | BIT(1);
605 MUTATOR_HOOKFUNCTION(as, CheckRules_World)
607 M_ARGV(0, float) = WinningCondition_Assault();
611 MUTATOR_HOOKFUNCTION(as, ReadLevelCvars)
615 sv_ready_restart_after_countdown = 0;
618 MUTATOR_HOOKFUNCTION(as, OnEntityPreSpawn)
620 entity ent = M_ARGV(0, entity);
622 switch(ent.classname)
624 case "info_player_team1":
625 case "info_player_team2":
626 case "info_player_team3":
627 case "info_player_team4":
632 MUTATOR_HOOKFUNCTION(as, ReadyRestart_Deny)
634 // readyrestart not supported (yet)