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!