From 1382478774a36f450cf5e4feda028427f4577167 Mon Sep 17 00:00:00 2001 From: Lukas Werling Date: Sat, 31 Mar 2018 15:59:19 +0200 Subject: [PATCH] Extract Goal_MultiRoundMelee from HotIce There are currently multiple multi-round scenarios in development that all copy most of HotIce's >500-line scenario script for the multi-round logic. This commit isolates that logic in a goal with an easy-to-use interface. --- planet/Arena.ocf/HotIce.ocs/Scenario.txt | 2 +- planet/Arena.ocf/HotIce.ocs/Script.c | 325 +------------- planet/Arena.ocf/HotIce.ocs/StringTblDE.txt | 6 - planet/Arena.ocf/HotIce.ocs/StringTblUS.txt | 6 - .../Arena.ocf/HotIce.ocs/System.ocg/Melee.c | 17 - .../HotIce.ocs/System.ocg/StringTblDE.txt | 2 - .../HotIce.ocs/System.ocg/StringTblUS.txt | 2 - .../Goals.ocd/MultiRoundMelee.ocd/DefCore.txt | 7 + .../MultiRoundMelee.ocd/Graphics.2.png | Bin 0 -> 18692 bytes .../Goals.ocd/MultiRoundMelee.ocd/Script.c | 419 ++++++++++++++++++ .../MultiRoundMelee.ocd/StringTblDE.txt | 11 + .../MultiRoundMelee.ocd/StringTblUS.txt | 11 + 12 files changed, 459 insertions(+), 349 deletions(-) delete mode 100644 planet/Arena.ocf/HotIce.ocs/System.ocg/Melee.c delete mode 100644 planet/Arena.ocf/HotIce.ocs/System.ocg/StringTblDE.txt delete mode 100644 planet/Arena.ocf/HotIce.ocs/System.ocg/StringTblUS.txt create mode 100644 planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/DefCore.txt create mode 100644 planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/Graphics.2.png create mode 100644 planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/Script.c create mode 100644 planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/StringTblDE.txt create mode 100644 planet/Objects.ocd/Goals.ocd/MultiRoundMelee.ocd/StringTblUS.txt 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 0000000000000000000000000000000000000000..8efd90a8ba3f9a0b1290d104d156735ba98825cb GIT binary patch literal 18692 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 literal 0 HcmV?d00001 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!