diff --git a/planet/Arena.ocf/HotIce.ocs/Scenario.txt b/planet/Arena.ocf/HotIce.ocs/Scenario.txt
index 38694dabd..a17143dc4 100644
--- a/planet/Arena.ocf/HotIce.ocs/Scenario.txt
+++ b/planet/Arena.ocf/HotIce.ocs/Scenario.txt
@@ -7,7 +7,7 @@ Icon=21
[Game]
Mode=Melee
-Goals=Goal_Melee=1;
+Goals=Goal_MultiRoundMelee=1;
Rules=Rule_KillLogs=1;Rule_Gravestones=1;
[Landscape]
diff --git a/planet/Arena.ocf/HotIce.ocs/Script.c b/planet/Arena.ocf/HotIce.ocs/Script.c
index 529d6b0fe..790e174dc 100644
--- a/planet/Arena.ocf/HotIce.ocs/Script.c
+++ b/planet/Arena.ocf/HotIce.ocs/Script.c
@@ -1,66 +1,7 @@
/* Hot ice */
-static g_remaining_rounds, g_winners, g_check_victory_effect;
-static g_gameover;
-
-func Initialize()
+func InitializeRound() // called by Goal_MultiRoundMelee
{
- g_remaining_rounds = SCENPAR_Rounds;
- g_winners = [];
- InitializeRound();
-
- Scoreboard->Init([
- // Invisible team column for sorting players under their teams.
- {key = "team", title = "", sorted = true, desc = false, default = "", priority = 90},
- {key = "wins", title = "Wins", sorted = true, desc = true, default = 0, priority = 100},
- {key = "death", title = "", sorted = false, default = "", priority = 0},
- ]);
-
-}
-
-// Resets the scenario, redrawing the map.
-func ResetRound()
-{
- // Retrieve all Clonks.
- var clonks = [];
- for (var clonk in FindObjects(Find_OCF(OCF_CrewMember)))
- {
- var container = clonk->Contained();
- if (container)
- {
- clonk->Exit();
- container->RemoveObject();
- }
- else
- {
- // Players not waiting for a relaunch get a new Clonk to prevent
- // status effects from carrying over to the next round.
- var new_clonk = CreateObject(clonk->GetID(), 0, 0, clonk->GetOwner());
- new_clonk->GrabObjectInfo(clonk);
- clonk = new_clonk;
- }
- PushBack(clonks, clonk);
- clonk->SetObjectStatus(C4OS_INACTIVE);
- }
- // Clear and redraw the map.
- LoadScenarioSection("main");
- InitializeRound();
- AssignHandicaps();
- // Re-enable the players.
- for (var clonk in clonks)
- {
- clonk->SetObjectStatus(C4OS_NORMAL);
- SetCursor(clonk->GetOwner(), clonk);
- // Select the first item. This fixes item ordering.
- clonk->SetHandItemPos(0, 0);
- InitPlayerRound(clonk->GetOwner());
- }
-}
-
-func InitializeRound()
-{
- // Checking for victory: Only active after a Clonk dies.
- g_check_victory_effect = AddEffect("CheckVictory", nil, 1, 0);
g_player_spawn_index = 0;
if (GetType(g_player_spawn_positions) == C4V_Array)
ShuffleArray(g_player_spawn_positions);
@@ -105,55 +46,20 @@ func InitializeRound()
if (IsFirestoneSpot(pos.x,pos.y))
CreateObjectAbove([Firestone,IronBomb][Random(Random(3))],pos.x,pos.y-1);
- // The game starts after a delay to ensure that everyone is ready.
- GUI_Clock->CreateCountdown(3);
-
SetSky(g_theme.Sky);
g_theme->InitializeRound();
g_theme->InitializeMusic();
-
- return true;
}
static g_player_spawn_positions, g_map_width, g_player_spawn_index;
-global func ScoreboardTeam(int team) { return team * 100; }
-
-func InitializePlayer(int plr)
+func InitPlayerRound(int plr, object crew) // called by Goal_MultiRoundMelee
{
- // Add the player and their team to the scoreboard.
- Scoreboard->NewPlayerEntry(plr);
- Scoreboard->SetPlayerData(plr, "wins", "");
- var team = GetPlayerTeam(plr);
- Scoreboard->NewEntry(ScoreboardTeam(team), GetTeamName(team));
- Scoreboard->SetData(ScoreboardTeam(team), "team", "", ScoreboardTeam(team));
- Scoreboard->SetPlayerData(plr, "team", "", ScoreboardTeam(team) + 1);
-
- // Players joining at runtime will participate in the following round.
- // Should only happen if it's not game start, else Clonks would start stuck in a RelaunchContainer.
- if (FrameCounter() > 1) PutInRelaunchContainer(GetCrew(plr));
-}
-
-func InitializePlayers()
-{
- AssignHandicaps();
- for (var i = 0; i < GetPlayerCount(); i++)
- {
- var plr = GetPlayerByIndex(i);
- InitPlayerRound(plr);
- }
-}
-
-func InitPlayerRound(int plr)
-{
- // Unmark death on scoreboard.
- Scoreboard->SetPlayerData(plr, "death", "");
// everything visible
SetFoW(false, plr);
- SetPlayerViewLock(plr, true);
// Player positioning.
var ls_wdt = LandscapeWidth(), ls_hgt = LandscapeHeight();
- var crew = GetCrew(plr), start_pos;
+ var start_pos;
// Position by map type?
if (SCENPAR_SpawnType == 0)
{
@@ -202,7 +108,7 @@ func InitPlayerRound(int plr)
var ammo = launcher->CreateContents(IronBomb);
launcher->AddTimer(Scenario.ReplenishLauncherAmmo, 10);
// Start reloading the launcher during the countdown.
- if (!IsHandicapped(plr))
+ if (!Goal_MultiRoundMelee->IsHandicapped(plr))
{
crew->SetHandItemPos(0, crew->GetItemPos(launcher));
// This doesn't play the animation properly - simulate a click instead.
@@ -214,14 +120,16 @@ func InitPlayerRound(int plr)
}
crew.MaxEnergy = 100000;
crew->DoEnergy(1000);
- // Disable the Clonk during the countdown.
- crew->SetCrewEnabled(false);
- crew->SetComDir(COMD_Stop);
if (SCENPAR_SpawnType == 1 && balloon)
balloon->CreateEffect(IntNoGravity, 1, 1);
+}
- return true;
+func StartRound() // called by Goal_MultiRoundMelee
+{
+ for (var clonk in FindObjects(Find_OCF(OCF_CrewMember)))
+ if (SCENPAR_SpawnType == 1 && clonk->GetActionTarget())
+ RemoveEffect("IntNoGravity", clonk->GetActionTarget());
}
local IntNoGravity = new Effect {
@@ -230,219 +138,6 @@ local IntNoGravity = new Effect {
}
};
-// Called by the round start countdown.
-func OnCountdownFinished()
-{
- // Re-enable all Clonks.
- for (var clonk in FindObjects(Find_OCF(OCF_CrewMember)))
- {
- clonk->SetCrewEnabled(true);
- SetCursor(clonk->GetOwner(), clonk);
- if (SCENPAR_SpawnType == 1 && clonk->GetActionTarget())
- RemoveEffect("IntNoGravity", clonk->GetActionTarget());
- }
-}
-
-func PutInRelaunchContainer(object clonk)
-{
- var plr = clonk->GetOwner();
- var relaunch = CreateObject(RelaunchContainer, LandscapeWidth() / 2, LandscapeHeight() / 2, plr);
- // We just use the relaunch object as a dumb container.
- clonk->Enter(relaunch);
- // Allow scrolling around the landscape.
- SetPlayerViewLock(plr, false);
-}
-
-func OnClonkDeath(object clonk)
-{
- var plr = clonk->GetOwner();
- // Mark death on scoreboard.
- Scoreboard->SetPlayerData(plr, "death", "{{Scoreboard_Death}}");
- // Skip eliminated players, NO_OWNER, etc.
- if (GetPlayerName(plr))
- {
- var crew = CreateObject(Clonk, 0, 0, plr);
- crew->MakeCrewMember(plr);
- PutInRelaunchContainer(crew);
- }
-
- // Check for victory after three seconds to allow stalemates.
- if (!g_gameover)
- g_check_victory_effect.Interval = g_check_victory_effect.Time + 36 * 3;
-}
-
-// Returns an array of team -> number of players in team.
-func GetTeamPlayers()
-{
- var result = CreateArray(GetTeamCount() + 1);
- for (var i = 0; i < GetPlayerCount(); i++)
- {
- var plr = GetPlayerByIndex(i), team = GetPlayerTeam(plr);
- SetLength(result, Max(team + 1, GetLength(result)));
- result[team] = result[team] ?? [];
- PushBack(result[team], plr);
- }
- return result;
-}
-
-static g_handicapped_players;
-
-func _MinSize(int a, array b) { if (b == nil) return a; else return Min(a, GetLength(b)); }
-
-// Assigns handicaps so that the number of not-handicapped players is the same for all teams.
-func AssignHandicaps()
-{
- g_handicapped_players = CreateArray(GetPlayerCount());
- var teams = GetTeamPlayers();
- var smallest_size = Reduce(teams, Scenario._MinSize, ~(1<<31));
- for (var team in teams) if (team != nil)
- {
- var to_handicap = GetLength(team) - smallest_size;
- while (GetLength(team) > to_handicap)
- RemoveArrayIndexUnstable(team, Random(GetLength(team)));
- for (var plr in team)
- {
- SetLength(g_handicapped_players, Max(plr + 1, GetLength(g_handicapped_players)));
- g_handicapped_players[plr] = true;
- }
- }
-}
-
-func IsHandicapped(int plr)
-{
- return !!g_handicapped_players[plr];
-}
-
-// Returns a list of colored player names, for example "Sven2, Maikel, Luchs"
-global func GetTeamPlayerNames(int team)
-{
- var str = "";
- for (var i = 0; i < GetPlayerCount(); i++)
- {
- var plr = GetPlayerByIndex(i);
- if (GetPlayerTeam(plr) == team)
- {
- var comma = "";
- if (str != "") comma = ", ";
- str = Format("%s%s%s", str, comma, GetPlayerColor(plr), GetPlayerName(plr));
- }
- }
- return str;
-}
-
-global func FxCheckVictoryTimer(_, proplist effect)
-{
- var find_living = Find_And(Find_OCF(OCF_CrewMember), Find_NoContainer());
- var clonk = FindObject(find_living);
- var msg;
- if (!clonk)
- {
- // Stalemate!
- msg = "$Stalemate$";
- Log(msg);
- GameCall("ResetRound");
- }
- else if (!FindObject(find_living, Find_Hostile(clonk->GetOwner())))
- {
- // We have a winner!
- var team = GetPlayerTeam(clonk->GetOwner());
- PushBack(g_winners, team);
- // Announce the winning team.
- msg = Format("$WinningTeam$", GetTeamPlayerNames(team));
- Log(msg);
-
- // Update the scoreboard.
- UpdateScoreboardWins(team);
-
- // The leading team has to win the last round.
- if (--g_remaining_rounds > 0 || GetLeadingTeam() != team)
- {
- var msg2 = CurrentRoundStr();
- Log(msg2);
- msg = Format("%s|%s", msg, msg2);
- GameCall("ResetRound");
- }
- else
- {
- GameCall("EliminateLosers");
- }
- }
- // Switching scenario sections makes the Log() messages hard to see, so announce them using a message as well.
- CustomMessage(msg);
- // Go to sleep again.
- effect.Interval = 0;
- return FX_OK;
-}
-
-global func CurrentRoundStr()
-{
- if (g_remaining_rounds == 1)
- return "$LastRound$";
- else if (g_remaining_rounds > 1)
- return Format("$RemainingRounds$", g_remaining_rounds);
- else if (GetLeadingTeam() == nil)
- return "$Tiebreak$";
- else
- return "$BonusRound$";
-}
-
-global func UpdateScoreboardWins(int team)
-{
- var wins = GetTeamWins(team);
- Scoreboard->SetData(ScoreboardTeam(team), "wins", wins, wins);
- // We have to update each player as well to make the sorting work.
- for (var i = 0; i < GetPlayerCount(); i++)
- {
- var plr = GetPlayerByIndex(i);
- if (GetPlayerTeam(plr) == team)
- {
- Scoreboard->SetPlayerData(plr, "wins", "", wins);
- }
- }
-}
-
-global func GetTeamWins(int team)
-{
- var wins = 0;
- for (var w in g_winners)
- if (w == team)
- wins++;
- return wins;
-}
-
-// Returns the team which won the most rounds, or nil if there is a tie.
-global func GetLeadingTeam()
-{
- var teams = [], winning_team = g_winners[0];
- for (var w in g_winners)
- {
- teams[w] += 1;
- if (teams[w] > teams[winning_team])
- winning_team = w;
- }
- // Detect a tie.
- for (var i = 0; i < GetLength(teams); i++)
- {
- if (i != winning_team && teams[i] == teams[winning_team])
- return nil;
- }
- return winning_team;
-}
-
-func EliminateLosers()
-{
- g_gameover = true;
- // Determine the winning team.
- var winning_team = GetLeadingTeam();
- // Eliminate everybody who isn't on the winning team.
- for (var i = 0; i < GetPlayerCount(); i++)
- {
- var plr = GetPlayerByIndex(i);
- if (GetPlayerTeam(plr) != winning_team)
- EliminatePlayer(plr);
- }
- // The scenario goal will end the scenario.
-}
/* Called periodically in grenade launcher */
func ReplenishLauncherAmmo()
diff --git a/planet/Arena.ocf/HotIce.ocs/StringTblDE.txt b/planet/Arena.ocf/HotIce.ocs/StringTblDE.txt
index eb04b2160..1375dcbf8 100644
--- a/planet/Arena.ocf/HotIce.ocs/StringTblDE.txt
+++ b/planet/Arena.ocf/HotIce.ocs/StringTblDE.txt
@@ -32,9 +32,3 @@ DescBalloonSpawn=Die Clonks fallen mit Ballons vom Himmel.
Rounds=Rundenzahl
DescRounds=Mehrere Runden spielen
-Stalemate=Unentschieden!
-WinningTeam=Gewinner: %s
-RemainingRounds=Noch %d Runden.
-LastRound=Letzte Runde!
-Tiebreak=Entscheidende Runde!
-BonusRound=Bonusrunde!
diff --git a/planet/Arena.ocf/HotIce.ocs/StringTblUS.txt b/planet/Arena.ocf/HotIce.ocs/StringTblUS.txt
index 7f371cbed..1a76f0d34 100644
--- a/planet/Arena.ocf/HotIce.ocs/StringTblUS.txt
+++ b/planet/Arena.ocf/HotIce.ocs/StringTblUS.txt
@@ -32,9 +32,3 @@ DescBalloonSpawn=The clonks will drop with balloons from the sky.
Rounds=Number of rounds
DescRounds=Play for multiple rounds.
-Stalemate=Stalemate!
-WinningTeam=Winning team: %s
-RemainingRounds=%d rounds remaining.
-LastRound=Last round!
-Tiebreak=Tiebreak!
-BonusRound=Bonus round!
diff --git a/planet/Arena.ocf/HotIce.ocs/System.ocg/Melee.c b/planet/Arena.ocf/HotIce.ocs/System.ocg/Melee.c
deleted file mode 100644
index eb601ec1d..000000000
--- a/planet/Arena.ocf/HotIce.ocs/System.ocg/Melee.c
+++ /dev/null
@@ -1,17 +0,0 @@
-#appendto Goal_Melee
-
-public func GetDescription(int plr)
-{
- // Count active enemy clonks.
- var hostile_count = ObjectCount(Find_OCF(OCF_CrewMember), Find_NoContainer(), Find_Hostile(plr));
- var message;
- if (!hostile_count)
- message = "$MsgGoalFulfilled$";
- else
- message = Format("$MsgGoalUnfulfilled$", hostile_count);
-
- // Also report the remaining rounds.
- message = Format("%s|%s", message, CurrentRoundStr());
-
- return message;
-}
diff --git a/planet/Arena.ocf/HotIce.ocs/System.ocg/StringTblDE.txt b/planet/Arena.ocf/HotIce.ocs/System.ocg/StringTblDE.txt
deleted file mode 100644
index e4ab3a67d..000000000
--- a/planet/Arena.ocf/HotIce.ocs/System.ocg/StringTblDE.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-MsgGoalFulfilled=Eure Gegner sind eliminiert.
-MsgGoalUnfulfilled=Es sind noch %d Gegner im Spiel.
diff --git a/planet/Arena.ocf/HotIce.ocs/System.ocg/StringTblUS.txt b/planet/Arena.ocf/HotIce.ocs/System.ocg/StringTblUS.txt
deleted file mode 100644
index 280afb70c..000000000
--- a/planet/Arena.ocf/HotIce.ocs/System.ocg/StringTblUS.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-MsgGoalFulfilled=All opponents eliminated.
-MsgGoalUnfulfilled=There are still %d opponents in the game.
diff --git a/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/DefCore.txt b/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/DefCore.txt
new file mode 100644
index 000000000..4bbc04f58
--- /dev/null
+++ b/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/DefCore.txt
@@ -0,0 +1,7 @@
+[DefCore]
+id=Goal_MultiRoundMelee
+Version=8,0
+Category=C4D_StaticBack|C4D_Goal
+Width=64
+Height=64
+Offset=-32,-32
diff --git a/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/Graphics.2.png b/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/Graphics.2.png
new file mode 100644
index 000000000..8efd90a8b
Binary files /dev/null and b/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/Graphics.2.png differ
diff --git a/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/Script.c b/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/Script.c
new file mode 100644
index 000000000..0c9083d11
--- /dev/null
+++ b/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/Script.c
@@ -0,0 +1,419 @@
+/*-- Multi-Round Melee --*/
+/* Originally part of HotIce, but now also used in other scenarios.
+
+ Usage:
+ In the scenario script, implement the following functions:
+
+ InitializeRound():
+ InitializeRound should create scenario objects for each round.
+
+ InitPlayerRound(int plr, object crew):
+ InitPlayerRound is called every round for each player and should equip
+ and position their Clonks. Note that the players won't be able to
+ control their Clonks until the round start countdown finishes.
+ Check Goal_MultiRoundMelee->IsHandicapped(plr) to improve balance
+ with unequal team sizes (see documentation below).
+
+ StartRound():
+ Called after the round start countdown finishes and the players can
+ control their Clonks.
+
+ @author Luchs
+*/
+
+#include Goal_Melee
+
+/* Public Interface */
+
+// SetRounds changes the number of remaining rounds. The number of rounds
+// defaults to the `Rounds` scenario parameter if available.
+public func SetRounds(int rounds)
+{
+ if (this == Goal_MultiRoundMelee) return FindObject(Find_ID(Goal_MultiRoundMelee))->SetRounds(rounds);
+ remaining_rounds = rounds;
+}
+
+// IsHandicapped indicates whether the given player should receive a handicap.
+// When playing with inbalanced teams, the goal randomly selects players to be
+// handicapped so that the number of non-handicapped players is tha same for
+// all teams.
+public func IsHandicapped(int plr)
+{
+ if (this == Goal_MultiRoundMelee) return FindObject(Find_ID(Goal_MultiRoundMelee))->IsHandicapped(plr);
+ return !!handicapped_players[plr];
+}
+
+/* Implementation */
+
+local remaining_rounds, winners, check_victory_effect, gameover;
+local handicapped_players;
+
+protected func Initialize()
+{
+ // Don't allow creating the goal at runtime. This is important as the
+ // engine will recreate goals during section changes, but we need to retain
+ // all data.
+ if (FrameCounter() > 1) return RemoveObject();
+
+ remaining_rounds = SCENPAR.Rounds ?? 1;
+ winners = [];
+ InitializeRound();
+
+ Scoreboard->Init([
+ // Invisible team column for sorting players under their teams.
+ {key = "team", title = "", sorted = true, desc = false, default = "", priority = 90},
+ {key = "wins", title = "Wins", sorted = true, desc = true, default = 0, priority = 100},
+ {key = "death", title = "", sorted = false, default = "", priority = 0},
+ ]);
+
+ return inherited(...);
+}
+
+protected func InitializePlayer(int plr, int x, int y, object base, int team)
+{
+ // Add the player and their team to the scoreboard.
+ Scoreboard->NewPlayerEntry(plr);
+ Scoreboard->SetPlayerData(plr, "wins", "");
+ Scoreboard->NewEntry(ScoreboardTeam(team), GetTeamName(team));
+ Scoreboard->SetData(ScoreboardTeam(team), "team", "", ScoreboardTeam(team));
+ Scoreboard->SetPlayerData(plr, "team", "", ScoreboardTeam(team) + 1);
+
+ // Players joining at runtime will participate in the following round.
+ // Should only happen if it's not game start, else Clonks would start stuck in a RelaunchContainer.
+ if (FrameCounter() > 1) PutInRelaunchContainer(GetCrew(plr));
+
+ return inherited(plr, x, y, base, team, ...);
+}
+
+protected func InitializePlayers()
+{
+ AssignHandicaps();
+ for (var i = 0; i < GetPlayerCount(); i++)
+ {
+ var plr = GetPlayerByIndex(i);
+ InitPlayerRound(plr);
+ }
+}
+
+// InitPlayerRound initializes the round for the given player.
+private func InitPlayerRound(int plr)
+{
+ // Unmark death on scoreboard.
+ Scoreboard->SetPlayerData(plr, "death", "");
+ // Players can scroll freely while waiting for the next round. Disable this now.
+ SetPlayerViewLock(plr, true);
+ // Disable the Clonk during the countdown.
+ var crew = GetCrew(plr);
+ crew->SetCrewEnabled(false);
+ crew->SetComDir(COMD_Stop);
+
+ // Let the scenario do its thing.
+ Scenario->~InitPlayerRound(plr, crew);
+}
+
+// ResetRound resets the scenario, redrawing the map.
+private func ResetRound()
+{
+ // Retrieve all Clonks.
+ var clonks = [];
+ for (var clonk in FindObjects(Find_OCF(OCF_CrewMember)))
+ {
+ var container = clonk->Contained();
+ if (container)
+ {
+ clonk->Exit();
+ container->RemoveObject();
+ }
+ else
+ {
+ // Players not waiting for a relaunch get a new Clonk to prevent
+ // status effects from carrying over to the next round.
+ var new_clonk = CreateObject(clonk->GetID(), 0, 0, clonk->GetOwner());
+ new_clonk->GrabObjectInfo(clonk);
+ clonk = new_clonk;
+ }
+ PushBack(clonks, clonk);
+ clonk->SetObjectStatus(C4OS_INACTIVE);
+ }
+
+ // Clear and redraw the map while retaining the goal.
+ SetObjectStatus(C4OS_INACTIVE);
+ LoadScenarioSection("main");
+ SetObjectStatus(C4OS_NORMAL);
+
+ InitializeRound();
+ AssignHandicaps();
+ // Re-enable the players.
+ for (var clonk in clonks)
+ {
+ clonk->SetObjectStatus(C4OS_NORMAL);
+ SetCursor(clonk->GetOwner(), clonk);
+ // Select the first item. This fixes item ordering.
+ clonk->SetHandItemPos(0, 0);
+ InitPlayerRound(clonk->GetOwner());
+ }
+
+ // Fix goal icon in the HUD.
+ NotifyHUD();
+}
+
+local CheckVictory = new Effect
+{
+ goal = nil,
+
+ Construction = func(object g)
+ {
+ goal = g;
+ },
+
+ Timer = func()
+ {
+ var find_living = Find_And(Find_OCF(OCF_CrewMember), Find_NoContainer());
+ var clonk = FindObject(find_living);
+ var msg;
+ if (!clonk)
+ {
+ // Stalemate!
+ msg = "$Stalemate$";
+ Log(msg);
+ goal->ResetRound();
+ }
+ else if (!FindObject(find_living, Find_Hostile(clonk->GetOwner())))
+ {
+ // We have a winner!
+ var team = GetPlayerTeam(clonk->GetOwner());
+ PushBack(goal.winners, team);
+ // Announce the winning team.
+ msg = Format("$WinningTeam$", GetTeamPlayerNames(team));
+ Log(msg);
+
+ // Update the scoreboard.
+ goal->UpdateScoreboardWins(team);
+
+ // The leading team has to win the last round.
+ if (--goal.remaining_rounds > 0 || goal->GetLeadingTeam() != team)
+ {
+ var msg2 = goal->CurrentRoundStr();
+ Log(msg2);
+ msg = Format("%s|%s", msg, msg2);
+ goal->ResetRound();
+ }
+ else
+ {
+ goal->EliminateLosers();
+ }
+ }
+ // Switching scenario sections makes the Log() messages hard to see, so announce them using a message as well.
+ CustomMessage(msg);
+ // Go to sleep again.
+ this.Interval = 0;
+ return FX_OK;
+ },
+
+ // GetTeamPlayerNames returns a list of colored player names, for example
+ // "Sven2, Maikel, Luchs"
+ GetTeamPlayerNames = func(int team)
+ {
+ var str = "";
+ for (var i = 0; i < GetPlayerCount(); i++)
+ {
+ var plr = GetPlayerByIndex(i);
+ if (GetPlayerTeam(plr) == team)
+ {
+ var comma = "";
+ if (str != "") comma = ", ";
+ str = Format("%s%s%s", str, comma, GetPlayerColor(plr), GetPlayerName(plr));
+ }
+ }
+ return str;
+ },
+};
+
+private func CurrentRoundStr()
+{
+ if (remaining_rounds == 1)
+ return "$LastRound$";
+ else if (remaining_rounds > 1)
+ return Format("$RemainingRounds$", remaining_rounds);
+ else if (GetLeadingTeam() == nil)
+ return "$Tiebreak$";
+ else
+ return "$BonusRound$";
+}
+
+private func InitializeRound()
+{
+ // Checking for victory: Only active after a Clonk dies.
+ if (!check_victory_effect)
+ check_victory_effect = CreateEffect(CheckVictory, 1, 0, this);
+
+ // Now let the scenario do its thing.
+ Scenario->~InitializeRound();
+
+ // The game starts after a delay to ensure that everyone is ready.
+ GUI_Clock->CreateCountdown(3);
+
+ return true;
+}
+
+protected func OnCountdownFinished() // called by the round start countdown
+{
+ // Re-enable all Clonks.
+ for (var clonk in FindObjects(Find_OCF(OCF_CrewMember)))
+ {
+ clonk->SetCrewEnabled(true);
+ SetCursor(clonk->GetOwner(), clonk);
+ }
+ Scenario->~StartRound();
+}
+
+private func PutInRelaunchContainer(object clonk)
+{
+ var plr = clonk->GetOwner();
+ var relaunch = CreateObject(RelaunchContainer, LandscapeWidth() / 2, LandscapeHeight() / 2, plr);
+ // We just use the relaunch object as a dumb container.
+ clonk->Enter(relaunch);
+ // Allow scrolling around the landscape.
+ SetPlayerViewLock(plr, false);
+}
+
+protected func OnClonkDeath(object clonk)
+{
+ var plr = clonk->GetOwner();
+ // Mark death on scoreboard.
+ Scoreboard->SetPlayerData(plr, "death", "{{Scoreboard_Death}}");
+ // Skip eliminated players, NO_OWNER, etc.
+ if (GetPlayerName(plr))
+ {
+ var crew = CreateObject(Clonk, 0, 0, plr);
+ crew->MakeCrewMember(plr);
+ PutInRelaunchContainer(crew);
+ }
+
+ // Check for victory after three seconds to allow stalemates.
+ if (!gameover)
+ check_victory_effect.Interval = check_victory_effect.Time + 36 * 3;
+}
+
+// GetTeamPlayers returns an array of team -> number of players in team.
+private func GetTeamPlayers()
+{
+ var result = CreateArray(GetTeamCount() + 1);
+ for (var i = 0; i < GetPlayerCount(); i++)
+ {
+ var plr = GetPlayerByIndex(i), team = GetPlayerTeam(plr);
+ SetLength(result, Max(team + 1, GetLength(result)));
+ result[team] = result[team] ?? [];
+ PushBack(result[team], plr);
+ }
+ return result;
+}
+
+func _MinSize(int a, array b) { if (b == nil) return a; else return Min(a, GetLength(b)); }
+
+// Assigns handicaps so that the number of not-handicapped players is the same for all teams.
+func AssignHandicaps()
+{
+ handicapped_players = CreateArray(GetPlayerCount());
+ var teams = GetTeamPlayers();
+ var smallest_size = Reduce(teams, this._MinSize, ~(1<<31));
+ for (var team in teams) if (team != nil)
+ {
+ var to_handicap = GetLength(team) - smallest_size;
+ while (GetLength(team) > to_handicap)
+ RemoveArrayIndexUnstable(team, Random(GetLength(team)));
+ for (var plr in team)
+ {
+ SetLength(handicapped_players, Max(plr + 1, GetLength(handicapped_players)));
+ handicapped_players[plr] = true;
+ }
+ }
+}
+
+// GetLeadingTeam returns the team which won the most rounds, or nil if there is a tie.
+private func GetLeadingTeam()
+{
+ var teams = [], winning_team = winners[0];
+ for (var w in winners)
+ {
+ teams[w] += 1;
+ if (teams[w] > teams[winning_team])
+ winning_team = w;
+ }
+ // Detect a tie.
+ for (var i = 0; i < GetLength(teams); i++)
+ {
+ if (i != winning_team && teams[i] == teams[winning_team])
+ return nil;
+ }
+ return winning_team;
+}
+
+private func EliminateLosers()
+{
+ gameover = true;
+ // Determine the winning team.
+ var winning_team = GetLeadingTeam();
+ // Eliminate everybody who isn't on the winning team.
+ for (var i = 0; i < GetPlayerCount(); i++)
+ {
+ var plr = GetPlayerByIndex(i);
+ if (GetPlayerTeam(plr) != winning_team)
+ EliminatePlayer(plr);
+ }
+ // The included melee goal will end the scenario.
+}
+
+/* Scoreboard */
+
+private func ScoreboardTeam(int team) { return team * 100; }
+
+// GetTeamWins returns how many rounds that team has won.
+private func GetTeamWins(int team)
+{
+ var wins = 0;
+ for (var w in winners)
+ if (w == team)
+ wins++;
+ return wins;
+}
+
+private func UpdateScoreboardWins(int team)
+{
+ var wins = GetTeamWins(team);
+ Scoreboard->SetData(ScoreboardTeam(team), "wins", wins, wins);
+ // We have to update each player as well to make the sorting work.
+ for (var i = 0; i < GetPlayerCount(); i++)
+ {
+ var plr = GetPlayerByIndex(i);
+ if (GetPlayerTeam(plr) == team)
+ {
+ Scoreboard->SetPlayerData(plr, "wins", "", wins);
+ }
+ }
+}
+
+/* Goal interface */
+
+public func GetDescription(int plr)
+{
+ // Count active enemy clonks.
+ var hostile_count = ObjectCount(Find_OCF(OCF_CrewMember), Find_NoContainer(), Find_Hostile(plr));
+ var message;
+ if (!hostile_count)
+ message = "$MsgGoalFulfilled$";
+ else
+ message = Format("$MsgGoalUnfulfilled$", hostile_count);
+
+ // Also report the remaining rounds.
+ message = Format("%s|%s", message, CurrentRoundStr());
+
+ return message;
+}
+
+public func GetShortDescription(int plr)
+{
+ return CurrentRoundStr();
+}
+
+local Name = "$Name$";
diff --git a/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/StringTblDE.txt b/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/StringTblDE.txt
new file mode 100644
index 000000000..02be35811
--- /dev/null
+++ b/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/StringTblDE.txt
@@ -0,0 +1,11 @@
+Name=Mehrrundiges Melee
+
+MsgGoalFulfilled=Eure Gegner sind eliminiert.
+MsgGoalUnfulfilled=Es sind noch %d Gegner in der aktuellen Runde.
+
+Stalemate=Unentschieden!
+WinningTeam=Gewinner: %s
+RemainingRounds=Noch %d Runden.
+LastRound=Letzte Runde!
+Tiebreak=Entscheidende Runde!
+BonusRound=Bonusrunde!
diff --git a/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/StringTblUS.txt b/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/StringTblUS.txt
new file mode 100644
index 000000000..056c8a088
--- /dev/null
+++ b/planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/StringTblUS.txt
@@ -0,0 +1,11 @@
+Name=Multi-Round Melee
+
+MsgGoalFulfilled=All opponents eliminated.
+MsgGoalUnfulfilled=There are still %d opponents in the current round.
+
+Stalemate=Stalemate!
+WinningTeam=Winning team: %s
+RemainingRounds=%d rounds remaining.
+LastRound=Last round!
+Tiebreak=Tiebreak!
+BonusRound=Bonus round!