1 /**
2 	Parkour
3 
4 	The goal is to be the first to reach the finish, the team or player to do so wins the round.
5 	Checkpoints can be added to make the path more interesting and more complex.
6 	Checkpoints can have different functionalities:
7 		* Respawn: On/Off - The clonk respawns at the last passed checkpoint.
8 		* Check: On/Off - The clonk must pass through these checkpoints before being able to finish.
9 		* Ordered: On/Off - The checkpoints mussed be passed in the order specified.
10 		* The start and finish are also checkpoints.
11 
12 	@author Maikel
13 */
14 
15 
16 #include Library_Goal
17 
18 local finished; // Whether the goal has been reached by some player.
19 local cp_list; // List of checkpoints.
20 local cp_count; // Number of checkpoints.
21 local respawn_list; // List of last reached respawn CP per player.
22 local plr_list; // Number of checkpoints the player completed.
23 local team_list; // Number of checkpoints the team completed.
24 local time_store; // String for best time storage in player file.
25 local no_respawn_handling; // Set to true if this goal should not handle respawn.
26 local transfer_contents; // Set to true if contents should be transferred on respawn.
27 
28 
29 /*-- General --*/
30 
Initialize(...)31 protected func Initialize(...)
32 {
33 	finished = false;
34 	no_respawn_handling = false;
35 	transfer_contents = false;
36 	cp_list = [];
37 	cp_count = 0;
38 	respawn_list = [];
39 	plr_list = [];
40 	team_list = [];
41 	// Best time tracking.
42 	time_store = Format("Parkour_%s_BestTime", GetScenTitle());
43 	AddEffect("IntBestTime", this, 100, 1, this);
44 	// Add a message board command "/resetpb" to reset the pb for this round.
45 	AddMsgBoardCmd("resetpb", "Goal_Parkour->~ResetPersonalBest(%player%)");
46 	// Activate restart rule, if there isn't any. But check delayed because it may be created later.
47 	ScheduleCall(this, this.EnsureRestartRule, 1, 1);
48 	// Scoreboard.
49 	InitScoreboard();
50 	// Assign unassigned checkpoints
51 	for (var obj in FindObjects(Find_ID(ParkourCheckpoint)))
52 		if (!obj->GetCPController())
53 			obj->SetCPController(this);
54 	return _inherited(...);
55 }
56 
EnsureRestartRule()57 private func EnsureRestartRule()
58 {
59 	var relaunch = GetRelaunchRule();
60 	relaunch->SetAllowPlayerRestart(true);
61 	relaunch->SetPerformRestart(false);
62 	return true;
63 }
64 
Destruction(...)65 protected func Destruction(...)
66 {
67 	// Unassign checkpoints (updates editor help message)
68 	for (var obj in FindObjects(Find_ID(ParkourCheckpoint)))
69 		if (obj->GetCPController() == this)
70 			obj->SetCPController(nil);
71 	return _inherited(...);
72 }
73 
74 
75 /*-- Checkpoint creation --*/
76 
SetStartpoint(int x,int y)77 public func SetStartpoint(int x, int y)
78 {
79 	// Safety, x and y inside landscape bounds.
80 	x = BoundBy(x, 0, LandscapeWidth());
81 	y = BoundBy(y, 0, LandscapeHeight());
82 	var cp = FindObject(Find_ID(ParkourCheckpoint), Find_Func("FindCPMode", PARKOUR_CP_Start));
83 	if (!cp)
84 		cp = CreateObjectAbove(ParkourCheckpoint, x, y, NO_OWNER);
85 	cp->SetCPController(this);
86 	cp->SetPosition(x, y);
87 	cp->SetCPMode(PARKOUR_CP_Start);
88 	return cp;
89 }
90 
SetFinishpoint(int x,int y,bool team)91 public func SetFinishpoint(int x, int y, bool team)
92 {
93 	// Safety, x and y inside landscape bounds.
94 	x = BoundBy(x, 0, LandscapeWidth());
95 	y = BoundBy(y, 0, LandscapeHeight());
96 	var cp = FindObject(Find_ID(ParkourCheckpoint), Find_Func("FindCPMode", PARKOUR_CP_Finish));
97 	if (!cp)
98 		cp = CreateObjectAbove(ParkourCheckpoint, x, y, NO_OWNER);
99 	cp->SetCPController(this);
100 	cp->SetPosition(x, y);
101 	var mode = PARKOUR_CP_Finish;
102 	if (team)
103 		mode = mode | PARKOUR_CP_Team;
104 	cp->SetCPMode(mode);
105 	return cp;
106 }
107 
AddCheckpoint(int x,int y,int mode)108 public func AddCheckpoint(int x, int y, int mode)
109 {
110 	// Safety, x and y inside landscape bounds.
111 	x = BoundBy(x, 0, LandscapeWidth());
112 	y = BoundBy(y, 0, LandscapeHeight());
113 	var cp = CreateObjectAbove(ParkourCheckpoint, x, y, NO_OWNER);
114 	cp->SetCPController(this);
115 	cp->SetPosition(x, y);
116 	cp->SetCPMode(mode);
117 	return cp;
118 }
119 
DisableRespawnHandling()120 public func DisableRespawnHandling()
121 {
122 	// Call this to disable respawn handling by goal. This might be useful if
123 	// a) you don't want any respawns, or
124 	// b) the scenario already provides an alternate respawn handling.
125 	no_respawn_handling = true;
126 	return true;
127 }
128 
TransferContentsOnRelaunch(bool on)129 public func TransferContentsOnRelaunch(bool on)
130 {
131 	transfer_contents = on;
132 	return;
133 }
134 
SetIndexedCP(object cp,int index)135 public func SetIndexedCP(object cp, int index)
136 {
137 	// Called directly from checkpoints after index assignment, resorting, etc.
138 	// Update internal list
139 	cp_list[index] = cp;
140 	if (cp->GetCPMode() & PARKOUR_CP_Finish)
141 	{
142 		cp_count = index;
143 		SetLength(cp_list, cp_count+1);
144 	}
145 	UpdateScoreboardTitle();
146 	return true;
147 }
148 
149 
150 /*-- Checkpoint interaction --*/
151 
152 // Called from a finish CP to indicate that plr has reached it.
PlayerReachedFinishCP(int plr,object cp,bool is_first_clear)153 public func PlayerReachedFinishCP(int plr, object cp, bool is_first_clear)
154 {
155 	if (finished)
156 		return;
157 	var plrid = GetPlayerID(plr);
158 	var team = GetPlayerTeam(plr);
159 	plr_list[plrid]++;
160 	if (team)
161 		team_list[team]++;
162 	UpdateScoreboard(plr);
163 	DoBestTime(plr);
164 	SetEvalData(plr);
165 	EliminatePlayers(plr);
166 	finished = true;
167 	if (is_first_clear) UserAction->EvaluateAction(on_checkpoint_first_cleared, this, cp, plr);
168 	UserAction->EvaluateAction(on_checkpoint_cleared, this, cp, plr);
169 	return;
170 }
171 
172 // Called from a respawn CP to indicate that plr has reached it.
SetPlayerRespawnCP(int plr,object cp)173 public func SetPlayerRespawnCP(int plr, object cp)
174 {
175 	if (respawn_list[plr] == cp)
176 		return;
177 	respawn_list[plr] = cp;
178 	cp->PlayerMessage(plr, "$MsgNewRespawn$");
179 	return;
180 }
181 
182 // Called from a check CP to indicate that plr has cleared it.
AddPlayerClearedCP(int plr,object cp,bool is_first_clear,bool is_team_auto_clear)183 public func AddPlayerClearedCP(int plr, object cp, bool is_first_clear, bool is_team_auto_clear)
184 {
185 	if (finished)
186 		return;
187 	var plrid = GetPlayerID(plr);
188 	plr_list[plrid]++;
189 	UpdateScoreboard(plr);
190 	if (!is_team_auto_clear) // No callback if only auto-cleared for other team members after another player cleared it
191 	{
192 		if (is_first_clear) UserAction->EvaluateAction(on_checkpoint_first_cleared, this, cp, plr);
193 		UserAction->EvaluateAction(on_checkpoint_cleared, this, cp, plr);
194 	}
195 	return;
196 }
197 
198 // Called from a check CP to indicate that plr has cleared it.
AddTeamClearedCP(int team,object cp)199 public func AddTeamClearedCP(int team, object cp)
200 {
201 	if (finished)
202 		return;
203 	if (team)
204 		team_list[team]++;
205 	return;
206 }
207 
ResetAllClearedCP()208 private func ResetAllClearedCP()
209 {
210 	plr_list = [];
211 	team_list = [];
212 	respawn_list = [];
213 	for (var cp in FindObjects(Find_ID(ParkourCheckpoint)))
214 		cp->ResetCleared();
215 	return true;
216 }
217 
218 
219 /*-- Goal interface --*/
220 
221 // Eliminates all players apart from the winner and his team.
EliminatePlayers(int winner)222 private func EliminatePlayers(int winner)
223 {
224 	var winteam = GetPlayerTeam(winner);
225 	for (var i = 0; i < GetPlayerCount(); i++)
226 	{
227 		var plr = GetPlayerByIndex(i);
228 		var team = GetPlayerTeam(plr);
229 		if (plr == winner) // The winner self.
230 			continue;
231 		if (team && team == winteam) // In the same team as the winner.
232 			continue;
233 		EliminatePlayer(plr);
234 	}
235 	return;
236 }
237 
IsFulfilled()238 public func IsFulfilled()
239 {
240 	return finished;
241 }
242 
GetDescription(int plr)243 public func GetDescription(int plr)
244 {
245 	var team = GetPlayerTeam(plr);
246 	var msg;
247 	if (finished)
248 	{
249 		if (team)
250 		{
251 			if (IsWinner(plr))
252 				msg = "$MsgParkourWonTeam$";
253 			else
254 				msg = "$MsgParkourLostTeam$";
255 		}
256 		else
257 		{
258 			if (IsWinner(plr))
259 				msg = "$MsgParkourWon$";
260 			else
261 				msg = "$MsgParkourLost$";
262 		}
263 	}
264 	else
265 		msg = Format("$MsgParkour$", cp_count);
266 
267 	return msg;
268 }
269 
Activate(int plr)270 public func Activate(int plr)
271 {
272 	var team = GetPlayerTeam(plr);
273 	var msg;
274 	if (finished)
275 	{
276 		if (team)
277 		{
278 			if (IsWinner(plr))
279 				msg = "$MsgParkourWonTeam$";
280 			else
281 				msg = "$MsgParkourLostTeam$";
282 		}
283 		else
284 		{
285 			if (IsWinner(plr))
286 				msg = "$MsgParkourWon$";
287 			else
288 				msg = "$MsgParkourLost$";
289 		}
290 	}
291 	else
292 		msg = Format("$MsgParkour$", cp_count);
293 	// Show goal message.
294 	MessageWindow(msg, plr);
295 	return;
296 }
297 
GetShortDescription(int plr)298 public func GetShortDescription(int plr)
299 {
300 	var team = GetPlayerTeam(plr);
301 	var parkour_length = GetParkourLength();
302 	if (parkour_length == 0)
303 		return "";
304 	var length;
305 	if (team)
306 		length = GetTeamPosition(team);
307 	else
308 		length = GetPlayerPosition(plr);
309 	var percentage =  100 * length / parkour_length;
310 	var red = BoundBy(255 - percentage * 255 / 100, 0, 255);
311 	var green = BoundBy(percentage * 255 / 100, 0, 255);
312 	var color = RGB(red, green, 0);
313 	return Format("<c %x>$MsgShortDesc$</c>", color, percentage, color);
314 }
315 
316 // Returns the length the player has completed.
GetPlayerPosition(int plr)317 private func GetPlayerPosition(int plr)
318 {
319 	var plrid = GetPlayerID(plr);
320 	var cleared = plr_list[plrid];
321 	var length = 0;
322 	// Add length of cleared checkpoints.
323 	for (var i = 0; i < cleared; i++)
324 		length += ObjectDistance(cp_list[i], cp_list[i + 1]);
325 	// Add length of current checkpoint.
326 	var add_length = 0;
327 	if (cleared < cp_count)
328 	{
329 		var path_length = ObjectDistance(cp_list[cleared], cp_list[cleared + 1]);
330 		add_length = Max(path_length - ObjectDistance(cp_list[cleared + 1], GetCursor(plr)), 0);
331 	}
332 	return length + add_length;
333 }
334 
335 // Returns the length the team has completed.
GetTeamPosition(int team)336 private func GetTeamPosition(int team)
337 {
338 	var cleared = team_list[team];
339 	var length = 0;
340 	// Add length of cleared checkpoints.
341 	for (var i = 0; i < cleared; i++)
342 		length += ObjectDistance(cp_list[i], cp_list[i + 1]);
343 	// Add length of current checkpoint.
344 	var add_length = 0;
345 	if (cleared < cp_count)
346 	{
347 		for (var i = 0; i < GetPlayerCount(); i++)
348 		{
349 			var plr = GetPlayerByIndex(i);
350 			if (GetPlayerTeam(plr) == team)
351 			{
352 				var path_length = ObjectDistance(cp_list[cleared], cp_list[cleared + 1]);
353 				var test_length = Max(path_length - ObjectDistance(cp_list[cleared + 1], GetCursor(plr)), 0);
354 				if (test_length > add_length)
355 					add_length = test_length;
356 			}
357 		}
358 	}
359 	return length + add_length;
360 }
361 
362 // Returns the length of this parkour.
GetParkourLength()363 private func GetParkourLength()
364 {
365 	var length = 0;
366 	for (var i = 0; i < cp_count; i++)
367 		length += ObjectDistance(cp_list[i], cp_list[i + 1]);
368 	return length;
369 }
370 
371 // Returns the number of checkpoints cleared by the player.
GetPlayerClearedCheckpoints(int plr)372 public func GetPlayerClearedCheckpoints(int plr)
373 {
374 	var plrid = GetPlayerID(plr);
375 	return plr_list[plrid];
376 }
377 
GetLeaderClearedCheckpoints()378 public func GetLeaderClearedCheckpoints()
379 {
380 	return Max(plr_list);
381 }
382 
IsWinner(int plr)383 private func IsWinner(int plr)
384 {
385 	var team = GetPlayerTeam(plr);
386 	var finish = cp_list[cp_count];
387 	if (!finish)
388 		return false;
389 	if (team)
390 	{
391 		if (finish->ClearedByTeam(team))
392 			return true;
393 	}
394 	else
395 	{
396 		if (finish->ClearedByPlayer(plr))
397 			return true;
398 	}
399 	return false;
400 }
401 
402 /*-- Player section --*/
403 
InitializePlayer(int plr,int x,int y,object base,int team)404 protected func InitializePlayer(int plr, int x, int y, object base, int team)
405 {
406 	// If the parkour is already finished, then immediately eliminate player.
407 	if (finished)
408 		return EliminatePlayer(plr);
409 	// Remove all hostilities.
410 	for (var i = 0; i < GetPlayerCount(); i++)
411 	{
412 		SetHostility(plr, GetPlayerByIndex(i), false, true);
413 		SetHostility(GetPlayerByIndex(i), plr, false, true);
414 	}
415 	// Init Respawn CP to start CP.
416 	var plrid = GetPlayerID(plr);
417 	respawn_list[plr] = cp_list[0];
418 	plr_list[plrid] = 0;
419 	if (team)
420 		if (!team_list[team])
421 			team_list[team] = 0;
422 	// Scoreboard.
423 	Scoreboard->NewPlayerEntry(plr);
424 	UpdateScoreboard(plr);
425 	DoScoreboardShow(1, plr + 1);
426 	JoinPlayer(plr);
427 	// Scenario script callback.
428 	GameCall("OnPlayerRespawn", plr, FindRespawnCP(plr));
429 	return;
430 }
431 
OnClonkDeath(object clonk,int killed_by)432 protected func OnClonkDeath(object clonk, int killed_by)
433 {
434 	var plr = clonk->GetOwner();
435 	// Only respawn if required and if the player still exists.
436 	if (no_respawn_handling || !GetPlayerName(plr) || GetCrewCount(plr))
437 		return;
438 	var new_clonk = CreateObjectAbove(Clonk, 0, 0, plr);
439 	new_clonk->MakeCrewMember(plr);
440 	SetCursor(plr, new_clonk);
441 	JoinPlayer(plr);
442 	// Transfer contents if active.
443 	if (transfer_contents)
444 		GetRelaunchRule()->TransferInventory(clonk, new_clonk);
445 	// Scenario script callback.
446 	GameCall("OnPlayerRespawn", plr, FindRespawnCP(plr));
447 	// Log message.
448 	Log(RndRespawnMsg(), GetPlayerName(plr));
449 	// Respawn actions
450 	var cp = FindRespawnCP(plr);
451 	UserAction->EvaluateAction(on_respawn, this, clonk, plr);
452 	if (cp)
453 		cp->OnPlayerRespawn(new_clonk, plr);
454 	return;
455 }
456 
RndRespawnMsg()457 private func RndRespawnMsg()
458 {
459 	return Translate(Format("MsgRespawn%d", Random(4)));
460 }
461 
JoinPlayer(int plr)462 protected func JoinPlayer(int plr)
463 {
464 	var clonk = GetCrew(plr);
465 	clonk->DoEnergy(clonk.MaxEnergy / 1000);
466 	var pos = FindRespawnPos(plr);
467 	clonk->SetPosition(pos[0], pos[1]);
468 	AddEffect("IntDirNextCP", clonk, 100, 1, this);
469 	return;
470 }
471 
472 // You always respawn at the last completed checkpoint you passed by.
473 // More complicated behavior should be set by the scenario.
FindRespawnCP(int plr)474 private func FindRespawnCP(int plr)
475 {
476 	var respawn_cp = respawn_list[plr];
477 	if (!respawn_cp)
478 		respawn_cp = respawn_list[plr] = cp_list[0];
479 	return respawn_cp;
480 }
481 
FindRespawnPos(int plr)482 private func FindRespawnPos(int plr)
483 {
484 	var cp = FindRespawnCP(plr);
485 	if (!cp) cp = this; // Have to start somewhere
486 	return [cp->GetX(), cp->GetY()];
487 }
488 
RemovePlayer(int plr)489 protected func RemovePlayer(int plr)
490 {
491 	respawn_list[plr] = nil;
492 	if (!finished)
493 		AddEvalData(plr);
494 	return;
495 }
496 
497 
498 /*-- Scenario saving --*/
499 
SaveScenarioObject(props)500 public func SaveScenarioObject(props)
501 {
502 	if (!inherited(props, ...))
503 		return false;
504 	props->AddCall("Goal", this, "EnsureRestartRule");
505 	if (no_respawn_handling)
506 		props->AddCall("Goal", this, "DisableRespawnHandling");
507 	if (transfer_contents)
508 		props->AddCall("Goal", this, "TransferContentsOnRelaunch", true);
509 	return true;
510 }
511 
512 
513 /*-- Scoreboard --*/
514 
515 static const SBRD_Checkpoints = 0;
516 static const SBRD_BestTime = 1;
517 
UpdateScoreboardTitle()518 private func UpdateScoreboardTitle()
519 {
520 	if (cp_count > 0)
521 		var caption = Format("$MsgCaptionX$", cp_count);
522 	else
523 		var caption = "$MsgCaptionNone$";
524 	return Scoreboard->SetTitle(caption);
525 }
526 
InitScoreboard()527 private func InitScoreboard()
528 {
529 	Scoreboard->Init(
530 		[
531 		{key = "checkpoints", title = "#", sorted = true, desc = true, default = 0, priority = 80},
532 		{key = "besttime", title = GUI_Clock, sorted = true, desc = true, default = 0, priority = 70}
533 		]
534 		);
535 	UpdateScoreboardTitle();
536 	return;
537 }
538 
UpdateScoreboard(int plr)539 private func UpdateScoreboard(int plr)
540 {
541 	if (finished)
542 		return;
543 	var plrid = GetPlayerID(plr);
544 	Scoreboard->SetPlayerData(plr, "checkpoints", plr_list[plrid]);
545 	var bt = GetPlrExtraData(plr, time_store);
546 	Scoreboard->SetPlayerData(plr, "besttime", TimeToString(bt), bt);
547 	return;
548 }
549 
550 
551 /*-- Direction indication --*/
552 
553 // Effect for direction indication for the clonk.
FxIntDirNextCPStart(object target,effect fx)554 protected func FxIntDirNextCPStart(object target, effect fx)
555 {
556 	var arrow = CreateObjectAbove(GUI_GoalArrow, 0, 0, target->GetOwner());
557 	arrow->SetAction("Show", target);
558 	fx.arrow = arrow;
559 	return FX_OK;
560 }
561 
FxIntDirNextCPTimer(object target,effect fx)562 protected func FxIntDirNextCPTimer(object target, effect fx)
563 {
564 	var plr = target->GetOwner();
565 	var team = GetPlayerTeam(plr);
566 	var arrow = fx.arrow;
567 	// Find nearest CP.
568 	var nextcp;
569 	for (var cp in FindObjects(Find_ID(ParkourCheckpoint), Find_Func("FindCPMode", PARKOUR_CP_Check | PARKOUR_CP_Finish), Sort_Distance(target->GetX() - GetX(), target->GetY() - GetY())))
570 		if (!cp->ClearedByPlayer(plr) && (cp->IsActiveForPlayer(plr) || cp->IsActiveForTeam(team)))
571 		{
572 			nextcp = cp;
573 			break;
574 		}
575 	if (!nextcp)
576 		return arrow->SetClrModulation(RGBa(0, 0, 0, 0));
577 	// Calculate parameters.
578 	var angle = Angle(target->GetX(), target->GetY(), nextcp->GetX(), nextcp->GetY());
579 	var dist = Min(510 * ObjectDistance(GetCrew(plr), nextcp) / 400, 510);
580 	var red = BoundBy(dist, 0, 255);
581 	var green = BoundBy(510 - dist, 0, 255);
582 	var blue = 0;
583 	// Arrow is colored a little different for the finish.
584 	if (cp->GetCPMode() & PARKOUR_CP_Finish)
585 		blue = 128;
586 	var color = RGBa(red, green, blue, 128);
587 	// Draw arrow.
588 	arrow->SetR(angle);
589 	arrow->SetClrModulation(color);
590 	// Check if clonk is contained in a vehicle, if so attach arrow to vehicle.
591 	var container = target->Contained();
592 	if (container && container->GetCategory() & C4D_Vehicle)
593 	{
594 		if (arrow->GetActionTarget() != container)
595 		{
596 			arrow->SetActionTargets(container);
597 			arrow->SetCategory(C4D_Vehicle);
598 		}
599 	}
600 	else
601 	{
602 		if (arrow->GetActionTarget() != target)
603 		{
604 			arrow->SetActionTargets(target);
605 			arrow->SetCategory(C4D_StaticBack);
606 		}
607 	}
608 	return FX_OK;
609 }
610 
FxIntDirNextCPStop(object target,effect fx)611 protected func FxIntDirNextCPStop(object target, effect fx)
612 {
613 	fx.arrow->RemoveObject();
614 	return;
615 }
616 
617 
618 /*-- Time tracker --*/
619 
620 // Store the best time in the player file, same for teammembers.
DoBestTime(int plr)621 private func DoBestTime(int plr)
622 {
623 	var effect = GetEffect("IntBestTime", this);
624 	var time = effect.besttime;
625 	var winteam = GetPlayerTeam(plr);
626 	for (var i = 0; i < GetPlayerCount(); i++)
627 	{
628 		var check_plr = GetPlayerByIndex(i);
629 		if (winteam == 0 && check_plr != plr)
630 			continue;
631 		if (winteam != GetPlayerTeam(check_plr))
632 			continue;
633 		// Store best time for all players in the winning team.
634 		var rectime = GetPlrExtraData(check_plr, time_store);
635 		if (time != 0 && (!rectime || time < rectime))
636 		{
637 			SetPlrExtraData(check_plr, time_store, time);
638 			Log(Format("$MsgBestTime$", GetPlayerName(check_plr), TimeToString(time)));
639 		}
640 	}
641 	return;
642 }
643 
644 // Starts at goal initialization, should be equivalent to gamestart.
FxIntBestTimeTimer(object target,effect,time)645 protected func FxIntBestTimeTimer(object target, effect, time)
646 {
647 	effect.besttime = time;
648 	return FX_OK;
649 }
650 
651 // Returns a best time string.
TimeToString(int time)652 private func TimeToString(int time)
653 {
654 	if (!time) // No time.
655 		return "N/A";
656 	if (time > 36 * 60 * 60) // Longer than an hour.
657 		return Format("%d:%.2d:%.2d.%.1d", (time / 60 / 60 / 36) % 24, (time / 60 / 36) % 60, (time / 36) % 60, (10 * time / 36) % 10);
658 	if (time > 36 * 60) // Longer than a minute.
659 		return Format("%d:%.2d.%.1d", (time / 60 / 36) % 60, (time / 36) % 60, (10 * time / 36) % 10);
660 	else // Only seconds.
661 		return Format("%d.%.1d", (time / 36) % 60, (10 * time / 36) % 10);
662 }
663 
664 // Resets the personal best (call from message board).
ResetPersonalBest(int plr)665 public func ResetPersonalBest(int plr)
666 {
667 	if (!GetPlayerName(plr))
668 		return;
669 	// Forward call to actual goal.
670 	if (this == Goal_Parkour)
671 	{
672 		var goal = FindObject(Find_ID(Goal_Parkour));
673 		if (goal)
674 			goal->ResetPersonalBest(plr);
675 	}
676 	SetPlrExtraData(plr, time_store, nil);
677 	// Also update the scoreboard.
678 	UpdateScoreboard(plr);
679 	return;
680 }
681 
682 
683 /*-- Evaluation data --*/
684 
SetEvalData(int winner)685 private func SetEvalData(int winner)
686 {
687 	var winteam = GetPlayerTeam(winner);
688 	var effect = GetEffect("IntBestTime", this);
689 	var time = effect.besttime;
690 	var msg;
691 	// General data.
692 	if (winteam)
693 		msg = Format("$MsgEvalTeamWon$", GetTeamName(winteam), TimeToString(time));
694 	else
695 		msg = Format("$MsgEvalPlrWon$", GetPlayerName(winner), TimeToString(time));
696 	AddEvaluationData(msg, 0);
697 	// Individual data.
698 	for (var i = 0; i < GetPlayerCount(); i++)
699 		AddEvalData(GetPlayerByIndex(i));
700 	// Obviously get rid of settlement score.
701 	HideSettlementScoreInEvaluation(true);
702 	return;
703 }
704 
AddEvalData(int plr)705 private func AddEvalData(int plr)
706 {
707 	if (finished)
708 		return;
709 	var plrid = GetPlayerID(plr);
710 	var cps = plr_list[plrid];
711 	var msg;
712 	if (cps == cp_count)
713 		msg = "$MsgEvalPlayerAll$";
714 	else
715 		msg = Format("$MsgEvalPlayerX$", cps, cp_count);
716 	AddEvaluationData(msg, plrid);
717 	return;
718 }
719 
720 
721 /* Editor */
722 
723 local on_checkpoint_cleared, on_checkpoint_first_cleared, on_respawn;
724 
SetOnCheckpointCleared(v)725 public func SetOnCheckpointCleared(v) { on_checkpoint_cleared=v; return true; }
SetOnCheckpointFirstCleared(v)726 public func SetOnCheckpointFirstCleared(v) { on_checkpoint_first_cleared=v; return true; }
SetOnRespawn(v)727 public func SetOnRespawn(v) { on_respawn=v; return true; }
728 
Definition(def)729 public func Definition(def)
730 {
731 	_inherited(def);
732 	if (!def.EditorProps) def.EditorProps = {};
733 	def.EditorProps.on_checkpoint_cleared = new UserAction.Prop { Name="$OnCleared$", EditorHelp="$OnClearedHelp$", Set="SetOnCheckpointCleared", Save="Checkpoint" };
734 	def.EditorProps.on_checkpoint_first_cleared = new UserAction.Prop { Name="$OnFirstCleared$", EditorHelp="$OnFirstClearedHelp$", Set="SetOnCheckpointFirstCleared", Save="Checkpoint" };
735 	def.EditorProps.on_respawn = new UserAction.Prop { Name="$OnRespawn$", EditorHelp="$OnRespawnHelp$", Set="SetOnRespawn", Save = "Checkpoint" };
736 	if (!def.EditorActions) def.EditorActions = {};
737 	def.EditorActions.reset_all_cleared = { Name="$ResetAllCleared$", EditorHelp="$ResetAllClearedHelp$", Command="ResetAllClearedCP()" };
738 }
739 
740 
741 /*-- Proplist --*/
742 
743 local Name = "$Name$";
744