forked from Mirrors/openclonk
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.master
parent
07e66279be
commit
1382478774
|
@ -7,7 +7,7 @@ Icon=21
|
||||||
|
|
||||||
[Game]
|
[Game]
|
||||||
Mode=Melee
|
Mode=Melee
|
||||||
Goals=Goal_Melee=1;
|
Goals=Goal_MultiRoundMelee=1;
|
||||||
Rules=Rule_KillLogs=1;Rule_Gravestones=1;
|
Rules=Rule_KillLogs=1;Rule_Gravestones=1;
|
||||||
|
|
||||||
[Landscape]
|
[Landscape]
|
||||||
|
|
|
@ -1,66 +1,7 @@
|
||||||
/* Hot ice */
|
/* Hot ice */
|
||||||
|
|
||||||
static g_remaining_rounds, g_winners, g_check_victory_effect;
|
func InitializeRound() // called by Goal_MultiRoundMelee
|
||||||
static g_gameover;
|
|
||||||
|
|
||||||
func Initialize()
|
|
||||||
{
|
{
|
||||||
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;
|
g_player_spawn_index = 0;
|
||||||
if (GetType(g_player_spawn_positions) == C4V_Array)
|
if (GetType(g_player_spawn_positions) == C4V_Array)
|
||||||
ShuffleArray(g_player_spawn_positions);
|
ShuffleArray(g_player_spawn_positions);
|
||||||
|
@ -105,55 +46,20 @@ func InitializeRound()
|
||||||
if (IsFirestoneSpot(pos.x,pos.y))
|
if (IsFirestoneSpot(pos.x,pos.y))
|
||||||
CreateObjectAbove([Firestone,IronBomb][Random(Random(3))],pos.x,pos.y-1);
|
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);
|
SetSky(g_theme.Sky);
|
||||||
g_theme->InitializeRound();
|
g_theme->InitializeRound();
|
||||||
g_theme->InitializeMusic();
|
g_theme->InitializeMusic();
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static g_player_spawn_positions, g_map_width, g_player_spawn_index;
|
static g_player_spawn_positions, g_map_width, g_player_spawn_index;
|
||||||
|
|
||||||
global func ScoreboardTeam(int team) { return team * 100; }
|
func InitPlayerRound(int plr, object crew) // called by Goal_MultiRoundMelee
|
||||||
|
|
||||||
func InitializePlayer(int plr)
|
|
||||||
{
|
{
|
||||||
// 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
|
// everything visible
|
||||||
SetFoW(false, plr);
|
SetFoW(false, plr);
|
||||||
SetPlayerViewLock(plr, true);
|
|
||||||
// Player positioning.
|
// Player positioning.
|
||||||
var ls_wdt = LandscapeWidth(), ls_hgt = LandscapeHeight();
|
var ls_wdt = LandscapeWidth(), ls_hgt = LandscapeHeight();
|
||||||
var crew = GetCrew(plr), start_pos;
|
var start_pos;
|
||||||
// Position by map type?
|
// Position by map type?
|
||||||
if (SCENPAR_SpawnType == 0)
|
if (SCENPAR_SpawnType == 0)
|
||||||
{
|
{
|
||||||
|
@ -202,7 +108,7 @@ func InitPlayerRound(int plr)
|
||||||
var ammo = launcher->CreateContents(IronBomb);
|
var ammo = launcher->CreateContents(IronBomb);
|
||||||
launcher->AddTimer(Scenario.ReplenishLauncherAmmo, 10);
|
launcher->AddTimer(Scenario.ReplenishLauncherAmmo, 10);
|
||||||
// Start reloading the launcher during the countdown.
|
// Start reloading the launcher during the countdown.
|
||||||
if (!IsHandicapped(plr))
|
if (!Goal_MultiRoundMelee->IsHandicapped(plr))
|
||||||
{
|
{
|
||||||
crew->SetHandItemPos(0, crew->GetItemPos(launcher));
|
crew->SetHandItemPos(0, crew->GetItemPos(launcher));
|
||||||
// This doesn't play the animation properly - simulate a click instead.
|
// This doesn't play the animation properly - simulate a click instead.
|
||||||
|
@ -214,14 +120,16 @@ func InitPlayerRound(int plr)
|
||||||
}
|
}
|
||||||
crew.MaxEnergy = 100000;
|
crew.MaxEnergy = 100000;
|
||||||
crew->DoEnergy(1000);
|
crew->DoEnergy(1000);
|
||||||
// Disable the Clonk during the countdown.
|
|
||||||
crew->SetCrewEnabled(false);
|
|
||||||
crew->SetComDir(COMD_Stop);
|
|
||||||
|
|
||||||
if (SCENPAR_SpawnType == 1 && balloon)
|
if (SCENPAR_SpawnType == 1 && balloon)
|
||||||
balloon->CreateEffect(IntNoGravity, 1, 1);
|
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 {
|
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<c %x>%s</c>", 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 */
|
/* Called periodically in grenade launcher */
|
||||||
func ReplenishLauncherAmmo()
|
func ReplenishLauncherAmmo()
|
||||||
|
|
|
@ -32,9 +32,3 @@ DescBalloonSpawn=Die Clonks fallen mit Ballons vom Himmel.
|
||||||
|
|
||||||
Rounds=Rundenzahl
|
Rounds=Rundenzahl
|
||||||
DescRounds=Mehrere Runden spielen
|
DescRounds=Mehrere Runden spielen
|
||||||
Stalemate=Unentschieden!
|
|
||||||
WinningTeam=Gewinner: %s
|
|
||||||
RemainingRounds=Noch %d Runden.
|
|
||||||
LastRound=Letzte Runde!
|
|
||||||
Tiebreak=Entscheidende Runde!
|
|
||||||
BonusRound=Bonusrunde!
|
|
||||||
|
|
|
@ -32,9 +32,3 @@ DescBalloonSpawn=The clonks will drop with balloons from the sky.
|
||||||
|
|
||||||
Rounds=Number of rounds
|
Rounds=Number of rounds
|
||||||
DescRounds=Play for multiple rounds.
|
DescRounds=Play for multiple rounds.
|
||||||
Stalemate=Stalemate!
|
|
||||||
WinningTeam=Winning team: %s
|
|
||||||
RemainingRounds=%d rounds remaining.
|
|
||||||
LastRound=Last round!
|
|
||||||
Tiebreak=Tiebreak!
|
|
||||||
BonusRound=Bonus round!
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
MsgGoalFulfilled=Eure Gegner sind eliminiert.
|
|
||||||
MsgGoalUnfulfilled=Es sind noch %d Gegner im Spiel.
|
|
|
@ -1,2 +0,0 @@
|
||||||
MsgGoalFulfilled=All opponents eliminated.
|
|
||||||
MsgGoalUnfulfilled=There are still %d opponents in the game.
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
[DefCore]
|
||||||
|
id=Goal_MultiRoundMelee
|
||||||
|
Version=8,0
|
||||||
|
Category=C4D_StaticBack|C4D_Goal
|
||||||
|
Width=64
|
||||||
|
Height=64
|
||||||
|
Offset=-32,-32
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
|
@ -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<c %x>%s</c>", 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$";
|
|
@ -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!
|
|
@ -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!
|
Loading…
Reference in New Issue