]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_domination.qc
Merge branch 'master' into terencehill/hud_cleanups
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_domination.qc
1 #include "gamemode_domination.qh"
2
3 #include "gamemode.qh"
4
5 #include "../teamplay.qh"
6
7 void dom_EventLog(string mode, float team_before, entity actor) // use an alias for easy changing and quick editing later
8 {
9         if(autocvar_sv_eventlog)
10                 GameLogEcho(strcat(":dom:", mode, ":", ftos(team_before), ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
11 }
12
13 void set_dom_state(entity e)
14 {
15         e.dom_total_pps = total_pps;
16         e.dom_pps_red = pps_red;
17         e.dom_pps_blue = pps_blue;
18         if(domination_teams >= 3)
19                 e.dom_pps_yellow = pps_yellow;
20         if(domination_teams >= 4)
21                 e.dom_pps_pink = pps_pink;
22 }
23
24 void dompoint_captured ()
25 {SELFPARAM();
26         entity head;
27         float old_delay, old_team, real_team;
28
29         // now that the delay has expired, switch to the latest team to lay claim to this point
30         head = self.owner;
31
32         real_team = self.cnt;
33         self.cnt = -1;
34
35         dom_EventLog("taken", self.team, self.dmg_inflictor);
36         self.dmg_inflictor = world;
37
38         self.goalentity = head;
39         self.model = head.mdl;
40         self.modelindex = head.dmg;
41         self.skin = head.skin;
42
43         float points, wait_time;
44         if (autocvar_g_domination_point_amt)
45                 points = autocvar_g_domination_point_amt;
46         else
47                 points = self.frags;
48         if (autocvar_g_domination_point_rate)
49                 wait_time = autocvar_g_domination_point_rate;
50         else
51                 wait_time = self.wait;
52
53         if(domination_roundbased)
54                 bprint(sprintf("^3%s^3%s\n", head.netname, self.message));
55         else
56                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_DOMINATION_CAPTURE_TIME, head.netname, self.message, points, wait_time);
57
58         if(self.enemy.playerid == self.enemy_playerid)
59                 PlayerScore_Add(self.enemy, SP_DOM_TAKES, 1);
60         else
61                 self.enemy = world;
62
63         if (head.noise != "")
64                 if(self.enemy)
65                         _sound(self.enemy, CH_TRIGGER, head.noise, VOL_BASE, ATTEN_NORM);
66                 else
67                         _sound(self, CH_TRIGGER, head.noise, VOL_BASE, ATTEN_NORM);
68         if (head.noise1 != "")
69                 play2all(head.noise1);
70
71         self.delay = time + wait_time;
72
73         // do trigger work
74         old_delay = self.delay;
75         old_team = self.team;
76         self.team = real_team;
77         self.delay = 0;
78         activator = self;
79         SUB_UseTargets ();
80         self.delay = old_delay;
81         self.team = old_team;
82
83         entity msg = WP_DomNeut;
84         switch(self.team)
85         {
86                 case NUM_TEAM_1: msg = WP_DomRed; break;
87                 case NUM_TEAM_2: msg = WP_DomBlue; break;
88                 case NUM_TEAM_3: msg = WP_DomYellow; break;
89                 case NUM_TEAM_4: msg = WP_DomPink; break;
90         }
91
92         WaypointSprite_UpdateSprites(self.sprite, msg, WP_Null, WP_Null);
93
94         total_pps = 0, pps_red = 0, pps_blue = 0, pps_yellow = 0, pps_pink = 0;
95         for(head = world; (head = find(head, classname, "dom_controlpoint")) != world; )
96         {
97                 if (autocvar_g_domination_point_amt)
98                         points = autocvar_g_domination_point_amt;
99                 else
100                         points = head.frags;
101                 if (autocvar_g_domination_point_rate)
102                         wait_time = autocvar_g_domination_point_rate;
103                 else
104                         wait_time = head.wait;
105                 switch(head.goalentity.team)
106                 {
107                         case NUM_TEAM_1:
108                                 pps_red += points/wait_time;
109                                 break;
110                         case NUM_TEAM_2:
111                                 pps_blue += points/wait_time;
112                                 break;
113                         case NUM_TEAM_3:
114                                 pps_yellow += points/wait_time;
115                                 break;
116                         case NUM_TEAM_4:
117                                 pps_pink += points/wait_time;
118                                 break;
119                 }
120                 total_pps += points/wait_time;
121         }
122
123         WaypointSprite_UpdateTeamRadar(self.sprite, RADARICON_DOMPOINT, colormapPaletteColor(self.goalentity.team - 1, 0));
124         WaypointSprite_Ping(self.sprite);
125
126         self.captime = time;
127
128         FOR_EACH_REALCLIENT(head)
129                 set_dom_state(head);
130 }
131
132 void AnimateDomPoint()
133 {SELFPARAM();
134         if(self.pain_finished > time)
135                 return;
136         self.pain_finished = time + self.t_width;
137         if(self.nextthink > self.pain_finished)
138                 self.nextthink = self.pain_finished;
139
140         self.frame = self.frame + 1;
141         if(self.frame > self.t_length)
142                 self.frame = 0;
143 }
144
145 void dompointthink()
146 {SELFPARAM();
147         float fragamt;
148
149         self.nextthink = time + 0.1;
150
151         //self.frame = self.frame + 1;
152         //if(self.frame > 119)
153         //      self.frame = 0;
154         AnimateDomPoint();
155
156         // give points
157
158         if (gameover || self.delay > time || time < game_starttime)     // game has ended, don't keep giving points
159                 return;
160
161         if(autocvar_g_domination_point_rate)
162                 self.delay = time + autocvar_g_domination_point_rate;
163         else
164                 self.delay = time + self.wait;
165
166         // give credit to the team
167         // NOTE: this defaults to 0
168         if (!domination_roundbased)
169         if (self.goalentity.netname != "")
170         {
171                 if(autocvar_g_domination_point_amt)
172                         fragamt = autocvar_g_domination_point_amt;
173                 else
174                         fragamt = self.frags;
175                 TeamScore_AddToTeam(self.goalentity.team, ST_SCORE, fragamt);
176                 TeamScore_AddToTeam(self.goalentity.team, ST_DOM_TICKS, fragamt);
177
178                 // give credit to the individual player, if he is still there
179                 if (self.enemy.playerid == self.enemy_playerid)
180                 {
181                         PlayerScore_Add(self.enemy, SP_SCORE, fragamt);
182                         PlayerScore_Add(self.enemy, SP_DOM_TICKS, fragamt);
183                 }
184                 else
185                         self.enemy = world;
186         }
187 }
188
189 void dompointtouch()
190 {SELFPARAM();
191         entity head;
192         if (!IS_PLAYER(other))
193                 return;
194         if (other.health < 1)
195                 return;
196
197         if(round_handler_IsActive() && !round_handler_IsRoundStarted())
198                 return;
199
200         if(time < self.captime + 0.3)
201                 return;
202
203         // only valid teams can claim it
204         head = find(world, classname, "dom_team");
205         while (head && head.team != other.team)
206                 head = find(head, classname, "dom_team");
207         if (!head || head.netname == "" || head == self.goalentity)
208                 return;
209
210         // delay capture
211
212         self.team = self.goalentity.team; // this stores the PREVIOUS team!
213
214         self.cnt = other.team;
215         self.owner = head; // team to switch to after the delay
216         self.dmg_inflictor = other;
217
218         // self.state = 1;
219         // self.delay = time + cvar("g_domination_point_capturetime");
220         //self.nextthink = time + cvar("g_domination_point_capturetime");
221         //self.think = dompoint_captured;
222
223         // go to neutral team in the mean time
224         head = find(world, classname, "dom_team");
225         while (head && head.netname != "")
226                 head = find(head, classname, "dom_team");
227         if(head == world)
228                 return;
229
230         WaypointSprite_UpdateSprites(self.sprite, WP_DomNeut, WP_Null, WP_Null);
231         WaypointSprite_UpdateTeamRadar(self.sprite, RADARICON_DOMPOINT, '0 1 1');
232         WaypointSprite_Ping(self.sprite);
233
234         self.goalentity = head;
235         self.model = head.mdl;
236         self.modelindex = head.dmg;
237         self.skin = head.skin;
238
239         self.enemy = other; // individual player scoring
240         self.enemy_playerid = other.playerid;
241         dompoint_captured();
242 }
243
244 void dom_controlpoint_setup()
245 {SELFPARAM();
246         entity head;
247         // find the spawnfunc_dom_team representing unclaimed points
248         head = find(world, classname, "dom_team");
249         while(head && head.netname != "")
250                 head = find(head, classname, "dom_team");
251         if (!head)
252                 objerror("no spawnfunc_dom_team with netname \"\" found\n");
253
254         // copy important properties from spawnfunc_dom_team entity
255         self.goalentity = head;
256         _setmodel(self, head.mdl); // precision already set
257         self.skin = head.skin;
258
259         self.cnt = -1;
260
261         if(self.message == "")
262                 self.message = " has captured a control point";
263
264         if(self.frags <= 0)
265                 self.frags = 1;
266         if(self.wait <= 0)
267                 self.wait = 5;
268
269         float points, waittime;
270         if (autocvar_g_domination_point_amt)
271                 points = autocvar_g_domination_point_amt;
272         else
273                 points = self.frags;
274         if (autocvar_g_domination_point_rate)
275                 waittime = autocvar_g_domination_point_rate;
276         else
277                 waittime = self.wait;
278
279         total_pps += points/waittime;
280
281         if(!self.t_width)
282                 self.t_width = 0.02; // frame animation rate
283         if(!self.t_length)
284                 self.t_length = 239; // maximum frame
285
286         self.think = dompointthink;
287         self.nextthink = time;
288         self.touch = dompointtouch;
289         self.solid = SOLID_TRIGGER;
290         self.flags = FL_ITEM;
291         setsize(self, '-32 -32 -32', '32 32 32');
292         setorigin(self, self.origin + '0 0 20');
293         droptofloor();
294
295         waypoint_spawnforitem(self);
296         WaypointSprite_SpawnFixed(WP_DomNeut, self.origin + '0 0 32', self, sprite, RADARICON_DOMPOINT);
297 }
298
299 float total_controlpoints;
300 void Domination_count_controlpoints()
301 {
302         entity e;
303         total_controlpoints = redowned = blueowned = yellowowned = pinkowned = 0;
304         for(e = world; (e = find(e, classname, "dom_controlpoint")) != world; )
305         {
306                 ++total_controlpoints;
307                 redowned += (e.goalentity.team == NUM_TEAM_1);
308                 blueowned += (e.goalentity.team == NUM_TEAM_2);
309                 yellowowned += (e.goalentity.team == NUM_TEAM_3);
310                 pinkowned += (e.goalentity.team == NUM_TEAM_4);
311         }
312 }
313
314 float Domination_GetWinnerTeam()
315 {
316         float winner_team = 0;
317         if(redowned == total_controlpoints)
318                 winner_team = NUM_TEAM_1;
319         if(blueowned == total_controlpoints)
320         {
321                 if(winner_team) return 0;
322                 winner_team = NUM_TEAM_2;
323         }
324         if(yellowowned == total_controlpoints)
325         {
326                 if(winner_team) return 0;
327                 winner_team = NUM_TEAM_3;
328         }
329         if(pinkowned == total_controlpoints)
330         {
331                 if(winner_team) return 0;
332                 winner_team = NUM_TEAM_4;
333         }
334         if(winner_team)
335                 return winner_team;
336         return -1; // no control points left?
337 }
338
339 #define DOM_OWNED_CONTROLPOINTS() ((redowned > 0) + (blueowned > 0) + (yellowowned > 0) + (pinkowned > 0))
340 #define DOM_OWNED_CONTROLPOINTS_OK() (DOM_OWNED_CONTROLPOINTS() < total_controlpoints)
341 float Domination_CheckWinner()
342 {
343         if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
344         {
345                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER);
346                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER);
347                 round_handler_Init(5, autocvar_g_domination_warmup, autocvar_g_domination_round_timelimit);
348                 return 1;
349         }
350
351         Domination_count_controlpoints();
352
353         float winner_team = Domination_GetWinnerTeam();
354
355         if(winner_team == -1)
356                 return 0;
357
358         if(winner_team > 0)
359         {
360                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner_team, CENTER_ROUND_TEAM_WIN_));
361                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner_team, INFO_ROUND_TEAM_WIN_));
362                 TeamScore_AddToTeam(winner_team, ST_DOM_CAPS, +1);
363         }
364         else if(winner_team == -1)
365         {
366                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_TIED);
367                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_TIED);
368         }
369
370         round_handler_Init(5, autocvar_g_domination_warmup, autocvar_g_domination_round_timelimit);
371
372         return 1;
373 }
374
375 float Domination_CheckPlayers()
376 {
377         return 1;
378 }
379
380 void Domination_RoundStart()
381 {
382         entity e;
383         FOR_EACH_PLAYER(e)
384                 e.player_blocked = 0;
385 }
386
387 //go to best items, or control points you don't own
388 void havocbot_role_dom()
389 {SELFPARAM();
390         if(self.deadflag != DEAD_NO)
391                 return;
392
393         if (self.bot_strategytime < time)
394         {
395                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
396                 navigation_goalrating_start();
397                 havocbot_goalrating_controlpoints(10000, self.origin, 15000);
398                 havocbot_goalrating_items(8000, self.origin, 8000);
399                 //havocbot_goalrating_enemyplayers(3000, self.origin, 2000);
400                 //havocbot_goalrating_waypoints(1, self.origin, 1000);
401                 navigation_goalrating_end();
402         }
403 }
404
405 MUTATOR_HOOKFUNCTION(dom_GetTeamCount)
406 {
407         ret_float = domination_teams;
408         return 0;
409 }
410
411 MUTATOR_HOOKFUNCTION(dom_ResetMap)
412 {SELFPARAM();
413         total_pps = 0, pps_red = 0, pps_blue = 0, pps_yellow = 0, pps_pink = 0;
414         entity e;
415         FOR_EACH_PLAYER(e)
416         {
417                 setself(e);
418                 PutClientInServer();
419                 self.player_blocked = 1;
420                 if(IS_REAL_CLIENT(self))
421                         set_dom_state(self);
422         }
423         return 1;
424 }
425
426 MUTATOR_HOOKFUNCTION(dom_PlayerSpawn)
427 {SELFPARAM();
428         if(domination_roundbased)
429         if(!round_handler_IsRoundStarted())
430                 self.player_blocked = 1;
431         else
432                 self.player_blocked = 0;
433         return false;
434 }
435
436 MUTATOR_HOOKFUNCTION(dom_ClientConnect)
437 {SELFPARAM();
438         set_dom_state(self);
439         return false;
440 }
441
442 MUTATOR_HOOKFUNCTION(dom_BotRoles)
443 {SELFPARAM();
444         self.havocbot_role = havocbot_role_dom;
445         return true;
446 }
447
448 /*QUAKED spawnfunc_dom_controlpoint (0 .5 .8) (-16 -16 -24) (16 16 32)
449 Control point for Domination gameplay.
450 */
451 spawnfunc(dom_controlpoint)
452 {
453         if(!g_domination)
454         {
455                 remove(self);
456                 return;
457         }
458         self.think = dom_controlpoint_setup;
459         self.nextthink = time + 0.1;
460         self.reset = dom_controlpoint_setup;
461
462         if(!self.scale)
463                 self.scale = 0.6;
464
465         self.effects = self.effects | EF_LOWPRECISION;
466         if (autocvar_g_domination_point_fullbright)
467                 self.effects |= EF_FULLBRIGHT;
468 }
469
470 /*QUAKED spawnfunc_dom_team (0 .5 .8) (-32 -32 -24) (32 32 32)
471 Team declaration for Domination gameplay, this allows you to decide what team
472 names and control point models are used in your map.
473
474 Note: If you use spawnfunc_dom_team entities you must define at least 3 and only two
475 can have netname set!  The nameless team owns all control points at start.
476
477 Keys:
478 "netname"
479  Name of the team (for example Red Team, Blue Team, Green Team, Yellow Team, Life, Death, etc)
480 "cnt"
481  Scoreboard color of the team (for example 4 is red and 13 is blue)
482 "model"
483  Model to use for control points owned by this team (for example
484  "progs/b_g_key.mdl" is a gold keycard, and "progs/b_s_key.mdl" is a silver
485  keycard)
486 "skin"
487  Skin of the model to use (for team skins on a single model)
488 "noise"
489  Sound to play when this team captures a point.
490  (this is a localized sound, like a small alarm or other effect)
491 "noise1"
492  Narrator speech to play when this team captures a point.
493  (this is a global sound, like "Red team has captured a control point")
494 */
495
496 spawnfunc(dom_team)
497 {
498         if(!g_domination || autocvar_g_domination_teams_override >= 2)
499         {
500                 remove(self);
501                 return;
502         }
503         precache_model(self.model);
504         if (self.noise != "")
505                 precache_sound(self.noise);
506         if (self.noise1 != "")
507                 precache_sound(self.noise1);
508         self.classname = "dom_team";
509         _setmodel(self, self.model); // precision not needed
510         self.mdl = self.model;
511         self.dmg = self.modelindex;
512         self.model = "";
513         self.modelindex = 0;
514         // this would have to be changed if used in quakeworld
515         if(self.cnt)
516                 self.team = self.cnt + 1; // WHY are these different anyway?
517 }
518
519 // scoreboard setup
520 void ScoreRules_dom(float teams)
521 {
522         if(domination_roundbased)
523         {
524                 ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, 0, true);
525                 ScoreInfo_SetLabel_TeamScore  (ST_DOM_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
526                 ScoreInfo_SetLabel_PlayerScore(SP_DOM_TAKES, "takes", 0);
527                 ScoreRules_basics_end();
528         }
529         else
530         {
531                 float sp_domticks, sp_score;
532                 sp_score = sp_domticks = 0;
533                 if(autocvar_g_domination_disable_frags)
534                         sp_domticks = SFL_SORT_PRIO_PRIMARY;
535                 else
536                         sp_score = SFL_SORT_PRIO_PRIMARY;
537                 ScoreRules_basics(teams, sp_score, sp_score, true);
538                 ScoreInfo_SetLabel_TeamScore  (ST_DOM_TICKS,    "ticks",     sp_domticks);
539                 ScoreInfo_SetLabel_PlayerScore(SP_DOM_TICKS,    "ticks",     sp_domticks);
540                 ScoreInfo_SetLabel_PlayerScore(SP_DOM_TAKES,    "takes",     0);
541                 ScoreRules_basics_end();
542         }
543 }
544
545 // code from here on is just to support maps that don't have control point and team entities
546 void dom_spawnteam (string teamname, float teamcolor, string pointmodel, float pointskin, string capsound, string capnarration, string capmessage)
547 {SELFPARAM();
548         setself(spawn());
549         self.classname = "dom_team";
550         self.netname = teamname;
551         self.cnt = teamcolor;
552         self.model = pointmodel;
553         self.skin = pointskin;
554         self.noise = capsound;
555         self.noise1 = capnarration;
556         self.message = capmessage;
557
558         // this code is identical to spawnfunc_dom_team
559         _setmodel(self, self.model); // precision not needed
560         self.mdl = self.model;
561         self.dmg = self.modelindex;
562         self.model = "";
563         self.modelindex = 0;
564         // this would have to be changed if used in quakeworld
565         self.team = self.cnt + 1;
566
567         //eprint(self);
568         setself(this);
569 }
570
571 void _spawnfunc_dom_controlpoint() { SELFPARAM(); spawnfunc_dom_controlpoint(self); }
572 void dom_spawnpoint(vector org)
573 {SELFPARAM();
574         setself(spawn());
575         self.classname = "dom_controlpoint";
576         self.think = _spawnfunc_dom_controlpoint;
577         self.nextthink = time;
578         setorigin(self, org);
579         spawnfunc_dom_controlpoint(this);
580         setself(this);
581 }
582
583 // spawn some default teams if the map is not set up for domination
584 void dom_spawnteams(float teams)
585 {
586         dom_spawnteam("Red", NUM_TEAM_1-1, "models/domination/dom_red.md3", 0, SND(DOM_CLAIM), "", "Red team has captured a control point");
587         dom_spawnteam("Blue", NUM_TEAM_2-1, "models/domination/dom_blue.md3", 0, SND(DOM_CLAIM), "", "Blue team has captured a control point");
588         if(teams >= 3)
589                 dom_spawnteam("Yellow", NUM_TEAM_3-1, "models/domination/dom_yellow.md3", 0, SND(DOM_CLAIM), "", "Yellow team has captured a control point");
590         if(teams >= 4)
591                 dom_spawnteam("Pink", NUM_TEAM_4-1, "models/domination/dom_pink.md3", 0, SND(DOM_CLAIM), "", "Pink team has captured a control point");
592         dom_spawnteam("", 0, "models/domination/dom_unclaimed.md3", 0, "", "", "");
593 }
594
595 void dom_DelayedInit() // Do this check with a delay so we can wait for teams to be set up.
596 {
597         // if no teams are found, spawn defaults
598         if(find(world, classname, "dom_team") == world || autocvar_g_domination_teams_override >= 2)
599         {
600                 LOG_INFO("No ""dom_team"" entities found on this map, creating them anyway.\n");
601                 domination_teams = bound(2, ((autocvar_g_domination_teams_override < 2) ? autocvar_g_domination_default_teams : autocvar_g_domination_teams_override), 4);
602                 dom_spawnteams(domination_teams);
603         }
604
605         CheckAllowedTeams(world);
606         domination_teams = ((c4>=0) ? 4 : (c3>=0) ? 3 : 2);
607
608         addstat(STAT_DOM_TOTAL_PPS, AS_FLOAT, dom_total_pps);
609         addstat(STAT_DOM_PPS_RED, AS_FLOAT, dom_pps_red);
610         addstat(STAT_DOM_PPS_BLUE, AS_FLOAT, dom_pps_blue);
611         if(domination_teams >= 3) addstat(STAT_DOM_PPS_YELLOW, AS_FLOAT, dom_pps_yellow);
612         if(domination_teams >= 4) addstat(STAT_DOM_PPS_PINK, AS_FLOAT, dom_pps_pink);
613
614         domination_roundbased = autocvar_g_domination_roundbased;
615
616         ScoreRules_dom(domination_teams);
617
618         if(domination_roundbased)
619         {
620                 round_handler_Spawn(Domination_CheckPlayers, Domination_CheckWinner, Domination_RoundStart);
621                 round_handler_Init(5, autocvar_g_domination_warmup, autocvar_g_domination_round_timelimit);
622         }
623 }
624
625 void dom_Initialize()
626 {
627         InitializeEntity(world, dom_DelayedInit, INITPRIO_GAMETYPE);
628 }
629
630
631 MUTATOR_DEFINITION(gamemode_domination)
632 {
633         MUTATOR_HOOK(GetTeamCount, dom_GetTeamCount, CBC_ORDER_ANY);
634         MUTATOR_HOOK(reset_map_players, dom_ResetMap, CBC_ORDER_ANY);
635         MUTATOR_HOOK(PlayerSpawn, dom_PlayerSpawn, CBC_ORDER_ANY);
636         MUTATOR_HOOK(ClientConnect, dom_ClientConnect, CBC_ORDER_ANY);
637         MUTATOR_HOOK(HavocBot_ChooseRole, dom_BotRoles, CBC_ORDER_ANY);
638
639         MUTATOR_ONADD
640         {
641                 if(time > 1) // game loads at time 1
642                         error("This is a game type and it cannot be added at runtime.");
643                 dom_Initialize();
644         }
645
646         MUTATOR_ONREMOVE
647         {
648                 LOG_INFO("This is a game type and it cannot be removed at runtime.");
649                 return -1;
650         }
651
652         return 0;
653 }